fix(assets): 统一角色卡封面选择逻辑 (#617)

Co-authored-by: ddcat666 <26564566+ddcat666@users.noreply.github.com>
This commit is contained in:
yepwait
2026-09-24 06:36:12 +08:00
committed by ddcat666
parent f3ab23e0c3
commit 267d7746b4
4 changed files with 40 additions and 4 deletions

View File

@@ -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;

View File

@@ -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" ? "图片待补全" : "图片未绑定";

View File

@@ -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];
}

View File

@@ -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)");
});
});