新增排队叫号功能

This commit is contained in:
gyq 2024-12-12 09:33:01 +08:00
parent 0711d4b07d
commit 4b54fdaff1
12 changed files with 17112 additions and 93 deletions

File diff suppressed because one or more lines are too long

View File

@ -1,79 +1,80 @@
{
"name": "vite-electron",
"private": true,
"version": "1.4.25",
"main": "dist-electron/main.js",
"scripts": {
"dev": "chcp 65001 && vite",
"build": "node ./addVersion.js && vite build && electron-builder",
"build:test": "vite build --mode test && electron-builder",
"preview": "vite preview",
"build:win": "node ./addVersion.js && vite build && electron-builder --w"
},
"dependencies": {
"@element-plus/icons-vue": "^2.3.1",
"axios": "^1.6.2",
"dayjs": "^1.11.10",
"electron-pos-printer": "^1.3.6",
"electron-pos-printer-vue": "^1.0.9",
"element-plus": "^2.4.3",
"js-md5": "^0.8.3",
"lodash": "^4.17.21",
"pinia": "^2.1.7",
"pinia-plugin-persistedstate": "^3.2.1",
"qrcode": "^1.5.3",
"reconnecting-websocket": "^4.4.0",
"serialport": "^12.0.0",
"swiper": "^11.1.1",
"uuid": "^10.0.0",
"vue": "^3.3.8",
"vue-router": "^4.2.5"
},
"devDependencies": {
"@vitejs/plugin-vue": "^4.5.0",
"electron": "^28.2.3",
"electron-builder": "^24.13.3",
"electron-rebuild": "^3.2.9",
"path": "^0.12.7",
"sass": "^1.69.5",
"sass-loader": "^13.3.2",
"tree-kill": "^1.2.2",
"vite": "^5.0.0",
"vite-plugin-electron": "^0.15.4",
"vite-plugin-electron-renderer": "^0.14.5"
},
"build": {
"appId": "com.cashierdesktop.app",
"productName": "银收客",
"asar": true,
"files": [
"./dist/**/*",
"./dist-electron/**/*"
],
"directories": {
"buildResources": "build",
"output": "release"
},
"win": {
"icon": "./public/logo.ico",
"target": [
{
"target": "nsis",
"arch": [
"ia32"
]
}
]
},
"nsis": {
"oneClick": false,
"allowElevation": true,
"allowToChangeInstallationDirectory": true,
"installerIcon": "./public/logo.ico",
"uninstallerIcon": "./public/logo.ico",
"installerHeaderIcon": "./public/logo.ico",
"createDesktopShortcut": true,
"createStartMenuShortcut": true
}
}
}
"name": "vite-electron",
"private": true,
"version": "1.4.25",
"main": "dist-electron/main.js",
"scripts": {
"dev": "chcp 65001 && vite",
"build": "node ./addVersion.js && vite build && electron-builder",
"build:test": "vite build --mode test && electron-builder",
"preview": "vite preview",
"build:win": "node ./addVersion.js && vite build && electron-builder --w"
},
"dependencies": {
"@element-plus/icons-vue": "^2.3.1",
"axios": "^1.6.2",
"dayjs": "^1.11.10",
"electron-pos-printer": "^1.3.6",
"electron-pos-printer-vue": "^1.0.9",
"element-plus": "^2.4.3",
"js-md5": "^0.8.3",
"lodash": "^4.17.21",
"pinia": "^2.1.7",
"pinia-plugin-persistedstate": "^3.2.1",
"qrcode": "^1.5.3",
"reconnecting-websocket": "^4.4.0",
"serialport": "^12.0.0",
"speak-tts": "^2.0.8",
"swiper": "^11.1.1",
"uuid": "^10.0.0",
"vue": "^3.3.8",
"vue-router": "^4.2.5"
},
"devDependencies": {
"@vitejs/plugin-vue": "^4.5.0",
"electron": "^28.2.3",
"electron-builder": "^24.13.3",
"electron-rebuild": "^3.2.9",
"path": "^0.12.7",
"sass": "^1.69.5",
"sass-loader": "^13.3.2",
"tree-kill": "^1.2.2",
"vite": "^5.0.0",
"vite-plugin-electron": "^0.15.4",
"vite-plugin-electron-renderer": "^0.14.5"
},
"build": {
"appId": "com.cashierdesktop.app",
"productName": "银收客",
"asar": true,
"files": [
"./dist/**/*",
"./dist-electron/**/*"
],
"directories": {
"buildResources": "build",
"output": "release"
},
"win": {
"icon": "./public/logo.ico",
"target": [
{
"target": "nsis",
"arch": [
"ia32"
]
}
]
},
"nsis": {
"oneClick": false,
"allowElevation": true,
"allowToChangeInstallationDirectory": true,
"installerIcon": "./public/logo.ico",
"uninstallerIcon": "./public/logo.ico",
"installerHeaderIcon": "./public/logo.ico",
"createDesktopShortcut": true,
"createStartMenuShortcut": true
}
}
}

