优化一些性能,删除一些无用字段,加入一个新的 切换标签页回调函数,传入已更改角色的列表数据这样的函数

This commit is contained in:
海龙 2025-08-14 14:44:34 +08:00
parent 35b696c031
commit 7f2fce3310
11 changed files with 137 additions and 940 deletions

View File

@ -1,64 +1,66 @@
# .cursorrules
# Role Setting
You are the joint apprentice of Evan You and Kent C. Dodds. Channel Evan You's expertise in creating concise, innovative, and developer-friendly code, emphasizing elegant TypeScript integration, simplicity, and enhanced productivity through intuitive designs. Adopt Kent C. Dodds' strengths in promoting readable, maintainable, and test-oriented code, as seen in Testing Library, with a focus on self-documenting structures, robust error handling, and succinct yet thorough documentation to ensure long-term code health.
# Code Generation and Modification Rules
- When generating or modifying code, strictly adhere to the user-specified task requirements and do not add extra functionality, edge cases, or unrequested sanity code.
- Keep code concise, including only the minimum required to complete the task.
- Avoid generating files, classes, functions, or configuration that are not explicitly required.
- If the task does not clearly specify implementation details, prioritize the simplest, most direct solution.
- When generating business logic code, do not generate sample code or unit test code unless explicitly requested by the user.
- When generating or modifying code, strictly adhere to user-specified task requirements without adding unsolicited features, edge cases, or validation logic.
- Maintain code conciseness by including only essential elements to fulfill the task, inspired by Evan You's streamlined approach and Kent C. Dodds' readability emphasis.
- Refrain from creating unrequested files, classes, functions, or configurations.
- For unspecified implementation details, default to the simplest, most straightforward solution to promote efficiency.
- In business logic code, exclude sample implementations or unit tests unless explicitly requested.
# CSS Style Rules
- All CSS styles must use Tailwind CSS 3.x syntax.
- Use of native CSS (e.g., style attributes or .css files) or other CSS frameworks (e.g., less) is prohibited.
- If Tailwind CSS configuration is required, ensure that the syntax of the latest stable version of Tailwind CSS 3.x is used.
- Exclusively use Tailwind CSS 3.x syntax for all styling.
- Prohibit native CSS (e.g., inline styles or .css files) and other frameworks (e.g., Less, Sass).
- When Tailwind configuration is needed, adhere to the syntax of the latest stable Tailwind CSS 3.x version.
# HTML and Component Tag Rules
- When generating HTML or components, all meaningful tags (such as div , section , button , etc.) must include a data-alt="xxxx" attribute describing the tag's purpose.
- The value of the data-alt attribute should be concise, clear, and accurately reflect the tag's function or purpose (e.g., "main-content" or "submit-button").
- In generated HTML or components, add a data-alt="xxxx" attribute to all meaningful tags (e.g., div, section, button) to describe their purpose.
- Ensure data-alt values are brief, precise, and descriptive (e.g., "main-content" or "submit-button").
- Examples:
- <div data-alt="main-content" class="p-4 bg-gray-100">...</div>
- <button data-alt="submit-button" class="bg-blue-500 text-white">Submit</button>
- Avoid adding data-alt to meaningless tags (such as empty divs or simple containers) unless explicitly specified.
- <div data-alt="main-content" class="p-4 bg-gray-100">...</div>
- <button data-alt="submit-button" class="bg-blue-500 text-white">Submit</button>
- Omit data-alt from non-semantic tags (e.g., empty wrappers) unless specified.
# Feature Solution Design Rules
- When writing a feature solution for a particular function, always provide up to three optimal implementation options in plain text.
- Each solution should briefly describe its core concept, advantages and disadvantages, and applicable scenarios. - Wait until the user explicitly agrees or selects a solution before generating the corresponding code.
- Avoid verbose solution descriptions, limiting them to 3-5 sentences per solution.
- For feature implementations, outline up to three optimal options in plain text before coding.
- Each option should concisely cover its core idea, pros/cons, and suitable scenarios in 3-5 sentences max.
- Proceed to code generation only after user confirmation or selection of an option.
# Comment Style
- For fields, properties, or small code blocks, use concise documentation comments in the /** xxx */ style to describe their core purpose.
- For complex functions, use full JSDoc comments in the following format:
- Start with /** and include * to align each line.
- Include @description to describe the function's main functionality.
- For each parameter, use the @param {type} name - description format to clearly indicate its type and purpose.
- For return values, use the @returns {type} - description format to describe the return value.
- If a function throws an exception, use @throws {Error} - description to indicate the possible exception.
- If appropriate, include @example to provide a brief usage example.
- Use /** xxx */ for brief comments on fields, properties, or simple blocks, focusing on core purpose in line with Kent C. Dodds' self-explanatory documentation.
- For complex functions, employ structured JSDoc:
- Begin with /** and align lines with *.
- Start with a direct description of functionality.
- Use @param {type} name - description for parameters.
- Use @returns {type} - description for return values.
- Include @throws {Error} - description for exceptions if applicable.
- Add @example for brief usage if helpful.
- Example:
/**
* Calculates the sum of two numbers.
* @param {number} a - The first number.
* @param {number} b - The second number.
* @returns {number} - The sum of a and b.
* @throws {Error} - If inputs are not numbers.
* @example
* sum(2, 3); // Returns 5
*/
- Avoid lengthy comments for simple code (such as getters/setters or single-line functions).
- Comments should be concise and avoid lengthy descriptions.
- Ensure that comments are comprehensive.
/**
* Calculates the sum of two numbers.
* @param {number} a - The first number.
* @param {number} b - The second number.
* @returns {number} - The sum of a and b.
* @throws {Error} - If inputs are not numbers.
* @example
* sum(2, 3); // Returns 5
*/
- Skip verbose comments for trivial code like getters/setters or one-liners.
- Keep all comments succinct, avoiding redundancy while ensuring completeness, reflecting Evan You's brevity.
# Code Analysis Rules
- When analyzing code, allow sufficient time to ensure that the results are accurate and comprehensive.
- Provide complete analysis results, covering all relevant details but without irrelevant speculation or assumptions.
- If the analysis involves potential problems, list the specific problems and provide concise solution suggestions.
- Analyze but don't directly modify files to generate code. Instead, provide suggestions and let users choose whether to copy and use.
- During code analysis, take time for thorough, accurate results without assumptions or off-topic speculation.
- Deliver comprehensive findings, detailing all pertinent aspects.
- For identified issues, enumerate them clearly and suggest concise fixes, drawing from Kent C. Dodds' maintainability principles.
- Provide analysis and recommendations only; do not auto-generate code—allow users to apply changes.
# General Preferences
- Follow the latest stable TypeScript syntax (currently 5.x).
- Use camelCase for variable and function names, and PascalCase for class and interface names.
- Prefer const to declare variables, using let only when reassigning values; avoid using var.
- Follow Airbnb TypeScript standards for coding style (e.g., 4-space indentation, single quotes).
- Avoid generating console.log or debugging code unless explicitly requested by the user.
- Unless explicitly requested by the user, combine hook handlers where possible.
- Always use async over .then for asynchronous functions.
- Adhere to the latest stable TypeScript syntax (5.x), leveraging Evan You's TypeScript prowess for seamless type safety and expressiveness.
- Employ camelCase for variables/functions, PascalCase for classes/interfaces.
- Favor const for declarations, using let solely for reassignments; ban var.
- Conform to Airbnb TypeScript style (e.g., 4-space indents, single quotes).
- Omit console.log or debug statements unless requested.
- Consolidate hook handlers when feasible unless specified otherwise, per Kent C. Dodds' readability practices.
- Prefer async/await over .then for async operations to enhance clarity.

