fix(assets): 统一角色卡封面选择逻辑 (#617)
Co-authored-by: ddcat666 <26564566+ddcat666@users.noreply.github.com>
This commit is contained in:
@@ -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;
|
||||
|
||||
@@ -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" ? "图片待补全" : "图片未绑定";
|
||||
|
||||
8
web/src/pages/projects/detail/project-character-cover.ts
Normal file
8
web/src/pages/projects/detail/project-character-cover.ts
Normal 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];
|
||||
}
|
||||
26
web/test/project-character-cover.test.ts
Normal file
26
web/test/project-character-cover.test.ts
Normal 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)");
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user