This repository has been archived on 2026-09-13. You can view files and clone it. You cannot open issues or pull requests or push a commit.
Files
BPOS-MVP/apps/web/src/shared/gov/flow/flow-node-paint.ts
T
ChaiHaoandClaude Opus 5 6cff6ab34c 删掉节点的「说明」字段 —— 它填得进、存得下,却从来画不出来
`sub` 声称是「画布上的第二行小字」,但画笔里那支只在 `variant !== 'business'`
(步骤图)才走,而全仓的调用点一律是 business:这个字段写了、存了、
一次也没显示过。属性面上那句提示是句假话。

整条清:属性面的一行 · `ProcessNode.sub`(契约) · `FlowNode.sub` ·
图↔包两处映射 · 画笔里那支画不出来的分支(连同它带来的「两种画法」分叉,
现在节点框里就一行活动名) · 节点宽度算法里 `node.sub || node.ghost` 的宽框判据
(sub 没了,只剩 ghost 决定宽窄)。

存量数据不受影响:库里没有一条带 sub 的定义(查过),schema 非严格,
真有旧值也只是解析时被剥掉。

五闸绿;契约快照随本提交入库(去一个可选字段,加法的反面,无下游)。

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KeDBx2PHLavbURXB8LJJ7m
2026-07-29 17:48:12 +08:00

