diff --git a/apps/admin-web/src/assets/big-screen/celebrate-t1.mp3 b/apps/admin-web/src/assets/big-screen/celebrate-t1.mp3 new file mode 100644 index 0000000..8c7ef92 Binary files /dev/null and b/apps/admin-web/src/assets/big-screen/celebrate-t1.mp3 differ diff --git a/apps/admin-web/src/assets/big-screen/celebrate-t2.mp3 b/apps/admin-web/src/assets/big-screen/celebrate-t2.mp3 new file mode 100644 index 0000000..d38a9f7 Binary files /dev/null and b/apps/admin-web/src/assets/big-screen/celebrate-t2.mp3 differ diff --git a/apps/admin-web/src/assets/big-screen/celebrate-t3.mp3 b/apps/admin-web/src/assets/big-screen/celebrate-t3.mp3 new file mode 100644 index 0000000..6fdf981 Binary files /dev/null and b/apps/admin-web/src/assets/big-screen/celebrate-t3.mp3 differ diff --git a/apps/admin-web/src/pages/BigScreenPage.tsx b/apps/admin-web/src/pages/BigScreenPage.tsx index d85e78f..bb4214c 100644 --- a/apps/admin-web/src/pages/BigScreenPage.tsx +++ b/apps/admin-web/src/pages/BigScreenPage.tsx @@ -1,5 +1,8 @@ import { useCallback, useEffect, useMemo, useRef, useState, type CSSProperties } from 'react'; import { request } from '../lib/api'; +import sfxTier1 from '../assets/big-screen/celebrate-t1.mp3'; +import sfxTier2 from '../assets/big-screen/celebrate-t2.mp3'; +import sfxTier3 from '../assets/big-screen/celebrate-t3.mp3'; export type BigScreenOrder = { id: string; @@ -21,6 +24,57 @@ const MIN_SCROLL_ROWS = 10; const FX_DURATION_MS = 10_000; const WEEKDAYS = ['星期日', '星期一', '星期二', '星期三', '星期四', '星期五', '星期六']; +const CELEBRATE_SFX: Record = { + 1: sfxTier1, + 2: sfxTier2, + 3: sfxTier3, +}; + +/** 浏览器自动播放策略:需用户手势后才能出声 */ +let audioUnlocked = false; +const sharedAudio = typeof Audio !== 'undefined' ? new Audio() : null; + +function unlockCelebrateAudio() { + if (audioUnlocked || !sharedAudio) return; + sharedAudio.muted = true; + sharedAudio.src = CELEBRATE_SFX[1]; + void sharedAudio + .play() + .then(() => { + sharedAudio.pause(); + sharedAudio.currentTime = 0; + sharedAudio.muted = false; + audioUnlocked = true; + }) + .catch(() => { + /* 等待下次手势 */ + }); +} + +function playCelebrateSfx(tier: AmountTier): () => void { + if (!sharedAudio) return () => undefined; + try { + sharedAudio.pause(); + sharedAudio.currentTime = 0; + sharedAudio.src = CELEBRATE_SFX[tier]; + sharedAudio.volume = tier === 3 ? 0.85 : tier === 2 ? 0.75 : 0.65; + sharedAudio.muted = false; + void sharedAudio.play().catch(() => { + /* 未解锁时静默失败 */ + }); + } catch { + /* ignore */ + } + return () => { + try { + sharedAudio.pause(); + sharedAudio.currentTime = 0; + } catch { + /* ignore */ + } + }; +} + function pad(n: number) { return String(n).padStart(2, '0'); } @@ -184,6 +238,11 @@ function CelebrateFx({ order, onDone }: { order: BigScreenOrder; onDone: () => v return () => window.clearTimeout(timer); }, [order.id]); + useEffect(() => { + const stop = playCelebrateSfx(tier); + return stop; + }, [order.id, tier]); + useEffect(() => { if (tier !== 3) return; const start = performance.now(); @@ -357,6 +416,17 @@ export default function BigScreenPage() { return () => clearInterval(id); }, [fetchData]); + useEffect(() => { + unlockCelebrateAudio(); + const unlock = () => unlockCelebrateAudio(); + window.addEventListener('pointerdown', unlock, { once: true }); + window.addEventListener('keydown', unlock, { once: true }); + return () => { + window.removeEventListener('pointerdown', unlock); + window.removeEventListener('keydown', unlock); + }; + }, []); + useEffect(() => { const html = document.documentElement; const prevHtml = html.style.overflow; diff --git a/apps/admin-web/src/vite-env.d.ts b/apps/admin-web/src/vite-env.d.ts new file mode 100644 index 0000000..5ec1c50 --- /dev/null +++ b/apps/admin-web/src/vite-env.d.ts @@ -0,0 +1,6 @@ +/// + +declare module '*.mp3' { + const src: string; + export default src; +}