forked from 77media/video-flow
点击聚焦的交互bug,导航部分的宽度,打字机效果Random效果,主题标签加号去掉
This commit is contained in:
parent
77f82537a9
commit
f9a6b48f41
@ -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">
|
||||||
|
|||||||
@ -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', // 惊悚
|
||||||
}
|
}
|
||||||
@ -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>
|
||||||
|
|||||||
@ -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,
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user