新增角色和场景服务Hook,支持角色和场景的生成、应用及标签修改功能;更新API接口以支持新功能。

This commit is contained in:
海龙 2025-07-30 11:04:33 +08:00
parent 39a3de215c
commit bf4247cfae
12 changed files with 3172 additions and 2309 deletions

View File

@ -2,6 +2,7 @@ import { post } from './request';
import { ProjectTypeEnum } from '@/app/model/enums'; import { ProjectTypeEnum } from '@/app/model/enums';
import { ApiResponse } from '@/api/common'; import { ApiResponse } from '@/api/common';
import { BASE_URL } from './constants' import { BASE_URL } from './constants'
import { AITextEntity, RoleEntity, SceneEntity, ShotEntity, TagEntity } from '@/app/service/domain/Entities';
// API 响应类型 // API 响应类型
interface BaseApiResponse<T> { interface BaseApiResponse<T> {
@ -86,7 +87,7 @@ export interface Character {
// 剧本到分镜头提示词模型 // 剧本到分镜头提示词模型
export interface ScenePrompts { export interface ScenePrompts {
scenes: Scene[]; // 分场景列表 scenes: Scene[]; // 分场景列表
characters?: Character[]; // 角色列表 characters?: Character[]; // 角色列表
summary?: string; // 剧情概要 summary?: string; // 剧情概要
scene?: string; // 场景描述 scene?: string; // 场景描述
atmosphere?: string; // 氛围描述 atmosphere?: string; // 氛围描述
@ -102,7 +103,7 @@ export interface ScriptToSceneRequest {
project_type: ProjectTypeEnum.SCRIPT_TO_VIDEO; project_type: ProjectTypeEnum.SCRIPT_TO_VIDEO;
} }
// 视频转分镜头请求接口 // 视频转分镜头请求接口
export interface VideoToSceneRequest { export interface VideoToSceneRequest {
video_url: string; video_url: string;
episode_id: number; episode_id: number;
@ -183,8 +184,8 @@ export const convertVideoToScene = async (
// 新-获取剧集详情 // 新-获取剧集详情
export const detailScriptEpisodeNew = async (data: { project_id: string }): Promise<ApiResponse<any>> => { export const detailScriptEpisodeNew = async (data: { project_id: string }): Promise<ApiResponse<any>> => {
return post<ApiResponse<any>>('/movie/get_movie_project_detail', data); return post<ApiResponse<any>>('/movie/get_movie_project_detail', data);
}; };
// 获取 title 接口 // 获取 title 接口
export const getScriptTitle = async (data: { project_id: string }): Promise<ApiResponse<any>> => { export const getScriptTitle = async (data: { project_id: string }): Promise<ApiResponse<any>> => {
@ -214,4 +215,127 @@ export const getShotSketchJson = async (data: { project_id: string }): Promise<A
// 获取 loading-视频 接口 // 获取 loading-视频 接口
export const getVideoJson = async (data: { project_id: string }): Promise<ApiResponse<any>> => { export const getVideoJson = async (data: { project_id: string }): Promise<ApiResponse<any>> => {
return post<any>('/movie/video_json', data); return post<any>('/movie/video_json', data);
}; };
/**
*
* @param request -
* @returns Promise<ApiResponse<RoleEntity>>
*/
export const regenerateRole = async (request: {
/** 角色提示词 */
prompt: string;
/** 标签类型列表 */
tagTypes: (number | string)[];
/** 角色ID可选如果重新生成现有角色 */
roleId?: string;
}): Promise<ApiResponse<RoleEntity>> => {
return post<ApiResponse<any>>('/movie/regenerate_role', request);
};
/**
*
* @param request -
* @returns Promise<ApiResponse<应用结果>>
*/
export const applyRoleToShots = async (request: {
/** 角色ID */
roleId: string;
/** 分镜ID列表 */
shotIds: string[];
}): Promise<ApiResponse<any>> => {
return post<ApiResponse<any>>('/movie/apply_role_to_shots', request);
};
/**
*
* @param request -
* @returns Promise<ApiResponse<修改后的标签>>
*/
export const updateTag = async (request: {
/** 标签ID */
tagId: string;
/** 新的标签内容 */
content: string|number;
}): Promise<ApiResponse<TagEntity>> => {
return post<ApiResponse<any>>('/movie/update_tag', request);
};
/**
*
* @param request -
* @returns Promise<ApiResponse<修改后的文案>>
*/
export const updateText = async (request: {
/** 文案ID */
textId: string;
/** 新的文案内容 */
content: string;
}): Promise<ApiResponse<AITextEntity>> => {
return post<ApiResponse<any>>('/movie/update_text', request);
};
/**
*
* @param request -
* @returns Promise<ApiResponse<重新生成的场景>>
*/
export const regenerateScene = async (request: {
/** 场景提示词 */
prompt: string;
/** 标签类型列表 */
tagTypes: (number | string)[];
/** 场景ID可选如果重新生成现有场景 */
sceneId?: string;
}): Promise<ApiResponse<SceneEntity>> => {
return post<ApiResponse<any>>('/movie/regenerate_scene', request);
};
/**
*
* @param request -
* @returns Promise<ApiResponse<应用结果>>
*/
export const applySceneToShots = async (request: {
/** 场景ID */
sceneId: string;
/** 分镜ID列表 */
shotIds: string[];
}): Promise<ApiResponse<any>> => {
return post<ApiResponse<any>>('/movie/apply_scene_to_shots', request);
};
/**
*
* @param request -
* @returns Promise<ApiResponse<分镜列表>>
*/
export const getRoleShots = async (request: {
/** 角色ID */
roleId: string;
}): Promise<ApiResponse<{
/** 分镜列表 */
shots: ShotEntity[];
/** 已应用的分镜ID列表 */
appliedShotIds: string[];
}>> => {
return post<ApiResponse<any>>('/movie/get_role_shots', request);
};
/**
*
* @param request -
* @returns Promise<ApiResponse<分镜列表>>
*/
export const getSceneShots = async (request: {
/** 场景ID */
sceneId: string;
}): Promise<ApiResponse<{
/** 分镜列表 */
shots: ShotEntity[];
/** 已应用的分镜ID列表 */
appliedShotIds: string[];
}>> => {
return post<ApiResponse<any>>('/movie/get_scene_shots', request);
};

View File

@ -0,0 +1,151 @@
@startuml RoleService Hook 架构图
!theme plain
skinparam backgroundColor #FFFFFF
skinparam componentStyle rectangle
' 主要模块
package "RoleService Hook" as RoleServiceHook {
component "响应式状态管理" as StateManagement
component "计算属性" as ComputedProps
component "角色操作方法" as RoleOperations
component "文本操作方法" as TextOperations
component "标签操作方法" as TagOperations
component "分镜操作方法" as ShotOperations
}
' API层
package "API接口层" as APILayer {
component "角色相关API" as RoleAPI {
[regenerateRole()]
[applyRoleToShots()]
[getRoleShots()]
}
component "文本相关API" as TextAPI {
[updateText()]
}
component "标签相关API" as TagAPI {
[updateTag()]
}
}
' UseCase层
package "UseCase层" as UseCaseLayer {
component "RoleEditUseCase" as RoleEditUseCase {
[AIgenerateRole(prompt, tags)]
[applyRole(shotIds)]
}
component "TextEditUseCase" as TextEditUseCase {
[updateText(content)]
[getOptimizedContent()]
}
component "TagEditUseCase" as TagEditUseCase {
[updateTag(content)]
}
}
' Domain层
package "Domain层" as DomainLayer {
component "实体定义" as Entities {
[RoleEntity]
[AITextEntity]
[TagEntity]
[ShotEntity]
}
component "可编辑项" as Items {
[RoleItem]
[TextItem]
[TagItem]
[ShotItem]
}
}
' React Hook
package "React Hook" as ReactHook {
component "useState" as UseState
component "useCallback" as UseCallback
component "useMemo" as UseMemo
}
' 依赖关系
' Hook内部依赖
RoleServiceHook --> StateManagement : 管理状态
RoleServiceHook --> ComputedProps : 计算属性
RoleServiceHook --> RoleOperations : 角色操作
RoleServiceHook --> TextOperations : 文本操作
RoleServiceHook --> TagOperations : 标签操作
RoleServiceHook --> ShotOperations : 分镜操作
' 操作方法依赖UseCase
RoleOperations --> RoleEditUseCase : 调用
TextOperations --> TextEditUseCase : 调用
TagOperations --> TagEditUseCase : 调用
ShotOperations --> RoleEditUseCase : 调用
' UseCase依赖API
RoleEditUseCase --> RoleAPI : 调用
TextEditUseCase --> TextAPI : 调用
TagEditUseCase --> TagAPI : 调用
' 状态管理依赖Domain
StateManagement --> Items : 使用
ComputedProps --> Entities : 计算
RoleOperations --> Items : 操作
TextOperations --> Items : 操作
TagOperations --> Items : 操作
ShotOperations --> Items : 操作
' React Hook依赖
RoleServiceHook --> UseState : 状态管理
RoleServiceHook --> UseCallback : 方法优化
RoleServiceHook --> UseMemo : 计算优化
' 数据流
note right of StateManagement
响应式状态:
- roleList: 角色列表
- selectedRole: 当前选中角色
- currentRoleText: 当前AI文本
- currentRoleTags: 当前标签列表
- shotSelectionList: 分镜选择列表
end note
note right of ComputedProps
计算属性:
- roleImageUrl: 角色图片URL
- isAllShotsSelected: 是否全选
- selectedShotsCount: 选中数量
end note
note right of RoleOperations
角色操作:
- selectRole: 选择角色
- regenerateRole: 重新生成
end note
note right of TextOperations
文本操作:
- optimizeRoleText: 优化文本
- updateRoleText: 修改文本
end note
note right of TagOperations
标签操作:
- updateTagContent: 修改标签
end note
note right of ShotOperations
分镜操作:
- fetchRoleShots: 获取分镜列表
- selectAllShots: 全选
- invertShotSelection: 反选
- toggleShotSelection: 切换选择
- applyRoleToSelectedShots: 应用角色
end note
@enduml

View File

@ -0,0 +1,391 @@
import { useState, useCallback, useMemo } from 'react';
import { RoleEntity, TagEntity, AITextEntity, ShotEntity } from '../domain/Entities';
import { RoleItem, TagItem, TextItem, ShotItem } from '../domain/Item';
import { RoleEditUseCase } from '../usecase/RoleEditUseCase';
import { TagEditUseCase } from '../usecase/TagEditUseCase';
import { TextEditUseCase } from '../usecase/TextEditUseCase';
import { getRoleShots } from '@/api/video_flow';
/**
*
*/
interface ShotSelectionItem {
/** 分镜ID */
id: string;
/** 分镜名称 */
name: string;
/** 是否已选中 */
selected: boolean;
/** 是否已应用角色 */
applied: boolean;
/** 分镜数据 */
shot: ShotEntity;
}
/**
* Hook返回值接口
*/
interface UseRoleService {
// 响应式数据
/** 角色列表 */
roleList: RoleItem[];
/** 当前选中的角色 */
selectedRole: RoleItem | null;
/** 当前角色的AI文本 */
currentRoleText: TextItem | null;
/** 当前角色的标签列表 */
currentRoleTags: TagItem[];
/** 角色图片URL */
roleImageUrl: string;
/** 分镜选择列表 */
shotSelectionList: ShotSelectionItem[];
/** 是否全选分镜 */
isAllShotsSelected: boolean;
/** 已选中的分镜数量 */
selectedShotsCount: number;
// 操作方法
/** 选择角色 */
selectRole: (roleId: string) => void;
/** 设置当前角色的AI文本和标签 */
setCurrentRoleData: (text: TextItem, tags: TagItem[]) => void;
/** 优化AI文本 */
optimizeRoleText: () => Promise<void>;
/** 修改AI文本 */
updateRoleText: (newContent: string) => Promise<void>;
/** 修改标签内容 */
updateTagContent: (tagId: string, newContent: string | number) => Promise<void>;
/** 重新生成角色 */
regenerateRole: () => Promise<void>;
/** 获取角色出现的分镜列表 */
fetchRoleShots: () => Promise<void>;
/** 切换全选与全不选 */
toggleSelectAllShots: () => void;
/** 选择/取消选择单个分镜 */
toggleShotSelection: (shotId: string) => void;
/** 应用角色到选中的分镜 */
applyRoleToSelectedShots: () => Promise<void>;
}
/**
* Hook
*
*/
export const useRoleServiceHook = (): UseRoleService => {
// 响应式状态
const [roleList, setRoleList] = useState<RoleItem[]>([]);
const [selectedRole, setSelectedRole] = useState<RoleItem | null>(null);
const [currentRoleText, setCurrentRoleText] = useState<TextItem | null>(null);
const [currentRoleTags, setCurrentRoleTags] = useState<TagItem[]>([]);
const [shotSelectionList, setShotSelectionList] = useState<ShotSelectionItem[]>([]);
// UseCase实例 - 在角色选择时初始化
const [roleEditUseCase, setRoleEditUseCase] = useState<RoleEditUseCase | null>(null);
const [textEditUseCase, setTextEditUseCase] = useState<TextEditUseCase | null>(null);
const [tagEditUseCases, setTagEditUseCases] = useState<Map<string, TagEditUseCase>>(new Map());
// 计算属性
/**
* URL
* @description URL
*/
const roleImageUrl = useMemo(() => {
return selectedRole?.entity.imageUrl || '';
}, [selectedRole]);
/**
*
* @description
*/
const isAllShotsSelected = useMemo(() => {
return shotSelectionList.length > 0 && shotSelectionList.every(shot => shot.selected);
}, [shotSelectionList]);
/**
*
* @description
*/
const selectedShotsCount = useMemo(() => {
return shotSelectionList.filter(shot => shot.selected).length;
}, [shotSelectionList]);
/**
*
* @description ID选择角色UseCase实例
* @param roleId ID
*/
const selectRole = useCallback((roleId: string) => {
const role = roleList.find(r => r.entity.id === roleId);
if (role) {
setSelectedRole(role);
// 初始化UseCase实例
setRoleEditUseCase(new RoleEditUseCase(role));
setTextEditUseCase(null); // 文本UseCase在获取到文本后初始化
setTagEditUseCases(new Map()); // 标签UseCase在获取到标签后初始化
setCurrentRoleText(null);
setCurrentRoleTags([]);
}
}, [roleList]);
/**
* AI文本
* @description AI文本进行优化
* @throws {Error} UseCase未初始化时抛出错误
* @returns {Promise<void>} Promise
*/
const optimizeRoleText = useCallback(async () => {
if (!textEditUseCase) {
throw new Error('文本编辑UseCase未初始化');
}
if (!currentRoleText || !currentRoleText.entity.content) {
throw new Error('没有可优化的文本内容');
}
const optimizedContent = await textEditUseCase.getOptimizedContent();
// 更新文本内容
const updatedTextItem = await textEditUseCase.updateText(optimizedContent);
setCurrentRoleText(updatedTextItem);
}, [textEditUseCase, currentRoleText]);
/**
* AI文本
* @description AI文本内容
* @param newContent
* @throws {Error} UseCase未初始化时抛出错误
* @returns {Promise<void>} Promise
*/
const updateRoleText = useCallback(async (newContent: string) => {
if (!textEditUseCase) {
throw new Error('文本编辑UseCase未初始化');
}
if (!currentRoleText) {
throw new Error('没有可编辑的文本');
}
const updatedTextItem = await textEditUseCase.updateText(newContent);
setCurrentRoleText(updatedTextItem);
}, [textEditUseCase, currentRoleText]);
/**
*
* @description
* @param tagId ID
* @param newContent
* @throws {Error} UseCase未初始化时抛出错误
* @returns {Promise<void>} Promise
*/
const updateTagContent = useCallback(async (tagId: string, newContent: string | number) => {
const tagEditUseCase = tagEditUseCases.get(tagId);
if (!tagEditUseCase) {
throw new Error(`标签编辑UseCase未初始化标签ID: ${tagId}`);
}
const updatedTagItem = await tagEditUseCase.updateTag(newContent);
// 更新标签列表
setCurrentRoleTags(prev =>
prev.map(tag =>
tag.entity.id === tagId
? updatedTagItem
: tag
)
);
}, [tagEditUseCases]);
/**
*
* @description 使AI文本和标签重新生成角色
* @throws {Error} UseCase未初始化时抛出错误
* @returns {Promise<void>} Promise
*/
const regenerateRole = useCallback(async () => {
if (!roleEditUseCase) {
throw new Error('角色编辑UseCase未初始化');
}
if (!selectedRole || !currentRoleText || currentRoleTags.length === 0) {
throw new Error('缺少重新生成角色所需的数据');
}
const newRoleEntity = await roleEditUseCase.AIgenerateRole(currentRoleText, currentRoleTags);
// 更新角色
const newRoleItem = new RoleItem(newRoleEntity);
setSelectedRole(newRoleItem);
// 更新角色列表
setRoleList(prev =>
prev.map(role =>
role.entity.id === newRoleEntity.id ? newRoleItem : role
)
);
}, [roleEditUseCase, selectedRole, currentRoleText, currentRoleTags]);
/**
*
* @description
* @throws {Error} API调用失败时抛出错误
* @returns {Promise<void>} Promise
*/
const fetchRoleShots = useCallback(async () => {
if (!selectedRole) {
throw new Error('请先选择角色');
}
try {
const response = await getRoleShots({
roleId: selectedRole.entity.id
});
if (response.successful) {
const { shots, appliedShotIds } = response.data;
const shotSelectionItems: ShotSelectionItem[] = shots.map(shot => ({
id: shot.id,
name: shot.name,
selected: false,
applied: appliedShotIds.includes(shot.id), // 根据API返回的已应用列表判断
shot
}));
setShotSelectionList(shotSelectionItems);
} else {
throw new Error(`获取角色分镜列表失败: ${response.message}`);
}
} catch (error) {
console.error('获取角色分镜列表失败:', error);
throw error;
}
}, [selectedRole]);
/**
*
* @description
*/
const toggleSelectAllShots = useCallback(() => {
setShotSelectionList(prev => {
const isAllSelected = prev.length > 0 && prev.every(shot => shot.selected);
return prev.map(shot => ({ ...shot, selected: !isAllSelected }));
});
}, []);
/**
* /
* @description
* @param shotId ID
*/
const toggleShotSelection = useCallback((shotId: string) => {
setShotSelectionList(prev =>
prev.map(shot =>
shot.id === shotId
? { ...shot, selected: !shot.selected }
: shot
)
);
}, []);
/**
*
* @description
* @throws {Error} UseCase未初始化时抛出错误
* @returns {Promise<void>} Promise
*/
const applyRoleToSelectedShots = useCallback(async () => {
if (!roleEditUseCase) {
throw new Error('角色编辑UseCase未初始化');
}
if (!selectedRole) {
throw new Error('请先选择角色');
}
const selectedShotIds = shotSelectionList
.filter(shot => shot.selected)
.map(shot => shot.id);
if (selectedShotIds.length === 0) {
throw new Error('请先选择要应用的分镜');
}
await roleEditUseCase.applyRole(selectedShotIds);
// 更新分镜列表,标记已应用
setShotSelectionList(prev =>
prev.map(shot =>
selectedShotIds.includes(shot.id)
? { ...shot, applied: true, selected: false }
: shot
)
);
}, [roleEditUseCase, selectedRole, shotSelectionList]);
/**
* AI文本和标签
* @description AI文本和标签UseCase
* @param text AI文本项
* @param tags
*/
const setCurrentRoleData = useCallback((text: TextItem, tags: TagItem[]) => {
setCurrentRoleText(text);
setCurrentRoleTags(tags);
// 初始化文本UseCase
if (text) {
setTextEditUseCase(new TextEditUseCase(text));
}
// 初始化标签UseCase
const newTagEditUseCases = new Map<string, TagEditUseCase>();
tags.forEach(tag => {
newTagEditUseCases.set(tag.entity.id, new TagEditUseCase(tag));
});
setTagEditUseCases(newTagEditUseCases);
}, []);
return {
// 响应式数据
/** 角色列表 */
roleList,
/** 当前选中的角色 */
selectedRole,
/** 当前角色的AI文本 */
currentRoleText,
/** 当前角色的标签列表 */
currentRoleTags,
/** 角色图片URL */
roleImageUrl,
/** 分镜选择列表 */
shotSelectionList,
/** 是否全选分镜 */
isAllShotsSelected,
/** 已选中的分镜数量 */
selectedShotsCount,
// 操作方法
/** 选择角色 */
selectRole,
/** 设置当前角色的AI文本和标签 */
setCurrentRoleData,
/** 优化AI文本 */
optimizeRoleText,
/** 修改AI文本 */
updateRoleText,
/** 修改标签内容 */
updateTagContent,
/** 重新生成角色 */
regenerateRole,
/** 获取角色出现的分镜列表 */
fetchRoleShots,
/** 切换全选与全不选 */
toggleSelectAllShots,
/** 选择/取消选择单个分镜 */
toggleShotSelection,
/** 应用角色到选中的分镜 */
applyRoleToSelectedShots
};
};

View File

@ -0,0 +1,301 @@
import { useState, useCallback, useMemo } from 'react';
import { SceneEntity, TagEntity, AITextEntity, ShotEntity } from '../domain/Entities';
import { SceneItem, TagItem, TextItem, ShotItem } from '../domain/Item';
import { SceneEditUseCase } from '../usecase/SceneEditUseCase';
import { TagEditUseCase } from '../usecase/TagEditUseCase';
import { TextEditUseCase } from '../usecase/TextEditUseCase';
import { getSceneShots } from '@/api/video_flow';
/**
*
*/
interface ShotSelectionItem {
/** 分镜ID */
id: string;
/** 分镜名称 */
name: string;
/** 是否已选中 */
selected: boolean;
/** 是否已应用场景 */
applied: boolean;
/** 分镜数据 */
shot: ShotEntity;
}
/**
* Hook返回值接口
*/
interface UseSceneService {
// 响应式数据
/** 场景列表 */
sceneList: SceneItem[];
/** 当前选中的场景 */
selectedScene: SceneItem | null;
/** 当前场景的AI文本 */
currentSceneText: TextItem | null;
/** 当前场景的标签列表 */
currentSceneTags: TagItem[];
/** 场景图片URL */
sceneImageUrl: string;
/** 分镜选择列表 */
shotSelectionList: ShotSelectionItem[];
/** 是否全选分镜 */
isAllShotsSelected: boolean;
/** 已选中的分镜数量 */
selectedShotsCount: number;
// 操作方法
/** 选择场景 */
selectScene: (sceneId: string) => void;
/** 设置当前场景的AI文本和标签 */
setCurrentSceneData: (text: TextItem, tags: TagItem[]) => void;
/** 优化AI文本 */
optimizeSceneText: () => Promise<void>;
/** 修改AI文本 */
updateSceneText: (newContent: string) => Promise<void>;
/** 修改标签内容 */
updateTagContent: (tagId: string, newContent: string | number) => Promise<void>;
/** 重新生成场景 */
regenerateScene: () => Promise<void>;
/** 获取场景出现的分镜列表 */
fetchSceneShots: () => Promise<void>;
/** 切换全选与全不选 */
toggleSelectAllShots: () => void;
/** 选择/取消选择单个分镜 */
toggleShotSelection: (shotId: string) => void;
/** 应用场景到选中的分镜 */
applySceneToSelectedShots: () => Promise<void>;
}
/**
* Hook
*
*/
export const useSceneServiceHook = (): UseSceneService => {
// 响应式状态
const [sceneList, setSceneList] = useState<SceneItem[]>([]);
const [selectedScene, setSelectedScene] = useState<SceneItem | null>(null);
const [currentSceneText, setCurrentSceneText] = useState<TextItem | null>(null);
const [currentSceneTags, setCurrentSceneTags] = useState<TagItem[]>([]);
const [shotSelectionList, setShotSelectionList] = useState<ShotSelectionItem[]>([]);
// UseCase实例 - 在场景选择时初始化
const [sceneEditUseCase, setSceneEditUseCase] = useState<SceneEditUseCase | null>(null);
const [textEditUseCase, setTextEditUseCase] = useState<TextEditUseCase | null>(null);
const [tagEditUseCases, setTagEditUseCases] = useState<Map<string, TagEditUseCase>>(new Map());
// 计算属性
const sceneImageUrl = useMemo(() => {
return selectedScene?.entity.imageUrl || '';
}, [selectedScene]);
const isAllShotsSelected = useMemo(() => {
return shotSelectionList.length > 0 && shotSelectionList.every(shot => shot.selected);
}, [shotSelectionList]);
const selectedShotsCount = useMemo(() => {
return shotSelectionList.filter(shot => shot.selected).length;
}, [shotSelectionList]);
// 选择场景
const selectScene = useCallback((sceneId: string) => {
const scene = sceneList.find(s => s.entity.id === sceneId);
if (scene) {
setSelectedScene(scene);
setSceneEditUseCase(new SceneEditUseCase(scene));
setTextEditUseCase(null);
setTagEditUseCases(new Map());
setCurrentSceneText(null);
setCurrentSceneTags([]);
}
}, [sceneList]);
// 设置当前场景数据
const setCurrentSceneData = useCallback((text: TextItem, tags: TagItem[]) => {
setCurrentSceneText(text);
setCurrentSceneTags(tags);
if (text) {
setTextEditUseCase(new TextEditUseCase(text));
}
const newTagEditUseCases = new Map<string, TagEditUseCase>();
tags.forEach(tag => {
newTagEditUseCases.set(tag.entity.id, new TagEditUseCase(tag));
});
setTagEditUseCases(newTagEditUseCases);
}, []);
// 优化AI文本
const optimizeSceneText = useCallback(async () => {
if (!textEditUseCase) {
throw new Error('文本编辑UseCase未初始化');
}
if (!currentSceneText || !currentSceneText.entity.content) {
throw new Error('没有可优化的文本内容');
}
const optimizedContent = await textEditUseCase.getOptimizedContent();
const updatedTextItem = await textEditUseCase.updateText(optimizedContent);
setCurrentSceneText(updatedTextItem);
}, [textEditUseCase, currentSceneText]);
// 修改AI文本
const updateSceneText = useCallback(async (newContent: string) => {
if (!textEditUseCase) {
throw new Error('文本编辑UseCase未初始化');
}
if (!currentSceneText) {
throw new Error('没有可编辑的文本');
}
const updatedTextItem = await textEditUseCase.updateText(newContent);
setCurrentSceneText(updatedTextItem);
}, [textEditUseCase, currentSceneText]);
// 修改标签内容
const updateTagContent = useCallback(async (tagId: string, newContent: string | number) => {
const tagEditUseCase = tagEditUseCases.get(tagId);
if (!tagEditUseCase) {
throw new Error(`标签编辑UseCase未初始化标签ID: ${tagId}`);
}
const updatedTagItem = await tagEditUseCase.updateTag(newContent);
setCurrentSceneTags(prev =>
prev.map(tag =>
tag.entity.id === tagId
? updatedTagItem
: tag
)
);
}, [tagEditUseCases]);
// 重新生成场景
const regenerateScene = useCallback(async () => {
if (!sceneEditUseCase) {
throw new Error('场景编辑UseCase未初始化');
}
if (!selectedScene || !currentSceneText || currentSceneTags.length === 0) {
throw new Error('缺少重新生成场景所需的数据');
}
const newSceneEntity = await sceneEditUseCase.AIgenerateScene(currentSceneText, currentSceneTags);
const newSceneItem = new SceneItem(newSceneEntity);
setSelectedScene(newSceneItem);
setSceneList(prev =>
prev.map(scene =>
scene.entity.id === newSceneEntity.id ? newSceneItem : scene
)
);
}, [sceneEditUseCase, selectedScene, currentSceneText, currentSceneTags]);
// 获取场景分镜列表
const fetchSceneShots = useCallback(async () => {
if (!selectedScene) {
throw new Error('请先选择场景');
}
try {
const response = await getSceneShots({
sceneId: selectedScene.entity.id
});
if (response.successful) {
const { shots, appliedShotIds } = response.data;
const shotSelectionItems: ShotSelectionItem[] = shots.map(shot => ({
id: shot.id,
name: shot.name,
selected: false,
applied: appliedShotIds.includes(shot.id),
shot
}));
setShotSelectionList(shotSelectionItems);
} else {
throw new Error(`获取场景分镜列表失败: ${response.message}`);
}
} catch (error) {
console.error('获取场景分镜列表失败:', error);
throw error;
}
}, [selectedScene]);
// 切换全选与全不选
const toggleSelectAllShots = useCallback(() => {
setShotSelectionList(prev => {
const isAllSelected = prev.length > 0 && prev.every(shot => shot.selected);
return prev.map(shot => ({ ...shot, selected: !isAllSelected }));
});
}, []);
// 选择/取消选择单个分镜
const toggleShotSelection = useCallback((shotId: string) => {
setShotSelectionList(prev =>
prev.map(shot =>
shot.id === shotId
? { ...shot, selected: !shot.selected }
: shot
)
);
}, []);
// 应用场景到选中的分镜
const applySceneToSelectedShots = useCallback(async () => {
if (!sceneEditUseCase) {
throw new Error('场景编辑UseCase未初始化');
}
if (!selectedScene) {
throw new Error('请先选择场景');
}
const selectedShotIds = shotSelectionList
.filter(shot => shot.selected)
.map(shot => shot.id);
if (selectedShotIds.length === 0) {
throw new Error('请先选择要应用的分镜');
}
await sceneEditUseCase.applyScene(selectedShotIds);
setShotSelectionList(prev =>
prev.map(shot =>
selectedShotIds.includes(shot.id)
? { ...shot, applied: true, selected: false }
: shot
)
);
}, [sceneEditUseCase, selectedScene, shotSelectionList]);
return {
// 响应式数据
sceneList,
selectedScene,
currentSceneText,
currentSceneTags,
sceneImageUrl,
shotSelectionList,
isAllShotsSelected,
selectedShotsCount,
// 操作方法
selectScene,
setCurrentSceneData,
optimizeSceneText,
updateSceneText,
updateTagContent,
regenerateScene,
fetchSceneShots,
toggleSelectAllShots,
toggleShotSelection,
applySceneToSelectedShots
};
};

View File

@ -0,0 +1,99 @@
/**
*
*
*/
/**
*
*/
export interface BaseEntity {
/** 唯一标识 */
readonly id: string;
/** 更新时间 */
readonly updatedAt: number;
/**loading进度 0-100 */
loadingProgress: number;
/** 禁止编辑 */
disableEdit: boolean;
}
/**
* AI文本实体接口
*/
export interface AITextEntity extends BaseEntity {
/** 文本内容 */
content: string;
}
/**
*
*/
export interface RoleEntity extends BaseEntity {
/** 角色名称 */
name: string;
/** 角色提示词Id */
generateTextId: string;
/**角色标签 */
tagIds: string[];
/** 角色图片URL */
imageUrl: string;
}
/**
*
*/
export interface TagEntity extends BaseEntity {
/** 标签名称 */
name: string;
/** 内容标签类型 */
content: number | string;
}
/**
*
*/
export interface SceneEntity extends BaseEntity {
/** 场景名称 */
name: string;
/** 场景图片URL */
imageUrl: string;
/** 场景标签 */
tagIds: string[];
/** 场景提示词Id */
generateTextId: string;
}
interface RoleMap {
/** 角色ID */
roleId: string;
/** 人物ID */
figureId: string;
}
interface ContentItem {
/** 角色ID */
roleId: string;
/** 对话内容 */
content: string;
}
/**
*
*/
export interface ShotEntity extends BaseEntity {
/** 分镜名称 */
name: string;
/**分镜草图Url */
sketchUrl: string;
/**分镜视频Url */
videoUrl: string;
/**人物ID */
roleMap: RoleMap[];
/**对话内容 */
content: ContentItem[];
/**镜头项 */
shot: string[];
}

127
app/service/domain/Item.ts Normal file
View File

@ -0,0 +1,127 @@
import {
BaseEntity,
AITextEntity,
RoleEntity,
TagEntity,
SceneEntity,
ShotEntity
} from './Entities';
/**
*
*/
export enum ItemType {
/** 文本 */
TEXT,
/** 图片 */
IMAGE,
/** 视频 */
VIDEO,
}
/**
*
*/
export abstract class EditItem<T extends BaseEntity> {
/** 被包装的实体 */
entity!: T;
/** 编辑元数据 */
metadata: Record<string, any>;
/**禁用编辑 */
disableEdit!: boolean;
/** 类型 */
abstract type: ItemType;
constructor(
entity: T,
metadata: Record<string, any> = {}
) {
this.metadata = metadata;
this.setEntity(entity);
}
/**
*
*/
setMetadata(metadata: Record<string, any>): void {
this.metadata = metadata;
}
/**
*
*/
setEntity(entity: T): void {
this.entity = entity;
this.disableEdit = entity.disableEdit;
}
}
/**
* AI文本可编辑项
*/
export class TextItem extends EditItem<AITextEntity> {
type: ItemType.TEXT = ItemType.TEXT;
constructor(
entity: AITextEntity,
metadata: Record<string, any> = {}
) {
super(entity, metadata);
}
}
/**
*
*/
export class RoleItem extends EditItem<RoleEntity> {
type: ItemType.IMAGE = ItemType.IMAGE;
constructor(
entity: RoleEntity,
metadata: Record<string, any> = {}
) {
super(entity, metadata);
}
}
/**
*
*/
export class TagItem extends EditItem<TagEntity> {
type: ItemType.TEXT = ItemType.TEXT;
constructor(
entity: TagEntity,
metadata: Record<string, any> = {}
) {
super(entity, metadata);
}
}
/**
*
*/
export class SceneItem extends EditItem<SceneEntity> {
type: ItemType.IMAGE = ItemType.IMAGE;
constructor(
entity: SceneEntity,
metadata: Record<string, any> = {}
) {
super(entity, metadata);
}
}
/**
*
*/
export class ShotItem extends EditItem<ShotEntity> {
type: ItemType.IMAGE = ItemType.IMAGE;
constructor(
entity: ShotEntity,
metadata: Record<string, any> = {}
) {
super(entity, metadata);
}
}

View File

@ -0,0 +1,52 @@
import { RoleEntity } from '../domain/Entities';
import { RoleItem, TagItem, TextItem } from '../domain/Item';
import { regenerateRole, applyRoleToShots } from '@/api/video_flow';
/**
*
*
*/
export class RoleEditUseCase {
constructor(private roleItem: RoleItem) {
}
/**
* @description:
* @param {TextItem} prompt
* @param {TagItem[]} tags
* @return {*}
*/
async AIgenerateRole(prompt: TextItem, tags: TagItem[]): Promise<RoleEntity> {
const promptText = prompt.entity.content;
const tagList = tags.map((tag) => tag.entity.content);
// 调用重新生成角色接口
const response = await regenerateRole({
roleId: this.roleItem.entity.id||'',
prompt: promptText,
tagTypes: tagList,
});
if (response.successful) {
const roleEntity = response.data;
this.roleItem.setEntity(roleEntity);
return roleEntity;
} else {
throw new Error(`重新生成角色失败: ${response.message}`);
}
}
/**
*
* @param shotIds ID列表
* @returns
*/
async applyRole(shotIds: string[]) {
const roleId = this.roleItem.entity.id;
return await applyRoleToShots({
roleId,
shotIds,
});
}
}

View File

@ -0,0 +1,52 @@
import { SceneEntity } from '../domain/Entities';
import { SceneItem, TagItem, TextItem } from '../domain/Item';
import { regenerateScene, applySceneToShots } from '@/api/video_flow';
/**
*
*
*/
export class SceneEditUseCase {
constructor(private sceneItem: SceneItem) {
}
/**
* @description:
* @param {TextItem} prompt
* @param {TagItem[]} tags
* @return {*}
*/
async AIgenerateScene(prompt: TextItem, tags: TagItem[]): Promise<SceneEntity> {
const promptText = prompt.entity.content;
const tagList = tags.map((tag) => tag.entity.content);
// 调用重新生成场景接口
const response = await regenerateScene({
sceneId: this.sceneItem.entity.id || '',
prompt: promptText,
tagTypes: tagList,
});
if (response.successful) {
const sceneEntity = response.data;
this.sceneItem.setEntity(sceneEntity);
return sceneEntity;
} else {
throw new Error(`重新生成场景失败: ${response.message}`);
}
}
/**
*
* @param shotIds ID列表
* @returns
*/
async applyScene(shotIds: string[]) {
const sceneId = this.sceneItem.entity.id;
return await applySceneToShots({
sceneId,
shotIds
});
}
}

View File

@ -0,0 +1,40 @@
import { TagItem } from '../domain/Item';
import { updateTag } from '@/api/video_flow';
/**
*
*
*/
export class TagEditUseCase {
constructor(private readonly tagItem: TagItem) {
this.tagItem = tagItem;
}
/**
*
* @param newContent
* @returns
*/
async updateTag(newContent: string|number): Promise<TagItem> {
if (!this.tagItem) {
throw new Error('标签项未初始化');
}
if (this.tagItem.entity.disableEdit) {
throw new Error('标签项已禁用编辑');
}
// 请求更新接口
const response = await updateTag({
tagId: this.tagItem.entity.id,
content: newContent
});
if (response.successful) {
this.tagItem.setEntity(response.data);
return this.tagItem;
} else {
throw new Error(`修改标签失败: ${response.message}`);
}
}
}

View File

@ -0,0 +1,54 @@
import { TextItem } from '../domain/Item';
import { updateText } from '@/api/video_flow';
/**
*
*
*/
export class TextEditUseCase {
constructor(private readonly textItem: TextItem) {
this.textItem = textItem;
}
/**
*
* @param newContent
* @returns
*/
async updateText(newContent: string): Promise<TextItem> {
if (!this.textItem) {
throw new Error('文本项未初始化,请先调用 initializeText');
}
if (this.textItem.entity.disableEdit) {
throw new Error('文本项已禁用编辑');
}
const response = await updateText({
textId: this.textItem.entity.id,
content: newContent
});
if (response.successful) {
this.textItem.setEntity(response.data);
return this.textItem;
} else {
throw new Error(`修改文案失败: ${response.message}`);
}
}
/**
*
* @param optimizationOptions
* @returns
*/
async getOptimizedContent(
): Promise<string> {
if (!this.textItem) {
throw new Error('没有内容可优化');
}
return this.textItem.entity.content;
}
}

View File

@ -72,20 +72,20 @@ export function CreateToVideo2() {
const getEpisodeList = async (userId: number) => { const getEpisodeList = async (userId: number) => {
if (isLoading || isLoadingMore) return; if (isLoading || isLoadingMore) return;
console.log('getEpisodeList', userId); console.log('getEpisodeList', userId);
setIsLoading(true); setIsLoading(true);
try { try {
const params = { const params = {
user_id: String(userId), user_id: String(userId),
}; };
const episodeListResponse = await getScriptEpisodeListNew(params); const episodeListResponse = await getScriptEpisodeListNew(params);
console.log('episodeListResponse', episodeListResponse); console.log('episodeListResponse', episodeListResponse);
if (episodeListResponse.code === 0) { if (episodeListResponse.code === 0) {
setEpisodeList(episodeListResponse.data.movie_projects); setEpisodeList(episodeListResponse.data.movie_projects);
// 每一项 有 // 每一项 有
// final_video_url: "", // 生成的视频地址 // final_video_url: "", // 生成的视频地址
// last_message: "", // last_message: "",
// name: "After the Flood", // 剧集名称 // name: "After the Flood", // 剧集名称
@ -93,7 +93,7 @@ export function CreateToVideo2() {
// status: "INIT", // 剧集状态 INIT 初始化 // status: "INIT", // 剧集状态 INIT 初始化
// step: "INIT" // 剧集步骤 INIT 初始化 // step: "INIT" // 剧集步骤 INIT 初始化
} }
} catch (error) { } catch (error) {
console.error('Failed to fetch episode list:', error); console.error('Failed to fetch episode list:', error);
} finally { } finally {
@ -266,7 +266,7 @@ export function CreateToVideo2() {
const textNode = Array.from(editorRef.current.childNodes).find( const textNode = Array.from(editorRef.current.childNodes).find(
node => node.nodeType === Node.TEXT_NODE node => node.nodeType === Node.TEXT_NODE
); );
if (!textNode) { if (!textNode) {
const newTextNode = document.createTextNode(script || ''); const newTextNode = document.createTextNode(script || '');
editorRef.current.appendChild(newTextNode); editorRef.current.appendChild(newTextNode);
@ -284,7 +284,7 @@ export function CreateToVideo2() {
const handleEditorChange = (e: React.FormEvent<HTMLDivElement>) => { const handleEditorChange = (e: React.FormEvent<HTMLDivElement>) => {
const newText = e.currentTarget.textContent || ''; const newText = e.currentTarget.textContent || '';
// 如果正在输入中文,只更新内部文本,不更新状态 // 如果正在输入中文,只更新内部文本,不更新状态
if (isComposing) { if (isComposing) {
return; return;
@ -292,13 +292,13 @@ export function CreateToVideo2() {
// 更新状态 // 更新状态
setInputText(newText); setInputText(newText);
// 保存当前选区位置 // 保存当前选区位置
const selection = window.getSelection(); const selection = window.getSelection();
if (selection && selection.rangeCount > 0) { if (selection && selection.rangeCount > 0) {
const range = selection.getRangeAt(0); const range = selection.getRangeAt(0);
const currentPosition = range.startOffset; const currentPosition = range.startOffset;
// 使用 requestAnimationFrame 确保在下一帧恢复光标位置 // 使用 requestAnimationFrame 确保在下一帧恢复光标位置
requestAnimationFrame(() => { requestAnimationFrame(() => {
if (editorRef.current) { if (editorRef.current) {
@ -306,20 +306,20 @@ export function CreateToVideo2() {
let textNode = Array.from(editorRef.current.childNodes).find( let textNode = Array.from(editorRef.current.childNodes).find(
node => node.nodeType === Node.TEXT_NODE node => node.nodeType === Node.TEXT_NODE
) as Text; ) as Text;
if (!textNode) { if (!textNode) {
textNode = document.createTextNode(newText); textNode = document.createTextNode(newText);
editorRef.current.appendChild(textNode); editorRef.current.appendChild(textNode);
} }
// 计算正确的光标位置 // 计算正确的光标位置
const finalPosition = Math.min(currentPosition, textNode.length); const finalPosition = Math.min(currentPosition, textNode.length);
// 设置新的选区 // 设置新的选区
const newRange = document.createRange(); const newRange = document.createRange();
newRange.setStart(textNode, finalPosition); newRange.setStart(textNode, finalPosition);
newRange.setEnd(textNode, finalPosition); newRange.setEnd(textNode, finalPosition);
selection.removeAllRanges(); selection.removeAllRanges();
selection.addRange(newRange); selection.addRange(newRange);
} }
@ -335,7 +335,7 @@ export function CreateToVideo2() {
// 处理中文输入结束 // 处理中文输入结束
const handleCompositionEnd = (e: React.CompositionEvent<HTMLDivElement>) => { const handleCompositionEnd = (e: React.CompositionEvent<HTMLDivElement>) => {
setIsComposing(false); setIsComposing(false);
// 在输入完成后更新内容 // 在输入完成后更新内容
const newText = e.currentTarget.textContent || ''; const newText = e.currentTarget.textContent || '';
setInputText(newText); setInputText(newText);
@ -345,26 +345,26 @@ export function CreateToVideo2() {
if (selection && selection.rangeCount > 0) { if (selection && selection.rangeCount > 0) {
const range = selection.getRangeAt(0); const range = selection.getRangeAt(0);
const currentPosition = range.startOffset; const currentPosition = range.startOffset;
requestAnimationFrame(() => { requestAnimationFrame(() => {
if (editorRef.current) { if (editorRef.current) {
let textNode = Array.from(editorRef.current.childNodes).find( let textNode = Array.from(editorRef.current.childNodes).find(
node => node.nodeType === Node.TEXT_NODE node => node.nodeType === Node.TEXT_NODE
) as Text; ) as Text;
if (!textNode) { if (!textNode) {
textNode = document.createTextNode(newText); textNode = document.createTextNode(newText);
editorRef.current.appendChild(textNode); editorRef.current.appendChild(textNode);
} }
// 计算正确的光标位置 // 计算正确的光标位置
const finalPosition = Math.min(currentPosition, textNode.length); const finalPosition = Math.min(currentPosition, textNode.length);
// 设置新的选区 // 设置新的选区
const newRange = document.createRange(); const newRange = document.createRange();
newRange.setStart(textNode, finalPosition); newRange.setStart(textNode, finalPosition);
newRange.setEnd(textNode, finalPosition); newRange.setEnd(textNode, finalPosition);
selection.removeAllRanges(); selection.removeAllRanges();
selection.addRange(newRange); selection.addRange(newRange);
} }
@ -410,14 +410,14 @@ export function CreateToVideo2() {
<Video className="w-12 h-12 text-white/30" /> <Video className="w-12 h-12 text-white/30" />
</div> </div>
)} )}
{/* 播放按钮覆盖 */} {/* 播放按钮覆盖 */}
<div className="absolute inset-0 bg-black/20 flex items-center justify-center opacity-0 group-hover:opacity-100 transition-opacity duration-300"> <div className="absolute inset-0 bg-black/20 flex items-center justify-center opacity-0 group-hover:opacity-100 transition-opacity duration-300">
<div className="w-12 h-12 bg-white/20 backdrop-blur-sm rounded-full flex items-center justify-center"> <div className="w-12 h-12 bg-white/20 backdrop-blur-sm rounded-full flex items-center justify-center">
<Play className="w-6 h-6 text-white ml-1" /> <Play className="w-6 h-6 text-white ml-1" />
</div> </div>
</div> </div>
{/* 状态标签 */} {/* 状态标签 */}
<div className="absolute top-3 left-3"> <div className="absolute top-3 left-3">
<span className={`px-2 py-1 rounded-full text-xs font-medium ${ <span className={`px-2 py-1 rounded-full text-xs font-medium ${
@ -428,7 +428,7 @@ export function CreateToVideo2() {
{episode.status === 'COMPLETED' ? 'finished' : 'processing'} {episode.status === 'COMPLETED' ? 'finished' : 'processing'}
</span> </span>
</div> </div>
{/* 时长标签 */} {/* 时长标签 */}
{episode.duration && ( {episode.duration && (
<div className="absolute bottom-3 right-3"> <div className="absolute bottom-3 right-3">
@ -438,22 +438,22 @@ export function CreateToVideo2() {
</div> </div>
)} )}
</div> </div>
{/* 内容区域 */} {/* 内容区域 */}
<div className="p-4"> <div className="p-4">
<h3 className="text-white font-medium text-sm mb-2 line-clamp-2 group-hover:text-blue-300 transition-colors"> <h3 className="text-white font-medium text-sm mb-2 line-clamp-2 group-hover:text-blue-300 transition-colors">
{episode.name || episode.title || 'Unnamed episode'} {episode.name || episode.title || 'Unnamed episode'}
</h3> </h3>
{/* 元数据 */} {/* 元数据 */}
<div className="flex items-center justify-between text-xs text-white/40"> <div className="flex items-center justify-between text-xs text-white/40">
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<Calendar className="w-3 h-3" /> <Calendar className="w-3 h-3" />
<span>{new Date(episode.created_at).toLocaleDateString()}</span> <span>{new Date(episode.created_at).toLocaleDateString()}</span>
</div> </div>
<div className="flex items-center gap-3"> <div className="flex items-center gap-3">
<div className="flex items-center gap-1"> <div className="flex items-center gap-1">
<Clock className="w-3 h-3" /> <Clock className="w-3 h-3" />
<span>{new Date(episode.updated_at).toLocaleTimeString([], {hour: '2-digit', minute:'2-digit'})}</span> <span>{new Date(episode.updated_at).toLocaleTimeString([], {hour: '2-digit', minute:'2-digit'})}</span>
@ -461,7 +461,7 @@ export function CreateToVideo2() {
</div> </div>
</div> </div>
</div> </div>
{/* 操作按钮 */} {/* 操作按钮 */}
<div className="absolute top-3 right-3 opacity-0 group-hover:opacity-100 transition-opacity duration-300"> <div className="absolute top-3 right-3 opacity-0 group-hover:opacity-100 transition-opacity duration-300">
<div className="flex gap-2"> <div className="flex gap-2">
@ -479,7 +479,7 @@ export function CreateToVideo2() {
<div className="container mx-auto h-[calc(100vh-6rem)] flex flex-col absolute top-[4rem] left-0 right-0 px-[1rem]"> <div className="container mx-auto h-[calc(100vh-6rem)] flex flex-col absolute top-[4rem] left-0 right-0 px-[1rem]">
{/* 优化后的主要内容区域 */} {/* 优化后的主要内容区域 */}
<div className="flex-1 min-h-0"> <div className="flex-1 min-h-0">
<div <div
ref={scrollContainerRef} ref={scrollContainerRef}
className="h-full overflow-y-auto custom-scrollbar" className="h-full overflow-y-auto custom-scrollbar"
style={{ style={{
@ -491,8 +491,8 @@ export function CreateToVideo2() {
/* 优化的加载状态 */ /* 优化的加载状态 */
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 2xl:grid-cols-5 gap-6 pb-6"> <div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 2xl:grid-cols-5 gap-6 pb-6">
{[...Array(10)].map((_, index) => ( {[...Array(10)].map((_, index) => (
<div <div
key={index} key={index}
className="bg-white/[0.05] backdrop-blur-[20px] border border-white/[0.08] rounded-2xl overflow-hidden animate-pulse" className="bg-white/[0.05] backdrop-blur-[20px] border border-white/[0.08] rounded-2xl overflow-hidden animate-pulse"
> >
<div className="h-[200px] bg-gradient-to-br from-white/[0.08] to-white/[0.04]"> <div className="h-[200px] bg-gradient-to-br from-white/[0.08] to-white/[0.04]">
@ -515,7 +515,7 @@ export function CreateToVideo2() {
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 2xl:grid-cols-5 gap-6"> <div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 2xl:grid-cols-5 gap-6">
{episodeList.map(renderEpisodeCard)} {episodeList.map(renderEpisodeCard)}
</div> </div>
{/* 加载更多指示器 */} {/* 加载更多指示器 */}
{isLoadingMore && ( {isLoadingMore && (
<div className="flex justify-center py-12"> <div className="flex justify-center py-12">
@ -525,7 +525,7 @@ export function CreateToVideo2() {
</div> </div>
</div> </div>
)} )}
{/* 到底提示 */} {/* 到底提示 */}
{!hasMore && episodeList.length > 0 && ( {!hasMore && episodeList.length > 0 && (
<div className="flex justify-center py-12"> <div className="flex justify-center py-12">
@ -707,4 +707,4 @@ export function CreateToVideo2() {
)} )}
</> </>
); );
} }

4014
package-lock.json generated

File diff suppressed because it is too large Load Diff