262 lines
9.5 KiB
TypeScript

import React, { useRef, useState, useMemo, useEffect } from 'react';
import { motion, AnimatePresence } from 'framer-motion';
import { SquarePen, Lightbulb, Navigation, Globe, Copy, SendHorizontal, X, Plus } from 'lucide-react';
import { ScriptData, ScriptBlock, ScriptContent, ThemeTagBgColor, ThemeType } from './types';
import ContentEditable, { ContentEditableEvent } from 'react-contenteditable';
import { toast } from 'sonner';
import { SelectDropdown } from '@/components/ui/select-dropdown';
import { TypewriterText } from '@/components/workflow/work-office/common/TypewriterText';
interface ScriptRendererProps {
data: ScriptData;
}
export const ScriptRenderer: React.FC<ScriptRendererProps> = ({ data }) => {
const [activeBlockId, setActiveBlockId] = useState<string | null>(null);
const [hoveredBlockId, setHoveredBlockId] = useState<string | null>(null);
const contentRefs = useRef<{ [key: string]: HTMLDivElement | null }>({});
const [editBlockId, setEditBlockId] = useState<string | null>(null);
const contentEditableRef = useRef<HTMLElement>(null);
const [addThemeTag, setAddThemeTag] = useState<string | null>(null);
const [isInit, setIsInit] = useState(true);
useEffect(() => {
setEditBlockId(null);
}, [activeBlockId]);
const scrollToBlock = (blockId: string) => {
const element = contentRefs.current[blockId];
if (element) {
element.scrollIntoView({ behavior: 'smooth' });
setActiveBlockId(blockId);
}
};
// 使用 useMemo 缓存标签颜色映射
const randomThemeTagBgColor = useMemo(() => {
return Object.values(ThemeTagBgColor).reduce((acc: Record<string, string>, color: string) => {
acc[color] = color;
return acc;
}, {});
}, []);
// 用于渲染展示的 JSX
const renderContent = (content: ScriptContent) => {
switch (content.type) {
case 'heading':
return <h3 className="text-xl font-semibold mb-2">{content.text}</h3>;
case 'bold':
return <strong className="font-bold">{content.text}</strong>;
case 'italic':
return <em className="italic">{content.text}</em>;
default:
return <p className="mb-2">
{
isInit ? (
<TypewriterText text={content.text || ''} stableId={content.type} />
) : (
<span>{content.text}</span>
)
}
</p>;
}
};
// 用于生成编辑时的 HTML 字符串
const contentToHtml = (content: ScriptContent): string => {
switch (content.type) {
case 'heading':
return `<h3 class="text-xl font-semibold mb-2">${content.text}</h3>`;
case 'bold':
return `<strong class="font-bold">${content.text}</strong>`;
case 'italic':
return `<em class="italic">${content.text}</em>`;
default:
return `<p class="mb-2">${content.text}</p>`;
}
};
// 格式化文本为 HTML
const formatTextToHtml = (text: string) => {
return text
.split('\n')
.map(line => line || '<br>')
.join('<div>');
};
const handleBlockTextChange = (block: ScriptBlock) => (e: ContentEditableEvent) => {
console.log(e.target.value);
};
const handleBlockTextBlur = (block: ScriptBlock) => (e: ContentEditableEvent) => {
console.log(e.target.value);
setEditBlockId(null);
};
const renderEditBlock = (block: ScriptBlock) => {
let blockHtmlText = '';
block.content.forEach(item => {
blockHtmlText += contentToHtml(item);
});
return (
<ContentEditable
innerRef={contentEditableRef}
html={formatTextToHtml(blockHtmlText)}
onChange={handleBlockTextChange(block)}
onBlur={handleBlockTextBlur(block)}
autoFocus={true}
className="block w-full min-h-[120px] bg-white/5 backdrop-blur-md p-4 text-white/90
rounded-lg border-unset outline-none pb-12
whitespace-pre-wrap break-words"
placeholder=""
/>
);
};
const renderTypeBlock = (block: ScriptBlock, isHovered: boolean, isActive: boolean, isEditing: boolean) => {
switch (block.type) {
case 'theme':
return (
<div className="flex flex-wrap gap-2 mt-2">
{block.content.map((item, index) => (
<div key={index} className={`flex items-center gap-1 px-2 rounded-full ${Object.values(ThemeTagBgColor)[index]}`}>
<span className={`text-sm px-2 py-1 rounded-md`}>{item.text}</span>
<X className="w-4 h-4 cursor-pointer text-blue-500/80" onClick={() => console.log(item.text)} />
</div>
))}
{/* 新增主题标签 */}
<div className='flex items-center gap-1'>
<div className='w-[10rem]'>
<SelectDropdown
dropdownId="theme-type"
label=""
options={Object.values(ThemeType).map(type => ({ label: type, value: type }))}
value={addThemeTag}
placeholder="Select Theme Type"
onChange={() => console.log('主题类型')}
/>
</div>
<button className='p-2 rounded-full bg-white/5 backdrop-blur-md'>
<Plus className="w-4 h-4 cursor-pointer text-white-600" onClick={() => console.log('新增主题标签')} />
</button>
</div>
</div>
)
default:
return (
<>
<AnimatePresence>
{(isHovered || isActive) && (
<motion.div
className="absolute top-4 right-4 flex gap-2"
initial={{ opacity: 0, y: -10 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: -10 }}
>
<SquarePen
className="w-6 h-6 p-1 cursor-pointer text-gray-600 hover:text-blue-500 transition-colors"
onClick={() => {
setEditBlockId(block.id);
setActiveBlockId(block.id);
setIsInit(false);
}}
/>
<Copy
className="w-6 h-6 p-1 cursor-pointer text-gray-600 hover:text-blue-500 transition-colors"
onClick={() => {
navigator.clipboard.writeText(block.content.map(item => item.text).join('\n'));
toast.success('Copied!');
}}
/>
</motion.div>
)}
</AnimatePresence>
<div className="leading-relaxed">
{isEditing ? (
renderEditBlock(block)
) : (
block.content.map((item, index) => (
<div key={index}>{renderContent(item)}</div>
))
)}
</div>
</>
);
}
};
const renderBlock = (block: ScriptBlock) => {
const isHovered = hoveredBlockId === block.id;
const isActive = activeBlockId === block.id;
const isEditing = editBlockId === block.id;
return (
<motion.div
key={block.id}
className={`relative p-4 mb-1 rounded-lg shadow-md transition-colors duration-300
${isActive ? 'bg-slate-700/50' : ''} hover:bg-slate-700/30`}
ref={(el) => (contentRefs.current[block.id] = el)}
onMouseEnter={() => setHoveredBlockId(block.id)}
onMouseLeave={() => setHoveredBlockId(null)}
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
transition={{ duration: 0.3 }}
>
<h2 className="text-2xl font-semibold mb-1 text-blue-500">{block.title}</h2>
{
renderTypeBlock(block, isHovered, isActive, isEditing)
}
</motion.div>
);
};
return (
<div className="flex h-full overflow-hidden pt-2">
<div className="flex-[0_0_70%] overflow-y-auto pr-4">
{data.blocks.map(renderBlock)}
</div>
<div className="flex-[0_0_30%] flex flex-col overflow-y-auto relative">
{/* 翻译功能 待开发 */}
{/* <div className="p-2 rounded-lg">
<h3 className="text-lg font-semibold mb-1 text-blue-500 flex items-center gap-1">
<Globe className="w-4 h-4 transition-colors" />
翻译
</h3>
<div className="mt-1 flex flex-col gap-2">
<select className="w-full p-2 rounded-md bg-white/5 backdrop-blur-md">
<option value="zh">中文</option>
<option value="en">英文</option>
</select>
<div className="flex gap-2">
<button className="w-full p-2 rounded-md bg-white/5 backdrop-blur-md">
翻译
</button>
<button className="w-full p-2 rounded-md bg-white/5 backdrop-blur-md">
还原
</button>
</div>
</div>
</div> */}
<div className="p-2 rounded-lg">
<h3 className="text-lg font-semibold mb-1 text-blue-500 flex items-center gap-1">
<Navigation className="w-4 h-4 transition-colors" />
</h3>
{data.blocks.map((block) => (
<motion.div
key={block.id}
className={`py-1 px-1 my-1 rounded cursor-pointer transition-all duration-300 text-white/80
${activeBlockId === block.id ? 'text-blue-500/100' : 'hover:text-blue-500/80'}`}
onClick={() => scrollToBlock(block.id)}
whileHover={{ scale: 1.02 }}
whileTap={{ scale: 0.98 }}
>
{block.title}
</motion.div>
))}
</div>
</div>
</div>
);
};