forked from 77media/video-flow
410 lines
14 KiB
TypeScript
410 lines
14 KiB
TypeScript
'use client';
|
||
|
||
import React, { useState, useEffect, SetStateAction, useRef } from 'react';
|
||
import { motion, AnimatePresence } from 'framer-motion';
|
||
import { X, Image, Users, Video, Music, Settings, FileText, Undo2, TriangleAlert } from 'lucide-react';
|
||
import { cn } from '@/public/lib/utils';
|
||
import { ScriptTabContent } from './script-tab-content';
|
||
import { SceneTabContent } from './scene-tab-content';
|
||
import { ShotTabContent } from './shot-tab-content';
|
||
import { SettingsTabContent } from './settings-tab-content';
|
||
import { CharacterTabContent } from './character-tab-content';
|
||
import { MusicTabContent } from './music-tab-content';
|
||
import FloatingGlassPanel from './FloatingGlassPanel';
|
||
import { SaveEditUseCase } from '@/app/service/usecase/SaveEditUseCase';
|
||
|
||
interface EditModalProps {
|
||
isOpen: boolean;
|
||
onClose: () => void;
|
||
activeEditTab: string;
|
||
currentSketchIndex: number;
|
||
roles?: any[];
|
||
setIsPauseWorkFlow: (isPauseWorkFlow: boolean) => void;
|
||
isPauseWorkFlow: boolean;
|
||
fallbackToStep: any;
|
||
originalText?: string;
|
||
}
|
||
|
||
const tabs = [
|
||
{ id: '0', label: 'Script', icon: FileText },
|
||
{ id: '1', label: 'Character', icon: Users },
|
||
// { id: '2', label: 'Scene', icon: Image },
|
||
{ id: '3', label: 'Video', icon: Video },
|
||
{ id: '4', label: 'Music', icon: Music },
|
||
// { id: '5', label: '剪辑', icon: Scissors },
|
||
{ id: 'settings', label: 'Settings', icon: Settings },
|
||
];
|
||
|
||
export function EditModal({
|
||
isOpen,
|
||
onClose,
|
||
activeEditTab,
|
||
currentSketchIndex,
|
||
roles = [],
|
||
setIsPauseWorkFlow,
|
||
isPauseWorkFlow,
|
||
fallbackToStep,
|
||
originalText
|
||
}: EditModalProps) {
|
||
const [activeTab, setActiveTab] = useState(activeEditTab);
|
||
const [currentIndex, setCurrentIndex] = useState(currentSketchIndex);
|
||
const [currentRoleIndex, setCurrentRoleIndex] = useState(0);
|
||
const [isRemindFallbackOpen, setIsRemindFallbackOpen] = useState(false);
|
||
const [isRemindResetOpen, setIsRemindResetOpen] = useState(false);
|
||
const [resetKey, setResetKey] = useState(0);
|
||
const [remindFallbackText, setRemindFallbackText] = useState('The task will be regenerated and edited. Do you want to continue?');
|
||
const scriptTabContentRef = useRef<any>(null);
|
||
const characterTabContentRef = useRef<any>(null);
|
||
// 添加一个状态来标记是否是从切换tab触发的提醒
|
||
const [pendingSwitchTabId, setPendingSwitchTabId] = useState<string | null>(null);
|
||
|
||
useEffect(() => {
|
||
setCurrentIndex(currentSketchIndex);
|
||
}, [isOpen]);
|
||
|
||
// 当 activeEditTab 改变时更新 activeTab
|
||
useEffect(() => {
|
||
setActiveTab(activeEditTab);
|
||
}, [activeEditTab]);
|
||
|
||
const isTabDisabled = (tabId: string) => {
|
||
if (tabId === 'settings') return false;
|
||
// 换成 如果对应标签下 数据存在 就不禁用
|
||
// if (tabId === '1') return roles.length === 0;
|
||
// if (tabId === '2') return taskScenes.length === 0;
|
||
// if (tabId === '3') return sketchVideo.length === 0;
|
||
if (tabId === '4') return false;
|
||
return false;
|
||
};
|
||
|
||
const hanldeChangeSelect = (index: number) => {
|
||
if (activeTab === '1') {
|
||
setCurrentRoleIndex(index);
|
||
} else {
|
||
setCurrentIndex(index);
|
||
}
|
||
}
|
||
|
||
const checkUpdate = (tabId: string) => {
|
||
if (activeTab === '0') {
|
||
const scriptTabContent = scriptTabContentRef.current;
|
||
if (scriptTabContent) {
|
||
return scriptTabContent.switchBefore(tabId);
|
||
}
|
||
} else if (activeTab === '1') {
|
||
const characterTabContent = characterTabContentRef.current;
|
||
if (characterTabContent) {
|
||
return characterTabContent.switchBefore(tabId);
|
||
}
|
||
}
|
||
return false;
|
||
}
|
||
|
||
const handleChangeTab = (tabId: string, disabled: boolean) => {
|
||
if (disabled) return;
|
||
// 切换前 检查是否更新
|
||
const isUpdate = checkUpdate(tabId);
|
||
if (isUpdate) {
|
||
return;
|
||
}
|
||
setActiveTab(tabId);
|
||
setCurrentIndex(0);
|
||
}
|
||
|
||
const handleSave = () => {
|
||
console.log('handleSave');
|
||
// setIsRemindFallbackOpen(true);
|
||
if (activeTab === '0') {
|
||
scriptTabContentRef.current.saveBefore();
|
||
} else if (activeTab === '1') {
|
||
characterTabContentRef.current.saveBefore();
|
||
} else if (activeTab === '3') {
|
||
handleConfirmGotoFallback();
|
||
}
|
||
}
|
||
|
||
const handleApply = () => {
|
||
console.log('handleApply');
|
||
handleConfirmGotoFallback();
|
||
}
|
||
|
||
const handleConfirmGotoFallback = () => {
|
||
console.log('handleConfirmGotoFallback');
|
||
SaveEditUseCase.saveData();
|
||
if (activeTab === '0') {
|
||
fallbackToStep('script');
|
||
// 应用剧本
|
||
} else {
|
||
fallbackToStep('video');
|
||
}
|
||
setIsRemindFallbackOpen(false);
|
||
// 关闭弹窗
|
||
onClose();
|
||
}
|
||
const handleCloseRemindFallbackPanel = () => {
|
||
if (pendingSwitchTabId) {
|
||
// 如果是从切换tab触发的提醒,关闭后执行tab切换
|
||
setActiveTab(pendingSwitchTabId);
|
||
setCurrentIndex(0);
|
||
setPendingSwitchTabId(null); // 清除记录
|
||
}
|
||
setIsRemindFallbackOpen(false);
|
||
}
|
||
|
||
const handleReset = () => {
|
||
console.log('handleReset');
|
||
// 重置当前tab修改的数据
|
||
setIsRemindResetOpen(true);
|
||
}
|
||
|
||
const handleConfirmReset = () => {
|
||
console.log('handleConfirmReset');
|
||
setIsRemindResetOpen(false);
|
||
setResetKey(resetKey + 1);
|
||
}
|
||
|
||
const renderTabContent = () => {
|
||
switch (activeTab) {
|
||
case '0':
|
||
return (
|
||
<ScriptTabContent
|
||
ref={scriptTabContentRef}
|
||
setIsPauseWorkFlow={setIsPauseWorkFlow}
|
||
isPauseWorkFlow={isPauseWorkFlow}
|
||
originalText={originalText}
|
||
onApply={handleApply}
|
||
setActiveTab={setActiveTab}
|
||
/>
|
||
);
|
||
case '1':
|
||
return (
|
||
<CharacterTabContent
|
||
ref={characterTabContentRef}
|
||
onClose={onClose}
|
||
onApply={handleApply}
|
||
setActiveTab={setActiveTab}
|
||
/>
|
||
);
|
||
case '2':
|
||
return (
|
||
<SceneTabContent
|
||
currentSketchIndex={currentIndex}
|
||
/>
|
||
);
|
||
case '3':
|
||
return (
|
||
<ShotTabContent
|
||
currentSketchIndex={currentIndex}
|
||
roles={roles}
|
||
/>
|
||
);
|
||
case '4':
|
||
return (
|
||
<MusicTabContent
|
||
currentSketchIndex={currentIndex}
|
||
/>
|
||
);
|
||
case 'settings':
|
||
return (
|
||
<SettingsTabContent
|
||
onSettingChange={(key, value) => {
|
||
console.log('Setting changed:', key, value);
|
||
// TODO: 实现设置更新逻辑
|
||
}}
|
||
/>
|
||
);
|
||
default:
|
||
return (
|
||
<div className="min-h-[400px] flex items-center justify-center text-white/50">
|
||
{tabs.find(tab => tab.id === activeTab)?.label} Content area
|
||
</div>
|
||
);
|
||
}
|
||
};
|
||
|
||
return (
|
||
<AnimatePresence>
|
||
{isOpen && (
|
||
<>
|
||
{/* 背景遮罩 */}
|
||
<motion.div
|
||
className="fixed inset-0 bg-black/60 backdrop-blur-sm z-50"
|
||
initial={{ opacity: 0 }}
|
||
animate={{ opacity: 1 }}
|
||
exit={{ opacity: 0 }}
|
||
onClick={onClose}
|
||
/>
|
||
|
||
{/* 弹窗内容 */}
|
||
<div className="fixed inset-x-0 bottom-0 z-50 flex justify-center">
|
||
<motion.div
|
||
className={cn(
|
||
"w-[88%] min-w-[888px] h-[95vh] bg-[#1a1b1e] rounded-t-2xl overflow-hidden"
|
||
)}
|
||
initial={{ y: '100%' }}
|
||
animate={{ y: 0 }}
|
||
exit={{ y: '100%' }}
|
||
transition={{
|
||
type: 'spring',
|
||
damping: 25,
|
||
stiffness: 200,
|
||
}}
|
||
>
|
||
{/* 标签栏 */}
|
||
<div className="flex items-center gap-1 p-2 overflow-x-auto hide-scrollbar bg-white/5">
|
||
<div className="flex-1 flex gap-1">
|
||
{tabs.map((tab) => {
|
||
const Icon = tab.icon;
|
||
const disabled = isTabDisabled(tab.id);
|
||
return (
|
||
<motion.button
|
||
key={tab.id}
|
||
className={cn(
|
||
'flex items-center gap-2 px-4 py-2 rounded-lg transition-colors relative',
|
||
activeTab === tab.id ? 'text-white' : 'text-white/50',
|
||
disabled ? 'opacity-50 cursor-not-allowed' : 'hover:bg-white/10',
|
||
)}
|
||
onClick={() => handleChangeTab(tab.id, disabled)}
|
||
whileHover={disabled ? undefined : { scale: 1.02 }}
|
||
whileTap={disabled ? undefined : { scale: 0.98 }}
|
||
>
|
||
<Icon className="w-4 h-4" />
|
||
<span>{tab.label}</span>
|
||
{activeTab === tab.id && (
|
||
<motion.div
|
||
className="absolute bottom-0 left-0 right-0 h-0.5 bg-blue-500"
|
||
layoutId="activeTab"
|
||
transition={{ type: 'spring', damping: 20 }}
|
||
/>
|
||
)}
|
||
</motion.button>
|
||
);
|
||
})}
|
||
</div>
|
||
|
||
{/* 弹窗操作按钮 */}
|
||
<div className="pl-4 border-l border-white/10">
|
||
{/* 关闭按钮 */}
|
||
<motion.button
|
||
className="p-2 rounded-full hover:bg-white/10 transition-colors"
|
||
onClick={onClose}
|
||
whileHover={{ rotate: 90 }}
|
||
whileTap={{ scale: 0.9 }}
|
||
>
|
||
<X className="w-5 h-5 text-white/70" />
|
||
</motion.button>
|
||
</div>
|
||
</div>
|
||
|
||
{/* 内容区域 */}
|
||
<div className="overflow-y-auto p-4" style={{ height: 'calc(100% - 8rem)' }}>
|
||
<AnimatePresence mode="wait">
|
||
<motion.div
|
||
key={`${activeTab}-${resetKey}`}
|
||
initial={{ opacity: 0, y: 20 }}
|
||
animate={{ opacity: 1, y: 0 }}
|
||
exit={{ opacity: 0, y: -20 }}
|
||
transition={{ duration: 0.2 }}
|
||
className="h-full"
|
||
>
|
||
{renderTabContent()}
|
||
</motion.div>
|
||
</AnimatePresence>
|
||
</div>
|
||
|
||
{/* 底部操作栏 */}
|
||
<div className="p-4 border-t border-white/10 bg-black/20">
|
||
<div className="flex justify-end gap-3">
|
||
<motion.button
|
||
className="px-4 py-2 rounded-lg bg-white/10 text-white hover:bg-white/20 transition-colors"
|
||
whileHover={{ scale: 1.02 }}
|
||
whileTap={{ scale: 0.98 }}
|
||
onClick={handleReset}
|
||
>
|
||
Reset
|
||
</motion.button>
|
||
<motion.button
|
||
className="px-4 py-2 rounded-lg bg-blue-500 text-white hover:bg-blue-600 transition-colors"
|
||
whileHover={{ scale: 1.02 }}
|
||
whileTap={{ scale: 0.98 }}
|
||
onClick={() => {handleSave()}}
|
||
>
|
||
Apply
|
||
</motion.button>
|
||
</div>
|
||
</div>
|
||
</motion.div>
|
||
</div>
|
||
|
||
{/* 提醒用户 点击保存 工作流将回退 并重新执行工作流 */}
|
||
<FloatingGlassPanel
|
||
open={isRemindFallbackOpen}
|
||
width='500px'
|
||
clickMaskClose={false}
|
||
>
|
||
<div className="flex flex-col items-center gap-4 text-white py-4">
|
||
<div className="flex items-center gap-3">
|
||
<TriangleAlert className="w-6 h-6 text-yellow-400" />
|
||
<p className="text-lg font-medium">{remindFallbackText}</p>
|
||
</div>
|
||
|
||
<div className="flex gap-3 mt-2">
|
||
<button
|
||
onClick={() => handleConfirmGotoFallback()}
|
||
data-alt="confirm-replace-button"
|
||
className="px-4 py-2 bg-blue-600 hover:bg-blue-700 rounded-md transition-colors duration-200 flex items-center gap-2"
|
||
>
|
||
<Undo2 className="w-4 h-4" />
|
||
Apply
|
||
</button>
|
||
|
||
<button
|
||
onClick={() => handleCloseRemindFallbackPanel()}
|
||
data-alt="ignore-button"
|
||
className="px-4 py-2 bg-gray-600 hover:bg-gray-700 rounded-md transition-colors duration-200 flex items-center gap-2"
|
||
>
|
||
<X className="w-4 h-4" />
|
||
Cancel
|
||
</button>
|
||
</div>
|
||
</div>
|
||
</FloatingGlassPanel>
|
||
|
||
{/* 提醒用户 重置当前tab修改的数据 */}
|
||
<FloatingGlassPanel
|
||
open={isRemindResetOpen}
|
||
width='500px'
|
||
clickMaskClose={false}
|
||
>
|
||
<div className="flex flex-col items-center gap-4 text-white py-4">
|
||
<div className="flex items-center gap-3">
|
||
<TriangleAlert className="w-6 h-6 text-yellow-400" />
|
||
<p className="text-lg font-medium">Are you sure you want to reset the current tab?</p>
|
||
</div>
|
||
<div className="flex gap-3 mt-2">
|
||
<button
|
||
onClick={() => handleConfirmReset()}
|
||
data-alt="confirm-replace-button"
|
||
className="px-4 py-2 bg-blue-600 hover:bg-blue-700 rounded-md transition-colors duration-200 flex items-center gap-2"
|
||
>
|
||
<Undo2 className="w-4 h-4" />
|
||
Reset
|
||
</button>
|
||
|
||
<button
|
||
onClick={() => setIsRemindResetOpen(false)}
|
||
data-alt="ignore-button"
|
||
className="px-4 py-2 bg-gray-600 hover:bg-gray-700 rounded-md transition-colors duration-200 flex items-center gap-2"
|
||
>
|
||
<X className="w-4 h-4" />
|
||
Cancel
|
||
</button>
|
||
</div>
|
||
</div>
|
||
</FloatingGlassPanel>
|
||
</>
|
||
)}
|
||
</AnimatePresence>
|
||
);
|
||
}
|