feat: 移除不再使用的配置文件和更新组件,提升代码整洁性
- 删除 .claude/settings.local.json 配置文件,清理不必要的代码。 - 更新多个组件以引入音频资源,增强用户互动体验。 - 调整引导对话文本,确保信息准确性和一致性。 - 更新图像资源,提升视觉内容的准确性和一致性。
This commit is contained in:
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="停止"
|
||||
>
|
||||
|
||||
@@ -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"
|
||||
/>
|
||||
)}
|
||||
|
||||
Reference in new issue
Block a user