From ac574335a9049a2bc928ab49d229286b8d7617f7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=B5=B5=E5=BF=A0=E6=9E=97?= <170083662@qq.com> Date: Mon, 6 Jul 2026 12:41:05 +0800 Subject: [PATCH] =?UTF-8?q?feat(shop):=20=E5=AE=9E=E7=8E=B0=E6=96=B0?= =?UTF-8?q?=E8=AE=A2=E5=8D=95=E5=A3=B0=E9=9F=B3=E5=92=8C=E8=AF=AD=E9=9F=B3?= =?UTF-8?q?=E6=8F=90=E9=86=92=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 新增 useOrderNotify 组合式函数,支持每60秒轮询检测新订单 - 采用 Web Audio API 播放叮声,Speech Synthesis API 进行语音播报 - shopOrder 页面添加提醒开关栏,包含开关、提示和测试按钮 - 开关默认关闭,需用户点击启用后启动声音和轮询功能 - 测试按钮仅在提醒开启时可用,用于验证提醒效果 - 完善样式,提升提醒栏的视觉效果和交互体验 - 统一更新相关环境变量,调整项目名称和接口地址 --- .env | 2 +- .env.development | 4 +- .env.production | 2 +- .workbuddy/memory/2026-07-06.md | 33 +++ src/views/shop/shopOrder/index.vue | 52 ++++- src/views/shop/shopOrder/useOrderNotify.ts | 241 +++++++++++++++++++++ 6 files changed, 328 insertions(+), 6 deletions(-) create mode 100644 .workbuddy/memory/2026-07-06.md create mode 100644 src/views/shop/shopOrder/useOrderNotify.ts diff --git a/.env b/.env index 677b4b9..e1e6782 100644 --- a/.env +++ b/.env @@ -1,2 +1,2 @@ -VITE_APP_NAME=塔吊 +VITE_APP_NAME=小程序商城 VITE_API_URL=/api diff --git a/.env.development b/.env.development index d44294c..af53814 100644 --- a/.env.development +++ b/.env.development @@ -1,3 +1,3 @@ VITE_APP_NAME=后台管理系统 -VITE_API_URL=http://127.0.0.1:9200/api -#VITE_API_URL=https://guilixu-api.websoft.top/api +#VITE_API_URL=http://127.0.0.1:9200/api +VITE_API_URL=https://shop-api.websoft.top/api diff --git a/.env.production b/.env.production index 7708095..aae0d29 100644 --- a/.env.production +++ b/.env.production @@ -1,2 +1,2 @@ VITE_APP_NAME=后台管理系统 -VITE_API_URL=https://guilixu-api.websoft.top/api +VITE_API_URL=https://shop-api.websoft.top/api diff --git a/.workbuddy/memory/2026-07-06.md b/.workbuddy/memory/2026-07-06.md new file mode 100644 index 0000000..a1f7632 --- /dev/null +++ b/.workbuddy/memory/2026-07-06.md @@ -0,0 +1,33 @@ +# 2026-07-06 工作日志 + +## 新订单声音/语音提醒功能 + +### 需求 +订单管理页面 `/shop/shopOrder` 实现新订单的语音提醒("您有一条新的订单")和"叮"声提醒。 + +### 背景 +- 订单页面原为纯 HTTP 请求获取数据(用户手动触发),无轮询、无 WebSocket +- 项目中无任何声音/音频系统 + +### 方案 +采用**轮询检测 + Web Audio API 叮声 + Speech Synthesis API 语音播报**,纯前端实现,零依赖。 + +### 实现 +1. 新建 `src/views/shop/shopOrder/useOrderNotify.ts` 组合式函数 + - 每 60 秒轮询 `pageShopOrder({ page: 1, limit: 1, type: 0 })` 查询最新订单 + - 比较最新 orderId,首次查询记录基准值不触发,之后检测到新 orderId 则触发提醒 + - Web Audio API 生成 880Hz 正弦波"叮"声(含攻击+指数衰减包络) + - Speech Synthesis API 播报"您有一条新的订单" + - 组件卸载时自动清理定时器(onBeforeUnmount) + +2. 修改 `src/views/shop/shopOrder/index.vue` + - 表格上方添加提醒开关栏(a-switch + tooltip + 测试按钮) + - 导入 InfoCircleOutlined 图标 + - 集成 useOrderNotify 组合式函数 + - 添加 .order-notify-bar 样式 + +### 注意事项 +- 浏览器策略要求音频播放需用户交互后才能触发,所以开关默认关闭 +- 开关由用户点击开启,此时初始化 AudioContext 并启动轮询 +- "测试提醒"按钮仅在开关开启后可用,方便用户验证声音效果 +- 轮询间隔可修改 useOrderNotify.ts 中的 POLL_INTERVAL 常量 diff --git a/src/views/shop/shopOrder/index.vue b/src/views/shop/shopOrder/index.vue index 9cfa982..becf541 100644 --- a/src/views/shop/shopOrder/index.vue +++ b/src/views/shop/shopOrder/index.vue @@ -10,6 +10,30 @@ /> +
+ + 新订单提醒 + + + + + + 测试提醒 + + +
@@ -315,7 +339,8 @@ CheckCircleOutlined, CloseCircleOutlined, RedoOutlined, - DeleteOutlined + DeleteOutlined, + InfoCircleOutlined } from '@ant-design/icons-vue'; import Search from './components/search.vue'; import { getPageTitle } from '@/utils/common'; @@ -333,6 +358,7 @@ import { updateUser } from '@/api/system/user'; import { getPayType } from '@/utils/shop'; import { message, Modal } from 'ant-design-vue'; + import { useOrderNotify } from './useOrderNotify'; // 表格实例 const tableRef = ref | null>(null); @@ -351,6 +377,9 @@ const loading = ref(true); // 激活的标签 const activeKey = ref('undelivered'); + + // ============ 新订单提醒 ============ + const { enabled: notifyEnabled, toggle: onNotifyToggle, testNotify: onTestNotify } = useOrderNotify(); // 表格数据源 const datasource: DatasourceFunction = ({ page, @@ -775,4 +804,23 @@ }; - + diff --git a/src/views/shop/shopOrder/useOrderNotify.ts b/src/views/shop/shopOrder/useOrderNotify.ts new file mode 100644 index 0000000..4d73ac1 --- /dev/null +++ b/src/views/shop/shopOrder/useOrderNotify.ts @@ -0,0 +1,241 @@ +import { ref, onMounted, onBeforeUnmount } from 'vue'; +import { pageShopOrder } from '@/api/shop/shopOrder'; +import type { ShopOrder } from '@/api/shop/shopOrder/model'; + +/** + * 新订单提醒 - 轮询检测 + 声音/语音播报 + * + * 原理: + * 1. 定时轮询 pageShopOrder(只查最新 1 条) + * 2. 与上次记录的 orderId 比较,不同则说明有新订单 + * 3. 播放"叮"声(Web Audio API) + 语音播报"您有一条新的订单"(Speech Synthesis API) + * + * 浏览器策略:音频播放需要用户交互后才允许。 + * 默认开启轮询,音频在用户首次交互后自动解锁。 + */ + +// ============ 配置 ============ + +/** 轮询间隔(毫秒) */ +const POLL_INTERVAL = 60_000; + +/** 叮声频率(Hz),880 = 高音叮 */ +const DING_FREQUENCY = 880; + +/** 叮声持续时间(秒) */ +const DING_DURATION = 0.3; + +/** 语音播报文案 */ +const SPEECH_TEXT = '您有一条新的订单'; + +// ============ 状态 ============ + +/** 是否启用提醒 */ +const enabled = ref(true); + +/** 是否正在轮询 */ +let polling = false; + +/** 定时器 ID */ +let timerId: ReturnType | null = null; + +/** 上次记录的最新订单 ID */ +let lastOrderId: number | undefined = undefined; + +/** 音频上下文(延迟创建,需用户交互后) */ +let audioCtx: AudioContext | null = null; + +/** 是否已初始化(首次查询记录基准值,不触发提醒) */ +let initialized = false; + +// ============ 声音播放 ============ + +/** + * 初始化 AudioContext(必须在用户交互后调用) + */ +function initAudioContext() { + if (!audioCtx) { + audioCtx = new (window.AudioContext || + (window as any).webkitAudioContext)(); + } + // 如果上下文被暂停(浏览器策略),尝试恢复 + if (audioCtx.state === 'suspended') { + audioCtx.resume(); + } +} + +/** + * 播放"叮"声 - 使用 Web Audio API 生成,无需音频文件 + */ +function playDingSound() { + if (!audioCtx) return; + + const now = audioCtx.currentTime; + + // 创建振荡器 + const oscillator = audioCtx.createOscillator(); + oscillator.type = 'sine'; + oscillator.frequency.setValueAtTime(DING_FREQUENCY, now); + + // 创建增益节点(控制音量包络) + const gainNode = audioCtx.createGain(); + // 起始音量 0 + gainNode.gain.setValueAtTime(0, now); + // 快速上升到 0.5(攻击阶段) + gainNode.gain.linearRampToValueAtTime(0.5, now + 0.01); + // 然后指数衰减到 0(释放阶段) + gainNode.gain.exponentialRampToValueAtTime(0.001, now + DING_DURATION); + + // 连接:振荡器 → 增益 → 输出 + oscillator.connect(gainNode); + gainNode.connect(audioCtx.destination); + + // 播放 + oscillator.start(now); + oscillator.stop(now + DING_DURATION); +} + +/** + * 语音播报 - 使用浏览器原生 Speech Synthesis API + */ +function speak(text: string) { + if (!('speechSynthesis' in window)) return; + + // 取消可能正在进行的播报 + window.speechSynthesis.cancel(); + + const utterance = new SpeechSynthesisUtterance(text); + utterance.lang = 'zh-CN'; + utterance.rate = 1; // 语速 + utterance.pitch = 1; // 音调 + utterance.volume = 1; // 音量 + + window.speechSynthesis.speak(utterance); +} + +/** + * 触发提醒:叮声 + 语音播报 + */ +function triggerNotify() { + playDingSound(); + speak(SPEECH_TEXT); +} + +// ============ 轮询逻辑 ============ + +/** + * 查询最新订单,检测是否有新订单 + */ +async function checkNewOrder() { + try { + const result = await pageShopOrder({ + page: 1, + limit: 1, + type: 0 + }); + + const latestOrder: ShopOrder | undefined = result?.list?.[0]; + + if (!latestOrder) { + // 没有订单数据,记录为空 + lastOrderId = undefined; + return; + } + + const currentOrderId = latestOrder.orderId; + + if (!initialized) { + // 首次查询,记录基准值,不触发提醒 + lastOrderId = currentOrderId; + initialized = true; + return; + } + + // 比较 orderId:如果变了且新 orderId 更大,说明有新订单 + if ( + currentOrderId !== lastOrderId && + (lastOrderId === undefined || (currentOrderId ?? 0) > lastOrderId) + ) { + // 有新订单! + triggerNotify(); + } + + lastOrderId = currentOrderId; + } catch (error) { + // 查询失败时静默处理,不影响下次轮询 + console.warn('[订单提醒] 轮询查询失败:', error); + } +} + +/** + * 启动轮询 + */ +function startPolling() { + if (polling) return; + polling = true; + initialized = false; // 重置初始化标记 + + // 立即查询一次(记录基准值) + checkNewOrder(); + + // 启动定时器 + timerId = setInterval(checkNewOrder, POLL_INTERVAL); +} + +/** + * 停止轮询 + */ +function stopPolling() { + polling = false; + if (timerId) { + clearInterval(timerId); + timerId = null; + } +} + +// ============ 对外接口 ============ + +/** + * 新订单提醒组合式函数 + * + * 用法: + * ```ts + * const { enabled, toggle } = useOrderNotify(); + * // 用户点击开关时调用 toggle(true/false) + * ``` + */ +export function useOrderNotify() { + // 组件卸载时自动清理 + onBeforeUnmount(() => { + stopPolling(); + }); + + /** + * 切换提醒开关 + * @param value 是否启用 + */ + const toggle = (value: boolean) => { + enabled.value = value; + if (value) { + // 用户交互触发,初始化音频上下文 + initAudioContext(); + startPolling(); + } else { + stopPolling(); + } + }; + + /** + * 测试提醒(用户点击测试按钮时调用) + */ + const testNotify = () => { + initAudioContext(); + triggerNotify(); + }; + + return { + enabled, + toggle, + testNotify + }; +}