调整项目库拆帧与长帧序列加载
- 删除项目库右上角独立新建项目入口,保留导入视频/DICOM 自动建项目流程 - 视频项目支持已生成帧后的重新生成帧入口,并提示会清空旧帧、标注和 mask - 后端重新拆帧任务开始前清理旧帧、旧标注和旧 mask,避免重复帧序列 - 项目帧列表接口默认返回完整帧序列,避免工作区总帧数被 1000 条默认 limit 截断 - 增加可选 docker-compose.gpu.yml,并补充 Docker 使用本机 GPU 的前提和启动说明 - 更新项目库、API 映射、恢复演示文案、后端媒体/项目测试和前端文档
This commit is contained in:
@@ -70,20 +70,11 @@ describe('ProjectLibrary', () => {
|
||||
expect(screen.queryByText('30FPS')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('creates a new project from the modal', async () => {
|
||||
apiMock.createProject.mockResolvedValueOnce({ id: 'p2', name: 'New Project', status: 'pending' });
|
||||
|
||||
it('does not expose manual project creation from the project library header', async () => {
|
||||
render(<ProjectLibrary onProjectSelect={vi.fn()} />);
|
||||
fireEvent.click(screen.getByText('新建项目'));
|
||||
fireEvent.change(screen.getByPlaceholderText('输入项目名称'), { target: { value: 'New Project' } });
|
||||
fireEvent.change(screen.getByPlaceholderText('输入项目描述'), { target: { value: 'desc' } });
|
||||
fireEvent.click(screen.getByRole('button', { name: '创建' }));
|
||||
|
||||
await waitFor(() => expect(apiMock.createProject).toHaveBeenCalledWith({
|
||||
name: 'New Project',
|
||||
description: 'desc',
|
||||
}));
|
||||
expect(useStore.getState().projects[0]).toEqual(expect.objectContaining({ id: 'p2' }));
|
||||
await waitFor(() => expect(apiMock.getProjects).toHaveBeenCalled());
|
||||
expect(screen.queryByRole('button', { name: '新建项目' })).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('imports video by creating a project and uploading media without parsing frames', async () => {
|
||||
@@ -156,6 +147,30 @@ describe('ProjectLibrary', () => {
|
||||
expect(await screen.findByRole('status')).toHaveTextContent('视频帧生成完成,项目封面已自动更新');
|
||||
});
|
||||
|
||||
it('allows regenerating an already parsed video and clears the current workspace cache', async () => {
|
||||
apiMock.getProjects
|
||||
.mockResolvedValueOnce([{ id: 'p-ready', name: 'ready.mp4', status: 'ready', frames: 1500, video_path: 'uploads/ready.mp4', parse_fps: 30, source_type: 'video' }])
|
||||
.mockResolvedValueOnce([{ id: 'p-ready', name: 'ready.mp4', status: 'parsing', frames: 0, video_path: 'uploads/ready.mp4', parse_fps: 24, source_type: 'video' }]);
|
||||
apiMock.parseMedia.mockResolvedValueOnce({ status: 'queued', progress: 0 });
|
||||
useStore.setState({
|
||||
currentProject: { id: 'p-ready', name: 'ready.mp4', status: 'ready' },
|
||||
frames: [{ id: 'old-frame', projectId: 'p-ready', index: 0, url: '/old.jpg', width: 640, height: 360 }],
|
||||
masks: [{ id: 'old-mask', frameId: 'old-frame', pathData: 'M 0 0 Z', label: 'old', color: '#fff' }],
|
||||
selectedMaskIds: ['old-mask'],
|
||||
});
|
||||
|
||||
render(<ProjectLibrary onProjectSelect={vi.fn()} />);
|
||||
|
||||
fireEvent.click(await screen.findByRole('button', { name: '重新生成帧' }));
|
||||
expect(screen.getByText('重新生成会清空该项目现有帧序列、标注和 mask,再按新的 FPS 从源视频生成帧。')).toBeInTheDocument();
|
||||
fireEvent.click(screen.getByRole('button', { name: '开始生成帧' }));
|
||||
|
||||
await waitFor(() => expect(apiMock.parseMedia).toHaveBeenCalledWith('p-ready', { parseFps: 30 }));
|
||||
expect(useStore.getState().frames).toEqual([]);
|
||||
expect(useStore.getState().masks).toEqual([]);
|
||||
expect(useStore.getState().selectedMaskIds).toEqual([]);
|
||||
});
|
||||
|
||||
it('hides frame generation while editing a project name', async () => {
|
||||
apiMock.getProjects.mockResolvedValueOnce([
|
||||
{ id: 'p-edit', name: 'Editable Clip', status: 'pending', frames: 0, video_path: 'uploads/editable.mp4', parse_fps: 30, source_type: 'video' },
|
||||
|
||||
Reference in New Issue
Block a user