From 7417370ae9ced961621c39918da228ec352fe7b2 Mon Sep 17 00:00:00 2001 From: kdletters Date: Mon, 29 Jun 2026 17:22:42 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BF=AE=E5=A4=8D=E7=94=BB=E5=B8=83=E4=BB=BB?= =?UTF-8?q?=E5=8A=A1=E5=88=97=E8=A1=A8=E6=9C=8D=E5=8A=A1=E7=AB=AF=E4=BB=BB?= =?UTF-8?q?=E5=8A=A1=E6=98=BE=E7=A4=BA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 任务列表只使用服务端返回的生成任务。 保留生成任务水合后的开始和结束时间。 同步任务列表与画布阶段视图测试。 --- .../ImageCanvasEditorModel.test.ts | 4 + .../image-editor/ImageCanvasEditorModel.ts | 8 + .../ImageCanvasEditorShellView.test.tsx | 3 - .../image-editor/ImageCanvasEditorView.tsx | 3 - .../image-editor/ImageCanvasStageView.tsx | 11 -- .../ImageCanvasTaskSidebarView.test.tsx | 121 +++++++------- .../ImageCanvasTaskSidebarView.tsx | 153 ++---------------- 7 files changed, 88 insertions(+), 215 deletions(-) diff --git a/src/components/image-editor/ImageCanvasEditorModel.test.ts b/src/components/image-editor/ImageCanvasEditorModel.test.ts index 935f5be14..ec8ad5ca5 100644 --- a/src/components/image-editor/ImageCanvasEditorModel.test.ts +++ b/src/components/image-editor/ImageCanvasEditorModel.test.ts @@ -569,6 +569,8 @@ describe('ImageCanvasEditorModel', () => { composerOpen: false, generatedLayerId: 'layer-generated', imageModel: 'gpt-image-2', + generationStartedAt: 1_771_400_000_000, + generationFinishedAt: 1_771_400_004_000, placeholder: { x: 100, y: 120, @@ -624,6 +626,8 @@ describe('ImageCanvasEditorModel', () => { status: 'generating', generatedLayerId: 'layer-generated', imageModel: 'gpt-image-2', + generationStartedAt: 1_771_400_000_000, + generationFinishedAt: 1_771_400_004_000, placeholder: { x: 100, y: 120, diff --git a/src/components/image-editor/ImageCanvasEditorModel.ts b/src/components/image-editor/ImageCanvasEditorModel.ts index dc772ff27..435a3c146 100644 --- a/src/components/image-editor/ImageCanvasEditorModel.ts +++ b/src/components/image-editor/ImageCanvasEditorModel.ts @@ -592,6 +592,8 @@ export function hydrateCanvasGenerationDialog( aspectRatio: stringOrUndefined(snapshot.aspectRatio), imageSize: stringOrUndefined(snapshot.imageSize), errorMessage: stringOrUndefined(snapshot.errorMessage), + generationStartedAt: numberOrUndefined(snapshot.generationStartedAt), + generationFinishedAt: numberOrUndefined(snapshot.generationFinishedAt), placeholder: hydrateGenerationPlaceholder(snapshot.placeholder), }; } @@ -823,6 +825,12 @@ export function numberFromSnapshot(value: unknown, fallback: number) { return typeof value === 'number' && Number.isFinite(value) ? value : fallback; } +function numberOrUndefined(value: unknown) { + return typeof value === 'number' && Number.isFinite(value) + ? value + : undefined; +} + function audioDurationOrNull(value: unknown) { if (value && typeof value === 'object' && 'durationSeconds' in value) { return audioDurationOrNull( diff --git a/src/components/image-editor/ImageCanvasEditorShellView.test.tsx b/src/components/image-editor/ImageCanvasEditorShellView.test.tsx index 3e10708ce..623d6d43a 100644 --- a/src/components/image-editor/ImageCanvasEditorShellView.test.tsx +++ b/src/components/image-editor/ImageCanvasEditorShellView.test.tsx @@ -133,7 +133,6 @@ function createStageProps(): ImageCanvasStageViewProps { hoveredLayerId: null, canvasMarquee: null, canvasGenerationDialogs: [], - backgroundRemovalTasks: [], isTaskSidebarOpen: false, generateDialog: null, cropExpandPanel: null, @@ -176,8 +175,6 @@ function createStageProps(): ImageCanvasStageViewProps { onUpdateLayerAssetKind: vi.fn(), onGenerationFramePointerDown: vi.fn(), onActivateGenerationDialog: vi.fn(), - onFocusGenerationDialog: vi.fn(), - onFocusTaskLayer: vi.fn(), onFocusExternalTask: vi.fn(), onToggleTaskSidebar: vi.fn(), onCropExpandHandlePointerDown: vi.fn(), diff --git a/src/components/image-editor/ImageCanvasEditorView.tsx b/src/components/image-editor/ImageCanvasEditorView.tsx index 8b6e858a2..3d1ad0012 100644 --- a/src/components/image-editor/ImageCanvasEditorView.tsx +++ b/src/components/image-editor/ImageCanvasEditorView.tsx @@ -1678,7 +1678,6 @@ export function ImageCanvasEditorView() { hoveredLayerId, canvasMarquee, canvasGenerationDialogs, - backgroundRemovalTasks: generationSurface.backgroundRemovalTasks, isTaskSidebarOpen: generationSurface.isTaskSidebarOpen, generateDialog, cropExpandPanel: generationSurface.cropExpandPanel, @@ -1726,8 +1725,6 @@ export function ImageCanvasEditorView() { onUpdateLayerAssetKind: updateLayerAssetKind, onGenerationFramePointerDown: handleGenerationFramePointerDown, onActivateGenerationDialog: activateCanvasGenerationDialog, - onFocusGenerationDialog: focusCanvasGenerationDialog, - onFocusTaskLayer: focusCanvasLayerById, onFocusExternalTask: focusExternalGenerationTask, onToggleTaskSidebar: generationSurface.toggleTaskSidebar, onCropExpandHandlePointerDown: generationSurface.startCropExpandFrameResize, diff --git a/src/components/image-editor/ImageCanvasStageView.tsx b/src/components/image-editor/ImageCanvasStageView.tsx index c731cd54c..ebe47c183 100644 --- a/src/components/image-editor/ImageCanvasStageView.tsx +++ b/src/components/image-editor/ImageCanvasStageView.tsx @@ -17,7 +17,6 @@ import type { CanvasClipboard, CanvasContextMenuState, CanvasGenerationDialogState, - CanvasTaskListItem, CanvasLayer, CanvasMarqueeState, CanvasTool, @@ -58,7 +57,6 @@ export type ImageCanvasStageViewProps = { hoveredLayerId: string | null; canvasMarquee: CanvasMarqueeState | null; canvasGenerationDialogs: CanvasGenerationDialogState[]; - backgroundRemovalTasks: CanvasTaskListItem[]; isTaskSidebarOpen: boolean; generateDialog: GenerateDialogState | null; cropExpandPanel: CropExpandPanelState | null; @@ -117,8 +115,6 @@ export type ImageCanvasStageViewProps = { dialog: CanvasGenerationDialogState, ) => void; onActivateGenerationDialog: (dialog: CanvasGenerationDialogState) => void; - onFocusGenerationDialog: (dialog: CanvasGenerationDialogState) => void; - onFocusTaskLayer: (layerId: string) => void; onFocusExternalTask: (task: ExternalGenerationTaskRecord) => void; onToggleTaskSidebar: () => void; onCropExpandHandlePointerDown: ( @@ -199,7 +195,6 @@ export function ImageCanvasStageView({ hoveredLayerId, canvasMarquee, canvasGenerationDialogs, - backgroundRemovalTasks, isTaskSidebarOpen, generateDialog, cropExpandPanel, @@ -243,8 +238,6 @@ export function ImageCanvasStageView({ onUpdateLayerAssetKind, onGenerationFramePointerDown, onActivateGenerationDialog, - onFocusGenerationDialog, - onFocusTaskLayer, onFocusExternalTask, onToggleTaskSidebar, onCropExpandHandlePointerDown, @@ -445,12 +438,8 @@ export function ImageCanvasStageView({ diff --git a/src/components/image-editor/ImageCanvasTaskSidebarView.test.tsx b/src/components/image-editor/ImageCanvasTaskSidebarView.test.tsx index d8640f038..c56213bcb 100644 --- a/src/components/image-editor/ImageCanvasTaskSidebarView.test.tsx +++ b/src/components/image-editor/ImageCanvasTaskSidebarView.test.tsx @@ -6,7 +6,6 @@ import { beforeEach, describe, expect, it, vi } from 'vitest'; import type { ExternalGenerationTaskRecord } from '../../../packages/shared/src/contracts/externalGeneration'; import { listExternalGenerationTasks } from '../../services/external-generation'; import { ImageCanvasTaskSidebarView } from './ImageCanvasTaskSidebarView'; -import type { CanvasGenerationDialogState } from './ImageCanvasEditorTypes'; vi.mock('../../services/external-generation', () => ({ listExternalGenerationTasks: vi.fn().mockResolvedValue({ @@ -35,27 +34,6 @@ beforeEach(() => { }); }); -function createDialog( - overrides: Partial, -): CanvasGenerationDialogState { - return { - id: overrides.id ?? 'dialog-1', - mode: overrides.mode ?? 'generate', - prompt: overrides.prompt ?? '', - status: overrides.status ?? 'idle', - composerOpen: false, - placeholder: { - x: 0, - y: 0, - width: 320, - height: 320, - originalWidth: 1024, - originalHeight: 1024, - }, - ...overrides, - }; -} - function createExternalTask( overrides: Partial = {}, ): ExternalGenerationTaskRecord { @@ -85,40 +63,53 @@ function createExternalTask( describe('ImageCanvasTaskSidebarView', () => { it('groups tasks by tabs and keeps running tasks before queued tasks', async () => { - const focusGenerationDialog = vi.fn(); + const focusExternalTask = vi.fn(); const parentWheel = vi.fn(); + const completedAt = new Date(Date.now() - 1000).toISOString(); + listExternalGenerationTasksMock.mockImplementation( + (options: Parameters[0] = {}) => + Promise.resolve({ + overview: { + pendingCount: 1, + runningCount: 1, + unacknowledgedTerminalCount: 1, + updatedAtMicros: 1, + }, + tasks: options.statuses?.includes('running') + ? [ + createExternalTask({ + jobId: 'queued-task', + requestLabel: '图标素材', + status: 'queued', + }), + createExternalTask({ + jobId: 'running-task', + requestLabel: '角色图片', + status: 'running', + startedAt: new Date(Date.now() - 3000).toISOString(), + }), + ] + : [ + createExternalTask({ + jobId: 'done-task', + requestLabel: '视频生成', + requestPrompt: '生成一段开场动画', + status: 'completed', + startedAt: new Date(Date.now() - 4000).toISOString(), + completedAt, + updatedAt: completedAt, + }), + ], + }), + ); + render(
, ); @@ -149,11 +140,18 @@ describe('ImageCanvasTaskSidebarView', () => { .filter((text) => text.includes('角色图片') || text.includes('图标素材')); expect(activeTitles[0]).toContain('角色图片'); expect(activeTitles[1]).toContain('图标素材'); + await waitFor(() => + expect( + screen + .getByRole('tab', { name: /排队\/生成中/u }) + .getAttribute('aria-selected'), + ).toBe('true'), + ); const completedTab = screen.getByRole('tab', { name: /已完成/u }); fireEvent.click(completedTab); - expect(screen.queryByText('视频生成')).not.toBeNull(); - expect(screen.queryByText(/生成一段开场动画/u)).not.toBeNull(); + expect(await screen.findByText('视频生成')).toBeTruthy(); + expect(screen.getByText(/生成一段开场动画/u)).toBeTruthy(); expect(screen.queryByText(/完成于/u)).not.toBeNull(); expect(screen.queryByText(/生成完成/u)).toBeNull(); @@ -161,8 +159,8 @@ describe('ImageCanvasTaskSidebarView', () => { expect(parentWheel).not.toHaveBeenCalled(); fireEvent.click(screen.getByText('视频生成')); - expect(focusGenerationDialog).toHaveBeenCalledWith( - expect.objectContaining({ id: 'done-dialog' }), + expect(focusExternalTask).toHaveBeenCalledWith( + expect.objectContaining({ jobId: 'done-task' }), ); expect(completedTab.getAttribute('aria-selected')).toBe('true'); }); @@ -216,12 +214,8 @@ describe('ImageCanvasTaskSidebarView', () => { render( , ); @@ -231,9 +225,16 @@ describe('ImageCanvasTaskSidebarView', () => { ).toBeTruthy(); expect(screen.getByText(/已用时 1分/u)).toBeTruthy(); expect(screen.queryByText(/不该显示的外部项目任务/u)).toBeNull(); + await waitFor(() => + expect( + screen + .getByRole('tab', { name: /排队\/生成中/u }) + .getAttribute('aria-selected'), + ).toBe('true'), + ); fireEvent.click(screen.getByRole('tab', { name: /已完成/u })); - expect(screen.getByText(/已完成提示词/u)).toBeTruthy(); + expect(await screen.findByText(/已完成提示词/u)).toBeTruthy(); expect(screen.getByText(/完成于/u)).toBeTruthy(); expect(screen.queryByText(/另一个项目的已完成任务/u)).toBeNull(); }); @@ -266,12 +267,8 @@ describe('ImageCanvasTaskSidebarView', () => { render( , ); diff --git a/src/components/image-editor/ImageCanvasTaskSidebarView.tsx b/src/components/image-editor/ImageCanvasTaskSidebarView.tsx index 464e6e1c4..5fe705dfc 100644 --- a/src/components/image-editor/ImageCanvasTaskSidebarView.tsx +++ b/src/components/image-editor/ImageCanvasTaskSidebarView.tsx @@ -17,11 +17,7 @@ import { import type { ExternalGenerationTaskRecord } from '../../../packages/shared/src/contracts/externalGeneration'; import { listExternalGenerationTasks } from '../../services/external-generation'; import { EditorIconButton } from './ImageCanvasEditorPrimitives'; -import type { - CanvasGenerationDialogState, - CanvasTaskListItem, - CanvasTaskStatus, -} from './ImageCanvasEditorTypes'; +import type { CanvasTaskStatus } from './ImageCanvasEditorTypes'; const COMPLETED_TASK_LIST_LIMIT = 20; const ACTIVE_TASK_LIST_LIMIT = 100; @@ -44,12 +40,8 @@ type TaskSidebarItem = { type ImageCanvasTaskSidebarViewProps = { projectId?: string | null; - generationDialogs: CanvasGenerationDialogState[]; - backgroundRemovalTasks: CanvasTaskListItem[]; open: boolean; onToggleOpen: () => void; - onFocusGenerationDialog: (dialog: CanvasGenerationDialogState) => void; - onFocusLayer: (layerId: string) => void; onFocusExternalTask: (task: ExternalGenerationTaskRecord) => void; }; @@ -85,58 +77,11 @@ function formatTaskTimeMs(timestampMs?: number) { )}:${pad(date.getMinutes())}`; } -function generationDialogTitle(dialog: CanvasGenerationDialogState) { - switch (dialog.mode) { - case 'quick-edit': - return '局部修改'; - case 'character-animation': - return '角色动画'; - case 'video': - return '视频生成'; - case 'audio-sound-effect': - return '音效生成'; - case 'audio-background-music': - return '背景音乐'; - case 'icon': - return '图标素材'; - case 'ui-design': - return 'UI素材提取'; - case 'publication': - return '宣发素材'; - case 'character': - return '角色图片'; - case 'spec': - return '图片规范'; - default: - return '图片生成'; - } -} - -function generationDialogDetail(dialog: CanvasGenerationDialogState) { - if (dialog.status === 'generating') { - return '生成中'; - } - if (dialog.status === 'failed') { - return dialog.errorMessage || '生成失败'; - } - return dialog.generatedLayerId ? '生成完成' : '待提交'; -} - function normalizeTaskPrompt(value?: string | null) { const prompt = value?.trim(); return prompt || null; } -function generationDialogTaskDetail( - dialog: CanvasGenerationDialogState, - status: CanvasTaskStatus, -) { - if (status === 'done') { - return normalizeTaskPrompt(dialog.prompt) ?? generationDialogTitle(dialog); - } - return generationDialogDetail(dialog); -} - function externalTaskDetail( task: ExternalGenerationTaskRecord, status: CanvasTaskStatus, @@ -183,7 +128,7 @@ function canvasTaskStatusFromExternalTask( function statusLabel(status: CanvasTaskStatus) { if (status === 'pending') { - return '待提交'; + return '排队中'; } if (status === 'running') { return '进行中'; @@ -267,12 +212,8 @@ function mergeExternalTaskRefresh( export function ImageCanvasTaskSidebarView({ projectId, - generationDialogs, - backgroundRemovalTasks, open, onToggleOpen, - onFocusGenerationDialog, - onFocusLayer, onFocusExternalTask, }: ImageCanvasTaskSidebarViewProps) { const [activeTab, setActiveTab] = useState('active'); @@ -280,7 +221,6 @@ export function ImageCanvasTaskSidebarView({ const [externalTasks, setExternalTasks] = useState< ExternalGenerationTaskRecord[] >([]); - const observedStartedAtRef = useRef(new Map()); const previousActiveItemCountRef = useRef(0); const filterVisibleExternalTasks = useCallback( (tasks: ExternalGenerationTaskRecord[]) => @@ -395,13 +335,16 @@ export function ImageCanvasTaskSidebarView({ window.clearTimeout(timerId); } }; - }, [activeExternalTaskIds, activeExternalTaskKey, filterVisibleExternalTasks]); + }, [ + activeExternalTaskIds, + activeExternalTaskKey, + filterVisibleExternalTasks, + ]); useEffect(() => { - const hasRunningTask = - generationDialogs.some((dialog) => dialog.status === 'generating') || - backgroundRemovalTasks.some((task) => task.status === 'running') || - externalTasks.some((task) => task.status === 'running'); + const hasRunningTask = externalTasks.some( + (task) => task.status === 'running', + ); if (!hasRunningTask) { return undefined; } @@ -409,66 +352,9 @@ export function ImageCanvasTaskSidebarView({ setNow(Date.now()); }, 1000); return () => window.clearInterval(timerId); - }, [backgroundRemovalTasks, externalTasks, generationDialogs]); + }, [externalTasks]); const items = useMemo(() => { - const resolveStartedAt = (itemId: string, startedAt?: number) => { - if (typeof startedAt === 'number' && Number.isFinite(startedAt)) { - return startedAt; - } - const observedStartedAt = observedStartedAtRef.current.get(itemId); - if (observedStartedAt) { - return observedStartedAt; - } - observedStartedAtRef.current.set(itemId, now); - return now; - }; - const generationItems: TaskSidebarItem[] = generationDialogs.map( - (dialog) => { - const status = - dialog.status === 'generating' - ? 'running' - : dialog.status === 'failed' - ? 'failed' - : dialog.generatedLayerId - ? 'done' - : 'pending'; - const id = `generation-${dialog.id}`; - const startedAt = - status === 'running' - ? resolveStartedAt(id, dialog.generationStartedAt) - : dialog.generationStartedAt; - const finishedAt = dialog.generationFinishedAt; - return { - id, - title: generationDialogTitle(dialog), - detail: generationDialogTaskDetail(dialog, status), - status, - startedAt, - finishedAt, - elapsedMs: - status === 'running' && startedAt - ? now - startedAt - : startedAt && finishedAt - ? finishedAt - startedAt - : undefined, - errorMessage: dialog.errorMessage, - onClick: () => onFocusGenerationDialog(dialog), - sortTime: finishedAt ?? startedAt ?? 0, - }; - }, - ); - const backgroundItems: TaskSidebarItem[] = backgroundRemovalTasks.map( - (task) => ({ - ...task, - elapsedMs: - task.status === 'running' ? now - task.startedAt : task.elapsedMs, - onClick: task.focusLayerId - ? () => onFocusLayer(task.focusLayerId!) - : undefined, - sortTime: task.finishedAt ?? task.startedAt, - }), - ); const externalVisibleTasks = mergeExternalTasks( externalTasks.filter( (task) => task.status === 'queued' || task.status === 'running', @@ -515,16 +401,8 @@ export function ImageCanvasTaskSidebarView({ 0, }; }); - return [...backgroundItems, ...generationItems, ...externalItems]; - }, [ - backgroundRemovalTasks, - externalTasks, - generationDialogs, - now, - onFocusExternalTask, - onFocusGenerationDialog, - onFocusLayer, - ]); + return externalItems; + }, [externalTasks, now, onFocusExternalTask]); const activeItems = useMemo( () => @@ -597,7 +475,10 @@ export function ImageCanvasTaskSidebarView({ 任务列表 {items.length} -
+