From aef417409a205c94223eb8049ed10196f7427200 Mon Sep 17 00:00:00 2001 From: Kroytz Date: Wed, 8 Jul 2026 21:18:21 +0800 Subject: [PATCH] =?UTF-8?q?=E5=B7=A5=E4=BD=9C=E5=8F=B0:=20=E6=96=B0?= =?UTF-8?q?=E8=AE=A2=E5=8D=95=E6=8F=90=E9=86=92?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- yshop-drink-boot3/sql/yixiang-drink-open.sql | 3 + .../src/views/mall/order/storeOrder/work.vue | 119 +++++++++++++++++- 2 files changed, 121 insertions(+), 1 deletion(-) diff --git a/yshop-drink-boot3/sql/yixiang-drink-open.sql b/yshop-drink-boot3/sql/yixiang-drink-open.sql index b9f7b4f..1049777 100644 --- a/yshop-drink-boot3/sql/yixiang-drink-open.sql +++ b/yshop-drink-boot3/sql/yixiang-drink-open.sql @@ -4549,6 +4549,9 @@ INSERT INTO `infra_config` VALUES (7, 'url', 2, 'MySQL 监控的地址', 'url.dr INSERT INTO `infra_config` VALUES (8, 'url', 2, 'SkyWalking 监控的地址', 'url.skywalking', '', b'1', '', '1', '2023-04-07 13:41:16', '1', '2023-04-07 14:57:03', b'0'); INSERT INTO `infra_config` VALUES (9, 'url', 2, 'Spring Boot Admin 监控的地址', 'url.spring-boot-admin', '', b'1', '', '1', '2023-04-07 13:41:16', '1', '2023-04-07 14:52:07', b'0'); INSERT INTO `infra_config` VALUES (10, 'url', 2, 'Swagger 接口文档的地址', 'url.swagger', '', b'1', '', '1', '2023-04-07 13:41:16', '1', '2023-04-07 14:59:00', b'0'); +INSERT INTO `infra_config` VALUES (11, 'order', 2, '订单工作台-轮询间隔', 'order.work.poll.interval', '10000', b'1', '订单工作台自动刷新轮询间隔(毫秒),范围 3000-300000,默认 10000', '1', '2026-07-08 00:00:00', '1', '2026-07-08 00:00:00', b'0'); +INSERT INTO `infra_config` VALUES (12, 'order', 2, '订单工作台-新订单通知开关', 'order.work.notify.enabled', 'true', b'1', '订单工作台来新订单时是否通知(true/false),默认 true', '1', '2026-07-08 00:00:00', '1', '2026-07-08 00:00:00', b'0'); +INSERT INTO `infra_config` VALUES (13, 'order', 2, '订单工作台-通知声音开关', 'order.work.notify.sound', 'true', b'1', '订单工作台新订单通知是否播放声音(true/false),默认 true', '1', '2026-07-08 00:00:00', '1', '2026-07-08 00:00:00', b'0'); COMMIT; -- ---------------------------- diff --git a/yshop-drink-vue3/src/views/mall/order/storeOrder/work.vue b/yshop-drink-vue3/src/views/mall/order/storeOrder/work.vue index 0fe4301..572fadf 100644 --- a/yshop-drink-vue3/src/views/mall/order/storeOrder/work.vue +++ b/yshop-drink-vue3/src/views/mall/order/storeOrder/work.vue @@ -109,6 +109,7 @@ import StoreOrderRemark from './StoreOrderRemark.vue' import OrderDetail from './OrderDetail.vue' import OrderRecord from './OrderRecord.vue' import type { TabsPaneContext } from 'element-plus' +import { ElNotification } from 'element-plus' import { formatDate } from '@/utils/formatTime' const message = useMessage() // 消息弹窗 const { t } = useI18n() // 国际化 @@ -140,14 +141,115 @@ const handleClick = (tab: TabsPaneContext, event: Event) => { } +/** 新订单通知(可开关,由 infra 配置 order.work.notify.* 控制) */ +const NOTIFY_ENABLED_KEY = 'order.work.notify.enabled' +const NOTIFY_SOUND_KEY = 'order.work.notify.sound' +let notifyEnabled = true // 通知总开关,默认开 +let soundEnabled = true // 声音开关,默认开 +let lastMaxOrderId: number | null = null // 上次轮询的最大订单 id,用于判断新订单 +const newOrderCount = ref(0) // 未读新订单数(用于标题提示) +const originalTitle = typeof document !== 'undefined' ? document.title : '' + +/** 读取通知配置 */ +const loadNotifyConfig = async () => { + try { + const [enabled, sound] = await Promise.all([ + ConfigApi.getConfigKey(NOTIFY_ENABLED_KEY), + ConfigApi.getConfigKey(NOTIFY_SOUND_KEY) + ]) + // 默认 true,仅显式 'false' 才关闭 + notifyEnabled = enabled !== 'false' + soundEnabled = sound !== 'false' + } catch {} +} + +/** 更新浏览器标题显示未读数 */ +const updateTitle = () => { + if (!document) return + document.title = newOrderCount.value > 0 + ? `(${newOrderCount.value}) 新订单 - ${originalTitle}` + : originalTitle +} + +/** 播放提示音(Web Audio API 合成,无需音频文件) */ +let audioCtx: AudioContext | null = null +const playBeep = () => { + try { + const Ctx = window.AudioContext || (window as any).webkitAudioContext + if (!Ctx) return + if (!audioCtx) audioCtx = new Ctx() + // 连响两声,更易引起注意 + const beepAt = (startAt: number) => { + const osc = audioCtx!.createOscillator() + const gain = audioCtx!.createGain() + osc.connect(gain) + gain.connect(audioCtx!.destination) + osc.type = 'sine' + osc.frequency.value = 880 + gain.gain.setValueAtTime(0.3, audioCtx!.currentTime + startAt) + gain.gain.exponentialRampToValueAtTime(0.01, audioCtx!.currentTime + startAt + 0.3) + osc.start(audioCtx!.currentTime + startAt) + osc.stop(audioCtx!.currentTime + startAt + 0.3) + } + beepAt(0) + beepAt(0.35) + } catch {} +} + +/** 系统通知(页面隐藏时使用,需已授权) */ +const notifySystem = (count: number) => { + if (!('Notification' in window)) return + if (Notification.permission !== 'granted') return + new Notification('新订单提醒', { body: `收到 ${count} 个新订单,请及时处理` }) +} + +/** 首次点击时请求桌面通知授权(浏览器要求用户手势,故挂在 pointerdown 上,一次性) */ +const requestNotifyPermissionOnce = () => { + if (!('Notification' in window)) return + if (Notification.permission === 'default') { + Notification.requestPermission() + } +} + +/** 触发新订单通知 */ +const notifyNewOrders = (count: number) => { + newOrderCount.value += count + updateTitle() + if (!notifyEnabled) return + if (soundEnabled) playBeep() + if (document.hidden) { + notifySystem(count) + } else { + ElNotification({ + title: '新订单提醒', + message: `收到 ${count} 个新订单,请及时处理`, + type: 'success', + duration: 5000 + }) + } +} + +/** 检测新订单:对比本次与上次的订单 id */ +const checkNewOrders = (orders: any[]) => { + if (!orders || orders.length === 0) return + const ids = orders.map((o) => Number(o.id)) + const currentMaxId = Math.max(...ids) + if (lastMaxOrderId !== null && currentMaxId > lastMaxOrderId) { + const newCount = ids.filter((id) => id > lastMaxOrderId!).length + if (newCount > 0) notifyNewOrders(newCount) + } + lastMaxOrderId = currentMaxId +} + + /** 查询列表 */ const getList = async () => { loading.value = true try { const data = await StoreOrderApi.getStoreOrderPage(queryParams) list.value = data.list - //console.log("aa:",list.value) total.value = data.total + checkNewOrders(data.list) } finally { loading.value = false } @@ -260,6 +362,12 @@ const handleVisibilityChange = async () => { } else { getList() await loadPollInterval() + await loadNotifyConfig() + // 回到页面,清零未读新订单计数 + if (newOrderCount.value > 0) { + newOrderCount.value = 0 + updateTitle() + } startPolling() } } @@ -268,13 +376,22 @@ const handleVisibilityChange = async () => { onMounted(async () => { getList() await loadPollInterval() + await loadNotifyConfig() startPolling() document.addEventListener('visibilitychange', handleVisibilityChange) + // 首次点击请求桌面通知授权(浏览器要求用户手势) + document.addEventListener('pointerdown', requestNotifyPermissionOnce, { once: true }) }) onUnmounted(() => { stopPolling() document.removeEventListener('visibilitychange', handleVisibilityChange) + document.removeEventListener('pointerdown', requestNotifyPermissionOnce) + // 离开页面恢复标题 + if (newOrderCount.value > 0) { + newOrderCount.value = 0 + document.title = originalTitle + } })