From b4d2b34faeb6c553f1595a978d7ef76fb674b83e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=88=B1=E7=AC=91=E7=9A=84=E6=AF=9B=E6=AF=9B=E8=99=AB?= <315515767@qq.com> Date: Mon, 3 Aug 2026 21:38:38 +0800 Subject: [PATCH] =?UTF-8?q?feat(canvas):=20=E8=AE=BE=E8=AE=A1token?= =?UTF-8?q?=E7=B3=BB=E7=BB=9F=20-=20=E5=BB=BA=E7=AB=8B=E4=B8=89=E5=B1=82?= =?UTF-8?q?=E6=9E=B6=E6=9E=84=E9=AA=A8=E6=9E=B6=20(#101)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat(canvas): 设计token系统 - 建立三层架构骨架 #94 #96 建立 Primitive→Semantic→Component 三层设计 token 体系,覆盖 9 个维度: 色彩/间距/字号/圆角/阴影/Elevation/动效/描边/不透明度。 - Primitive 层:纯值无语义,含 neutral 色板+节点类型色板+状态色、 4px 栅格间距、10档字号、10档圆角、阴影+glow、12档 z-index、 动效(duration+easing+delay+motion-scale)、描边、不透明度 - Semantic 层:引用 Primitive,随主题切换,含通用语义色、 Canvas 语义(--cn-*)、Aceternity spatial 语义、节点类型语义、状态语义 - Component 层:引用 Semantic,含 Dock/Node/Modal/Panel/Prompt面板/ 进度条/缩放控制专属 token - @theme inline 映射:节点类型色、状态色、Canvas 语义色、字号映射到 Tailwind utility - prefers-reduced-motion 三级降级(L0正常/L1简化/L2禁用) - AGENTS.md 新增 Design Token 使用规范,禁止新增裸 px 值 - 附带修复 canvas-drawing-excalidraw-editor.tsx TS7006 类型注解 - 附带修复 canvas-drawing-editor-modal.tsx TS2339 engine 字段缺失 Wayfinder 决策来源:#95 #96 #97 #98 #99 * fix(canvas): 设计token系统 - 修复CSS变量循环引用和命名兼容性 #94 #96 修复两个问题: 1. CSS变量循环引用:Primitive层 --color-node-*/--color-status-* 与 @theme inline 中的 --color-node-*/--color-status-* 同名,经 Semantic 层中转后形成循环(A→B→A)。改用 --palette- 前缀隔离 Primitive 色板, 打断循环链路。 2. 变量名小数点兼容性:--space-0.5/1.5/2.5/3.5 中的小数点被部分 CSS linter 误判为类选择器。改为 --space-half/-1-half/-2-half/-3-half, 保持语义清晰的同时提升工具链兼容性。 * refactor(canvas): 画布核心组件 - 迁移硬编码值到设计token #94 批次1:5个画布核心组件的裸 text-[Npx]/rounded-[Npx]/z-[N] 替换为 对应设计token,统一视觉语言: - canvas-node.tsx:text-[10/11/9/17px]→fs-tiny/label/micro/heading-lg, rounded-[18px]→node-radius - canvas-node-hover-toolbar.tsx:text-[11/17/10/9px]→fs-*, rounded-[14/9px]→dock-radius/dock-item-radius,z-[70]→z-panel-floating - canvas-toolbar.tsx:text-[9/10/11px]→fs-*, rounded-[17/16/11/10px]→panel-radius/dock-item-radius-*, z-[30/40]→z-toolbar/z-toolbar-floating - canvas-assistant-panel.tsx:text-[8/9/10/11px]→fs-* - canvas-script-node.tsx:text-[9/10/11px]→fs-* 视觉零回归(token值与原硬编码值一致),代码可维护性提升。 * refactor(canvas): 画布辅助组件 - 迁移硬编码值到设计token #94 批次2:6个画布辅助组件的裸 text-[Npx]/rounded-[Npx]/z-[N] 替换为 对应设计token: - canvas-style-picker-modal.tsx:text-[10/11px]→fs-tiny/label - canvas-file-drop-overlay.tsx:z-[110]→z-modal-overlay, rounded-[28/20px]→r-5xl/r-3xl - canvas-character-reference-node.tsx:text-[10px]→fs-tiny - canvas-drawing-editor-modal.tsx:text-[11px]→fs-label - canvas-video-prompt-tools.tsx:z-[1500]→z-toast, rounded-[12/10px]→r-lg/dock-item-radius - asset-picker-modal.tsx:text-[10px]→fs-tiny 视觉零回归(token值与原硬编码值一致)。 * refactor(*): 全项目硬编码值 - 批量迁移到设计token #94 批次3:全项目 .tsx 文件中 372 处裸 text-[Npx]/rounded-[Npx] 批量 替换为对应设计token,剩余 25 处无直接 token 对应的低频值保留。 替换覆盖(按频率): - text-[10px]→fs-tiny (132处) - text-[11px]→fs-label (100处) - text-[9px]→fs-micro (45处) - text-[12px]→fs-caption, text-[13/14px]→fs-body, text-[15px]→fs-body-lg - text-[18px]→fs-heading-lg, text-[22px]→fs-title, text-[8px]→fs-micro - rounded-[9/10px]→dock-item-radius, rounded-[12px]→r-lg - rounded-[14px]→dock-radius, rounded-[16px]→r-2xl - rounded-[17px]→panel-radius, rounded-[8px]→r-md 涉及 components/ 和 pages/ 下所有 .tsx 文件。 视觉零回归(token值与原硬编码值一致)。 * feat(canvas): 节点视觉层次规范 - 色带+状态徽章+四态递进 #97 #94 基于 Wayfinder 地图 #97 决议执行节点视觉重设计: 1. 节点类型色带(决策1) - 新增 nodeTypeColorVar() 函数,10种节点类型对应色带色 - 节点左侧3px色条,引用 --cn-type-* token - 远距离最强信号,用户扫视画布即可识别节点类型 2. 节点状态徽章(决策2) - 新增 NodeStatusBadge 组件,loading/success/error 三态 - 左上角圆角徽章,backdrop-blur 玻璃质感 - success 态 2s 后自动淡出(canvas-status-success-fade 动画) 3. 四态递进视觉权重(决策3) - idle:无额外阴影 - hover:轻微抬升阴影(16px 48px) - selected:2px 边框环(accent.primary 40% 透明度) - selected-primary:4px 软色环 + resize handle - active:6px 外发光环 + inner glow(最强强调) - 120-150ms ease-out 过渡 4. 阴影迁移 - boxShadow 从硬编码改为分层语义化表达 - 边框色 hover 态使用 color-mix 动态加深 Closes #97 --------- Co-authored-by: sikongyue <3155157671@qq.com> --- AGENTS.md | 20 + .../components/canvas/asset-picker-modal.tsx | 2 +- .../canvas/canvas-active-task-panel.tsx | 14 +- .../canvas/canvas-agent-chat-ui.tsx | 10 +- .../canvas/canvas-agent-panel-chrome.tsx | 8 +- .../components/canvas/canvas-asset-tray.tsx | 26 +- .../canvas/canvas-assistant-panel.tsx | 10 +- .../canvas-character-reference-modal.tsx | 8 +- .../canvas-character-reference-node.tsx | 4 +- .../canvas/canvas-config-composer.tsx | 2 +- .../canvas/canvas-config-node-panel.tsx | 8 +- .../components/canvas/canvas-context-menu.tsx | 10 +- .../canvas/canvas-create-command-grid.tsx | 4 +- .../canvas/canvas-drawing-editor-modal.tsx | 2 +- .../canvas/canvas-emotion-workspace.tsx | 8 +- .../canvas/canvas-file-drop-overlay.tsx | 8 +- .../components/canvas/canvas-frame-node.tsx | 6 +- .../canvas-image-toolbar-settings-modal.tsx | 6 +- .../canvas/canvas-local-agent-panel.tsx | 10 +- .../canvas/canvas-node-angle-dialog.tsx | 26 +- .../canvas/canvas-node-emotion-panel.tsx | 28 +- .../canvas/canvas-node-hover-toolbar.tsx | 12 +- .../canvas/canvas-node-prompt-panel.tsx | 26 +- web/src/components/canvas/canvas-node.tsx | 124 ++++-- .../canvas-portrait-texture-popover.tsx | 6 +- .../canvas/canvas-preset-picker.tsx | 6 +- .../canvas/canvas-project-asset-modal.tsx | 8 +- .../components/canvas/canvas-project-card.tsx | 10 +- .../canvas/canvas-project-sidebar.tsx | 16 +- .../components/canvas/canvas-script-node.tsx | 14 +- .../canvas/canvas-short-drama-entry.tsx | 12 +- .../canvas/canvas-style-picker-modal.tsx | 20 +- .../canvas/canvas-text-editor-modal.tsx | 10 +- web/src/components/canvas/canvas-toolbar.tsx | 24 +- .../canvas/canvas-version-compare-modal.tsx | 4 +- .../canvas/canvas-video-prompt-tools.tsx | 4 +- .../canvas/canvas-workspace-overlays.tsx | 16 +- .../canvas/canvas-zoom-controls.tsx | 12 +- .../director/canvas-director-node-panel.tsx | 8 +- .../director/canvas-director-workbench.tsx | 14 +- .../components/image-generation-pending.tsx | 2 +- web/src/components/image-settings-panel.tsx | 4 +- web/src/components/layout/app-top-nav.tsx | 6 +- .../layout/identity-provider-badge.tsx | 2 +- .../layout/system-announcement-center.tsx | 2 +- web/src/components/layout/workspace-page.tsx | 2 +- .../layout/workspace-sidebar-footer.tsx | 6 +- web/src/components/layout/workspace-state.tsx | 2 +- web/src/components/model-picker.tsx | 10 +- .../announcement-timeline-modal.tsx | 6 +- .../ui/aceternity/floating-dock.tsx | 10 +- web/src/components/video-settings-panel.tsx | 16 +- .../pages/admin/components/admin-shell.tsx | 10 +- .../components/channel-model-manager.tsx | 4 +- web/src/pages/admin/logs/logs-page.tsx | 2 +- web/src/pages/assets/index.tsx | 16 +- web/src/pages/auth/auth-scene.tsx | 2 +- .../pages/canvas/canvas-project-feedback.tsx | 14 +- .../canvas/canvas-project-status-dialogs.tsx | 2 +- .../pages/canvas/canvas-project-top-bar.tsx | 14 +- web/src/pages/canvas/shared.tsx | 6 +- web/src/pages/home/index.tsx | 26 +- web/src/pages/not-found/index.tsx | 2 +- web/src/pages/projects/detail.tsx | 6 +- web/src/pages/projects/detail/assets.tsx | 20 +- web/src/pages/projects/detail/canvases.tsx | 8 +- web/src/pages/projects/detail/chapters.tsx | 20 +- web/src/pages/projects/detail/overview.tsx | 22 +- .../detail/project-character-card.tsx | 10 +- web/src/pages/projects/detail/settings.tsx | 6 +- web/src/pages/projects/detail/shared.tsx | 4 +- web/src/pages/projects/index.tsx | 16 +- .../pages/settings/channel-video-pricing.tsx | 8 +- web/src/pages/settings/index.tsx | 2 +- web/src/pages/settings/model-default-grid.tsx | 6 +- web/src/pages/skills/index.tsx | 6 +- web/src/pages/tasks/index.tsx | 18 +- web/src/pages/wallet/index.tsx | 8 +- web/src/styles/globals.css | 352 ++++++++++++++++++ 79 files changed, 844 insertions(+), 400 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index 2527c3dad..aea07a8fb 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -55,6 +55,26 @@ - 图片节点默认保持原始比例;批量生成、多图和助手面板不得长期遮挡主要画布空间。 - 动效服务于状态变化和空间关系,并尊重 `prefers-reduced-motion`;不要添加持续干扰创作的装饰动画。 +## Design Token 系统 + +画布页面使用三层设计 token 架构(Primitive → Semantic → Component),详见 `globals.css` 和 Wayfinder #96。 + +### 三层结构 + +- **Primitive 层**:纯值,无语义,不随主题变。包含 9 个维度:色彩(neutral 色板 + 节点类型色板 + 状态色)、间距(4px 栅格 `--space-0.5` ~ `--space-24`)、字号(10 档 `--fs-micro` ~ `--fs-display`)、圆角(10 档 `--r-xs` ~ `--r-full`)、阴影(`--shadow-xs` ~ `--shadow-2xl` + glow)、Elevation(12 档 `--z-canvas` ~ `--z-max`)、动效(duration 5 档 + easing 5 曲线 + delay 5 档 + motion-scale)、描边(4 档)、不透明度(6 档)。 +- **Semantic 层**:引用 Primitive,随主题切换。包含通用语义色(`--bg`/`--fg`/`--border-semantic`)、Canvas 语义(`--cn-text`/`--cn-muted`/`--cn-stroke` 等)、Aceternity spatial 语义(`--cn-spatial-*`)、节点类型语义(`--cn-type-*`)、状态语义(`--status-*`)。 +- **Component 层**:引用 Semantic,某组件专属。包含 Dock(`--dock-*`)、Node(`--node-*`)、Modal(`--modal-*`)、Panel(`--panel-*`)、Prompt 面板(`--prompt-panel-*`)、进度条(`--progress-bar-*`)、缩放控制(`--zoom-control-*`)。 + +### 使用规则 + +- **禁止新增裸值**:组件中不得新增 `text-[Npx]`、`rounded-[Npx]`、`z-[N]`、`p-[Npx]`、`gap-[Npx]` 等硬编码 Tailwind 任意值。必须使用对应 token。 + - 反例:`className="text-[10px] rounded-[12px] z-[1500]"` + - 正例:`className="text-tiny rounded-[var(--r-lg)] z-[var(--z-modal)]"` +- **CSS 变量优先**:在 inline style 中使用 `var(--token-name)` 引用 token,不写字面值。 +- **Ant Design 同步**:AntD ConfigProvider 从 CSS 变量反序列化初始化,改 token 则 AntD + shadcn 同步变化。 +- **Aceternity 风格**:`canvas-theme.ts` 和 `aceternity-motion.ts` 从 CSS 变量读取,CSS 是单一信源。 +- **新增 token**:如需新增 token,先加到 Primitive 层(纯值),再在 Semantic 层引用,最后在 Component 层定义组件专属值。 + ## 文档 - README 只保留项目定位、核心功能、快速开始、数据说明和文档入口。 diff --git a/web/src/components/canvas/asset-picker-modal.tsx b/web/src/components/canvas/asset-picker-modal.tsx index 5e5b4e8db..f6d678a3e 100644 --- a/web/src/components/canvas/asset-picker-modal.tsx +++ b/web/src/components/canvas/asset-picker-modal.tsx @@ -53,7 +53,7 @@ function PickerCard({ asset, onClick }: { asset: InsertableAsset; onClick: () =>
{title} - {kind === "image" ? "图片" : kind === "video" ? "视频" : kind === "audio" ? "音频" : "文本"} + {kind === "image" ? "图片" : kind === "video" ? "视频" : kind === "audio" ? "音频" : "文本"}
插入
diff --git a/web/src/components/canvas/canvas-active-task-panel.tsx b/web/src/components/canvas/canvas-active-task-panel.tsx index 2bb4c0ea6..58455fd9d 100644 --- a/web/src/components/canvas/canvas-active-task-panel.tsx +++ b/web/src/components/canvas/canvas-active-task-panel.tsx @@ -44,7 +44,7 @@ export function CanvasActiveTaskPanel({ tasks }: { tasks: GenerationTask[] }) { @@ -57,12 +57,12 @@ export function CanvasActiveTaskPanel({ tasks }: { tasks: GenerationTask[] }) { aria-controls="canvas-active-task-list" > - + 生成任务 - 当前画布 · {tasks.length} 个进行中 + 当前画布 · {tasks.length} 个进行中 @@ -127,9 +127,9 @@ function ActiveTaskCard({ task, now, theme, expanded, onToggle, reducedMotion }: {formatTaskKind(task)} - {statusLabel[task.status]} + {statusLabel[task.status]} - {task.stage || statusLabel[task.status]} + {task.stage || statusLabel[task.status]} {expanded ? : } @@ -138,7 +138,7 @@ function ActiveTaskCard({ task, now, theme, expanded, onToggle, reducedMotion }: -
+
{durationLabel} {billingLabel}
@@ -151,7 +151,7 @@ function ActiveTaskCard({ task, now, theme, expanded, onToggle, reducedMotion }: animate={{ opacity: 1, height: "auto" }} exit={{ opacity: 0, height: 0 }} transition={transition} - className="border-t px-3 pb-3 pt-2 text-[11px]" + className="border-t px-3 pb-3 pt-2 text-[var(--fs-label)]" style={{ borderColor: theme.toolbar.border, color: theme.node.muted }} >
diff --git a/web/src/components/canvas/canvas-agent-chat-ui.tsx b/web/src/components/canvas/canvas-agent-chat-ui.tsx index 68dc8471c..e9f900912 100644 --- a/web/src/components/canvas/canvas-agent-chat-ui.tsx +++ b/web/src/components/canvas/canvas-agent-chat-ui.tsx @@ -49,7 +49,7 @@ export function AgentChatMessage({ item, theme, user, onRejectTool, onApproveToo
{item.text}
{item.attachments?.length ? : null} - {item.meta ?
{item.meta}
: null} + {item.meta ?
{item.meta}
: null}
{isUser ? : null}
@@ -69,7 +69,7 @@ export function AgentPendingToolCard({ summary, detail, theme, onReject, onAppro
确认工具调用 - 等待确认 + 等待确认
{summary}
@@ -103,7 +103,7 @@ export function AgentPendingToolCard({ summary, detail, theme, onReject, onAppro } function ImpactMetric({ label, value, attention = false, theme }: { label: string; value: number; attention?: boolean; theme: (typeof canvasThemes)[keyof typeof canvasThemes] }) { - return
{label}
{value}
; + return
{label}
{value}
; } function agentImpactFromDetail(detail: unknown) { @@ -132,7 +132,7 @@ export function AgentToolCard({ title, text, detail, theme }: { title: string; t
{title} - + {state.label} {detail ? 详情 : null} @@ -272,7 +272,7 @@ export function AgentPanelTabs({ value, items, theme, right, o function AgentDetailBlock({ detail, theme }: { detail: unknown; theme: (typeof canvasThemes)[keyof typeof canvasThemes] }) { return ( -
+        
             {JSON.stringify(detail, null, 2)}
         
); diff --git a/web/src/components/canvas/canvas-agent-panel-chrome.tsx b/web/src/components/canvas/canvas-agent-panel-chrome.tsx index 5c857598f..44fddbd69 100644 --- a/web/src/components/canvas/canvas-agent-panel-chrome.tsx +++ b/web/src/components/canvas/canvas-agent-panel-chrome.tsx @@ -38,7 +38,7 @@ export function AgentPanelChrome({
Agent
-
画布协作
+
画布协作
@@ -46,7 +46,7 @@ export function AgentPanelChrome({
-
+
{context.nodeCount} 个节点 {context.selectedCount ? 选中 {context.selectedCount} : 未选择节点} {context.chapterLabel ? {context.chapterLabel}{context.shotLabel ? ` · ${context.shotLabel}` : ""} : null} @@ -67,7 +67,7 @@ export function AgentPanelChrome({ function AgentModeSwitch({ value, theme, onChange }: { value: CanvasAgentMode; theme: CanvasTheme; onChange: (value: CanvasAgentMode) => void }) { return ( -
+
{(["online", "local"] as const).map((item) => { const active = value === item; const Icon = item === "online" ? Globe2 : Laptop; @@ -95,7 +95,7 @@ export function AgentChatEmptyState({ theme, nodeCount, onSelect }: { theme: Can
- {nodeCount} 个节点已就绪 + {nodeCount} 个节点已就绪

从当前画布开始

diff --git a/web/src/components/canvas/canvas-asset-tray.tsx b/web/src/components/canvas/canvas-asset-tray.tsx index 5c7fbbd85..c8d734d3c 100644 --- a/web/src/components/canvas/canvas-asset-tray.tsx +++ b/web/src/components/canvas/canvas-asset-tray.tsx @@ -143,12 +143,12 @@ export function CanvasAssetTray({ assetImages, canvasImages, showLibrary = true,
- + 素材空间 - 拖入画布,或定位已经使用的图片 + 拖入画布,或定位已经使用的图片
setOpen(false)} aria-label="收起素材空间"> @@ -156,14 +156,14 @@ export function CanvasAssetTray({ assetImages, canvasImages, showLibrary = true,
-
+
{showLibrary ? setTab("library")} /> : null} setTab("canvas")} />
@@ -189,7 +189,7 @@ export function CanvasAssetTray({ assetImages, canvasImages, showLibrary = true, )}
-
+
{showLibrary && tab === "library" ? "点击插入 · 拖拽定位" : "点击回到节点"} {activeItems.length} 项
@@ -206,8 +206,8 @@ type CanvasTheme = (typeof canvasThemes)[keyof typeof canvasThemes]; function TrayTabButton({ active, label, theme, onClick }: { active: boolean; label: string; theme: CanvasTheme; onClick: () => void }) { return ( - ); @@ -223,19 +223,19 @@ function AssetTrayRow({ title, imageUrl, storageKey, icon, active = false, dragg whileHover={motionEnabled ? { x: 4, scale: 1.008 } : undefined} whileTap={motionEnabled ? { scale: 0.985 } : undefined} transition={aceternityMotion.spring.dock} - className="group grid h-12 w-full grid-cols-[36px_minmax(0,1fr)_auto_24px] items-center gap-1.5 rounded-[12px] border px-1.5 text-left outline-none focus-visible:ring-2" + className="group grid h-12 w-full grid-cols-[36px_minmax(0,1fr)_auto_24px] items-center gap-1.5 rounded-[var(--r-lg)] border px-1.5 text-left outline-none focus-visible:ring-2" style={{ background: active ? theme.accent.primarySoft : theme.spatial.surface, borderColor: active ? theme.spatial.glowStrong : theme.toolbar.border, color: active ? theme.accent.primary : theme.node.text }} onClick={onClick} onDragStartCapture={onDragStart} > - + {imageUrl ? : } - {title} - {active ? "当前已选择" : draggable ? "拖入画布或点击插入" : "点击定位到画布"} + {title} + {active ? "当前已选择" : draggable ? "拖入画布或点击插入" : "点击定位到画布"} - + {resourceStorageLabel(storageKey)} {icon} @@ -245,7 +245,7 @@ function AssetTrayRow({ title, imageUrl, storageKey, icon, active = false, dragg function TrayEmpty({ text, theme }: { text: string; theme: CanvasTheme }) { return ( -
+
{text}
); diff --git a/web/src/components/canvas/canvas-assistant-panel.tsx b/web/src/components/canvas/canvas-assistant-panel.tsx index 147d742b8..62db18943 100644 --- a/web/src/components/canvas/canvas-assistant-panel.tsx +++ b/web/src/components/canvas/canvas-assistant-panel.tsx @@ -795,7 +795,7 @@ export function CanvasAssistantPanel({ nodes, selectedNodeIds, snapshot, project left={ <> updateConfig("textModel", model)} /> - {cinematicEntryActive ? 影视项目 : null} + {cinematicEntryActive ? 影视项目 : null} } /> @@ -952,13 +952,13 @@ function AssistantHistory({
- {session.id === activeSession?.id ? 当前 : null} + {session.id === activeSession?.id ? 当前 : null}
{session.title}
-
{sessionPreview(session)}
+
{sessionPreview(session)}
- {formatSessionTime(session.updatedAt || session.createdAt)} + {formatSessionTime(session.updatedAt || session.createdAt)} @@ -1057,7 +1057,7 @@ function AssistantReferenceChip({ item, label, onRemove }: { item: CanvasAssista {item.dataUrl ? ( - {label ? {label} : null} + {label ? {label} : null} ) : ( diff --git a/web/src/components/canvas/canvas-character-reference-modal.tsx b/web/src/components/canvas/canvas-character-reference-modal.tsx index 2ad93e66a..1b2e2315d 100644 --- a/web/src/components/canvas/canvas-character-reference-modal.tsx +++ b/web/src/components/canvas/canvas-character-reference-modal.tsx @@ -27,7 +27,7 @@ export function CanvasCharacterReferenceModal({ node, open, onClose }: { node: C
- + 人物三视图 @@ -60,7 +60,7 @@ export function CanvasCharacterReferenceModal({ node, open, onClose }: { node: C

{name}

-

项目角色引用 · 当前版本

+

项目角色引用 · 当前版本

@@ -109,7 +109,7 @@ function DetailSection({ icon, title, children }: { icon: ReactNode; title: stri function DetailRow({ label, value, emphasis = false }: { label: string; value?: string; emphasis?: boolean }) { return ( -
+
{label}
{value || "未设置"}
@@ -118,7 +118,7 @@ function DetailRow({ label, value, emphasis = false }: { label: string; value?: function StatusDot({ icon, ready, readyLabel, emptyLabel }: { icon: ReactNode; ready: boolean; readyLabel: string; emptyLabel: string }) { return ( - + {icon} {ready ? readyLabel : emptyLabel} diff --git a/web/src/components/canvas/canvas-character-reference-node.tsx b/web/src/components/canvas/canvas-character-reference-node.tsx index 3cba65c52..0366af021 100644 --- a/web/src/components/canvas/canvas-character-reference-node.tsx +++ b/web/src/components/canvas/canvas-character-reference-node.tsx @@ -9,7 +9,7 @@ export function CanvasCharacterReferenceNodeContent({ node }: { node: CanvasNode return
{node.metadata?.characterCoverUrl ? {node.metadata?.characterName :
} - 角色引用 + 角色引用
{node.metadata?.characterName || node.title}
@@ -22,5 +22,5 @@ export function CanvasCharacterReferenceNodeContent({ node }: { node: CanvasNode } function State({ icon, ready, label }: { icon: ReactNode; ready: boolean; label: string }) { - return {icon}{label}; + return {icon}{label}; } diff --git a/web/src/components/canvas/canvas-config-composer.tsx b/web/src/components/canvas/canvas-config-composer.tsx index 9870408aa..6b530bc37 100644 --- a/web/src/components/canvas/canvas-config-composer.tsx +++ b/web/src/components/canvas/canvas-config-composer.tsx @@ -168,7 +168,7 @@ export function CanvasConfigComposer({ value, inputs, skillReferences = [], gene
{simpleMode ? "快速生成" : "组装提示词"}
-
{simpleMode ? "已连接素材会自动带入" : "@ 引用已连接素材或已激活技能,发送前自动组装"}
+
{simpleMode ? "已连接素材会自动带入" : "@ 引用已连接素材或已激活技能,发送前自动组装"}
{simpleMode ? null : } diff --git a/web/src/components/canvas/canvas-config-node-panel.tsx b/web/src/components/canvas/canvas-config-node-panel.tsx index fbaf3b8a5..535684f7c 100644 --- a/web/src/components/canvas/canvas-config-node-panel.tsx +++ b/web/src/components/canvas/canvas-config-node-panel.tsx @@ -57,7 +57,7 @@ export function CanvasConfigNodePanel({ node, isRunning, inputSummary, onConfigC
event.stopPropagation()}>
{simpleMode ? "快速生成" : "生成配置"}
- {simpleMode ? 自动配置 :
event.stopPropagation()}> + {simpleMode ? 自动配置 :
event.stopPropagation()}> - @@ -137,7 +137,7 @@ export function CanvasConfigNodePanel({ node, isRunning, inputSummary, onConfigC ) : null} {simpleMode ? ( -
将使用当前默认模型与生成参数
+
将使用当前默认模型与生成参数
) : (
event.stopPropagation()}> onConfigChange(node.id, { model })} capability={mode} onMissingConfig={() => navigateToSettings({ continueCreation: true })} fullWidth /> @@ -197,7 +197,7 @@ function defaultVideoOperation(inputSummary: CanvasConfigNodePanelProps["inputSu function InputChip({ label, value, style }: { label: string; value: string; style: CSSProperties }) { return ( -
+
{label} {value}
diff --git a/web/src/components/canvas/canvas-context-menu.tsx b/web/src/components/canvas/canvas-context-menu.tsx index 75b9c36cf..4f0cbf46b 100644 --- a/web/src/components/canvas/canvas-context-menu.tsx +++ b/web/src/components/canvas/canvas-context-menu.tsx @@ -274,7 +274,7 @@ function AddNodeContextMenu({ parentPosition, workspaceMode, isProjectLinked, re animate={{ opacity: 1, x: 0, scale: 1 }} exit={reducedMotion ? { opacity: 0 } : { opacity: 0, x: left > parentPosition.left ? -4 : 4, scale: 0.98 }} transition={{ duration: aceternityMotion.duration.instant, ease: aceternityMotion.easing.enter }} - className="aceternity-floating-panel fixed z-[181] w-[260px] origin-top overflow-hidden rounded-[14px] border p-2 backdrop-blur-2xl" + className="aceternity-floating-panel fixed z-[181] w-[260px] origin-top overflow-hidden rounded-[var(--dock-radius)] border p-2 backdrop-blur-2xl" style={{ left, top: parentPosition.top, background: theme.spatial.elevated, borderColor: theme.toolbar.border, color: theme.node.text, boxShadow: `0 30px 90px ${theme.spatial.shadow}` }} onContextMenu={(event) => event.preventDefault()} onPointerDown={(event) => event.stopPropagation()} @@ -296,13 +296,13 @@ function MenuHeader({ title, description, onBack }: { title: string; description return (
{onBack ? : null} - {title}{description && description !== title ? {description} : null} + {title}{description && description !== title ? {description} : null}
); } function MenuSection({ label }: { label: string }) { - return
{label}
; + return
{label}
; } function MenuButton({ icon, label, detail, shortcut, badge, chevron = false, active = false, disabled = false, danger = false, onClick }: { icon: ReactNode; label: string; detail?: string; shortcut?: string; badge?: string; chevron?: boolean; active?: boolean; disabled?: boolean; danger?: boolean; onClick?: () => void }) { @@ -317,8 +317,8 @@ function MenuButton({ icon, label, detail, shortcut, badge, chevron = false, act onClick={onClick} > {icon} - {label}{badge ? {badge} : null}{detail ? {detail} : null} - {shortcut ? {shortcut} : null} + {label}{badge ? {badge} : null}{detail ? {detail} : null} + {shortcut ? {shortcut} : null} {chevron ? : null} ); diff --git a/web/src/components/canvas/canvas-create-command-grid.tsx b/web/src/components/canvas/canvas-create-command-grid.tsx index 9228edc8b..c145f3e23 100644 --- a/web/src/components/canvas/canvas-create-command-grid.tsx +++ b/web/src/components/canvas/canvas-create-command-grid.tsx @@ -28,7 +28,7 @@ export function CanvasCreateCommandGrid({ commands, variant = "node" }: { comman transition={aceternityMotion.spring.dock} className={cn( "group relative min-w-0 border border-black/10 bg-white/70 text-center outline-none transition-colors hover:border-black/20 hover:bg-black/5 focus-visible:ring-2 dark:border-white/10 dark:bg-white/[.04] dark:hover:border-white/20 dark:hover:bg-white/8", - variant === "node" ? "flex h-12 flex-col items-center justify-center gap-1 rounded-[10px]" : "flex h-9 items-center justify-center gap-1.5 rounded-[9px] px-2", + variant === "node" ? "flex h-12 flex-col items-center justify-center gap-1 rounded-[var(--dock-item-radius)]" : "flex h-9 items-center justify-center gap-1.5 rounded-[var(--dock-item-radius)] px-2", )} style={{ color: theme.node.text, "--tw-ring-color": theme.node.muted } as CSSProperties} title={command.label} @@ -36,7 +36,7 @@ export function CanvasCreateCommandGrid({ commands, variant = "node" }: { comman onClick={command.onClick} > {command.icon} - {command.label} + {command.label} {command.badge ? {command.badge} : null} ))} diff --git a/web/src/components/canvas/canvas-drawing-editor-modal.tsx b/web/src/components/canvas/canvas-drawing-editor-modal.tsx index 1cf579805..4366818c5 100644 --- a/web/src/components/canvas/canvas-drawing-editor-modal.tsx +++ b/web/src/components/canvas/canvas-drawing-editor-modal.tsx @@ -83,7 +83,7 @@ export function CanvasDrawingEditorModal({ open, projectId, node, onClose, onSav void handleClose()} footer={null} closable={false} destroyOnHidden width="100vw" centered styles={{ body: { padding: 0 }, container: { padding: 0, overflow: "hidden" } }} className="canvas-drawing-editor-modal">
-
{node?.title || "绘图"}{drawingEngineLabel(engine)} · {ready ? "已加载" : "正在加载"}
+
{node?.title || "绘图"}{drawingEngineLabel(engine)} · {ready ? "已加载" : "正在加载"}
diff --git a/web/src/components/canvas/canvas-emotion-workspace.tsx b/web/src/components/canvas/canvas-emotion-workspace.tsx index 4a2bd0f6f..ec168a4ba 100644 --- a/web/src/components/canvas/canvas-emotion-workspace.tsx +++ b/web/src/components/canvas/canvas-emotion-workspace.tsx @@ -207,7 +207,7 @@ function FaceSelectionOverlay({ node, viewport, containerRef, imageWidth, imageH
{ event.preventDefault(); @@ -263,7 +263,7 @@ function FaceSelectionOverlay({ node, viewport, containerRef, imageWidth, imageH onPointerDown={(event) => event.stopPropagation()} onClick={(event) => { event.stopPropagation(); onFaceSelect(face); }} > - {selected ? {selected.name} : null} + {selected ? {selected.name} : null} ); })} @@ -295,8 +295,8 @@ function SelectionToolbar({ node, viewport, containerRef, status, faceCount, err {status === "detecting" ? : } - {label} - {status !== "detecting" ? : null} + {label} + {status !== "detecting" ? : null}
); diff --git a/web/src/components/canvas/canvas-file-drop-overlay.tsx b/web/src/components/canvas/canvas-file-drop-overlay.tsx index c5d0a6811..186c9936d 100644 --- a/web/src/components/canvas/canvas-file-drop-overlay.tsx +++ b/web/src/components/canvas/canvas-file-drop-overlay.tsx @@ -15,15 +15,15 @@ export function CanvasFileDropOverlay({ active, theme }: { active: boolean; them animate={{ opacity: 1 }} exit={{ opacity: 0 }} transition={{ duration: aceternityMotion.duration.state }} - className="pointer-events-none absolute inset-3 z-[110] grid place-items-center overflow-hidden rounded-[28px] border backdrop-blur-2xl" + className="pointer-events-none absolute inset-3 z-[var(--z-modal-overlay)] grid place-items-center overflow-hidden rounded-[var(--r-5xl)] border backdrop-blur-2xl" style={{ background: theme.spatial.dropzone, borderColor: theme.spatial.glowStrong, color: theme.node.text, boxShadow: `inset 0 0 0 1px ${theme.spatial.glow}, 0 30px 100px rgba(0,0,0,.22)` }} >
- - - + + +
diff --git a/web/src/components/canvas/canvas-frame-node.tsx b/web/src/components/canvas/canvas-frame-node.tsx index a3c6e4571..25309d2e1 100644 --- a/web/src/components/canvas/canvas-frame-node.tsx +++ b/web/src/components/canvas/canvas-frame-node.tsx @@ -158,7 +158,7 @@ export const CanvasFrameNode = React.memo(function CanvasFrameNode({ > )} - + {childNodes.length}
@@ -272,7 +272,7 @@ function FramePreview({ nodes, frame, theme }: { nodes: CanvasNodeData[]; frame:
)) ) : ( -
空背板
+
空背板
)}
); diff --git a/web/src/components/canvas/canvas-image-toolbar-settings-modal.tsx b/web/src/components/canvas/canvas-image-toolbar-settings-modal.tsx index c9242d4bf..f7b2e8bb5 100644 --- a/web/src/components/canvas/canvas-image-toolbar-settings-modal.tsx +++ b/web/src/components/canvas/canvas-image-toolbar-settings-modal.tsx @@ -57,7 +57,7 @@ export function ImageToolSettingsModal({ open, tools, selectedIds, showLabels, o >
- + 显示功能名 @@ -69,13 +69,13 @@ export function ImageToolSettingsModal({ open, tools, selectedIds, showLabels, o
-
快捷工具{selectedTools.length}/{maxSelected}
+
快捷工具{selectedTools.length}/{maxSelected}
updateSelectedTools(values as ImageQuickToolId[])}> {tools.map((tool) => ( ))} diff --git a/web/src/components/canvas/canvas-local-agent-panel.tsx b/web/src/components/canvas/canvas-local-agent-panel.tsx index d46ac5130..79483d21a 100644 --- a/web/src/components/canvas/canvas-local-agent-panel.tsx +++ b/web/src/components/canvas/canvas-local-agent-panel.tsx @@ -562,7 +562,7 @@ export const CanvasLocalAgentPanel = memo(function CanvasLocalAgentPanel({ snaps onSubmit={sendPrompt} onAddFiles={addAttachments} onRemoveAttachment={removeAttachment} - left={attachments.length ? {formatBytes(attachmentPayloadBytes(attachments))} / 30MB : null} + left={attachments.length ? {formatBytes(attachmentPayloadBytes(attachments))} / 30MB : null} /> )} @@ -662,7 +662,7 @@ function AgentConnectView({ theme, url, token, enabled, connected, activity, con
网页连接 - + {statusText} @@ -734,13 +734,13 @@ function AgentHistoryView({ theme, threads, activeThreadId, workspacePath, loadi
- {active ? 当前 : null} + {active ? 当前 : null}
{thread.name || thread.preview || "未命名对话"}
-
{thread.preview || thread.id}
+
{thread.preview || thread.id}
- {formatThreadTime(thread.updatedAt || thread.createdAt)} + {formatThreadTime(thread.updatedAt || thread.createdAt)} diff --git a/web/src/components/canvas/canvas-node-angle-dialog.tsx b/web/src/components/canvas/canvas-node-angle-dialog.tsx index b221e4cd4..3ba4b282a 100644 --- a/web/src/components/canvas/canvas-node-angle-dialog.tsx +++ b/web/src/components/canvas/canvas-node-angle-dialog.tsx @@ -54,13 +54,13 @@ export function CanvasNodeAnglePanel({ dataUrl, onClose, onConfirm }: { dataUrl: initial={reducedMotion ? { opacity: 0 } : { opacity: 0, y: 8, scale: 0.97 }} animate={{ opacity: 1, y: 0, scale: 1 }} transition={reducedMotion ? { duration: 0 } : aceternityMotion.spring.panel} - className="aceternity-floating-panel w-[580px] max-w-full overflow-hidden rounded-[16px] border backdrop-blur-2xl" + className="aceternity-floating-panel w-[580px] max-w-full overflow-hidden rounded-[var(--r-2xl)] border backdrop-blur-2xl" style={{ background: theme.spatial.elevated, borderColor: theme.toolbar.border, color: theme.node.text, boxShadow: `0 28px 80px ${theme.spatial.shadow}` }} onMouseDown={(event) => event.stopPropagation()} onPointerDown={(event) => event.stopPropagation()} >
- + 多角度编辑器 @@ -70,40 +70,40 @@ export function CanvasNodeAnglePanel({ dataUrl, onClose, onConfirm }: { dataUrl: {presets.map((preset) => setParams((current) => ({ ...current, horizontalAngle: preset.horizontalAngle, pitchAngle: preset.pitchAngle }))} />)}
-
{ dragRef.current = null; event.currentTarget.releasePointerCapture(event.pointerId); }} onPointerCancel={() => { dragRef.current = null; }}> +
{ dragRef.current = null; event.currentTarget.releasePointerCapture(event.pointerId); }} onPointerCancel={() => { dragRef.current = null; }}> - 角度参考 + 角度参考
- 拖动调整摄影机 + 拖动调整摄影机
-
+
update("horizontalAngle", value)} /> update("pitchAngle", value)} /> update("cameraDistance", value)} />
- 镜头 + 镜头 update("wideAngle", value === "wide")} />
- 当前视角 + 当前视角 {activePreset?.label || "自定义"} - {params.horizontalAngle}° / {params.pitchAngle}° · {params.cameraDistance.toFixed(1)} {distanceLabel(params.cameraDistance)} + {params.horizontalAngle}° / {params.pitchAngle}° · {params.cameraDistance.toFixed(1)} {distanceLabel(params.cameraDistance)} - - onConfirm(params)}>生成新角度 + + onConfirm(params)}>生成新角度
); } function AngleSlider({ label, value, min, max, step = 1, suffix, onChange }: { label: string; value: number; min: number; max: number; step?: number; suffix: string; onChange: (value: number) => void }) { - return
{label}{Number.isInteger(value) ? value : value.toFixed(1)}{suffix.startsWith("°") ? suffix : ""}{!suffix.startsWith("°") ? ` ${suffix}` : ""}
; + return
{label}{Number.isInteger(value) ? value : value.toFixed(1)}{suffix.startsWith("°") ? suffix : ""}{!suffix.startsWith("°") ? ` ${suffix}` : ""}
; } function AnglePresetButton({ active, label, theme, onClick }: { active: boolean; label: string; theme: (typeof canvasThemes)[keyof typeof canvasThemes]; onClick?: () => void }) { - return ; + return ; } function GlobeGrid({ color }: { color: string }) { diff --git a/web/src/components/canvas/canvas-node-emotion-panel.tsx b/web/src/components/canvas/canvas-node-emotion-panel.tsx index 99499c7d5..63ed7d949 100644 --- a/web/src/components/canvas/canvas-node-emotion-panel.tsx +++ b/web/src/components/canvas/canvas-node-emotion-panel.tsx @@ -64,7 +64,7 @@ export function CanvasNodeEmotionPanel({ dataUrl, imageWidth, imageHeight, chara animate={{ opacity: 1, y: 0, scale: 1 }} exit={reducedMotion ? { opacity: 0 } : { opacity: 0, y: 5, scale: 0.98 }} transition={reducedMotion ? { duration: 0 } : aceternityMotion.spring.panel} - className="aceternity-floating-panel w-[580px] max-w-full overflow-hidden rounded-[16px] border backdrop-blur-2xl" + className="aceternity-floating-panel w-[580px] max-w-full overflow-hidden rounded-[var(--r-2xl)] border backdrop-blur-2xl" style={{ background: theme.spatial.elevated, borderColor: theme.toolbar.border, color: theme.node.text, boxShadow: `0 28px 80px ${theme.spatial.shadow}` }} >
@@ -75,16 +75,16 @@ export function CanvasNodeEmotionPanel({ dataUrl, imageWidth, imageHeight, chara type="button" layout whileTap={reducedMotion ? undefined : { scale: 0.96 }} - className="relative flex h-8 shrink-0 items-center gap-1.5 rounded-[9px] border px-1.5 pr-2 text-[11px] font-medium outline-none" + className="relative flex h-8 shrink-0 items-center gap-1.5 rounded-[var(--dock-item-radius)] border px-1.5 pr-2 text-[var(--fs-label)] font-medium outline-none" style={{ background: activeCharacterId === character.id ? theme.toolbar.activeBg : theme.spatial.surface, borderColor: activeCharacterId === character.id ? theme.spatial.glowStrong : theme.toolbar.border }} onClick={() => onSelectCharacter(character.id)} > - {activeCharacterId === character.id ? : null} + {activeCharacterId === character.id ? : null} {character.name} ))} -
@@ -97,17 +97,17 @@ export function CanvasNodeEmotionPanel({ dataUrl, imageWidth, imageHeight, chara
- 情绪定位 + 情绪定位 {preset.label} - {error ? {error} : } + {error ? {error} : } @@ -144,11 +144,11 @@ function EmotionPad({ preset, onChange }: { preset: CanvasEmotionPreset; onChang const selectedColumn = 2 - preset.intimacy; const selectedRow = 2 - preset.arousal; return ( -
- 激动 - 平静 - 亲近 - 疏离 +
+ 激动 + 平静 + 亲近 + 疏离
state.theme)]; return ( -
+
@@ -202,7 +202,7 @@ function EmotionHeadPreview({ preset }: { preset: CanvasEmotionPreset }) {
- 实时预览 · {preset.label} + 实时预览 · {preset.label}
); diff --git a/web/src/components/canvas/canvas-node-hover-toolbar.tsx b/web/src/components/canvas/canvas-node-hover-toolbar.tsx index d46f01b28..269143c70 100644 --- a/web/src/components/canvas/canvas-node-hover-toolbar.tsx +++ b/web/src/components/canvas/canvas-node-hover-toolbar.tsx @@ -314,7 +314,7 @@ export function CanvasNodeHoverToolbar({ <>
onKeep(node.id)} onMouseLeave={() => { @@ -323,7 +323,7 @@ export function CanvasNodeHoverToolbar({ onMouseDown={(event) => event.stopPropagation()} onPointerDown={(event) => event.stopPropagation()} > -
+
{dockItems.length ? : null} {hasImage && !simpleMode ? ( ) : null} @@ -433,7 +433,7 @@ export function CanvasNodeInfoModal({ node, open, onClose, onMetadataChange, rea const title = (
-
节点信息
+
节点信息
{node ?
{node.id}
: null}
saveAssetCategory(option.value)} className="h-7 rounded-md border px-2 text-xs font-medium transition-colors disabled:cursor-not-allowed disabled:opacity-60" style={{ borderColor: active ? theme.accent.primary : theme.toolbar.border, background: active ? theme.accent.primarySoft : theme.toolbar.panel, color: active ? theme.accent.primary : theme.node.muted }}>{option.label}; })}
-
生成后会按此分类进入项目资产;角色、场景和画风工作流会自动预填。
+
生成后会按此分类进入项目资产;角色、场景和画风工作流会自动预填。
) : null} {node.metadata?.prompt ? ( diff --git a/web/src/components/canvas/canvas-node-prompt-panel.tsx b/web/src/components/canvas/canvas-node-prompt-panel.tsx index a1d75fae7..84d612490 100644 --- a/web/src/components/canvas/canvas-node-prompt-panel.tsx +++ b/web/src/components/canvas/canvas-node-prompt-panel.tsx @@ -148,7 +148,7 @@ export function CanvasNodePromptPanel({ node, isRunning, onPromptChange, onConfi - {modeDisplayName(mode)}创作 + {modeDisplayName(mode)}创作
)} {!simpleMode ? ( @@ -182,12 +182,12 @@ export function CanvasNodePromptPanel({ node, isRunning, onPromptChange, onConfi const renderComposerControls = (expanded: boolean) => simpleMode ? (
- + {activeReferenceCount ? `已连接 ${activeReferenceCount} 个素材` : "将使用默认模型与参数"} ))} @@ -379,8 +379,8 @@ function ReferenceThumbnail({ reference }: { reference: CanvasResourceReference function GenerationCostBadge({ credits, theme }: { credits: number | null; theme: CanvasTheme }) { if (credits === null) return null; return ( - - + + {credits.toLocaleString()} ); diff --git a/web/src/components/canvas/canvas-node.tsx b/web/src/components/canvas/canvas-node.tsx index c7fb6cf78..224be03fe 100644 --- a/web/src/components/canvas/canvas-node.tsx +++ b/web/src/components/canvas/canvas-node.tsx @@ -1,6 +1,6 @@ import React, { useCallback, useEffect, useMemo, useRef, useState } from "react"; import type { ReactNode } from "react"; -import { BookOpenCheck, ChevronRight, Clapperboard, Clock3, FileText, Image as ImageIcon, LoaderCircle, Lock, Maximize2, Music2, Pencil, Play, Plus, RefreshCw, Replace, Settings2, Square, Star, Type, Video } from "lucide-react"; +import { AlertCircle, BookOpenCheck, CheckCircle2, ChevronRight, Clapperboard, Clock3, FileText, Image as ImageIcon, LoaderCircle, Lock, Maximize2, Music2, Pencil, Play, Plus, RefreshCw, Replace, Settings2, Square, Star, Type, Video } from "lucide-react"; import { canvasThemes } from "@/lib/canvas-theme"; import { CometCard } from "@/components/ui/aceternity/comet-card"; @@ -320,7 +320,7 @@ export const CanvasNode = React.memo(function CanvasNode({ /> onMouseDown(event, data.id)} onDoubleClick={(event) => { @@ -376,6 +385,16 @@ export const CanvasNode = React.memo(function CanvasNode({ } as React.CSSProperties } > + {/* 节点类型色带(对应 #97 决策1:左侧3px色条,远距离最强信号)*/} +
+ {/* 节点状态徽章(对应 #97 决策2:左上角 loading/success/error,近距离确认信号)*/} + {data.metadata?.status && data.metadata.status !== "idle" && data.type !== CanvasNodeType.Frame ? ( + + ) : null} event.stopPropagation()} @@ -560,7 +579,7 @@ function DrawingContent({ node, theme, drawingProjectId }: NodeContentRendererPr
{node.title || "绘图"}
-
{shapeCount} 个图形 · {pageCount} 个页面
+
{shapeCount} 个图形 · {pageCount} 个页面
@@ -576,10 +595,10 @@ function LoadingContent({ node, theme, onCancelTask, onOpenTaskDetails }: Pick
- {node.metadata?.taskStage || (taskId ? "任务处理中" : "正在创建任务")} + {node.metadata?.taskStage || (taskId ? "任务处理中" : "正在创建任务")} {taskId ? (
-
+
{statusLabel} {progress !== null ? ` · ${progress}%` : ""}
@@ -588,12 +607,12 @@ function LoadingContent({ node, theme, onCancelTask, onOpenTaskDetails }: Pick
) : null} -
+
{elapsed} · {shortTaskId(taskId)}
- - + +
) : null} @@ -635,7 +654,7 @@ function ErrorContent({ node, theme, onRetry }: Pick
{generationErrorMessage(node.metadata?.errorDetails)}
{moderationFailure ? ( -
+
修改节点提示词后,可重新点击生成。
) : ( @@ -724,7 +743,7 @@ function SkillContent({ node, theme }: NodeContentRendererProps) {
{skill?.name || node.title || "技能"}
-
+
{skillCategoryLabel(skill?.category)} · {skillOutputModeLabel(skill?.outputMode)} @@ -749,10 +768,10 @@ function SkillContent({ node, theme }: NodeContentRendererProps) {
{tags.length ? tags.map((tag) => ( - + {tag} - )) : 连接到图片、视频、音频或文本节点后生效} + )) : 连接到图片、视频、音频或文本节点后生效}
); @@ -775,7 +794,7 @@ function skillOutputModeLabel(mode?: string) { function ResourceLabelBadge({ reference, theme }: { reference: CanvasResourceReference; theme: CanvasTheme }) { return ( - + {reference.label} ); @@ -785,7 +804,7 @@ function ResourceStorageBadge({ storageKey, active, theme }: { storageKey?: stri const location = resourceStorageLocation(storageKey); const background = active ? (location === "local" ? "rgba(245,158,11,.9)" : theme.accent.primary) : "rgba(0,0,0,.35)"; return ( - + {resourceStorageLabel(storageKey)} ); @@ -797,7 +816,7 @@ function NodeLockBadge({ theme }: { theme: CanvasTheme }) { function BatchToggleBadge({ count, expanded, theme, onToggle }: { count: number; expanded: boolean; theme: CanvasTheme; onToggle: () => void }) { return ( - @@ -806,7 +825,7 @@ function BatchToggleBadge({ count, expanded, theme, onToggle }: { count: number; function BatchPrimaryBadge({ visible, selected, theme, onSelect }: { visible: boolean; selected: boolean; theme: CanvasTheme; onSelect: () => void }) { return ( - @@ -819,7 +838,7 @@ function AssetTagBadges({ tags, theme }: { tags: string[]; theme: (typeof canvas {tags.map((tag, index) => ( {tag.trim()} @@ -871,9 +890,9 @@ function EmptyImageContent({ node, theme, isBatchRoot, batchCount, batchExpanded {isCharacterReference ? (
{node.metadata?.characterName || node.title}
-
多视角参考 · 待生成
+
多视角参考 · 待生成
- ) : 空图片节点} + ) : 空图片节点}
); if (isBatchRoot) @@ -898,7 +917,7 @@ function VideoNodeContent({ node, theme, reduceMediaEffects }: NodeContentRender if (!url) { return : } label={loading ? "正在缓存视频" : "加载并缓存视频"} disabled={loading} onClick={() => { playWhenReadyRef.current = true; void load(); }} />; } - return { playWhenReadyRef.current = false; }} brandColor={theme.accent.primary} className="h-full w-full rounded-[18px] bg-black" dataCanvasNoZoom compactControls />; + return { playWhenReadyRef.current = false; }} brandColor={theme.accent.primary} className="h-full w-full rounded-[var(--node-radius)] bg-black" dataCanvasNoZoom compactControls />; } function AudioNodeContent({ node, theme }: NodeContentRendererProps) { @@ -956,7 +975,7 @@ function ImageContent({ return ( -
+
{url ? ( void }) { return ( - @@ -1063,7 +1082,7 @@ function ImageInfoBar({ node }: { node: CanvasNodeData }) { const height = Math.round(node.metadata?.naturalHeight || node.height); const size = formatBytes(node.metadata?.bytes || 0); return ( - + {width} x {height} {size ? ` · ${size}` : ""} @@ -1185,6 +1204,59 @@ function nodeTypeIcon(type: CanvasNodeType) { return Type; } +// 节点类型色带色(对应 #97 决策1 色带方案,引用 --cn-type-* token) +function nodeTypeColorVar(type: CanvasNodeType, isCharacter?: boolean): string { + if (isCharacter) return "var(--cn-type-character)"; + if (type === CanvasNodeType.Image) return "var(--cn-type-image)"; + if (type === CanvasNodeType.Video) return "var(--cn-type-video)"; + if (type === CanvasNodeType.Audio) return "var(--cn-type-audio)"; + if (type === CanvasNodeType.Drawing) return "var(--cn-type-drawing)"; + if (type === CanvasNodeType.Script) return "var(--cn-type-script)"; + if (type === CanvasNodeType.Config) return "var(--cn-type-config)"; + if (type === CanvasNodeType.Skill) return "var(--cn-type-skill)"; + if (type === CanvasNodeType.Frame) return "var(--cn-type-frame)"; + return "var(--cn-type-text)"; +} + +// 节点状态徽章(对应 #97 决策2:左上角状态指示,loading/success/error) +function NodeStatusBadge({ status }: { status: "loading" | "success" | "error" }) { + if (status === "loading") { + return ( +
+ + 生成中 +
+ ); + } + if (status === "error") { + return ( +
+ + 失败 +
+ ); + } + // success:短暂闪现后淡出(由 CSS animation 控制,2s 后消失) + return ( +
+ + 完成 +
+ ); +} + function ConnectionSideRail({ side, scale, visible, theme, onPointerDown }: { side: "left" | "right"; scale: number; visible: boolean; theme: CanvasTheme; onPointerDown: (event: React.PointerEvent, anchorRatio: number) => void }) { const handleRef = useRef(null); const anchorRatioRef = useRef(0.5); diff --git a/web/src/components/canvas/canvas-portrait-texture-popover.tsx b/web/src/components/canvas/canvas-portrait-texture-popover.tsx index 8848143b2..9428e28fd 100644 --- a/web/src/components/canvas/canvas-portrait-texture-popover.tsx +++ b/web/src/components/canvas/canvas-portrait-texture-popover.tsx @@ -33,7 +33,7 @@ export function CanvasPortraitTexturePopover({ value, placement = "topLeft", onC
{PORTRAIT_TEXTURE_GROUPS.map((group) => (
- {group.label} + {group.label}
{group.options.map((option) => { const selected = settings[group.key] === option.value; @@ -43,7 +43,7 @@ export function CanvasPortraitTexturePopover({ value, placement = "topLeft", onC type="button" role="radio" aria-checked={selected} - className="h-7 min-w-0 rounded-md border px-1 text-[10px] transition-colors focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-1 motion-reduce:transition-none" + className="h-7 min-w-0 rounded-md border px-1 text-[var(--fs-tiny)] transition-colors focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-1 motion-reduce:transition-none" style={{ background: selected ? theme.accent.primarySoft : theme.toolbar.itemHover, borderColor: selected ? theme.accent.primary : "transparent", @@ -78,7 +78,7 @@ export function CanvasPortraitTexturePopover({ value, placement = "topLeft", onC aria-label="打开人物质感调节面板" > - 人物质感调节 + 人物质感调节 ); diff --git a/web/src/components/canvas/canvas-preset-picker.tsx b/web/src/components/canvas/canvas-preset-picker.tsx index ef1425fa9..a6d19d54e 100644 --- a/web/src/components/canvas/canvas-preset-picker.tsx +++ b/web/src/components/canvas/canvas-preset-picker.tsx @@ -104,9 +104,9 @@ export function CanvasPresetPicker({ mode, skillReferences = [], open, onOpenCha {preset.name} - {preset.source === "skill" ? "技能" : "预设"} + {preset.source === "skill" ? "技能" : "预设"} - {preset.description} + {preset.description} )) :
没有匹配的预设
} @@ -118,7 +118,7 @@ export function CanvasPresetPicker({ mode, skillReferences = [], open, onOpenCha ); diff --git a/web/src/components/canvas/canvas-project-asset-modal.tsx b/web/src/components/canvas/canvas-project-asset-modal.tsx index 4e971e102..37b265f3a 100644 --- a/web/src/components/canvas/canvas-project-asset-modal.tsx +++ b/web/src/components/canvas/canvas-project-asset-modal.tsx @@ -44,12 +44,12 @@ export function CanvasProjectAssetModal({ open, detail, initialCategory = "all", return
-

引用项目角色与资产

已选 {selectedIds.size} 项
+

引用项目角色与资产

已选 {selectedIds.size} 项
- +
{visible.length ?
{visible.map((item) => toggle(item.id)} />)}
: }
-
角色引用会在生成时解析当前角色版本
+
角色引用会在生成时解析当前角色版本
; } @@ -62,7 +62,7 @@ function ProjectAssetCard({ item, selected, onToggle }: { item: ProjectPickerIte const Icon = character ? UserRound : media?.kind === "video" ? Video : media?.kind === "audio" ? Music2 : media?.kind === "text" ? FileText : ImageIcon; const label = character ? "角色卡" : media?.kind === "video" ? "视频" : media?.kind === "audio" ? "音频" : media?.kind === "text" ? "文本" : "图片"; const title = character?.title || media?.title || "未命名资产"; - return
{title}
{character ?
{character.character?.visualStatus === "ready" ? "形象就绪" : "形象待完善"} · {character.character?.voiceStatus === "ready" ? "声音已绑定" : "声音未绑定"}
: null}
; + return
{title}
{character ?
{character.character?.visualStatus === "ready" ? "形象就绪" : "形象待完善"} · {character.character?.voiceStatus === "ready" ? "声音已绑定" : "声音未绑定"}
: null}
; } function toInsertPayload(item: ProjectPickerItem): InsertAssetPayload { diff --git a/web/src/components/canvas/canvas-project-card.tsx b/web/src/components/canvas/canvas-project-card.tsx index 5dd137453..ab2155777 100644 --- a/web/src/components/canvas/canvas-project-card.tsx +++ b/web/src/components/canvas/canvas-project-card.tsx @@ -45,7 +45,7 @@ export function CanvasProjectCard({ project, projectName, variant = "library" }: {!compact ? event.stopPropagation()}> toggleSelected(project.id, event.target.checked)} className="app-canvas-project-checkbox size-3.5" aria-label={`选择 ${project.title}`} /> : null} - {project.nodes.length} 节点 + {project.nodes.length} 节点
@@ -61,7 +61,7 @@ export function CanvasProjectCard({ project, projectName, variant = "library" }: open(); }} > -

{project.title}

+

{project.title}

)} {editing ? ( @@ -88,12 +88,12 @@ export function CanvasProjectCard({ project, projectName, variant = "library" }:
)}
-
+
{projectName || "自由画布"} {project.connections.length} 条连线
-

更新于 {formatProjectTime(project.updatedAt)}

+

更新于 {formatProjectTime(project.updatedAt)}

); @@ -128,7 +128,7 @@ function ProjectPreview({ project }: { project: CanvasProject }) { return ( {presentation.icon} - {node.title || presentation.label} + {node.title || presentation.label} ); })} diff --git a/web/src/components/canvas/canvas-project-sidebar.tsx b/web/src/components/canvas/canvas-project-sidebar.tsx index d2ad01e81..93c2c2129 100644 --- a/web/src/components/canvas/canvas-project-sidebar.tsx +++ b/web/src/components/canvas/canvas-project-sidebar.tsx @@ -133,17 +133,17 @@ export function CanvasProjectSidebar({ projectId, detail, onAddChapter, onLocate
-
项目画风
+
项目画风
- 剧情章节 {orderedUnits.length.toLocaleString("zh-CN")} + 剧情章节 {orderedUnits.length.toLocaleString("zh-CN")} {selectedId ? : null}
@@ -152,7 +152,7 @@ export function CanvasProjectSidebar({ projectId, detail, onAddChapter, onLocate setQuery(event.target.value)} onKeyDown={(event) => { if (event.key === "Enter") selectFirstSearchResult(); }} className="min-w-0 flex-1 bg-transparent text-xs outline-none placeholder:text-foreground/28" placeholder="搜索标题或输入章节序号" aria-label="搜索项目章节" /> {query ? : null} - {deferredQuery ?
找到 {visibleUnits.length.toLocaleString("zh-CN")} 章
: null} + {deferredQuery ?
找到 {visibleUnits.length.toLocaleString("zh-CN")} 章
: null}
@@ -168,7 +168,7 @@ export function CanvasProjectSidebar({ projectId, detail, onAddChapter, onLocate
{ const payload: CanvasProjectChapterPayload = { id: unit.id, projectId, title: unit.title, position: unit.position }; event.dataTransfer.setData(CANVAS_PROJECT_CHAPTER_DND_TYPE, JSON.stringify(payload)); event.dataTransfer.effectAllowed = "copy"; }} className={`group flex h-9 cursor-grab items-center gap-1 rounded-md px-1 text-xs active:cursor-grabbing ${active ? "bg-foreground/[.08]" : "hover:bg-foreground/[.045]"}`}> @@ -182,7 +182,7 @@ export function CanvasProjectSidebar({ projectId, detail, onAddChapter, onLocate
- +
{selectedUnit ? unit.id === selectedUnit.id) + 1} loading={selectedUnitQuery.isLoading} loadError={selectedUnitQuery.isError} adding={addingId === selectedUnit.id} onAdd={() => insertChapter(selectedUnit, selectedUnitQuery.data?.unit.sourceText)} onClose={() => setSelectedId("")} /> : null} @@ -194,8 +194,8 @@ function ChapterPreview({ projectId, unit, chapterNumber, loading, loadError, ad const text = htmlToPlainText(unit.sourceText); return (
-
第 {chapterNumber.toLocaleString("zh-CN")} 章

{unit.title}

-

{loading ? "正在读取本章正文…" : loadError ? "正文读取失败,请稍后重试" : text || "本章还没有正文"}

{!loading && !loadError ?
{text.length.toLocaleString("zh-CN")} 字
: null}
+
第 {chapterNumber.toLocaleString("zh-CN")} 章

{unit.title}

+

{loading ? "正在读取本章正文…" : loadError ? "正文读取失败,请稍后重试" : text || "本章还没有正文"}

{!loading && !loadError ?
{text.length.toLocaleString("zh-CN")} 字
: null}
打开编辑器
); diff --git a/web/src/components/canvas/canvas-script-node.tsx b/web/src/components/canvas/canvas-script-node.tsx index 7e072910b..b34c94a7c 100644 --- a/web/src/components/canvas/canvas-script-node.tsx +++ b/web/src/components/canvas/canvas-script-node.tsx @@ -127,7 +127,7 @@ export function CanvasScriptNodeContent({ node, batch, pipeline, scale, mentionR
{node.title || "分镜脚本"} - {batchSummary ? {batchSummary} : taskFeedback ? {taskFeedback} : null} + {batchSummary ? {batchSummary} : taskFeedback ? {taskFeedback} : null} {rows.length} 镜 · {totalDuration}s {batch ? <> {hasFailedBatchItems ? : null} @@ -167,7 +167,7 @@ export function CanvasScriptNodeContent({ node, batch, pipeline, scale, mentionR > {rows.length ? rows.map((row) => (
-
{row.shotNumber}{batchItemByRowId.get(row.id) ? {generationBatchItemLabel(batchItemByRowId.get(row.id)!)} : null}
+
{row.shotNumber}{batchItemByRowId.get(row.id) ? {generationBatchItemLabel(batchItemByRowId.get(row.id)!)} : null}
onUpdateRow(row.id, { durationSeconds: Math.max(1, row.durationSeconds - 1) })}> {row.durationSeconds}s @@ -213,7 +213,7 @@ export function CanvasScriptNodeContent({ node, batch, pipeline, scale, mentionR
- {simpleMode ? 自动拆分 · 默认时长 : + {simpleMode ? 自动拆分 · 默认时长 : className="min-w-32" size="small" value={shotCount} @@ -328,10 +328,10 @@ function PipelineStageCell({ label, stage, theme, children, last = false }: { la return (
-
{label}
-
{pipelineStatusLabel(stage)}
+
{label}
+
{pipelineStatusLabel(stage)}
-
{children}
+
{children}
); } diff --git a/web/src/components/canvas/canvas-short-drama-entry.tsx b/web/src/components/canvas/canvas-short-drama-entry.tsx index 695876d29..f504f2599 100644 --- a/web/src/components/canvas/canvas-short-drama-entry.tsx +++ b/web/src/components/canvas/canvas-short-drama-entry.tsx @@ -12,11 +12,11 @@ export function CanvasLinkedProjectEmptyState({ projectName, hasChapter, onAddFi return (
-

{projectName}

项目画布为空

+

{projectName}

项目画布为空

- - - + + +
@@ -138,14 +138,14 @@ export function CanvasShortDramaGuide({ progress, collapsed, onToggle, onSkip, o } as CSSProperties} onClick={() => onStepClick(step.id)} > - {step.status === "completed" ? : index + 1} + {step.status === "completed" ? : index + 1} {step.label} ))}
- {!progress.completed ? : null} + {!progress.completed ? : null}
); diff --git a/web/src/components/canvas/canvas-style-picker-modal.tsx b/web/src/components/canvas/canvas-style-picker-modal.tsx index bbc33f388..d8229e93f 100644 --- a/web/src/components/canvas/canvas-style-picker-modal.tsx +++ b/web/src/components/canvas/canvas-style-picker-modal.tsx @@ -373,10 +373,10 @@ export function CanvasStylePickerModal({ open, value, onClose, onSelect }: { ope
-
选择项目画风{canvasStylePresets.length} 套视觉预设
-
画风会统一角色三视图、项目资产、分镜和视频提示词。
+
选择项目画风{canvasStylePresets.length} 套视觉预设
+
画风会统一角色三视图、项目资产、分镜和视频提示词。
- 先看规范,再做选择 + 先看规范,再做选择
{canvasStylePresets.map((preset) => { @@ -386,12 +386,12 @@ export function CanvasStylePickerModal({ open, value, onClose, onSelect }: { ope @@ -399,7 +399,7 @@ export function CanvasStylePickerModal({ open, value, onClose, onSelect }: { ope ); })}
-
图片是风格示意,实际效果取决于模型、角色参考图与分镜提示词。
+
图片是风格示意,实际效果取决于模型、角色参考图与分镜提示词。
setDetailPreset(null)} onSelect={(preset) => { setDetailPreset(null); onSelect(preset); }} /> @@ -418,15 +418,15 @@ export function CanvasStyleDetailModal({ open, preset, selected = false, onClose
-
{preset.category}
+
{preset.category}

{preset.title}

{preset.description}

-
{preset.tags.map((tag) => {tag})}
+
{preset.tags.map((tag) => {tag})}
{sections.map((section) =>

{section.title}

{section.content}

)}
-
角色三视图会自动提取其中的色彩、造型和材质规范
{onSelect ? : null}
+
角色三视图会自动提取其中的色彩、造型和材质规范
{onSelect ? : null}
: null} diff --git a/web/src/components/canvas/canvas-text-editor-modal.tsx b/web/src/components/canvas/canvas-text-editor-modal.tsx index 3e7e6a1d6..5bd6a56be 100644 --- a/web/src/components/canvas/canvas-text-editor-modal.tsx +++ b/web/src/components/canvas/canvas-text-editor-modal.tsx @@ -51,7 +51,7 @@ export function CanvasTextEditorModal({ node, open, onClose, onSave }: CanvasTex content: emptyTextDocument(), editorProps: { attributes: { - class: "min-h-full px-8 py-7 text-[15px] leading-7 outline-none sm:px-12 lg:px-16", + class: "min-h-full px-8 py-7 text-[var(--fs-body-lg)] leading-7 outline-none sm:px-12 lg:px-16", "aria-label": "文本节点富文本编辑区", }, }, @@ -129,8 +129,8 @@ export function CanvasTextEditorModal({ node, open, onClose, onSave }: CanvasTex placeholder="文本节点标题" aria-label="文本节点标题" /> - {characterCount.toLocaleString("zh-CN")} 字 · {wordCount.toLocaleString("zh-CN")} 词 - {dirty ? "有未保存修改" : "已保存"} + {characterCount.toLocaleString("zh-CN")} 字 · {wordCount.toLocaleString("zh-CN")} 词 + {dirty ? "有未保存修改" : "已保存"}
-