* 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>
122 lines
4.5 KiB
TypeScript
122 lines
4.5 KiB
TypeScript
import { Tag } from "antd";
|
|
import type { ReactNode } from "react";
|
|
|
|
import type { ProjectDetail, ProjectUnit } from "@/services/api/projects";
|
|
|
|
export type ProjectDetailViewProps = {
|
|
detail: ProjectDetail;
|
|
refreshProject: () => void;
|
|
onCreateCanvas: () => void;
|
|
};
|
|
|
|
export const categoryLabels: Record<string, string> = {
|
|
character: "角色",
|
|
environment: "场景",
|
|
wardrobe: "服饰",
|
|
prop: "道具",
|
|
weapon: "武器",
|
|
style: "画风",
|
|
other: "其他",
|
|
};
|
|
|
|
export const mediaLabels: Record<string, string> = {
|
|
image: "图片",
|
|
video: "视频",
|
|
audio: "音频",
|
|
text: "文本",
|
|
model: "3D 模型",
|
|
entity: "角色卡",
|
|
};
|
|
|
|
const statusLabels: Record<string, string> = {
|
|
active: "进行中",
|
|
archived: "已归档",
|
|
draft: "草稿",
|
|
ready: "待制作",
|
|
completed: "已完成",
|
|
review: "待审核",
|
|
confirmed: "已确认",
|
|
pending: "待处理",
|
|
pending_confirmation: "待确认",
|
|
running: "进行中",
|
|
failed: "失败",
|
|
ignored: "已忽略",
|
|
skipped: "已跳过",
|
|
cancelled: "已取消",
|
|
succeeded: "已完成",
|
|
disabled: "已停用",
|
|
idle: "待开始",
|
|
loading: "处理中",
|
|
queued: "排队中",
|
|
success: "已完成",
|
|
error: "异常",
|
|
deleted: "已删除",
|
|
reserved: "已冻结",
|
|
settled: "已结算",
|
|
refunded: "已退款",
|
|
uncertain: "待核对",
|
|
};
|
|
|
|
const sourceTypeLabels: Record<string, string> = {
|
|
blank: "空白开始",
|
|
novel: "导入小说",
|
|
text: "粘贴文本",
|
|
};
|
|
|
|
export function categoryLabel(value: string) {
|
|
return categoryLabels[value] || "其他";
|
|
}
|
|
|
|
export function statusLabel(value: string) {
|
|
return statusLabels[value] || "未知状态";
|
|
}
|
|
|
|
export function mediaLabel(value: string) {
|
|
return mediaLabels[value] || "其他类型";
|
|
}
|
|
|
|
export function sourceTypeLabel(value: string) {
|
|
return sourceTypeLabels[value] || "其他来源";
|
|
}
|
|
|
|
export function StatusPill({ status }: { status: string }) {
|
|
const color = status === "completed" || status === "confirmed" || status === "succeeded" ? "success" : status === "failed" ? "error" : status === "running" || status === "active" ? "processing" : status === "review" || status === "pending_confirmation" ? "warning" : "default";
|
|
return <Tag color={color} className="m-0 !rounded-full !px-2 !text-[var(--fs-label)]">{statusLabel(status)}</Tag>;
|
|
}
|
|
|
|
export function SectionTitle({ eyebrow, title, description, action }: { eyebrow?: string; title: string; description?: string; action?: ReactNode }) {
|
|
return (
|
|
<div className="flex flex-col gap-2 border-b border-border/70 pb-3 sm:flex-row sm:items-end sm:justify-between">
|
|
<div className="min-w-0">
|
|
{eyebrow ? <div className="mb-1 text-[var(--fs-tiny)] font-semibold text-foreground/40">{eyebrow}</div> : null}
|
|
<h2 className="text-lg font-semibold tracking-normal">{title}</h2>
|
|
{description ? <p className="mt-1 text-sm leading-5 text-foreground/55">{description}</p> : null}
|
|
</div>
|
|
{action ? <div className="shrink-0">{action}</div> : null}
|
|
</div>
|
|
);
|
|
}
|
|
|
|
export function MetricTile({ label, value, detail, accent = false }: { label: string; value: string | number; detail?: string; accent?: boolean }) {
|
|
return (
|
|
<div className={`overflow-hidden rounded-lg border px-3 py-3 ${accent ? "border-[color-mix(in_srgb,var(--workspace-accent)_30%,transparent)] bg-[var(--workspace-accent-soft)]" : "border-border/80 bg-background/70"}`}>
|
|
<div className="text-xs text-foreground/50">{label}</div>
|
|
<div className="mt-2 flex items-end gap-2"><strong className="text-2xl font-semibold tracking-normal">{value}</strong>{detail ? <span className="pb-0.5 text-xs text-foreground/45">{detail}</span> : null}</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
export function UnitProgress({ unit }: { unit: ProjectUnit }) {
|
|
const progress = unit.status === "completed" ? 100 : unit.status === "ready" ? 66 : 24;
|
|
return <div className="h-1.5 w-20 overflow-hidden rounded-full bg-foreground/10"><div className="h-full rounded-full bg-[var(--workspace-accent)] transition-[width] duration-200" style={{ width: `${progress}%` }} /></div>;
|
|
}
|
|
|
|
export function formatTime(value?: string) {
|
|
if (!value) return "-";
|
|
return new Date(value).toLocaleString("zh-CN", { month: "2-digit", day: "2-digit", hour: "2-digit", minute: "2-digit" });
|
|
}
|
|
|
|
export function formatCount(value: number) {
|
|
return new Intl.NumberFormat("zh-CN").format(value);
|
|
}
|