Appearance
用户指南 12 · 流程设计器二次开发(扩展指南)
流程设计器(npm 包
mldong-flow-designer-plus,canvas/dingtalk双模式)面向 集成方开放三条由轻到重的扩展路径。本文用一个真实的二开需求贯穿全篇: 「参与人从系统用户中选择、参与人处理类用内置字典下拉」,给出每种路径的写法。前置阅读:11 · 流程设计器(字段 ↔ 引擎配置映射、生命周期)。 完整代码样板见 mldong-vben5-wf 仓
apps/web-antd/src/components/flow-designer/(真实集成方,已重绘任务节点属性页)。
1. 扩展全景:三条路径
| 路径 | 手段 | 改动量 | 能力 | 典型场景 |
|---|---|---|---|---|
| A · 表单元数据 | dndPanel[].form(FDFormType) | 最小(纯配置) | 改字段集 / 组件 / 默认值 / 提示 | 静态下拉、加字段、改默认值 |
| B · 自定义控件 | FDFormItemType.render | 小(一个函数) | 任意 Vue 组件,可接远程数据 | 远程用户选择器、字典下拉 |
| C · 事件接管 | dndPanel[].nodeClick / blankContextmenu + 自绘抽屉 | 大(完整 UI) | 完全掌控表单与布局 | 复杂属性页(vben5-wf 现状) |
选择建议:单字段替换选 A;要接接口(用户 / 字典)但不想重绘 UI 选 B; 属性项多、要 Tab 分组 / 联动校验时选 C。三条路径可混用——例如流程属性用内置 (不传
processForm),任务节点用路径 C 自绘。
2. 核心概念:表单元数据
2.1 类型定义
typescript
/** 表单配置(一张表单 = 一组表单项) */
type FDFormType = {
labelWidth?: string; // label 宽度,默认 120px
formItems: Array<FDFormItemType>;
}
/** 表单项 */
type FDFormItemType = {
name: string; // 字段名 → 读写节点 properties.<name>
label?: string; // 标签
component?: 'Input' | 'Select'; // 内置组件二选一(仅这两个值生效)
componentProps?: any; // 传给组件的 props(Select 的 options 等)
render?: (args: any) => VNode; // 自定义渲染,返回 VNode(优先级高于 component)
slot?: string; // 具名插槽名(见 4.3 双模式差异)
helpMessage?: string | Array<string>; // label 旁问号提示
formItemProps?: any;
defaultValue?: any; // 新节点/空值时回填的默认值
}2.2 表单来源优先级(双模式一致)
dndPanel 项的 form(patternItem.form) > 内置 schema(plugins/schema.ts 单一事实源)- 内置字段集:
start/end/task(19 项)/process(15 项)/subProcess/decision/fork/join/custom/edge十组,见包内src/plugins/schema.ts。 - 未配置
form的节点类型 → 自动回退内置 schema,因此不传 form 也能拿到完整字段集。 - 面板只编辑列出的字段;未列出的 properties 字段编辑后原样保留(先写 JSON 再开设计器, 或通过扩展把字段加进面板)。
2.3 表单项渲染规则(FDSchemaForm 分派逻辑)
| 条件 | 渲染 |
|---|---|
component: 'Input' | 内置输入框 FDInput(v-model 绑定 model[name]) |
component: 'Select' | 内置下拉 FDSelect(options 走 componentProps.options,静态,无远程请求) |
slot 有值 | 渲染具名插槽(宿主通过 <FlowDesigner> 传同名插槽内容) |
| 其他 | 调用 render(args) 返回的组件,并自动绑定 v-model / v-model:value / v-model:checked |
3. 路径 A:表单元数据扩展(dndPanel.form)
在 dndPanel 里给目标节点类型配一份 form,即可整体替换(或部分覆盖)该类型的内置表单。 示例:任务节点加入「参与人处理类」静态下拉(内置字典语义)+ 默认值 + 问号提示:
ts
import type { FDPatternItem } from 'mldong-flow-designer-plus';
const dndPanel: FDPatternItem[] = [
{
type: 'snaker:task',
form: {
labelWidth: '120px',
formItems: [
{ name: 'name', label: '唯一编码', component: 'Input', componentProps: { placeholder: '请输入唯一编码' } },
{ name: 'displayName', label: '显示名称', component: 'Input', componentProps: { placeholder: '请输入显示名称' } },
{ name: 'assignee', label: '参与人', component: 'Input', componentProps: { placeholder: '请输入参与人' } },
{
name: 'assignmentHandler',
label: '参与人处理类',
component: 'Select',
defaultValue: 'user',
helpMessage: ['参与人处理类对应引擎 AssignmentHandler SPI 注册名', '留空使用引擎默认处理器'],
componentProps: {
options: [
{ label: '直接指定用户', value: 'user' },
{ label: '按角色解析', value: 'role' },
{ label: '按部门解析', value: 'dept' },
],
},
},
],
},
},
];
<FlowDesigner :dnd-panel="dndPanel" ... />要点:
- 静态 options 预加载:FDSelect 不发起远程请求,字典/用户数据需在组件挂载前拉好塞进
componentProps.options(或走路径 B / C)。 - 默认值:
defaultValue在节点属性为空时回填;已有属性优先。 - 钉钉模式合并策略:自定义
dndPanel与钉钉默认项合并,同名 type 以业务方为准。 - 只补一个字段时不必整份重写——可用
...内置 formItems展开后再追加/覆盖(见 4.2 的 写法参考),或直接提需求把字段补进内置 schema(双模式共用,一次到位)。
4. 路径 B:render 自定义控件(接远程数据)
render 返回任意 Vue 组件的 VNode,控件由业务方实现,可接自家接口。 示例:参与人改为从系统用户选择(远程搜索 + 多选,自行封装一个 UserSelect 组件):
ts
// 业务组件 user-select.vue —— 接自家用户接口
// 必须遵循 v-model 约定:props.modelValue / emit('update:modelValue'),
// 因为 FDSchemaForm 会同时绑定 v-model / v-model:value / v-model:checked
<script setup lang="ts">
defineProps<{ modelValue?: string }>();
const emit = defineEmits<{ (e: 'update:modelValue', v: string): void }>();
// onMounted 拉取 /sys/user/select,选中后 emit('update:modelValue', ids.join(','))
</script>ts
// 在 dndPanel form 中使用(h 来自 vue)
import { h } from 'vue';
import UserSelect from './user-select.vue';
const dndPanel: FDPatternItem[] = [
{
type: 'snaker:task',
form: {
formItems: [
{ name: 'name', label: '唯一编码', component: 'Input' },
{ name: 'displayName', label: '显示名称', component: 'Input' },
{ name: 'assignee', label: '参与人', render: () => h(UserSelect) },
],
},
},
];要点:
- 回写路径不变:render 只接管「控件」,提交仍走内置回写(
name→ changeNodeId、displayName→ updateText、其余字段 → setProperties)。不要在 render 组件里再调 setProperties,避免双写。 - 组件收到的
modelValue即当前节点属性值(字符串);初始化时内置逻辑已从properties[name]/defaultValue回填。 render参数args为渲染上下文(画布模式含当前事件对象data/lf等;钉钉模式 暂为 undefined),业务组件建议自行从接口/prop 取数,不依赖args。- 双模式差异(slot 场景):
slot插槽仅在画布模式内置抽屉透传(drawer.vue会转发 宿主的$slots);钉钉模式内置抽屉未透传插槽——需要插槽时用render,或走路径 C 自绘。
5. 路径 C:事件接管 + 自绘抽屉(vben5-wf 样板)
当属性项多、要 Tab 分组 / 联动 / 校验时,最彻底的做法是接管节点点击事件,自绘抽屉。 这也是 mldong-vben5-wf 集成方的现状(任务节点 4 个 Tab:常规 / 表单 / 高级 / 扩展)。
5.1 接管节点点击(dndPanel[].nodeClick)
ts
// flow-designer.vue(vben5-wf 样板,节选)
const taskDrawerRef = ref();
const dndPanel: FDPatternItem[] = [
{
type: 'snaker:task',
// 重新自定义任务节点点击事件:打开自绘抽屉,把节点数据 + lf 实例传进去
nodeClick: (e: any) => {
taskDrawerRef.value.setData({
record: e.data, // 节点数据(id / text.value / properties)
lf: lfInstance.value, // 设计器实例(回写 API,见 5.4)
});
taskDrawerRef.value.open();
},
},
];事件优先级(双模式一致):dndPanel[].nodeClick > 全局 nodeClick prop > 内置编辑抽屉。 同理可接管:边点击 edgeClick prop(内置分支编辑)、空白右键 blankContextmenu prop (内置流程属性抽屉)。
5.2 接管流程属性(blankContextmenu)
ts
const handleBlankContextmenu = (eventParams: any) => {
eventParams.e?.preventDefault();
processDrawerRef.value?.open(eventParams);
};5.3 自绘抽屉骨架
html
<!-- task-drawer.vue:Tabs + Card 组织属性分组 -->
<BasicDrawer :title="`设置【${record?.text?.value}】节点属性`">
<Tabs @change="handleTabChange">
<TabPane key="base" tab="常规配置">
<Card title="基础信息"><BaseForm :lf-instance="lfInstance" :node-data="record" /></Card>
<Card title="人员配置"><UserForm :lf-instance="lfInstance" :node-data="record" /></Card>
</TabPane>
<!-- 表单配置 / 高级配置 / 扩展配置 ... -->
</Tabs>
</BasicDrawer>
handleTabChange里用lfInstance.getNodeDataById(record.id)刷新 record—— 画布上的实时修改(如改节点 id)要同步回抽屉内表单。
5.4 lfInstance 回写 API 速查
| 方法 | 作用 | 示例 |
|---|---|---|
setProperties(nodeId, { key: value }) | 更新节点 properties 字段 | lf.setProperties('node1', { assignee: 'u1,u2' }) |
deleteProperty(nodeId, key) | 删除节点 properties 字段(清空时用) | lf.deleteProperty('node1', 'assignee') |
updateText(nodeId, text) | 更新节点显示名称(text.value) | lf.updateText('node1', '主管审批') |
changeNodeId(oldId, newId) | 修改节点唯一编码(同步画布与数据) | lf.changeNodeId('node1', 'node1-new') |
getNodeDataById(id) | 取节点最新数据 | lf.getNodeDataById('node1') |
getData() | 导出完整流程 JSON(保存时用) | lf.getData() |
钉钉模式
lf为兼容代理(designerApi),上述方法与画布模式同名同签名——二开代码 双模式通用。
5.5 场景 1:参与人从系统用户选择
ts
// user-form.vue(vben5-wf 样板,节选)—— 表单用自家 ApiSelect 远程组件
const formOptions: VbenFormProps = {
schema: [
{
fieldName: 'assignee',
label: '参与人',
component: 'ApiSelect', // vben5 远程下拉(内部接 /sys/user/select)
componentProps: {
api: '/sys/user/select',
mode: 'multiple', // 多选
showSearch: true,
allowClear: true,
},
},
],
};
// 初始化:properties.assignee 是逗号分隔字符串 → 数组回填
let assignee = props.nodeData?.properties?.assignee;
if (assignee) assignee = assignee.split(',');
formApi.setValues({ ...props.nodeData?.properties, assignee: assignee || [] });
// 回写:数组 → 逗号分隔字符串 → setProperties;空值 → deleteProperty
if (['assignee', 'candidateGroups', 'candidateUsers'].includes(key)) {
props.lfInstance?.setProperties(taskFormData.name, { [key]: n?.join(',') });
} else {
props.lfInstance?.setProperties(taskFormData.name, { [key]: n });
}
// 清空时:
props.lfInstance?.deleteProperty(taskFormData.name, key);5.6 场景 2:参与人处理类内置字典下拉
ts
{
fieldName: 'assignmentHandler',
label: '参与人处理类',
component: 'ApiDict', // vben5 字典下拉(内部接字典接口)
componentProps: {
code: 'wf_assignment_handler', // 字典编码(框架内置)
allowClear: true,
},
},字典编码来自框架
wf_assignment_handler/wf_candidate_handler(分别对应引擎 AssignmentHandler / 候选用户解析 SPI 注册名),实现清单见 07 · 参与者解析。
5.7 唯一编码 / 显示名称回写
ts
// base-form.vue(vben5-wf 样板,节选)
watch(
() => taskFormData.name,
(n, o) => { if (n && o) props.lfInstance?.changeNodeId(o, n); }, // 改唯一编码
);
watch(
() => taskFormData.displayName,
(n) => { props.lfInstance?.updateText(taskFormData.name, n); }, // 改显示名称
);6. 事件与数据流
| 事件 | 触发时机 | 参数 |
|---|---|---|
node-click | 点击任意节点(内置面板动作前触发) | { data, patternItem, lf }(data 为节点数据,patternItem 为 dndPanel 匹配项) |
edge-click | 点击边 | { data, patternItem, lf } |
on-init | 设计器实例初始化完成 | lf(实例,可 getData() 导出) |
on-render | 数据渲染完成 | lf |
update:value | 数据变更(v-model) | 完整流程 JSON |
- 节点点击处理链:
dndPanel[].nodeClick(匹配类型)→ 全局nodeClickprop → 内置 编辑抽屉;边:edgeClickprop → 内置分支编辑;空白:blankContextmenuprop → 内置 流程属性抽屉。 - 未编辑字段保留:无论哪条路径,只要不写某 properties 字段,导出 JSON 均原样保留—— 这是「先写 JSON 再设计器微调」与「扩展面板」两条路线共存的基础。
- 钉钉模式
lf.eventCenter与画布模式同名事件(update:graphModel/update:graphData/update:highlight),兼容业务方已有订阅代码。
7. 常见问题
| 问题 | 原因 | 处理 |
|---|---|---|
| 表单项没渲染 | component 只识别 'Input' / 'Select' | 自定义控件用 render |
| 字段改了节点没变化 | form 项 name 与 properties 字段名不一致 | name 必须是 properties 的顶层 key |
| render 组件值不回写 | 组件未遵循 v-model 约定 | 组件需接收 modelValue 并 emit('update:modelValue') |
| 钉钉模式 slot 不渲染 | 钉钉内置抽屉未透传插槽 | 用 render 或路径 C 自绘 |
| 下拉要远程数据 | FDSelect 无远程加载(路径 A 静态) | 预加载 options / render / 路径 C |
| 导出后字段丢了 | 该字段不在面板字段集内(仅限内置面板场景) | 加进 form / render,或自绘后按需回写;未被写入的字段本身不丢 |
| 改了节点 id 画布与数据不同步 | 只改了表单值没调 changeNodeId | 唯一编码必须走 changeNodeId(路径 C 样板见 5.7) |