feat(shop): 实现新订单声音和语音提醒功能

- 新增 useOrderNotify 组合式函数,支持每60秒轮询检测新订单
- 采用 Web Audio API 播放叮声,Speech Synthesis API 进行语音播报
- shopOrder 页面添加提醒开关栏,包含开关、提示和测试按钮
- 开关默认关闭,需用户点击启用后启动声音和轮询功能
- 测试按钮仅在提醒开启时可用,用于验证提醒效果
- 完善样式,提升提醒栏的视觉效果和交互体验
- 统一更新相关环境变量,调整项目名称和接口地址
This commit is contained in:
2026-07-06 12:43:04 +08:00
parent ac574335a9
commit f0b0280c4f
2 changed files with 51 additions and 5 deletions

View File

@@ -27,7 +27,8 @@
- 添加 .order-notify-bar 样式 - 添加 .order-notify-bar 样式
### 注意事项 ### 注意事项
- 浏览器策略要求音频播放需用户交互后才能触发,所以开关默认关闭 - 开关**默认开启**,进入订单页面即自动开始轮询
- 开关由用户点击开启,此时初始化 AudioContext 并启动轮询 - 浏览器策略要求音频播放需用户交互后才能触发,因此注册了 document click/keydown 一次性监听器,用户首次点击或按键时自动解锁音频
- 用户未交互前检测到新订单时,语音和叮声可能静默失败,交互后即恢复正常
- "测试提醒"按钮仅在开关开启后可用,方便用户验证声音效果 - "测试提醒"按钮仅在开关开启后可用,方便用户验证声音效果
- 轮询间隔可修改 useOrderNotify.ts 中的 POLL_INTERVAL 常量 - 轮询间隔可修改 useOrderNotify.ts 中的 POLL_INTERVAL 常量

View File

@@ -48,15 +48,23 @@ let audioCtx: AudioContext | null = null;
/** 是否已初始化(首次查询记录基准值,不触发提醒) */ /** 是否已初始化(首次查询记录基准值,不触发提醒) */
let initialized = false; let initialized = false;
/** 用户交互解锁音频的监听器(一次性) */
let unlockHandler: (() => void) | null = null;
// ============ 声音播放 ============ // ============ 声音播放 ============
/** /**
* 初始化 AudioContext必须在用户交互后调用 * 初始化 AudioContext必须在用户交互后调用才能生效
*/ */
function initAudioContext() { function initAudioContext() {
if (!audioCtx) { if (!audioCtx) {
audioCtx = new (window.AudioContext || try {
(window as any).webkitAudioContext)(); audioCtx = new (window.AudioContext ||
(window as any).webkitAudioContext)();
} catch (e) {
console.warn('[订单提醒] AudioContext 创建失败:', e);
return;
}
} }
// 如果上下文被暂停(浏览器策略),尝试恢复 // 如果上下文被暂停(浏览器策略),尝试恢复
if (audioCtx.state === 'suspended') { 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 生成,无需音频文件 * 播放"叮"声 - 使用 Web Audio API 生成,无需音频文件
*/ */
@@ -117,6 +146,8 @@ function speak(text: string) {
* 触发提醒:叮声 + 语音播报 * 触发提醒:叮声 + 语音播报
*/ */
function triggerNotify() { function triggerNotify() {
// 尝试初始化音频上下文(如果已解锁则生效,未解锁则静默失败)
initAudioContext();
playDingSound(); playDingSound();
speak(SPEECH_TEXT); speak(SPEECH_TEXT);
} }
@@ -205,9 +236,23 @@ function stopPolling() {
* ``` * ```
*/ */
export function useOrderNotify() { export function useOrderNotify() {
// 组件挂载时自动启动(默认开启)
onMounted(() => {
// 注册用户交互解锁音频监听器
registerAudioUnlock();
// 自动开始轮询
startPolling();
});
// 组件卸载时自动清理 // 组件卸载时自动清理
onBeforeUnmount(() => { onBeforeUnmount(() => {
stopPolling(); stopPolling();
// 清理交互监听器
if (unlockHandler) {
document.removeEventListener('click', unlockHandler);
document.removeEventListener('keydown', unlockHandler);
unlockHandler = null;
}
}); });
/** /**