From 2132805acd81c7990f78c0c3fb2cdcdb367d092b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=8C=97=E6=9E=B3?= <7854742+wang_rumeng@user.noreply.gitee.com> Date: Tue, 5 Aug 2025 18:48:59 +0800 Subject: [PATCH] =?UTF-8?q?=E6=96=B0=E5=A2=9E=E8=AF=AD=E8=A8=80=E9=80=89?= =?UTF-8?q?=E6=8B=A9=E5=8A=9F=E8=83=BD=EF=BC=8C=E4=BC=98=E5=8C=96=E8=A7=86?= =?UTF-8?q?=E9=A2=91=E5=88=9B=E5=BB=BA=E9=A1=B5=E9=9D=A2=EF=BC=8C=E7=A7=BB?= =?UTF-8?q?=E9=99=A4=E4=B8=8D=E5=BF=85=E8=A6=81=E7=9A=84=E7=8A=B6=E6=80=81?= =?UTF-8?q?=E7=AE=A1=E7=90=86=EF=BC=8C=E6=9B=B4=E6=96=B0=E5=89=A7=E6=9C=AC?= =?UTF-8?q?=E6=B8=B2=E6=9F=93=E9=80=BB=E8=BE=91=E4=BB=A5=E6=94=AF=E6=8C=81?= =?UTF-8?q?=E5=8A=A8=E6=80=81=E5=8A=A0=E8=BD=BD=E5=92=8C=E5=8D=A0=E4=BD=8D?= =?UTF-8?q?=E7=AC=A6=E6=98=BE=E7=A4=BA=E3=80=82?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- components/pages/create-to-video2.tsx | 118 +++++++++++--- components/pages/work-flow.tsx | 2 + components/pages/work-flow/media-viewer.tsx | 20 ++- .../pages/work-flow/use-workflow-data.tsx | 2 + components/script-renderer/ScriptRenderer.tsx | 151 +++++++++++++----- components/script-renderer/mock.ts | 42 ++++- components/script-renderer/types.ts | 42 ++++- components/ui/select-dropdown.tsx | 9 +- 8 files changed, 318 insertions(+), 68 deletions(-) diff --git a/components/pages/create-to-video2.tsx b/components/pages/create-to-video2.tsx index 8f2338a..442f46b 100644 --- a/components/pages/create-to-video2.tsx +++ b/components/pages/create-to-video2.tsx @@ -1,10 +1,8 @@ "use client"; import { useState, useEffect, useRef, useCallback } from 'react'; -import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'; -import { ArrowLeft, ChevronDown, ChevronUp, Video, ListOrdered, Play, Loader2, Pause, MoreHorizontal, Edit2, Check, X, RefreshCw, Lightbulb, Package, Crown, ArrowUp, Search, Filter, Grid, Grid3X3, Calendar, Clock, Eye, Heart, Share2 } from 'lucide-react'; +import { ArrowLeft, ChevronDown, ChevronUp, Video, ListOrdered, Play, Loader2, Pause, MoreHorizontal, Edit2, Check, X, RefreshCw, Lightbulb, Package, Crown, ArrowUp, Search, Filter, Grid, Grid3X3, Calendar, Clock, Eye, Heart, Share2, Globe } from 'lucide-react'; import { useRouter, useSearchParams } from 'next/navigation'; -import { Sheet, SheetContent, SheetHeader, SheetTitle, SheetTrigger, SheetClose } from "@/components/ui/sheet"; import { Input } from "@/components/ui/input"; import './style/create-to-video2.css'; import { Dropdown, Menu } from 'antd'; @@ -14,7 +12,6 @@ import { ModeEnum, ResolutionEnum } from "@/app/model/enums"; import { createScriptEpisodeNew, getScriptEpisodeListNew, CreateScriptEpisodeRequest } from "@/api/script_episode"; import { getUploadToken, uploadToQiniu } from "@/api/common"; import { EmptyStateAnimation } from '@/components/common/EmptyStateAnimation2'; -import { ScriptEditDialog } from '@/components/script-edit-dialog'; const ideaText = 'a cute capybara with an orange on its head, staring into the distance and walking forward'; @@ -32,6 +29,7 @@ export function CreateToVideo2() { const [isFocus, setIsFocus] = useState(false); const [selectedMode, setSelectedMode] = useState(ModeEnum.AUTOMATIC); const [selectedResolution, setSelectedResolution] = useState(ResolutionEnum.HD_720P); + const [selectedLanguage, setSelectedLanguage] = useState('en'); const [script, setInputText] = useState(''); const editorRef = useRef(null); const [runTour, setRunTour] = useState(true); @@ -45,15 +43,12 @@ export function CreateToVideo2() { const [limit, setLimit] = useState(12); const [isLoading, setIsLoading] = useState(false); const [hasMore, setHasMore] = useState(true); - const [searchQuery, setSearchQuery] = useState(''); - const [filterStatus, setFilterStatus] = useState('all'); - const [sortBy, setSortBy] = useState('created_at'); const [isLoadingMore, setIsLoadingMore] = useState(false); const scrollContainerRef = useRef(null); - const [isSmartAssistantExpanded, setIsSmartAssistantExpanded] = useState(false); const [userId, setUserId] = useState(0); const [isComposing, setIsComposing] = useState(false); - const [isScriptEditDialogOpen, setIsScriptEditDialogOpen] = useState(false); + const [loadingIdea, setLoadingIdea] = useState(false); + // 在客户端挂载后读取localStorage useEffect(() => { if (typeof window !== 'undefined') { @@ -135,8 +130,6 @@ export function CreateToVideo2() { } const handleCreateVideo = async () => { - setIsScriptEditDialogOpen(true); - return; setIsCreating(true); // 创建剧集数据 let episodeData: any = { @@ -242,6 +235,53 @@ export function CreateToVideo2() { }, ]; + // 语言选项配置 + const languageItems: MenuProps['items'] = [ + { + type: 'group', + label: ( +
Language
+ ), + children: [ + { + key: 'en', + label: ( +
+ English +
+ ), + }, + { + key: 'zh', + label: ( +
+ Chinese + +
+ ), + }, + { + key: 'ja', + label: ( +
+ Japanese + +
+ ), + }, + { + key: 'ko', + label: ( +
+ Korean + +
+ ), + }, + ], + }, + ]; + // 处理模式选择 const handleModeSelect: MenuProps['onClick'] = ({ key }) => { setSelectedMode(key as ModeEnum); @@ -252,6 +292,21 @@ export function CreateToVideo2() { setSelectedResolution(key as ResolutionEnum); }; + // 处理语言选择 + const handleLanguageSelect: MenuProps['onClick'] = ({ key }) => { + setSelectedLanguage(key as string); + }; + + // 处理获取想法 + const handleGetIdea = () => { + if (loadingIdea) return; + setLoadingIdea(true); + setTimeout(() => { + setInputText('idea'); + setLoadingIdea(false); + }, 3000); + }; + const handleStartCreating = () => { setActiveTab('script'); setInputText(ideaText); @@ -402,6 +457,8 @@ export function CreateToVideo2() { className="w-full h-full object-cover" muted loop + preload="none" + poster={`${episode.final_video_url}?vframe/jpg/offset/1`} onMouseEnter={(e) => (e.target as HTMLVideoElement).play()} onMouseLeave={(e) => (e.target as HTMLVideoElement).pause()} /> @@ -618,9 +675,15 @@ export function CreateToVideo2() { Describe the content you want to action. Get an setInputText(ideaText)} + onClick={() => handleGetIdea()} > - idea + {loadingIdea ? ( + + ) : ( + <> + idea + + )} @@ -669,6 +732,28 @@ export function CreateToVideo2() { + + {/* 语言 */} + +
+ + + {selectedLanguage === 'en' ? 'English' : + selectedLanguage === 'zh' ? 'Chinese' : + selectedLanguage === 'ja' ? 'Japanese' : 'Korean'} + + +
+
@@ -698,13 +783,6 @@ export function CreateToVideo2() { )} - - {isScriptEditDialogOpen && ( - setIsScriptEditDialogOpen(false)} - /> - )} ); } diff --git a/components/pages/work-flow.tsx b/components/pages/work-flow.tsx index 11a16fe..0bb85bf 100644 --- a/components/pages/work-flow.tsx +++ b/components/pages/work-flow.tsx @@ -22,6 +22,7 @@ export default function WorkFlow() { // 使用自定义 hooks 管理状态 const { taskObject, + scriptData, taskSketch, taskScenes, taskShotSketch, @@ -170,6 +171,7 @@ export default function WorkFlow() {
{ return (
- + { + scriptData ? ( + + ) : ( +
+
+ + + +
+
+ +
+
+ ) + }
); }; diff --git a/components/pages/work-flow/use-workflow-data.tsx b/components/pages/work-flow/use-workflow-data.tsx index f3b1629..906a3c8 100644 --- a/components/pages/work-flow/use-workflow-data.tsx +++ b/components/pages/work-flow/use-workflow-data.tsx @@ -57,6 +57,7 @@ export function useWorkflowData() { // 更新 taskObject 的类型 const [taskObject, setTaskObject] = useState(null); + const [scriptData, setScriptData] = useState(null); const [taskSketch, setTaskSketch] = useState([]); const [taskScenes, setTaskScenes] = useState([]); const [taskShotSketch, setTaskShotSketch] = useState([]); @@ -554,6 +555,7 @@ export function useWorkflowData() { return { taskObject, + scriptData, taskSketch, taskScenes, taskShotSketch, diff --git a/components/script-renderer/ScriptRenderer.tsx b/components/script-renderer/ScriptRenderer.tsx index 3125294..5db2227 100644 --- a/components/script-renderer/ScriptRenderer.tsx +++ b/components/script-renderer/ScriptRenderer.tsx @@ -1,10 +1,11 @@ -import React, { useRef, useState } from 'react'; +import React, { useRef, useState, useMemo, useEffect } from 'react'; import { motion, AnimatePresence } from 'framer-motion'; -import { SquarePen, Lightbulb, Navigation, Globe, Copy, SendHorizontal } from 'lucide-react'; -import { ScriptData, ScriptBlock, ScriptContent } from './types'; +import { SquarePen, Lightbulb, Navigation, Globe, Copy, SendHorizontal, X, Plus } from 'lucide-react'; +import { ScriptData, ScriptBlock, ScriptContent, ThemeTagBgColor, ThemeType } from './types'; import ContentEditable, { ContentEditableEvent } from 'react-contenteditable'; import { toast } from 'sonner'; - +import { SelectDropdown } from '@/components/ui/select-dropdown'; +import { TypewriterText } from '@/components/workflow/work-office/common/TypewriterText'; interface ScriptRendererProps { data: ScriptData; @@ -16,6 +17,12 @@ export const ScriptRenderer: React.FC = ({ data }) => { const contentRefs = useRef<{ [key: string]: HTMLDivElement | null }>({}); const [editBlockId, setEditBlockId] = useState(null); const contentEditableRef = useRef(null); + const [addThemeTag, setAddThemeTag] = useState(null); + const [isInit, setIsInit] = useState(true); + + useEffect(() => { + setEditBlockId(null); + }, [activeBlockId]); const scrollToBlock = (blockId: string) => { const element = contentRefs.current[blockId]; @@ -25,6 +32,14 @@ export const ScriptRenderer: React.FC = ({ data }) => { } }; + // 使用 useMemo 缓存标签颜色映射 + const randomThemeTagBgColor = useMemo(() => { + return Object.values(ThemeTagBgColor).reduce((acc: Record, color: string) => { + acc[color] = color; + return acc; + }, {}); + }, []); + // 用于渲染展示的 JSX const renderContent = (content: ScriptContent) => { switch (content.type) { @@ -35,7 +50,15 @@ export const ScriptRenderer: React.FC = ({ data }) => { case 'italic': return {content.text}; default: - return

{content.text}

; + return

+ { + isInit ? ( + + ) : ( + {content.text} + ) + } +

; } }; @@ -65,6 +88,11 @@ export const ScriptRenderer: React.FC = ({ data }) => { console.log(e.target.value); }; + const handleBlockTextBlur = (block: ScriptBlock) => (e: ContentEditableEvent) => { + console.log(e.target.value); + setEditBlockId(null); + }; + const renderEditBlock = (block: ScriptBlock) => { let blockHtmlText = ''; block.content.forEach(item => { @@ -76,6 +104,8 @@ export const ScriptRenderer: React.FC = ({ data }) => { innerRef={contentEditableRef} html={formatTextToHtml(blockHtmlText)} onChange={handleBlockTextChange(block)} + onBlur={handleBlockTextBlur(block)} + autoFocus={true} className="block w-full min-h-[120px] bg-white/5 backdrop-blur-md p-4 text-white/90 rounded-lg border-unset outline-none pb-12 whitespace-pre-wrap break-words" @@ -84,6 +114,78 @@ export const ScriptRenderer: React.FC = ({ data }) => { ); }; + const renderTypeBlock = (block: ScriptBlock, isHovered: boolean, isActive: boolean, isEditing: boolean) => { + switch (block.type) { + case 'theme': + return ( +
+ {block.content.map((item, index) => ( +
+ {item.text} + console.log(item.text)} /> +
+ ))} + {/* 新增主题标签 */} +
+
+ ({ label: type, value: type }))} + value={addThemeTag} + placeholder="Select Theme Type" + onChange={() => console.log('主题类型')} + /> +
+ +
+
+ ) + default: + return ( + <> + + {(isHovered || isActive) && ( + + { + setEditBlockId(block.id); + setActiveBlockId(block.id); + setIsInit(false); + }} + /> + { + navigator.clipboard.writeText(block.content.map(item => item.text).join('\n')); + toast.success('Copied!'); + }} + /> + + )} + +
+ {isEditing ? ( + renderEditBlock(block) + ) : ( + block.content.map((item, index) => ( +
{renderContent(item)}
+ )) + )} +
+ + ); + } + }; + const renderBlock = (block: ScriptBlock) => { const isHovered = hoveredBlockId === block.id; const isActive = activeBlockId === block.id; @@ -93,7 +195,7 @@ export const ScriptRenderer: React.FC = ({ data }) => { (contentRefs.current[block.id] = el)} onMouseEnter={() => setHoveredBlockId(block.id)} onMouseLeave={() => setHoveredBlockId(null)} @@ -102,40 +204,9 @@ export const ScriptRenderer: React.FC = ({ data }) => { transition={{ duration: 0.3 }} >

{block.title}

- - {(isHovered || isActive) && ( - - { - setEditBlockId(block.id); - setActiveBlockId(block.id); - }} - /> - { - navigator.clipboard.writeText(block.content.map(item => item.text).join('\n')); - toast.success('Copied!'); - }} - /> - - )} - -
- {isEditing ? ( - renderEditBlock(block) - ) : ( - block.content.map((item, index) => ( -
{renderContent(item)}
- )) - )} -
+ { + renderTypeBlock(block, isHovered, isActive, isEditing) + }
); }; diff --git a/components/script-renderer/mock.ts b/components/script-renderer/mock.ts index 6c295dc..99c5020 100644 --- a/components/script-renderer/mock.ts +++ b/components/script-renderer/mock.ts @@ -4,16 +4,54 @@ export const mockScriptData: ScriptData = { blocks: [ { id: 'core', - title: "SCENE'S CORE CONFLICT", + title: "SUMMARY", type: 'core', content: [ { type: 'paragraph', text: 'The desperate, on-stage improvisation of a failing comedian triggers a physical and professional collapse, forcing him and his partner to confront the absurd meaninglessness of their ambitions. This scene serves as the inciting incident and rising action, where the internal conflict of artistic decay manifests as a literal, catastrophic failure, propelling them from a state of professional anxiety into a fight for survival.' + } + ] + }, + { + id: 'theme', + title: 'THEME', + type: 'theme', + content: [ + { + type: 'tag', + text: 'Satire' + }, + { + type: 'tag', + text: 'Absurdist Comedy' + }, + { + type: 'tag', + text: 'Disaster' + } + ] + }, + { + id: 'roles', + title: 'ROLES', + type: 'roles', + content: [ + { + type: 'bold', + text: 'Anna' + }, + { + type: 'paragraph', + text: 'Anna is a young woman who is trying to find her place in the world. She is a bit of a mess, but she is also a bit of a mess.' }, { type: 'bold', - text: 'GENRE: Satire | Absurdist Comedy | Disaster' + text: 'Mark' + }, + { + type: 'card', + text: 'Mark starts as resentful and aimless, harboring guilt over past family conflicts. His involvement in Anna’s quest rekindles his sense of responsibility and belonging. He moves from avoidance and cynicism to active support, ultimately risking his own safety for Anna and the creature. Mark’s arc is one of redemption and reconnection.' } ] }, diff --git a/components/script-renderer/types.ts b/components/script-renderer/types.ts index 440c9d7..ae01027 100644 --- a/components/script-renderer/types.ts +++ b/components/script-renderer/types.ts @@ -2,13 +2,20 @@ export interface ScriptBlock { id: string; title: string; content: ScriptContent[]; - type: 'core' | 'scene' | 'summary'; + type: 'core' | 'scene' | 'summary' | 'theme' | 'roles'; sceneNumber?: number; } export interface ScriptContent { - type: 'paragraph' | 'bold' | 'italic' | 'heading'; - text: string; + type: 'paragraph' | 'bold' | 'italic' | 'heading' | 'tag' | 'card'; + text?: string; + roleInfo?: { + name: string; + gender: string; + role: string; + desc: string; + color: string; + }; } export interface ScriptData { @@ -23,4 +30,33 @@ export interface NavigationItem { id: string; title: string; type: 'core' | 'scene' | 'summary'; +} + +// 主题标签背景色 enum +export enum ThemeTagBgColor { + blue = 'bg-blue-500/20', + green = 'bg-green-500/20', + red = 'bg-red-500/20', + yellow = 'bg-yellow-500/20', + purple = 'bg-purple-500/20', + orange = 'bg-orange-500/20', + pink = 'bg-pink-500/20', + brown = 'bg-brown-500/20', + gray = 'bg-gray-500/20' +} + +// 主题类型 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', // 惊悚 } \ No newline at end of file diff --git a/components/ui/select-dropdown.tsx b/components/ui/select-dropdown.tsx index 7a83b14..079d08f 100644 --- a/components/ui/select-dropdown.tsx +++ b/components/ui/select-dropdown.tsx @@ -13,6 +13,7 @@ interface SelectDropdownProps { label: string; options: SettingOption[]; value: string; + placeholder?: string; onChange: (value: string) => void; } @@ -22,6 +23,7 @@ export const SelectDropdown = ( label, options, value, + placeholder, onChange }: SelectDropdownProps ) => { @@ -35,7 +37,7 @@ export const SelectDropdown = (
- {options.find(opt => opt.value === value)?.label || value} +
+ {options.find(opt => opt.value === value)?.label || value} + {placeholder && {placeholder}} +