+
{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 (
-
+
- 实时预览 · {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 ? (
- {showDockLabels ? 更多 : null}
+ {showDockLabels ? 更多 : null}
) : 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} 个素材` : "将使用默认模型与参数"}
- onConfigChange(node.id, { model })} capability={mode} onMissingConfig={() => navigateToSettings({ continueCreation: true })} showSelectedPrice={false} />
+ onConfigChange(node.id, { model })} capability={mode} onMissingConfig={() => navigateToSettings({ continueCreation: true })} showSelectedPrice={false} />
{mode === "image" ? (
onConfigChange(node.id, key === "count" ? { count: Number(value) || 1 } : { [key]: value })}
onMissingConfig={() => navigateToSettings({ continueCreation: true })}
onOpenChange={expanded ? undefined : onImageSettingsOpenChange}
/>
) : mode === "video" ? (
- onConfigChange(node.id, videoConfigPatch(key, value))} />
+ onConfigChange(node.id, videoConfigPatch(key, value))} />
) : mode === "audio" ? (
- onConfigChange(node.id, audioConfigPatch(key, value))} />
+ onConfigChange(node.id, audioConfigPatch(key, value))} />
) : null}
{icon}
@@ -355,7 +355,7 @@ function ConnectedReferenceShelf({ references, theme, onInsert }: { references:
- {index + 1}
+ {index + 1}
))}
@@ -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)}
- event.stopPropagation()} onClick={(event) => { event.stopPropagation(); onOpenTaskDetails?.(node); }}>详情
- event.stopPropagation()} onClick={(event) => { event.stopPropagation(); onCancelTask?.(node); }}>取消
+ event.stopPropagation()} onClick={(event) => { event.stopPropagation(); onOpenTaskDetails?.(node); }}>详情
+ event.stopPropagation()} onClick={(event) => { event.stopPropagation(); onCancelTask?.(node); }}>取消
) : 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 (
- { event.stopPropagation(); onToggle(); }} onMouseDown={(event) => event.stopPropagation()} onPointerDown={(event) => event.stopPropagation()}>
+ { event.stopPropagation(); onToggle(); }} onMouseDown={(event) => event.stopPropagation()} onPointerDown={(event) => event.stopPropagation()}>
{count}
@@ -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 (
- { event.stopPropagation(); onSelect(); }} onMouseDown={(event) => event.stopPropagation()} onPointerDown={(event) => event.stopPropagation()}>
+ { event.stopPropagation(); onSelect(); }} onMouseDown={(event) => event.stopPropagation()} onPointerDown={(event) => event.stopPropagation()}>
{selected ? "当前主图" : "主图"}
@@ -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 (
-
{ event.stopPropagation(); onClick(); }} onMouseDown={(event) => event.stopPropagation()} onPointerDown={(event) => event.stopPropagation()}>
+ { event.stopPropagation(); onClick(); }} onMouseDown={(event) => event.stopPropagation()} onPointerDown={(event) => event.stopPropagation()}>
{icon}
{label}
@@ -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
- {compact ? null : 预设}
+ {compact ? null : 预设}
);
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
{cover ?

:
} />}{label}{title}
{character ?
{character.character?.visualStatus === "ready" ? "形象就绪" : "形象待完善"} · {character.character?.voiceStatus === "ready" ? "声音已绑定" : "声音未绑定"}
: null}
;
+ return
{cover ?

:
} />}{label}{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
-
+
{style ?
: }
- {style?.title || "尚未设置画风"}{style ? "随项目设置自动同步" : "前往项目设置选择"}
+ {style?.title || "尚未设置画风"}{style ? "随项目设置自动同步" : "前往项目设置选择"}
{style ? : null}
- 剧情章节 {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 ?
setQuery("")} className="grid size-5 shrink-0 place-items-center rounded text-foreground/32 hover:bg-foreground/[.06] hover:text-foreground" aria-label="清空章节搜索"> : 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]"}`}>
setSelectedId(unit.id)} className="flex min-w-0 flex-1 items-center gap-1.5 text-left focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-[var(--workspace-accent)]" aria-label={`预览第 ${index + 1} 章 ${unit.title}`}>
- {String(index + 1).padStart(Math.max(2, String(orderedUnits.length).length), "0")}
+ {String(index + 1).padStart(Math.max(2, String(orderedUnits.length).length), "0")}
{unit.title}
void insertChapter(unit)}>{addingId === unit.id ? : }
@@ -182,7 +182,7 @@ export function CanvasProjectSidebar({ projectId, detail, onAddChapter, onLocate
- 引用项目资产{mediaAssetCount.toLocaleString("zh-CN")}
+ 引用项目资产{mediaAssetCount.toLocaleString("zh-CN")}
{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 (
-
- {loading ? "正在读取本章正文…" : loadError ? "正文读取失败,请稍后重试" : text || "本章还没有正文"}
{!loading && !loadError ?
{text.length.toLocaleString("zh-CN")} 字
: null}
+
+ {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 ?
event.stopPropagation()} onClick={(event) => { event.stopPropagation(); onRetryBatch(batch.id); }} aria-label="重试失败项"> : 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 ?
自动拆分 · 默认时长 :
- {!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.map((preset) => {
@@ -386,12 +386,12 @@ export function CanvasStylePickerModal({ open, value, onClose, onSelect }: { ope
onSelect(preset)}>
- {preset.category}{active ? : null}
+ {preset.category}{active ? : null}
- {preset.title}{active ? 已选择 : null}
+ {preset.title}{active ? 已选择 : null}
{preset.description}
- {preset.tags.map((tag) => {tag})}
+ {preset.tags.map((tag) => {tag})}
setDetailPreset(preset)} aria-label={`查看${preset.title}详情`} title="查看画风详情">
@@ -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
{sections.map((section) =>
{section.title}
{section.content}
)}
-
+
: 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 ? "有未保存修改" : "已保存"}
} loading={saving} disabled={!dirty} onClick={() => void save()}>保存
} aria-label="关闭文本编辑器" onClick={close} />
@@ -147,7 +147,7 @@ export function CanvasTextEditorModal({ node, open, onClose, onSave }: CanvasTex
-