feat: 移除不再使用的配置文件和更新组件,提升代码整洁性

- 删除 .claude/settings.local.json 配置文件,清理不必要的代码。
- 更新多个组件以引入音频资源,增强用户互动体验。
- 调整引导对话文本,确保信息准确性和一致性。
- 更新图像资源,提升视觉内容的准确性和一致性。
This commit is contained in:
zhongyidian committed 2026-01-20 18:06:55 +08:00
1 parent cf64e7ec50
commit 8ba36a4a1a
132 files changed
+546 -454

No files matched your search

@@ -1,14 +1,19 @@
import { useEffect, useRef, useState, useCallback } from "react";
import { useEffect } from "react";
import { cn, getBackgroundImage } from "@/lib/utils";
import { AnimatePresence, motion } from "framer-motion";
import { useAudioManager } from "@/hooks/useAudioManager";
export interface ExhibitDescriptionNarrationProps {
/** 描述文本 */
text?: string;
/** 是否可见 */
visible?: boolean;
/** 是否自动播放语音 */
/** 是否自动播放音频(默认 true) */
autoPlay?: boolean;
/** 音频文件地址 */
audioSrc?: string;
/** 音频分组 id,相同 channel 内互斥,不同 channel 可同时播放;不传则全局互斥 */
audioChannelId?: string;
/** 额外的自定义类 */
className?: string;
}
@@ -16,114 +21,41 @@ export interface ExhibitDescriptionNarrationProps {
/**
* 展品描述播报组件
* - 显示展品描述文本
* - 支持文本转语音(TTS)播报
* - 提供播放/暂停控制
* - 支持音频文件播放(通过 useAudioManager)
* - 提供播放/停止控制
*/
const ExhibitDescriptionNarration = ({
text,
visible = true,
autoPlay = false,
autoPlay = true,
audioSrc,
audioChannelId,
className,
}: ExhibitDescriptionNarrationProps) => {
const [isPlaying, setIsPlaying] = useState(false);
const [isPaused, setIsPaused] = useState(false);
const utteranceRef = useRef<SpeechSynthesisUtterance | null>(null);
const synthRef = useRef<typeof window.speechSynthesis | null>(null);
// 音频管理:默认全局互斥,如果传了 audioChannelId 则按 channel 互斥
const { currentSrc, isPlaying, play, stop } = useAudioManager(
audioChannelId
? { mode: "channel", channelId: audioChannelId }
: { mode: "global" }
);
// 初始化语音合成
// 当前组件对应的音频是否正在播放
const isCurrentAudioPlaying =
audioSrc && currentSrc === audioSrc && isPlaying;
// 自动播放音频
useEffect(() => {
if (typeof window !== "undefined" && "speechSynthesis" in window) {
synthRef.current = window.speechSynthesis;
if (visible && autoPlay && audioSrc) {
play(audioSrc);
}
}, []);
}, [visible, autoPlay, audioSrc, play]);
// 播放语音
const playSpeech = useCallback(() => {
if (!text || !synthRef.current) return;
// 如果已有正在播放的语音,先停止
if (utteranceRef.current) {
synthRef.current.cancel();
}
const utterance = new SpeechSynthesisUtterance(text);
utterance.lang = "zh-CN";
utterance.rate = 0.9; // 语速稍慢
utterance.pitch = 1; // 音调
utterance.volume = 1; // 音量
utterance.onstart = () => {
setIsPlaying(true);
setIsPaused(false);
};
utterance.onend = () => {
setIsPlaying(false);
setIsPaused(false);
utteranceRef.current = null;
};
utterance.onerror = () => {
setIsPlaying(false);
setIsPaused(false);
utteranceRef.current = null;
};
utteranceRef.current = utterance;
synthRef.current.speak(utterance);
}, [text]);
// 暂停语音
const pauseSpeech = useCallback(() => {
if (synthRef.current && isPlaying) {
synthRef.current.pause();
setIsPaused(true);
}
}, [isPlaying]);
// 继续播放语音
const resumeSpeech = useCallback(() => {
if (synthRef.current && isPaused) {
synthRef.current.resume();
setIsPaused(false);
}
}, [isPaused]);
// 停止语音
const stopSpeech = useCallback(() => {
if (synthRef.current) {
synthRef.current.cancel();
setIsPlaying(false);
setIsPaused(false);
utteranceRef.current = null;
}
}, []);
// 自动播放
// 组件不可见时停止音频(仅当当前播放的是该组件的 audioSrc)
useEffect(() => {
if (visible && autoPlay && text && synthRef.current) {
// 延迟一下确保组件已渲染
const timer = setTimeout(() => {
playSpeech();
}, 300);
return () => {
clearTimeout(timer);
stopSpeech();
};
if (!visible && audioSrc && currentSrc === audioSrc) {
stop();
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [visible, autoPlay, text]);
// 组件卸载或不可见时停止语音
useEffect(() => {
if (!visible) {
stopSpeech();
}
return () => {
stopSpeech();
};
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [visible]);
}, [visible, audioSrc, currentSrc, stop]);
if (!text) return null;
@@ -166,12 +98,12 @@ const ExhibitDescriptionNarration = ({
{text}
</div>
{/* 控制按钮 */}
{synthRef.current && (
{/* 控制按钮 - 仅在提供了 audioSrc 时显示 */}
{audioSrc && (
<div className="flex items-center justify-end gap-2 flex-shrink-0">
{!isPlaying && !isPaused && (
{!isCurrentAudioPlaying && (
<button
onClick={playSpeech}
onClick={() => play(audioSrc)}
className="w-10 h-10 flex items-center justify-center rounded-full bg-white/30 hover:bg-white/50 transition-colors shadow-lg"
aria-label="播放"
>
@@ -184,39 +116,9 @@ const ExhibitDescriptionNarration = ({
</svg>
</button>
)}
{isPlaying && !isPaused && (
{isCurrentAudioPlaying && (
<button
onClick={pauseSpeech}
className="w-10 h-10 flex items-center justify-center rounded-full bg-white/30 hover:bg-white/50 transition-colors shadow-lg"
aria-label="暂停"
>
<svg
className="w-4 h-4 text-white"
fill="currentColor"
viewBox="0 0 20 20"
>
<path d="M5.75 3a.75.75 0 00-.75.75v12.5c0 .414.336.75.75.75h1.5a.75.75 0 00.75-.75V3.75A.75.75 0 007.25 3h-1.5zM12.75 3a.75.75 0 00-.75.75v12.5c0 .414.336.75.75.75h1.5a.75.75 0 00.75-.75V3.75a.75.75 0 00-.75-.75h-1.5z" />
</svg>
</button>
)}
{isPaused && (
<button
onClick={resumeSpeech}
className="w-10 h-10 flex items-center justify-center rounded-full bg-white/30 hover:bg-white/50 transition-colors shadow-lg"
aria-label="继续播放"
>
<svg
className="w-4 h-4 text-white"
fill="currentColor"
viewBox="0 0 20 20"
>
<path d="M6.3 2.841A1.5 1.5 0 004 4.11V15.89a1.5 1.5 0 002.3 1.269l9.344-5.89a1.5 1.5 0 000-2.538L6.3 2.84z" />
</svg>
</button>
)}
{(isPlaying || isPaused) && (
<button
onClick={stopSpeech}
onClick={stop}
className="w-10 h-10 flex items-center justify-center rounded-full bg-white/30 hover:bg-white/50 transition-colors shadow-lg"
aria-label="停止"
>
+9 -1
View File
@@ -26,6 +26,10 @@ export interface ExhibitOverlayProps {
description?: string;
/** 是否自动播放描述语音,默认 false */
autoPlayDescription?: boolean;
/** 展品描述对应的音频地址(可选) */
audioSrc?: string;
/** 音频分组 id,相同 channel 内互斥,不同 channel 可同时播放;不传则全局互斥 */
audioChannelId?: string;
/** 点击展示 */
onClick?: () => void;
/** 打开回调 */
@@ -48,7 +52,9 @@ const ExhibitOverlay = ({
visible = true,
className,
description,
autoPlayDescription = false,
autoPlayDescription = true,
audioSrc,
audioChannelId,
onClick,
onOpen,
}: ExhibitOverlayProps) => {
@@ -515,6 +521,8 @@ const ExhibitOverlay = ({
text={description}
visible={visible}
autoPlay={autoPlayDescription}
audioSrc={audioSrc}
audioChannelId={audioChannelId}
className="absolute left-20 top-1/2 -translate-y-1/2"
/>
)}