feat:init
This commit is contained in:
1 parent
53d0eeac6b
commit
ae0c050010
494 files changed
+17701
-2318
No files matched your search
@@ -0,0 +1,155 @@
|
||||
import { useState, useEffect } from "react";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
export interface NarrationProps {
|
||||
/** 旁白文本 */
|
||||
text: string;
|
||||
/** 是否可见 */
|
||||
visible: boolean;
|
||||
/** 关闭回调 */
|
||||
onClose: () => void;
|
||||
/** 语音播报的音频文件路径(可选) */
|
||||
audioSrc?: string;
|
||||
/** 自定义样式类名 */
|
||||
className?: string;
|
||||
/** 是否启用逐字动画,默认 true */
|
||||
enableAnimation?: boolean;
|
||||
/** 文字动画速度(毫秒/字),默认 100ms */
|
||||
animationSpeed?: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Narration - 旁白组件
|
||||
*
|
||||
* 功能:
|
||||
* - 文字逐字显示动画
|
||||
* - 可选语音播报
|
||||
* - 关闭按钮
|
||||
*/
|
||||
const Narration = ({
|
||||
text,
|
||||
visible,
|
||||
onClose,
|
||||
audioSrc,
|
||||
className,
|
||||
enableAnimation = true,
|
||||
animationSpeed = 100,
|
||||
}: NarrationProps) => {
|
||||
const [displayedText, setDisplayedText] = useState("");
|
||||
const [isAnimating, setIsAnimating] = useState(true);
|
||||
const [audio, setAudio] = useState<HTMLAudioElement | null>(null);
|
||||
const [isSkipped, setIsSkipped] = useState(false);
|
||||
|
||||
// 文字动画效果
|
||||
useEffect(() => {
|
||||
if (!visible) {
|
||||
setDisplayedText("");
|
||||
setIsAnimating(true);
|
||||
setIsSkipped(false);
|
||||
return;
|
||||
}
|
||||
|
||||
// 如果已经跳过,不再执行动画
|
||||
if (isSkipped) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 如果不启用动画,直接显示全部文本
|
||||
if (!enableAnimation) {
|
||||
setDisplayedText(text);
|
||||
setIsAnimating(false);
|
||||
return;
|
||||
}
|
||||
|
||||
let currentIndex = 0;
|
||||
setDisplayedText("");
|
||||
setIsAnimating(true);
|
||||
|
||||
const timer = setInterval(() => {
|
||||
if (currentIndex < text.length) {
|
||||
setDisplayedText(text.slice(0, currentIndex + 1));
|
||||
currentIndex++;
|
||||
} else {
|
||||
setIsAnimating(false);
|
||||
clearInterval(timer);
|
||||
}
|
||||
}, animationSpeed);
|
||||
|
||||
return () => clearInterval(timer);
|
||||
}, [text, visible, enableAnimation, animationSpeed, isSkipped]);
|
||||
|
||||
// 跳过动画
|
||||
const handleSkip = () => {
|
||||
setDisplayedText(text);
|
||||
setIsAnimating(false);
|
||||
setIsSkipped(true);
|
||||
};
|
||||
|
||||
// 语音播报
|
||||
useEffect(() => {
|
||||
if (!visible || !audioSrc) return;
|
||||
|
||||
const audioElement = new Audio(audioSrc);
|
||||
audioElement.play().catch((error) => {
|
||||
console.warn("Audio playback failed:", error);
|
||||
});
|
||||
setAudio(audioElement);
|
||||
|
||||
return () => {
|
||||
audioElement.pause();
|
||||
audioElement.currentTime = 0;
|
||||
};
|
||||
}, [visible, audioSrc]);
|
||||
|
||||
// 停止音频
|
||||
const handleClose = () => {
|
||||
if (audio) {
|
||||
audio.pause();
|
||||
audio.currentTime = 0;
|
||||
}
|
||||
onClose();
|
||||
};
|
||||
|
||||
if (!visible) return null;
|
||||
|
||||
return (
|
||||
<div className="absolute inset-0 flex items-center justify-center bg-black/80 z-50">
|
||||
<div
|
||||
className={cn(
|
||||
"w-[1600px] bg-black/90 rounded-[32px] px-20 py-16",
|
||||
"shadow-[0_20px_60px_rgba(240,190,117,0.8)] border-4 border-[#d4c5a9]",
|
||||
"relative",
|
||||
className
|
||||
)}
|
||||
>
|
||||
{/* 跳过按钮 - 只在动画进行中显示 */}
|
||||
{isAnimating && enableAnimation && (
|
||||
<button
|
||||
onClick={handleSkip}
|
||||
className="absolute top-8 right-8 px-6 py-2 rounded-full bg-black/60 text-white text-xl backdrop-blur-sm border border-white/30 hover:bg-black/75 transition-colors"
|
||||
>
|
||||
跳过
|
||||
</button>
|
||||
)}
|
||||
|
||||
<div className="text-white text-3xl leading-relaxed space-y-8">
|
||||
<p className="whitespace-pre-wrap">{displayedText}</p>
|
||||
</div>
|
||||
|
||||
{/* 关闭按钮 - 只在动画完成后显示 */}
|
||||
{!isAnimating && (
|
||||
<div className="flex justify-center mt-14">
|
||||
<button
|
||||
onClick={handleClose}
|
||||
className="px-12 py-2 bg-gradient-to-br from-[#d4c5a9] to-[#b8a88a] text-[#4e402e] text-3xl font-bold rounded-full border-3 border-[#8B7355] shadow-lg hover:scale-105 transition-transform"
|
||||
>
|
||||
已知晓
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Narration;
|
||||
@@ -0,0 +1,2 @@
|
||||
export { default as Narration } from "./Narration";
|
||||
export type { NarrationProps } from "./Narration";
|
||||
Reference in new issue
Block a user