705 lines
22 KiB
Vue
705 lines
22 KiB
Vue
<template>
|
||
<div class="container">
|
||
<div class="header_wrap">
|
||
<div class="btn" @click="userStore.logout()">
|
||
<el-icon>
|
||
<SwitchButton />
|
||
</el-icon>
|
||
<el-text>退出</el-text>
|
||
</div>
|
||
<div class="title"></div>
|
||
<div class="name">
|
||
<div class="dot" :class="{ online: socketStore.connected }"></div>
|
||
<el-text>
|
||
{{ userStore.shopStaff.name || userStore.shopInfo.shopName }}
|
||
</el-text>
|
||
</div>
|
||
</div>
|
||
<div class="tab_wrap">
|
||
<div class="left">
|
||
<div class="item" :class="{ active: checkType == item.value }" v-for="(item, index) in checkTypeList"
|
||
:key="index" @click="queryForm.tableName = ''; checkTypeHandle(item)">
|
||
<span>{{ item.label }}</span>
|
||
</div>
|
||
</div>
|
||
<div class="right" @click="showScanTipsHandle">
|
||
<scanIcon />
|
||
<el-text>扫码出菜</el-text>
|
||
</div>
|
||
</div>
|
||
<div class="search_wrap">
|
||
<div class="left">
|
||
<el-form inline @submit.prevent>
|
||
<el-form-item :label="checkType == 1 ? '台桌查找' : '菜品查找'">
|
||
<el-input v-model="queryForm.tableName" :placeholder="checkType == 1 ? '请输入台桌' : '请输入菜品'"
|
||
clearable @clear="inputClear"></el-input>
|
||
</el-form-item>
|
||
<el-form-item>
|
||
<el-button type="primary" @click="searchHandle">查询</el-button>
|
||
</el-form-item>
|
||
</el-form>
|
||
</div>
|
||
<!-- <div class="right">
|
||
<el-form inline>
|
||
<el-form-item>
|
||
<el-button type="primary" style="width: 100px;">出菜</el-button>
|
||
</el-form-item>
|
||
</el-form>
|
||
</div> -->
|
||
</div>
|
||
<div class="table_list_wrap" v-if="checkType == 1">
|
||
<div class="item list" :class="{ empty: tableList.length <= 0 }">
|
||
<div class="tab_item" v-for="item in tableList" :key="item.orderId" @click="changeTableItem(item)">
|
||
<div class="tab_header_wrap">
|
||
{{ item.tableName }} | {{ item.areaName }}
|
||
</div>
|
||
<div class="content">
|
||
<div class="btn_wrap">
|
||
<div class="btn">
|
||
<el-button type="primary" style="width: 100%;">待出菜({{ item.pendingDishCount
|
||
}})</el-button>
|
||
</div>
|
||
</div>
|
||
<!-- <div class="people_wrap">
|
||
<peopleIcon />
|
||
<span>1人</span>
|
||
</div> -->
|
||
</div>
|
||
</div>
|
||
<el-empty v-if="tableList.length <= 0"></el-empty>
|
||
</div>
|
||
<div class="item table_info">
|
||
<div class="table_info_wrap">
|
||
<el-button type="primary">{{ selectItem.tableName || '-' }} | {{ selectItem.areaName || '-'
|
||
}}</el-button>
|
||
<el-text>待出菜({{ selectItem.pendingDishCount }})</el-text>
|
||
<el-text>员工名称:{{ selectItem.staffName || notStaff }}</el-text>
|
||
<el-text>下单时间:{{ selectItem.orderTime || '-' }}</el-text>
|
||
</div>
|
||
<div class="table_wrap">
|
||
<el-table :data="tableData.list" border stripe>
|
||
<el-table-column label="菜品名称" prop="productName">
|
||
<template v-slot="scope">
|
||
{{ scope.row.productName }} * {{ scope.row.num }}
|
||
</template>
|
||
</el-table-column>
|
||
<el-table-column label="用时" prop="time_taken"></el-table-column>
|
||
<el-table-column label="下单时间" prop="orderTime"></el-table-column>
|
||
<el-table-column label="状态" prop="subStatus" width="150">
|
||
<template v-slot="scope">
|
||
<el-tag disable-transitions :type="statusFilter(scope.row.subStatus).type" size="large">
|
||
{{ statusFilter(scope.row.subStatus).label }}
|
||
</el-tag>
|
||
</template>
|
||
</el-table-column>
|
||
<el-table-column label="操作" width="150">
|
||
<template v-slot="scope">
|
||
<el-button type="primary"
|
||
v-if="scope.row.subStatus == 'READY_TO_SERVE' || scope.row.subStatus == 'TIMEOUT'"
|
||
@click="upOrderDetailAjax(scope.row)">出菜</el-button>
|
||
</template>
|
||
</el-table-column>
|
||
</el-table>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<div class="goods_list_wrap" v-else>
|
||
<div class="tablef_head">
|
||
<div class="item" @click="changeGoodsIndexHandle({ id: '' }, -1)">
|
||
<el-text :type="goodsListActive == -1 ? 'primary' : ''">全部</el-text>
|
||
</div>
|
||
<div class="item" v-for="(item, index) in categorys" :key="item.id"
|
||
@click="changeGoodsIndexHandle(item, index)">
|
||
<el-text :type="goodsListActive == index ? 'primary' : ''">{{ item.name }}</el-text>
|
||
</div>
|
||
</div>
|
||
<div class="table_wrap">
|
||
<div class="table" v-for="(item, index) in goodsTableData" :key="index">
|
||
<div class="goods_table_title">
|
||
<span>{{ item.productName }} 待出菜({{ item.total
|
||
}})</span>
|
||
</div>
|
||
<el-table :data="item.foodItems" border stripe>
|
||
<el-table-column label="下单台桌" prop="areaName">
|
||
<template v-slot="scope">
|
||
{{ scope.row.tableName }} | {{ scope.row.areaName }} * {{ scope.row.num }}
|
||
</template>
|
||
</el-table-column>
|
||
<el-table-column label="用时" prop="time_taken"></el-table-column>
|
||
<el-table-column label="状态" prop="subStatus" width="150">
|
||
<template v-slot="scope">
|
||
<el-tag disable-transitions :type="statusFilter(scope.row.subStatus).type" size="large">
|
||
{{ statusFilter(scope.row.subStatus).label }}
|
||
</el-tag>
|
||
</template>
|
||
</el-table-column>
|
||
<el-table-column label="下单时间" prop="orderTime"></el-table-column>
|
||
<el-table-column label="员工" prop="staffName">
|
||
<template v-slot="scope">
|
||
{{ scope.row.staffName || notStaff }}
|
||
</template>
|
||
</el-table-column>
|
||
<el-table-column label="操作" width="150">
|
||
<template v-slot="scope">
|
||
<el-button type="primary"
|
||
v-if="scope.row.subStatus == 'READY_TO_SERVE' || scope.row.subStatus == 'TIMEOUT'"
|
||
@click="upOrderDetailAjax(scope.row)">出菜</el-button>
|
||
</template>
|
||
</el-table-column>
|
||
</el-table>
|
||
</div>
|
||
<el-empty v-if="goodsTableData.length <= 0"></el-empty>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</template>
|
||
|
||
<script setup>
|
||
import _ from 'lodash'
|
||
import dayjs from 'dayjs';
|
||
import { useUserStore } from '@/stores/user';
|
||
import scanIcon from '@/components/icons/scanIcon.vue';
|
||
import peopleIcon from '@/components/icons/peopleIcon.vue';
|
||
import tableStatus from '@/utils/tableStatusList.js'
|
||
import { formatTimeDiff, isMoreThanSpecifiedMinutes, calculateTimeoutDuration } from '@/utils'
|
||
import { categoryList } from "@/api/product.js";
|
||
import { getKitchenTable, getKitchenTableFoods, getKitchenFood, upOrderDetail } from '@/api/order.js'
|
||
import { ref, reactive, onMounted } from 'vue';
|
||
import { ElMessage, ElNotification } from 'element-plus';
|
||
import { useScanListener } from '@/composables/useScanListener'
|
||
import { useSocketStore } from "@/stores/socket.js";
|
||
import { onUnmounted } from 'vue';
|
||
|
||
const socketStore = useSocketStore()
|
||
|
||
const unsub = socketStore.subscribe(msg => {
|
||
console.log('got', msg)
|
||
if (msg.type == 'bc') {
|
||
checkTypeHandle({ value: checkType.value })
|
||
}
|
||
})
|
||
|
||
const userStore = useUserStore()
|
||
|
||
const notStaff = ref('管理员')
|
||
|
||
// 扫码获取扫码内容
|
||
const { lastScan } = useScanListener({
|
||
onScan(code) {
|
||
let str = code.split(':')
|
||
handleScan(str[1])
|
||
}
|
||
})
|
||
|
||
// 扫码提示
|
||
function showScanTipsHandle() {
|
||
ElNotification({
|
||
type: 'warning',
|
||
title: '注意',
|
||
message: '请使用扫码设备扫描菜品小票出菜'
|
||
})
|
||
}
|
||
|
||
const checkTypeList = ref([
|
||
{
|
||
label: '按台桌查看',
|
||
value: 1
|
||
},
|
||
{
|
||
label: '按菜品查看',
|
||
value: 2
|
||
}
|
||
])
|
||
|
||
const checkType = ref(1)
|
||
|
||
// 清除搜索内容
|
||
function inputClear() {
|
||
goodsListActive.value = -1
|
||
checkTypeHandle({ value: checkType.value })
|
||
}
|
||
|
||
// 搜索
|
||
function searchHandle() {
|
||
// goodsListActive.value = -1
|
||
checkTypeHandle({ value: checkType.value })
|
||
}
|
||
|
||
// 切换查看类型
|
||
async function checkTypeHandle(item) {
|
||
checkType.value = item.value
|
||
if (item.value == 1) {
|
||
await getKitchenTableAjax()
|
||
} else {
|
||
await getKitchenFoodAjax()
|
||
}
|
||
setTimeout(() => {
|
||
startUpdate()
|
||
}, 1000);
|
||
}
|
||
|
||
const categorys = ref([])
|
||
const categoryId = ref('')
|
||
|
||
async function categoryListAjax() {
|
||
try {
|
||
const res = await categoryList()
|
||
categorys.value = res
|
||
} catch (error) {
|
||
console.log(error);
|
||
}
|
||
}
|
||
|
||
const queryForm = reactive({
|
||
tableName: ''
|
||
})
|
||
|
||
// 按台桌查看
|
||
const tableList = ref([])
|
||
const selectItem = ref({})
|
||
async function getKitchenTableAjax() {
|
||
try {
|
||
const res = await getKitchenTable({
|
||
tableName: queryForm.tableName
|
||
})
|
||
tableList.value = res
|
||
if (res.length > 0) {
|
||
if (!selectItem.value.id) {
|
||
selectItem.value = res[0]
|
||
}
|
||
getKitchenTableFoodsAjax()
|
||
} else {
|
||
selectItem.value = {}
|
||
tableData.list = []
|
||
}
|
||
} catch (error) {
|
||
console.log(error);
|
||
}
|
||
}
|
||
|
||
// 切换台桌
|
||
function changeTableItem(item) {
|
||
selectItem.value = item
|
||
getKitchenTableFoodsAjax()
|
||
}
|
||
|
||
// 按台桌查看 商品内容
|
||
const tableData = reactive({
|
||
list: []
|
||
})
|
||
async function getKitchenTableFoodsAjax() {
|
||
try {
|
||
const res = await getKitchenTableFoods({
|
||
orderId: selectItem.value.orderId,
|
||
tableCode: selectItem.value.tableCode,
|
||
isNoTable: selectItem.value.tableCode ? '' : 1
|
||
})
|
||
|
||
res.forEach(item => {
|
||
item.time_taken = timeTakenUtils(item)
|
||
})
|
||
|
||
tableData.list = res
|
||
} catch (error) {
|
||
console.log(error);
|
||
}
|
||
}
|
||
|
||
// 按菜品查看
|
||
const goodsListActive = ref(-1)
|
||
const goodsList = ref([])
|
||
const goodsTableData = ref([])
|
||
async function getKitchenFoodAjax() {
|
||
try {
|
||
const res = await getKitchenFood({
|
||
productName: queryForm.tableName,
|
||
categoryId: categoryId.value
|
||
})
|
||
res.forEach(val => {
|
||
val.total = 0
|
||
val.foodItems.forEach(item => {
|
||
item.time_taken = timeTakenUtils(item)
|
||
if (item.subStatus == 'READY_TO_SERVE') {
|
||
val.total += +item.num
|
||
}
|
||
})
|
||
})
|
||
goodsList.value = res
|
||
goodsTableData.value = goodsList.value
|
||
} catch (error) {
|
||
console.log(error);
|
||
}
|
||
}
|
||
|
||
// 切换类型
|
||
function changeGoodsIndexHandle(item, index) {
|
||
categoryId.value = item.id
|
||
goodsListActive.value = index
|
||
// if (index == -1) {
|
||
// goodsTableData.value = goodsList.value
|
||
// } else {
|
||
// goodsTableData.value = [goodsList.value[index]]
|
||
// }
|
||
getKitchenFoodAjax()
|
||
}
|
||
|
||
// 扫码出菜 1秒内最多调用1次,防止重复提交
|
||
const handleScan = _.throttle(async function (code) {
|
||
console.log('handleScan', code);
|
||
try {
|
||
upOrderDetailAjax({
|
||
orderDetailId: code
|
||
})
|
||
} catch (err) {
|
||
console.error('handleScan error', err)
|
||
}
|
||
}, 1000, { leading: true, trailing: false })
|
||
|
||
// 出菜
|
||
async function upOrderDetailAjax(item) {
|
||
try {
|
||
await upOrderDetail({
|
||
orderDetailId: item.orderDetailId,
|
||
type: 2
|
||
})
|
||
ElMessage.success('出菜成功');
|
||
checkTypeHandle({ value: checkType.value })
|
||
} catch (error) {
|
||
console.log(error);
|
||
}
|
||
}
|
||
|
||
// 菜品状态 待起菜 PENDING_PREP 待出菜 READY_TO_SERVE 已出菜 SENT_OUT 已上菜 DELIVERED
|
||
function statusFilter(status) {
|
||
let statusList = [
|
||
{
|
||
value: 'PENDING_PREP',
|
||
label: '待起菜',
|
||
type: 'info'
|
||
},
|
||
{
|
||
value: 'READY_TO_SERVE',
|
||
label: '待出菜',
|
||
type: 'warning'
|
||
},
|
||
{
|
||
value: 'SENT_OUT',
|
||
label: '已出菜',
|
||
type: 'success'
|
||
},
|
||
{
|
||
value: 'DELIVERED',
|
||
label: '已上菜',
|
||
type: 'primary'
|
||
},
|
||
{
|
||
value: 'TIMEOUT',
|
||
label: '已超时',
|
||
type: 'danger'
|
||
}
|
||
]
|
||
|
||
let obj = statusList.find(item => item.value == status)
|
||
if (obj && obj.value) {
|
||
return obj
|
||
} else {
|
||
return {
|
||
value: status,
|
||
label: '未知',
|
||
type: 'warning'
|
||
}
|
||
}
|
||
}
|
||
|
||
// 刷新所有数据的状态
|
||
function updateOrderStatus() {
|
||
// console.log('刷新所有数据的状态.tableData.list', tableData.list);
|
||
// console.log('刷新所有数据的状态.goodsTableData.value', goodsTableData.value);
|
||
|
||
if (checkType.value == 1) {
|
||
// 刷新按台桌查看的数据
|
||
tableData.list.forEach(item => {
|
||
let timeSpent = formatTimeDiff(item.startOrderTime)
|
||
if (item.subStatus == 'READY_TO_SERVE' && isMoreThanSpecifiedMinutes(timeSpent, userStore.shopInfo.serveTime)) {
|
||
item.subStatus = 'TIMEOUT'
|
||
}
|
||
item.time_taken = timeTakenUtils(item)
|
||
})
|
||
} else {
|
||
// 刷新按菜品查看的数据
|
||
goodsTableData.value.forEach(val => {
|
||
val.foodItems.forEach(item => {
|
||
let timeSpent = formatTimeDiff(item.startOrderTime)
|
||
if (item.subStatus == 'READY_TO_SERVE' && isMoreThanSpecifiedMinutes(timeSpent, userStore.shopInfo.serveTime)) {
|
||
item.subStatus = 'TIMEOUT'
|
||
}
|
||
item.time_taken = timeTakenUtils(item)
|
||
})
|
||
})
|
||
}
|
||
}
|
||
|
||
// 根据类型返回用时
|
||
function timeTakenUtils(row) {
|
||
// 待出菜用时
|
||
if (row.subStatus == 'READY_TO_SERVE') {
|
||
return formatTimeDiff(row.startOrderTime)
|
||
}
|
||
// 超时用时
|
||
if (row.subStatus == 'TIMEOUT') {
|
||
// let timeSpent = formatTimeDiff(row.startOrderTime)
|
||
// return calculateTimeoutDuration(timeSpent, userStore.shopInfo.serveTime)
|
||
return formatTimeDiff(row.startOrderTime)
|
||
}
|
||
// 出菜用时
|
||
if (row.subStatus == 'SENT_OUT') {
|
||
return formatTimeDiff(row.startOrderTime, row.dishOutTime)
|
||
}
|
||
// 上菜用时
|
||
if (row.subStatus == 'DELIVERED') {
|
||
return formatTimeDiff(row.foodServeTime)
|
||
}
|
||
}
|
||
|
||
// 启动刷新
|
||
const timer = ref(null)
|
||
function startUpdate() {
|
||
if (userStore.shopInfo.isServeTimeControl && (goodsTableData.length || tableData.list.length)) {
|
||
console.log('启动刷新');
|
||
|
||
// 只有开启的情况下调用
|
||
updateOrderStatus()
|
||
if (timer.value !== null) {
|
||
clearInterval(timer.value)
|
||
}
|
||
timer.value = setInterval(() => {
|
||
updateOrderStatus()
|
||
}, 1000);
|
||
}
|
||
}
|
||
|
||
onMounted(async () => {
|
||
checkTypeHandle({ value: checkType.value })
|
||
categoryListAjax()
|
||
socketStore.init(import.meta.env.VITE_API_WSS)
|
||
socketStore.startHeartbeat()
|
||
|
||
// 如果需要在打开时发送初始化,可使用 onOpen 注册一次性回调:
|
||
const off = socketStore.onOpen(() => {
|
||
socketStore.send({ operate_type: 'init' })
|
||
off() // 若只需一次,注册后立即取消
|
||
})
|
||
})
|
||
|
||
onUnmounted(() => {
|
||
clearInterval(timer.value)
|
||
timer.value = null
|
||
// unsub.close()
|
||
})
|
||
</script>
|
||
|
||
<style scoped lang="scss">
|
||
.container {
|
||
--padding: 24px;
|
||
|
||
.header_wrap {
|
||
--height: 57px;
|
||
display: flex;
|
||
height: var(--height);
|
||
align-items: center;
|
||
padding: 0 var(--padding);
|
||
background-color: #fff;
|
||
|
||
.btn {
|
||
width: var(--height);
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
gap: 4px;
|
||
}
|
||
|
||
.title {
|
||
flex: 1;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
}
|
||
|
||
.name {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
gap: 6px;
|
||
|
||
.dot {
|
||
--size: 8px;
|
||
width: var(--size);
|
||
height: var(--size);
|
||
border-radius: 50%;
|
||
position: relative;
|
||
top: 1px;
|
||
background-color: var(--el-color-danger);
|
||
|
||
&.online {
|
||
background-color: var(--el-color-success);
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
.tab_wrap {
|
||
display: flex;
|
||
justify-content: space-between;
|
||
border-bottom: 1px solid #ececec;
|
||
padding: 0 var(--padding) 14px;
|
||
background-color: #fff;
|
||
|
||
.left {
|
||
display: flex;
|
||
gap: var(--padding);
|
||
|
||
.item {
|
||
&.active {
|
||
span {
|
||
font-weight: bold;
|
||
color: var(--el-color-primary);
|
||
}
|
||
}
|
||
|
||
span {
|
||
font-size: 16px;
|
||
color: #333;
|
||
}
|
||
}
|
||
}
|
||
|
||
.right {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 4px;
|
||
}
|
||
}
|
||
|
||
.search_wrap {
|
||
background-color: #fff;
|
||
padding: 16px var(--padding) 0 var(--padding);
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
}
|
||
|
||
.table_list_wrap {
|
||
margin-top: 14px;
|
||
background-color: #fff;
|
||
padding: var(--padding);
|
||
display: flex;
|
||
gap: var(--padding);
|
||
|
||
.item {
|
||
flex: 1;
|
||
|
||
&.list {
|
||
display: grid;
|
||
grid-template-columns: repeat(4, 1fr);
|
||
grid-template-rows: repeat(auto, 1fr);
|
||
grid-column-gap: var(--padding);
|
||
grid-row-gap: var(--padding);
|
||
|
||
&.empty {
|
||
display: flex;
|
||
justify-content: center;
|
||
}
|
||
|
||
.tab_item {
|
||
height: 150px;
|
||
background-color: #3F9EFF;
|
||
padding: 7px;
|
||
display: flex;
|
||
flex-direction: column;
|
||
border-radius: 8px;
|
||
overflow: hidden;
|
||
transition: all 0.3s ease-in-out;
|
||
|
||
&.active {
|
||
transform: translateY(-10px) scale(1.1);
|
||
box-shadow: 0 10px 20px rgba(0, 0, 0, 0.15);
|
||
}
|
||
|
||
.tab_header_wrap {
|
||
font-size: 14px;
|
||
color: #fff;
|
||
padding-bottom: 10px;
|
||
flex-shrink: 0;
|
||
}
|
||
|
||
.content {
|
||
flex: 1;
|
||
display: flex;
|
||
flex-direction: column;
|
||
background-color: #fff;
|
||
border-radius: 3px;
|
||
padding: 0 7px;
|
||
|
||
.btn_wrap {
|
||
flex: 1;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
background-color: #fff;
|
||
border-radius: 20px;
|
||
}
|
||
|
||
.people_wrap {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 4px;
|
||
padding: 10px 0 20px 0;
|
||
border-top: 1px solid #ececec;
|
||
|
||
span {
|
||
font-size: 14px;
|
||
color: #666;
|
||
}
|
||
}
|
||
}
|
||
|
||
}
|
||
}
|
||
|
||
&.table_info {
|
||
.table_info_wrap {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 10px;
|
||
}
|
||
|
||
.table_wrap {
|
||
padding-top: var(--padding);
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
.goods_list_wrap {
|
||
margin-top: 14px;
|
||
background-color: #fff;
|
||
padding: var(--padding);
|
||
gap: var(--padding);
|
||
|
||
.tablef_head {
|
||
display: flex;
|
||
gap: var(--padding);
|
||
}
|
||
|
||
.goods_table_title {
|
||
padding: 10px 0 5px 0;
|
||
|
||
span {
|
||
font-size: 16px;
|
||
font-weight: bold;
|
||
color: #333;
|
||
}
|
||
}
|
||
}
|
||
}
|
||
</style> |