Files
kitchen-desktop/src/renderer/src/views/home.vue

705 lines
22 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<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>