From 267d7746b40b660c5a1cee975115a34b36fa4272 Mon Sep 17 00:00:00 2001 From: yepwait <46060092+yepwait@users.noreply.github.com> Date: Thu, 24 Sep 2026 06:36:12 +0800 Subject: [PATCH] =?UTF-8?q?fix(assets):=20=E7=BB=9F=E4=B8=80=E8=A7=92?= =?UTF-8?q?=E8=89=B2=E5=8D=A1=E5=B0=81=E9=9D=A2=E9=80=89=E6=8B=A9=E9=80=BB?= =?UTF-8?q?=E8=BE=91=20(#617)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: ddcat666 <26564566+ddcat666@users.noreply.github.com> --- web/src/pages/projects/detail/assets.tsx | 7 ++--- .../detail/project-character-card.tsx | 3 ++- .../detail/project-character-cover.ts | 8 ++++++ web/test/project-character-cover.test.ts | 26 +++++++++++++++++++ 4 files changed, 40 insertions(+), 4 deletions(-) create mode 100644 web/src/pages/projects/detail/project-character-cover.ts create mode 100644 web/test/project-character-cover.test.ts diff --git a/web/src/pages/projects/detail/assets.tsx b/web/src/pages/projects/detail/assets.tsx index aa5467120..8d340a17a 100644 --- a/web/src/pages/projects/detail/assets.tsx +++ b/web/src/pages/projects/detail/assets.tsx @@ -47,6 +47,7 @@ import { useConfigStore, useEffectiveConfig } from "@/stores/use-config-store"; import { CanvasNodeType, type CanvasFolderStyle, type CanvasFolderTheme, type CanvasNodeData } from "@/types/canvas"; import { ProjectCharacterCard } from "./project-character-card"; +import { projectCharacterCover } from "./project-character-cover"; import { linkSelectedProjectAssets } from "./project-asset-linking"; import { generateCharacterTurnaround } from "./project-character-media"; import { categoryLabels, categoryLabel, mediaLabel, StatusPill, formatTime, textValue, type ProjectDetailViewProps } from "./shared"; @@ -380,7 +381,7 @@ export default function ProjectAssetsView({ detail, refreshProject }: ProjectDet await downloadBrowserMedia({ storageKey: personal.data.storageKey, url, fileName: `${asset.title || "asset"}.${extension}` }); return; } - const cover = asset.character?.representations.find((item) => item.role === "turnaround_sheet") || asset.character?.representations.find((item) => item.role === "primary") || asset.character?.representations[0]; + const cover = projectCharacterCover(asset.character?.representations); if (cover) { await downloadBrowserMedia({ storageKey: resourceStorageKey(cover.resourceId), fileName: `${asset.title || "character"}.png` }); return; @@ -602,7 +603,7 @@ function ProjectAssetFolderCard({ folder, folders, assets, folderCounts, persona function projectAssetCanvasPreviewNode(asset: ProjectAsset, personalAsset: Asset | undefined, index: number): CanvasNodeData { const type = asset.mediaType === "image" || asset.category === "character" ? CanvasNodeType.Image : asset.mediaType === "video" ? CanvasNodeType.Video : asset.mediaType === "audio" ? CanvasNodeType.Audio : CanvasNodeType.Text; - const characterCover = asset.character?.representations.find((item) => item.role === "turnaround_sheet") || asset.character?.representations.find((item) => item.role === "primary") || asset.character?.representations[0]; + const characterCover = projectCharacterCover(asset.character?.representations); const content = characterCover ? resourceFileUrl(characterCover.resourceId) : personalAsset?.kind === "image" @@ -712,7 +713,7 @@ function ProjectAssetMedia({ asset, personalAsset }: { asset: ProjectAsset; pers } function ProjectAssetPreviewModal({ asset, personalAsset, onClose, onDownload, onReplaceImage }: { asset: ProjectAsset | null; personalAsset?: Asset; onClose: () => void; onDownload: () => void; onReplaceImage: () => void }) { - const characterCover = asset?.character?.representations.find((item) => item.role === "turnaround_sheet") || asset?.character?.representations.find((item) => item.role === "primary") || asset?.character?.representations[0]; + const characterCover = projectCharacterCover(asset?.character?.representations); const remoteUrl = asset ? projectAssetRemoteUrl(asset) : ""; const canDownload = Boolean(personalAsset && ["image", "video", "audio", "model"].includes(personalAsset.kind)) || Boolean(characterCover) || Boolean(remoteUrl); const previewKind = personalAsset?.kind || asset?.mediaType; diff --git a/web/src/pages/projects/detail/project-character-card.tsx b/web/src/pages/projects/detail/project-character-card.tsx index 443c5f3f8..3edbc0411 100644 --- a/web/src/pages/projects/detail/project-character-card.tsx +++ b/web/src/pages/projects/detail/project-character-card.tsx @@ -7,6 +7,7 @@ import { resourceFileUrl } from "@/services/api/resources"; import type { ProjectAsset } from "@/services/api/projects"; import { AssetLibraryCard, AssetLibraryCardMedia } from "@/components/assets/asset-library-card"; +import { projectCharacterCover } from "./project-character-cover"; import { textValue } from "./shared"; export function ProjectCharacterCard({ asset, folderItems, generating, removing, onOpen, onEdit, onGenerate, onBindImages, onBindVoice, onMove, onRemove }: { @@ -23,7 +24,7 @@ export function ProjectCharacterCard({ asset, folderItems, generating, removing, onRemove: () => void; }) { const character = asset.character; - const cover = character?.representations.find((item) => item.role === "turnaround_sheet") || character?.representations.find((item) => item.role === "primary") || character?.representations.find((item) => item.role === "front") || character?.representations[0]; + const cover = projectCharacterCover(character?.representations); const role = textValue(character?.definition.role) || "未填写剧情定位"; const appearance = textValue(character?.definition.appearance) || textValue(character?.definition.consistencyPrompt) || "角色设定待完善"; const imageStatus = character?.visualStatus === "ready" ? "图片已绑定" : character?.visualStatus === "partial" ? "图片待补全" : "图片未绑定"; diff --git a/web/src/pages/projects/detail/project-character-cover.ts b/web/src/pages/projects/detail/project-character-cover.ts new file mode 100644 index 000000000..54f5eddb5 --- /dev/null +++ b/web/src/pages/projects/detail/project-character-cover.ts @@ -0,0 +1,8 @@ +import type { CharacterRepresentation } from "@/services/api/projects"; + +export function projectCharacterCover(representations?: CharacterRepresentation[]) { + return representations?.find((item) => item.role === "turnaround_sheet") + || representations?.find((item) => item.role === "primary") + || representations?.find((item) => item.role === "front") + || representations?.[0]; +} diff --git a/web/test/project-character-cover.test.ts b/web/test/project-character-cover.test.ts new file mode 100644 index 000000000..3df54e494 --- /dev/null +++ b/web/test/project-character-cover.test.ts @@ -0,0 +1,26 @@ +import { readFileSync } from "node:fs"; +import { resolve } from "node:path"; + +import { describe, expect, test } from "bun:test"; + +import type { CharacterRepresentation } from "../src/services/api/projects"; +import { projectCharacterCover } from "../src/pages/projects/detail/project-character-cover"; + +const characterCardSource = readFileSync(resolve(import.meta.dir, "../src/pages/projects/detail/project-character-card.tsx"), "utf8"); +const projectAssetsSource = readFileSync(resolve(import.meta.dir, "../src/pages/projects/detail/assets.tsx"), "utf8"); + +describe("project character cover selection", () => { + test("prefers the front representation over an earlier side representation", () => { + const representations: CharacterRepresentation[] = [ + { id: "side", resourceId: "side-image", mediaType: "image", role: "side" }, + { id: "front", resourceId: "front-image", mediaType: "image", role: "front" }, + ]; + + expect(projectCharacterCover(representations)?.resourceId).toBe("front-image"); + }); + + test("uses the same cover selector for cards and the preview modal", () => { + expect(characterCardSource).toContain("projectCharacterCover(character?.representations)"); + expect(projectAssetsSource).toContain("projectCharacterCover(asset?.character?.representations)"); + }); +});