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

@@ -7,6 +7,7 @@ import { aceternityMotion } from "@/lib/aceternity-motion";
import { SpotlightSurface } from "@/components/ui/aceternity/spotlight-surface";
import { canvasThemes } from "@/lib/canvas-theme";
import { canvasNodeAssetCategory } from "@/lib/canvas/canvas-node-asset";
import { isCanvasFolderNode } from "@/lib/canvas/canvas-frame";
import { resolveAddNodeMenuCommands, type AddNodeMenuContext } from "@/lib/canvas/tool-registry";
import { useThemeStore } from "@/stores/use-theme-store";
import { CanvasNodeType, type CanvasNodeData, type CanvasWorkspaceMode, type ContextMenuState, type Position } from "@/types/canvas";
@@ -33,6 +34,7 @@ type CanvasNodeContextMenuProps = {
canPaste: boolean;
onClose: () => void;
onAddNode: (type: CanvasNodeType) => void;
onAddFolder: () => void;
onChooseStyle: () => void;
onOpenDirector: (position: Position) => void;
onUpload: () => void;
@@ -65,6 +67,7 @@ export function CanvasNodeContextMenu({
canPaste,
onClose,
onAddNode,
onAddFolder,
onChooseStyle,
onOpenDirector,
onUpload,
@@ -128,6 +131,7 @@ export function CanvasNodeContextMenu({
const isMedia = isImage || isVideo;
const isAudio = node?.type === CanvasNodeType.Audio;
const isFrame = node?.type === CanvasNodeType.Frame;
const isFolder = isCanvasFolderNode(node);
const hasNodeContent = isText ? Boolean(nodeContent.trim()) : Boolean(nodeContent);
const canSaveAsset = Boolean(node && !isCharacterReference && (isText ? hasNodeContent : hasNodeContent && (isImage || isVideo || isAudio)));
const canOpenPreview = Boolean(isMedia && hasNodeContent);
@@ -207,17 +211,17 @@ export function CanvasNodeContextMenu({
<>
<MenuHeader title={node?.title || nodeTypeLabel(node)} />
<MenuSection label="节点操作" />
{isFrame ? <MenuButton icon={<PanelTop />} label={node?.metadata?.frame?.collapsed ? "展开背板" : "折叠背板"} onClick={() => runAction(onToggleFrame)} /> : <MenuButton icon={<FolderPlus />} label="保存到我的素材" disabled={!canSaveAsset} onClick={() => runAction(onSaveAsset)} />}
{isFrame ? <MenuButton icon={isFolder ? <FolderOpen /> : <PanelTop />} label={node?.metadata?.frame?.collapsed ? `展开${isFolder ? "文件夹" : "背板"}` : `折叠${isFolder ? "文件夹" : "背板"}`} onClick={() => runAction(onToggleFrame)} /> : <MenuButton icon={<FolderPlus />} label="保存到我的素材" disabled={!canSaveAsset} onClick={() => runAction(onSaveAsset)} />}
{isText ? <MenuButton icon={<Maximize2 />} label="放大编辑" onClick={() => runAction(onEditText)} /> : null}
{isDrawing ? <MenuButton icon={<Pencil />} label="打开绘图" onClick={() => runAction(onOpenDrawing)} /> : null}
{isText ? <MenuButton icon={<ImageIcon />} label="用文本生图" disabled={!canGenerateFromText} onClick={() => runAction(onGenerateImage)} /> : null}
<MenuDivider />
<MenuSection label="副本与内容" />
<MenuButton icon={<Copy />} label={isFrame ? "复制背板及内容" : "复制节点"} shortcut="⌘C" onClick={() => runAction(onCopyNode)} />
<MenuButton icon={<Copy />} label={isFrame ? `复制${isFolder ? "文件夹" : "背板"}及内容` : "复制节点"} shortcut="⌘C" onClick={() => runAction(onCopyNode)} />
{isText ? <MenuButton icon={<Clipboard />} label="复制文本" disabled={!hasNodeContent} onClick={() => runAction(onCopyContent)} /> : null}
<MenuButton icon={<Copy />} label={isFrame ? "创建背板副本" : "创建参数变体"} shortcut="⌘D" onClick={() => runAction(onDuplicate)} />
<MenuButton icon={<Copy />} label={isFrame ? `创建${isFolder ? "文件夹" : "背板"}副本` : "创建参数变体"} shortcut="⌘D" onClick={() => runAction(onDuplicate)} />
<MenuButton icon={<Clipboard />} label="粘贴" shortcut="⌘V" disabled={!canPaste} onClick={() => runAction(onPaste)} />
<MenuButton icon={<Trash2 />} label={isFrame ? "删除背板" : "删除节点"} danger onClick={() => runAction(onDelete)} />
<MenuButton icon={<Trash2 />} label={isFrame ? `删除${isFolder ? "文件夹" : "背板"}` : "删除节点"} danger onClick={() => runAction(onDelete)} />
</>
)}
</>
@@ -238,6 +242,7 @@ export function CanvasNodeContextMenu({
isProjectLinked={isProjectLinked}
reducedMotion={Boolean(reducedMotion)}
onAddNode={(type) => runAction(() => onAddNode(type))}
onAddFolder={() => runAction(onAddFolder)}
onChooseStyle={() => runAction(onChooseStyle)}
onOpenDirector={() => runAction(() => onOpenDirector(menu.position))}
onUpload={() => runAction(onUpload)}
@@ -250,7 +255,7 @@ export function CanvasNodeContextMenu({
);
}
function AddNodeContextMenu({ parentPosition, workspaceMode, isProjectLinked, reducedMotion, onAddNode, onChooseStyle, onOpenDirector, onUpload, onOpenAssets, onOpenProjectCharacters }: { parentPosition: { left: number; top: number }; workspaceMode: CanvasWorkspaceMode; isProjectLinked: boolean; reducedMotion: boolean; onAddNode: (type: CanvasNodeType) => void; onChooseStyle: () => void; onOpenDirector: () => void; onUpload: () => void; onOpenAssets: () => void; onOpenProjectCharacters: () => void }) {
function AddNodeContextMenu({ parentPosition, workspaceMode, isProjectLinked, reducedMotion, onAddNode, onAddFolder, onChooseStyle, onOpenDirector, onUpload, onOpenAssets, onOpenProjectCharacters }: { parentPosition: { left: number; top: number }; workspaceMode: CanvasWorkspaceMode; isProjectLinked: boolean; reducedMotion: boolean; onAddNode: (type: CanvasNodeType) => void; onAddFolder: () => void; onChooseStyle: () => void; onOpenDirector: () => void; onUpload: () => void; onOpenAssets: () => void; onOpenProjectCharacters: () => void }) {
const theme = canvasThemes[useThemeStore((state) => state.theme)];
const left = getSubmenuLeft(parentPosition.left);
const createContext: AddNodeMenuContext = {
@@ -263,6 +268,7 @@ function AddNodeContextMenu({ parentPosition, workspaceMode, isProjectLinked, re
onAddAudio: () => onAddNode(CanvasNodeType.Audio),
onAddScript: () => onAddNode(CanvasNodeType.Script),
onAddFrame: () => onAddNode(CanvasNodeType.Frame),
onAddFolder,
onAddDrawing: () => onAddNode(CanvasNodeType.Drawing),
onChooseStyle,
onOpenDirector,
@@ -364,6 +370,6 @@ function nodeTypeLabel(node?: CanvasNodeData | null) {
if (node.type === CanvasNodeType.Video) return "视频节点";
if (node.type === CanvasNodeType.Audio) return "音频节点";
if (node.type === CanvasNodeType.Drawing) return "绘图节点";
if (node.type === CanvasNodeType.Frame) return "背板";
if (node.type === CanvasNodeType.Frame) return isCanvasFolderNode(node) ? "文件夹" : "背板";
return "生成配置节点";
}