131 lines
4.3 KiB
TypeScript
131 lines
4.3 KiB
TypeScript
import { useState, useEffect } from "react";
|
|
import { motion } from "framer-motion";
|
|
import { Image } from "@/components";
|
|
import { getStaticImage } from "@/lib/utils";
|
|
import { DialogueClueData } from "./types";
|
|
|
|
export interface DialogueContentProps {
|
|
/** 对话线索数据 */
|
|
clue: DialogueClueData;
|
|
}
|
|
|
|
/**
|
|
* 对话内容组件
|
|
* 依次显示多条对话,所有对话同时展示,带有动画效果
|
|
*/
|
|
const DialogueContent = ({ clue }: DialogueContentProps) => {
|
|
const [visibleIndices, setVisibleIndices] = useState<number[]>([]);
|
|
|
|
// 依次显示对话:先显示第一条,延迟后显示第二条
|
|
useEffect(() => {
|
|
if (clue.dialogues.length === 0) return;
|
|
|
|
// 立即显示第一条对话
|
|
setVisibleIndices([0]);
|
|
|
|
// 延迟显示后续对话
|
|
if (clue.dialogues.length > 1) {
|
|
const timer = setTimeout(() => {
|
|
setVisibleIndices((prev) => [...prev, 1]);
|
|
}, 1500); // 第一条显示1.5秒后显示第二条
|
|
|
|
return () => clearTimeout(timer);
|
|
}
|
|
}, [clue.dialogues.length]);
|
|
|
|
return (
|
|
<div className="w-full h-full flex flex-col items-center gap-30 relative px-10">
|
|
{clue.dialogues.map((dialogue, index) => {
|
|
const isVisible = visibleIndices.includes(index);
|
|
|
|
return (
|
|
<motion.div
|
|
key={index}
|
|
initial={{ opacity: 0, y: 30, scale: 0.95 }}
|
|
animate={
|
|
isVisible
|
|
? { opacity: 1, y: 0, scale: 1 }
|
|
: { opacity: 0, y: 30, scale: 0.95 }
|
|
}
|
|
transition={{
|
|
duration: 0.6,
|
|
ease: [0.16, 1, 0.3, 1], // 自定义缓动函数,更流畅
|
|
}}
|
|
className="flex items-center gap-8 w-full max-w-7xl"
|
|
>
|
|
{/* 角色头像 */}
|
|
{dialogue.avatar && (
|
|
<motion.div
|
|
initial={{ opacity: 0, x: -20 }}
|
|
animate={
|
|
isVisible ? { opacity: 1, x: 0 } : { opacity: 0, x: -20 }
|
|
}
|
|
transition={{
|
|
duration: 0.5,
|
|
delay: 0.1,
|
|
ease: "easeOut",
|
|
}}
|
|
className="flex-shrink-0 flex flex-col items-center"
|
|
>
|
|
<Image
|
|
src={getStaticImage(dialogue.avatar)}
|
|
className="w-60 h-60 object-contain"
|
|
alt={dialogue.character}
|
|
/>
|
|
<motion.div
|
|
initial={{ opacity: 0, y: 10 }}
|
|
animate={
|
|
isVisible ? { opacity: 1, y: 0 } : { opacity: 0, y: 10 }
|
|
}
|
|
transition={{
|
|
duration: 0.4,
|
|
delay: 0.3,
|
|
ease: "easeOut",
|
|
}}
|
|
className="mt-3 px-5 py-2 bg-gray-800/90 rounded-lg"
|
|
>
|
|
<span className="text-white text-base font-medium">
|
|
{dialogue.character}
|
|
</span>
|
|
</motion.div>
|
|
</motion.div>
|
|
)}
|
|
|
|
{/* 对话气泡 */}
|
|
<motion.div
|
|
initial={{ opacity: 0, x: 20 }}
|
|
animate={isVisible ? { opacity: 1, x: 0 } : { opacity: 0, x: 20 }}
|
|
transition={{
|
|
duration: 0.5,
|
|
delay: 0.2,
|
|
ease: "easeOut",
|
|
}}
|
|
className="flex-1 relative"
|
|
>
|
|
<div className="bg-gray-800/95 rounded-3xl px-20 py-8 shadow-2xl">
|
|
<motion.div
|
|
initial={{ opacity: 0 }}
|
|
animate={isVisible ? { opacity: 1 } : { opacity: 0 }}
|
|
transition={{
|
|
duration: 0.4,
|
|
delay: 0.4,
|
|
}}
|
|
className="text-white text-4xl leading-relaxed"
|
|
>
|
|
{dialogue.text}
|
|
</motion.div>
|
|
</div>
|
|
{/* 对话气泡箭头 - 指向左侧头像 */}
|
|
<div className="absolute left-0 top-1/2 -translate-y-1/2 -translate-x-3">
|
|
<div className="w-0 h-0 border-t-[12px] border-t-transparent border-r-[12px] border-r-gray-800/95 border-b-[12px] border-b-transparent" />
|
|
</div>
|
|
</motion.div>
|
|
</motion.div>
|
|
);
|
|
})}
|
|
</div>
|
|
);
|
|
};
|
|
|
|
export default DialogueContent;
|