forked from 77media/video-flow
326 lines
12 KiB
TypeScript
326 lines
12 KiB
TypeScript
import React, { useRef, useState, useMemo, useEffect, SetStateAction } 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 { SelectDropdown } from '@/components/ui/select-dropdown';
|
|
import { TypewriterText } from '@/components/workflow/work-office/common/TypewriterText';
|
|
|
|
interface ScriptRendererProps {
|
|
data: any[];
|
|
setIsPauseWorkFlow: (isPause: boolean) => void;
|
|
setAnyAttribute: any;
|
|
isPauseWorkFlow: boolean;
|
|
applyScript: any;
|
|
mode: string;
|
|
from?: string;
|
|
setIsUpdate?: (isUpdate: boolean) => void;
|
|
}
|
|
|
|
export const ScriptRenderer: React.FC<ScriptRendererProps> = ({ data, setIsPauseWorkFlow, setAnyAttribute, isPauseWorkFlow, applyScript, mode, from, setIsUpdate }) => {
|
|
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<HTMLDivElement>(null);
|
|
const [addThemeTag, setAddThemeTag] = useState<string[]>([]);
|
|
const [isInit, setIsInit] = useState(true);
|
|
|
|
// 监听继续 请求更新数据
|
|
|
|
useEffect(() => {
|
|
const themeBlock = data.find(block => block.id === 'categories');
|
|
if (themeBlock && themeBlock.content.length > 0) {
|
|
const themeTag = themeBlock.content[0].text.split(',').map((item: string) => item.trim());
|
|
console.log('themeTag', themeTag);
|
|
setAddThemeTag(themeTag);
|
|
}
|
|
}, [data]);
|
|
|
|
// 添加聚焦效果
|
|
useEffect(() => {
|
|
if (editBlockId && contentEditableRef.current) {
|
|
setTimeout(() => {
|
|
contentEditableRef.current?.focus();
|
|
// 可选:将光标移到文本末尾
|
|
const range = document.createRange();
|
|
const sel = window.getSelection();
|
|
range.selectNodeContents(contentEditableRef.current as Node);
|
|
range.collapse(false);
|
|
sel?.removeAllRanges();
|
|
sel?.addRange(range);
|
|
}, 0);
|
|
}
|
|
}, [editBlockId]);
|
|
|
|
const scrollToBlock = (blockId: string) => {
|
|
const element = contentRefs.current[blockId];
|
|
if (element) {
|
|
element.scrollIntoView({ behavior: 'smooth' });
|
|
setActiveBlockId(blockId);
|
|
}
|
|
};
|
|
|
|
// 用于渲染展示的 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">
|
|
<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) => () => {
|
|
setEditBlockId(null);
|
|
if (contentEditableRef.current) {
|
|
const text = contentEditableRef.current.innerText;
|
|
console.log('contentEditableRef---text', text);
|
|
console.log('contentEditableRef---block', block.id, block);
|
|
if (block.content[0].text !== text && setIsUpdate) {
|
|
setIsUpdate(true);
|
|
}
|
|
setAnyAttribute(block.id, text,from !== 'tab',(old: string)=>{
|
|
if(old!==text){
|
|
console.log('contentEditableRef---change?')
|
|
// mode.includes('auto') && applyScript();
|
|
setIsPauseWorkFlow(false);
|
|
}
|
|
});
|
|
}
|
|
};
|
|
|
|
const handleThemeTagChange = (value: string[]) => {
|
|
console.log('主题标签更改', value);
|
|
if (value.length > 5) {
|
|
window.msg.error('max 5 theme tags', 3000);
|
|
return;
|
|
}
|
|
setAddThemeTag(value);
|
|
if (setIsUpdate) {
|
|
setIsUpdate(true);
|
|
}
|
|
from !== 'tab' && setIsPauseWorkFlow(true);
|
|
setAnyAttribute('categories', value.join(','),from !== 'tab',(old: string)=>{
|
|
if(old!==value.join(',')){
|
|
// mode.includes('auto') && applyScript();
|
|
setIsPauseWorkFlow(false);
|
|
}
|
|
});
|
|
};
|
|
|
|
const handleEditBlock = (block: ScriptBlock) => {
|
|
from !== 'tab' && setIsPauseWorkFlow(true);
|
|
setIsInit(false);
|
|
setEditBlockId(block.id);
|
|
setActiveBlockId(block.id);
|
|
};
|
|
|
|
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.id) {
|
|
case 'categories':
|
|
return (
|
|
<div className="flex flex-wrap gap-2 mt-2">
|
|
{addThemeTag.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}</span>
|
|
{/* <X className="w-4 h-4 cursor-pointer text-blue-500/80" onClick={() =>
|
|
handleThemeTagChange(addThemeTag.filter(v => v !== item))
|
|
} /> */}
|
|
</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={(value) => {
|
|
console.log('主题标签更改', value);
|
|
handleThemeTagChange(value as string[]);
|
|
}}
|
|
/>
|
|
</div>
|
|
</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={() => {
|
|
// 提示权限不够
|
|
window.msg.error('No permission!');
|
|
return;
|
|
handleEditBlock(block);
|
|
}}
|
|
/> */}
|
|
<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'));
|
|
window.msg.success('Copied!');
|
|
}}
|
|
/>
|
|
</motion.div>
|
|
)}
|
|
</AnimatePresence>
|
|
<div className="leading-relaxed">
|
|
{isEditing ? (
|
|
renderEditBlock(block)
|
|
) : (
|
|
block.content.map((item, index) => (
|
|
// <div key={index} onDoubleClick={() => handleEditBlock(block)}>{renderContent(item)}</div>
|
|
<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;
|
|
console.log('block', block)
|
|
return (
|
|
<motion.div
|
|
key={block.id}
|
|
className={`relative p-2 mb-1 rounded-lg shadow-md transition-colors duration-300
|
|
${isActive ? 'bg-slate-700/50' : ''} hover:bg-slate-700/30`}
|
|
id={`section-${block.id}`}
|
|
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-lg 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 ${from === 'h5' ? '!p-0' : ''}`} data-alt={from === 'h5' ? 'script-h5-container' : 'script-container'}>
|
|
<div className={`flex-1 overflow-y-auto ${from === 'h5' ? 'pr-0' : 'pr-4'}`}>
|
|
{data.map(renderBlock)}
|
|
</div>
|
|
{from !== 'h5' && (
|
|
<div className="flex-shrink-0 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" />
|
|
navigation
|
|
</h3>
|
|
{data.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>
|
|
);
|
|
};
|