video-flow-b/components/ui/edit-modal.tsx
2025-09-06 16:58:21 +08:00

491 lines
17 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

'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';
import { TaskObject } from '@/api/DTO/movieEdit';
interface EditModalProps {
isOpen: boolean;
onClose: () => void;
activeEditTab: string;
currentSketchIndex: number;
roles?: any[];
setIsPauseWorkFlow: (isPauseWorkFlow: boolean) => void;
isPauseWorkFlow: boolean;
fallbackToStep: any;
originalText?: string;
taskObject?: TaskObject;
}
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,
taskObject
}: 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);
const shotTabContentRef = useRef<any>(null);
// 添加一个状态来标记是否是从切换tab触发的提醒
const [pendingSwitchTabId, setPendingSwitchTabId] = useState<string | null>(null);
const [disabledBtn, setDisabledBtn] = useState(false);
const [isRemindCloseOpen, setIsRemindCloseOpen] = useState(false);
useEffect(() => {
setCurrentIndex(currentSketchIndex);
}, [isOpen]);
// 当 activeEditTab 改变时更新 activeTab
useEffect(() => {
setActiveTab(activeEditTab);
}, [activeEditTab]);
const isTabDisabled = (tabId: string) => {
if (tabId === 'settings') return false;
// 换成 如果对应标签下 数据存在 就不禁用
if (tabId === '1') return taskObject?.roles.data.length === 0;
// if (tabId === '2') return taskScenes.length === 0;
if (tabId === '3') return taskObject?.videos.data.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);
}
} else if (activeTab === '3') {
const shotTabContent = shotTabContentRef.current;
if (shotTabContent) {
return shotTabContent.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 = () => {
window.msg.error('No permission!');
return;
console.log('handleSave');
// setIsRemindFallbackOpen(true);
if (activeTab === '0') {
scriptTabContentRef.current.saveOrCloseBefore();
} else if (activeTab === '1') {
characterTabContentRef.current.saveOrCloseBefore();
} else if (activeTab === '3') {
shotTabContentRef.current.saveOrCloseBefore('apply');
}
}
const handleApply = () => {
console.log('handleApply');
handleConfirmGotoFallback();
}
const handleConfirmGotoFallback = () => {
window.msg.error('No permission!');
return;
setDisabledBtn(true);
console.log('handleConfirmGotoFallback');
SaveEditUseCase.saveData();
if (activeTab === '0') {
fallbackToStep('script');
// 应用剧本
} else {
fallbackToStep('video');
}
setIsRemindFallbackOpen(false);
// 关闭弹窗
onClose();
setDisabledBtn(false);
}
const handleCloseRemindFallbackPanel = () => {
if (pendingSwitchTabId) {
// 如果是从切换tab触发的提醒关闭后执行tab切换
setActiveTab(pendingSwitchTabId);
setCurrentIndex(0);
setPendingSwitchTabId(null); // 清除记录
}
setIsRemindFallbackOpen(false);
}
const handleReset = () => {
window.msg.error('No permission!');
return;
console.log('handleReset');
// 重置当前tab修改的数据
setIsRemindResetOpen(true);
}
const handleConfirmReset = () => {
console.log('handleConfirmReset');
setIsRemindResetOpen(false);
setResetKey(resetKey + 1);
}
const handleClickClose = () => {
// TODO 关闭前 检查 当前tab 下是否有更新 如果有更新 则提醒用户 是否确认应用
// 暂时 默认弹出提醒
if (activeTab === '0') {
scriptTabContentRef.current.saveOrCloseBefore();
} else if (activeTab === '1') {
characterTabContentRef.current.saveOrCloseBefore();
} else if (activeTab === '3') {
shotTabContentRef.current.saveOrCloseBefore('close');
}
}
const handleConfirmApply = () => {
console.log('handleConfirmApply');
setIsRemindCloseOpen(false);
handleConfirmGotoFallback();
}
const handleCloseRemindClosePanel = () => {
setIsRemindCloseOpen(false);
onClose();
}
const renderTabContent = () => {
switch (activeTab) {
case '0':
return (
<ScriptTabContent
ref={scriptTabContentRef}
setIsPauseWorkFlow={setIsPauseWorkFlow}
isPauseWorkFlow={isPauseWorkFlow}
originalText={originalText}
onApply={handleApply}
setActiveTab={setActiveTab}
onClose={onClose}
/>
);
case '1':
return (
<CharacterTabContent
ref={characterTabContentRef}
onClose={onClose}
onApply={handleApply}
setActiveTab={setActiveTab}
originalRoles={taskObject?.roles.data || []}
/>
);
case '2':
return (
<SceneTabContent
currentSketchIndex={currentIndex}
/>
);
case '3':
return (
<ShotTabContent
ref={shotTabContentRef}
originalVideos={taskObject?.videos.data || []}
currentSketchIndex={currentIndex}
onApply={handleApply}
onClose={onClose}
setActiveTab={setActiveTab}
/>
);
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 }}
/>
{/* 弹窗内容 */}
<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={handleClickClose}
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 disabled:opacity-50 disabled:cursor-not-allowed"
whileHover={{ scale: 1.02 }}
whileTap={{ scale: 0.98 }}
onClick={handleReset}
disabled={disabledBtn}
>
Reset
</motion.button>
<motion.button
className="px-4 py-2 rounded-lg bg-blue-500 text-white hover:bg-blue-600 transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
whileHover={{ scale: 1.02 }}
whileTap={{ scale: 0.98 }}
onClick={() => {handleSave()}}
disabled={disabledBtn}
>
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>
{/* 提醒用户 关闭当前弹窗 是否确认应用 */}
<FloatingGlassPanel
open={isRemindCloseOpen}
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">If you have modified the content, closing it will lose the modified content. Do you want to apply the modifications?</p>
</div>
<div className="flex gap-3 mt-2">
<button
onClick={() => handleConfirmApply()}
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={() => handleCloseRemindClosePanel()}
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" />
Ignore
</button>
</div>
</div>
</FloatingGlassPanel>
</>
)}
</AnimatePresence>
);
}