diff --git a/api/video_flow.ts b/api/video_flow.ts index 3d55204..88f9705 100644 --- a/api/video_flow.ts +++ b/api/video_flow.ts @@ -2,7 +2,7 @@ import { post } from './request'; import { ProjectTypeEnum } from '@/app/model/enums'; import { ApiResponse } from '@/api/common'; import { BASE_URL } from './constants' -import { AITextEntity, RoleEntity, SceneEntity, ShotEntity, TagEntity } from '@/app/service/domain/Entities'; +import { AITextEntity, RoleEntity, SceneEntity, ShotEntity, TagEntity, ScriptSliceEntity } from '@/app/service/domain/Entities'; // API 响应类型 interface BaseApiResponse { @@ -247,6 +247,75 @@ export const applyRoleToShots = async (request: { return post>('/movie/apply_role_to_shots', request); }; +/** + * 获取角色应用到的分镜列表 + * @param request - 获取角色分镜列表请求参数 + * @returns Promise> + */ +export const getRoleShots = async (request: { + /** 角色ID */ + roleId: string; +}): Promise> => { + return post>('/movie/get_role_shots', request); +}; + +/** + * 获取角色列表 + * @param request - 获取角色列表请求参数 + * @returns Promise> + */ +export const getRoleList = async (request: { + /** 项目ID */ + projectId: string; +}): Promise> => { + return post>('/movie/get_role_list', request); +}; + +/** + * 获取角色数据 + * @param request - 获取角色数据请求参数 + * @returns Promise> + */ +export const getRoleData = async (request: { + /** 角色ID */ + roleId: string; +}): Promise> => { + return post>('/movie/get_role_data', request); +}; + +/** + * 获取用户角色库 + * @returns Promise> + */ +export const getUserRoleLibrary = async (): Promise> => { + return post>('/movie/get_user_role_library', {}); +}; + +/** + * 替换角色 + * @param request - 替换角色请求参数 + * @returns Promise> + */ +export const replaceRole = async (request: { + /** 当前角色ID */ + currentRoleId: string; + /** 替换的角色ID */ + replaceRoleId: string; +}): Promise> => { + return post>('/movie/replace_role', request); +}; + + /** * 修改标签 * @param request - 修改标签请求参数 @@ -305,52 +374,6 @@ export const applySceneToShots = async (request: { return post>('/movie/apply_scene_to_shots', request); }; -/** - * 获取角色应用到的分镜列表 - * @param request - 获取角色分镜列表请求参数 - * @returns Promise> - */ -export const getRoleShots = async (request: { - /** 角色ID */ - roleId: string; -}): Promise> => { - return post>('/movie/get_role_shots', request); -}; - -/** - * 获取角色列表 - * @param request - 获取角色列表请求参数 - * @returns Promise> - */ -export const getRoleList = async (request: { - /** 项目ID */ - projectId: string; -}): Promise> => { - return post>('/movie/get_role_list', request); -}; - -/** - * 获取角色数据 - * @param request - 获取角色数据请求参数 - * @returns Promise> - */ -export const getRoleData = async (request: { - /** 角色ID */ - roleId: string; -}): Promise> => { - return post>('/movie/get_role_data', request); -}; - /** * 获取场景数据 * @param request - 获取场景数据请求参数 @@ -398,24 +421,171 @@ export const getSceneShots = async (request: { }; /** - * 获取用户角色库 + * 获取分镜关联的角色信息列表 + * @param request - 获取分镜角色信息请求参数 * @returns Promise> */ -export const getUserRoleLibrary = async (): Promise> => { - return post>('/movie/get_user_role_library', {}); +export const getShotRoles = async (request: { + /** 分镜ID */ + shotId: string; +}): Promise> => { + return post>('/movie/get_shot_roles', request); }; /** - * 替换角色 - * @param request - 替换角色请求参数 - * @returns Promise> + * 获取分镜关联的场景信息列表 + * @param request - 获取分镜场景信息请求参数 + * @returns Promise> */ -export const replaceRole = async (request: { - /** 当前角色ID */ - currentRoleId: string; - /** 替换的角色ID */ - replaceRoleId: string; -}): Promise> => { - return post>('/movie/replace_role', request); +export const getShotScenes = async (request: { + /** 分镜ID */ + shotId: string; +}): Promise> => { + return post>('/movie/get_shot_scenes', request); }; +/** + * 获取分镜详细数据 + * @param request - 获取分镜数据请求参数 + * @returns Promise> + */ +export const getShotData = async (request: { + /** 分镜ID */ + shotId: string; +}): Promise> => { + return post>('/movie/get_shot_data', request); +}; + +/** + * 重新生成分镜 + * @param request - 重新生成分镜请求参数 + * @returns Promise> + */ +export const regenerateShot = async (request: { + /** 分镜ID */ + shotId: string; + /** 镜头描述 */ + shotPrompt: string; + /** 对话内容 */ + dialogueContent: string; + /** 角色ID替换参数,格式为{oldId:string,newId:string}[] */ + roleReplaceParams: { oldId: string; newId: string }[]; + /** 场景ID替换参数,格式为{oldId:string,newId:string}[] */ + sceneReplaceParams: { oldId: string; newId: string }[]; +}): Promise> => { + return post>('/movie/regenerate_shot', request); +}; + +/** + * 修改分镜对话内容 + * @param request - 修改分镜对话内容请求参数 + * @returns Promise> + */ +export const updateShotContent = async (request: { + /** 分镜ID */ + shotId: string; + /** 新的对话内容,ContentItem数量和ID顺序不能变,只能修改content字段 */ + content: Array<{ + /** 角色ID */ + roleId: string; + /** 对话内容 */ + content: string; + }>; +}): Promise> => { + return post>('/movie/update_shot_content', request); +}; + +/** + * 获取分镜列表 + * @param request - 获取分镜列表请求参数 + * @returns Promise> + */ +export const getShotList = async (request: { + /** 项目ID */ + projectId: string; +}): Promise> => { + return post>('/movie/get_shot_list', request); +}; + +/** + * 获取分镜详情 + * @param request - 获取分镜详情请求参数 + * @returns Promise> + */ +export const getShotDetail = async (request: { + /** 分镜ID */ + shotId: string; +}): Promise> => { + return post>('/movie/get_shot_detail', request); +}; + +/** + * 获取分镜草图数据 + * @param request - 获取分镜草图数据请求参数 + * @returns Promise> + */ +export const getShotSketchData = async (request: { + /** 分镜ID */ + shotId: string; +}): Promise> => { + return post>('/movie/get_shot_sketch_data', request); +}; + +/** + * 获取分镜视频数据 + * @param request - 获取分镜视频数据请求参数 + * @returns Promise> + */ +export const getShotVideoData = async (request: { + /** 分镜ID */ + shotId: string; +}): Promise> => { + return post>('/movie/get_shot_video_data', request); +}; + +/** + * 修改分镜镜头 + * @param request - 修改分镜镜头请求参数 + * @returns Promise> + */ +export const updateShotShot = async (request: { + /** 分镜ID */ + shotId: string; + /** 新的镜头数据 */ + shot: string[]; +}): Promise> => { + return post>('/movie/update_shot_shot', request); +}; + +/** + * 替换分镜角色 + * @param request - 替换分镜角色请求参数 + * @returns Promise> + */ +export const replaceShotRole = async (request: { + /** 分镜ID */ + shotId: string; + /** 旧角色ID */ + oldRoleId: string; + /** 新角色ID */ + newRoleId: string; +}): Promise> => { + return post>('/movie/replace_shot_role', request); +}; + +/** + * 获取分镜视频剧本内容 + * @param request - 获取分镜视频剧本请求参数 + * @returns Promise> + */ +export const getShotVideoScript = async (request: { + /** 分镜ID */ + shotId: string; +}): Promise> => { + return post>('/movie/get_shot_video_script', request); +}; diff --git a/app/service/Interaction/ShotService.ts b/app/service/Interaction/ShotService.ts new file mode 100644 index 0000000..855c466 --- /dev/null +++ b/app/service/Interaction/ShotService.ts @@ -0,0 +1,383 @@ +import { useState, useCallback, useMemo } from 'react'; +import { ShotEntity, RoleEntity, SceneEntity, ScriptSliceEntity } from '../domain/Entities'; +import { ShotItem } from '../domain/Item'; +import { ShotEditUseCase } from '../usecase/ShotEditUsecase'; +import { + getShotList, + getShotDetail, + updateShotContent, + updateShotShot, + getUserRoleLibrary, + replaceShotRole, + getShotVideoScript +} from '@/api/video_flow'; + +/** + * 分镜服务Hook接口 + * 定义分镜服务Hook的所有状态和操作方法 + */ +export interface UseShotService { + // 响应式状态 + /** 分镜列表 */ + shotList: ShotEntity[]; + /** 当前选中的分镜 */ + selectedShot: ShotItem | null; + /** 当前分镜的草图数据URL */ + shotSketchData: string | null; + /** 当前分镜的视频数据URL */ + shotVideoData: string | null; + + /** 用户角色库 */ + userRoleLibrary: RoleEntity[]; + /** 当前分镜的视频剧本片段 */ + shotVideoScript: ScriptSliceEntity[]; + /** 加载状态 */ + loading: boolean; + /** 错误信息 */ + error: string | null; + + // 操作方法 + /** 获取分镜列表 */ + fetchShotList: (projectId: string) => Promise; + /** 选择分镜并获取详情 */ + selectShot: (shotId: string) => Promise; + /** 修改分镜对话内容 */ + updateShotContent: (newContent: Array<{ roleId: string; content: string }>) => Promise; + /** 修改分镜镜头 */ + updateShotShot: (newShot: string[]) => Promise; + /** 获取用户角色库 */ + fetchUserRoleLibrary: () => Promise; + /** 替换分镜角色 */ + replaceShotRole: (oldRoleId: string, newRoleId: string) => Promise; + /** 获取分镜视频剧本内容 */ + fetchShotVideoScript: () => Promise; + /** 获取分镜关联的角色信息 */ + getShotRoles: () => Promise; + /** 获取分镜关联的场景信息 */ + getShotScenes: () => Promise; + + /** 重新生成分镜 */ + regenerateShot: ( + shotPrompt: string, + dialogueContent: string, + roleReplaceParams: { oldId: string; newId: string }[], + sceneReplaceParams: { oldId: string; newId: string }[] + ) => Promise; +} + +/** + * 分镜服务Hook + * 提供分镜相关的所有状态管理和操作方法 + * 包括分镜列表管理、分镜选择、数据获取、内容修改等功能 + */ +export const useShotService = (): UseShotService => { + // 响应式状态 + const [shotList, setShotList] = useState([]); + const [selectedShot, setSelectedShot] = useState(null); + const [shotSketchData, setShotSketchData] = useState(null); + const [shotVideoData, setShotVideoData] = useState(null); + const [userRoleLibrary, setUserRoleLibrary] = useState([]); + const [shotVideoScript, setShotVideoScript] = useState([]); + const [loading, setLoading] = useState(false); + const [error, setError] = useState(null); + + // UseCase实例 + const [shotEditUseCase, setShotEditUseCase] = useState(null); + + /** + * 获取分镜列表 + * @description 根据项目ID获取所有分镜列表 + * @param projectId 项目ID + */ + const fetchShotList = useCallback(async (projectId: string) => { + try { + setLoading(true); + setError(null); + const response = await getShotList({ projectId }); + if (response.successful) { + setShotList(response.data); + } else { + setError(`获取分镜列表失败: ${response.message}`); + } + } catch (err) { + setError(`获取分镜列表失败: ${err instanceof Error ? err.message : '未知错误'}`); + } finally { + setLoading(false); + } + }, []); + + /** + * 选择分镜并获取详情 + * @description 根据分镜ID获取分镜详情,并初始化相关的UseCase和数据 + * @param shotId 分镜ID + */ + const selectShot = useCallback(async (shotId: string) => { + try { + setLoading(true); + setError(null); + + // 获取分镜详情 + const response = await getShotDetail({ shotId }); + if (response.successful) { + const shotEntity = response.data; + const shotItem = new ShotItem(shotEntity); + setSelectedShot(shotItem); + + // 初始化UseCase + const newShotEditUseCase = new ShotEditUseCase(shotItem); + setShotEditUseCase(newShotEditUseCase); + + + + // 从分镜实体中获取草图数据和视频数据 + setShotSketchData(shotEntity.sketchUrl || null); + setShotVideoData(shotEntity.videoUrl || null); + } else { + setError(`获取分镜详情失败: ${response.message}`); + } + } catch (err) { + setError(`选择分镜失败: ${err instanceof Error ? err.message : '未知错误'}`); + } finally { + setLoading(false); + } + }, []); + + /** + * 修改分镜对话内容 + * @description 更新分镜的对话内容,ContentItem数量和ID顺序不能变,只能修改content字段 + * @param newContent 新的对话内容数组 + */ + const updateShotContentHandler = useCallback(async (newContent: Array<{ roleId: string; content: string }>) => { + if (!shotEditUseCase) { + setError('分镜编辑用例未初始化'); + return; + } + + try { + setLoading(true); + setError(null); + const updatedShot = await shotEditUseCase.updateShotContent(newContent); + setSelectedShot(new ShotItem(updatedShot)); + } catch (err) { + setError(`修改分镜对话内容失败: ${err instanceof Error ? err.message : '未知错误'}`); + } finally { + setLoading(false); + } + }, [shotEditUseCase]); + + /** + * 修改分镜镜头 + * @description 更新分镜的镜头数据 + * @param newShot 新的镜头数据 + */ + const updateShotShotHandler = useCallback(async (newShot: string[]) => { + if (!selectedShot) { + setError('未选择分镜'); + return; + } + + try { + setLoading(true); + setError(null); + const response = await updateShotShot({ + shotId: selectedShot.entity.id, + shot: newShot + }); + if (response.successful) { + const updatedShot = response.data; + setSelectedShot(new ShotItem(updatedShot)); + } else { + setError(`修改分镜镜头失败: ${response.message}`); + } + } catch (err) { + setError(`修改分镜镜头失败: ${err instanceof Error ? err.message : '未知错误'}`); + } finally { + setLoading(false); + } + }, [selectedShot]); + + /** + * 获取用户角色库 + * @description 获取当前用户的所有角色库数据 + */ + const fetchUserRoleLibrary = useCallback(async () => { + try { + setLoading(true); + setError(null); + const response = await getUserRoleLibrary(); + if (response.successful) { + setUserRoleLibrary(response.data); + } else { + setError(`获取用户角色库失败: ${response.message}`); + } + } catch (err) { + setError(`获取用户角色库失败: ${err instanceof Error ? err.message : '未知错误'}`); + } finally { + setLoading(false); + } + }, []); + + /** + * 替换分镜角色 + * @description 将分镜中的某个角色替换为角色库中的另一个角色 + * @param oldRoleId 旧角色ID + * @param newRoleId 新角色ID + */ + const replaceShotRoleHandler = useCallback(async (oldRoleId: string, newRoleId: string) => { + if (!selectedShot) { + setError('未选择分镜'); + return; + } + + try { + setLoading(true); + setError(null); + const response = await replaceShotRole({ + shotId: selectedShot.entity.id, + oldRoleId, + newRoleId + }); + if (response.successful) { + // 重新获取分镜详情 + await selectShot(selectedShot.entity.id); + } else { + setError(`替换分镜角色失败: ${response.message}`); + } + } catch (err) { + setError(`替换分镜角色失败: ${err instanceof Error ? err.message : '未知错误'}`); + } finally { + setLoading(false); + } + }, [selectedShot, selectShot]); + + /** + * 获取分镜视频剧本内容 + * @description 获取分镜视频的剧本内容,通过接口返回多个ScriptSliceEntity片段 + */ + const fetchShotVideoScript = useCallback(async () => { + if (!selectedShot) { + setError('未选择分镜'); + return; + } + + try { + setLoading(true); + setError(null); + const response = await getShotVideoScript({ shotId: selectedShot.entity.id }); + if (response.successful) { + setShotVideoScript(response.data); + } else { + setError(`获取分镜视频剧本失败: ${response.message}`); + } + } catch (err) { + setError(`获取分镜视频剧本失败: ${err instanceof Error ? err.message : '未知错误'}`); + } finally { + setLoading(false); + } + }, [selectedShot]); + + /** + * 获取分镜关联的角色信息 + * @description 获取当前分镜可以使用的角色列表 + * @returns Promise 角色信息列表 + */ + const getShotRoles = useCallback(async (): Promise => { + if (!shotEditUseCase) { + throw new Error('分镜编辑用例未初始化'); + } + return await shotEditUseCase.getShotRoles(); + }, [shotEditUseCase]); + + /** + * 获取分镜关联的场景信息 + * @description 获取当前分镜可以使用的场景列表 + * @returns Promise 场景信息列表 + */ + const getShotScenes = useCallback(async (): Promise => { + if (!shotEditUseCase) { + throw new Error('分镜编辑用例未初始化'); + } + return await shotEditUseCase.getShotScenes(); + }, [shotEditUseCase]); + + + + /** + * 重新生成分镜 + * @description 使用镜头、对话内容、角色ID替换参数、场景ID替换参数重新生成分镜 + * @param shotPrompt 镜头描述 + * @param dialogueContent 对话内容 + * @param roleReplaceParams 角色ID替换参数,格式为{oldId:string,newId:string}[] + * @param sceneReplaceParams 场景ID替换参数,格式为{oldId:string,newId:string}[] + */ + const regenerateShot = useCallback(async ( + shotPrompt: string, + dialogueContent: string, + roleReplaceParams: { oldId: string; newId: string }[], + sceneReplaceParams: { oldId: string; newId: string }[] + ) => { + if (!shotEditUseCase) { + setError('分镜编辑用例未初始化'); + return; + } + + try { + setLoading(true); + setError(null); + const updatedShot = await shotEditUseCase.regenerateShot( + shotPrompt, + dialogueContent, + roleReplaceParams, + sceneReplaceParams + ); + setSelectedShot(new ShotItem(updatedShot)); + } catch (err) { + setError(`重新生成分镜失败: ${err instanceof Error ? err.message : '未知错误'}`); + } finally { + setLoading(false); + } + }, [shotEditUseCase]); + + return { + // 响应式状态 - 用于UI组件订阅和渲染 + /** 分镜列表 - 当前项目的所有分镜数据 */ + shotList, + /** 当前选中的分镜 - 包含分镜实体和编辑状态 */ + selectedShot, + /** 当前分镜的草图数据URL - 从分镜实体中获取的草图图片链接 */ + shotSketchData, + /** 当前分镜的视频数据URL - 从分镜实体中获取的视频链接 */ + shotVideoData, + + /** 用户角色库 - 当前用户可用的所有角色数据 */ + userRoleLibrary, + /** 当前分镜的视频剧本片段 - 通过接口获取的剧本内容 */ + shotVideoScript, + /** 加载状态 - 标识当前是否有异步操作正在进行 */ + loading, + /** 错误信息 - 记录最近一次操作的错误信息 */ + error, + + // 操作方法 - 提供给UI组件调用的业务逻辑方法 + /** 获取分镜列表 - 根据项目ID获取所有分镜数据 */ + fetchShotList, + /** 选择分镜并获取详情 - 选择指定分镜并初始化相关数据 */ + selectShot, + /** 修改分镜对话内容 - 更新分镜的对话内容,保持ContentItem结构不变 */ + updateShotContent: updateShotContentHandler, + /** 修改分镜镜头 - 更新分镜的镜头数据 */ + updateShotShot: updateShotShotHandler, + /** 获取用户角色库 - 获取当前用户的所有角色数据 */ + fetchUserRoleLibrary, + /** 替换分镜角色 - 将分镜中的角色替换为角色库中的另一个角色 */ + replaceShotRole: replaceShotRoleHandler, + /** 获取分镜视频剧本内容 - 通过接口获取视频剧本片段 */ + fetchShotVideoScript, + /** 获取分镜关联的角色信息 - 获取当前分镜可用的角色列表 */ + getShotRoles, + /** 获取分镜关联的场景信息 - 获取当前分镜可用的场景列表 */ + getShotScenes, + /** 重新生成分镜 - 使用新参数重新生成分镜内容 */ + regenerateShot, + }; +}; diff --git a/app/service/domain/Entities.ts b/app/service/domain/Entities.ts index 5a9613d..534b2c1 100644 --- a/app/service/domain/Entities.ts +++ b/app/service/domain/Entities.ts @@ -65,19 +65,21 @@ export interface SceneEntity extends BaseEntity { /** 场景提示词Id */ generateTextId: string; } - -interface RoleMap { - /** 角色ID */ - roleId: string; - /** 人物ID */ - figureId: string; -} +/**对话内容项 */ interface ContentItem { /** 角色ID */ roleId: string; /** 对话内容 */ content: string; } +export enum ShotStatus { + /** 草稿加载中 */ + sketchLoading = 0, + /** 视频加载中 */ + videoLoading = 1, + /** 完成 */ + finished = 2, +} /** * 分镜实体接口 */ @@ -88,10 +90,40 @@ export interface ShotEntity extends BaseEntity { sketchUrl: string; /**分镜视频Url */ videoUrl: string; - /**人物ID */ - roleMap: RoleMap[]; + /**分镜状态 */ + status: ShotStatus; + /**角色ID列表 */ + roleList: string[]; + /**场景ID列表 */ + sceneList: string[]; /**对话内容 */ content: ContentItem[]; /**镜头项 */ shot: string[]; + /**分镜剧本 */ + scriptId: string; +} + +/**不同类型 将有不同元数据 */ +export enum ScriptSliceType { + /** 文本 */ + text = 'text', + /** 高亮 */ + highlight = 'highlight', + /**角色 */ + role = 'role', + /** 场景 */ + scene = 'scene', + +} +/** + * 剧本片段实体接口 + */ +export interface ScriptSliceEntity extends BaseEntity { + /** 类型 */ + type: ScriptSliceType; + /** 剧本内容 */ + text: string; + /** 元数据 */ + metaData: any; } diff --git a/app/service/domain/Item.ts b/app/service/domain/Item.ts index 20b29d2..a90c305 100644 --- a/app/service/domain/Item.ts +++ b/app/service/domain/Item.ts @@ -116,7 +116,7 @@ export class SceneItem extends EditItem { * 分镜可编辑项 */ export class ShotItem extends EditItem { - type: ItemType.IMAGE = ItemType.IMAGE; + type: ItemType = ItemType.IMAGE; constructor( entity: ShotEntity, @@ -124,4 +124,10 @@ export class ShotItem extends EditItem { ) { super(entity, metadata); } + /** + * 更新为视频状态 + */ + updateToVideoStatus(): void { + this.type = ItemType.VIDEO; + } } diff --git a/app/service/test/Role.test.ts b/app/service/test/Role.test.ts index 4b5da97..8544d12 100644 --- a/app/service/test/Role.test.ts +++ b/app/service/test/Role.test.ts @@ -3,7 +3,7 @@ import { RoleEditUseCase } from '../usecase/RoleEditUseCase'; import { TextEditUseCase } from '../usecase/TextEditUseCase'; import { TagEditUseCase } from '../usecase/TagEditUseCase'; import { RoleItem, TextItem, TagItem } from '../domain/Item'; -import { RoleEntity, AITextEntity, TagEntity, ShotEntity } from '../domain/Entities'; +import { RoleEntity, AITextEntity, TagEntity, ShotEntity, ShotStatus } from '../domain/Entities'; // Mock API模块 jest.mock('@/api/video_flow', () => ({ @@ -71,7 +71,10 @@ describe('RoleService 业务逻辑测试', () => { name: '分镜1', sketchUrl: 'http://example.com/sketch1.jpg', videoUrl: 'http://example.com/video1.mp4', - roleMap: [], + roleList: [], + sceneList: [], + status: ShotStatus.sketchLoading, + scriptId: 'script1', content: [], shot: [], updatedAt: Date.now(), diff --git a/app/service/test/Scene.test.ts b/app/service/test/Scene.test.ts new file mode 100644 index 0000000..6fb7c0e --- /dev/null +++ b/app/service/test/Scene.test.ts @@ -0,0 +1,565 @@ +import { getSceneList, getSceneData, updateText, updateTag, regenerateScene, getSceneShots, applySceneToShots } from '@/api/video_flow'; +import { SceneEditUseCase } from '../usecase/SceneEditUseCase'; +import { TextEditUseCase } from '../usecase/TextEditUseCase'; +import { TagEditUseCase } from '../usecase/TagEditUseCase'; +import { SceneItem, TextItem, TagItem } from '../domain/Item'; +import { SceneEntity, AITextEntity, TagEntity, ShotEntity, ShotStatus } from '../domain/Entities'; + +// Mock API模块 +jest.mock('@/api/video_flow', () => ({ + getSceneList: jest.fn(), + getSceneData: jest.fn(), + updateText: jest.fn(), + updateTag: jest.fn(), + regenerateScene: jest.fn(), + getSceneShots: jest.fn(), + applySceneToShots: jest.fn(), +})); + +// Mock UseCase模块 +jest.mock('../usecase/SceneEditUseCase'); +jest.mock('../usecase/TextEditUseCase'); +jest.mock('../usecase/TagEditUseCase'); + +// Mock Domain模块 +jest.mock('../domain/Item', () => ({ + SceneItem: jest.fn(), + TextItem: jest.fn(), + TagItem: jest.fn(), +})); + +describe('SceneService 业务逻辑测试', () => { + let mockSceneEditUseCase: jest.Mocked; + let mockTextEditUseCase: jest.Mocked; + let mockTagEditUseCase: jest.Mocked; + + // 测试数据 + const mockSceneEntity: SceneEntity = { + id: 'scene1', + name: '测试场景', + imageUrl: 'http://example.com/scene1.jpg', + tagIds: ['tag1', 'tag2'], + generateTextId: 'text1', + updatedAt: Date.now(), + loadingProgress: 100, + disableEdit: false, + }; + + const mockTextEntity: AITextEntity = { + id: 'text1', + content: '这是AI生成的场景文本内容', + updatedAt: Date.now(), + loadingProgress: 100, + disableEdit: false, + }; + + const mockTagEntity1: TagEntity = { + id: 'tag1', + name: '场景标签1', + content: '场景标签内容1', + updatedAt: Date.now(), + loadingProgress: 100, + disableEdit: false, + }; + + const mockTagEntity2: TagEntity = { + id: 'tag2', + name: '场景标签2', + content: '场景标签内容2', + updatedAt: Date.now(), + loadingProgress: 100, + disableEdit: false, + }; + + const mockShotEntity: ShotEntity = { + id: 'shot1', + name: '分镜1', + sketchUrl: 'http://example.com/sketch1.jpg', + videoUrl: 'http://example.com/video1.mp4', + roleList: [], + sceneList: [], + content: [], + status: ShotStatus.sketchLoading, + shot: [], + scriptId: 'script1', + updatedAt: Date.now(), + loadingProgress: 100, + disableEdit: false, + }; + + beforeEach(() => { + jest.clearAllMocks(); + + // 设置Mock UseCase实例 + mockSceneEditUseCase = { + AIgenerateScene: jest.fn(), + applyScene: jest.fn(), + refreshSceneData: jest.fn(), + } as any; + + mockTextEditUseCase = { + getOptimizedContent: jest.fn(), + updateText: jest.fn(), + } as any; + + mockTagEditUseCase = { + updateTag: jest.fn(), + } as any; + + // 设置Mock构造函数 + (SceneEditUseCase as jest.MockedClass).mockImplementation(() => mockSceneEditUseCase); + (TextEditUseCase as jest.MockedClass).mockImplementation(() => mockTextEditUseCase); + (TagEditUseCase as jest.MockedClass).mockImplementation(() => mockTagEditUseCase); + + // 设置Mock Item构造函数 + (SceneItem as jest.MockedClass).mockImplementation((entity) => ({ + entity, + metadata: {}, + disableEdit: entity.disableEdit, + type: 3, + } as any)); + + (TextItem as jest.MockedClass).mockImplementation((entity) => ({ + entity, + metadata: {}, + disableEdit: entity.disableEdit, + type: 0, + } as any)); + + (TagItem as jest.MockedClass).mockImplementation((entity) => ({ + entity, + metadata: {}, + disableEdit: entity.disableEdit, + type: 2, + } as any)); + }); + + describe('数据初始化测试', () => { + it('应该成功获取场景列表', async () => { + const mockScenes = [mockSceneEntity]; + (getSceneList as jest.Mock).mockResolvedValue({ + successful: true, + data: mockScenes, + message: 'success', + }); + + const result = await getSceneList({ projectId: 'project1' }); + + expect(getSceneList).toHaveBeenCalledWith({ projectId: 'project1' }); + expect(result.successful).toBe(true); + expect(result.data).toEqual(mockScenes); + }); + + it('获取场景列表失败时应该返回错误信息', async () => { + (getSceneList as jest.Mock).mockResolvedValue({ + successful: false, + message: '获取失败', + }); + + const result = await getSceneList({ projectId: 'project1' }); + + expect(result.successful).toBe(false); + expect(result.message).toBe('获取失败'); + }); + + it('应该成功获取场景数据', async () => { + (getSceneData as jest.Mock).mockResolvedValue({ + successful: true, + data: { + text: mockTextEntity, + tags: [mockTagEntity1, mockTagEntity2], + }, + }); + + const result = await getSceneData({ sceneId: 'scene1' }); + + expect(getSceneData).toHaveBeenCalledWith({ sceneId: 'scene1' }); + expect(result.successful).toBe(true); + expect(result.data.text).toEqual(mockTextEntity); + expect(result.data.tags).toEqual([mockTagEntity1, mockTagEntity2]); + }); + }); + + describe('修改文本和标签测试', () => { + it('应该成功修改AI文本', async () => { + const updatedTextEntity = { ...mockTextEntity, content: '更新后的场景文本' }; + (updateText as jest.Mock).mockResolvedValue({ + successful: true, + data: updatedTextEntity, + }); + + const result = await updateText({ + textId: 'text1', + content: '新的场景文本内容' + }); + + expect(updateText).toHaveBeenCalledWith({ + textId: 'text1', + content: '新的场景文本内容' + }); + expect(result.successful).toBe(true); + expect(result.data.content).toBe('更新后的场景文本'); + }); + + it('应该成功修改标签内容', async () => { + const updatedTagEntity = { ...mockTagEntity1, content: '更新后的场景标签' }; + (updateTag as jest.Mock).mockResolvedValue({ + successful: true, + data: updatedTagEntity, + }); + + const result = await updateTag({ + tagId: 'tag1', + content: '新的场景标签内容' + }); + + expect(updateTag).toHaveBeenCalledWith({ + tagId: 'tag1', + content: '新的场景标签内容' + }); + expect(result.successful).toBe(true); + expect(result.data.content).toBe('更新后的场景标签'); + }); + }); + + describe('文本AI优化测试', () => { + it('应该成功优化AI文本', async () => { + const optimizedContent = '优化后的场景文本内容'; + const updatedTextEntity = { ...mockTextEntity, content: optimizedContent }; + + mockTextEditUseCase.getOptimizedContent.mockResolvedValue(optimizedContent); + mockTextEditUseCase.updateText.mockResolvedValue({ + entity: updatedTextEntity, + metadata: {}, + disableEdit: false, + type: 0, + } as any); + + (updateText as jest.Mock).mockResolvedValue({ + successful: true, + data: updatedTextEntity, + }); + + // 模拟优化流程 + const optimizedContentResult = await mockTextEditUseCase.getOptimizedContent(); + const updateResult = await mockTextEditUseCase.updateText(optimizedContentResult); + + expect(mockTextEditUseCase.getOptimizedContent).toHaveBeenCalled(); + expect(mockTextEditUseCase.updateText).toHaveBeenCalledWith(optimizedContent); + expect(updateResult.entity.content).toBe(optimizedContent); + }); + + it('没有文本内容时优化应该抛出错误', async () => { + const emptyTextEntity = { ...mockTextEntity, content: '' }; + mockTextEditUseCase.getOptimizedContent.mockRejectedValue(new Error('没有可优化的文本内容')); + + await expect(mockTextEditUseCase.getOptimizedContent()).rejects.toThrow('没有可优化的文本内容'); + }); + }); + + describe('重新生成场景测试', () => { + it('应该成功重新生成场景', async () => { + const newSceneEntity = { ...mockSceneEntity, id: 'scene2', name: '新场景' }; + (regenerateScene as jest.Mock).mockResolvedValue({ + successful: true, + data: newSceneEntity, + }); + + mockSceneEditUseCase.AIgenerateScene.mockResolvedValue(newSceneEntity); + + const result = await regenerateScene({ + prompt: '重新生成场景', + tagTypes: ['tag1', 'tag2'], + sceneId: 'scene1' + }); + + expect(regenerateScene).toHaveBeenCalledWith({ + prompt: '重新生成场景', + tagTypes: ['tag1', 'tag2'], + sceneId: 'scene1' + }); + expect(result.successful).toBe(true); + expect(result.data.id).toBe('scene2'); + expect(result.data.name).toBe('新场景'); + }); + + it('重新生成场景失败时应该返回错误信息', async () => { + (regenerateScene as jest.Mock).mockResolvedValue({ + successful: false, + message: '重新生成失败', + }); + + const result = await regenerateScene({ + prompt: '重新生成场景', + tagTypes: ['tag1', 'tag2'], + sceneId: 'scene1' + }); + + expect(result.successful).toBe(false); + expect(result.message).toBe('重新生成失败'); + }); + }); + + describe('重新获取场景数据测试', () => { + it('应该成功重新获取场景数据并更新实体', async () => { + const mockTextEntity = { + id: 'text1', + content: '更新后的场景AI文本', + updatedAt: Date.now(), + loadingProgress: 100, + disableEdit: false, + }; + + const mockTags = [ + { + id: 'tag1', + name: '更新场景标签1', + content: '更新场景内容1', + updatedAt: Date.now(), + loadingProgress: 100, + disableEdit: false, + }, + { + id: 'tag2', + name: '更新场景标签2', + content: '更新场景内容2', + updatedAt: Date.now(), + loadingProgress: 100, + disableEdit: false, + } + ]; + + (getSceneData as jest.Mock).mockResolvedValue({ + successful: true, + data: { + text: mockTextEntity, + tags: mockTags, + }, + }); + + // 模拟SceneItem的setEntity方法 + const mockSetEntity = jest.fn(); + (SceneItem as jest.MockedClass).mockImplementation((entity) => ({ + entity, + metadata: {}, + disableEdit: entity.disableEdit, + type: 3, + setEntity: mockSetEntity, + } as any)); + + const sceneItem = new SceneItem(mockSceneEntity); + const useCase = new SceneEditUseCase(sceneItem); + + const result = await useCase.refreshSceneData(); + + expect(getSceneData).toHaveBeenCalledWith({ sceneId: 'scene1' }); + expect(result.text).toEqual(mockTextEntity); + expect(result.tags).toEqual(mockTags); + expect(mockSetEntity).toHaveBeenCalledWith(expect.objectContaining({ + generateTextId: 'text1', + tagIds: ['tag1', 'tag2'], + updatedAt: expect.any(Number), + })); + }); + + it('场景ID不存在时应该抛出错误', async () => { + const emptySceneEntity = { ...mockSceneEntity, id: '' }; + const sceneItem = new SceneItem(emptySceneEntity); + const useCase = new SceneEditUseCase(sceneItem); + + await expect(useCase.refreshSceneData()).rejects.toThrow('场景ID不存在,无法获取场景数据'); + }); + + it('API调用失败时应该抛出错误', async () => { + (getSceneData as jest.Mock).mockResolvedValue({ + successful: false, + message: '获取失败', + }); + + const sceneItem = new SceneItem(mockSceneEntity); + const useCase = new SceneEditUseCase(sceneItem); + + await expect(useCase.refreshSceneData()).rejects.toThrow('获取场景数据失败: 获取失败'); + }); + }); + + describe('场景应用到多个分镜测试', () => { + it('应该成功获取场景分镜列表', async () => { + const mockShots = [mockShotEntity]; + (getSceneShots as jest.Mock).mockResolvedValue({ + successful: true, + data: { + shots: mockShots, + appliedShotIds: [], + }, + }); + + const result = await getSceneShots({ sceneId: 'scene1' }); + + expect(getSceneShots).toHaveBeenCalledWith({ sceneId: 'scene1' }); + expect(result.successful).toBe(true); + expect(result.data.shots).toEqual(mockShots); + expect(result.data.appliedShotIds).toEqual([]); + }); + + it('应该成功应用场景到选中的分镜', async () => { + (applySceneToShots as jest.Mock).mockResolvedValue({ + successful: true, + data: { success: true }, + }); + + mockSceneEditUseCase.applyScene.mockResolvedValue({} as any); + + const result = await applySceneToShots({ + sceneId: 'scene1', + shotIds: ['shot1', 'shot2'] + }); + + expect(applySceneToShots).toHaveBeenCalledWith({ + sceneId: 'scene1', + shotIds: ['shot1', 'shot2'] + }); + expect(result.successful).toBe(true); + }); + + it('应用场景失败时应该返回错误信息', async () => { + (applySceneToShots as jest.Mock).mockResolvedValue({ + successful: false, + message: '应用失败', + }); + + const result = await applySceneToShots({ + sceneId: 'scene1', + shotIds: ['shot1'] + }); + + expect(result.successful).toBe(false); + expect(result.message).toBe('应用失败'); + }); + + it('应该正确处理已应用的分镜状态', async () => { + const mockShots = [mockShotEntity]; + (getSceneShots as jest.Mock).mockResolvedValue({ + successful: true, + data: { + shots: mockShots, + appliedShotIds: ['shot1'], // 分镜1已应用 + }, + }); + + const result = await getSceneShots({ sceneId: 'scene1' }); + + expect(result.data.appliedShotIds).toEqual(['shot1']); + expect(result.data.shots).toEqual(mockShots); + }); + }); + + describe('UseCase业务逻辑测试', () => { + it('SceneEditUseCase应该正确初始化', () => { + const sceneItem = new SceneItem(mockSceneEntity); + const useCase = new SceneEditUseCase(sceneItem); + + expect(SceneEditUseCase).toHaveBeenCalledWith(sceneItem); + expect(useCase).toBeDefined(); + }); + + it('TextEditUseCase应该正确初始化', () => { + const textItem = new TextItem(mockTextEntity); + const useCase = new TextEditUseCase(textItem); + + expect(TextEditUseCase).toHaveBeenCalledWith(textItem); + expect(useCase).toBeDefined(); + }); + + it('TagEditUseCase应该正确初始化', () => { + const tagItem = new TagItem(mockTagEntity1); + const useCase = new TagEditUseCase(tagItem); + + expect(TagEditUseCase).toHaveBeenCalledWith(tagItem); + expect(useCase).toBeDefined(); + }); + }); + + describe('Domain实体测试', () => { + it('SceneItem应该正确包装SceneEntity', () => { + const sceneItem = new SceneItem(mockSceneEntity); + + expect(SceneItem).toHaveBeenCalledWith(mockSceneEntity); + expect(sceneItem.entity).toEqual(mockSceneEntity); + expect(sceneItem.disableEdit).toBe(false); + }); + + it('TextItem应该正确包装AITextEntity', () => { + const textItem = new TextItem(mockTextEntity); + + expect(TextItem).toHaveBeenCalledWith(mockTextEntity); + expect(textItem.entity).toEqual(mockTextEntity); + expect(textItem.disableEdit).toBe(false); + }); + + it('TagItem应该正确包装TagEntity', () => { + const tagItem = new TagItem(mockTagEntity1); + + expect(TagItem).toHaveBeenCalledWith(mockTagEntity1); + expect(tagItem.entity).toEqual(mockTagEntity1); + expect(tagItem.disableEdit).toBe(false); + }); + }); + + describe('错误处理测试', () => { + it('API调用失败时应该正确处理错误', async () => { + (getSceneList as jest.Mock).mockRejectedValue(new Error('网络错误')); + + await expect(getSceneList({ projectId: 'project1' })).rejects.toThrow('网络错误'); + }); + + it('API返回失败状态时应该正确处理', async () => { + (getSceneList as jest.Mock).mockResolvedValue({ + successful: false, + message: '服务器错误', + }); + + const result = await getSceneList({ projectId: 'project1' }); + + expect(result.successful).toBe(false); + expect(result.message).toBe('服务器错误'); + }); + + it('UseCase未初始化时应该抛出相应错误', async () => { + const sceneItem = new SceneItem(mockSceneEntity); + const useCase = new SceneEditUseCase(sceneItem); + + // 模拟UseCase未初始化的情况 + mockSceneEditUseCase.AIgenerateScene.mockRejectedValue(new Error('场景编辑UseCase未初始化')); + + await expect(useCase.AIgenerateScene({} as any, [])).rejects.toThrow('场景编辑UseCase未初始化'); + }); + }); + + describe('场景数据完整性测试', () => { + it('应该验证场景实体的完整性', () => { + const sceneItem = new SceneItem(mockSceneEntity); + + expect(sceneItem.entity.id).toBe('scene1'); + expect(sceneItem.entity.name).toBe('测试场景'); + expect(sceneItem.entity.imageUrl).toBe('http://example.com/scene1.jpg'); + expect(sceneItem.entity.tagIds).toEqual(['tag1', 'tag2']); + expect(sceneItem.entity.generateTextId).toBe('text1'); + }); + + it('应该验证文本实体的完整性', () => { + const textItem = new TextItem(mockTextEntity); + + expect(textItem.entity.id).toBe('text1'); + expect(textItem.entity.content).toBe('这是AI生成的场景文本内容'); + }); + + it('应该验证标签实体的完整性', () => { + const tagItem = new TagItem(mockTagEntity1); + + expect(tagItem.entity.id).toBe('tag1'); + expect(tagItem.entity.name).toBe('场景标签1'); + expect(tagItem.entity.content).toBe('场景标签内容1'); + }); + }); +}); diff --git a/app/service/usecase/SceneEditUseCase.ts b/app/service/usecase/SceneEditUseCase.ts index ea87d30..34198c7 100644 --- a/app/service/usecase/SceneEditUseCase.ts +++ b/app/service/usecase/SceneEditUseCase.ts @@ -1,6 +1,6 @@ -import { SceneEntity } from '../domain/Entities'; +import { SceneEntity, AITextEntity, TagEntity } from '../domain/Entities'; import { SceneItem, TagItem, TextItem } from '../domain/Item'; -import { regenerateScene, applySceneToShots } from '@/api/video_flow'; +import { regenerateScene, applySceneToShots, getSceneData } from '@/api/video_flow'; /** * 场景编辑用例 @@ -49,4 +49,42 @@ export class SceneEditUseCase { shotIds }); } + + /** + * 重新获取当前场景的数据 + * @description 从服务器重新获取当前场景的AI文本和标签数据,并更新当前实体 + * @returns Promise<{ text: AITextEntity; tags: TagEntity[] }> 场景相关的AI文本和标签数据 + * @throws {Error} 当API调用失败时抛出错误 + */ + async refreshSceneData(): Promise<{ text: AITextEntity; tags: TagEntity[] }> { + const sceneId = this.sceneItem.entity.id; + + if (!sceneId) { + throw new Error('场景ID不存在,无法获取场景数据'); + } + + const response = await getSceneData({ + sceneId: sceneId + }); + + if (response.successful) { + // 更新当前场景的实体数据 + const { text, tags } = response.data; + + // 更新场景实体中的相关字段 + const updatedSceneEntity = { + ...this.sceneItem.entity, + generateTextId: text.id, // 更新AI文本ID + tagIds: tags.map(tag => tag.id), // 更新标签ID列表 + updatedAt: Date.now(), // 更新时间戳 + }; + + // 更新当前UseCase中的实体 + this.sceneItem.setEntity(updatedSceneEntity); + + return response.data; + } else { + throw new Error(`获取场景数据失败: ${response.message}`); + } + } } diff --git a/app/service/usecase/ShotEditUsecase.ts b/app/service/usecase/ShotEditUsecase.ts new file mode 100644 index 0000000..0397ccc --- /dev/null +++ b/app/service/usecase/ShotEditUsecase.ts @@ -0,0 +1,203 @@ +import { ShotEntity, RoleEntity, SceneEntity, AITextEntity, TagEntity } from '../domain/Entities'; +import { ShotItem, RoleItem, SceneItem, TextItem, TagItem } from '../domain/Item'; +import { + getShotRoles, + getShotScenes, + getShotData, + regenerateShot, + updateShotContent +} from '@/api/video_flow'; + +/** + * 分镜编辑用例 + * 负责分镜内容的初始化、修改和优化 + */ +export class ShotEditUseCase { + constructor(private shotItem: ShotItem) { + } + + /** + * 获取分镜关联的角色信息列表 + * @description 获取当前分镜可以使用的角色列表 + * @returns Promise 角色信息列表 + * @throws {Error} 当API调用失败时抛出错误 + */ + async getShotRoles(): Promise { + const shotId = this.shotItem.entity.id; + + if (!shotId) { + throw new Error('分镜ID不存在,无法获取角色信息'); + } + + const response = await getShotRoles({ + shotId: shotId + }); + + if (response.successful) { + return response.data; + } else { + throw new Error(`获取分镜角色信息失败: ${response.message}`); + } + } + + /** + * 获取分镜关联的场景信息列表 + * @description 获取当前分镜可以使用的场景列表 + * @returns Promise 场景信息列表 + * @throws {Error} 当API调用失败时抛出错误 + */ + async getShotScenes(): Promise { + const shotId = this.shotItem.entity.id; + + if (!shotId) { + throw new Error('分镜ID不存在,无法获取场景信息'); + } + + const response = await getShotScenes({ + shotId: shotId + }); + + if (response.successful) { + return response.data; + } else { + throw new Error(`获取分镜场景信息失败: ${response.message}`); + } + } + + /** + * 重新获取当前分镜信息 + * @description 从服务器重新获取当前分镜的详细数据,并更新当前实体 + * @returns Promise<{ text: AITextEntity; tags: TagEntity[] }> 分镜相关的AI文本和标签数据 + * @throws {Error} 当API调用失败时抛出错误 + */ + async refreshShotData(): Promise<{ text: AITextEntity; tags: TagEntity[] }> { + const shotId = this.shotItem.entity.id; + + if (!shotId) { + throw new Error('分镜ID不存在,无法获取分镜数据'); + } + + const response = await getShotData({ + shotId: shotId + }); + + if (response.successful) { + // 更新当前分镜的实体数据 + const { text, tags } = response.data; + + // 更新分镜实体中的相关字段 + const updatedShotEntity = { + ...this.shotItem.entity, + generateTextId: text.id, // 更新AI文本ID + tagIds: tags.map((tag: TagEntity) => tag.id), // 更新标签ID列表 + updatedAt: Date.now(), // 更新时间戳 + }; + + // 更新当前UseCase中的实体 + this.shotItem.setEntity(updatedShotEntity); + // 检查状态是否需要更新为视频状态 + this.checkAndUpdateVideoStatus(updatedShotEntity); + + return response.data; + } else { + throw new Error(`获取分镜数据失败: ${response.message}`); + } + } + + /** + * 重新生成当前分镜 + * @description 使用镜头、对话内容、角色ID替换参数、场景ID替换参数重新生成分镜 + * @param shotPrompt 镜头描述 + * @param dialogueContent 对话内容 + * @param roleReplaceParams 角色ID替换参数,格式为{oldId:string,newId:string}[] + * @param sceneReplaceParams 场景ID替换参数,格式为{oldId:string,newId:string}[] + * @returns Promise 重新生成的分镜实体 + * @throws {Error} 当API调用失败时抛出错误 + */ + async regenerateShot( + shotPrompt: string, + dialogueContent: string, + roleReplaceParams: { oldId: string; newId: string }[], + sceneReplaceParams: { oldId: string; newId: string }[] + ): Promise { + const shotId = this.shotItem.entity.id; + + if (!shotId) { + throw new Error('分镜ID不存在,无法重新生成分镜'); + } + + // 调用重新生成分镜接口 + const response = await regenerateShot({ + shotId: shotId, + shotPrompt: shotPrompt, + dialogueContent: dialogueContent, + roleReplaceParams: roleReplaceParams, + sceneReplaceParams: sceneReplaceParams, + }); + + if (response.successful) { + const shotEntity = response.data; + this.shotItem.setEntity(shotEntity); + // 检查状态是否需要更新为视频状态 + this.checkAndUpdateVideoStatus(shotEntity); + return shotEntity; + } else { + throw new Error(`重新生成分镜失败: ${response.message}`); + } + } + + /** + * 修改分镜对话内容 + * @description 更新分镜的对话内容,ContentItem数量和ID顺序不能变,只能修改content字段 + * @param newContent 新的对话内容数组 + * @returns Promise 修改后的分镜实体 + * @throws {Error} 当API调用失败时抛出错误 + */ + async updateShotContent(newContent: Array<{ roleId: string; content: string }>): Promise { + const shotId = this.shotItem.entity.id; + + if (!shotId) { + throw new Error('分镜ID不存在,无法修改对话内容'); + } + + // 验证ContentItem数量和ID顺序 + const currentContent = this.shotItem.entity.content; + if (newContent.length !== currentContent.length) { + throw new Error('ContentItem数量不能改变'); + } + + // 验证角色ID顺序 + for (let i = 0; i < newContent.length; i++) { + if (newContent[i].roleId !== currentContent[i].roleId) { + throw new Error('ContentItem的roleId顺序不能改变'); + } + } + + const response = await updateShotContent({ + shotId: shotId, + content: newContent, + }); + + if (response.successful) { + const shotEntity = response.data; + this.shotItem.setEntity(shotEntity); + // 检查状态是否需要更新为视频状态 + this.checkAndUpdateVideoStatus(shotEntity); + return shotEntity; + } else { + throw new Error(`修改分镜对话内容失败: ${response.message}`); + } + } + + /** + * 检查并更新视频状态 + * @description 当分镜状态变为视频加载中或完成时,调用updateToVideoStatus + * @param shotEntity 分镜实体 + */ + private checkAndUpdateVideoStatus(shotEntity: ShotEntity): void { + // 当状态为视频加载中或完成时,更新为视频状态 + if (shotEntity.status === 1 || shotEntity.status === 2) { // videoLoading 或 finished + this.shotItem.updateToVideoStatus(); + } + } +} diff --git a/package.json b/package.json index cfd7fe8..c00f8ab 100644 --- a/package.json +++ b/package.json @@ -6,7 +6,9 @@ "dev": "next dev", "build": "next build", "start": "next start", - "lint": "next lint" + "lint": "next lint", + "test": "jest", + "test:watch": "jest --watch" }, "dependencies": { "@dnd-kit/core": "^6.3.1",