Appearance
BizFlowDesigner 流程设计器
审批流设计器(v1,钉钉式结构树形态):垂直节点卡链(发起人 → 审批 / 抄送 / 条件 → 结束),节点间「+」弹出类型菜单插入;条件分支多列渲染(优先级 P1/P2…,分支内子链可继续嵌套条件,列尾可加分支,最多 8 列);点击编辑打开右侧属性面板(审批人类型 / 审批人 tag 列表 / 或签会签 / 条件表达式 / 分支优先级排序);工具栏支持 JSON 导入导出(格式化查看 + 复制 + 解析导入)与结构校验(首节点唯一 start、id 全局唯一、条件至少 2 分支、审批人非空;缺 end 仅告警),校验错误可点击定位到节点并闪烁描边;preview 预览态只读且 start 高亮模拟运行。schema 为纯 JSON(copy-on-write 更新),审批流搭建、OA 流程配置场景的底座。
v1 范围:钉钉式结构树;自由画布拖拽、并行分支、子流程留待 v2。
基础用法
发起人
结束
节点间「+」可插入审批 / 抄送 / 条件节点;工具栏按钮插入到结束节点之前;卡片悬浮右上角可编辑、删除。
默认「发起人 → 结束」骨架:节点间「+」插入节点,工具栏按钮插到结束节点之前,卡片悬浮编辑 / 删除。
<script setup lang="ts">
/** 场景:基础——默认骨架(发起人 → 结束)+ 工具栏添加节点全流程 */
import { ref } from 'vue'
import { BizFlowDesigner } from 'vue3-ep-biz-lib'
import type { BizFlowSchema } from 'vue3-ep-biz-lib'
const schema = ref<BizFlowSchema>()
</script>
<template>
<div>
<BizFlowDesigner v-model="schema" />
<p style="margin: 10px 0 0; font-size: 12px; color: var(--el-text-color-secondary)">
节点间「+」可插入审批 / 抄送 / 条件节点;工具栏按钮插入到结束节点之前;卡片悬浮右上角可编辑、删除。
</p>
</div>
</template>
显示代码
审批人配置
发起人
部门经理审批
成员审批张三或签
财务会签
角色审批财务主管出纳会签
结束
最近一次面板保存:(尚未保存过面板)
属性面板编辑审批人类型(成员 / 角色 / 直属主管)、审批人 tag 列表(回车添加、tag 删除)与或签 / 会签;node-edit 回传补丁。
<script setup lang="ts">
/** 场景:审批人配置与会签——属性面板编辑审批人类型 / 审批人列表 / 或签会签 */
import { ref } from 'vue'
import { BizFlowDesigner } from 'vue3-ep-biz-lib'
import type { BizFlowNode, BizFlowSchema } from 'vue3-ep-biz-lib'
const schema = ref<BizFlowSchema>({
nodes: [
{ id: 'a-start', type: 'start', name: '发起人' },
{
id: 'a-manager',
type: 'approver',
name: '部门经理审批',
assigneeType: 'member',
assignees: ['张三'],
multiSign: 'or',
},
{
id: 'a-finance',
type: 'approver',
name: '财务会签',
assigneeType: 'role',
assignees: ['财务主管', '出纳'],
multiSign: 'and',
},
{ id: 'a-end', type: 'end', name: '结束' },
],
})
const lastPatch = ref('(尚未保存过面板)')
const onNodeEdit = (payload: { id: string; patch: Partial<BizFlowNode> }): void => {
lastPatch.value = `${payload.id} → ${JSON.stringify(payload.patch)}`
}
</script>
<template>
<div>
<BizFlowDesigner v-model="schema" @node-edit="onNodeEdit" />
<p style="margin: 10px 0 0; font-size: 12px; color: var(--el-text-color-secondary); word-break: break-all">
最近一次面板保存:{{ lastPatch }}
</p>
</div>
</template>
显示代码
条件分支
发起人
报销金额判断
共 2 个条件分支
P1小额(≤ 1千)
主管审批
直属主管审批直属主管或签
P2大额(> 1千)
是否加急
共 2 个条件分支
P1加急
总经理审批
成员审批王总或签
P2普通
财务审批
角色审批财务主管会签
结束
「大额」分支内嵌套了「是否加急」二级条件;编辑条件节点可在属性面板调整分支名、条件表达式与优先级顺序。
多分支优先级 P1/P2…;分支列内可继续嵌套条件节点;属性面板调整分支名、条件表达式与优先级顺序;列尾「添加分支」上限 8 列。
<script setup lang="ts">
/** 场景:条件分支——多分支优先级 P1/P2… + 分支内继续嵌套条件,列尾可加分支(≤8) */
import { ref } from 'vue'
import { BizFlowDesigner } from 'vue3-ep-biz-lib'
import type { BizFlowSchema } from 'vue3-ep-biz-lib'
const schema = ref<BizFlowSchema>({
nodes: [
{ id: 'c-start', type: 'start', name: '发起人' },
{
id: 'c-amount',
type: 'condition',
name: '报销金额判断',
branches: [
{
id: 'c-low',
name: '小额(≤ 1千)',
conditionText: 'amount <= 1000',
children: [
{
id: 'c-lead',
type: 'approver',
name: '主管审批',
assigneeType: 'leader',
assignees: ['直属主管'],
multiSign: 'or',
},
],
},
{
id: 'c-high',
name: '大额(> 1千)',
conditionText: 'amount > 1000',
children: [
{
id: 'c-urgent',
type: 'condition',
name: '是否加急',
branches: [
{
id: 'c-urgent-yes',
name: '加急',
conditionText: 'urgent == true',
children: [
{
id: 'c-boss',
type: 'approver',
name: '总经理审批',
assigneeType: 'member',
assignees: ['王总'],
multiSign: 'or',
},
],
},
{
id: 'c-urgent-no',
name: '普通',
conditionText: 'urgent == false',
children: [
{
id: 'c-fin',
type: 'approver',
name: '财务审批',
assigneeType: 'role',
assignees: ['财务主管'],
multiSign: 'and',
},
],
},
],
},
],
},
],
},
{ id: 'c-end', type: 'end', name: '结束' },
],
})
</script>
<template>
<div>
<BizFlowDesigner v-model="schema" />
<p style="margin: 10px 0 0; font-size: 12px; color: var(--el-text-color-secondary)">
「大额」分支内嵌套了「是否加急」二级条件;编辑条件节点可在属性面板调整分支名、条件表达式与优先级顺序。
</p>
</div>
</template>
显示代码
抄送节点
发起人
主管审批
直属主管审批直属主管或签
抄送备案
财务行政
结束
抄送节点无审批动作:编辑面板仅维护节点名称与接收人列表(回车添加、tag 删除)。
抄送节点展示接收人 chips,面板维护接收人列表(无审批动作)。
<script setup lang="ts">
/** 场景:抄送节点——卡片展示接收人 chips,属性面板维护抄送接收人列表 */
import { ref } from 'vue'
import { BizFlowDesigner } from 'vue3-ep-biz-lib'
import type { BizFlowSchema } from 'vue3-ep-biz-lib'
const schema = ref<BizFlowSchema>({
nodes: [
{ id: 'k-start', type: 'start', name: '发起人' },
{
id: 'k-manager',
type: 'approver',
name: '主管审批',
assigneeType: 'leader',
assignees: ['直属主管'],
multiSign: 'or',
},
{
id: 'k-cc',
type: 'cc',
name: '抄送备案',
assignees: ['财务', '行政'],
},
{ id: 'k-end', type: 'end', name: '结束' },
],
})
</script>
<template>
<div>
<BizFlowDesigner v-model="schema" />
<p style="margin: 10px 0 0; font-size: 12px; color: var(--el-text-color-secondary)">
抄送节点无审批动作:编辑面板仅维护节点名称与接收人列表(回车添加、tag 删除)。
</p>
</div>
</template>
显示代码
预览态
预览态:节点只读
发起人
主管审批
直属主管审批直属主管或签
抄送备案
财务
结束
预览态下「+」插入钮、编辑 / 删除入口全部隐藏,发起人节点带 success 高亮描边模拟流程运行。
preview 隐藏全部编辑入口,发起人节点 success 高亮模拟运行。<script setup lang="ts">
/** 场景:预览态——preview=true 隐藏全部编辑入口,start 节点高亮模拟运行 */
import { ref } from 'vue'
import { ElButton } from 'element-plus'
import { BizFlowDesigner } from 'vue3-ep-biz-lib'
import type { BizFlowSchema } from 'vue3-ep-biz-lib'
const preview = ref(true)
const schema = ref<BizFlowSchema>({
nodes: [
{ id: 'p-start', type: 'start', name: '发起人' },
{
id: 'p-approver',
type: 'approver',
name: '主管审批',
assigneeType: 'leader',
assignees: ['直属主管'],
multiSign: 'or',
},
{ id: 'p-cc', type: 'cc', name: '抄送备案', assignees: ['财务'] },
{ id: 'p-end', type: 'end', name: '结束' },
],
})
</script>
<template>
<div>
<div style="margin-bottom: 10px">
<ElButton size="small" @click="preview = !preview">
{{ preview ? '切到设计态' : '切到预览态' }}
</ElButton>
</div>
<BizFlowDesigner v-model="schema" :preview="preview" :height="420" />
<p style="margin: 10px 0 0; font-size: 12px; color: var(--el-text-color-secondary)">
预览态下「+」插入钮、编辑 / 删除入口全部隐藏,发起人节点带 success 高亮描边模拟流程运行。
</p>
</div>
</template>
显示代码
JSON 导入导出
发起人
结束
(未操作)
exportJson 导出格式化 JSON、importJson 解析(try/catch + 结构校验)成功替换画布,失败返回错误文案;非法 type / 残缺 JSON 分别演示两条失败分支。
<script setup lang="ts">
/** 场景:JSON 导入导出——exportJson 持久化、importJson 解析回显(含非法结构 / 残缺 JSON 失败分支) */
import { ref } from 'vue'
import { ElButton, ElInput } from 'element-plus'
import { BizFlowDesigner } from 'vue3-ep-biz-lib'
import type { BizFlowDesignerInstance, BizFlowSchema } from 'vue3-ep-biz-lib'
const schema = ref<BizFlowSchema>()
const designerRef = ref<BizFlowDesignerInstance | null>(null)
const text = ref('')
const tip = ref('(未操作)')
const exportJson = (): void => {
text.value = designerRef.value?.exportJson() ?? ''
tip.value = '已导出当前流程 JSON'
}
const importJson = (): void => {
const result = designerRef.value?.importJson(text.value)
if (result === undefined) return
tip.value = result.ok ? '导入成功,画布已整体替换' : `导入失败:${result.error ?? '未知错误'}`
}
const fillInvalidType = (): void => {
text.value = '{ "nodes": [ { "id": "n1", "type": "hack", "name": "非法类型" } ] }'
tip.value = '已填入非法 type 的 JSON,可点「解析并导入」看结构校验失败提示'
}
const fillBroken = (): void => {
text.value = '{ "nodes": [ { "id": "n1", "type": "start" '
tip.value = '已填入残缺 JSON,可点「解析并导入」看 JSON 解析失败提示'
}
</script>
<template>
<div>
<BizFlowDesigner ref="designerRef" v-model="schema" :height="420" />
<div style="margin-top: 10px; display: flex; gap: 8px; flex-wrap: wrap">
<ElButton size="small" @click="exportJson">导出 JSON</ElButton>
<ElButton size="small" type="primary" @click="importJson">解析并导入</ElButton>
<ElButton size="small" @click="fillInvalidType">填入非法结构</ElButton>
<ElButton size="small" @click="fillBroken">填入残缺 JSON</ElButton>
</div>
<ElInput
v-model="text"
type="textarea"
:rows="6"
placeholder="点击「导出 JSON」或粘贴流程 JSON 到此处"
style="margin-top: 8px"
/>
<p style="margin: 8px 0 0; font-size: 12px; color: var(--el-text-color-secondary)">
{{ tip }}
</p>
</div>
</template>
显示代码
校验与定位
首节点非法
成员审批未配置审批人或签
分支不足
共 1 个条件分支
P1唯一分支
(未校验)
非法 schema(首节点非 start、审批人缺失、分支不足、缺 end 告警)触发校验:画布顶部提示错误清单,点击条目定位节点并闪烁描边。
<script setup lang="ts">
/** 场景:校验——构造非法 schema 触发校验定位(首节点非 start / 审批人缺失 / 分支不足 / 缺 end 告警) */
import { ref } from 'vue'
import { ElButton } from 'element-plus'
import { BizFlowDesigner } from 'vue3-ep-biz-lib'
import type { BizFlowDesignerInstance, BizFlowSchema } from 'vue3-ep-biz-lib'
const schema = ref<BizFlowSchema>({
nodes: [
{ id: 'x-1', type: 'approver', name: '首节点非法', assignees: [] },
{
id: 'x-2',
type: 'condition',
name: '分支不足',
branches: [{ id: 'xb-1', name: '唯一分支', conditionText: '', children: [] }],
},
],
})
const fixedSchema: BizFlowSchema = {
nodes: [
{ id: 'x-fix-start', type: 'start', name: '发起人' },
{
id: 'x-fix-a',
type: 'approver',
name: '主管审批',
assigneeType: 'leader',
assignees: ['直属主管'],
multiSign: 'or',
},
{ id: 'x-fix-end', type: 'end', name: '结束' },
],
}
const designerRef = ref<BizFlowDesignerInstance | null>(null)
const summary = ref('(未校验)')
const run = (): void => {
const result = designerRef.value?.validate()
if (result === undefined) return
summary.value = result.valid
? '校验通过'
: `${result.errors.length} 处错误:${result.errors.map((e) => e.message).join(';')}`
}
const fix = (): void => {
schema.value = fixedSchema
summary.value = '已替换为合法 schema,可再次校验'
}
</script>
<template>
<div>
<BizFlowDesigner ref="designerRef" v-model="schema" :height="420" />
<div style="margin-top: 10px; display: flex; gap: 8px">
<ElButton size="small" type="primary" @click="run">触发校验</ElButton>
<ElButton size="small" @click="fix">一键修复</ElButton>
</div>
<p style="margin: 8px 0 0; font-size: 12px; color: var(--el-text-color-secondary)">
{{ summary }}
</p>
</div>
</template>
显示代码
自定义节点卡
发起人
主管审批2 人
抄送备案1 人
结束
最近点击:(未点击)(插槽替换卡内容,左侧类型色条、hover 操作与 node-click 事件保留)
node 插槽接管节点卡内容,保留卡片容器、类型色条、hover 操作与 node-click。<script setup lang="ts">
/** 场景:自定义节点卡——node 插槽接管节点内容(保留卡片容器、点击与定位行为) */
import { ref } from 'vue'
import { BizFlowDesigner } from 'vue3-ep-biz-lib'
import type { BizFlowNode, BizFlowSchema } from 'vue3-ep-biz-lib'
const schema = ref<BizFlowSchema>({
nodes: [
{ id: 's-start', type: 'start', name: '发起人' },
{
id: 's-a',
type: 'approver',
name: '主管审批',
assigneeType: 'member',
assignees: ['张三', '李四'],
multiSign: 'and',
},
{ id: 's-cc', type: 'cc', name: '抄送备案', assignees: ['财务'] },
{ id: 's-end', type: 'end', name: '结束' },
],
})
const clicked = ref('(未点击)')
const onClick = (node: BizFlowNode): void => {
clicked.value = `${node.name}(${node.type})`
}
const typeColor = (node: BizFlowNode): string =>
({
start: 'var(--el-color-success)',
approver: 'var(--el-color-primary)',
cc: 'var(--el-color-info)',
condition: 'var(--el-color-warning)',
end: 'var(--el-color-danger)',
})[node.type]
</script>
<template>
<div>
<BizFlowDesigner v-model="schema" @node-click="onClick">
<template #node="{ node, readonly }">
<div style="display: flex; align-items: center; gap: 8px; width: 100%">
<span
:style="{
width: 8,
height: 8,
borderRadius: '50%',
flexShrink: 0,
backgroundColor: typeColor(node),
}"
/>
<span style="flex: 1; min-width: 0; font-size: 14px; font-weight: 600; overflow: hidden; text-overflow: ellipsis; white-space: nowrap">
{{ node.name }}
</span>
<span v-if="node.assignees?.length" style="font-size: 12px; color: var(--el-text-color-secondary)">
{{ node.assignees.length }} 人
</span>
<span v-if="readonly" style="font-size: 12px; color: var(--el-text-color-placeholder)">只读</span>
</div>
</template>
</BizFlowDesigner>
<p style="margin: 10px 0 0; font-size: 12px; color: var(--el-text-color-secondary)">
最近点击:{{ clicked }}(插槽替换卡内容,左侧类型色条、hover 操作与 node-click 事件保留)
</p>
</div>
</template>
显示代码
事件联动
发起人
结束
操作流水(最近 8 条)
(暂无操作)
change(结构变化)/ node-click(点击节点)/ node-edit(面板保存补丁)流水回显。
<script setup lang="ts">
/** 场景:事件联动——change / node-click / node-edit 回显最近操作流水 */
import { ref } from 'vue'
import { BizFlowDesigner } from 'vue3-ep-biz-lib'
import type { BizFlowNode, BizFlowSchema } from 'vue3-ep-biz-lib'
const schema = ref<BizFlowSchema>()
const logs = ref<string[]>([])
const push = (line: string): void => {
logs.value = [line, ...logs.value].slice(0, 8)
}
const onChange = (next: BizFlowSchema): void => {
push(`change:顶层 ${next.nodes.length} 个节点`)
}
const onNodeClick = (node: BizFlowNode): void => {
push(`node-click:${node.name}(${node.type})`)
}
const onNodeEdit = (payload: { id: string; patch: Partial<BizFlowNode> }): void => {
push(`node-edit:${payload.id} 修改了 ${Object.keys(payload.patch).join(' / ')}`)
}
</script>
<template>
<div>
<BizFlowDesigner v-model="schema" :height="420" @change="onChange" @node-click="onNodeClick" @node-edit="onNodeEdit" />
<div style="margin-top: 10px">
<div style="font-size: 13px; font-weight: 600; margin-bottom: 6px">操作流水(最近 8 条)</div>
<div
v-for="(line, index) in logs"
:key="`${index}-${line}`"
style="font-size: 12px; color: var(--el-text-color-secondary); line-height: 22px"
>
· {{ line }}
</div>
<div v-if="logs.length === 0" style="font-size: 12px; color: var(--el-text-color-placeholder)">
(暂无操作)
</div>
</div>
</div>
</template>
显示代码
Props
| 属性名 | 类型 | 默认值 | 说明 |
|---|---|---|---|
| v-model | BizFlowSchema | 内置骨架(发起人→结束) | 流程 schema(nodes 首个必须 start) |
| readonly | boolean | false | 只读:隐藏「+」插入钮与编辑 / 删除入口 |
| show-toolbar | boolean | true | 顶部工具栏(左侧添加节点 / 右侧 JSON 与校验) |
| preview | boolean | false | 预览态:只读 + start 节点高亮模拟运行 |
| node-types | BizFlowNodeType[] | ['approver','cc','condition'] | 「+」菜单与工具栏可添加的节点类型 |
| height | number | string | 640 | 画布区高度(内部滚动) |
Events
| 事件名 | 参数 | 说明 |
|---|---|---|
| update:modelValue | (schema: BizFlowSchema) | schema 更新(copy-on-write,始终为新引用) |
| change | (schema: BizFlowSchema) | 结构变化后触发(与 update:modelValue 同步) |
| node-click | (node: BizFlowNode) | 点击节点卡(含预览态) |
| node-edit | (payload: { id: string; patch: Partial<BizFlowNode> }) | 属性面板保存或 panel 插槽调用 patch |
Slots
| 插槽名 | 参数 | 说明 |
|---|---|---|
| node | { node: BizFlowNode; readonly: boolean } | 自定义节点卡内容(保留容器 / 色条 / 定位行为) |
| panel | { node: BizFlowNode; patch: (patch: Partial<BizFlowNode>) => void } | 自定义属性面板整块(patch 立即写回并触发 node-edit) |
| toolbar-extra | — | 工具栏右侧扩展位(JSON / 校验按钮之前) |
| empty | — | 流程为空时画布占位(默认 ElEmpty) |
Expose
| 名称 | 类型 | 说明 |
|---|---|---|
| getSchema | () => BizFlowSchema | 取流程 schema(深拷贝) |
| setSchema | (schema: BizFlowSchema) => void | 整体替换 schema(深拷贝 + 抬高 id 种子防撞号) |
| validate | () => { valid: boolean; errors: BizFlowValidateError[] } | 结构校验;告警项在画布顶部提示条展示 |
| addNode | (type: BizFlowNodeType) => boolean | 添加节点(approver/cc/condition 插到末端 end 前;start/end 仅允许补充一次) |
| removeNode | (id: string) => boolean | 删除节点(start / end 不可删,分支子树整体移除) |
| exportJson | () => string | 导出格式化 JSON 字符串 |
| importJson | (text: string) => { ok: boolean; error?: string } | 解析并导入 JSON(成功替换画布,失败返回错误文案) |