设计器组件
设计器组件
业务用途
设计器是低代码平台的核心生产力工具,让业务人员通过可视化拖拽就能产出可运行的表单、流程、页面、报表,无需编写代码。本平台在 frontend/src/components/ 下内置四套设计器组件:
- FormCreate(
@form-create/designer)-- 表单设计器,拖拽组件拼装表单,产出formJson配置。 - SimpleProcessDesignerV2 -- 钉钉式简易流程设计器,节点编排 + 审批人配置,产出
simpleModel节点树 JSON。 - BpmnDesigner -- 基于 bpmn-js 的专业 BPMN 2.0 流程图设计器,产出标准 BPMN XML。
- FormComponents -- 表单渲染组件库(签名、定位、富文本、附件等),供表单运行时渲染。
涉及文件
frontend/src/components/FormCreate/src/utils.ts-- 表单字段解析工具frontend/src/components/SimpleProcessDesignerV2/src/-- 简易流程设计器(入口 / 节点树 / 节点配置 / 常量 / 组合式函数)frontend/src/components/BpmnDesigner/BpmnDesigner.vue-- BPMN 流程图设计器frontend/src/components/FormComponents/-- 表单渲染组件库frontend/src/views/designer/form/builder.vue-- 表单设计器页面(调用 FcDesigner)frontend/src/views/bpm/model/designNew.vue-- 流程设计页面(调用 SimpleProcessDesigner)
实现机制
一、FormCreate 表单设计器
概述
表单设计器基于第三方库 @form-create/designer(FcDesigner 组件),提供三栏布局:左侧组件面板、中间画布、右侧属性面板。业务侧通过 frontend/src/views/designer/form/builder.vue 对其进行二次封装,增加了数据表字段绑定、子表块、AI 辅助生成等能力。

