From 1713607572324e64d9ff99f0795c6c06123c0dfd Mon Sep 17 00:00:00 2001 From: suyx Date: Wed, 5 Aug 2026 19:00:07 +0800 Subject: [PATCH] =?UTF-8?q?fix(POSTV1-09):=20=E5=B1=95=E7=A4=BA=E9=A1=B9?= =?UTF-8?q?=E7=9B=AE=E7=94=9F=E6=88=90=E5=9B=BE=E7=89=87?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/web/src/project-pages.css | 33 +++++++++++++----- apps/web/src/project-pages.tsx | 52 ++++++++++++++++++++++++++-- tests/e2e/projects-workspace.spec.ts | 33 +++++++++++++++++- 3 files changed, 106 insertions(+), 12 deletions(-) diff --git a/apps/web/src/project-pages.css b/apps/web/src/project-pages.css index 1f8e744..35e9986 100644 --- a/apps/web/src/project-pages.css +++ b/apps/web/src/project-pages.css @@ -592,15 +592,26 @@ height: 18px; } -.project-placeholder { +.project-placeholder, +.project-preview { display: grid; height: 154px; - grid-template-columns: repeat(4, 1fr); overflow: hidden; border-bottom: 1px solid #a5a59f; background: #d8d8d3; } +.project-placeholder { + grid-template-columns: repeat(4, 1fr); +} + +.project-preview img { + display: block; + width: 100%; + height: 100%; + object-fit: cover; +} + .project-placeholder span { display: grid; place-items: end center; @@ -935,9 +946,9 @@ font-size: 19px; } -.project-current > .project-placeholder { - height: auto; - min-height: 480px; +.project-current > .project-placeholder, +.project-current > .project-preview { + height: 480px; border: 1px solid #73736d; } @@ -945,6 +956,10 @@ font-size: 80px; } +.project-current > .project-preview img { + object-fit: contain; +} + .project-actions { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); @@ -1013,7 +1028,8 @@ padding: 6px; } -.project-history li .project-placeholder { +.project-history li .project-placeholder, +.project-history li .project-preview { height: 88px; border: 0; } @@ -1439,8 +1455,9 @@ flex-direction: column; } - .project-current > .project-placeholder { - min-height: 360px; + .project-current > .project-placeholder, + .project-current > .project-preview { + height: 360px; } .local-only-footer { diff --git a/apps/web/src/project-pages.tsx b/apps/web/src/project-pages.tsx index af4fc19..835a0d6 100644 --- a/apps/web/src/project-pages.tsx +++ b/apps/web/src/project-pages.tsx @@ -231,6 +231,33 @@ function ProjectPlaceholder({ ratio, status }: { ratio: Ratio; status: ProjectSt ); } +function ProjectPreview({ alt, imageId, loading = "lazy", projectId, ratio, status }: { + alt: string; + imageId: string | null; + loading?: "eager" | "lazy"; + projectId: string; + ratio: Ratio; + status: ProjectStatus; +}) { + const [loadFailed, setLoadFailed] = useState(false); + + useEffect(() => setLoadFailed(false), [imageId, projectId]); + + if (!imageId || loadFailed) return ; + + return ( +
+ {alt} setLoadFailed(true)} + src={`/api/v1/private-assets/projects/${encodeURIComponent(projectId)}/images/${encodeURIComponent(imageId)}`} + /> +
+ ); +} + export function WorkspacePage() { const promptId = useId(); const [session, setSession] = useState(); @@ -568,7 +595,13 @@ function ProjectCard({ activeLimitReached, busy, onPurge, onRestore, onSelect, o /> ) : null} - +

{project.name}

{project.status === "failed_empty" ? "生成失败" : project.status === "trashed" ? "回收站" : "项目"}

{project.successful_image_count} 张成功图 · {project.ratio}

@@ -953,7 +986,14 @@ export function ProjectDetailPage({ projectId }: { projectId: string }) {

当前底图

{project.pixel_width ?? 1080} × {project.pixel_height ?? 1440}
- +
{conflicted || !project.current_image_id ? : 进入编辑器} @@ -970,7 +1010,13 @@ export function ProjectDetailPage({ projectId }: { projectId: string }) {
    {project.images.toReversed().map((image, index) => (
  1. - +
    生成结果 {project.images.length - index}下载原始图
  2. ))} diff --git a/tests/e2e/projects-workspace.spec.ts b/tests/e2e/projects-workspace.spec.ts index bb9b351..59d13f2 100644 --- a/tests/e2e/projects-workspace.spec.ts +++ b/tests/e2e/projects-workspace.spec.ts @@ -33,6 +33,10 @@ function routeSession(page: Page) { })); } +function generatedImageSvg(label: string) { + return `${label}`; +} + async function captureEvidence(page: Page, caseId: string, name: string) { const root = process.env.DADA_EVIDENCE_DIR_PROJECTS; if (!root) return; @@ -75,6 +79,12 @@ test("TDD-WP2-PROJ-005 limits failed-empty selection on the project list", async ], }), contentType: "application/json", status: 200, })); + await page.route(`**/api/v1/private-assets/projects/${successId}/images/*`, (route) => route.fulfill({ + body: generatedImageSvg("城市工作室"), + contentType: "image/svg+xml", + headers: { "Content-Disposition": "attachment; filename=\"dada-original.png\"" }, + status: 200, + })); let batchPayload: unknown; await page.route("**/api/v1/projects/failed-empty/trash", async (route) => { batchPayload = route.request().postDataJSON(); @@ -86,6 +96,12 @@ test("TDD-WP2-PROJ-005 limits failed-empty selection on the project list", async await expect(page.getByRole("heading", { name: "项目" })).toBeVisible(); await expect(page.getByText("2 / 20 active")).toBeVisible(); + const projectPreview = page.getByRole("img", { name: "城市工作室预览图" }); + await expect(projectPreview).toHaveAttribute( + "src", + `/api/v1/private-assets/projects/${successId}/images/00000000-0000-4000-8000-000000000213`, + ); + await expect(projectPreview).toHaveCSS("object-fit", "cover"); await expect(page.getByLabel("选择失败草稿:失败草稿")).toBeVisible(); await expect(page.getByLabel("选择失败草稿:城市工作室")).toHaveCount(0); await page.getByLabel("选择失败草稿:失败草稿").check(); @@ -99,9 +115,10 @@ test("TDD-WP2-PROJ-005 limits failed-empty selection on the project list", async test("TDD-WP2-PROJ-001 keeps ratio fixed and blocks an eleventh image in project detail", async ({ page }) => { await routeSession(page); const projectId = "00000000-0000-4000-8000-000000000221"; + const currentImageId = "00000000-0000-4000-8000-000000000222"; await page.route(`**/api/v1/projects/${projectId}`, (route) => route.fulfill({ body: JSON.stringify({ - created_at: "2026-07-28T08:00:00.000Z", current_image_id: "00000000-0000-4000-8000-000000000222", + created_at: "2026-07-28T08:00:00.000Z", current_image_id: currentImageId, draft_prompt: "城市工作室", generations: [], images: Array.from({ length: 10 }, (_, index) => ({ created_at: `2026-07-28T08:${String(index).padStart(2, "0")}:00.000Z`, generation_id: `00000000-0000-4000-8000-${String(223 + index).padStart(12, "0")}`, @@ -111,11 +128,25 @@ test("TDD-WP2-PROJ-001 keeps ratio fixed and blocks an eleventh image in project status: "active", successful_image_count: 10, updated_at: "2026-07-28T08:10:00.000Z", }), contentType: "application/json", status: 200, })); + await page.route(`**/api/v1/private-assets/projects/${projectId}/images/*`, (route) => route.fulfill({ + body: generatedImageSvg("生成结果"), + contentType: "image/svg+xml", + headers: { "Content-Disposition": "attachment; filename=\"dada-original.png\"" }, + status: 200, + })); await page.goto(`${webUrl}/app/projects/${projectId}`); await expect(page.getByRole("heading", { name: "城市工作室" })).toBeVisible(); await expect(page.getByText("固定比例 3:4")).toBeVisible(); await expect(page.getByText("10 / 10 张成功图")).toBeVisible(); + const currentImage = page.getByRole("img", { name: "城市工作室当前底图" }); + await expect(currentImage).toHaveAttribute( + "src", + `/api/v1/private-assets/projects/${projectId}/images/${currentImageId}`, + ); + await expect(currentImage).toHaveAttribute("loading", "eager"); + await expect(currentImage).toHaveCSS("object-fit", "contain"); + await expect(page.getByRole("img", { name: "生成结果 10" })).toBeVisible(); await expect(page.getByRole("button", { name: "继续生成" })).toBeDisabled(); await expect(page.getByText("请先删除一张非当前底图的历史图")).toBeVisible(); await expect(page.getByRole("radio")).toHaveCount(0);