feat(canvas): 画布工作区 - 整合侧栏与风格预设并修复提示词回填

选择性整合 #547,保留主分支节点与工具条设计。新增默认折叠的节点、资产、任务和历史面板,风格与预设保留在节点内,移除重复入口。

补齐工具权限、提示词解析、资源引用保护及数据库迁移,修复提示词清空和连续编辑后的生成配置同步。

验证:前端全量 1925 项测试、构建、类型检查及 lint 通过;后端 go test ./... 通过。真实模型、PostgreSQL 实库与登录态页面验收仍待进行。

Refs #547

Co-authored-by: bensharp <bensharp@qq.com>
This commit is contained in:
ddCatandbensharp committed 2026-09-20 16:40:08 +08:00
1 parent be5f2733b6
commit e93bf3ca39
66 files changed
+6127 -97

No files matched your search

@@ -1,10 +1,10 @@
import type { ReactNode } from "react";
import { Brush, Camera, Copy, FileText, Globe2, Grid2x2, Layers3, Lock, LockOpen, Maximize2, PencilLine, Crop, SlidersHorizontal, Smile, Sun, Upload, Scaling, WandSparkles } from "lucide-react";
import { isValidElement, type ReactNode } from "react";
import { Brush, Camera, Clapperboard, Contrast, Copy, FastForward, FileText, Globe2, Grid2x2, Grid3x3, Layers3, Lock, LockOpen, Maximize2, Package, PencilLine, PersonStanding, Crop, Rewind, ScanFace, SlidersHorizontal, Smile, Sun, Upload, Scaling, WandSparkles } from "lucide-react";
import type { CanvasNodeData } from "@/types/canvas";
import type { NodeToolbarGroup } from "@/lib/canvas/tool-registry";
type ImageNodeActionToolId = "copyPrompt" | "reversePrompt" | "replace" | "resize" | "annotation" | "annotationEdit" | "textEdit" | "maskEdit" | "removeBackground" | "layerDecomposition" | "emotion" | "portraitTexture" | "crop" | "split" | "upscale" | "superResolve" | "angle" | "lighting" | "panorama" | "view";
type ImageNodeActionToolId = "copyPrompt" | "reversePrompt" | "replace" | "resize" | "annotation" | "annotationEdit" | "textEdit" | "maskEdit" | "removeBackground" | "layerDecomposition" | "emotion" | "portraitTexture" | "crop" | "split" | "upscale" | "superResolve" | "angle" | "lighting" | "panorama" | "view" | "multi_camera_nine_grid" | "story_pitch_four_grid" | "character_face_three_view" | "product_three_view" | "storyboard_25_grid" | "character_three_view_generation" | "cinematic_light_correction" | "image_projection_after_3s" | "image_projection_before_5s";
type ImageToolHandlers = {
onUpload: (node: CanvasNodeData) => void;
@@ -26,6 +26,7 @@ type ImageToolHandlers = {
onViewImage: (node: CanvasNodeData) => void;
onCopyPrompt: (node: CanvasNodeData) => void;
onReversePrompt: (node: CanvasNodeData) => void;
onNineGrid: (node: CanvasNodeData, toolId: number, label: string, icon: string) => void;
};
type ImageToolDefinition = {
@@ -37,9 +38,25 @@ type ImageToolDefinition = {
section?: string;
description?: string;
active?: (node: CanvasNodeData) => boolean;
run: (node: CanvasNodeData, handlers: ImageToolHandlers) => void;
run: (node: CanvasNodeData, handlers: ImageToolHandlers, tool: ImageToolDefinition) => void;
/** 仅 group === "nine_grid" 时使用,对应后端工具 ID */
toolId?: number;
};
/** lucide 组件把 PascalCase 图标名写入 displayName,从 icon 渲染结果反查图标名,未命中时回退 Grid3x3。 */
function resolveToolIconName(tool: ImageToolDefinition, node: CanvasNodeData) {
const element = tool.icon(node);
const icon_type = isValidElement(element) ? element.type : null;
const name = icon_type && typeof icon_type !== "string" ? (icon_type as { displayName?: string }).displayName : undefined;
return name || "Grid3x3";
}
/** 九宫格工具共用行为:run 收到自身定义后直接取 toolId/label,图标名从 icon 反查。 */
function nineGridRun(node: CanvasNodeData, handlers: ImageToolHandlers, tool: ImageToolDefinition) {
if (tool.toolId == null) return;
handlers.onNineGrid(node, tool.toolId, resolveToolText(tool.label, node), resolveToolIconName(tool, node));
}
const imageToolDefinitions: ImageToolDefinition[] = [
{
id: "copyPrompt",
@@ -222,6 +239,106 @@ const imageToolDefinitions: ImageToolDefinition[] = [
order: 80,
run: (node, handlers) => handlers.onPanorama(node),
},
// 九宫格工具组——对应后端 nine_grid 种子数据
{
id: "multi_camera_nine_grid",
label: "多机位九宫格",
section: "宫格生成",
description: "生成 3x3 多机位联系表",
icon: () => <Grid3x3 className="size-3.5" />,
group: "nine_grid",
order: 10,
toolId: 79,
run: nineGridRun,
},
{
id: "story_pitch_four_grid",
label: "剧情推演四宫格",
section: "宫格生成",
description: "生成 2x2 剧情推演联系表",
icon: () => <Grid2x2 className="size-3.5" />,
group: "nine_grid",
order: 20,
toolId: 80,
run: nineGridRun,
},
{
id: "character_face_three_view",
label: "角色脸部三视图",
section: "设定图",
description: "生成 3x2 角色脸部联系表",
icon: () => <ScanFace className="size-3.5" />,
group: "nine_grid",
order: 30,
toolId: 81,
run: nineGridRun,
},
{
id: "product_three_view",
label: "产品三视图",
section: "设定图",
description: "生成 3x2 产品联系表",
icon: () => <Package className="size-3.5" />,
group: "nine_grid",
order: 40,
toolId: 82,
run: nineGridRun,
},
{
id: "storyboard_25_grid",
label: "25宫格连贯分镜",
section: "宫格生成",
description: "生成 5x5 连贯分镜联系表",
icon: () => <Clapperboard className="size-3.5" />,
group: "nine_grid",
order: 50,
toolId: 83,
run: nineGridRun,
},
{
id: "character_three_view_generation",
label: "角色三视图",
section: "设定图",
description: "生成 16:9 角色三视图联系表",
icon: () => <PersonStanding className="size-3.5" />,
group: "nine_grid",
order: 60,
toolId: 85,
run: nineGridRun,
},
{
id: "cinematic_light_correction",
label: "电影级光影校正",
section: "光影",
description: "修正电影灯光,使场景更真实",
icon: () => <Contrast className="size-3.5" />,
group: "nine_grid",
order: 70,
toolId: 84,
run: nineGridRun,
},
{
id: "image_projection_after_3s",
label: "画面推演-3秒后",
section: "画面推演",
description: "生成 3 秒后的画面帧",
icon: () => <FastForward className="size-3.5" />,
group: "nine_grid",
order: 80,
toolId: 86,
run: nineGridRun,
},
{
id: "image_projection_before_5s",
label: "画面推演-5秒前",
section: "画面推演",
description: "生成 5 秒前的画面帧",
icon: () => <Rewind className="size-3.5" />,
group: "nine_grid",
order: 90,
toolId: 87,
run: nineGridRun,
},
];
export function buildImageToolbarTools(node: CanvasNodeData, handlers: ImageToolHandlers) {
@@ -234,10 +351,34 @@ export function buildImageToolbarTools(node: CanvasNodeData, handlers: ImageTool
section: tool.section,
description: tool.description,
active: tool.active?.(node),
onClick: () => tool.run(node, handlers),
onClick: () => tool.run(node, handlers, tool),
}));
}
function resolveToolText(value: string | ((node: CanvasNodeData) => string), node: CanvasNodeData) {
return typeof value === "function" ? value(node) : value;
}
export type NineGridMenuItem = {
id: string;
label: string;
section: string;
description: string;
toolId: number;
toolIconName: string;
};
/** 从 imageToolDefinitions 中筛选 nine_grid 组,返回九宫格菜单项数据 */
export function getNineGridMenuItems(): NineGridMenuItem[] {
return imageToolDefinitions
.filter((tool) => tool.group === "nine_grid" && tool.toolId != null)
.sort((a, b) => a.order - b.order)
.map((tool) => ({
id: tool.id,
label: typeof tool.label === "function" ? tool.label({} as CanvasNodeData) : tool.label,
section: tool.section || "常用操作",
description: tool.description || "",
toolId: tool.toolId!,
toolIconName: resolveToolIconName(tool, {} as CanvasNodeData),
}));
}