169 lines
6.6 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { fitLine, type FlowNode, type FlowTone, type FlowVariant } from "./flow-layout";
/**
* 流程图节点/连线的共用画笔:FlowDiagram(diagram-js 渲染)与 FlowEditor(手绘编辑)
* 都从这里取形——同一套配色/字级/几何,编辑态与展示态不许有视觉漂移。
*/
const svgNS = "http://www.w3.org/2000/svg";
export function svgEl<K extends keyof SVGElementTagNameMap>(
tag: K,
attrs: Record<string, string | number>,
text?: string,
): SVGElementTagNameMap[K] {
const el = document.createElementNS(svgNS, tag);
for (const [key, value] of Object.entries(attrs)) el.setAttribute(key, String(value));
if (text !== undefined) el.textContent = text;
return el;
}
export const FLOW_SANS = '"PingFang SC","Noto Sans SC","Hiragino Sans GB","Microsoft YaHei",sans-serif';
export const FLOW_MONO = '"SF Mono","JetBrains Mono",Menlo,ui-monospace,monospace';
export const TONE_COLOR: Record<FlowTone, string> = { main: "#46586A", ok: "#3F7A5C", bad: "#B0413E" };
/** 节点画形(往 group 里追加图元;调用方自己定位 group)。 */
export function paintNode(group: SVGElement, node: FlowNode, w: number, h: number, variant: FlowVariant): void {
if (node.kind === "start" || node.kind === "end" || node.kind === "end-bad") {
const bad = node.kind === "end-bad";
group.append(
svgEl("circle", {
cx: w / 2,
cy: h / 2,
r: w / 2,
fill: bad ? "#F6E7E6" : "#E6F1EA",
stroke: bad ? "#B0413E" : "#3F7A5C",
"stroke-width": node.kind === "start" ? 1.8 : bad ? 2.4 : 2.6,
}),
);
} else if (node.kind === "gateway") {
const hd = w / 2;
// 2026-07 精简:网关无弹窗、无铜点——判据存于节点数据,导出派生表与 L3 upstreamGates 承接。
group.append(
svgEl("polygon", {
points: `${hd},0 ${w},${hd} ${hd},${h} 0,${hd}`,
fill: "#EEF1F5",
stroke: "#46586A",
"stroke-width": 1.3,
}),
);
group.append(
svgEl(
"text",
{
x: hd,
y: hd + 4,
"text-anchor": "middle",
"font-family": FLOW_MONO,
"font-size": variant === "business" ? 13 : 12,
"font-weight": 700,
fill: "#46586A",
},
node.mode === "and" ? "+" : "×",
),
);
} else if (node.kind === "callout") {
// 卡点统一红(绿色让位给 Case 活动节点);去向以框内 →id 表达。
group.append(
svgEl("rect", {
x: 0,
y: 0,
width: w,
height: h,
rx: variant === "business" ? 5 : 6,
fill: "#F6E7E6",
stroke: "#B0413E",
"stroke-width": variant === "business" ? 1.2 : 1.3,
}),
);
const fitted = fitLine(
node.to ? `${node.label}→${node.to}` : node.label,
w - 12,
variant === "business" ? 10 : 11.5,
9,
);
group.append(
svgEl(
"text",
{
x: w / 2,
y: h / 2 + 3.5,
"text-anchor": "middle",
"font-family": FLOW_SANS,
"font-size": fitted.size,
"font-weight": 600,
fill: "#8F3330",
},
fitted.text,
),
);
} else {
// 方框三色:标准活动 浅蓝(缺省)/ 案例活动 绿(一事一议)/ 审批点 琥珀(人裁节点)。
// 审批点非活动,只是共用方框几何——模型上它是系统内置的编排要素(见 flow-layout 的 flavor 注释)。
// 活动调用已整体退役(2026-07 终拍:所有节点都是内置节点;schema 容忍遗留 wait 但不再有专属画法)。
const ghost = node.ghost === true;
const isCase = node.flavor === "case";
const isApproval = node.flavor === "approval";
const skin = isApproval
? { fill: "#FBF0DC", stroke: "#B98432", ink: "#6E4B12" }
: isCase
? { fill: "#E6F1EA", stroke: "#3F7A5C", ink: "#2F5C45" }
: { fill: "#E8F1FB", stroke: "#4E7DAF", ink: "#161B22" };
group.append(
svgEl("rect", {
x: 0,
y: 0,
width: w,
height: h,
rx: 6,
fill: ghost ? "#F4F6F9" : skin.fill,
stroke: ghost ? "#9AA6B4" : skin.stroke,
"stroke-width": ghost ? 1.2 : 1.3,
...(ghost ? { "stroke-dasharray": "4 3" } : {}),
}),
);
const titleSize = variant === "business" ? 12 : 11.5;
const inkColor = ghost ? "#6A7686" : skin.ink;
const title = fitLine(node.label, w - 12, titleSize, 10);
// 节点框里只写活动名(标号走左上角标)—— sub 第二行小字已退役,没有第二种画法了。
group.append(
svgEl(
"text",
{ x: w / 2, y: h / 2 + 4, "text-anchor": "middle", "font-family": FLOW_SANS, "font-size": title.size, "font-weight": 600, fill: inkColor },
title.text,
),
);
// 标号角标:business 一贯有;步骤图(steps)的真实节点也要 S#/B# 编号与指导书条目对齐
// (2026-07 用户纠正)——ghost 骨架节点不标。
if (variant === "business" || !ghost) {
group.append(
svgEl("text", { x: 6, y: 11, "font-family": FLOW_MONO, "font-size": 8.5, fill: "#8A93A0" }, node.code ?? node.id),
);
}
/* 责任人角标已退役(2026-07-30):编排不带岗位 —— 谁来干是运营时分配的,不画在图上。 */
// 分工角标已随词签退役(2026-07 user「删除 AI/系统/人 选项配置」):步骤图不再画 assign 角标。
}
}
/** 箭头三角(按折线最后一段方向)。 */
export function arrowHeadPoints(waypoints: Array<{ x: number; y: number }>): string {
const tip = waypoints[waypoints.length - 1];
const prev = waypoints[waypoints.length - 2];
// 严格索引:两点以下画不出箭头,直接退空(布局引擎不会产出这种连线,此处只为类型闭合)
if (tip === undefined || prev === undefined) return "";
const dx = Math.sign(tip.x - prev.x);
const dy = Math.sign(tip.y - prev.y);
return dx !== 0
? `${tip.x},${tip.y} ${tip.x - dx * 9},${tip.y - 4.5} ${tip.x - dx * 9},${tip.y + 4.5}`
: `${tip.x},${tip.y} ${tip.x - 4.5},${tip.y - dy * 9} ${tip.x + 4.5},${tip.y - dy * 9}`;
}
export const CAPTION_STYLE: Record<string, { fill: string; size: (v: FlowVariant) => number; font: string; weight?: number }> = {
gateway: { fill: "#3A4452", size: (v) => (v === "business" ? 10 : 9), font: FLOW_SANS, weight: 600 },
start: { fill: "#3F7A5C", size: (v) => (v === "business" ? 10 : 8.6), font: FLOW_SANS, weight: 600 },
end: { fill: "#2F5C45", size: (v) => (v === "business" ? 10 : 8.6), font: FLOW_SANS, weight: 600 },
"end-bad": { fill: "#8F3330", size: (v) => (v === "business" ? 10 : 8.6), font: FLOW_SANS, weight: 600 },
note: { fill: "#8A5F1C", size: (v) => (v === "business" ? 9.8 : 8.6), font: FLOW_MONO },
};