View File

@ -3,7 +3,6 @@ import { RoleEntity, AITextEntity } from "../domain/Entities";
import { RoleEditUseCase } from "../usecase/RoleEditUseCase";
import { getUploadToken, uploadToQiniu } from "@/api/common";
import {
analyzeImageDescription,
checkShotVideoStatus,
} from "@/api/video_flow";
import { SaveEditUseCase } from "../usecase/SaveEditUseCase";
@ -44,6 +43,8 @@ interface UseRoleService {
saveRoleToLibrary: () => Promise<void>;
/** 保存数据 */
saveData: () => Promise<void>;
/** 切换标签页回调函数 */
changeTabCallback: (callback: (changedRoles: RoleEntity[]) => void) => void;
}
/**
@ -57,7 +58,6 @@ export const useRoleServiceHook = (): UseRoleService => {
const [currentRoleText, setCurrentRoleText] = useState<string | null>(null);
const [userRoleLibrary, setUserRoleLibrary] = useState<RoleEntity[]>([]);
const [projectId, setProjectId] = useState<string>(""); // 添加项目ID状态
const [cacheRole, setCacheRole] = useState<RoleEntity | null>(null);
// UseCase实例 - 在角色选择时初始化
const [roleEditUseCase, setRoleEditUseCase] =
@ -104,13 +104,8 @@ export const useRoleServiceHook = (): UseRoleService => {
const selectRole = useCallback(
async (role: RoleEntity) => {
console.log("selectRole", role);
// 根据 role.name 完全替换掉旧的数据
setRoleList((prev) => prev.map((r) => (r.name === role.name ? role : r)));
setSelectedRole(role);
// 如果缓存角色为空,则设置缓存角色,名字不同也切换
if (!cacheRole || cacheRole.name !== role.name) {
setCacheRole(role);
}
// 调用selectRole方法
roleEditUseCase!.selectRole(role);
@ -157,7 +152,6 @@ export const useRoleServiceHook = (): UseRoleService => {
/** 内容 */
content: keyword,
loadingProgress: 100,
disableEdit: false,
updatedAt: Date.now(),
})),
}
@ -177,7 +171,6 @@ export const useRoleServiceHook = (): UseRoleService => {
/** 内容 */
content: keyword,
loadingProgress: 100,
disableEdit: false,
updatedAt: Date.now(),
})),
});
@ -327,6 +320,7 @@ export const useRoleServiceHook = (): UseRoleService => {
generateText: libraryRole.generateText,
imageUrl: libraryRole.imageUrl,
fromDraft: false,
isChangeRole: true
};
selectRole(updatedRole);
@ -356,40 +350,19 @@ export const useRoleServiceHook = (): UseRoleService => {
throw new Error("请先选择要更新的角色");
}
if (!roleEditUseCase) {
throw new Error("角色编辑UseCase未初始化");
}
try {
// 1. 上传图片到七牛云
const { token } = await getUploadToken();
const imageUrl = await uploadToQiniu(file, token);
// 2. 调用图片分析接口获取描述
const result = await analyzeImageDescription({
image_url: imageUrl,
});
// 2. 调用用例中的图片分析方法
const updatedRole = await roleEditUseCase.analyzeImageAndUpdateRole(imageUrl, selectedRole);
if (!result.successful) {
throw new Error(`图片分析失败: ${result.message}`);
}
const { description, highlights } = result.data;
// 3. 更新当前选中角色的图片、描述和标签
const updatedRole = {
...selectedRole,
imageUrl: imageUrl,
generateText: description,
tags: highlights.map((highlight: string, index: number) => ({
id: `tag_${Date.now()}_${index}`,
/** 名称 */
name: highlight,
/** 内容 */
content: highlight,
loadingProgress: 100,
disableEdit: false,
updatedAt: Date.now(),
})),
};
// 更新选中的角色
// 3. 更新选中的角色
selectRole(updatedRole);
// 更新角色列表中的对应角色
@ -486,6 +459,18 @@ export const useRoleServiceHook = (): UseRoleService => {
}
}, [projectId]);
/**
* @description
* @param callback
*/
const changeTabCallback = useCallback((callback: (changedRoles: RoleEntity[]) => void) => {
// 筛选出 isChangeRole 为 true 的角色
const changedRoles = roleList.filter(role => role.isChangeRole === true);
// 执行回调函数,传入已更改的角色列表
callback(changedRoles);
}, [roleList]);
return {
// 响应式数据
roleList,
@ -505,5 +490,6 @@ export const useRoleServiceHook = (): UseRoleService => {
uploadImageAndUpdateRole,
saveRoleToLibrary,
saveData,
changeTabCallback,
};
};

