点击聚焦的交互bug,导航部分的宽度,打字机效果Random效果,主题标签加号去掉

This commit is contained in:
北枳 2025-08-05 21:45:19 +08:00
parent 77f82537a9
commit f9a6b48f41
4 changed files with 82 additions and 48 deletions

View File

@ -16,13 +16,32 @@ export const ScriptRenderer: React.FC<ScriptRendererProps> = ({ data }) => {
const [hoveredBlockId, setHoveredBlockId] = useState<string | null>(null); const [hoveredBlockId, setHoveredBlockId] = useState<string | null>(null);
const contentRefs = useRef<{ [key: string]: HTMLDivElement | null }>({}); const contentRefs = useRef<{ [key: string]: HTMLDivElement | null }>({});
const [editBlockId, setEditBlockId] = useState<string | null>(null); const [editBlockId, setEditBlockId] = useState<string | null>(null);
const contentEditableRef = useRef<HTMLElement>(null); const contentEditableRef = useRef<HTMLDivElement>(null);
const [addThemeTag, setAddThemeTag] = useState<string | null>(null); const [addThemeTag, setAddThemeTag] = useState<string[]>([]);
const [isInit, setIsInit] = useState(true); const [isInit, setIsInit] = useState(true);
useEffect(() => { useEffect(() => {
setEditBlockId(null); const themeBlock = data.blocks.find(block => block.type === 'theme');
}, [activeBlockId]); if (themeBlock) {
setAddThemeTag(themeBlock.content.map(item => item.text || ''));
}
}, [data.blocks]);
// 添加聚焦效果
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 scrollToBlock = (blockId: string) => {
const element = contentRefs.current[blockId]; const element = contentRefs.current[blockId];
@ -32,14 +51,6 @@ export const ScriptRenderer: React.FC<ScriptRendererProps> = ({ data }) => {
} }
}; };
// 使用 useMemo 缓存标签颜色映射
const randomThemeTagBgColor = useMemo(() => {
return Object.values(ThemeTagBgColor).reduce((acc: Record<string, string>, color: string) => {
acc[color] = color;
return acc;
}, {});
}, []);
// 用于渲染展示的 JSX // 用于渲染展示的 JSX
const renderContent = (content: ScriptContent) => { const renderContent = (content: ScriptContent) => {
switch (content.type) { switch (content.type) {
@ -93,6 +104,20 @@ export const ScriptRenderer: React.FC<ScriptRendererProps> = ({ data }) => {
setEditBlockId(null); setEditBlockId(null);
}; };
const handleThemeTagChange = (value: string[]) => {
console.log('主题标签更改', value);
if (value.length > 5) {
return toast.error('最多可选择5个主题标签');
}
setAddThemeTag(value);
};
const handleEditBlock = (block: ScriptBlock) => {
setIsInit(false);
setEditBlockId(block.id);
setActiveBlockId(block.id);
};
const renderEditBlock = (block: ScriptBlock) => { const renderEditBlock = (block: ScriptBlock) => {
let blockHtmlText = ''; let blockHtmlText = '';
block.content.forEach(item => { block.content.forEach(item => {
@ -119,13 +144,15 @@ export const ScriptRenderer: React.FC<ScriptRendererProps> = ({ data }) => {
case 'theme': case 'theme':
return ( return (
<div className="flex flex-wrap gap-2 mt-2"> <div className="flex flex-wrap gap-2 mt-2">
{block.content.map((item, index) => ( {addThemeTag.map((item, index) => (
<div key={index} className={`flex items-center gap-1 px-2 rounded-full ${Object.values(ThemeTagBgColor)[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> <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={() => console.log(item.text)} /> <X className="w-4 h-4 cursor-pointer text-blue-500/80" onClick={() =>
handleThemeTagChange(addThemeTag.filter(v => v !== item))
} />
</div> </div>
))} ))}
{/* 新增主题标签 */} {/* 主题标签更改 */}
<div className='flex items-center gap-1'> <div className='flex items-center gap-1'>
<div className='w-[10rem]'> <div className='w-[10rem]'>
<SelectDropdown <SelectDropdown
@ -134,12 +161,12 @@ export const ScriptRenderer: React.FC<ScriptRendererProps> = ({ data }) => {
options={Object.values(ThemeType).map(type => ({ label: type, value: type }))} options={Object.values(ThemeType).map(type => ({ label: type, value: type }))}
value={addThemeTag} value={addThemeTag}
placeholder="Select Theme Type" placeholder="Select Theme Type"
onChange={() => console.log('主题类型')} onChange={(value) => {
console.log('主题标签更改', value);
handleThemeTagChange(value);
}}
/> />
</div> </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>
</div> </div>
) )
@ -157,9 +184,7 @@ export const ScriptRenderer: React.FC<ScriptRendererProps> = ({ data }) => {
<SquarePen <SquarePen
className="w-6 h-6 p-1 cursor-pointer text-gray-600 hover:text-blue-500 transition-colors" className="w-6 h-6 p-1 cursor-pointer text-gray-600 hover:text-blue-500 transition-colors"
onClick={() => { onClick={() => {
setEditBlockId(block.id); handleEditBlock(block);
setActiveBlockId(block.id);
setIsInit(false);
}} }}
/> />
<Copy <Copy
@ -177,7 +202,7 @@ export const ScriptRenderer: React.FC<ScriptRendererProps> = ({ data }) => {
renderEditBlock(block) renderEditBlock(block)
) : ( ) : (
block.content.map((item, index) => ( block.content.map((item, index) => (
<div key={index}>{renderContent(item)}</div> <div key={index} onDoubleClick={() => handleEditBlock(block)}>{renderContent(item)}</div>
)) ))
)} )}
</div> </div>
@ -213,10 +238,10 @@ export const ScriptRenderer: React.FC<ScriptRendererProps> = ({ data }) => {
return ( return (
<div className="flex h-full overflow-hidden pt-2"> <div className="flex h-full overflow-hidden pt-2">
<div className="flex-[0_0_70%] overflow-y-auto pr-4"> <div className="flex-1 overflow-y-auto pr-4">
{data.blocks.map(renderBlock)} {data.blocks.map(renderBlock)}
</div> </div>
<div className="flex-[0_0_30%] flex flex-col overflow-y-auto relative"> <div className="flex-shrink-0 flex flex-col overflow-y-auto relative">
{/* 翻译功能 待开发 */} {/* 翻译功能 待开发 */}
{/* <div className="p-2 rounded-lg"> {/* <div className="p-2 rounded-lg">
<h3 className="text-lg font-semibold mb-1 text-blue-500 flex items-center gap-1"> <h3 className="text-lg font-semibold mb-1 text-blue-500 flex items-center gap-1">

View File

@ -47,16 +47,16 @@ export enum ThemeTagBgColor {
// 主题类型 enum // 主题类型 enum
export enum ThemeType { export enum ThemeType {
satire = 'satire', // 讽刺 satire = 'Satire', // 讽刺
absurdistComedy = 'absurdistComedy', // 荒诞喜剧 absurdistComedy = 'Absurdist Comedy', // 荒诞喜剧
disaster = 'disaster', // 灾难 disaster = 'Disaster', // 灾难
tragedy = 'tragedy', // 悲剧 tragedy = 'Tragedy', // 悲剧
comedy = 'comedy', // 喜剧 comedy = 'Comedy', // 喜剧
drama = 'drama', // 戏剧 drama = 'Drama', // 戏剧
fantasy = 'fantasy', // 奇幻 fantasy = 'Fantasy', // 奇幻
horror = 'horror', // 恐怖 horror = 'Horror', // 恐怖
mystery = 'mystery', // 神秘 mystery = 'Mystery', // 神秘
romance = 'romance', // 爱情 romance = 'Romance', // 爱情
scienceFiction = 'scienceFiction', // 科幻 scienceFiction = 'Science Fiction', // 科幻
thriller = 'thriller', // 惊悚 thriller = 'Thriller', // 惊悚
} }

View File

@ -12,9 +12,9 @@ interface SelectDropdownProps {
dropdownId: string; dropdownId: string;
label: string; label: string;
options: SettingOption[]; options: SettingOption[];
value: string; value: string | Array<string>;
placeholder?: string; placeholder?: string;
onChange: (value: string) => void; onChange: (value: string | Array<string>) => void;
} }
export const SelectDropdown = ( export const SelectDropdown = (
@ -47,8 +47,12 @@ export const SelectDropdown = (
whileTap={{ scale: 0.99 }} whileTap={{ scale: 0.99 }}
> >
<div className="flex items-center gap-2 overflow-hidden text-ellipsis whitespace-nowrap"> <div className="flex items-center gap-2 overflow-hidden text-ellipsis whitespace-nowrap">
<span>{options.find(opt => opt.value === value)?.label || value}</span> {Array.isArray(value) ? (
{placeholder && <span className="text-gray-400/60">{placeholder}</span>} <span>{value.map(v => options.find(opt => opt.value === v)?.label).join(',')}</span>
) : (
<span>{options.find(opt => opt.value === value)?.label || value}</span>
)}
{(!value || value.length === 0) && <span className="text-gray-400/60">{placeholder}</span>}
</div> </div>
<motion.div <motion.div
animate={{ rotate: openDropdown === dropdownId ? 180 : 0 }} animate={{ rotate: openDropdown === dropdownId ? 180 : 0 }}
@ -72,16 +76,20 @@ export const SelectDropdown = (
key={option.value} key={option.value}
className={cn( className={cn(
"w-full px-4 py-2 text-left flex items-center justify-between hover:bg-white/5", "w-full px-4 py-2 text-left flex items-center justify-between hover:bg-white/5",
value === option.value && "text-blue-500" value.includes(option.value) && "text-blue-500"
)} )}
onClick={() => { onClick={() => {
onChange(option.value); if (Array.isArray(value) && value.includes(option.value)) {
onChange(value.filter(v => v !== option.value));
} else {
onChange(Array.isArray(value) ? [...value, option.value] : option.value);
}
handleDropdownToggle(dropdownId); handleDropdownToggle(dropdownId);
}} }}
whileHover={{ x: 4 }} whileHover={{ x: 4 }}
> >
{option.label} {option.label}
{value === option.value && <Check className="w-4 h-4" />} {value.includes(option.value) && <Check className="w-4 h-4" />}
</motion.button> </motion.button>
))} ))}
</motion.div> </motion.div>

View File

@ -35,15 +35,16 @@ export const TypewriterText: React.FC<TypewriterTextProps> = ({ text, stableId }
let currentIndex = 0; let currentIndex = 0;
const typeNextChar = () => { const typeNextChar = () => {
let addRandom = Math.floor(Math.random() * 10);
if (currentIndex < text.length) { if (currentIndex < text.length) {
const newText = text.slice(0, currentIndex + 1); const newText = text.slice(0, currentIndex + addRandom);
setDisplayState({ setDisplayState({
displayText: newText, displayText: newText,
isTyping: true, isTyping: true,
isComplete: false isComplete: false
}); });
currentIndex++; currentIndex += addRandom;
animationRef.current = setTimeout(typeNextChar, 30); animationRef.current = setTimeout(typeNextChar, 100);
} else { } else {
setDisplayState({ setDisplayState({
displayText: text, displayText: text,