"use client"; import { useState, useEffect, useRef } from "react"; import { ChevronDown, ChevronUp, Video, Loader2, Lightbulb, Package, Crown, Clapperboard, Globe, Clock, Trash2, LayoutTemplate, ImagePlay, Sparkles, Settings, MoreHorizontal, } from "lucide-react"; import { Dropdown, Modal, Tooltip, Upload, Popconfirm, Image, Popover, } from "antd"; import { UploadOutlined } from "@ant-design/icons"; import { StoryTemplateEntity } from "@/app/service/domain/Entities"; import { useImageStoryServiceHook } from "@/app/service/Interaction/ImageStoryService"; import TemplateCard from "./templateCard"; import { AudioRecorder } from "./AudioRecorder"; import { useTemplateStoryServiceHook } from "@/app/service/Interaction/templateStoryService"; import { useRouter } from "next/navigation"; import { createMovieProjectV1 } from "@/api/video_flow"; import { MovieProjectService, MovieProjectMode, } from "@/app/service/Interaction/MovieProjectService"; import { useLoadScriptText, useUploadFile } from "@/app/service/domain/service"; import { ActionButton } from "../common/ActionButton"; import { HighlightEditor } from "../common/HighlightEditor"; import GlobalLoad from "../common/GlobalLoad"; /**模板故事模式弹窗组件 */ /** * 防抖函数 * @param {Function} func - 需要防抖的函数 * @param {number} wait - 等待时间(ms) * @returns {Function} - 防抖后的函数 */ const debounce = (func: Function, wait: number) => { let timeout: NodeJS.Timeout; return function executedFunction(...args: any[]) { const later = () => { clearTimeout(timeout); func(...args); }; clearTimeout(timeout); timeout = setTimeout(later, wait); }; }; const RenderTemplateStoryMode = ({ isTemplateCreating, setIsTemplateCreating, isRoleGenerating, setIsRoleGenerating, isItemGenerating, setIsItemGenerating, isOpen, onClose, configOptions = { mode: "auto" as "auto" | "manual", resolution: "720p" as "720p" | "1080p" | "4k", language: "english", videoDuration: "1min", }, }: { isOpen: boolean; onClose: () => void; isTemplateCreating: boolean; setIsTemplateCreating: (value: boolean) => void; isRoleGenerating: { [key: string]: boolean }; setIsRoleGenerating: (value: { [key: string]: boolean } | ((prev: { [key: string]: boolean }) => { [key: string]: boolean })) => void; isItemGenerating: { [key: string]: boolean }; setIsItemGenerating: (value: { [key: string]: boolean } | ((prev: { [key: string]: boolean }) => { [key: string]: boolean })) => void; configOptions: { mode: "auto" | "manual"; resolution: "720p" | "1080p" | "4k"; language: string; videoDuration: string; }; }) => { // 使用 hook 管理状态 const { templateStoryList, selectedTemplate, isLoading, getTemplateStoryList, actionStory, setSelectedTemplate, AvatarAndAnalyzeFeatures, updateRoleImage, updateItemImage, handleRoleFieldBlur, handleItemFieldBlur, clearData, } = useTemplateStoryServiceHook(); // 防抖处理的输入更新函数 const debouncedUpdateInput = debounce((value: string) => { // 过滤特殊字符 const sanitizedValue = value.replace(/[<>]/g, ''); // 更新输入值 if (!selectedTemplate?.freeInputItem) return; const updatedTemplate: StoryTemplateEntity = { ...selectedTemplate, freeInputItem: { ...selectedTemplate.freeInputItem, free_input_text: sanitizedValue } }; setSelectedTemplate(updatedTemplate); }, 300); // 300ms 的防抖延迟 // 使用上传文件hook const { uploadFile, isUploading } = useUploadFile(); // 本地加载状态,用于 UI 反馈 const [localLoading, setLocalLoading] = useState(0); // 控制输入框显示状态 const [inputVisible, setInputVisible] = useState<{ [key: string]: boolean }>( {} ); const router = useRouter(); // 组件挂载时获取模板列表 useEffect(() => { if (isOpen) { getTemplateStoryList(); } }, [isOpen, getTemplateStoryList]); // 监听点击外部区域关闭输入框 useEffect(() => { const handleClickOutside = (event: MouseEvent) => { const target = event.target as Element; // 检查是否点击了输入框相关的元素 if ( !target.closest(".ant-tooltip") && !target.closest('[data-alt*="field-ai-button"]') ) { // 关闭所有打开的输入框 setInputVisible({}); } }; document.addEventListener("mousedown", handleClickOutside); return () => { document.removeEventListener("mousedown", handleClickOutside); }; }, []); // 处理模板选择 const handleTemplateSelect = (template: StoryTemplateEntity) => { setSelectedTemplate(template); }; // 处理确认操作 const handleConfirm = async () => { if (!selectedTemplate) return; if (isTemplateCreating) return; setIsTemplateCreating(true); let timer: NodeJS.Timeout | null = null; try { // 获取当前用户信息 const User = JSON.parse(localStorage.getItem("currentUser") || "{}"); if (!User.id) { console.error("用户未登录"); return; } // 启动进度条动画 timer = setInterval(() => { setLocalLoading((prev) => { if (prev >= 95) { return 95; } return prev + 0.1; }); }, 100); setLocalLoading(1); const projectId = await actionStory( String(User.id), configOptions.mode, configOptions.resolution, configOptions.language ); if (projectId) { // 跳转到电影详情页 router.push(`/movies/work-flow?episodeId=${projectId}`); onClose(); // 重置状态 setSelectedTemplate(null); } console.log("Story action created:", projectId); } catch (error) { console.error("Failed to create story action:", error); setIsTemplateCreating(false); // 这里可以添加 toast 提示 onClose(); // 重置状态 setSelectedTemplate(null); } finally { setLocalLoading(0); if (timer) { clearInterval(timer); } } }; // 模板列表渲染 const templateListRender = () => { return (
{templateStoryList.map((template, index) => (
handleTemplateSelect(template)} >
))}
); }; // 故事编辑器渲染 const storyEditorRender = () => { return selectedTemplate ? (
{/* 模板信息头部 - 增加顶部空间 */}
{/* 左侧图片 */}
{selectedTemplate.name}
{/* 右侧信息 - 增加文本渲染空间 */}