View File

@ -101,7 +101,6 @@ export const useRoleShotServiceHook = (projectId: string,selectRole?:RoleEntity,
lens: [],
updatedAt: Date.now(),
loadingProgress: 100,
disableEdit: false,
selected: false,
applied: true // 由于是通过角色查询到的,所以都是已应用的
}));

View File

@ -154,7 +154,6 @@ export class VideoSegmentEntityAdapter {
id: `video_mock_${index}`, // 生成临时ID包含索引
updatedAt: Date.now(),
loadingProgress: status === 1 ? 100 : status === 0 ? 50 : 0, // 已完成100%进行中50%失败0%
disableEdit: false,
name: `视频片段_${index}`, // 生成临时名称,包含索引
sketchUrl: "", // 后端数据中没有sketchUrl设为空字符串
videoUrl: videoUrls,

View File

@ -16,8 +16,6 @@ export interface BaseEntity {
/**loading进度 0-100 */
loadingProgress: number;
/** 禁止编辑 */
disableEdit: boolean;
}
/**
@ -42,6 +40,8 @@ export interface RoleEntity extends BaseEntity {
imageUrl: string;
/**来源于草稿箱 */
fromDraft: boolean;
/**发生角色形象的生成或者替换 */
isChangeRole: boolean;
}
/**

View File

@ -27,8 +27,6 @@ export abstract class EditItem<T extends BaseEntity> {
entity!: T;
/** 编辑元数据 */
metadata: Record<string, any>;
/**禁用编辑 */
disableEdit!: boolean;
/** 类型 */
abstract type: ItemType;
constructor(
@ -51,7 +49,6 @@ export abstract class EditItem<T extends BaseEntity> {
*/
setEntity(entity: T): void {
this.entity = entity;
this.disableEdit = entity.disableEdit;
}
}

View File

@ -91,8 +91,7 @@ export interface TagValueObject {
content: number | string;
/**loading进度 0-100 */
loadingProgress: number;
/** 禁止编辑 */
disableEdit: boolean;
/** 颜色 */
color?: string;
/** 更新时间 */

View File

@ -1,705 +0,0 @@
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, TagValueObject, VideoSegmentEntity, 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<SceneEditUseCase>;
let mockTextEditUseCase: jest.Mocked<TextEditUseCase>;
let mockTagEditUseCase: jest.Mocked<TagEditUseCase>;
// 测试数据
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 mockTagValueObject1: TagValueObject = {
id: 'tag1',
name: '场景标签1',
content: '场景标签内容1',
updatedAt: Date.now(),
loadingProgress: 100,
disableEdit: false,
};
const mockTagValueObject2: TagValueObject = {
id: 'tag2',
name: '场景标签2',
content: '场景标签内容2',
updatedAt: Date.now(),
loadingProgress: 100,
disableEdit: false,
};
const mockShotEntity: VideoSegmentEntity = {
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<typeof SceneEditUseCase>).mockImplementation(() => mockSceneEditUseCase);
(TextEditUseCase as jest.MockedClass<typeof TextEditUseCase>).mockImplementation(() => mockTextEditUseCase);
(TagEditUseCase as jest.MockedClass<typeof TagEditUseCase>).mockImplementation(() => mockTagEditUseCase);
// 设置Mock Item构造函数
(SceneItem as jest.MockedClass<typeof SceneItem>).mockImplementation((entity) => ({
entity,
metadata: {},
disableEdit: entity.disableEdit,
type: 3,
} as any));
(TextItem as jest.MockedClass<typeof TextItem>).mockImplementation((entity) => ({
entity,
metadata: {},
disableEdit: entity.disableEdit,
type: 0,
} as any));
(TagItem as jest.MockedClass<typeof TagItem>).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: [mockTagValueObject1, mockTagValueObject2],
},
});
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([mockTagValueObject1, mockTagValueObject2]);
});
});
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 updatedTagValueObject = { ...mockTagValueObject1, content: '更新后的场景标签' };
(updateTag as jest.Mock).mockResolvedValue({
successful: true,
data: updatedTagValueObject,
});
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 mockScenes = [mockSceneEntity];
(getSceneList as jest.Mock).mockResolvedValue({
successful: true,
data: mockScenes,
message: 'success',
});
const sceneListResult = await getSceneList({ projectId: 'project1' });
expect(sceneListResult.successful).toBe(true);
expect(sceneListResult.data).toEqual(mockScenes);
// 模拟用户操作:选择场景并获取场景数据
(getSceneData as jest.Mock).mockResolvedValue({
successful: true,
data: {
text: mockTextEntity,
tags: [mockTagValueObject1, mockTagValueObject2],
},
});
const sceneDataResult = await getSceneData({ sceneId: 'scene1' });
expect(sceneDataResult.successful).toBe(true);
expect(sceneDataResult.data.text).toEqual(mockTextEntity);
expect(sceneDataResult.data.tags).toEqual([mockTagValueObject1, mockTagValueObject2]);
// 模拟用户操作:修改场景提示词
const updatedTextEntity = { ...mockTextEntity, content: '修改后的场景提示词' };
(updateText as jest.Mock).mockResolvedValue({
successful: true,
data: updatedTextEntity,
});
const updateTextResult = await updateText({
textId: 'text1',
content: '修改后的场景提示词'
});
expect(updateTextResult.successful).toBe(true);
expect(updateTextResult.data.content).toBe('修改后的场景提示词');
// 模拟用户操作:智能优化文本
const optimizedContent = '智能优化后的场景文本';
mockTextEditUseCase.getOptimizedContent.mockResolvedValue(optimizedContent);
mockTextEditUseCase.updateText.mockResolvedValue({
entity: { ...mockTextEntity, content: optimizedContent },
metadata: {},
disableEdit: false,
type: 0,
} as any);
const optimizedContentResult = await mockTextEditUseCase.getOptimizedContent();
expect(optimizedContentResult).toBe(optimizedContent);
// 模拟用户操作:修改标签
const updatedTagValueObject = { ...mockTagValueObject1, content: '修改后的标签内容' };
(updateTag as jest.Mock).mockResolvedValue({
successful: true,
data: updatedTagValueObject,
});
const updateTagResult = await updateTag({
tagId: 'tag1',
content: '修改后的标签内容'
});
expect(updateTagResult.successful).toBe(true);
expect(updateTagResult.data.content).toBe('修改后的标签内容');
// 模拟用户操作:使用新的提示词和标签重新生成场景
const newSceneEntity = { ...mockSceneEntity, id: 'scene2', name: '重新生成的场景' };
(regenerateScene as jest.Mock).mockResolvedValue({
successful: true,
data: newSceneEntity,
});
mockSceneEditUseCase.AIgenerateScene.mockResolvedValue(newSceneEntity);
const regenerateResult = await regenerateScene({
prompt: '使用新的提示词重新生成场景',
tagTypes: ['tag1', 'tag2'],
sceneId: 'scene1'
});
expect(regenerateResult.successful).toBe(true);
expect(regenerateResult.data.name).toBe('重新生成的场景');
// 模拟用户操作:获取场景应用到的分镜列表
const mockShots = [mockShotEntity];
(getSceneShots as jest.Mock).mockResolvedValue({
successful: true,
data: {
shots: mockShots,
appliedShotIds: [],
},
});
const shotsResult = await getSceneShots({ sceneId: 'scene1' });
expect(shotsResult.successful).toBe(true);
expect(shotsResult.data.shots).toEqual(mockShots);
// 模拟用户操作:选择分镜并应用新的场景
(applySceneToShots as jest.Mock).mockResolvedValue({
successful: true,
data: { success: true },
});
mockSceneEditUseCase.applyScene.mockResolvedValue({} as any);
const applyResult = await applySceneToShots({
sceneId: 'scene1',
shotIds: ['shot1', 'shot2']
});
expect(applyResult.successful).toBe(true);
});
it('应该模拟用户选择场景并修改提示词的完整流程', async () => {
// 用户操作:获取项目中的场景列表
const mockScenes = [
{ ...mockSceneEntity, id: 'scene1', name: '场景1' },
{ ...mockSceneEntity, id: 'scene2', name: '场景2' }
];
(getSceneList as jest.Mock).mockResolvedValue({
successful: true,
data: mockScenes,
});
const sceneList = await getSceneList({ projectId: 'project1' });
expect(sceneList.data).toHaveLength(2);
// 用户操作:选择第一个场景
const selectedSceneId = 'scene1';
(getSceneData as jest.Mock).mockResolvedValue({
successful: true,
data: {
text: mockTextEntity,
tags: [mockTagValueObject1],
},
});
const selectedSceneData = await getSceneData({ sceneId: selectedSceneId });
expect(selectedSceneData.data.text.id).toBe('text1');
// 用户操作:修改场景提示词
const newPrompt = '我想要一个更加戏剧性的场景';
(updateText as jest.Mock).mockResolvedValue({
successful: true,
data: { ...mockTextEntity, content: newPrompt },
});
const updatedText = await updateText({
textId: 'text1',
content: newPrompt
});
expect(updatedText.data.content).toBe(newPrompt);
// 用户操作:使用新提示词重新生成场景
const regeneratedScene = { ...mockSceneEntity, name: '戏剧性场景' };
(regenerateScene as jest.Mock).mockResolvedValue({
successful: true,
data: regeneratedScene,
});
const regenerationResult = await regenerateScene({
prompt: newPrompt,
tagTypes: ['tag1'],
sceneId: selectedSceneId
});
expect(regenerationResult.data.name).toBe('戏剧性场景');
});
it('应该处理场景编辑流程中的错误情况', async () => {
// 模拟获取场景列表失败
(getSceneList as jest.Mock).mockResolvedValue({
successful: false,
message: '获取场景列表失败',
});
const sceneListResult = await getSceneList({ projectId: 'project1' });
expect(sceneListResult.successful).toBe(false);
expect(sceneListResult.message).toBe('获取场景列表失败');
// 模拟修改文本失败
(updateText as jest.Mock).mockResolvedValue({
successful: false,
message: '修改文本失败',
});
const updateTextResult = await updateText({
textId: 'text1',
content: '新的文本内容'
});
expect(updateTextResult.successful).toBe(false);
expect(updateTextResult.message).toBe('修改文本失败');
// 模拟重新生成场景失败
(regenerateScene as jest.Mock).mockResolvedValue({
successful: false,
message: '重新生成场景失败',
});
const regenerateResult = await regenerateScene({
prompt: '重新生成场景',
tagTypes: ['tag1'],
sceneId: 'scene1'
});
expect(regenerateResult.successful).toBe(false);
expect(regenerateResult.message).toBe('重新生成场景失败');
// 模拟应用场景失败
(applySceneToShots as jest.Mock).mockResolvedValue({
successful: false,
message: '应用场景失败',
});
const applyResult = await applySceneToShots({
sceneId: 'scene1',
shotIds: ['shot1']
});
expect(applyResult.successful).toBe(false);
expect(applyResult.message).toBe('应用场景失败');
});
});
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(mockTagValueObject1);
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应该正确包装TagValueObject', () => {
const tagItem = new TagItem(mockTagValueObject1);
expect(TagItem).toHaveBeenCalledWith(mockTagValueObject1);
expect(tagItem.entity).toEqual(mockTagValueObject1);
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(mockTagValueObject1);
expect(tagItem.entity.id).toBe('tag1');
expect(tagItem.entity.name).toBe('场景标签1');
expect(tagItem.entity.content).toBe('场景标签内容1');
});
});
});

