点击聚焦的交互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 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 contentEditableRef = useRef<HTMLDivElement>(null);
const [addThemeTag, setAddThemeTag] = useState<string[]>([]);
const [isInit, setIsInit] = useState(true);
useEffect(() => {
setEditBlockId(null);
}, [activeBlockId]);
const themeBlock = data.blocks.find(block => block.type === 'theme');
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 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
const renderContent = (content: ScriptContent) => {
switch (content.type) {
@ -93,6 +104,20 @@ export const ScriptRenderer: React.FC<ScriptRendererProps> = ({ data }) => {
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) => {
let blockHtmlText = '';
block.content.forEach(item => {
@ -119,13 +144,15 @@ export const ScriptRenderer: React.FC<ScriptRendererProps> = ({ data }) => {
case 'theme':
return (
<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]}`}>
<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)} />
<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
@ -134,12 +161,12 @@ export const ScriptRenderer: React.FC<ScriptRendererProps> = ({ data }) => {
options={Object.values(ThemeType).map(type => ({ label: type, value: type }))}
value={addThemeTag}
placeholder="Select Theme Type"
onChange={() => console.log('主题类型')}
onChange={(value) => {
console.log('主题标签更改', value);
handleThemeTagChange(value);
}}
/>
</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>
)
@ -157,9 +184,7 @@ export const ScriptRenderer: React.FC<ScriptRendererProps> = ({ data }) => {
<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);
handleEditBlock(block);
}}
/>
<Copy
@ -177,7 +202,7 @@ export const ScriptRenderer: React.FC<ScriptRendererProps> = ({ data }) => {
renderEditBlock(block)
) : (
block.content.map((item, index) => (
<div key={index}>{renderContent(item)}</div>
<div key={index} onDoubleClick={() => handleEditBlock(block)}>{renderContent(item)}</div>
))
)}
</div>
@ -213,10 +238,10 @@ export const ScriptRenderer: React.FC<ScriptRendererProps> = ({ data }) => {
return (
<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)}
</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">
<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
export enum ThemeType {
satire = 'satire', // 讽刺
absurdistComedy = 'absurdistComedy', // 荒诞喜剧
disaster = 'disaster', // 灾难
tragedy = 'tragedy', // 悲剧
comedy = 'comedy', // 喜剧
drama = 'drama', // 戏剧
fantasy = 'fantasy', // 奇幻
horror = 'horror', // 恐怖
mystery = 'mystery', // 神秘
romance = 'romance', // 爱情
scienceFiction = 'scienceFiction', // 科幻
thriller = 'thriller', // 惊悚
satire = 'Satire', // 讽刺
absurdistComedy = 'Absurdist Comedy', // 荒诞喜剧
disaster = 'Disaster', // 灾难
tragedy = 'Tragedy', // 悲剧
comedy = 'Comedy', // 喜剧
drama = 'Drama', // 戏剧
fantasy = 'Fantasy', // 奇幻
horror = 'Horror', // 恐怖
mystery = 'Mystery', // 神秘
romance = 'Romance', // 爱情
scienceFiction = 'Science Fiction', // 科幻
thriller = 'Thriller', // 惊悚
}

View File

@ -12,9 +12,9 @@ interface SelectDropdownProps {
dropdownId: string;
label: string;
options: SettingOption[];
value: string;
value: string | Array<string>;
placeholder?: string;
onChange: (value: string) => void;
onChange: (value: string | Array<string>) => void;
}
export const SelectDropdown = (
@ -47,8 +47,12 @@ export const SelectDropdown = (
whileTap={{ scale: 0.99 }}
>
<div className="flex items-center gap-2 overflow-hidden text-ellipsis whitespace-nowrap">
<span>{options.find(opt => opt.value === value)?.label || value}</span>
{placeholder && <span className="text-gray-400/60">{placeholder}</span>}
{Array.isArray(value) ? (
<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>
<motion.div
animate={{ rotate: openDropdown === dropdownId ? 180 : 0 }}
@ -72,16 +76,20 @@ export const SelectDropdown = (
key={option.value}
className={cn(
"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={() => {
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);
}}
whileHover={{ x: 4 }}
>
{option.label}
{value === option.value && <Check className="w-4 h-4" />}
{value.includes(option.value) && <Check className="w-4 h-4" />}
</motion.button>
))}
</motion.div>

View File

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