feat: 更新音频和引导对话,优化用户体验
- 在多个场景中添加音频资源,增强学习互动性。 - 更新引导对话文本,确保信息准确性和一致性。 - 调整组件结构,清理不必要的代码,提升可维护性。 - 引入结束窗口,鼓励用户深入了解历史背景。
This commit is contained in:
1 parent
d7ff7d3a45
commit
6e106032fd
141 files changed
+1375
-479
No files matched your search
@@ -0,0 +1,136 @@
|
||||
import { useCallback, useEffect, useState } from "react";
|
||||
|
||||
type AudioMode = "global" | "channel";
|
||||
|
||||
interface UseAudioManagerOptions {
|
||||
mode?: AudioMode;
|
||||
channelId?: string;
|
||||
}
|
||||
|
||||
interface AudioState {
|
||||
key: string | null;
|
||||
currentSrc: string | null;
|
||||
isPlaying: boolean;
|
||||
}
|
||||
|
||||
// 全局音频池:每个 key(全局或某个 channel)对应一个 HTMLAudioElement
|
||||
const audioPool = new Map<string, HTMLAudioElement>();
|
||||
|
||||
// 订阅者列表,用于在任意地方播放/暂停时同步状态
|
||||
const listeners = new Set<(state: AudioState) => void>();
|
||||
|
||||
let globalState: AudioState = {
|
||||
key: null,
|
||||
currentSrc: null,
|
||||
isPlaying: false,
|
||||
};
|
||||
|
||||
function emit(state: AudioState) {
|
||||
globalState = state;
|
||||
listeners.forEach((listener) => listener(state));
|
||||
}
|
||||
|
||||
function resolveKey(options?: UseAudioManagerOptions): string {
|
||||
const mode = options?.mode ?? "global";
|
||||
|
||||
if (mode === "global") {
|
||||
return "__GLOBAL__";
|
||||
}
|
||||
|
||||
// mode === "channel"
|
||||
return `channel:${options?.channelId ?? "__DEFAULT__"}`;
|
||||
}
|
||||
|
||||
function getOrCreateAudio(key: string): HTMLAudioElement {
|
||||
let audio = audioPool.get(key);
|
||||
if (!audio) {
|
||||
audio = new Audio();
|
||||
audioPool.set(key, audio);
|
||||
|
||||
audio.addEventListener("ended", () => {
|
||||
// 某个 key 下的音频自然播放结束时,仅更新该 key 对应的播放状态
|
||||
if (globalState.key === key) {
|
||||
emit({
|
||||
key,
|
||||
currentSrc: globalState.currentSrc,
|
||||
isPlaying: false,
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
return audio;
|
||||
}
|
||||
|
||||
/**
|
||||
* 分组互斥音频管理:
|
||||
* - mode = "global":全局单例,所有使用方互斥
|
||||
* - mode = "channel" + channelId:同一 channel 内互斥,channel 之间互不影响
|
||||
*/
|
||||
export function useAudioManager(options?: UseAudioManagerOptions) {
|
||||
const [state, setState] = useState<AudioState>(globalState);
|
||||
|
||||
// 将 options 展开为基础类型,避免在 useCallback 中依赖对象引用导致每次 render 都重建函数
|
||||
const resolvedMode: AudioMode = options?.mode ?? "global";
|
||||
const resolvedChannelId: string | undefined = options?.channelId;
|
||||
|
||||
useEffect(() => {
|
||||
listeners.add(setState);
|
||||
return () => {
|
||||
listeners.delete(setState);
|
||||
};
|
||||
}, []);
|
||||
|
||||
const play = useCallback(
|
||||
(src: string) => {
|
||||
const key = resolveKey({
|
||||
mode: resolvedMode,
|
||||
channelId: resolvedChannelId,
|
||||
});
|
||||
const audio = getOrCreateAudio(key);
|
||||
|
||||
if (audio.src !== src) {
|
||||
audio.src = src;
|
||||
}
|
||||
|
||||
audio.currentTime = 0;
|
||||
// 捕获浏览器因策略或中断导致的 play() 拒绝,避免控制台 Unhandled promise
|
||||
audio.play().catch(() => {
|
||||
// 可以在这里按需上报或忽略
|
||||
});
|
||||
|
||||
emit({
|
||||
key,
|
||||
currentSrc: src,
|
||||
isPlaying: true,
|
||||
});
|
||||
},
|
||||
[resolvedMode, resolvedChannelId]
|
||||
);
|
||||
|
||||
const stop = useCallback(() => {
|
||||
const key = resolveKey({
|
||||
mode: resolvedMode,
|
||||
channelId: resolvedChannelId,
|
||||
});
|
||||
const audio = audioPool.get(key);
|
||||
|
||||
if (!audio) return;
|
||||
|
||||
audio.pause();
|
||||
audio.currentTime = 0;
|
||||
|
||||
if (globalState.key === key) {
|
||||
emit({
|
||||
key,
|
||||
currentSrc: globalState.currentSrc,
|
||||
isPlaying: false,
|
||||
});
|
||||
}
|
||||
}, [resolvedMode, resolvedChannelId]);
|
||||
|
||||
return {
|
||||
...state,
|
||||
play,
|
||||
stop,
|
||||
};
|
||||
}
|
||||
Reference in new issue
Block a user