forked from ChaiHao/BPOS-MVP
清掉「节点决定数据契约」的三处遗留 —— 编排面从此一个字段都不碰
讨论定的界线:节点只决定四件(身份 · 类型 · 链接哪个活动 · 出边条件),
数据契约全归活动自己(它的 manifest,随活动升版),流程包的 callInterface 是
「这条流被调用时的签名」另算。照此清三处化石:
① `ProcessNode.in/out` —— schema 上还留着「这个节点**声明**吃什么、吐什么」,
注释还写着「闭合校验以**边映射**为准」;边映射上一轮已退役,这两行是上个模型的化石。
连同图↔包的两处映射一并撤。
② 手绘编辑器属性卡的 **进 / 出 / 查 / 执行** 四行 —— 其中「查」(use)与「执行」(owner)
连 ProcessNode 都没有,填了保存就丢:写了不生效的输入框。
同卡里**网关**与**卡点**两支也一并删(两种要素早已从模型里去掉,分支不可达,
而且正是网关那支在写 owner)。
③ 只读图双击卡的 **执行** 行、`onEditNode` 的 owner 形参、画笔里的**责任人角标** ——
编排不带岗位,图上不该画它。上一轮图例注释说「图上早已不画责任人角标」其实说错了:
画笔一直画着,只是没人写得进 owner;这次真的撤了,注释也改对。
连带:`FlowNode` 的 owner/in/out/use 四个字段退役;岗位选择器 support.tsx 成了孤儿
(depcruise no-orphans 当场逮住)—— 编排不带岗位,它没有归宿,删。
四闸绿;契约快照随本提交入库。
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KeDBx2PHLavbURXB8LJJ7m
This commit is contained in:
@@ -17,8 +17,6 @@ import {
|
||||
type ShapeLayout,
|
||||
} from "./flow-layout";
|
||||
import { arrowHeadPoints, CAPTION_STYLE, FLOW_MONO, paintNode, svgEl as svg, TONE_COLOR } from "./flow-node-paint";
|
||||
import { OwnerSelect } from "./support";
|
||||
import type { SpPosition } from "./support";
|
||||
import "diagram-js/assets/diagram-js.css";
|
||||
|
||||
/**
|
||||
@@ -145,9 +143,9 @@ export interface FlowDiagramProps {
|
||||
/** 点击连线(选中边,按边号/两端认);不传则边不可点。 */
|
||||
onEdgeClick?: ((edge: FlowEdge) => void) | undefined;
|
||||
/** 双击活动节点就地改文字(标题/小字/主办);不传则不可改。 */
|
||||
onEditNode?: ((nodeId: string, patch: { label: string; owner?: string }) => Promise<void>) | undefined;
|
||||
/** 双击活动就地改**名字** —— 图上可编辑的只有名字(数据归活动、岗位归运营) */
|
||||
onEditNode?: ((nodeId: string, patch: { label: string }) => Promise<void>) | undefined;
|
||||
/** 执行人候选(L1 业务能力)——传了则执行字段只能下拉选缩写,不开自由填。 */
|
||||
ownerOptions?: SpPosition[] | undefined;
|
||||
ariaLabel: string;
|
||||
}
|
||||
|
||||
@@ -157,7 +155,7 @@ interface NodeEdit {
|
||||
top: number;
|
||||
}
|
||||
|
||||
export function FlowDiagram({ graph, variant, onNodeClick, onEdgeClick, onEditNode, ownerOptions, ariaLabel }: FlowDiagramProps) {
|
||||
export function FlowDiagram({ graph, variant, onNodeClick, onEdgeClick, onEditNode, ariaLabel }: FlowDiagramProps) {
|
||||
const containerRef = useRef<HTMLDivElement>(null);
|
||||
const editRef = useRef<HTMLDivElement>(null);
|
||||
const clickRef = useRef(onNodeClick);
|
||||
@@ -170,7 +168,7 @@ export function FlowDiagram({ graph, variant, onNodeClick, onEdgeClick, onEditNo
|
||||
const drillTimer = useRef<number | null>(null);
|
||||
// 活动文字就地编辑浮层(双击活动)。
|
||||
const [nodeEdit, setNodeEdit] = useState<NodeEdit | null>(null);
|
||||
const [editDraft, setEditDraft] = useState({ label: "", owner: "" });
|
||||
const [editDraft, setEditDraft] = useState({ label: "" });
|
||||
const [editBusy, setEditBusy] = useState(false);
|
||||
const [editErr, setEditErr] = useState<string | null>(null);
|
||||
|
||||
@@ -254,7 +252,7 @@ export function FlowDiagram({ graph, variant, onNodeClick, onEdgeClick, onEditNo
|
||||
window.clearTimeout(drillTimer.current);
|
||||
drillTimer.current = null;
|
||||
}
|
||||
setEditDraft({ label: bo.node.label, owner: bo.node.owner ?? "" });
|
||||
setEditDraft({ label: bo.node.label });
|
||||
setEditErr(null);
|
||||
setEditBusy(false);
|
||||
setNodeEdit({ node: bo.node, ...overlayPos(event.originalEvent, 300, 190) });
|
||||
@@ -317,14 +315,6 @@ export function FlowDiagram({ graph, variant, onNodeClick, onEdgeClick, onEditNo
|
||||
<span className="lb">标题</span>
|
||||
<input value={editDraft.label} maxLength={16} onChange={(e) => setEditDraft((d) => ({ ...d, label: e.target.value }))} />
|
||||
</div>
|
||||
<div className="gp-edit-row">
|
||||
<span className="lb">执行</span>
|
||||
{ownerOptions ? (
|
||||
<OwnerSelect value={editDraft.owner} positions={ownerOptions} onChange={(v) => setEditDraft((d) => ({ ...d, owner: v }))} />
|
||||
) : (
|
||||
<input value={editDraft.owner} maxLength={16} placeholder="执行人岗位缩写(可空)" onChange={(e) => setEditDraft((d) => ({ ...d, owner: e.target.value }))} />
|
||||
)}
|
||||
</div>
|
||||
{editErr && <p className="gp-basis muted" style={{ color: "#8f3330" }}>{editErr}</p>}
|
||||
<div className="gp-btns">
|
||||
<button type="button" className="be-btn ghost" disabled={editBusy} onClick={() => setNodeEdit(null)}>
|
||||
@@ -338,10 +328,7 @@ export function FlowDiagram({ graph, variant, onNodeClick, onEdgeClick, onEditNo
|
||||
setEditBusy(true);
|
||||
setEditErr(null);
|
||||
editNodeRef
|
||||
.current?.(nodeEdit.node.id, {
|
||||
label: editDraft.label.trim(),
|
||||
...(editDraft.owner.trim() === "" ? {} : { owner: editDraft.owner.trim() }),
|
||||
})
|
||||
.current?.(nodeEdit.node.id, { label: editDraft.label.trim() })
|
||||
.then(() => setNodeEdit(null))
|
||||
.catch((err) => {
|
||||
setEditErr(err instanceof Error ? err.message : "保存失败");
|
||||
|
||||
@@ -2,8 +2,6 @@
|
||||
import { Ico } from "@ui";
|
||||
import { useEffect, useMemo, useRef, useState } from "react";
|
||||
import type { FlowEdge, FlowGraph, FlowNode, FlowNodeKind } from "./flow-layout.ts";
|
||||
import type { SpPosition } from "./support.tsx";
|
||||
import { OwnerSelect } from "./support.tsx";
|
||||
import { createFlowEditor, syncElementLabel, type FlowEditorHandles } from "./flow-editor-core.ts";
|
||||
import "diagram-js/assets/diagram-js.css";
|
||||
|
||||
@@ -26,8 +24,6 @@ export interface FlowEditorProps {
|
||||
/** idMap = 编辑期活动改号映射(自动顺序编号),服务端据此重键 operations。 */
|
||||
onSave: (graph: FlowGraph, idMap: Record<string, string>) => Promise<void>;
|
||||
onCancel: () => void;
|
||||
/** 执行人候选(L1 业务能力)——传了则活动属性卡的执行字段只能下拉选缩写。 */
|
||||
positions?: SpPosition[] | undefined;
|
||||
}
|
||||
|
||||
interface PropState {
|
||||
@@ -37,7 +33,7 @@ interface PropState {
|
||||
top: number;
|
||||
}
|
||||
|
||||
export function FlowEditor({ initial, drilledIds, onSave, onCancel, positions }: FlowEditorProps) {
|
||||
export function FlowEditor({ initial, drilledIds, onSave, onCancel }: FlowEditorProps) {
|
||||
const canvasRef = useRef<HTMLDivElement>(null);
|
||||
const wrapRef = useRef<HTMLDivElement>(null);
|
||||
const editorRef = useRef<FlowEditorHandles | null>(null);
|
||||
@@ -239,8 +235,6 @@ export function FlowEditor({ initial, drilledIds, onSave, onCancel, positions }:
|
||||
<PropCard
|
||||
key={`${prop.element.id}-${prop.top}`}
|
||||
prop={prop}
|
||||
registry={editorRef.current?.diagram.get("elementRegistry")}
|
||||
positions={positions}
|
||||
onClose={() => setProp(null)}
|
||||
onApply={(patch) => {
|
||||
const editor = editorRef.current;
|
||||
@@ -277,14 +271,10 @@ const KIND_NAME: Record<FlowNodeKind, string> = {
|
||||
|
||||
function PropCard({
|
||||
prop,
|
||||
registry,
|
||||
positions,
|
||||
onClose,
|
||||
onApply,
|
||||
}: {
|
||||
prop: PropState;
|
||||
registry: Any;
|
||||
positions?: SpPosition[] | undefined;
|
||||
onClose: () => void;
|
||||
onApply: (patch: Record<string, unknown>) => void;
|
||||
}) {
|
||||
@@ -292,57 +282,30 @@ function PropCard({
|
||||
const edge = prop.isEdge ? (prop.element.businessObject as FlowEdge) : undefined;
|
||||
|
||||
const [label, setLabel] = useState(node?.label ?? edge?.label ?? "");
|
||||
const [owner, setOwner] = useState(node?.owner ?? "");
|
||||
|
||||
const [flavor, setFlavor] = useState<"sop" | "case" | "approval">(
|
||||
node?.flavor === "case" ? "case" : node?.flavor === "approval" ? "approval" : "sop",
|
||||
);
|
||||
const [mode, setMode] = useState<"x" | "and">(node?.mode === "and" ? "and" : "x");
|
||||
const [basis, setBasis] = useState(node?.basis ?? "");
|
||||
const [to, setTo] = useState(node?.to ?? "");
|
||||
const [tone, setTone] = useState<string>(edge?.tone ?? "");
|
||||
// 数据线(进/出/查:台账规范名顿号分隔)——L2 只讲流转与数据对象;
|
||||
// 过程细节(分工/逐条)属 L3 指导书内部,节点级 per/assign 已退役(保存即清遗留值)。
|
||||
const [dataIn, setDataIn] = useState((node?.in ?? []).join("、"));
|
||||
const [dataOut, setDataOut] = useState((node?.out ?? []).join("、"));
|
||||
const [dataUse, setDataUse] = useState((node?.use ?? []).join("、"));
|
||||
|
||||
const targets: string[] = useMemo(() => {
|
||||
if (!registry) return [];
|
||||
const ids: string[] = [];
|
||||
for (const el of registry.getAll() as Any[]) {
|
||||
const bo = el.businessObject as FlowNode | undefined;
|
||||
if (el.parent && !el.waypoints && bo?.id && bo.id !== node?.id) ids.push(bo.id);
|
||||
}
|
||||
return ids.sort();
|
||||
}, [registry, node?.id]);
|
||||
|
||||
const save = () => {
|
||||
if (node) {
|
||||
const patch: Record<string, unknown> = {};
|
||||
const text = label.trim();
|
||||
if (text) patch.label = text.slice(0, 16);
|
||||
if (node.kind === "activity") {
|
||||
patch.owner = owner.trim() ? owner.trim().slice(0, 16) : undefined;
|
||||
patch.flavor = flavor === "sop" ? undefined : flavor;
|
||||
const lines = (raw: string) =>
|
||||
raw.split(/[、,,\n]+/).map((part) => part.trim().slice(0, 20)).filter(Boolean).slice(0, 5);
|
||||
const din = lines(dataIn);
|
||||
const dout = lines(dataOut);
|
||||
const duse = lines(dataUse);
|
||||
patch.in = din.length ? din : undefined;
|
||||
patch.out = dout.length ? dout : undefined;
|
||||
patch.use = duse.length ? duse : undefined;
|
||||
// 活动调用整体退役(2026-07 终拍,user「所有节点都是内置节点」):保存即清遗留 wait。
|
||||
/*
|
||||
* **节点不声明数据、不带岗位**(2026-07-30):进/出/查与执行四行已撤 ——
|
||||
* 数据契约归活动自己(它的 manifest),负责人归运营时分配。
|
||||
* 这四样连同更早退役的 wait/per/assign,保存时一并清掉遗留值。
|
||||
*/
|
||||
patch.owner = undefined;
|
||||
patch.in = undefined;
|
||||
patch.out = undefined;
|
||||
patch.use = undefined;
|
||||
patch.wait = undefined;
|
||||
patch.per = undefined;
|
||||
patch.assign = undefined;
|
||||
} else if (node.kind === "gateway") {
|
||||
patch.owner = owner.trim() ? owner.trim().slice(0, 12) : undefined;
|
||||
patch.mode = mode === "and" ? "and" : undefined;
|
||||
patch.basis = mode === "x" && basis.trim() ? basis.trim().slice(0, 120) : undefined;
|
||||
} else if (node.kind === "callout") {
|
||||
patch.to = to || undefined;
|
||||
}
|
||||
onApply(patch);
|
||||
} else if (edge) {
|
||||
@@ -382,64 +345,12 @@ function PropCard({
|
||||
<option value="approval">审批点(琥珀 · 人裁节点)</option>
|
||||
</select>
|
||||
</div>
|
||||
<div className="gp-edit-row">
|
||||
<span className="lb">执行</span>
|
||||
{positions ? (
|
||||
<OwnerSelect value={owner} positions={positions} onChange={setOwner} />
|
||||
) : (
|
||||
<input value={owner} maxLength={16} placeholder="执行人岗位缩写(可空)" onChange={(e) => setOwner(e.target.value)} />
|
||||
)}
|
||||
</div>
|
||||
<div className="gp-edit-row">
|
||||
<span className="lb">进</span>
|
||||
<input value={dataIn} placeholder="吃进的流转件,顿号分隔 ≤5(规范名)" onChange={(e) => setDataIn(e.target.value)} />
|
||||
</div>
|
||||
<div className="gp-edit-row">
|
||||
<span className="lb">出</span>
|
||||
<input value={dataOut} placeholder="产出的流转件;可带状态词:单据名(已受理)" onChange={(e) => setDataOut(e.target.value)} />
|
||||
</div>
|
||||
<div className="gp-edit-row">
|
||||
<span className="lb">查</span>
|
||||
<input value={dataUse} placeholder="照着查/判的底座与规则,顿号分隔" onChange={(e) => setDataUse(e.target.value)} />
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
|
||||
{node?.kind === "gateway" && (
|
||||
<>
|
||||
<div className="gp-edit-row">
|
||||
<span className="lb">类型</span>
|
||||
<select value={mode} onChange={(e) => setMode(e.target.value as "x" | "and")}>
|
||||
<option value="x">判断 ×(按判据走一条)</option>
|
||||
<option value="and">并行 +(全部同时走)</option>
|
||||
</select>
|
||||
</div>
|
||||
{mode === "x" && (
|
||||
<div className="gp-edit-row">
|
||||
<span className="lb">判据</span>
|
||||
<textarea value={basis} maxLength={120} rows={2} placeholder="白话可判的一句;卡点写死后果" onChange={(e) => setBasis(e.target.value)} />
|
||||
</div>
|
||||
)}
|
||||
<div className="gp-edit-row">
|
||||
<span className="lb">判定</span>
|
||||
<input value={owner} maxLength={12} placeholder={mode === "and" ? "(并行可空)" : "判定人缩写"} onChange={(e) => setOwner(e.target.value)} />
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
|
||||
{node?.kind === "callout" && (
|
||||
<div className="gp-edit-row">
|
||||
<span className="lb">去向</span>
|
||||
<select value={to} onChange={(e) => setTo(e.target.value)}>
|
||||
<option value="">(不标去向)</option>
|
||||
{targets.map((id) => (
|
||||
<option key={id} value={id}>
|
||||
{id}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
)}
|
||||
|
||||
|
||||
|
||||
{edge && (
|
||||
<div className="gp-edit-row">
|
||||
|
||||
@@ -14,7 +14,6 @@ import { Ico } from '@ui';
|
||||
import { FlowDiagram } from './FlowDiagram.tsx';
|
||||
import { FlowEditor } from './FlowEditor.tsx';
|
||||
import type { FlowEdge, FlowGraph } from './flow-layout.ts';
|
||||
import type { SpPosition } from './support.tsx';
|
||||
import { packageToGraph } from './pkg-to-graph.ts';
|
||||
import type { InstanceMark } from './pkg-to-graph.ts';
|
||||
import './flow-canvas.css';
|
||||
@@ -24,7 +23,6 @@ export function FlowFigureBlock({
|
||||
oneLiner,
|
||||
graph,
|
||||
drilledIds,
|
||||
positions,
|
||||
canEdit,
|
||||
onNodeClick,
|
||||
onEdgeClick,
|
||||
@@ -36,13 +34,12 @@ export function FlowFigureBlock({
|
||||
graph: FlowGraph | null;
|
||||
/** 已建立下钻内容的节点 id(删除这些节点时编辑器会二次确认) */
|
||||
drilledIds: string[];
|
||||
positions?: SpPosition[] | undefined;
|
||||
canEdit: boolean;
|
||||
onNodeClick: (nodeId: string) => void;
|
||||
/** 点击连线选中边(属性卡随之切到该边);不传则边不可点 */
|
||||
onEdgeClick?: ((edge: FlowEdge) => void) | undefined;
|
||||
/** 双击流程图活动就地改文字(标题 / 主办) */
|
||||
onEditNodeText?: ((nodeId: string, patch: { label: string; owner?: string }) => Promise<void>) | undefined;
|
||||
/** 双击流程图活动就地改**名字** —— 图上可编辑的只有名字 */
|
||||
onEditNodeText?: ((nodeId: string, patch: { label: string }) => Promise<void>) | undefined;
|
||||
/** 手工绘图:整图保存(idMap = 本次编辑会话 原id→现id,宿主用它重键下钻内容) */
|
||||
onSaveFlowGraph?: ((graph: FlowGraph, idMap: Record<string, string>) => Promise<void>) | undefined;
|
||||
}) {
|
||||
@@ -71,7 +68,6 @@ export function FlowFigureBlock({
|
||||
<FlowEditor
|
||||
initial={graph}
|
||||
drilledIds={drilledIds}
|
||||
positions={positions}
|
||||
onSave={async (next, idMap) => {
|
||||
await onSaveFlowGraph(next, idMap);
|
||||
setEditing(false);
|
||||
@@ -93,7 +89,6 @@ export function FlowFigureBlock({
|
||||
onNodeClick={onNodeClick}
|
||||
onEdgeClick={onEdgeClick}
|
||||
onEditNode={canEdit ? onEditNodeText : undefined}
|
||||
ownerOptions={positions}
|
||||
ariaLabel={`${title}业务流程图`}
|
||||
/>
|
||||
</div>
|
||||
@@ -111,8 +106,8 @@ export function FlowFigureBlock({
|
||||
* 读起来像图之外的第二块内容 —— 挪进框内,一眼看出它说的就是这张图上的记号。
|
||||
* 图标与绘图工具(元素盘)同款 SVG,蓝/琥珀由图标自己带,不必再写一遍「蓝 = …」。
|
||||
*
|
||||
* 「责任」那两格删了:编排**不带岗位**(负责人是运营时分配的),
|
||||
* 图上早已不画责任人角标 —— 图例里留着就是指着一件不存在的东西。
|
||||
* 「责任」那两格删了:编排**不带岗位**(负责人是运营时分配的)——
|
||||
* 责任人角标同轮一并从画笔里撤掉,图上不再画它。
|
||||
*/
|
||||
function Legend() {
|
||||
return (
|
||||
|
||||
@@ -37,16 +37,15 @@ export interface FlowNode {
|
||||
tone?: "ok" | "bad";
|
||||
/** 仅卡点:去向节点 id(回跳返工/前跳直达)——渲染时框内标 →id。 */
|
||||
to?: string | undefined;
|
||||
/** 责任下沉:活动=主办(A);网关=判定人。活动框内角标,网关随判据进浮层。 */
|
||||
owner?: string | undefined;
|
||||
/*
|
||||
* (已退役 2026-07-30)owner / in / out / use —— 责任人角标与「进/出/查」数据线。
|
||||
* 编排**不带岗位**(谁来干是运营时分配的),**也不声明数据**(契约归活动自己)。
|
||||
* 属性卡里那四行早已写了不生效(owner/use 连 ProcessNode 都没有),一并撤掉。
|
||||
*/
|
||||
/** 判据下沉:仅网关——判定标准(节点数据;画布不再弹窗,导出派生表可见)。 */
|
||||
basis?: string | undefined;
|
||||
/** 仅网关:x=判断(×,默认)/ and=并行(+,所有出口同时走)。 */
|
||||
mode?: "x" | "and" | undefined;
|
||||
/** 数据线下沉(仅活动):进/出=流转,查=底座——L2 图后「数据 · 台账与加工」段由此派生。 */
|
||||
in?: string[] | undefined;
|
||||
out?: string[] | undefined;
|
||||
use?: string[] | undefined;
|
||||
/** 执行基数(exec-spec):item=逐条办;缺省=跟单办。 */
|
||||
per?: "item";
|
||||
/** 人机分工:auto 自动 / assist 帮我做 / human 我来做(缺省)。 */
|
||||
|
||||
@@ -159,25 +159,7 @@ export function paintNode(group: SVGElement, node: FlowNode, w: number, h: numbe
|
||||
svgEl("text", { x: 6, y: 11, "font-family": FLOW_MONO, "font-size": 8.5, fill: "#8A93A0" }, node.code ?? node.id),
|
||||
);
|
||||
}
|
||||
// 责任下沉:执行人角标在右上——图即责任表。
|
||||
if (node.owner && !ghost) {
|
||||
const ownerFit = fitLine(node.owner, w * 0.55, 8.5, 7);
|
||||
group.append(
|
||||
svgEl(
|
||||
"text",
|
||||
{
|
||||
x: w - 6,
|
||||
y: 11,
|
||||
"text-anchor": "end",
|
||||
"font-family": FLOW_SANS,
|
||||
"font-size": ownerFit.size,
|
||||
"font-weight": 600,
|
||||
fill: skin.stroke,
|
||||
},
|
||||
ownerFit.text,
|
||||
),
|
||||
);
|
||||
}
|
||||
/* 责任人角标已退役(2026-07-30):编排不带岗位 —— 谁来干是运营时分配的,不画在图上。 */
|
||||
// 分工角标已随词签退役(2026-07 user「删除 AI/系统/人 选项配置」):步骤图不再画 assign 角标。
|
||||
}
|
||||
}
|
||||
|
||||
@@ -82,8 +82,6 @@ export function mergeGraphIntoPackage(
|
||||
// 契约不带负责人岗位:分派归运营(运行时默认全员轮值;审批点的 approver 是功能语义,另算)
|
||||
failure: priorAct?.failure ?? { kind: 'terminate' as const },
|
||||
...(priorAct?.message === undefined ? {} : { message: priorAct.message }),
|
||||
...(n.in === undefined || n.in.length === 0 ? {} : { in: [...n.in] }),
|
||||
...(n.out === undefined || n.out.length === 0 ? {} : { out: [...n.out] }),
|
||||
};
|
||||
});
|
||||
|
||||
|
||||
@@ -98,8 +98,6 @@ export function packageToGraph(pkg: ProcessPackage, marks: readonly InstanceMark
|
||||
...base,
|
||||
// 契约不带负责人岗位 —— 角标不再标岗位,链接的实现(装载面)以 act 携带
|
||||
act: n.activityId,
|
||||
...(n.in === undefined ? {} : { in: [...n.in] }),
|
||||
...(n.out === undefined ? {} : { out: [...n.out] }),
|
||||
};
|
||||
});
|
||||
|
||||
|
||||
@@ -1,50 +0,0 @@
|
||||
/**
|
||||
* 抽离配套件:画布组件对宿主应用仅有的两个依赖,原样搬来使之自足。
|
||||
* - SpPosition 来自 apps/web/src/lib/process-types.ts(业务能力/岗位台账行)
|
||||
* - OwnerSelect 来自 apps/web/src/components/workspace/process/BlockEdit.tsx
|
||||
* 执行人(owner)是按"能力缩写"的软引用:活动框右上角标、属性卡的执行下拉都用它。
|
||||
*/
|
||||
|
||||
/** 能力岗位:工作总览里的角色台账。 */
|
||||
export interface SpPosition {
|
||||
id: string;
|
||||
name: string;
|
||||
abbr: string;
|
||||
icon?: string;
|
||||
/** 绑定的负责人账号 userId(1:1)。 */
|
||||
account?: string;
|
||||
}
|
||||
|
||||
/** 执行人下拉:只能选能力缩写,空=不标;遗留值保留为一项防静默改值。 */
|
||||
export function OwnerSelect({
|
||||
value,
|
||||
onChange,
|
||||
positions,
|
||||
emptyLabel = "(不标执行人)",
|
||||
}: {
|
||||
value: string;
|
||||
onChange: (v: string) => void;
|
||||
positions: SpPosition[] | undefined;
|
||||
emptyLabel?: string;
|
||||
}) {
|
||||
// owner 是按缩写的软引用——同缩写只保留一条(否则重复能力会在下拉里重影)。
|
||||
const seenAbbr = new Set<string>();
|
||||
const options = (positions ?? []).filter((p) => {
|
||||
const abbr = p.abbr.trim();
|
||||
if (!abbr || seenAbbr.has(abbr)) return false;
|
||||
seenAbbr.add(abbr);
|
||||
return true;
|
||||
});
|
||||
const legacy = value && !options.some((p) => p.abbr === value) ? value : null;
|
||||
return (
|
||||
<select value={value} onChange={(e) => onChange(e.target.value)}>
|
||||
<option value="">{emptyLabel}</option>
|
||||
{legacy && <option value={legacy}>{legacy}(遗留)</option>}
|
||||
{options.map((p) => (
|
||||
<option key={p.id} value={p.abbr}>
|
||||
{p.abbr} · {p.name}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user