feat(*): 分镜与资产 - 完善五类资产、候选去重、搜索、中文镜头编号与分类整理

This commit is contained in:
ddCat
2026-08-31 13:15:38 +08:00
parent ce6e20f749
commit f548681787
53 changed files with 816 additions and 188 deletions

View File

@@ -2,6 +2,7 @@ import { useMemo } from "react";
import { AssetLibraryPickerModal, type AssetLibraryPickerItem } from "@/components/assets/asset-library-picker-modal";
import { useExternalAssetSources } from "@/hooks/use-external-asset-sources";
import { ASSET_CATEGORY_LABELS, normalizeAssetCategory } from "@/lib/asset-category";
import type { ExternalAssetPickerReference } from "@/lib/plugins/plugin-types";
import { useAssetStore, type Asset } from "@/stores/use-asset-store";
@@ -21,7 +22,7 @@ type Props = {
onClose: () => void;
};
const categoryLabels: Record<string, string> = { all: "全部素材", character: "角色", environment: "场景", wardrobe: "服饰", prop: "道具", weapon: "武器", style: "画风", other: "其他" };
const categoryLabels: Record<string, string> = { all: "全部素材", ...ASSET_CATEGORY_LABELS };
export function AssetPickerModal({ open, multiple = true, onInsert, onClose }: Props) {
const assets = useAssetStore((state) => state.assets);
@@ -31,7 +32,7 @@ export function AssetPickerModal({ open, multiple = true, onInsert, onClose }: P
...insertableAssets.map((asset) => ({
id: asset.id,
title: asset.title,
category: asset.category || "other",
category: normalizeAssetCategory(asset.category),
kindLabel: asset.kind === "image" ? "图片" : asset.kind === "video" ? "视频" : asset.kind === "audio" ? "音频" : "文本",
asset,
searchText: (asset.tags || []).join(" "),

View File

@@ -3,6 +3,7 @@ import { useEffect, useState, type CSSProperties, type ReactNode } from "react";
import { ArrowLeft, Check, ChevronRight, Clipboard, CloudUpload, Copy, FolderOpen, FolderPlus, Image as ImageIcon, Layers3, Link2, Maximize2, PanelTop, Pencil, Plus, Redo2, Tags, Trash2, Undo2, Upload, UserRound } from "lucide-react";
import { CanvasCreateMenu, type CanvasCreateCommand } from "@/components/canvas/canvas-create-menu";
import { ASSET_CATEGORY_OPTIONS } from "@/lib/asset-category";
import { aceternityMotion } from "@/lib/aceternity-motion";
import { SpotlightSurface } from "@/components/ui/aceternity/spotlight-surface";
import { canvasThemes } from "@/lib/canvas-theme";
@@ -15,15 +16,7 @@ import { CanvasNodeType, type CanvasNodeData, type CanvasNodeTypeId, type Canvas
type CanvasAssetCategory = NonNullable<NonNullable<CanvasNodeData["metadata"]>["assetCategory"]>;
const assetCategoryOptions: Array<{ value: CanvasAssetCategory; label: string }> = [
{ value: "character", label: "角色" },
{ value: "environment", label: "场景" },
{ value: "wardrobe", label: "服饰" },
{ value: "prop", label: "道具" },
{ value: "weapon", label: "武器" },
{ value: "style", label: "画风" },
{ value: "other", label: "其他" },
];
const assetCategoryOptions: Array<{ value: CanvasAssetCategory; label: string }> = ASSET_CATEGORY_OPTIONS;
type CanvasNodeContextMenuProps = {
menu: ContextMenuState;

View File

@@ -4,6 +4,7 @@ import type { MenuProps } from "antd";
import { ChevronDown, Ellipsis, Lock, Plus, Unlock } from "lucide-react";
import { canvasDockStyle } from "@/lib/canvas/canvas-aceternity-style";
import { ASSET_CATEGORY_OPTIONS } from "@/lib/asset-category";
import { canvasThemes } from "@/lib/canvas-theme";
import { resolveToolbarTools, type ToolContext, type ToolbarHandlers } from "@/lib/canvas/tool-registry";
import { subscribeCanvasViewportPreview } from "@/lib/canvas/canvas-live-viewport";
@@ -58,15 +59,7 @@ type CanvasNodeToolbarProps = {
type CanvasAssetCategory = NonNullable<NonNullable<CanvasNodeData["metadata"]>["assetCategory"]>;
const assetCategoryOptions: Array<{ value: CanvasAssetCategory; label: string }> = [
{ value: "character", label: "角色" },
{ value: "environment", label: "场景" },
{ value: "wardrobe", label: "服饰" },
{ value: "prop", label: "道具" },
{ value: "weapon", label: "武器" },
{ value: "style", label: "画风" },
{ value: "other", label: "其他" },
];
const assetCategoryOptions: Array<{ value: CanvasAssetCategory; label: string }> = ASSET_CATEGORY_OPTIONS;
type ToolbarTool = {
id: string;

View File

@@ -4,12 +4,13 @@ import { AssetLibraryPickerModal, type AssetLibraryPickerItem } from "@/componen
import { useExternalAssetSources } from "@/hooks/use-external-asset-sources";
import { externalAssetToInsertPayload, type InsertAssetPayload } from "@/components/canvas/asset-picker-modal";
import { compileCharacterReferencePrompt } from "@/lib/canvas/canvas-character-reference";
import { ASSET_CATEGORY_LABELS, normalizeAssetCategory } from "@/lib/asset-category";
import { resourceFileUrl, resourceIdFromStorageKey } from "@/services/api/resources";
import type { ProjectAsset, ProjectDetail } from "@/services/api/projects";
import { getRemoteAsset } from "@/services/api/user-data";
import { useAssetStore, type Asset } from "@/stores/use-asset-store";
const categoryLabels: Record<string, string> = { all: "全部资产", character: "角色", environment: "场景", wardrobe: "服饰", prop: "道具", weapon: "武器", style: "画风", other: "其他" };
const categoryLabels: Record<string, string> = { all: "全部资产", ...ASSET_CATEGORY_LABELS };
type ProjectPickerItem = { id: string; category: string; folderId?: string; project?: ProjectAsset; character?: ProjectAsset; media?: Asset };
export function CanvasProjectAssetModal({ open, detail, initialCategory = "all", initialFolderId = "all", onClose, onInsert, onInsertFolder }: { open: boolean; detail?: ProjectDetail; initialCategory?: string; initialFolderId?: string; onClose: () => void; onInsert: (payloads: InsertAssetPayload[]) => Promise<void> | void; onInsertFolder?: (folderId: string) => Promise<void> | void }) {
@@ -20,13 +21,13 @@ export function CanvasProjectAssetModal({ open, detail, initialCategory = "all",
const projectItems = (detail?.assets || []).flatMap((asset): ProjectPickerItem[] => {
if (asset.category === "character" && asset.character) return [{ id: asset.id, category: "character", folderId: asset.folderId, project: asset, character: asset }];
const media = mediaById.get(asset.id);
return asset.mediaType === "model" || asset.mediaType === "entity" ? [] : [{ id: asset.id, category: asset.category || media?.category || "other", folderId: asset.folderId, project: asset, media }];
return asset.mediaType === "model" || asset.mediaType === "entity" ? [] : [{ id: asset.id, category: normalizeAssetCategory(asset.category || media?.category), folderId: asset.folderId, project: asset, media }];
});
if (detail) return projectItems;
// 自由画布未关联项目时回退到个人素材库。
return mediaAssets
.filter((asset) => asset.kind !== "model" && asset.kind !== "entity")
.map((media): ProjectPickerItem => ({ id: media.id, category: media.category || "other", media }));
.map((media): ProjectPickerItem => ({ id: media.id, category: normalizeAssetCategory(media.category), media }));
}, [detail?.assets, mediaAssets]);
const localPickerItems = useMemo<AssetLibraryPickerItem[]>(() => items.map((item) => {
const character = item.character;

View File

@@ -4,6 +4,7 @@ import { createPortal } from "react-dom";
import { ArrowLeft, ChevronRight, FileText, Folder, Image as ImageIcon, Music2, Pencil, Search, UserRound, Video, Workflow } from "lucide-react";
import { canvasThemes } from "@/lib/canvas-theme";
import { ASSET_CATEGORY_LABELS } from "@/lib/asset-category";
import { useThemeStore } from "@/stores/use-theme-store";
import { buildAssetMentionReferences, canvasResourceMentionToken, type CanvasResourceReference } from "@/lib/canvas/canvas-resource-references";
import { useAssetStore, type AssetCategory } from "@/stores/use-asset-store";
@@ -504,16 +505,6 @@ function createInlinePreview(reference: CanvasResourceReference) {
return fallback;
}
const ASSET_CATEGORY_LABELS: Record<AssetCategory, string> = {
character: "角色",
environment: "场景",
wardrobe: "服饰",
prop: "道具",
weapon: "武器",
style: "画风",
other: "其他",
};
function MentionMenu({ anchor, connectedReferences, assetReferences, filteredReferences, query, cursorOffset, activeReferenceId, preferredWidth, onQueryChange, onClose, onSelect }: {
anchor: HTMLElement;
connectedReferences: CanvasResourceReference[];