feat(assets): 素材库与画布 - 新增可交互通用文件夹

This commit is contained in:
echoD886
2026-08-20 15:16:45 +08:00
parent 9ee71d4650
commit 935108edca
35 changed files with 2010 additions and 155 deletions

View File

@@ -56,10 +56,25 @@ export type ProjectAsset = {
primaryVersionId?: string;
versionCount: number;
usages: string[];
folderId?: string;
position: number;
storageKey?: string;
previewText?: string;
updatedAt: string;
character?: CharacterCardSummary;
};
export type ProjectAssetFolder = {
id: string;
projectId: string;
parentId?: string;
name: string;
style: "glass" | "stacked" | "midnight" | "paper" | "cinema" | "compact" | string;
position: number;
createdAt: string;
updatedAt: string;
};
export type CharacterRepresentation = {
id: string;
resourceId: string;
@@ -160,6 +175,7 @@ export type ProjectDetail = {
canvases: ProjectCanvas[];
canvasUnitLinks: CanvasUnitLink[];
assets: ProjectAsset[];
assetFolders: ProjectAssetFolder[];
workflows: ProjectWorkflow[];
shots: ProjectShot[];
shotReferences: ShotAssetReference[];
@@ -222,7 +238,7 @@ export function unlinkCanvasProject(projectId: string, canvasId: string) {
return request<{ canvasId: string }>(api.delete(`/projects/${encodeURIComponent(projectId)}/canvases/${encodeURIComponent(canvasId)}`));
}
export function linkProjectAsset(projectId: string, input: { assetId: string; category: string }, signal?: AbortSignal) {
export function linkProjectAsset(projectId: string, input: { assetId: string; category: string; folderId?: string }, signal?: AbortSignal) {
return request<{ asset: ProjectAsset }>(api.post(`/projects/${encodeURIComponent(projectId)}/assets`, input, { signal }));
}
@@ -234,6 +250,26 @@ export function updateProjectAssetCategory(projectId: string, assetId: string, c
return request<{ asset: ProjectAsset }>(api.patch(`/projects/${encodeURIComponent(projectId)}/assets/${encodeURIComponent(assetId)}`, { category }, { signal }));
}
export function moveProjectAsset(projectId: string, assetId: string, folderId: string, signal?: AbortSignal) {
return request<{ asset: ProjectAsset }>(api.patch(`/projects/${encodeURIComponent(projectId)}/assets/${encodeURIComponent(assetId)}`, { folderId }, { signal }));
}
export function listProjectAssetFolders(projectId: string, signal?: AbortSignal) {
return request<{ folders: ProjectAssetFolder[] }>(api.get(`/projects/${encodeURIComponent(projectId)}/asset-folders`, { signal }));
}
export function createProjectAssetFolder(projectId: string, input: { name: string; parentId?: string; style?: ProjectAssetFolder["style"] }) {
return request<{ folder: ProjectAssetFolder }>(api.post(`/projects/${encodeURIComponent(projectId)}/asset-folders`, input));
}
export function updateProjectAssetFolder(projectId: string, folderId: string, input: { name?: string; parentId?: string; style?: ProjectAssetFolder["style"] }) {
return request<{ folder: ProjectAssetFolder }>(api.patch(`/projects/${encodeURIComponent(projectId)}/asset-folders/${encodeURIComponent(folderId)}`, input));
}
export function deleteProjectAssetFolder(projectId: string, folderId: string) {
return request<{ id: string }>(api.delete(`/projects/${encodeURIComponent(projectId)}/asset-folders/${encodeURIComponent(folderId)}`));
}
export function createProjectAssetVersion(projectId: string, assetId: string, input: { prompt?: string; definitionJson?: string; note?: string }) {
return request<{ version: { id: string; assetId: string; version: number; status: string } }>(api.post(`/projects/${encodeURIComponent(projectId)}/assets/${encodeURIComponent(assetId)}/versions`, input));
}

View File

@@ -1,7 +1,7 @@
import { canvasNodeToAsset, declaredCanvasNodeAssetCategory, findCanvasNodeAsset, type CanvasAssetSource } from "@/lib/canvas/canvas-node-asset";
import { readImageMeta } from "@/lib/image-utils";
import { parseBackendGenerationResult, type BackendGenerationResult } from "@/services/api/generation-task";
import { linkProjectAsset, updateProjectAssetCategory } from "@/services/api/projects";
import { linkProjectAsset, moveProjectAsset, updateProjectAssetCategory } from "@/services/api/projects";
import type { GenerationTask, GenerationTaskOutput } from "@/services/api/task-center";
import { getMediaBlob, resolveMediaUrl, setMediaBlob } from "@/services/file-storage";
import { createGenerationTaskMaterializer, createIdempotentMaterializeOutput, type MaterializeGenerationTaskOutput } from "@/services/generation-task-materializer";
@@ -27,6 +27,7 @@ type EnsureCanvasNodeAssetOptions = {
source: CanvasAssetSource;
taskId?: string;
category?: AssetCategory;
folderId?: string;
signal?: AbortSignal;
};
@@ -68,16 +69,19 @@ async function persistCanvasNodeAsset(options: EnsureCanvasNodeAssetOptions): Pr
asset = useAssetStore.getState().assets.find((item) => item.id === asset?.id) || asset;
}
if (!options.domainProjectId) return { assetId: asset.id, created, linkedToProject: false };
await syncAssetToProject(asset.id, options.domainProjectId, declaredCategory, options.signal);
await syncAssetToProject(asset.id, options.domainProjectId, declaredCategory, options.folderId, options.signal);
return { assetId: asset.id, created, linkedToProject: true };
}
async function syncAssetToProject(assetId: string, domainProjectId: string, category?: AssetCategory, signal?: AbortSignal) {
async function syncAssetToProject(assetId: string, domainProjectId: string, category?: AssetCategory, folderId?: string, signal?: AbortSignal) {
throwIfAborted(signal);
const asset = useAssetStore.getState().assets.find((candidate) => candidate.id === assetId);
if (!asset) throw new Error("素材写入本地失败");
const linkedProjectIds = Array.isArray(asset.metadata?.projectIds) ? asset.metadata.projectIds.filter((id): id is string => typeof id === "string") : [];
if (linkedProjectIds.includes(domainProjectId)) return;
if (linkedProjectIds.includes(domainProjectId)) {
if (folderId !== undefined) await moveProjectAsset(domainProjectId, asset.id, folderId, signal);
return;
}
// 项目关联依赖后端 assets 记录,先强制完成素材同步,不能依赖延迟自动同步的时序。
await saveRemoteUserDataNow();
@@ -87,11 +91,13 @@ async function syncAssetToProject(assetId: string, domainProjectId: string, cate
{
assetId: asset.id,
category: category || asset.category || "other",
folderId,
},
signal,
);
throwIfAborted(signal);
const linked = category && linkedAsset.category !== category ? (await updateProjectAssetCategory(domainProjectId, asset.id, category, signal)).asset : linkedAsset;
let linked = category && linkedAsset.category !== category ? (await updateProjectAssetCategory(domainProjectId, asset.id, category, signal)).asset : linkedAsset;
if (folderId !== undefined && (linked.folderId || "") !== folderId) linked = (await moveProjectAsset(domainProjectId, asset.id, folderId, signal)).asset;
throwIfAborted(signal);
useAssetStore.getState().updateAsset(asset.id, {
category: linked.category as AssetCategory,