134
src/api/queue.js Normal file
View File

@ -0,0 +1,134 @@
/**
* 排队叫号
*/
import request from "@/utils/request.js";
/**
* 记录获取
* @param {*} params
* @returns
*/
export function callRecord(params) {
return request({
method: "get",
url: "/callTable/callRecord",
params,
});
}
/**
* 桌型列表
* @param {*} params
* @returns
*/
export function callTable(params) {
return request({
method: "get",
url: "/callTable",
params,
});
}
/**
* 添加桌型
* @param {*} data
* @returns
*/
export function addCallTable(data) {
return request({
method: data.id ? "put" : "post",
url: "/callTable",
data,
});
}
/**
* 删除桌型
* @param {*} data
* @returns
*/
export function delCallTable(data) {
return request({
method: "delete",
url: "/callTable",
data,
});
}
/**
* 配置信息 获取
* @param {*} params
* @returns
*/
export function callTableConfig(params) {
return request({
method: "get",
url: "/callTable/config",
params,
});
}
/**
* 配置信息 修改
* @param {*} params
* @returns
*/
export function callTableConfigPut(data) {
return request({
method: "put",
url: "/callTable/config",
data,
});
}
/**
* 取号 排队列表获取
* @param {*} params
* @returns
*/
export function callTableQueue(params) {
return request({
method: "GET",
url: "/callTable/queue",
params,
});
}
/**
* 取号 手动取号
* @param {*} params
* @returns
*/
export function takeNumber(data) {
return request({
method: "post",
url: "/callTable/takeNumber",
data,
});
}
/**
* 取号 修改叫号状态
* @param {*} params
* @returns
*/
export function updateState(data) {
return request({
method: "put",
url: "/callTable/updateState",
data,
});
}
/**
* 通知叫号
* @param {*} params
* @returns
*/
export function callTableCall(data) {
return request({
method: "post",
url: "/callTable/call",
data,
});
}

View File

