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 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">
|
||||
|
||||
@ -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', // 惊悚
|
||||
}
|
||||
@ -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>
|
||||
|
||||
@ -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,
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user