feat(admin): v3.5.12 大屏BGM、中文展示、分类删除修复与侧栏顺序

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-08-26 10:54:19 +08:00
parent 3afbdfe6b9
commit 58a8b47532
23 changed files with 773 additions and 111 deletions
@@ -1,5 +1,6 @@
import { useCallback, useEffect, useMemo, useRef, useState, type CSSProperties } from 'react';
import { request } from '../lib/api';
import bgmSrc from '../assets/big-screen/dukang-bgm.mp3';
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';
@@ -33,6 +34,50 @@ const CELEBRATE_SFX: Record<AmountTier, string> = {
/** 浏览器自动播放策略:需用户手势后才能出声 */
let audioUnlocked = false;
const sharedAudio = typeof Audio !== 'undefined' ? new Audio() : null;
const BGM_VOLUME = 0.32;
const BGM_DUCK_VOLUME = 0.08;
let bgmAudio: HTMLAudioElement | null = null;
let bgmDucked = false;
function getBgmAudio(): HTMLAudioElement | null {
if (typeof Audio === 'undefined') return null;
if (!bgmAudio) {
bgmAudio = new Audio(bgmSrc);
bgmAudio.loop = true;
bgmAudio.preload = 'auto';
bgmAudio.volume = BGM_VOLUME;
}
return bgmAudio;
}
function applyBgmVolume() {
if (!bgmAudio) return;
bgmAudio.volume = bgmDucked ? BGM_DUCK_VOLUME : BGM_VOLUME;
}
function duckBgm() {
bgmDucked = true;
applyBgmVolume();
}
function unduckBgm() {
bgmDucked = false;
applyBgmVolume();
}
function setBgmPlaying(on: boolean): Promise<boolean> {
const audio = getBgmAudio();
if (!audio) return Promise.resolve(false);
applyBgmVolume();
if (!on) {
audio.pause();
return Promise.resolve(false);
}
return audio
.play()
.then(() => true)
.catch(() => false);
}
function unlockCelebrateAudio() {
if (audioUnlocked || !sharedAudio) return;
@@ -53,6 +98,7 @@ function unlockCelebrateAudio() {
function playCelebrateSfx(tier: AmountTier): () => void {
if (!sharedAudio) return () => undefined;
duckBgm();
try {
sharedAudio.pause();
sharedAudio.currentTime = 0;
@@ -72,6 +118,7 @@ function playCelebrateSfx(tier: AmountTier): () => void {
} catch {
/* ignore */
}
unduckBgm();
};
}
@@ -341,10 +388,40 @@ function CelebrateFx({ order, onDone }: { order: BigScreenOrder; onDone: () => v
);
}
function BgmToggleButton({ on, onToggle }: { on: boolean; onToggle: () => void }) {
return (
<button
type="button"
className={`big-screen-bgm${on ? ' is-on' : ''}`}
onClick={onToggle}
aria-pressed={on}
aria-label={on ? '暂停背景音乐' : '播放背景音乐'}
>
<span className="big-screen-bgm-icon" aria-hidden>
{on ? (
<svg viewBox="0 0 24 24" width="16" height="16">
<path
fill="currentColor"
d="M9 18V6h2v12H9zm4 0V6h2v12h-2z"
/>
</svg>
) : (
<svg viewBox="0 0 24 24" width="16" height="16">
<path fill="currentColor" d="M8 5.14v13.72L19.06 12 8 5.14z" />
</svg>
)}
</span>
{on ? 'BGM 播放中' : '播放 BGM'}
</button>
);
}
export default function BigScreenPage() {
const [items, setItems] = useState<BigScreenOrder[]>([]);
const [loadError, setLoadError] = useState('');
const [paused, setPaused] = useState(false);
const [bgmOn, setBgmOn] = useState(false);
const bgmWantedRef = useRef(false);
const [celebrate, setCelebrate] = useState<BigScreenOrder | null>(null);
const seenIdsRef = useRef<Set<string> | null>(null);
const celebratedIdsRef = useRef<Set<string>>(new Set());
@@ -416,6 +493,22 @@ export default function BigScreenPage() {
return () => clearInterval(id);
}, [fetchData]);
const toggleBgm = useCallback(() => {
unlockCelebrateAudio();
const next = !bgmWantedRef.current;
bgmWantedRef.current = next;
if (!next) {
void setBgmPlaying(false);
setBgmOn(false);
return;
}
setBgmOn(true);
void setBgmPlaying(true).then((playing) => {
if (!bgmWantedRef.current) return;
setBgmOn(playing);
});
}, []);
useEffect(() => {
unlockCelebrateAudio();
const unlock = () => unlockCelebrateAudio();
@@ -424,6 +517,8 @@ export default function BigScreenPage() {
return () => {
window.removeEventListener('pointerdown', unlock);
window.removeEventListener('keydown', unlock);
bgmWantedRef.current = false;
void setBgmPlaying(false);
};
}, []);
@@ -480,6 +575,7 @@ export default function BigScreenPage() {
<span className="big-screen-subtitle">· </span>
</div>
<div className="big-screen-live-wrap">
<BgmToggleButton on={bgmOn} onToggle={toggleBgm} />
<span className="big-screen-live-label"></span>
<span className="big-screen-live">
<span className="big-screen-live-dot" />