{selectedTemplate.name}

{selectedTemplate.generateText}

{/* 角色配置区域 */} {selectedTemplate?.storyRole && selectedTemplate.storyRole.length > 0 && (

Character Configuration

{selectedTemplate.storyRole.map((role, index) => (
{/* 图片容器 */}
{ // 更新角色的描述字段 const updatedTemplate = { ...selectedTemplate!, storyRole: selectedTemplate!.storyRole.map( (r) => r.role_name === role.role_name ? { ...r, role_description: e.target.value, } : r ), }; setSelectedTemplate(updatedTemplate); }} placeholder={role.user_tips} className="w-[30rem] px-3 py-2 pr-16 bg-white/0 border border-white/10 rounded-lg text-white placeholder-gray-400 focus:outline-none focus:border-blue-500 focus:ring-1 focus:ring-blue-500/30 transition-all duration-200 text-sm" />
{/* AI生成按钮 */} { if ( role.role_description && role.role_description.trim() ) { setIsRoleGenerating(prev => ({...prev, [role.role_name]: true})); try { await handleRoleFieldBlur( role.role_name, role.role_description.trim() ); } finally { setIsRoleGenerating(prev => ({...prev, [role.role_name]: false})); } } setInputVisible((prev) => ({ ...prev, [role.role_name]: false, })); }} icon={} width="w-8" height="h-8" disabled={isRoleGenerating[role.role_name] || false} />
} placement="top" classNames={{ root: "max-w-none", }} open={inputVisible[role.role_name]} onOpenChange={(visible) => setInputVisible((prev) => ({ ...prev, [role.role_name]: visible, })) } trigger="contextMenu" styles={{ root: { zIndex: 1000 } }} > {/* 图片 */}
{role.role_name}
{/* 角色名称 - 图片下方 */}
{role.role_name}
{/* 按钮组 - 右上角 */}
{/* AI生成按钮 */} {/* 上传按钮 */} { const isImage = file.type.startsWith("image/"); if (!isImage) { console.error("只能上传图片文件"); return false; } const isLt5M = file.size / 1024 / 1024 < 5; if (!isLt5M) { console.error("图片大小不能超过5MB"); return false; } return true; }} customRequest={async ({ file, onSuccess, onError, }) => { try { const fileObj = file as File; const uploadedUrl = await uploadFile( fileObj, (progress) => { console.log(`上传进度: ${progress}%`); } ); await AvatarAndAnalyzeFeatures( uploadedUrl, role.role_name ); onSuccess?.(uploadedUrl); } catch (error) { console.error("角色图片上传失败:", error); onError?.(error as Error); } }} >
))}
)} {/* 道具配置区域 */} {selectedTemplate?.storyItem && selectedTemplate.storyItem.length > 0 && (

props Configuration

{selectedTemplate.storyItem.map((item, index) => (
{/* 图片容器 */}
{ // 更新道具的描述字段 const updatedTemplate = { ...selectedTemplate!, storyItem: selectedTemplate!.storyItem.map( (i) => i.item_name === item.item_name ? { ...i, item_description: e.target.value, } : i ), }; setSelectedTemplate(updatedTemplate); }} placeholder="Enter description for AI image generation..." className="w-[30rem] px-3 py-2 pr-16 bg-white/0 border border-white/10 rounded-lg text-white placeholder-gray-400 focus:outline-none focus:border-blue-500 focus:ring-1 focus:ring-blue-500/30 transition-all duration-200 text-sm" />
{/* AI生成按钮 */} { if ( item.item_description && item.item_description.trim() ) { setIsItemGenerating(prev => ({...prev, [item.item_name]: true})); try { await handleItemFieldBlur( item.item_name, item.item_description.trim() ); } finally { setIsItemGenerating(prev => ({...prev, [item.item_name]: false})); } } setInputVisible((prev) => ({ ...prev, [item.item_name]: false, })); }} icon={} width="w-8" height="h-8" disabled={isItemGenerating[item.item_name] || false} />
} placement="top" classNames={{ root: "max-w-none", }} open={inputVisible[item.item_name]} onOpenChange={(visible) => setInputVisible((prev) => ({ ...prev, [item.item_name]: visible, })) } trigger="contextMenu" styles={{ root: { zIndex: 1000 } }} > {/* 图片 */}
{item.item_name}
{/* 道具名称 - 图片下方 */}
{item.item_name}
{/* 按钮组 - 右上角 */}
{/* AI生成按钮 */} {/* 上传按钮 */} { const isImage = file.type.startsWith("image/"); if (!isImage) { console.error("只能上传图片文件"); return false; } const isLt5M = file.size / 1024 / 1024 < 5; if (!isLt5M) { console.error("图片大小不能超过5MB"); return false; } return true; }} customRequest={async ({ file, onSuccess, onError, }) => { try { const fileObj = file as File; const uploadedUrl = await uploadFile( fileObj, (progress) => { console.log(`上传进度: ${progress}%`); } ); updateItemImage(item.item_name, uploadedUrl); onSuccess?.(uploadedUrl); } catch (error) { console.error("道具图片上传失败:", error); onError?.(error as Error); } }} >
))}
)} {/* * 自由输入文字 {(selectedTemplate?.freeInputItem) && (

input Configuration