Skip to content

用户指南 12 · 流程设计器二次开发(扩展指南)

流程设计器(npm 包 mldong-flow-designer-pluscanvas / dingtalk 双模式)面向 集成方开放三条由轻到重的扩展路径。本文用一个真实的二开需求贯穿全篇: 「参与人从系统用户中选择、参与人处理类用内置字典下拉」,给出每种路径的写法。

前置阅读:11 · 流程设计器(字段 ↔ 引擎配置映射、生命周期)。 完整代码样板见 mldong-vben5-wf 仓 apps/web-antd/src/components/flow-designer/ (真实集成方,已重绘任务节点属性页)。

1. 扩展全景:三条路径

路径手段改动量能力典型场景
A · 表单元数据dndPanel[].formFDFormType最小(纯配置)改字段集 / 组件 / 默认值 / 提示静态下拉、加字段、改默认值
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(匹配类型)→ 全局 nodeClick prop → 内置 编辑抽屉;边:edgeClick prop → 内置分支编辑;空白:blankContextmenu prop → 内置 流程属性抽屉。
  • 未编辑字段保留:无论哪条路径,只要不写某 properties 字段,导出 JSON 均原样保留—— 这是「先写 JSON 再设计器微调」与「扩展面板」两条路线共存的基础。
  • 钉钉模式 lf.eventCenter 与画布模式同名事件(update:graphModel / update:graphData / update:highlight),兼容业务方已有订阅代码。

7. 常见问题

问题原因处理
表单项没渲染component 只识别 'Input' / 'Select'自定义控件用 render
字段改了节点没变化formname 与 properties 字段名不一致name 必须是 properties 的顶层 key
render 组件值不回写组件未遵循 v-model 约定组件需接收 modelValueemit('update:modelValue')
钉钉模式 slot 不渲染钉钉内置抽屉未透传插槽render 或路径 C 自绘
下拉要远程数据FDSelect 无远程加载(路径 A 静态)预加载 options / render / 路径 C
导出后字段丢了该字段不在面板字段集内(仅限内置面板场景)加进 form / render,或自绘后按需回写;未被写入的字段本身不丢
改了节点 id 画布与数据不同步只改了表单值没调 changeNodeId唯一编码必须走 changeNodeId(路径 C 样板见 5.7)

jeeflow · 轻量级多语言工作流引擎