@ -87,6 +87,11 @@ const menus = ref([
path: '/member',
icon: 'User'
},
{
label: '排队',
path: '/queue',
icon: 'Timer'
},
// {
// label: '',
// path: '/work',
@ -233,7 +238,7 @@ defineExpose({
}
&.more {
margin-top: 120px;
margin-top: 90px;
}
.icon {

View File

@ -0,0 +1,114 @@
<template>
<el-upload ref="uploadRef" v-model:file-list="fileList" :action="uploadUrl" :headers="headers" :list-type="listType"
:multiple="multiple" :limit="limit" :on-exceed="handleExceed" :on-change="handleChange"
:on-progress="handleProgress" :on-success="handleSuccess" :on-error="handleError" :before-upload="beforeUpload"
:accept="accept" :disabled="disabled">
<el-icon>
<Plus />
</el-icon>
<template v-slot:tip>
<div v-if="tip">{{ tip }}</div>
</template>
</el-upload>
</template>
<script setup>
import { ref } from 'vue';
import useStorage from '@/utils/useStorage'
import { ElUpload, ElMessage } from 'element-plus';
const fileList = ref([])
//
const props = defineProps({
uploadUrl: {
type: String,
default: import.meta.env.MODE == 'development' ? '/api/shopInfo/upload' : import.meta.env.VITE_API_URL + '/shopInfo/upload',
},
headers: {
type: Object,
default: () => ({
token: useStorage.get("token"),
loginName: useStorage.get("userInfo").loginName,
clientType: 'pc'
}),
},
listType: {
type: String,
default: 'picture-card',
},
multiple: {
type: Boolean,
default: false,
},
limit: {
type: Number,
default: 1,
},
tip: {
type: String,
default: '',
},
disabled: {
type: Boolean,
default: false,
},
accept: {
type: String,
default: 'image/jpeg,image/png,image/gif',
},
});
const emits = defineEmits(['success'])
//
const uploadRef = ref(null);
//
const handleExceed = (files, fileList) => {
ElMessage.warning(`超出最大允许上传图片数量:${props.limit}`);
};
//
const handleChange = (file, fileList) => {
console.log('图片文件状态改变', file, fileList);
};
//
const handleProgress = (event, file, fileList) => {
console.log('图片上传进度', event.percent);
};
//
const handleSuccess = (response, file, fileList) => {
ElMessage.success('图片上传成功');
console.log('图片上传成功响应', response);
emits('success', response.data)
};
//
const handleError = (error, file, fileList) => {
ElMessage.error('图片上传失败');
console.error('图片上传失败原因', error);
};
//
const beforeUpload = (file) => {
const isImage = props.accept.split(',').some(format => file.type === format.trim());
if (!isImage) {
ElMessage.error('请选择正确格式的图片文件');
return false;
}
return true;
};
function init(arr) {
fileList.value = arr
}
defineExpose({
init
})
</script>
<style scoped></style>

View File

@ -57,6 +57,14 @@ const routes = [
},
component: () => import("@/views/member/index.vue"),
},
{
path: "/queue",
name: "queue",
meta: {
index: 1,
},
component: () => import("@/views/queue/index.vue"),
},
{
path: "/work",
name: "work",

View File

@ -0,0 +1,139 @@
<template>
<el-dialog v-model="showAddTable" :title="addTabForm.id ? '编辑桌型' : '新增桌型'" top="10vh" @closed="addTabFormReset">
<el-form ref="AddTabFormRef" :model="addTabForm" :rules="addTabFormRules" label-position="left"
label-width="100">
<el-form-item label="名称" prop="name">
<el-input v-model="addTabForm.name" placeholder="请输入名称" />
</el-form-item>
<el-form-item label="描述" prop="note">
<el-input v-model="addTabForm.note" placeholder="请输入描述例如1-2人" />
</el-form-item>
<el-form-item label="等待时间" prop="waitTime">
<el-input v-model="addTabForm.waitTime" placeholder="0">
<template #append>分钟/1</template>
</el-input>
</el-form-item>
<el-form-item label="号码前缀" prop="prefix">
<el-input v-model="addTabForm.prefix" placeholder="请输入英文字母,不支持中文" />
</el-form-item>
<el-form-item label="开始号码" prop="start">
<el-input v-model="addTabForm.start" placeholder="请输入开始号码" />
</el-form-item>
<el-form-item label="过号保留">
<el-input v-model="addTabForm.nearNum" :disabled="!addTabForm.isPostpone" placeholder="临近几桌提醒">
<template #prepend>
<el-checkbox v-model="addTabForm.isPostpone" :true-value="1" :false-value="0" label="开启顺延" />
</template>
<template #append></template>
</el-input>
</el-form-item>
</el-form>
<div class="footer" style="display: flex;">
<el-button style="width: 100%" @click="showAddTable = false">
取消
</el-button>
<el-button type="primary" style="width: 100%" :loading="addTabFormLoading" @click="addTabConfirmHandle">
确认
</el-button>
</div>
</el-dialog>
</template>
<script setup>
import { ElMessage } from 'element-plus'
import { ref, onMounted } from 'vue'
import { addCallTable } from '@/api/queue.js'
import { useUser } from "@/store/user.js"
const store = useUser()
const emits = defineEmits(['success'])
const showAddTable = ref(false)
const addTabFormLoading = ref(false)
const AddTabFormRef = ref(null)
const resetAddTabForm = ref({})
const addTabForm = ref({
name: '',
note: '',
waitTime: '',
prefix: '',
start: '',
isPostpone: 0,
nearNum: ''
})
const addTabFormRules = ref({
name: [
{
required: true,
message: ' ',
trigger: 'blur',
}
],
waitTime: [
{
required: true,
message: ' ',
trigger: 'blur',
}
],
prefix: [
{
required: true,
message: ' ',
trigger: 'blur',
}
],
start: [
{
required: true,
message: ' ',
trigger: 'blur',
}
],
})
//
function addTabFormReset() {
addTabForm.value = { ...resetAddTabForm.value }
AddTabFormRef.value.resetFields()
}
//
function addTabConfirmHandle() {
AddTabFormRef.value.validate(async valid => {
try {
if (valid) {
addTabFormLoading.value = true
addTabForm.value.shopId = store.userInfo.shopId
if (addTabForm.value.id) {
addTabForm.value.callTableId = addTabForm.value.id
}
const res = await addCallTable(addTabForm.value)
addTabFormLoading.value = false
showAddTable.value = false
ElMessage.success(addTabForm.value.id ? '编辑成功' : '添加成功')
emits('success')
}
} catch (error) {
addTabFormLoading.value = false
console.log(error);
}
})
}
function show(obj) {
if (obj && obj.id) {
addTabForm.value = { ...obj }
}
showAddTable.value = true
}
defineExpose({
show
})
onMounted(() => {
resetAddTabForm.value = { ...addTabForm.value }
})
</script>

View File

@ -0,0 +1,108 @@
<!-- 取号 -->
<template>
<el-dialog title="取号" v-model="visible" @closed="onClose">
<el-form ref="formRef" :model="form" :rules="rules" label-position="top">
<el-form-item label="选择桌型">
<el-radio-group v-model="form.callTableId">
<el-radio :value="item.id" border v-for="item in list" :key="item.id">{{ item.name
}}</el-radio>
</el-radio-group>
</el-form-item>
<el-form-item label="手机号码" prop="phone">
<el-input v-model="form.phone" placeholder="请填写手机号" style="width: 60%;" />
</el-form-item>
</el-form>
<div class="footer" style="display: flex;padding-top: 30px;">
<el-button style="width: 100%" @click="visible = false">
取消
</el-button>
<el-button type="primary" style="width: 100%" :loading="loading" @click="confirmHandle">
确认
</el-button>
</div>
</el-dialog>
</template>
<script setup>
import { ElMessage } from 'element-plus'
import { onMounted, ref } from 'vue';
import { takeNumber } from '@/api/queue.js'
import { useUser } from "@/store/user.js"
const store = useUser()
const emits = defineEmits(['success'])
const visible = ref(false)
const list = ref([])
const loading = ref(false)
const formRef = ref(null)
const resetForm = ref({})
const form = ref({
callTableId: '',
shopId: '',
phone: '',
note: '',
name: ''
})
const rules = ref({
phone: [
{
required: true,
validator: (rule, value, callback) => {
let reg = /^(?:(?:\+|00)86)?1\d{10}$/
if (!reg.test(form.value.phone)) {
callback(new Error('手机号码不正确'))
} else {
callback()
}
},
trigger: 'blur',
}
]
})
//
function confirmHandle() {
formRef.value.validate(async vaild => {
try {
if (vaild) {
loading.value = true
form.value.shopId = store.userInfo.shopId
form.value.note = list.value.find(item => item.id == form.value.callTableId).note
form.value.name = list.value.find(item => item.id == form.value.callTableId).name
await takeNumber(form.value)
loading.value = false
ElMessage.success('取号成功')
emits('success')
visible.value = false
}
} catch (error) {
loading.value = false
console.log(error);
}
})
}
//
function onClose() {
form.value = { ...resetForm.value }
formRef.value.resetFields()
}
function show(arr) {
visible.value = true
list.value = [...arr]
form.value.callTableId = list.value[0].id
}
defineExpose({
show
})
onMounted(() => {
resetForm.value = { ...form }
})
</script>

View File

@ -0,0 +1,99 @@
<!-- 叫号记录 -->
<template>
<el-dialog v-model="dialogVisible" title="叫号记录" @closed="reset" width="80vw">
<el-table :data="tableData.list" border style="height: 84%;" height="50vh">
<el-table-column label="桌号" prop="name"></el-table-column>
<el-table-column label="桌型" prop="note"></el-table-column>
<el-table-column label="手机号" prop="phone"></el-table-column>
<el-table-column label="状态" prop="state">
<template v-slot="scope">
{{ statusList[scope.row.state].text }}
</template>
</el-table-column>
<el-table-column label="时间" prop="callTime" width="200">
<template v-slot="scope">{{ dayjs(scope.row.callTime).format('YYYY-MM-DD HH:mm:ss') }}</template>
</el-table-column>
</el-table>
<div class="pagination" style="padding-top:15px;display: flex;justify-content: flex-end;">
<el-pagination v-model:current-page="tableData.page" v-model:page-size="tableData.size"
layout="total, prev, pager, next" :total="tableData.total" background @current-change="paginationChange"
@size-change="paginationChange">
</el-pagination>
</div>
</el-dialog>
</template>
<script setup>
import { dayjs } from 'element-plus'
import { onMounted, reactive, ref } from 'vue';
import { callRecord } from '@/api/queue.js'
import { useUser } from "@/store/user.js"
const store = useUser()
const dialogVisible = ref(false)
const tableData = reactive({
loading: false,
list: [],
page: 1,
size: 20,
total: 0
})
const statusList = {
'-1': {
type: 'warning',
text: '已取消'
},
0: {
type: 'danger',
text: '排队中'
},
1: {
type: 'success',
text: '叫号中'
},
2: {
type: 'success',
text: '已入座'
},
3: {
type: 'success',
text: '已过号'
}
}
//
function paginationChange(e) {
callRecordAjax()
}
//
async function callRecordAjax() {
try {
tableData.loading = true
const res = await callRecord({
callTableId: '',
shopId: store.userInfo.shopId,
page: tableData.page,
size: tableData.size
})
tableData.loading = false
tableData.list = res.records
tableData.total = res.total
} catch (error) {
console.log(error);
}
}
function reset() {
tableData.page = 1
}
function show() {
dialogVisible.value = true
callRecordAjax()
}
defineExpose({
show
})
</script>

View File

@ -0,0 +1,75 @@
<!-- 播报结果 -->
<template>
<el-dialog title="提示" v-model="visible">
<div class="content">
<div style="font-size: 18px;">正在叫号请稍等</div>
<el-alert :title="statusList[item.status].text" :type="statusList[item.status].type" :closable="false" />
</div>
<div class="footer" style="display: flex;">
<el-button style="width: 100%" @click="confirmHandle(2)">完成</el-button>
<el-button type="primary" style="width: 100%" :loading="loading" @click="confirmHandle(3)">过号</el-button>
</div>
</el-dialog>
</template>
<script setup>
import { onMounted, reactive, ref } from 'vue';
import { updateState } from '@/api/queue.js'
import { useUser } from "@/store/user.js"
const store = useUser()
const emits = defineEmits(['success'])
const visible = ref(false)
const item = ref({})
const loading = ref(false)
const statusList = {
'-1': {
type: 'warning',
text: '用户未订阅'
},
0: {
type: 'danger',
text: '失败'
},
1: {
type: 'success',
text: '成功'
}
}
//
async function confirmHandle(state) {
try {
await updateState({
shopId: store.userInfo.shopId,
callQueueId: item.value.id,
state: state
})
visible.value = false
emits('success')
} catch (error) {
console.log(error);
}
}
function show(obj) {
visible.value = true
item.value = { ...obj }
}
defineExpose({
show
})
</script>
<style scoped lang="scss">
.content {
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
gap: 15px;
padding-bottom: 20px;
}
</style>

View File

@ -0,0 +1,233 @@
<template>
<el-dialog v-model="dialogVisible" title="基本设置" width="90vw" top="5vh" @closed="emits('success')">
<div class="scroll_y">
<el-form ref="formRef" :model="form" label-position="left" label-width="140">
<el-form-item label="排队页面地址">{{ config.pageAddress }}</el-form-item>
<el-form-item label="线上取号">
<el-switch v-model="form.isOnline" :active-value="1" :inactive-value="0"></el-switch>
</el-form-item>
<el-form-item label="背景图片">
<UploadImg ref="UploadImgRef" @success="e => form.bgCover = e" />
</el-form-item>
<el-form-item label="桌型">
<el-table :data="tableData.list" border v-loading="tableData.loading">
<el-table-column label="名称" prop="name"></el-table-column>
<el-table-column label="描述" prop="note"></el-table-column>
<el-table-column label="等待时间[桌]" prop="waitTime"></el-table-column>
<el-table-column label="号码前缀" prop="prefix"></el-table-column>
<el-table-column label="开始号码" prop="start"></el-table-column>
<el-table-column label="操作">
<template #header>
<el-button type="primary" @click="AddTabRef.show()">添加</el-button>
</template>
<template v-slot="scope">
<div style="display: flex;gap: 10px;">
<el-text type="primary" @click="AddTabRef.show(scope.row)">编辑</el-text>
<el-text type="danger" @click="delCallTableHandle(scope.row)">删除</el-text>
</div>
</template>
</el-table-column>
</el-table>
</el-form-item>
<div class="title" style="padding-bottom: 20px;">通知模板</div>
<el-form-item label="排队成功提醒">
<el-input disabled style="width: 50%;" v-model="config.successMsg"></el-input>
</el-form-item>
<el-form-item label="排队即将排到通知">
<div style="display: flex;flex-direction: column;">
<el-input disabled style="width: 50%;" v-model="config.nearMsg"></el-input>
<el-input v-model="config.nearNum" disabled style="margin-top: 10px;">
<template #prepend>前面等待</template>
<template #append>桌时提醒</template>
</el-input>
</div>
</el-form-item>
<el-form-item label="排队到号提醒">
<el-input disabled style="width: 50%;" v-model="config.callingMsg"></el-input>
</el-form-item>
</el-form>
</div>
<div class="footer">
<el-button style="width: 100%" @click="dialogVisible = false">
取消
</el-button>
<el-button type="primary" style="width: 100%" :loading="loading" @click="confirmHandle">
确认
</el-button>
</div>
</el-dialog>
<AddTab ref="AddTabRef" @success="getTableAjax" />
</template>
<script setup>
import UploadImg from '@/components/uploadImg.vue'
import { ElMessageBox, ElMessage } from 'element-plus'
import AddTab from './addTab.vue'
import { onMounted, reactive, ref } from 'vue';
import { callTable, delCallTable, callTableConfig, callTableConfigPut } from '@/api/queue.js'
import { useUser } from "@/store/user.js"
const store = useUser()
const dialogVisible = ref(false)
const AddTabRef = ref(null)
const formRef = ref(null)
const loading = ref(false)
const form = ref({
isOnline: 1,
bgCover: '',
nearNum: '',
})
function beforeAvatarUpload(file) {
console.log(2, file);
}
//
const tableData = reactive({
loading: false,
list: []
})
//
function show() {
dialogVisible.value = true
getTableAjax()
callTableConfigAjax()
}
//
function delCallTableHandle(row) {
ElMessageBox.confirm('确认要删除吗?', '注意').then(async () => {
try {
tableData.loading = true
const res = await delCallTable({
shopId: store.userInfo.shopId,
callTableId: row.id
})
getTableAjax()
} catch (error) {
console.log(error);
}
}).catch(() => { })
}
//
async function getTableAjax() {
try {
tableData.loading = true
const res = await callTable({
page: 1,
size: 100,
shopId: store.userInfo.shopId,
callTableId: '',
state: ''
})
tableData.loading = false
tableData.list = res.records
} catch (error) {
tableData.loading = false
console.log(error);
}
}
//
const config = ref({})
const UploadImgRef = ref(null)
async function callTableConfigAjax() {
try {
const res = await callTableConfig({ shopId: store.userInfo.shopId })
config.value = res
form.value.nearNum = res.nearNum
if (res.bgCover) {
UploadImgRef.value.init([{ url: res.bgCover }])
}
} catch (error) {
console.log(error);
}
}
//
const emits = defineEmits(['success'])
async function confirmHandle() {
try {
loading.value = true
form.value.shopId = store.userInfo.shopId
const res = await callTableConfigPut(form.value)
loading.value = false
ElMessage.success('保存成功')
dialogVisible.value = false
emits('success')
} catch (error) {
loading.value = false
console.log(error);
}
}
defineExpose({
show
})
</script>
<style>
.avatar-uploader .el-upload {
border: 1px dashed var(--el-border-color);
border-radius: 6px;
cursor: pointer;
position: relative;
overflow: hidden;
transition: var(--el-transition-duration-fast);
}
.avatar-uploader .el-upload:hover {
border-color: var(--el-color-primary);
}
.el-icon.avatar-uploader-icon {
font-size: 28px;
color: #8c939d;
width: 120px;
height: 120px;
text-align: center;
}
</style>
<style scoped lang="scss">
.title {
font-size: 18px;
font-weight: bold;
color: #333;
}
.avatar-uploader .avatar {
width: 120px;
height: 120px;
display: block;
}
$btmH: 50px;
.scroll_y {
height: 65vh;
overflow-y: auto;
padding-bottom: $btmH;
}
.footer {
display: flex;
padding: 0 100px 0;
position: relative;
&::before {
content: "";
height: $btmH;
background: linear-gradient(to bottom, rgba(255, 255, 255, 0), rgba(255, 255, 255, 1));
width: 100%;
position: absolute;
top: $btmH*-1;
left: 0;
z-index: 10;
}
}
</style>

282
src/views/queue/index.vue Normal file
View File

@ -0,0 +1,282 @@
<template>
<div class="content">
<div class="card" style="flex: 1;">
<div class="tab_head">
<el-radio-group v-model="tabActive" @change="callTableQueueAjax">
<el-radio-button label="全部" value=""></el-radio-button>
<el-radio-button :label="item.name" :value="item.id" v-for="item in tabHeader"
:key="item.id"></el-radio-button>
</el-radio-group>
<div class="btns">
<el-button type="danger" @click="GetNumberRef.show(tabHeader)">取号</el-button>
<el-button type="warning" @click="RecordRef.show()">叫号记录</el-button>
<el-button type="primary" @click="SettingRef.show()">基本设置</el-button>
</div>
</div>
<div class="queue_list" v-loading="queueList.loading">
<div class="item" v-for="item in queueList.list" :key="item.id">
<div class="head">
<div class="row">
<div class="row_item">用户</div>
<div class="row_item">号码</div>
<div class="row_item">桌型</div>
<div class="row_item">等待</div>
</div>
<div class="row">
<div class="row_item">{{ item.phone }}</div>
<div class="row_item">{{ item.callNum }}</div>
<div class="row_item">{{ item.name }}</div>
<div class="row_item">{{ item.waitingCount }}分钟</div>
</div>
</div>
<div class="btm">
<el-button @click="cancaleHandle(item)">取消</el-button>
<el-button type="primary" @click="callHandle(item)">播报</el-button>
</div>
</div>
<div class="empty">
<el-empty description="暂无数据~" v-if="!queueList.list.length" style="width: 100%;" />
</div>
</div>
<div class="pagination">
<el-pagination v-model:current-page="queueList.page" v-model:page-size="queueList.size"
layout="total, prev, pager, next" :total="queueList.total" background
@current-change="queueListChange" @size-change="queueListChange">
</el-pagination>
</div>
</div>
</div>
<!-- 基本设置 -->
<Setting ref="SettingRef" @success="callTableAjax" />
<!-- 叫号记录 -->
<Record ref="RecordRef" />
<!-- 取号 -->
<GetNumber ref="GetNumberRef" @success="callTableQueueAjax" />
<!-- 播报状态 -->
<ResultModal ref="ResultModalRef" @success="callTableQueueAjax" />
</template>
<script setup>
import Speech from 'speak-tts'
import { ElMessageBox, ElMessage } from 'element-plus'
import Setting from './components/setting.vue'
import Record from './components/record.vue'
import GetNumber from './components/getNumber.vue'
import ResultModal from './components/resultModal.vue'
import { onMounted, reactive, ref } from 'vue';
import { callRecord, callTable, callTableQueue, updateState, callTableCall } from '@/api/queue.js'
import { useUser } from "@/store/user.js"
const store = useUser()
const SettingRef = ref(null)
const RecordRef = ref(null)
const GetNumberRef = ref(null)
const ResultModalRef = ref(null)
//
const tabHeader = ref([])
const tabActive = ref('')
async function callTableAjax() {
try {
const res = await callTable({
page: 1,
size: 100,
shopId: store.userInfo.shopId,
callTableId: '',
state: ''
})
tabHeader.value = res.records
} catch (error) {
console.log(error);
}
}
// start
const queueList = reactive({
loading: false,
list: [],
page: 1,
size: 8,
total: 0
})
function queueListChange() {
callTableQueueAjax()
}
//
async function callTableQueueAjax() {
try {
queueList.loading = true
const res = await callTableQueue({
page: queueList.page,
size: queueList.size,
shopId: store.userInfo.shopId,
callTableId: tabActive.value,
state: ''
})
queueList.loading = false
queueList.list = res.records
queueList.total = res.total
} catch (error) {
console.log(error);
queueList.loading = false
}
}
// end
//
function cancaleHandle(item) {
ElMessageBox.confirm('确定要取消排队吗?', '注意').then(async () => {
try {
await updateState({
shopId: store.userInfo.shopId,
callQueueId: item.id,
state: '-1'
})
ElMessage.success('已取消')
callTableQueueAjax()
} catch (error) {
console.log(error);
}
}).catch(() => { })
}
//
async function callHandle(item) {
try {
startSpeech(`${item.callNum}用餐`)
const res = await callTableCall({
shopId: store.userInfo.shopId,
callQueueId: item.id,
})
ResultModalRef.value.show({
...item,
status: res.state
})
} catch (error) {
console.log(error);
}
}
//
const speech = new Speech()
function speechInit(params) {
speech.init({
volume: 1, //
lang: 'zh-CN', //
rate: 1, // 122
pitch: 1, //
splitSentences: true, //
listeners: {
//
onvoiceschanged: voices => {
// console.log('', voices);
},
},
}).then(data => {
console.log('语音已准备好,声音可用', data);
}).catch(err => {
console.log('初始化发生错误', err);
})
}
//
function startSpeech(text) {
speech.speak({
text: text, //使i18n
queue: true,
listeners: {
//
onstart: () => {
console.log('Start utterance')
},
//
onend: () => {
console.log('End utterance')
},
//
onresume: () => {
console.log('Resume utterance')
},
}
}).then(() => {
console.log('成功!');
}).catch(e => {
console.error('发生错误:', e);
});
}
onMounted(() => {
callTableAjax()
callTableQueueAjax()
if (speech.hasBrowserSupport()) {
console.log('语音播报加载成功,支持播报');
speechInit()
} else {
console.log('当前浏览器不支持语音播报');
}
})
</script>
<style lang="scss" scoped>
.card {
padding: 15px;
.pagination {
display: flex;
justify-content: flex-start;
}
}
.tab_head {
display: flex;
align-items: center;
justify-content: space-between;
}
.queue_list {
height: 80vh;
display: grid;
grid-template-columns: repeat(2, 1fr);
grid-template-rows: repeat(4, 1fr);
grid-column-gap: 15px;
grid-row-gap: 15px;
padding: 15px 0;
position: relative;
.empty {
position: absolute;
top: 40%;
left: 50%;
transform: translate(-50%, -50%);
}
.item {
border: 1px solid #ddd;
border-radius: 4px;
padding: 10px;
.head {
.row {
display: flex;
margin-bottom: 10px;
.row_item {
flex: 1;
&:last-child {
flex: 0.5;
}
}
}
}
.btm {
display: flex;
justify-content: flex-end;
}
}
}
</style>