新增图片画布项目页

新增 /project 项目页和我的页项目入口

补齐图片画布工程列表、重命名和删除 API

支持 /editor/canvas 按 projectid 加载指定工程

更新图片画布文档、TRACKING 和对应测试
This commit is contained in:
2026-06-14 00:11:36 +08:00
parent b2122481ff
commit 85834a423d
32 changed files with 1800 additions and 20 deletions

View File

@@ -2,13 +2,15 @@
import { fireEvent, render, screen, waitFor, within } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { afterEach, describe, expect, it, vi } from 'vitest';
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
import { ApiClientError } from '../../services/apiClient';
import { ImageCanvasEditorView } from './ImageCanvasEditorView';
const generateEditorImageMock = vi.hoisted(() => vi.fn());
const editEditorImageMock = vi.hoisted(() => vi.fn());
const loadEditorProjectMock = vi.hoisted(() => vi.fn());
const loadOrCreateRecentEditorProjectMock = vi.hoisted(() => vi.fn());
vi.mock('../../services/image-editor/editorProjectClient', async () => {
const actual = await vi.importActual<
@@ -18,6 +20,8 @@ vi.mock('../../services/image-editor/editorProjectClient', async () => {
...actual,
editEditorImage: editEditorImageMock,
generateEditorImage: generateEditorImageMock,
loadEditorProject: loadEditorProjectMock,
loadOrCreateRecentEditorProject: loadOrCreateRecentEditorProjectMock,
};
});
@@ -36,10 +40,47 @@ function dispatchPointerEvent(
}
describe('ImageCanvasEditorView', () => {
beforeEach(() => {
loadOrCreateRecentEditorProjectMock.mockResolvedValue({
projectId: 'editor-project-default',
title: '默认项目',
viewport: { x: 0, y: 0, scale: 1 },
layers: [],
resources: [],
updatedAt: '2026-06-12T00:00:00.000Z',
});
});
afterEach(() => {
vi.restoreAllMocks();
generateEditorImageMock.mockReset();
editEditorImageMock.mockReset();
loadEditorProjectMock.mockReset();
loadOrCreateRecentEditorProjectMock.mockReset();
window.history.replaceState(null, '', '/editor/canvas');
});
it('loads the project from projectid query before falling back to recent project', async () => {
loadEditorProjectMock.mockResolvedValueOnce({
projectId: 'editor-project-query',
title: '查询项目',
viewport: { x: 12, y: 16, scale: 0.8 },
layers: [],
resources: [],
updatedAt: '2026-06-12T00:00:00.000Z',
});
window.history.replaceState(
null,
'',
'/editor/canvas?projectid=editor-project-query',
);
render(<ImageCanvasEditorView />);
await waitFor(() => {
expect(loadEditorProjectMock).toHaveBeenCalledWith('editor-project-query');
});
expect(loadOrCreateRecentEditorProjectMock).not.toHaveBeenCalled();
});
it('toggles the shared sidebar from canvas panel buttons', () => {
@@ -196,6 +237,9 @@ describe('ImageCanvasEditorView', () => {
value: createObjectUrlSpy,
});
render(<ImageCanvasEditorView />);
await waitFor(() => {
expect(loadOrCreateRecentEditorProjectMock).toHaveBeenCalled();
});
const bottomToolbar = screen.getByRole('toolbar', { name: 'AI画布工具栏' });
@@ -344,11 +388,15 @@ describe('ImageCanvasEditorView', () => {
await waitFor(() => {
expect(screen.getByAltText(/画布图片:生成图片/)).toBeTruthy();
});
const generatedLayer = screen.getByAltText(/画布图片:生成图片/).closest('button')!;
const anchoredGenerateDialog = screen.getByRole('dialog', { name: '生成图片' });
expect(anchoredGenerateDialog).toBeTruthy();
expect(
Number.parseFloat((anchoredGenerateDialog as HTMLElement).style.top),
).toBeCloseTo(initialComposerTop, 1);
).toBeGreaterThan(Number.parseFloat((generatedLayer as HTMLElement).style.top));
expect(
Number.parseFloat((generatedLayer as HTMLElement).style.top),
).toBeLessThan(initialComposerTop);
expect(screen.queryByLabelText('图像生成占位图')).toBeNull();
const metadataButtons = screen.getAllByRole('button', {
name: /查看生成图片 .*元数据/,
@@ -369,6 +417,9 @@ describe('ImageCanvasEditorView', () => {
taskId: 'editor-drag-frame-1',
});
render(<ImageCanvasEditorView />);
await waitFor(() => {
expect(loadOrCreateRecentEditorProjectMock).toHaveBeenCalled();
});
fireEvent.click(screen.getByRole('button', { name: '生成工具' }));
const initialComposerTop = Number.parseFloat(
@@ -409,10 +460,10 @@ describe('ImageCanvasEditorView', () => {
expect(anchoredGenerateDialog).toBeTruthy();
expect(
Number.parseFloat((anchoredGenerateDialog as HTMLElement).style.top),
).toBeCloseTo(draggedComposerTop, 1);
).toBeGreaterThan(Number.parseFloat((generatedLayer as HTMLElement).style.top));
expect(screen.queryByLabelText('图像生成占位图')).toBeNull();
expect(Number.parseFloat((generatedLayer as HTMLElement).style.left)).toBeGreaterThan(700);
expect(Number.parseFloat((generatedLayer as HTMLElement).style.top)).toBeGreaterThan(150);
expect(Number.parseFloat((generatedLayer as HTMLElement).style.left)).toBeGreaterThan(300);
expect(Number.parseFloat((generatedLayer as HTMLElement).style.top)).toBeGreaterThan(180);
});
it('keeps the generation composer when selecting another image', () => {

View File

@@ -42,6 +42,7 @@ import {
type EditorImageGenerationResult,
type EditorProjectLayerSnapshot,
generateEditorImage,
loadEditorProject,
loadOrCreateRecentEditorProject,
saveEditorProjectLayout,
} from '../../services/image-editor/editorProjectClient';
@@ -639,7 +640,16 @@ export function ImageCanvasEditorView() {
useEffect(() => {
let cancelled = false;
loadOrCreateRecentEditorProject()
const projectIdFromQuery =
typeof window === 'undefined'
? null
: new URLSearchParams(window.location.search).get('projectid')?.trim() ||
null;
const loadProject = projectIdFromQuery
? loadEditorProject(projectIdFromQuery)
: loadOrCreateRecentEditorProject();
loadProject
.then((project) => {
if (cancelled) {
return;