FcDesigner 的使用
builder.vue 中通过 ref 获取设计器实例,调用其 API 获取 / 设置表单配置:
<template>
<FcDesigner
ref="designerRef"
:config="designerConfig"
:handle="designerHandle"
/>
</template>
<script setup lang="ts">
const designerRef = ref<any>(null)
const designerConfig = reactive({
showSaveBtn: false,
showImportBtn: false,
showJsonBtn: true,
showPreviewBtn: true,
ai: {
api: '/api/ai/form-designer-chat',
token: userStore.token || ''
}
})
</script>核心 API
| 方法 | 作用 | 返回值 |
|---|---|---|
designerRef.value.getRule() | 获取画布上的表单规则 JSON(组件树) | Array<Rule> |
designerRef.value.setRule(rules) | 设置画布表单规则 | -- |
designerRef.value.getOption() | 获取表单全局配置(labelWidth / 提交按钮等) | Object |
designerRef.value.setOption(options) | 设置表单全局配置 | -- |
拖拽机制
FcDesigner 内部基于 vuedraggable 实现拖拽:
- 左侧组件面板:列出所有可用组件(input、select、date、radio 等),每个组件是一个可拖拽的「物料」。
- 中间画布:作为放置区,接收拖入的组件,维护组件树(
rule数组)。 - 右侧属性面板:选中画布组件后,显示并编辑该组件的属性(
field、title、props、validate等)。
拖拽本质上是修改 rule 数组:从面板拖入 = push 新 rule;画布内拖动 = 数组重排;删除 = splice。FcDesigner 封装了这些操作,开发者通过 getRule() / setRule() 读写结果。
配置 JSON(formJson)的生成与保存
表单保存时,builder.vue 的 handleConfirmSave 将 rule + options + 元数据序列化为 formJson 字符串提交后端:
const handleConfirmSave = async () => {
const rule = designerRef.value.getRule()
const options = designerRef.value.getOption()
// 过滤掉子表块标记规则(运行时不需设计器标记)
const runtimeRule = rule.filter((item: any) => !item._childBlock)
const res = await request.post('/form/save', {
id: editingFormId.value,
formName: saveForm.formName,
formCode: saveForm.formCode,
appId: queryAppId ? Number(queryAppId) : null,
tableId: bindTableId.value || null,
approvalEnabled: saveForm.approvalEnabled,
processModelId: saveForm.approvalEnabled ? saveForm.processModelId : null,
formJson: JSON.stringify({
rule: runtimeRule,
options,
description: saveForm.description,
bindTableId: bindTableId.value,
childBlocks: childBlocks.value
})
})
}formJson 的结构:
{
"rule": [
{
"type": "input",
"field": "userName",
"title": "用户名",
"props": { "columnName": "user_name", "placeholder": "请输入用户名" },
"validate": [{ "required": true, "message": "请输入用户名", "trigger": "blur" }]
}
],
"options": { "submitBtn": true, "form": { "labelWidth": "100px" } },
"description": "表单描述",
"bindTableId": 12,
"childBlocks": []
}从数据表字段生成表单
builder.vue 的核心增强能力是「绑定数据表后,一键从字段生成表单」。buildRuleFromField 将数据库列定义转为 form-create rule:
const buildRuleFromField = (field: any): any => {
const type = getFormComponentType(field.columnType, field.htmlType)
const javaField = field.javaField || field.columnName.replace(/_([a-z])/g, (_: string, c: string) => c.toUpperCase())
const rule: any = {
type,
field: javaField, // 表单字段名 = Java 驼峰
title: field.columnComment || field.columnName, // 标题 = 列注释
props: {
columnName: field.columnName, // 原始列名(运行时回写数据库用)
dictType: field.dictType || '',
dictSql: field.dictSql || ''
},
_fc_drag_tag: type
}
// 根据类型补充 props(placeholder / maxlength / valueFormat 等)
if (type === 'datetime') {
rule.props.type = 'datetime'
rule.props.valueFormat = 'YYYY-MM-DD HH:mm:ss'
}
// 必填校验
if (field.isRequired === 'Y') {
rule.validate = [{ required: true, message: `请输入${field.columnComment}`, trigger: 'blur' }]
}
return rule
}字段类型到组件类型的映射规则(getFormComponentType):
| 数据库类型 / htmlType | 表单组件 |
|---|---|
TEXT / textarea | textarea |
INT / BIGINT / DECIMAL | input |
DATETIME / TIMESTAMP | datetime |
DATE | date |
TINYINT / BOOLEAN | switch |
select / radio / checkbox | 对应组件 |
| 其他 | input |
子表块机制
对于主子表关系,builder.vue 用 _childBlock 标记 rule 在设计器中占位,运行时替换为真正的子表明细:
const getChildBlockMarkerRule = (block: any) => ({
type: 'input',
field: `__child_${block.targetTableId}_${block.targetColumn}`,
title: `${block.title}(子表块)`,
props: { disabled: true, placeholder: '运行时显示为可增删的子表明细' },
_childBlock: true, // 标记为子表块
_fc_drag_tag: 'input'
})
// 同步子表块到画布:过滤旧标记 + 追加新标记
const syncChildBlocksToRules = (rules: any[], blocks: any[]) => {
const normalRules = rules.filter((rule: any) => !rule._childBlock)
return [...normalRules, ...blocks.map(getChildBlockMarkerRule)]
}保存时 runtimeRule 过滤掉 _childBlock 标记,子表块的真实配置存在 formJson.childBlocks 中,运行时由 AppRuntime.vue 渲染为可增删的明细表格。
表单字段解析(供流程设计器使用)
frontend/src/components/FormCreate/src/utils.ts 提供 parseFormFields,递归解析 form-create rule,提取扁平的字段列表。该函数被简易流程设计器调用,用于在审批节点配置「表单字段权限」时列出所有字段:
export function parseFormFields(rule: any, fields: any[] = []) {
if (!rule) return fields
if (Array.isArray(rule)) {
rule.forEach((item) => parseFormFields(item, fields))
return fields
}
if (rule.field && rule.title) {
fields.push({
field: rule.field,
title: rule.title,
type: rule.type || 'input',
required: rule.props?.required || rule.required || false
})
}
// 递归 children 和嵌套 rule
if (rule.children && Array.isArray(rule.children)) {
rule.children.forEach((child: any) => parseFormFields(child, fields))
}
if (rule.rule && Array.isArray(rule.rule)) {
rule.rule.forEach((r: any) => parseFormFields(r, fields))
}
return fields
}二、SimpleProcessDesignerV2 钉钉式流程设计器
概述
这是平台自研的「钉钉式」简易流程设计器,位于 frontend/src/components/SimpleProcessDesignerV2/。相比 BPMN 的专业性,它以直观的「节点链 + 分支」形式编排审批流,业务人员无需理解 BPMN 概念即可上手。

