优化页面小时,优化用时显示

This commit is contained in:
gyq
2025-12-01 18:50:45 +08:00
parent a15324ae9e
commit d3340eb953
6 changed files with 176 additions and 80 deletions

View File

@@ -11,8 +11,6 @@ import { useSocketStore } from '@/stores/socket';
const pinia = createPinia()
onMounted(() => {
console.log('WS URL:', import.meta.env.VITE_WS_URL);
// 重启或刷新防止ws丢失
const socketStore = useSocketStore(pinia)
socketStore.restoreFromStorage()

View File

@@ -7,7 +7,8 @@ export const useUserStore = defineStore('user', {
state: () => ({
token: '',
shopInfo: '',
shopStaff: ''
shopStaff: '',
account: ''
}),
actions: {
async login(params) {
@@ -19,6 +20,8 @@ export const useUserStore = defineStore('user', {
// 登录员工
if (res.loginType == 1) this.shopStaff = res.shopStaff
// async 函数直接返回值即可 resolve
this.account = params.username
return res
} else {
ElMessage.error('先下单模式下不可登录,请联系管理员')
@@ -68,7 +71,7 @@ export const useUserStore = defineStore('user', {
{
key: 'kitchen-user',
storage: localStorage,
paths: ['token', 'shopInfo', 'shopStaff'] // 持久化指定状态
paths: ['token', 'shopInfo', 'shopStaff', 'account'] // 持久化指定状态
}
]
}

View File

@@ -1,20 +1,24 @@
import dayjs from 'dayjs';
/**
* 计算当前时间与传入时间的时间差,并按规则格式化
* 计算基准时间与传入目标时间的时间差,并按规则格式化
* @param {string|number|Date} targetTime - 传入的目标时间支持字符串、时间戳、Date对象
* @returns {string} 格式化后的时间差≤1小时MM:ss>1小时HH:MM:ss| 错误提示
* @param {string|number|Date} [currentTime=dayjs()] - 基准时间默认当前时间支持字符串、时间戳、Date对象
* @returns {string} 格式化后的时间差≤1小时MM:ss>1小时HH:MM:ss| 无效时间返回 "00:00"
*/
export const formatTimeDiff = (targetTime) => {
// 1. 校验传入时间的有效性
export const formatTimeDiff = (targetTime, currentTime = dayjs()) => {
// 1. 校验目标时间和基准时间的有效性
const target = dayjs(targetTime);
if (!target.isValid()) {
return '00:00'; // 无效时间返回提示
const baseTime = dayjs(currentTime); // 解析基准时间(默认当前时间)
// 任一时间无效,返回 "00:00"
if (!target.isValid() || !baseTime.isValid()) {
return '00:00';
}
// 2. 计算当前时间与目标时间的**秒级总差值**当前时间 - 传入时间)
const diffSeconds = dayjs().diff(target, 'second');
// 处理差值为负数的情况(传入时间在当前时间之后,差值为负
// 2. 计算基准时间与目标时间的**秒级总差值**基准时间 - 目标时间)
const diffSeconds = baseTime.diff(target, 'second');
// 处理差值为负数的情况(目标时间在基准时间之后,取绝对值
const absDiffSeconds = Math.abs(diffSeconds);
// 3. 按秒数分解为 小时、分钟、秒
@@ -36,68 +40,104 @@ export const formatTimeDiff = (targetTime) => {
};
/**
* 判断时分秒字符串是否大于指定分钟数
* @param {string} timeStr - 时分秒字符串,如 "26:30:40"
* 判断时字符串是否大于指定分钟数(兼容分秒格式和时分秒格式)
* @param {string} timeStr - 时字符串,支持两种格式:
* 1. 分秒格式:"MM:SS"(如 "12:34"
* 2. 时分秒格式:"HH:MM:SS"(如 "07:23:45"
* @param {number} [thresholdMinutes=15] - 分钟阈值默认15分钟
* @returns {boolean} 大于阈值返回true否则false格式错误返回false
*/
export const isMoreThanSpecifiedMinutes = (timeStr, thresholdMinutes = 15) => {
// 1. 按冒号分割时分秒并转换为数字
const [hours, minutes, seconds] = timeStr.split(':').map(Number);
console.log('isMoreThanSpecifiedMinutes===', timeStr);
// 2. 校验时分秒格式是否合法
// 1. 按冒号分割时间字符串并转换为数字数组
const timeParts = timeStr.split(':').map(Number);
// 2. 校验格式合法性(仅允许 2 段【分秒】或 3 段【时分秒】,且所有部分为有效数字)
if (
isNaN(hours) ||
isNaN(minutes) ||
isNaN(seconds) ||
hours < 0 ||
minutes < 0 || minutes >= 60 ||
seconds < 0 || seconds >= 60
!([2, 3].includes(timeParts.length)) || // 仅支持 2 或 3 段
timeParts.some(part => isNaN(part)) || // 存在非数字部分
timeParts.some(part => part < 0) // 存在负数
) {
console.error('时分秒格式错误,示例:"26:30:40"');
console.error('时格式错误,支持:分秒格式(如 "12:34")或时分秒格式(如 "07:23:45"');
return false;
}
// 3. 转换为总秒数:小时*3600 + 分钟*60 +
// 3. 根据分割长度补全小时位,统一转换为 [小时, 分钟,]
let hours = 0, minutes = 0, seconds = 0;
if (timeParts.length === 3) {
// 时分秒格式HH:MM:SS
[hours, minutes, seconds] = timeParts;
// 额外校验:分钟和秒不能超过 59
if (minutes >= 60 || seconds >= 60) {
console.error('时分秒格式错误:分钟和秒必须小于 60');
return false;
}
} else if (timeParts.length === 2) {
// 分秒格式MM:SS → 补全小时为 0
[minutes, seconds] = timeParts;
// 额外校验:分钟和秒不能超过 59
if (minutes >= 60 || seconds >= 60) {
console.error('分秒格式错误:分钟和秒必须小于 60');
return false;
}
}
// 4. 转换为总秒数:小时*3600 + 分钟*60 + 秒
const totalSeconds = hours * 3600 + minutes * 60 + seconds;
// 4. 将分钟阈值转换为总秒数,进行比较
// 5. 将分钟阈值转换为总秒数,进行比较
const thresholdSeconds = thresholdMinutes * 60;
return totalSeconds > thresholdSeconds;
};
/**
* 计算时分秒字符串超出指定分钟阈值的时长返回HH:MM:SS格式
* @param {string} timeStr - 时分秒字符串,如 "26:30:40"
* 计算时字符串超出指定分钟阈值的时长返回HH:MM:SS格式,兼容分秒/时分秒输入
* @param {string} timeStr - 时字符串,支持两种格式:
* 1. 分秒格式:"MM:SS"(如 "12:34"
* 2. 时分秒格式:"HH:MM:SS"(如 "07:23:45"
* @param {number} [thresholdMinutes=15] - 分钟阈值默认15分钟
* @returns {string} 超时时长HH:MM:SS未超时返回"00:00:00",格式错误返回"格式错误"
*/
export const calculateTimeoutDuration = (timeStr, thresholdMinutes = 15) => {
// 1. 按冒号分割时分秒并转换为数字
const [hours, minutes, seconds] = timeStr.split(':').map(Number);
// 新增:清理输入字符串(去除前后空格、替换中文冒号为英文冒号)
const cleanedTimeStr = timeStr.trim().replace(//g, ':');
// 按英文冒号分割并转换为数字数组
const timeParts = cleanedTimeStr.split(':').map(Number);
// 2. 校验时分秒格式合法性
// 校验格式合法性(仅支持 2 段【分秒】或 3 段【时分秒】)
if (
isNaN(hours) ||
isNaN(minutes) ||
isNaN(seconds) ||
hours < 0 ||
minutes < 0 || minutes >= 60 ||
seconds < 0 || seconds >= 60
!([2, 3].includes(timeParts.length)) || // 仅允许 2/3 段分割结果
timeParts.some(part => isNaN(part)) || // 存在非数字部分
timeParts.some(part => part < 0) // 存在负数
) {
console.error('时分秒格式错误,示例:"26:30:40"');
console.error('时格式错误,支持:分秒格式(如 "12:34")或时分秒格式(如 "07:23:45"');
return '格式错误';
}
// 3. 转换为总秒数
// 根据分割长度补全小时位,统一转换为 [小时, 分钟, 秒]
let hours = 0, minutes = 0, seconds = 0;
if (timeParts.length === 3) {
[hours, minutes, seconds] = timeParts;
if (minutes >= 60 || seconds >= 60) {
console.error('时分秒格式错误:分钟和秒必须小于 60');
return '格式错误';
}
} else if (timeParts.length === 2) {
[minutes, seconds] = timeParts;
if (minutes >= 60 || seconds >= 60) {
console.error('分秒格式错误:分钟和秒必须小于 60');
return '格式错误';
}
}
// 计算总秒数、阈值秒数、超时秒数
const totalSeconds = hours * 3600 + minutes * 60 + seconds;
// 4. 计算阈值对应的总秒数
const thresholdSeconds = thresholdMinutes * 60;
// 5. 计算超时秒数差值≤0则未超时
const timeoutSeconds = Math.max(totalSeconds - thresholdSeconds, 0);
// 6. 将超时秒数转换回HH:MM:SS格式补零处理
// 格式化为 HH:MM:SS
const padZero = (num) => num.toString().padStart(2, '0');
const timeoutHours = padZero(Math.floor(timeoutSeconds / 3600));
const timeoutMins = padZero(Math.floor((timeoutSeconds % 3600) / 60));

View File

@@ -40,15 +40,15 @@ service.interceptors.request.use(
// 响应拦截器
service.interceptors.response.use(
(response) => {
const userStore = useUserStore()
// 对响应数据做点什么
if (+response.status === 200) {
if (+response.data.code == 200) {
return response.data.data;
} else if (+response.data.code == 501) {
useStorage.del("token");
useStorage.del("userInfo");
useStorage.del("shopInfo");
useStorage.del("douyin");
userStore.shopInfo = ''
userStore.token = ''
userStore.shopStaff = ''
ElMessage.error("登录已过期,请重新登录");
window.location.reload();
return Promise.reject("登录已过期,请重新登录");

View File

@@ -17,11 +17,9 @@
</div>
<div class="tab_wrap">
<div class="left">
<div class="item" v-for="(item, index) in checkTypeList" :key="index"
@click="queryForm.tableName = ''; checkTypeHandle(item)">
<el-text :type="checkType == item.value ? 'primary' : ''">
{{ item.label }}
</el-text>
<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">
@@ -62,10 +60,10 @@
}}</el-button>
</div>
</div>
<div class="people_wrap">
<!-- <div class="people_wrap">
<peopleIcon />
<span>1</span>
</div>
</div> -->
</div>
</div>
<el-empty v-if="tableList.length <= 0"></el-empty>
@@ -74,21 +72,19 @@
<div class="table_info_wrap">
<el-button type="primary">{{ selectItem.tableName || '-' }} | {{ selectItem.areaName || '-'
}}</el-button>
<el-text>待出菜{{ selectItem.pendingDishCount || '-' }}</el-text>
<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"></el-table-column>
<el-table-column label="用时" prop="startOrderTime">
<el-table-column label="菜品名称" prop="productName">
<template v-slot="scope">
<el-text v-if="scope.row.subStatus == 'READY_TO_SERVE'">{{
formatTimeDiff(scope.row.startOrderTime) }}</el-text>
<el-text v-if="scope.row.subStatus == 'TIMEOUT'">{{ scope.row.timeout_time }}</el-text>
<el-text v-if="scope.row.subStatus == 'SENT_OUT'">{{ scope.row.dishOutTime }}</el-text>
{{ 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">
@@ -120,15 +116,16 @@
<div class="table_wrap">
<div class="table" v-for="(item, index) in goodsTableData" :key="index">
<div class="goods_table_title">
<el-text size="large">{{ item.productName }}</el-text>
<span>{{ item.productName }} 待出菜{{ item.total
}}</span>
</div>
<el-table :data="item.foodItems" border stripe>
<el-table-column label="下单台桌" prop="areaName"></el-table-column>
<el-table-column label="用时" prop="startOrderTime">
<el-table-column label="下单台桌" prop="areaName">
<template v-slot="scope">
<el-text>{{ formatTimeDiff(scope.row.startOrderTime) }}</el-text>
{{ 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">
@@ -158,6 +155,7 @@
</template>
<script setup>
import _ from 'lodash'
import dayjs from 'dayjs';
import { useUserStore } from '@/stores/user';
import scanIcon from '@/components/icons/scanIcon.vue';
@@ -223,7 +221,7 @@ function inputClear() {
// 搜索
function searchHandle() {
goodsListActive.value = -1
// goodsListActive.value = -1
checkTypeHandle({ value: checkType.value })
}
@@ -235,7 +233,9 @@ async function checkTypeHandle(item) {
} else {
await getKitchenFoodAjax()
}
startUpdate()
setTimeout(() => {
startUpdate()
}, 1000);
}
const categorys = ref([])
@@ -264,7 +264,9 @@ async function getKitchenTableAjax() {
})
tableList.value = res
if (res.length > 0) {
selectItem.value = res[0]
if (!selectItem.value.id) {
selectItem.value = res[0]
}
getKitchenTableFoodsAjax()
} else {
selectItem.value = {}
@@ -294,7 +296,7 @@ async function getKitchenTableFoodsAjax() {
})
res.forEach(item => {
item.timeout_time = ''
item.time_taken = timeTakenUtils(item)
})
tableData.list = res
@@ -313,6 +315,15 @@ async function getKitchenFoodAjax() {
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) {
@@ -332,8 +343,8 @@ function changeGoodsIndexHandle(item, index) {
getKitchenFoodAjax()
}
// 扫码出菜
function handleScan(code) {
// 扫码出菜 1秒内最多调用1次防止重复提交
const handleScan = _.throttle(async function (code) {
console.log('handleScan', code);
try {
upOrderDetailAjax({
@@ -342,7 +353,7 @@ function handleScan(code) {
} catch (err) {
console.error('handleScan error', err)
}
}
}, 1000, { leading: true, trailing: false })
// 出菜
async function upOrderDetailAjax(item) {
@@ -402,8 +413,8 @@ function statusFilter(status) {
// 刷新所有数据的状态
function updateOrderStatus() {
console.log('刷新所有数据的状态.tableData.list', tableData.list);
console.log('刷新所有数据的状态.goodsTableData.value', goodsTableData.value);
// console.log('刷新所有数据的状态.tableData.list', tableData.list);
// console.log('刷新所有数据的状态.goodsTableData.value', goodsTableData.value);
if (checkType.value == 1) {
// 刷新按台桌查看的数据
@@ -411,8 +422,8 @@ function updateOrderStatus() {
let timeSpent = formatTimeDiff(item.startOrderTime)
if (item.subStatus == 'READY_TO_SERVE' && isMoreThanSpecifiedMinutes(timeSpent, userStore.shopInfo.serveTime)) {
item.subStatus = 'TIMEOUT'
item.timeout_time = calculateTimeoutDuration(timeSpent)
}
item.time_taken = timeTakenUtils(item)
})
} else {
// 刷新按菜品查看的数据
@@ -421,17 +432,41 @@ function updateOrderStatus() {
let timeSpent = formatTimeDiff(item.startOrderTime)
if (item.subStatus == 'READY_TO_SERVE' && isMoreThanSpecifiedMinutes(timeSpent, userStore.shopInfo.serveTime)) {
item.subStatus = 'TIMEOUT'
item.timeout_time = calculateTimeoutDuration(timeSpent)
}
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 && (tableList.length || tableData.list.length)) {
if (userStore.shopInfo.isServeTimeControl && (goodsTableData.length || tableData.list.length)) {
console.log('启动刷新');
// 只有开启的情况下调用
updateOrderStatus()
if (timer.value !== null) {
@@ -458,8 +493,8 @@ onMounted(async () => {
onUnmounted(() => {
clearInterval(timer.value)
this.timer = null
unsub.close()
timer.value = null
// unsub.close()
})
</script>
@@ -522,6 +557,20 @@ onUnmounted(() => {
.left {
display: flex;
gap: var(--padding);
.item {
&.active {
span {
font-weight: bold;
color: var(--el-color-primary);
}
}
span {
font-size: 16px;
color: #333;
}
}
}
.right {
@@ -644,6 +693,12 @@ onUnmounted(() => {
.goods_table_title {
padding: 10px 0 5px 0;
span {
font-size: 16px;
font-weight: bold;
color: #333;
}
}
}
}

View File

@@ -67,7 +67,7 @@ const formRef = ref(null)
const loading = ref(false)
const form = ref({
loginType: 0, // 登录类型 0:商户登录 1:员工登录
username: '',
username: userStore.account,
password: '',
staffUserName: '',
code: '',