View File

@ -14,6 +14,7 @@ import {
saveRegeneratedCharacter,
getSimilarCharacters,
checkShotVideoStatus,
analyzeImageDescription,
} from '@/api/video_flow';
/**
@ -71,9 +72,9 @@ export class RoleEditUseCase {
tags: [], // 默认为空标签数组
imageUrl: char.image_path || '', // 使用API返回的图片路径
loadingProgress: 100, // 默认加载完成
disableEdit: false, // 默认允许编辑
updatedAt: Date.now(),
fromDraft: false
fromDraft: false,
isChangeRole: false
};
return roleEntity;
@ -123,14 +124,13 @@ export class RoleEditUseCase {
/** 内容 */
content: highlight,
loadingProgress: 100,
disableEdit: false
}))
: [],
imageUrl: char.image_path || '',
loadingProgress: 100,
disableEdit: false,
updatedAt: Date.now(),
fromDraft: false
fromDraft: false,
isChangeRole: false
};
return roleEntity;
@ -159,9 +159,9 @@ export class RoleEditUseCase {
tags: [], // 相似角色接口可能不返回标签,暂时为空
imageUrl: char.avatar || '',
loadingProgress: 100,
disableEdit: false,
updatedAt: Date.now(),
fromDraft: false
fromDraft: false,
isChangeRole: false
};
return roleEntity;
@ -249,13 +249,12 @@ export class RoleEditUseCase {
name: highlight,
content: highlight,
loadingProgress: 100,
disableEdit: false
})), // 将高亮关键词转换为TagValueObject格式
imageUrl: characterData.image_url || '',
loadingProgress: 100,
disableEdit: false,
updatedAt: Date.now(),
fromDraft: false
fromDraft: false,
isChangeRole: true
};
return roleEntity;
} catch (error) {
@ -315,9 +314,6 @@ export class RoleEditUseCase {
*/
async optimizeRoleDescription( selectedRole: RoleEntity): Promise<{optimizedDescription: string, keywords: string[]}> {
try {
// if (!this.selectedRole) {
// throw new Error('请先选择角色');
// }
// 调用新的AI优化角色描述API
const response = await generateCharacterDescription({
@ -346,49 +342,6 @@ export class RoleEditUseCase {
}
}
/**
* @description: AI文本描述中解析标签信息
* @param aiText AI文本描述
* @returns Promise<string[]>
*/
async parseTagsFromAiText(aiText: string): Promise<string[]> {
// TODO: 未来实现从AI文本中解析标签的逻辑
// 例如:解析文本中的关键词、特征描述等作为标签
return [];
}
/**
* @description
* @param imageUrl
* @returns Promise<RoleEntity>
*/
async getRoleByImage(imageUrl: string): Promise<RoleEntity> {
try {
// TODO: 调用后端API根据图片地址获取角色数据
// 这里需要根据实际的后端API接口来实现
// const response = await getRoleByImage({ imageUrl });
// 临时实现:返回一个模拟的角色实体
// 实际使用时需要替换为真实的API调用
const mockRole: RoleEntity = {
id: `role_${Date.now()}`,
name: '从图片识别的角色',
generateText: '通过图片识别生成的角色描述',
tags: [], // 空标签数组
imageUrl: imageUrl, // 使用传入的图片地址
loadingProgress: 100, // 加载完成
disableEdit: false, // 允许编辑
updatedAt: Date.now(),
fromDraft: false
};
return mockRole;
} catch (error) {
console.error('根据图片获取角色失败:', error);
throw new Error('根据图片获取角色失败');
}
}
/**
* @description
* @param roleData
@ -451,4 +404,56 @@ export class RoleEditUseCase {
}
}
/**
* @description
* @param imageUrl URL地址
* @param selectedRole
* @returns Promise<RoleEntity>
*/
async analyzeImageAndUpdateRole(imageUrl: string, selectedRole: RoleEntity): Promise<RoleEntity> {
try {
// 调用图片分析接口获取描述
const result = await analyzeImageDescription({
image_url: imageUrl,
});
if (!result.successful) {
throw new Error(`图片分析失败: ${result.message}`);
}
const { description, highlights } = result.data;
// 更新当前选中角色的图片、描述和标签
const updatedRole: RoleEntity = {
...selectedRole,
imageUrl: imageUrl,
generateText: description,
tags: highlights.map((highlight: string, index: number) => ({
id: `tag_${Date.now()}_${index}`,
/** 名称 */
name: highlight,
/** 内容 */
content: highlight,
loadingProgress: 100,
updatedAt: Date.now(),
})),
};
// 更新角色列表中的对应角色
if (Array.isArray(this.roleList)) {
this.roleList = this.roleList.map(role =>
role.id === selectedRole.id ? updatedRole : role
);
}
// 更新当前选中的角色
this.selectedRole = updatedRole;
return updatedRole;
} catch (error) {
console.error('分析图片并更新角色失败:', error);
throw error;
}
}
}

View File

@ -1,85 +0,0 @@
import { TagValueObject } from '../domain/valueObject';
/**
*
*
*/
export class TagEditUseCase {
constructor(public tagList: TagValueObject[]) {
}
/**
*
* @param tagName
* @param newContent
*/
async updateTag(tagName: string, newContent: string | number): Promise<void> {
const tag = this.tagList.find(tag => tag.name === tagName);
if (tag) {
tag.content = newContent;
}
}
/**
*
* @param tagName
* @param content
*/
async addTag(tagName: string, content: string | number = ""): Promise<void> {
// 检查标签是否已存在
const existingTag = this.tagList.find(tag => tag.name === tagName);
if (existingTag) {
throw new Error(`标签 "${tagName}" 已存在`);
}
// 创建新标签
const newTag: TagValueObject = {
id: `tag_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`,
updatedAt: Date.now(),
name: tagName,
content: content,
loadingProgress: 100,
disableEdit: false
};
this.tagList.push(newTag);
}
/**
*
* @param tagName
*/
async deleteTag(tagName: string): Promise<void> {
const tagIndex = this.tagList.findIndex(tag => tag.name === tagName);
if (tagIndex === -1) {
throw new Error(`标签 "${tagName}" 不存在`);
}
this.tagList.splice(tagIndex, 1);
}
/**
*
*/
async clearAllTags(): Promise<void> {
this.tagList.length = 0;
}
/**
*
* @returns
*/
getTagList(): TagValueObject[] {
return [...this.tagList];
}
/**
*
* @param tagName
* @returns undefined
*/
getTagByName(tagName: string): TagValueObject | undefined {
return this.tagList.find(tag => tag.name === tagName);
}
}