目录结构
SimpleProcessDesignerV2/src/
├── index.ts # 导出 SimpleProcessDesigner / SimpleProcessViewer
├── SimpleProcessDesigner.vue # 设计器入口(加载选项数据,提供 inject)
├── SimpleProcessModel.vue # 画布(缩放 / 拖拽 / 导入导出)
├── SimpleProcessViewer.vue # 只读查看器
├── ProcessNodeTree.vue # 节点树递归渲染
├── NodeHandler.vue # 「+」添加节点弹层
├── consts.ts # 节点类型 / 候选人策略 / 枚举定义
├── node.ts # 组合式函数(useNodeForm / useFormFieldsPermission / ...)
├── nodes/ # 各类节点渲染组件
│ ├── StartUserNode.vue # 发起人节点
│ ├── UserTaskNode.vue # 审批节点
│ ├── CopyTaskNode.vue # 抄送节点
│ ├── ExclusiveNode.vue # 条件分支
│ ├── ParallelNode.vue # 并行分支
│ ├── InclusiveNode.vue # 包容分支
│ ├── DelayTimerNode.vue # 延迟器
│ ├── RouterNode.vue # 路由分支
│ ├── TriggerNode.vue # 触发器
│ ├── ChildProcessNode.vue # 子流程
│ └── EndEventNode.vue # 结束节点
├── nodes-config/ # 节点配置抽屉
│ ├── UserTaskNodeConfig.vue # 审批人配置
│ ├── CopyTaskNodeConfig.vue # 抄送人配置
│ ├── ConditionNodeConfig.vue # 条件配置
│ ├── StartUserNodeConfig.vue # 发起人配置
│ └── ...
└── utils.ts节点数据结构(SimpleFlowNode)
consts.ts 定义了核心的 SimpleFlowNode 接口,这是整个设计器的数据模型:
export interface SimpleFlowNode {
id: string
type: NodeType
name: string
showText?: string // 节点展示文案(如「指定成员:张三」)
childNode?: SimpleFlowNode // 下游节点(链式)
conditionNodes?: SimpleFlowNode[] // 分支节点(条件 / 并行 / 包容)
candidateStrategy?: number // 候选人策略
candidateParam?: string // 候选人参数
approveMethod?: ApproveMethodType // 多人审批方式
approveRatio?: number // 通过比例
fieldsPermission?: Array<Record<string, any>> // 表单字段权限
// ... 拒绝处理 / 超时处理 / 为空处理 / 监听器 / 条件设置 / 等
}数据结构是链式 + 分支的树:
发起人节点 (START_USER_NODE)
└── childNode: 审批节点 (USER_TASK_NODE)
└── childNode: 条件分支 (CONDITION_BRANCH_NODE)
├── conditionNodes[0]: 条件1
│ └── childNode: 审批节点
└── conditionNodes[1]: 其它情况
└── childNode: 结束节点 (END_EVENT_NODE)节点类型
consts.ts 定义了全部节点类型枚举:
export const NodeType = {
END_EVENT_NODE: 1, // 结束节点
START_USER_NODE: 10, // 发起人节点
USER_TASK_NODE: 11, // 审批节点
COPY_TASK_NODE: 12, // 抄送节点
TRANSACTOR_NODE: 13, // 办理节点
DELAY_TIMER_NODE: 14, // 延迟器
TRIGGER_NODE: 15, // 触发器
CHILD_PROCESS_NODE: 20, // 子流程
CONDITION_NODE: 50, // 条件
CONDITION_BRANCH_NODE: 51, // 条件分支(排他网关)
PARALLEL_BRANCH_NODE: 52, // 并行分支
INCLUSIVE_BRANCH_NODE: 53, // 包容分支
ROUTER_BRANCH_NODE: 54 // 路由分支
} as const添加节点机制(NodeHandler)
NodeHandler.vue 是每个节点底部的「+」按钮,点击弹出节点类型选择面板。addNode 根据类型创建带默认配置的 SimpleFlowNode,通过 update:childNode 事件插入到当前节点下游:
const addNode = (type: number) => {
popoverShow.value = false
if (type === NodeType.USER_TASK_NODE || type === NodeType.TRANSACTOR_NODE) {
const data: SimpleFlowNode = {
id: 'Activity_' + generateUUID(),
name: NODE_DEFAULT_NAME.get(type) as string,
showText: '',
type: type,
approveMethod: ApproveMethodType.SEQUENTIAL_APPROVE, // 默认依次审批
rejectHandler: { type: RejectHandlerType.FINISH_PROCESS }, // 默认拒绝=终止流程
timeoutHandler: { enable: false },
assignEmptyHandler: { type: AssignEmptyHandlerType.APPROVE },
assignStartUserHandlerType: AssignStartUserHandlerType.START_USER_AUDIT,
childNode: props.childNode, // 新节点插在当前节点和原下游之间
taskCreateListener: { enable: false },
taskAssignListener: { enable: false },
taskCompleteListener: { enable: false }
}
emits('update:childNode', data)
}
if (type === NodeType.CONDITION_BRANCH_NODE) {
const data: SimpleFlowNode = {
name: '条件分支',
type: NodeType.CONDITION_BRANCH_NODE,
id: 'GateWay_' + generateUUID(),
childNode: props.childNode,
conditionNodes: [
{ id: 'Flow_' + generateUUID(), name: '条件1', type: NodeType.CONDITION_NODE,
conditionSetting: { defaultFlow: false, conditionType: ConditionType.RULE,
conditionGroups: cloneDeep(DEFAULT_CONDITION_GROUP_VALUE) } },
{ id: 'Flow_' + generateUUID(), name: '其它情况', type: NodeType.CONDITION_NODE,
showText: '未满足其它条件时,将进入此分支',
conditionSetting: { defaultFlow: true } }
]
}
emits('update:childNode', data)
}
// ... 其他节点类型
}插入逻辑
新节点的 childNode 设为原下游节点(props.childNode),然后通过 update:childNode 事件让父组件把新节点设为当前节点的 childNode。这样新节点就「插队」到了当前节点和原下游之间,而非追加到链尾。
可选的节点类型(NodeHandler 模板中):审批人、办理人、抄送、条件分支、并行分支、包容分支、延迟器、路由分支、触发器、子流程。
节点树递归渲染(ProcessNodeTree)
ProcessNodeTree.vue 根据 currentNode.type 渲染对应的节点组件,并递归渲染 childNode 和 conditionNodes:
<template>
<!-- 根据类型渲染当前节点 -->
<StartUserNode v-if="currentNode.type === NodeType.START_USER_NODE" :flow-node="currentNode" />
<UserTaskNode v-if="currentNode.type === NodeType.USER_TASK_NODE" :flow-node="currentNode" ... />
<CopyTaskNode v-if="currentNode.type === NodeType.COPY_TASK_NODE" :flow-node="currentNode" ... />
<ExclusiveNode v-if="currentNode.type === NodeType.CONDITION_BRANCH_NODE" :flow-node="currentNode" ... />
<!-- ... 其他节点类型 -->
<!-- 递归渲染下游节点 -->
<ProcessNodeTree v-if="currentNode.childNode" :flow-node="currentNode.childNode" />
</template>每个节点组件内部渲染节点卡片(名称 + showText + 操作按钮)和底部的 NodeHandler,节点配置通过点击节点卡片打开 el-drawer 抽屉。
候选人策略(审批人配置)
consts.ts 定义了 15 种候选人策略,是审批人配置的核心:
export const CandidateStrategy = {
ROLE: 10, // 指定角色
DEPT_MEMBER: 20, // 部门成员
DEPT_LEADER: 21, // 部门负责人
MULTI_LEVEL_DEPT_LEADER: 23, // 连续多级部门负责人
POST: 22, // 指定岗位
USER: 30, // 指定成员
APPROVE_USER_SELECT: 34, // 审批人自选
START_USER_SELECT: 35, // 发起人自选
START_USER: 36, // 发起人本人
START_USER_DEPT_LEADER: 37, // 发起人部门负责人
START_USER_MULTI_LEVEL_DEPT_LEADER: 38, // 发起人连续部门负责人
USER_GROUP: 40, // 用户组
FORM_USER: 50, // 表单内用户字段
FORM_DEPT_LEADER: 51, // 表单内部门负责人
EXPRESSION: 60 // 流程表达式
} as const候选人参数序列化
node.ts 的 handleCandidateParam / parseCandidateParam 负责候选人参数的序列化与反序列化。不同策略的参数格式不同:
const handleCandidateParam = () => {
switch (configForm.value.candidateStrategy) {
case CandidateStrategy.USER:
return configForm.value.userIds!.join(',') // "1,2,3"
case CandidateStrategy.ROLE:
return configForm.value.roleIds!.join(',')
case CandidateStrategy.MULTI_LEVEL_DEPT_LEADER:
// 格式:部门IDs|层级,如 "1,2|3"
return configForm.value.deptIds!.join(',').concat('|' + configForm.value.deptLevel)
case CandidateStrategy.FORM_DEPT_LEADER:
// 格式:表单字段|层级,如 "deptField|3"
return configForm.value.formDept!.concat('|' + configForm.value.deptLevel)
// ...
}
}审批人配置抽屉(UserTaskNodeConfig)
nodes-config/UserTaskNodeConfig.vue 是审批节点的配置面板,以 el-drawer 形式打开,包含多个 Tab:
<el-drawer v-model="settingVisible" :size="580" :before-close="saveConfig">
<template #header>
<!-- 节点名称可编辑 -->
<input v-if="showInput" v-model="nodeName" @blur="blurEvent()" />
<div v-else>{{ nodeName }} <Icon icon="ep:edit-pen" @click="clickIcon()" /></div>
</template>
<!-- 审批类型:人工审批 / 自动通过 / 自动拒绝 -->
<el-radio-group v-model="approveType">...</el-radio-group>
<el-tabs type="border-card" v-model="activeTabName">
<el-tab-pane :label="`${nodeTypeName}人`" name="user">
<!-- 候选人策略单选 + 对应的选择器(用户/角色/部门/岗位/...) -->
</el-tab-pane>
<!-- 表单权限 / 操作按钮 / 超时处理 / 拒绝处理 等其他 Tab -->
</el-tabs>
</el-drawer>候选人策略变化时,changeCandidateStrategy 切换显示对应的选择器(用户多选、角色多选、部门树、岗位多选等),选项数据由 SimpleProcessDesigner.vue 在 onMounted 时加载并通过 provide 注入。
设计器入口(SimpleProcessDesigner)
SimpleProcessDesigner.vue 是设计器入口,负责:
- 加载数据:
onMounted时并发加载角色、岗位、用户、部门、用户组列表,通过provide注入子组件。 - 加载表单字段:监听
modelFormId变化,调用getForm获取流程表单字段,provide给节点配置做字段权限。 - 初始化节点树:无数据时调用
updateModel创建默认的「发起人 -> 结束」骨架。 - 保存:
saveSimpleFlowModel将节点树写入processData(inject 的响应式 ref)并 emitsuccess。
const updateModel = () => {
if (!processNodeTree.value) {
processNodeTree.value = {
name: '发起人',
type: NodeType.START_USER_NODE,
id: NodeId.START_USER_NODE_ID,
childNode: {
id: NodeId.END_EVENT_NODE_ID,
name: '结束',
type: NodeType.END_EVENT_NODE
}
}
saveSimpleFlowModel(processNodeTree.value)
}
}
const saveSimpleFlowModel = async (simpleModelNode: SimpleFlowNode) => {
processData.value = simpleModelNode // 回写父组件
emits('success', simpleModelNode)
}provide / inject 数据流
SimpleProcessDesigner.vue 通过 provide 向所有子节点组件注入数据:
provide('formFields', formFields) // 流程表单字段(JSON 字符串数组)
provide('formType', formType) // 表单类型
provide('roleList', roleOptions) // 角色列表
provide('postList', postOptions) // 岗位列表
provide('userList', userOptions) // 用户列表
provide('deptList', deptOptions) // 部门列表
provide('deptTree', deptTreeOptions) // 部门树
provide('userGroupList', userGroupOptions) // 用户组列表
provide('startUserIds', props.startUserIds)
provide('startDeptIds', props.startDeptIds)
provide('processNodeTree', processNodeTree)子组件(如 UserTaskNodeConfig)通过 inject 获取:
const roleOptions = inject<Ref<RoleApi.RoleVO[]>>('roleList', ref([]))
const postOptions = inject<Ref<PostApi.PostVO[]>>('postList', ref([]))
const userOptions = inject<Ref<UserApi.UserVO[]>>('userList', ref([]))
const deptOptions = inject<Ref<DeptApi.DeptVO[]>>('deptList', ref([]))
const formFields = inject<Ref<string[]>>('formFields', ref([]))simpleModel 的保存
simpleModel 就是 SimpleFlowNode 树 JSON。流程设计页面 designNew.vue 通过 provide('processData', ...) 传入响应式 ref,设计器修改后写回 processData.value,页面再将其序列化提交后端:
// designNew.vue 中
const processData = ref<SimpleFlowNode>()
provide('processData', processData)
// 保存时
const handleSaveAndDeploy = async () => {
await request.post('/bpm/model/save', {
...formData,
simpleModel: JSON.stringify(processData.value) // 节点树序列化
})
}三、BpmnDesigner bpmn-js 流程设计器
概述
frontend/src/components/BpmnDesigner/BpmnDesigner.vue 基于 bpmn-js/lib/Modeler 封装,提供专业级 BPMN 2.0 流程图设计能力。与简易流程设计器不同,它产出的是标准 BPMN XML,适用于复杂流程场景。

