feat(assets): 素材库与画布 - 新增可交互通用文件夹
This commit is contained in:
@@ -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));
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user