From f0b0280c4f0f1607abedda771d11eebf3d2c9138 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:43:04 +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 页面添加提醒开关栏,包含开关、提示和测试按钮 - 开关默认关闭,需用户点击启用后启动声音和轮询功能 - 测试按钮仅在提醒开启时可用,用于验证提醒效果 - 完善样式,提升提醒栏的视觉效果和交互体验 - 统一更新相关环境变量,调整项目名称和接口地址 --- .workbuddy/memory/2026-07-06.md | 5 ++- src/views/shop/shopOrder/useOrderNotify.ts | 51 ++++++++++++++++++++-- 2 files changed, 51 insertions(+), 5 deletions(-) diff --git a/.workbuddy/memory/2026-07-06.md b/.workbuddy/memory/2026-07-06.md index a1f7632..95f9b35 100644 --- a/.workbuddy/memory/2026-07-06.md +++ b/.workbuddy/memory/2026-07-06.md @@ -27,7 +27,8 @@ - 添加 .order-notify-bar 样式 ### 注意事项 -- 浏览器策略要求音频播放需用户交互后才能触发,所以开关默认关闭 -- 开关由用户点击开启,此时初始化 AudioContext 并启动轮询 +- 开关**默认开启**,进入订单页面即自动开始轮询 +- 浏览器策略要求音频播放需用户交互后才能触发,因此注册了 document click/keydown 一次性监听器,用户首次点击或按键时自动解锁音频 +- 用户未交互前检测到新订单时,语音和叮声可能静默失败,交互后即恢复正常 - "测试提醒"按钮仅在开关开启后可用,方便用户验证声音效果 - 轮询间隔可修改 useOrderNotify.ts 中的 POLL_INTERVAL 常量 diff --git a/src/views/shop/shopOrder/useOrderNotify.ts b/src/views/shop/shopOrder/useOrderNotify.ts index 4d73ac1..27cfb17 100644 --- a/src/views/shop/shopOrder/useOrderNotify.ts +++ b/src/views/shop/shopOrder/useOrderNotify.ts @@ -48,15 +48,23 @@ let audioCtx: AudioContext | null = null; /** 是否已初始化(首次查询记录基准值,不触发提醒) */ let initialized = false; +/** 用户交互解锁音频的监听器(一次性) */ +let unlockHandler: (() => void) | null = null; + // ============ 声音播放 ============ /** - * 初始化 AudioContext(必须在用户交互后调用) + * 初始化 AudioContext(必须在用户交互后调用才能生效) */ function initAudioContext() { if (!audioCtx) { - audioCtx = new (window.AudioContext || - (window as any).webkitAudioContext)(); + try { + audioCtx = new (window.AudioContext || + (window as any).webkitAudioContext)(); + } catch (e) { + console.warn('[订单提醒] AudioContext 创建失败:', e); + return; + } } // 如果上下文被暂停(浏览器策略),尝试恢复 if (audioCtx.state === 'suspended') { @@ -64,6 +72,27 @@ function initAudioContext() { } } +/** + * 注册用户交互监听器,首次交互时解锁音频 + * 浏览器策略:音频播放需要用户交互后才允许,注册一次性监听器自动解锁 + */ +function registerAudioUnlock() { + if (unlockHandler) return; + + unlockHandler = () => { + initAudioContext(); + // 解锁后移除监听器 + if (unlockHandler) { + document.removeEventListener('click', unlockHandler); + document.removeEventListener('keydown', unlockHandler); + unlockHandler = null; + } + }; + + document.addEventListener('click', unlockHandler, { once: true }); + document.addEventListener('keydown', unlockHandler, { once: true }); +} + /** * 播放"叮"声 - 使用 Web Audio API 生成,无需音频文件 */ @@ -117,6 +146,8 @@ function speak(text: string) { * 触发提醒:叮声 + 语音播报 */ function triggerNotify() { + // 尝试初始化音频上下文(如果已解锁则生效,未解锁则静默失败) + initAudioContext(); playDingSound(); speak(SPEECH_TEXT); } @@ -205,9 +236,23 @@ function stopPolling() { * ``` */ export function useOrderNotify() { + // 组件挂载时自动启动(默认开启) + onMounted(() => { + // 注册用户交互解锁音频监听器 + registerAudioUnlock(); + // 自动开始轮询 + startPolling(); + }); + // 组件卸载时自动清理 onBeforeUnmount(() => { stopPolling(); + // 清理交互监听器 + if (unlockHandler) { + document.removeEventListener('click', unlockHandler); + document.removeEventListener('keydown', unlockHandler); + unlockHandler = null; + } }); /**