初始化 Modeler
import BpmnModeler from 'bpmn-js/lib/Modeler'
import 'bpmn-js/dist/assets/diagram-js.css'
import 'bpmn-js/dist/assets/bpmn-js.css'
import 'bpmn-js/dist/assets/bpmn-font/css/bpmn.css'
let modeler: any = null
const initModeler = async () => {
modeler = new BpmnModeler({
container: canvasRef.value,
additionalModules: [],
moddleExtensions: {},
keyboard: { bindTo: document } // 支持快捷键撤销/重做
})
// 监听选择变化 -> 显示属性面板
modeler.on('selection.changed', (e: any) => {
const element = e.newSelection[0]
if (element) selectElement(element)
else clearSelection()
})
// 监听元素变化 -> 导出 XML
modeler.on('element.changed', (e: any) => {
if (selectedElement.value && e.element.id === selectedElement.value.id) {
updateSelectedElementInfo(e.element)
}
exportXml()
})
// 监听命令栈变化 -> 更新撤销/重做按钮状态
modeler.on('commandStack.changed', () => {
canUndo.value = modeler.get('commandStack').canUndo()
canRedo.value = modeler.get('commandStack').canRedo()
exportXml()
})
// 加载 XML(外部传入或默认)
const xml = props.modelValue || DEFAULT_BPMN_XML
await modeler.importXML(xml)
modeler.get('canvas').zoom('fit-viewport')
}双向绑定 BPMN XML
组件通过 v-model 与父组件双向绑定 BPMN XML 字符串:
const props = defineProps<{
modelValue?: string // BPMN XML
userList?: any[]
roleList?: any[]
deptTree?: any[]
postList?: any[]
userGroupList?: any[]
}>()
const emit = defineEmits<{
(e: 'update:modelValue', value: string): void
(e: 'change', value: string): void
}>()
// 画布变化时导出 XML 并 emit
const exportXml = async () => {
if (!modeler) return
const { xml } = await modeler.saveXML({ format: true })
emit('update:modelValue', xml)
emit('change', xml)
}属性面板与审批配置
右侧属性面板根据选中元素的 BPMN 类型(bpmn:UserTask / bpmn:Gateway / bpmn:StartEvent / bpmn:EndEvent)显示不同配置:
const updateSelectedElementInfo = (element: any) => {
const type = element.businessObject?.$type || ''
selectedElementType.value = type.replace('bpmn:', '')
isUserTask.value = type === 'bpmn:UserTask'
isGateway.value = type.includes('Gateway')
isStartEvent.value = type === 'bpmn:StartEvent'
isEndEvent.value = type === 'bpmn:EndEvent'
}UserTask 的审批配置通过 BPMN 扩展属性(extensionElements)存储,与标准 BPMN 兼容:
const saveApprovalProperties = () => {
const moddle = modeler.get('moddle')
const modeling = modeler.get('modeling')
const bo = selectedElement.value.businessObject
let extensionElements = bo.extensionElements
if (!extensionElements) {
extensionElements = moddle.create('bpmn:ExtensionElements', { values: [] })
}
// 移除旧的审批属性
extensionElements.values = extensionElements.values.filter(
(e: any) => e.$type !== 'approval:properties'
)
// 创建新的审批属性节点
const approvalProps = moddle.create('approval:properties', {
approvalType: elementProperties.approvalType,
approveMethod: elementProperties.approveMethod,
approveRatio: elementProperties.approveRatio,
canReject: elementProperties.canReject,
userIds: JSON.stringify(elementProperties.userIds),
roleIds: JSON.stringify(elementProperties.roleIds),
// ...
})
extensionElements.values.push(approvalProps)
modeling.updateProperties(selectedElement.value, { extensionElements })
}工具栏能力
- 撤销 / 重做:
modeler.get('commandStack').undo() / redo() - 缩放:
canvas.zoom(zoom * 1.2)/canvas.zoom(zoom / 1.2)/canvas.zoom('fit-viewport') - 导出 SVG:
modeler.saveSVG() - 导出 BPMN:
modeler.saveXML({ format: true }) - 导入 BPMN:弹窗粘贴 XML,
modeler.importXML(xml) - getXml():
defineExpose暴露给父组件获取当前 XML
用法示例
<template>
<BpmnDesigner
v-model="bpmnXml"
:userList="userList"
:roleList="roleList"
:deptTree="deptTree"
@change="onBpmnChange"
/>
</template>
<script setup lang="ts">
import BpmnDesigner from '@/components/BpmnDesigner/BpmnDesigner.vue'
const bpmnXml = ref('')
const onBpmnChange = (xml: string) => {
console.log('BPMN XML 已更新')
}
</script>四、FormComponents 表单渲染组件库
概述
frontend/src/components/FormComponents/ 是一组自定义表单渲染组件,作为 @form-create 的扩展组件库,在表单运行时渲染。当表单设计器中的标准组件(input / select)无法满足复杂场景时,使用这些组件。
组件清单
frontend/src/components/FormComponents/index.js 统一导出:
export { default as Signature } from './Signature.vue' // 手写签名
export { default as Location } from './Location.vue' // 地理位置
export { default as Discount } from './Discount.vue' // 折扣输入
export { default as Statistics } from './Statistics.vue' // 统计计算
export { default as Attachment } from './Attachment.vue' // 附件上传
export { default as RichTextEditor } from './RichTextEditor.vue' // 富文本编辑器代表组件:RichTextEditor
RichTextEditor.vue 基于 contenteditable div 实现轻量富文本编辑,通过 v-model 双向绑定 HTML:
<template>
<div class="rich-text-editor" :class="{ readonly }">
<div
ref="editorRef"
class="rich-text-content"
:contenteditable="!readonly"
:style="{ height: height + 'px' }"
@input="handleInput"
@blur="handleBlur"
></div>
</div>
</template>
<script setup lang="ts">
const props = defineProps<{
modelValue?: string
placeholder?: string
height?: number
readonly?: boolean
}>()
const emit = defineEmits(['update:modelValue'])
const editorRef = ref<HTMLDivElement | null>(null)
// 外部值变化时同步到编辑器(避免光标跳动,只在值不同时更新)
watch(() => props.modelValue, (val) => {
if (editorRef.value && editorRef.value.innerHTML !== (val || '')) {
editorRef.value.innerHTML = val || ''
}
}, { immediate: true })
const handleInput = () => {
if (editorRef.value) emit('update:modelValue', editorRef.value.innerHTML)
}
</script>为什么用 contenteditable 而非第三方富文本库
contenteditable 是浏览器原生能力,零依赖、体积小。对于低代码平台表单中「备注」「描述」等轻量富文本场景足够用。如需复杂排版(表格、图片、视频),可替换为 wangEditor / TinyMCE 等专业库。
Attachment 附件上传
Attachment.vue 使用 el-upload,上传地址为 /api/upload(注意:el-upload 不走 axios,需写完整 /api 路径,详见 ):
<el-upload :action="action || '/api/upload'" :headers="uploadHeaders" ...>操作步骤
使用表单设计器创建并保存表单
- 进入「页面设计 -> 表单管理」,点击「新建表单」进入
designer/form/builder。 - 在顶部工具栏「绑定数据表」下拉中选择已有表(或选「自动创建新表」)。
- 点击「快速生成」一键从表字段生成表单,或在左侧「数据字段」面板逐个点击字段添加。
- 在画布中拖拽调整组件顺序,右侧属性面板编辑组件属性。
- 点击「保存表单」,填写表单名称、编码,可选启用审批并绑定流程。
- 保存后表单
formJson存入后端,可在运行时通过form-create渲染。
使用简易流程设计器设计审批流
- 进入「流程中心 -> 流程模型」,新建或编辑流程,进入
designNew页面。 - 完成「基本信息」和「表单设计」步骤后,进入「审批流程」步骤。
SimpleProcessDesigner显示默认的「发起人 -> 结束」骨架。- 点击节点间的「+」按钮,选择要添加的节点类型(审批人 / 抄送 / 条件分支等)。
- 点击审批节点,在右侧抽屉配置候选人策略(指定成员 / 角色 / 部门等)、审批方式(依次 / 会签 / 或签)、表单字段权限。
- 条件分支节点配置条件规则(字段 + 操作符 + 值)或条件表达式。
- 点击「保存并部署」,
simpleModel节点树 JSON 提交后端并部署为流程定义。
使用 BpmnDesigner
- 在流程设计页面选择「BPMN 设计模式」。
- 从左侧元素面板拖拽节点(开始事件 / 用户任务 / 网关 / 结束事件)到画布。
- 连接节点形成流转路径。
- 选中 UserTask,在右侧属性面板配置审批人类型、审批方式、操作权限。
- 配置自动保存到
v-model绑定的 BPMN XML 字符串。 - 保存时 XML 提交后端,后端解析为 Flowable / Camunda 流程定义。
常见问题
FcDesigner 的 getRule() 返回空
- 确认
designerRef.value已挂载(onMounted中通过nextTick等待)。 - 确认
FcDesigner已在main.ts中app.use(FcDesigner)注册。 - 如果是路由懒加载进入,确保 DOM 渲染完成后再调用
getRule()。
简易流程设计器节点配置抽屉打不开
节点配置抽屉由各节点组件内部控制。如果点击节点无反应:
- 检查
SimpleProcessDesigner的provide是否注入了processNodeTree。 - 确认
modelFormId传入正确(否则表单字段加载失败,但不应阻止抽屉打开)。 - 检查浏览器控制台是否有子组件渲染错误。
简易流程设计器候选人选择器选项为空
候选人选项(用户 / 角色 / 部门等)由 SimpleProcessDesigner.vue 在 onMounted 时加载并 provide。如果为空:
- 检查网络面板,确认
getSimpleRoleList/getSimpleUserList等接口返回正常。 - 确认接口未返回 403(权限不足)。
- 检查
provide的 key 与子组件inject的 key 完全一致(如'roleList')。
BpmnDesigner 属性面板不显示
属性面板在选中元素后才显示(v-show="selectedElement")。如果选中后仍不显示:
- 确认
modeler.on('selection.changed')事件已绑定。 - 检查
selectElement函数是否正确设置了selectedElement.value。 - 确认
canvasRef已正确挂载(onMounted中调用initModeler)。
BPMN XML 循环更新
watch(() => props.modelValue) 监听外部 XML 变化时会重新 importXML,可能触发 element.changed -> exportXml -> emit('update:modelValue') -> 父组件更新 -> 再次触发 watch。组件通过「比较 XML 是否相同」避免循环:
watch(() => props.modelValue, async (newXml) => {
if (!modeler || !newXml) return
const { xml } = await modeler.saveXML({ format: true })
if (xml !== newXml) { // 内容不同才重新导入
await modeler.importXML(newXml)
}
})如果仍出现循环,检查父组件是否在 @change / @update:modelValue 中又修改了 modelValue。
下一步
- 回顾路由如何保护设计器页面:
- 了解请求层如何与设计器 API 交互:
