生成帧完成后自动刷新项目封面
- 项目库生成帧任务入队后继续轮询任务进度,解析成功后重新拉取项目列表和当前项目对象。 - 成功后显示视频帧生成完成提示,并让后端写入的 thumbnail_url 立即刷新到项目卡片封面。 - 补充 ProjectLibrary 回归测试,覆盖生成帧完成后自动显示新封面。 - 更新前端审计、设计冻结、测试计划、状态机和项目指南文档。
This commit is contained in:
@@ -134,8 +134,10 @@ describe('ProjectLibrary', () => {
|
||||
it('generates frames from an imported video with the selected FPS', async () => {
|
||||
apiMock.getProjects
|
||||
.mockResolvedValueOnce([{ id: 'p4', name: 'clip.mp4', status: 'pending', frames: 0, video_path: 'uploads/clip.mp4', parse_fps: 30 }])
|
||||
.mockResolvedValueOnce([{ id: 'p4', name: 'clip.mp4', status: 'parsing', frames: 0, video_path: 'uploads/clip.mp4', parse_fps: 12 }]);
|
||||
.mockResolvedValueOnce([{ id: 'p4', name: 'clip.mp4', status: 'parsing', frames: 0, video_path: 'uploads/clip.mp4', parse_fps: 12 }])
|
||||
.mockResolvedValueOnce([{ id: 'p4', name: 'clip.mp4', status: 'ready', frames: 24, video_path: 'uploads/clip.mp4', parse_fps: 12, thumbnail_url: 'http://thumb/frame.jpg' }]);
|
||||
apiMock.parseMedia.mockResolvedValueOnce({ id: 22, status: 'queued', progress: 0 });
|
||||
apiMock.getTask.mockResolvedValueOnce({ id: 22, status: 'success', progress: 100, message: '解析完成' });
|
||||
|
||||
const { container } = render(<ProjectLibrary onProjectSelect={vi.fn()} />);
|
||||
|
||||
@@ -146,6 +148,11 @@ describe('ProjectLibrary', () => {
|
||||
await waitFor(() => expect(apiMock.parseMedia).toHaveBeenCalledWith('p4', { parseFps: 12 }));
|
||||
expect(await screen.findByRole('status')).toHaveTextContent('生成帧任务已入队 #22');
|
||||
expect(await screen.findByText('12FPS')).toBeInTheDocument();
|
||||
expect(screen.getByText('正在生成视频帧')).toBeInTheDocument();
|
||||
|
||||
await waitFor(() => expect(apiMock.getTask).toHaveBeenCalledWith(22), { timeout: 2500 });
|
||||
expect(await screen.findByAltText('clip.mp4')).toHaveAttribute('src', 'http://thumb/frame.jpg');
|
||||
expect(await screen.findByRole('status')).toHaveTextContent('视频帧生成完成,项目封面已自动更新');
|
||||
});
|
||||
|
||||
it('hides frame generation while editing a project name', async () => {
|
||||
|
||||
@@ -96,6 +96,18 @@ export function ProjectLibrary({ onProjectSelect }: ProjectLibraryProps) {
|
||||
}
|
||||
};
|
||||
|
||||
const refreshProjects = async () => {
|
||||
const data = await getProjects();
|
||||
setProjects(data);
|
||||
if (currentProject?.id) {
|
||||
const refreshedCurrentProject = data.find((project) => project.id === currentProject.id);
|
||||
if (refreshedCurrentProject) {
|
||||
setCurrentProject(refreshedCurrentProject);
|
||||
}
|
||||
}
|
||||
return data;
|
||||
};
|
||||
|
||||
const ImportProgressPanel = () => {
|
||||
if (!importProgress) return null;
|
||||
const percent = typeof importProgress.percent === 'number'
|
||||
@@ -378,14 +390,75 @@ export function ProjectLibrary({ onProjectSelect }: ProjectLibraryProps) {
|
||||
|
||||
const handleGenerateFrames = async () => {
|
||||
if (!frameProject?.id) return;
|
||||
const targetProject = frameProject;
|
||||
setIsGeneratingFrames(true);
|
||||
try {
|
||||
const task = await parseMedia(frameProject.id, { parseFps: frameParseFps });
|
||||
const task = await parseMedia(targetProject.id, { parseFps: frameParseFps });
|
||||
showNotice(`生成帧任务已入队 #${task.id}\n帧率: ${frameParseFps} FPS\n可在 Dashboard 查看进度。`, 'success');
|
||||
const data = await getProjects();
|
||||
setProjects(data);
|
||||
await refreshProjects();
|
||||
setShowFrameConfig(false);
|
||||
setFrameProject(null);
|
||||
if (task.id) {
|
||||
setImportProgress({
|
||||
kind: 'video',
|
||||
phase: 'parsing',
|
||||
title: '正在生成视频帧',
|
||||
detail: task.message || `项目:${targetProject.name}`,
|
||||
percent: task.progress ?? 0,
|
||||
});
|
||||
void waitForTaskDone(task.id, (progress) => {
|
||||
setImportProgress({
|
||||
kind: 'video',
|
||||
phase: 'parsing',
|
||||
title: '正在生成视频帧',
|
||||
detail: progress.message || `任务状态: ${progress.status || 'running'}`,
|
||||
percent: progress.progress,
|
||||
});
|
||||
}).then(async (completed) => {
|
||||
if (completed.status === 'failed') {
|
||||
setImportProgress({
|
||||
kind: 'video',
|
||||
phase: 'error',
|
||||
title: '视频帧生成失败',
|
||||
detail: completed.error || completed.message || targetProject.name,
|
||||
percent: 100,
|
||||
});
|
||||
showNotice('视频帧生成失败,请检查后端服务或任务日志', 'error');
|
||||
return;
|
||||
}
|
||||
if (completed.status === 'cancelled') {
|
||||
setImportProgress({
|
||||
kind: 'video',
|
||||
phase: 'error',
|
||||
title: '视频帧生成已取消',
|
||||
detail: targetProject.name,
|
||||
percent: 100,
|
||||
});
|
||||
showNotice('视频帧生成任务已取消', 'error');
|
||||
return;
|
||||
}
|
||||
await refreshProjects();
|
||||
setImportProgress({
|
||||
kind: 'video',
|
||||
phase: 'done',
|
||||
title: '视频帧生成完成',
|
||||
detail: '项目封面已自动更新',
|
||||
percent: 100,
|
||||
});
|
||||
showNotice('视频帧生成完成,项目封面已自动更新', 'success');
|
||||
scheduleProgressDismiss();
|
||||
}).catch((err) => {
|
||||
console.error('Frame generation polling failed:', err);
|
||||
setImportProgress({
|
||||
kind: 'video',
|
||||
phase: 'error',
|
||||
title: '视频帧生成进度同步失败',
|
||||
detail: targetProject.name,
|
||||
percent: 100,
|
||||
});
|
||||
showNotice('视频帧生成进度同步失败,请刷新项目库重试', 'error');
|
||||
});
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Frame generation failed:', err);
|
||||
showNotice('生成帧失败,请检查后端服务或项目源文件', 'error');
|
||||
|
||||
Reference in New Issue
Block a user