forked from 77media/video-flow
Merge branch 'dev' of https://git.qikongjian.com/77media/video-flow into dev
This commit is contained in:
commit
4ef38e0c73
@ -738,6 +738,8 @@ export const createMovieProjectV1 = async (request: {
|
||||
resolution: "720p" | "1080p" | "4k";
|
||||
/** 语言 */
|
||||
language: string;
|
||||
/** 视频时长 */
|
||||
video_duration: string;
|
||||
}) => {
|
||||
return post<ApiResponse<{
|
||||
/** 项目ID */
|
||||
|
||||
@ -85,7 +85,8 @@ export interface UseScriptService {
|
||||
userId: string,
|
||||
mode: "automatic" | "manual",
|
||||
resolution: string,
|
||||
language: string
|
||||
language: string,
|
||||
video_duration: string
|
||||
) => Promise<void>;
|
||||
/** 设置任何属性 */
|
||||
setAnyAttribute: any;
|
||||
@ -158,7 +159,8 @@ export const useScriptService = (): UseScriptService => {
|
||||
userId: string,
|
||||
mode: "automatic" | "manual",
|
||||
resolution: string,
|
||||
language: string
|
||||
language: string,
|
||||
video_duration: string
|
||||
): Promise<void> => {
|
||||
try {
|
||||
setLoading(true);
|
||||
@ -169,7 +171,8 @@ export const useScriptService = (): UseScriptService => {
|
||||
userId,
|
||||
mode as "automatic" | "manual",
|
||||
resolution as "720p" | "1080p" | "4k",
|
||||
language
|
||||
language,
|
||||
video_duration
|
||||
);
|
||||
|
||||
setProjectId(projectData.project_id);
|
||||
|
||||
@ -60,10 +60,10 @@ export class TextToShotAdapter {
|
||||
let currentText = text;
|
||||
|
||||
// 按角色名称长度降序排序,避免短名称匹配到长名称的一部分
|
||||
// 既要兼容 首字母大写 其余小写、还要兼容 全部大写
|
||||
// 既要兼容 每个单词 首字母大写 其余小写、还要兼容 全部大写
|
||||
const sortedRoles = [...roles].sort((a, b) => b.name.length - a.name.length).map(role => ({
|
||||
...role,
|
||||
name: role.name.charAt(0).toUpperCase() + role.name.slice(1).toLowerCase()
|
||||
name: role.name.split(' ').map(word => word.charAt(0).toUpperCase() + word.slice(1).toLowerCase()).join(' ')
|
||||
})).concat([...roles].map(role => ({
|
||||
...role,
|
||||
name: role.name.toUpperCase()
|
||||
@ -257,14 +257,16 @@ export class TextToShotAdapter {
|
||||
if (shotData.shotDescContent.length > 0) {
|
||||
// 合并所有描述段落的文本内容
|
||||
shotData.shotDescContent.forEach(paragraph => {
|
||||
paragraph.content.forEach(node => {
|
||||
if (node.type === 'text') {
|
||||
currentScript += node.text;
|
||||
}
|
||||
if (node.type === 'characterToken') {
|
||||
currentScript += node.attrs.name;
|
||||
}
|
||||
});
|
||||
if (paragraph.content) {
|
||||
paragraph.content.forEach(node => {
|
||||
if (node.type === 'text') {
|
||||
currentScript += node.text;
|
||||
}
|
||||
if (node.type === 'characterToken') {
|
||||
currentScript += node.attrs.name;
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@ -40,7 +40,8 @@ describe("ScriptService 业务逻辑测试", () => {
|
||||
"user123",
|
||||
"automatic",
|
||||
"720p",
|
||||
"en"
|
||||
"en",
|
||||
"10"
|
||||
);
|
||||
expect(createRes.project_id).toBeDefined();
|
||||
projectId = createRes.project_id;
|
||||
|
||||
@ -126,7 +126,8 @@ export class ScriptEditUseCase {
|
||||
userId: string ,
|
||||
mode: "automatic" | "manual" = "automatic",
|
||||
resolution: "720p" | "1080p" | "4k" = "720p",
|
||||
language: string
|
||||
language: string,
|
||||
video_duration: string
|
||||
) {
|
||||
try {
|
||||
// 调用创建项目API
|
||||
@ -136,6 +137,7 @@ export class ScriptEditUseCase {
|
||||
mode,
|
||||
resolution,
|
||||
language,
|
||||
video_duration,
|
||||
});
|
||||
|
||||
if (!response.successful) {
|
||||
|
||||
@ -7,7 +7,7 @@ interface AISuggestionBarProps {
|
||||
suggestions: string[];
|
||||
onSuggestionClick: (suggestion: string) => void;
|
||||
onSubmit: (text: string) => void;
|
||||
onFocus: () => void;
|
||||
onClick: () => void;
|
||||
placeholder?: string;
|
||||
}
|
||||
|
||||
@ -15,7 +15,7 @@ export function AISuggestionBar({
|
||||
suggestions,
|
||||
onSuggestionClick,
|
||||
onSubmit,
|
||||
onFocus,
|
||||
onClick,
|
||||
placeholder = "输入你的想法,或点击预设词条获取 AI 建议..."
|
||||
}: AISuggestionBarProps) {
|
||||
const [inputText, setInputText] = useState('');
|
||||
@ -89,7 +89,7 @@ export function AISuggestionBar({
|
||||
</motion.div>
|
||||
</div>
|
||||
|
||||
<div className="max-w-5xl mx-auto px-6">
|
||||
<div className="max-w-5xl mx-auto px-6" onClick={onClick}>
|
||||
{/* 智能预设词条 英文 */}
|
||||
<AnimatePresence>
|
||||
{showSuggestions && !isCollapsed && (
|
||||
@ -179,7 +179,6 @@ export function AISuggestionBar({
|
||||
if (isCollapsed) {
|
||||
toggleCollapse();
|
||||
}
|
||||
onFocus();
|
||||
}}
|
||||
onBlur={() => setIsFocused(false)}
|
||||
placeholder={isCollapsed ? "点击展开..." : placeholder}
|
||||
|
||||
@ -30,10 +30,15 @@ import { AudioRecorder } from "./AudioRecorder";
|
||||
import { useTemplateStoryServiceHook } from "@/app/service/Interaction/templateStoryService";
|
||||
import { createScriptEpisodeNew } from "@/api/script_episode";
|
||||
import { useRouter } from "next/navigation";
|
||||
<<<<<<< HEAD
|
||||
import { EditorContent, useEditor } from "@tiptap/react";
|
||||
import StarterKit from "@tiptap/starter-kit";
|
||||
import { HighlightTextExtension } from "@/components/ui/main-editor/HighlightText";
|
||||
import Placeholder from "@tiptap/extension-placeholder";
|
||||
import { createMovieProjectV1 } from "@/api/video_flow";
|
||||
=======
|
||||
import { createMovieProjectV1 } from "@/api/video_flow";
|
||||
>>>>>>> 3a713f988f2a1b9f322258e14c192470d879eee5
|
||||
|
||||
// 自定义音频播放器样式
|
||||
const customAudioPlayerStyles = `
|
||||
@ -548,8 +553,8 @@ export function ChatInputBox() {
|
||||
};
|
||||
|
||||
// 调用创建剧集API
|
||||
const episodeResponse = await createScriptEpisodeNew(episodeData);
|
||||
console.log("episodeResponse", episodeResponse);
|
||||
const episodeResponse = await createMovieProjectV1(episodeData);
|
||||
console.log('episodeResponse', episodeResponse);
|
||||
if (episodeResponse.code !== 0) {
|
||||
console.error(`创建剧集失败: ${episodeResponse.message}`);
|
||||
alert(`创建剧集失败: ${episodeResponse.message}`);
|
||||
|
||||
@ -159,6 +159,10 @@
|
||||
display: flex;
|
||||
overflow: hidden;
|
||||
min-height: 0;
|
||||
|
||||
display: grid;
|
||||
grid-auto-rows: auto;
|
||||
justify-content: center;
|
||||
}
|
||||
.videoContainer-qteKNi {
|
||||
flex: 1;
|
||||
@ -166,13 +170,14 @@
|
||||
display: flex;
|
||||
position: relative;
|
||||
justify-content: center;
|
||||
|
||||
width: max-content;
|
||||
}
|
||||
.heroVideo-FIzuK1 {
|
||||
object-fit: cover;
|
||||
object-position: center;
|
||||
background-color: #0003;
|
||||
border-radius: 8px;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
.container-kIPoeH {
|
||||
|
||||
@ -21,7 +21,6 @@ const WorkFlow = React.memo(function WorkFlow() {
|
||||
console.log("init-WorkFlow");
|
||||
return () => console.log("unmount-WorkFlow");
|
||||
}, []);
|
||||
|
||||
const containerRef = useRef<HTMLDivElement>(null);
|
||||
const [isEditModalOpen, setIsEditModalOpen] = React.useState(false);
|
||||
const [activeEditTab, setActiveEditTab] = React.useState('1');
|
||||
@ -92,102 +91,98 @@ const WorkFlow = React.memo(function WorkFlow() {
|
||||
return (
|
||||
<ErrorBoundary>
|
||||
<div className="w-full overflow-hidden h-[calc(100vh-6rem)] absolute top-[4rem] left-0 right-0 px-[1rem]">
|
||||
<div className="flex h-full flex-col justify-start items-center">
|
||||
<div className="container-H2sRZG">
|
||||
<div className="splashContainer-otuV_A">
|
||||
<div className="content-vPGYx8">
|
||||
<div className="info-UUGkPJ">
|
||||
<ErrorBoundary>
|
||||
<TaskInfo
|
||||
isLoading={isLoading}
|
||||
taskObject={taskObject}
|
||||
currentLoadingText={currentLoadingText}
|
||||
dataLoadError={dataLoadError}
|
||||
roles={taskObject.roles.data}
|
||||
isPauseWorkFlow={isPauseWorkFlow}
|
||||
/>
|
||||
</ErrorBoundary>
|
||||
</div>
|
||||
<div className="w-full h-full">
|
||||
<div className="splashContainer-otuV_A">
|
||||
<div className="content-vPGYx8">
|
||||
<div className="info-UUGkPJ">
|
||||
<ErrorBoundary>
|
||||
<TaskInfo
|
||||
isLoading={isLoading}
|
||||
taskObject={taskObject}
|
||||
currentLoadingText={currentLoadingText}
|
||||
dataLoadError={dataLoadError}
|
||||
roles={taskObject.roles.data}
|
||||
isPauseWorkFlow={isPauseWorkFlow}
|
||||
/>
|
||||
</ErrorBoundary>
|
||||
</div>
|
||||
<div className="media-Ocdu1O rounded-lg">
|
||||
<div
|
||||
className="videoContainer-qteKNi"
|
||||
ref={containerRef}
|
||||
>
|
||||
{dataLoadError ? (
|
||||
</div>
|
||||
<div className="media-Ocdu1O rounded-lg">
|
||||
<div
|
||||
className="videoContainer-qteKNi"
|
||||
ref={containerRef}
|
||||
>
|
||||
{dataLoadError ? (
|
||||
<motion.div
|
||||
className="flex flex-col items-center justify-center w-full aspect-video rounded-lg bg-red-50 border-2 border-red-200"
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ duration: 0.5 }}
|
||||
>
|
||||
<motion.div
|
||||
className="flex flex-col items-center justify-center w-full aspect-video rounded-lg bg-red-50 border-2 border-red-200"
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ duration: 0.5 }}
|
||||
className="flex items-center gap-3 mb-4"
|
||||
initial={{ scale: 0.8 }}
|
||||
animate={{ scale: 1 }}
|
||||
transition={{ duration: 0.3, delay: 0.2 }}
|
||||
>
|
||||
<motion.div
|
||||
className="flex items-center gap-3 mb-4"
|
||||
initial={{ scale: 0.8 }}
|
||||
animate={{ scale: 1 }}
|
||||
transition={{ duration: 0.3, delay: 0.2 }}
|
||||
>
|
||||
<AlertCircle className="w-8 h-8 text-red-500" />
|
||||
<h3 className="text-lg font-medium text-red-800">数据加载失败</h3>
|
||||
</motion.div>
|
||||
|
||||
<p className="text-red-600 text-center mb-6 max-w-md px-4">
|
||||
{dataLoadError}
|
||||
</p>
|
||||
|
||||
<motion.button
|
||||
className="flex items-center gap-2 px-6 py-3 bg-red-500 text-white rounded-lg hover:bg-red-600 transition-colors"
|
||||
onClick={() => retryLoadData?.()}
|
||||
whileHover={{ scale: 1.05 }}
|
||||
whileTap={{ scale: 0.95 }}
|
||||
>
|
||||
<RefreshCw className="w-4 h-4" />
|
||||
重试加载
|
||||
</motion.button>
|
||||
<AlertCircle className="w-8 h-8 text-red-500" />
|
||||
<h3 className="text-lg font-medium text-red-800">数据加载失败</h3>
|
||||
</motion.div>
|
||||
) : isLoading ? (
|
||||
<Skeleton className="w-full aspect-video rounded-lg" />
|
||||
) : (
|
||||
<div className="heroVideo-FIzuK1" style={{ aspectRatio: "16 / 9" }} key={currentSketchIndex}>
|
||||
<ErrorBoundary>
|
||||
<MediaViewer
|
||||
taskObject={taskObject}
|
||||
scriptData={scriptData}
|
||||
currentSketchIndex={currentSketchIndex}
|
||||
isVideoPlaying={isVideoPlaying}
|
||||
onEditModalOpen={handleEditModalOpen}
|
||||
onToggleVideoPlay={toggleVideoPlay}
|
||||
setIsPauseWorkFlow={setIsPauseWorkFlow}
|
||||
setAnyAttribute={setAnyAttribute}
|
||||
isPauseWorkFlow={isPauseWorkFlow}
|
||||
applyScript={applyScript}
|
||||
mode={mode}
|
||||
/>
|
||||
</ErrorBoundary>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
{taskObject.currentStage !== 'final_video' && taskObject.currentStage !== 'script' && (
|
||||
<div className="imageGrid-ymZV9z hide-scrollbar">
|
||||
|
||||
<p className="text-red-600 text-center mb-6 max-w-md px-4">
|
||||
{dataLoadError}
|
||||
</p>
|
||||
|
||||
<motion.button
|
||||
className="flex items-center gap-2 px-6 py-3 bg-red-500 text-white rounded-lg hover:bg-red-600 transition-colors"
|
||||
onClick={() => retryLoadData?.()}
|
||||
whileHover={{ scale: 1.05 }}
|
||||
whileTap={{ scale: 0.95 }}
|
||||
>
|
||||
<RefreshCw className="w-4 h-4" />
|
||||
重试加载
|
||||
</motion.button>
|
||||
</motion.div>
|
||||
) : isLoading ? (
|
||||
<Skeleton className="w-full aspect-video rounded-lg" />
|
||||
) : (
|
||||
<div className={`heroVideo-FIzuK1 ${['final_video', 'script'].includes(taskObject.currentStage) ? 'h-[calc(100vh-6rem)] w-[calc((100vh-6rem)/9*16)]' : 'h-[calc(100vh-6rem-200px)] w-[calc((100vh-6rem-200px)/9*16)]'}`} style={{ aspectRatio: "16 / 9" }} key={currentSketchIndex}>
|
||||
<ErrorBoundary>
|
||||
<ThumbnailGrid
|
||||
isEditModalOpen={isEditModalOpen}
|
||||
<MediaViewer
|
||||
taskObject={taskObject}
|
||||
isLoading={isLoading}
|
||||
scriptData={scriptData}
|
||||
currentSketchIndex={currentSketchIndex}
|
||||
taskSketch={taskSketch}
|
||||
taskVideos={taskVideos}
|
||||
isGeneratingSketch={isGeneratingSketch}
|
||||
isGeneratingVideo={isGeneratingVideo}
|
||||
sketchCount={sketchCount}
|
||||
totalSketchCount={totalSketchCount}
|
||||
onSketchSelect={setCurrentSketchIndex}
|
||||
isVideoPlaying={isVideoPlaying}
|
||||
onEditModalOpen={handleEditModalOpen}
|
||||
onToggleVideoPlay={toggleVideoPlay}
|
||||
setIsPauseWorkFlow={setIsPauseWorkFlow}
|
||||
setAnyAttribute={setAnyAttribute}
|
||||
isPauseWorkFlow={isPauseWorkFlow}
|
||||
applyScript={applyScript}
|
||||
mode={mode}
|
||||
/>
|
||||
</ErrorBoundary>
|
||||
</div>
|
||||
)}
|
||||
|
||||
</div>
|
||||
{taskObject.currentStage !== 'final_video' && taskObject.currentStage !== 'script' && (
|
||||
<div className="h-[112px] w-[calc((100vh-6rem-200px)/9*16)]">
|
||||
<ThumbnailGrid
|
||||
isDisabledFocus={isEditModalOpen || isPauseWorkFlow}
|
||||
taskObject={taskObject}
|
||||
isLoading={isLoading}
|
||||
currentSketchIndex={currentSketchIndex}
|
||||
taskSketch={taskSketch}
|
||||
taskVideos={taskVideos}
|
||||
isGeneratingSketch={isGeneratingSketch}
|
||||
isGeneratingVideo={isGeneratingVideo}
|
||||
sketchCount={sketchCount}
|
||||
totalSketchCount={totalSketchCount}
|
||||
onSketchSelect={setCurrentSketchIndex}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -202,7 +197,7 @@ const WorkFlow = React.memo(function WorkFlow() {
|
||||
tooltip={isPauseWorkFlow ? "Play" : "Pause"}
|
||||
onClick={() => setIsPauseWorkFlow(!isPauseWorkFlow)}
|
||||
/>
|
||||
{ mode !== 'automatic' && (
|
||||
{ !mode.includes('auto') && (
|
||||
<GlassIconButton
|
||||
icon={ChevronLast}
|
||||
size='lg'
|
||||
@ -220,7 +215,7 @@ const WorkFlow = React.memo(function WorkFlow() {
|
||||
suggestions={mockSuggestions}
|
||||
onSuggestionClick={handleSuggestionClick}
|
||||
onSubmit={handleSubmit}
|
||||
onFocus={() => setIsPauseWorkFlow(true)}
|
||||
onClick={() => setIsPauseWorkFlow(true)}
|
||||
placeholder="Please input your ideas, or click the predefined tags to receive AI advice..."
|
||||
/>
|
||||
</ErrorBoundary>
|
||||
|
||||
@ -137,7 +137,7 @@ export const MediaViewer = React.memo(function MediaViewer({
|
||||
return (
|
||||
<video
|
||||
ref={finalVideoRef}
|
||||
className="w-[auto] h-full object-cover rounded-lg"
|
||||
className="w-full h-full object-cover rounded-lg"
|
||||
src={taskObject.final.url}
|
||||
autoPlay={isFinalVideoPlaying}
|
||||
loop
|
||||
@ -305,7 +305,7 @@ export const MediaViewer = React.memo(function MediaViewer({
|
||||
transition={{ duration: 0.8, ease: "easeInOut" }}
|
||||
>
|
||||
<video
|
||||
className="w-[auto] h-full rounded-lg object-cover object-center"
|
||||
className="w-full h-full rounded-lg object-cover object-center"
|
||||
src={taskObject.final.url}
|
||||
loop
|
||||
playsInline
|
||||
|
||||
@ -8,7 +8,7 @@ import { Loader2, X } from 'lucide-react';
|
||||
import { TaskObject } from '@/api/DTO/movieEdit';
|
||||
|
||||
interface ThumbnailGridProps {
|
||||
isEditModalOpen: boolean;
|
||||
isDisabledFocus: boolean;
|
||||
taskObject: TaskObject;
|
||||
isLoading: boolean;
|
||||
currentSketchIndex: number;
|
||||
@ -22,7 +22,7 @@ interface ThumbnailGridProps {
|
||||
}
|
||||
|
||||
export function ThumbnailGrid({
|
||||
isEditModalOpen,
|
||||
isDisabledFocus,
|
||||
taskObject,
|
||||
isLoading,
|
||||
currentSketchIndex,
|
||||
@ -124,7 +124,7 @@ export function ThumbnailGrid({
|
||||
// 监听键盘事件
|
||||
useEffect(() => {
|
||||
// 组件挂载时自动聚焦
|
||||
if (thumbnailsRef.current && !isEditModalOpen) {
|
||||
if (thumbnailsRef.current && !isDisabledFocus) {
|
||||
thumbnailsRef.current.focus();
|
||||
}
|
||||
|
||||
@ -134,7 +134,7 @@ export function ThumbnailGrid({
|
||||
|
||||
// 确保在数据变化时保持焦点
|
||||
useEffect(() => {
|
||||
if (thumbnailsRef.current && !isFocused && !isEditModalOpen) {
|
||||
if (thumbnailsRef.current && !isFocused && !isDisabledFocus) {
|
||||
thumbnailsRef.current.focus();
|
||||
}
|
||||
}, [taskObject.currentStage, isFocused]);
|
||||
@ -267,7 +267,6 @@ export function ThumbnailGrid({
|
||||
<div className="absolute inset-0 bg-black/10 flex items-center justify-center z-20">
|
||||
<div className="text-blue-500 text-xl font-bold flex items-center gap-2">
|
||||
<Loader2 className="w-10 h-10 animate-spin" />
|
||||
<span>Generating...</span>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
@ -275,7 +274,6 @@ export function ThumbnailGrid({
|
||||
<div className="absolute inset-0 bg-red-500/10 flex items-center justify-center z-20">
|
||||
<div className="text-red-500 text-xl font-bold flex items-center gap-2">
|
||||
<X className="w-10 h-10" />
|
||||
<span>Failed</span>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
@ -329,7 +327,6 @@ export function ThumbnailGrid({
|
||||
<div className="absolute inset-0 bg-black/10 flex items-center justify-center">
|
||||
<div className="text-blue-500 text-xl font-bold flex items-center gap-2">
|
||||
<Loader2 className="w-10 h-10 animate-spin" />
|
||||
<span>Generating...</span>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
@ -337,7 +334,6 @@ export function ThumbnailGrid({
|
||||
<div className="absolute inset-0 bg-red-500/10 flex items-center justify-center">
|
||||
<div className="text-red-500 text-xl font-bold flex items-center gap-2">
|
||||
<X className="w-10 h-10" />
|
||||
<span>Failed</span>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
@ -366,7 +362,7 @@ export function ThumbnailGrid({
|
||||
<div
|
||||
ref={thumbnailsRef}
|
||||
tabIndex={0}
|
||||
className="w-full grid grid-flow-col auto-cols-[20%] gap-4 overflow-x-auto hide-scrollbar px-1 py-1 cursor-grab active:cursor-grabbing focus:outline-none select-none"
|
||||
className="w-full h-full grid grid-flow-col auto-cols-[20%] gap-4 overflow-x-auto hide-scrollbar px-1 py-1 cursor-grab active:cursor-grabbing focus:outline-none select-none"
|
||||
autoFocus
|
||||
onMouseDown={handleMouseDown}
|
||||
onMouseMove={handleMouseMove}
|
||||
|
||||
@ -1,4 +1,5 @@
|
||||
import React, { forwardRef, useEffect, useRef, useState } from "react";
|
||||
import React, { forwardRef, useRef, useState } from "react";
|
||||
import { useDeepCompareEffect } from "@/hooks/useDeepCompareEffect";
|
||||
import { Plus, X, UserRoundPlus, MessageCirclePlus, MessageCircleMore, ClipboardType } from "lucide-react";
|
||||
import ShotEditor from "./ShotEditor";
|
||||
import { toast } from "sonner";
|
||||
@ -51,16 +52,16 @@ export const ShotsEditor = forwardRef<any, ShotsEditorProps>(function ShotsEdito
|
||||
const descEditorRef = useRef<any>(null);
|
||||
const dialogEditorRef = useRef<any>(null);
|
||||
|
||||
useEffect(() => {
|
||||
console.log('-==========shotInfo===========-', shotInfo);
|
||||
useDeepCompareEffect(() => {
|
||||
if (shotInfo) {
|
||||
console.log('-==========shotInfo===========-', shotInfo);
|
||||
const shots = shotInfo.map((shot) => {
|
||||
return TextToShotAdapter.fromLensType(shot, roles);
|
||||
});
|
||||
console.log('-==========shots===========-', shots);
|
||||
setShots(shots as Shot[]);
|
||||
}
|
||||
}, [shotInfo]);
|
||||
}, [shotInfo, roles]);
|
||||
|
||||
const handleDescContentChange = (content: any) => {
|
||||
const shot = shots[currentShotIndex];
|
||||
|
||||
@ -47,11 +47,19 @@ export const ShotTabContent = (props: ShotTabContentProps) => {
|
||||
const [isLoadingShots, setIsLoadingShots] = useState(false);
|
||||
const shotsEditorRef = useRef<any>(null);
|
||||
const [isRegenerate, setIsRegenerate] = useState(false);
|
||||
const [pendingRegeneration, setPendingRegeneration] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
console.log('shotTabContent-----roles', roles);
|
||||
}, [roles]);
|
||||
|
||||
useEffect(() => {
|
||||
if (pendingRegeneration) {
|
||||
regenerateVideoSegment();
|
||||
setPendingRegeneration(false);
|
||||
}
|
||||
}, [shotData[selectedIndex]?.lens]);
|
||||
|
||||
// 监听当前选中index变化
|
||||
useEffect(() => {
|
||||
console.log('shotTabContent-----shotData', shotData);
|
||||
@ -165,13 +173,12 @@ export const ShotTabContent = (props: ShotTabContentProps) => {
|
||||
console.log('regenerate');
|
||||
setIsRegenerate(true);
|
||||
const shotInfo = shotsEditorRef.current.getShotInfo();
|
||||
console.log('shotTabContent-----shotInfo', shotInfo);
|
||||
console.log('shotInfo', shotInfo);
|
||||
setSelectedSegment({
|
||||
...shotData[selectedIndex],
|
||||
lens: shotInfo
|
||||
});
|
||||
await regenerateVideoSegment();
|
||||
setIsRegenerate(false);
|
||||
setPendingRegeneration(true);
|
||||
};
|
||||
|
||||
// 新增分镜
|
||||
@ -408,7 +415,7 @@ export const ShotTabContent = (props: ShotTabContentProps) => {
|
||||
<motion.button
|
||||
onClick={() => handleRegenerate()}
|
||||
className="flex items-center justify-center gap-2 px-4 py-3 bg-blue-500/10 hover:bg-blue-500/20
|
||||
text-blue-500 rounded-lg transition-colors"
|
||||
text-blue-500 rounded-lg transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
whileHover={{ scale: 1.02 }}
|
||||
whileTap={{ scale: 0.98 }}
|
||||
disabled={isRegenerate}
|
||||
|
||||
20
hooks/useDeepCompareEffect.ts
Normal file
20
hooks/useDeepCompareEffect.ts
Normal file
@ -0,0 +1,20 @@
|
||||
import { useEffect, useRef } from 'react';
|
||||
import isEqual from 'lodash/isEqual';
|
||||
|
||||
function useDeepCompareMemoize<T>(value: T) {
|
||||
const ref = useRef<T>();
|
||||
|
||||
if (!isEqual(value, ref.current)) {
|
||||
ref.current = value;
|
||||
}
|
||||
|
||||
return ref.current;
|
||||
}
|
||||
|
||||
export function useDeepCompareEffect(
|
||||
callback: () => void | (() => void),
|
||||
dependencies: any[]
|
||||
) {
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
useEffect(callback, dependencies.map(useDeepCompareMemoize));
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user