|
|
|
@@ -56,6 +56,37 @@ function newId(prefix: string) {
|
|
|
|
|
return `${prefix}_${Date.now().toString(36)}${Math.floor(Math.random() * 1000)}`;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/** 中文字符(CJK)按 3 个权重计,其余按 1:英文最多 150 字、中文最多 50 字,混排按权重折算。 */
|
|
|
|
|
const NAME_MAX_WEIGHT = 150;
|
|
|
|
|
|
|
|
|
|
function isCjkChar(ch: string) {
|
|
|
|
|
const code = ch.codePointAt(0) ?? 0;
|
|
|
|
|
return (
|
|
|
|
|
(code >= 0x4e00 && code <= 0x9fff) ||
|
|
|
|
|
(code >= 0x3400 && code <= 0x4dbf) ||
|
|
|
|
|
(code >= 0xf900 && code <= 0xfaff)
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function clampWorkflowName(value: string) {
|
|
|
|
|
let weight = 0;
|
|
|
|
|
let out = "";
|
|
|
|
|
for (const ch of value) {
|
|
|
|
|
const w = isCjkChar(ch) ? 3 : 1;
|
|
|
|
|
if (weight + w > NAME_MAX_WEIGHT) break;
|
|
|
|
|
weight += w;
|
|
|
|
|
out += ch;
|
|
|
|
|
}
|
|
|
|
|
return out;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/** 名称框宽度:中文字符约 2ch、其余约 1ch,按内容自适应;上限防止过宽,空间不足时由 flex 收缩(不遮住右侧按钮)。 */
|
|
|
|
|
function workflowNameWidth(value: string) {
|
|
|
|
|
let w = 0;
|
|
|
|
|
for (const ch of value) w += isCjkChar(ch) ? 2 : 1;
|
|
|
|
|
return `${Math.min(160, Math.max(8, w + 3))}ch`;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export default function WorkflowCanvasPage() {
|
|
|
|
|
const { workflowId } = useParams<{ workflowId: string }>();
|
|
|
|
|
const workflows = useStore((s) => s.workflows);
|
|
|
|
@@ -528,7 +559,7 @@ export default function WorkflowCanvasPage() {
|
|
|
|
|
return (
|
|
|
|
|
<div className="flex h-full flex-col">
|
|
|
|
|
{/* 顶部工具栏 */}
|
|
|
|
|
<div className="flex items-center gap-3 border-b border-border bg-panel px-4 py-2.5">
|
|
|
|
|
<div className="flex items-center gap-2 border-b border-border bg-panel px-3 py-2.5 sm:gap-3 sm:px-4">
|
|
|
|
|
<Link
|
|
|
|
|
to="/workflows"
|
|
|
|
|
className="flex shrink-0 items-center gap-1 whitespace-nowrap rounded-lg px-2 py-1 text-xs text-slate-400 hover:bg-panel-2 hover:text-slate-200"
|
|
|
|
@@ -537,10 +568,12 @@ export default function WorkflowCanvasPage() {
|
|
|
|
|
返回工作流
|
|
|
|
|
</Link>
|
|
|
|
|
<input
|
|
|
|
|
className="input w-44 flex-none px-2 py-1 text-sm"
|
|
|
|
|
className="input min-w-0 shrink px-2 py-1 text-sm"
|
|
|
|
|
value={name}
|
|
|
|
|
onChange={(e) => setName(e.target.value)}
|
|
|
|
|
onChange={(e) => setName(clampWorkflowName(e.target.value))}
|
|
|
|
|
placeholder="工作流名称"
|
|
|
|
|
title="英文不超过 150 个字符,中文不超过 50 个(混排按中文字符权重折算)"
|
|
|
|
|
style={{ width: workflowNameWidth(name) }}
|
|
|
|
|
/>
|
|
|
|
|
<span
|
|
|
|
|
className="shrink-0 rounded bg-panel-2 px-1.5 py-0.5 text-[10px] text-slate-400"
|
|
|
|
@@ -548,10 +581,14 @@ export default function WorkflowCanvasPage() {
|
|
|
|
|
>
|
|
|
|
|
{workflow.kind === "preset" ? "预制" : "自定义"}
|
|
|
|
|
</span>
|
|
|
|
|
<span className="shrink-0 text-xs text-slate-500">
|
|
|
|
|
<span className="min-w-0 truncate text-xs text-slate-500">
|
|
|
|
|
{nodes.length} 节点 · {edges.length} 连线
|
|
|
|
|
</span>
|
|
|
|
|
{msg ? <span className="shrink-0 text-xs text-emerald-400">{msg}</span> : null}
|
|
|
|
|
{msg ? (
|
|
|
|
|
<span className="min-w-0 max-w-[140px] truncate text-xs text-emerald-400">
|
|
|
|
|
{msg}
|
|
|
|
|
</span>
|
|
|
|
|
) : null}
|
|
|
|
|
<div className="ml-auto flex shrink-0 items-center gap-2">
|
|
|
|
|
<button className="btn-secondary" onClick={() => saveWorkflow()}>
|
|
|
|
|
保存
|
|
|
|
|