Vue动态表单技术实现文档
概述
本文档详细记录了 Flowable系统中实现Vue动态表单加载和工作流集成的完整技术方案。该方案实现了Vue组件的动态导入、自动注册和在Flowable工作流中的无缝集成。
架构设计
1. 系统架构图
2. 核心技术栈
- 前端: Vue.js 2.6.12 + Element UI + Webpack动态导入
- 后端: Spring Boot + MyBatis-Plus
- 工作流: Flowable 6.8.0
- 数据库: MySQL 8.0
数据库设计
1. 表单组件映射表
-- 创建表单组件映射表
CREATE TABLE `sys_form_component` (
`id` bigint NOT NULL AUTO_INCREMENT COMMENT '主键',
`component_key` varchar(100) NOT NULL COMMENT '组件标识',
`component_name` varchar(100) NOT NULL COMMENT '组件名称',
`component_path` varchar(255) NOT NULL COMMENT '组件路径',
`description` varchar(500) DEFAULT NULL COMMENT '组件描述',
`status` char(1) DEFAULT '0' COMMENT '状态(0正常 1停用)',
`create_by` varchar(64) DEFAULT '' COMMENT '创建者',
`create_time` datetime DEFAULT NULL COMMENT '创建时间',
`update_by` varchar(64) DEFAULT '' COMMENT '更新者',
`update_time` datetime DEFAULT NULL COMMENT '更新时间',
`remark` varchar(500) DEFAULT NULL COMMENT '备注',
PRIMARY KEY (`id`),
UNIQUE KEY `uk_component_key` (`component_key`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='表单组件映射表';
2. 初始化数据
-- 插入示例组件数据
INSERT INTO `sys_form_component` VALUES
(1, 'accident-report', '事故报告表单', '@/components/CustomForms/AccidentReport', '用于安全事故报告的自定义表单', '0', 'admin', NOW(), 'admin', NOW(), '安全管理模块表单'),
(2, 'indicator-entry', '指标录入表单', '@/components/CustomForms/IndicatorEntry', '用于绩效指标数据录入的表单', '0', 'admin', NOW(), 'admin', NOW(), '指标管理模块表单');
后端实现
1. 实体类设计
// ruoyi-system/src/main/java/com/ruoyi/system/domain/SysFormComponent.java
package com.ruoyi.system.domain;
import com.ruoyi.common.annotation.Excel;
import com.ruoyi.common.core.domain.BaseEntity;
import org.apache.commons.lang3.builder.ToStringBuilder;
import org.apache.commons.lang3.builder.ToStringStyle;
/**
* 表单组件映射对象 sys_form_component
*
* @author ruoyi
* @date 2023-07-07
*/
public class SysFormComponent extends BaseEntity
{
private static final long serialVersionUID = 1L;
/** 主键 */
private Long id;
/** 组件标识 */
@Excel(name = "组件标识")
private String componentKey;
/** 组件名称 */
@Excel(name = "组件名称")
private String componentName;
/** 组件路径 */
@Excel(name = "组件路径")
private String componentPath;
/** 组件描述 */
@Excel(name = "组件描述")
private String description;
/** 状态(0正常 1停用) */
@Excel(name = "状态", readConverterExp = "0=正常,1=停用")
private String status;
// 省略getter/setter方法...
public Long getId()
{
return id;
}
public void setId(Long id)
{
this.id = id;
}
public void setComponentKey(String componentKey)
{
this.componentKey = componentKey;
}
public String getComponentKey()
{
return componentKey;
}
// ... 其他getter/setter方法
}
2. Mapper接口
// ruoyi-system/src/main/java/com/ruoyi/system/mapper/SysFormComponentMapper.java
package com.ruoyi.system.mapper;
import java.util.List;
import com.ruoyi.system.domain.SysFormComponent;
/**
* 表单组件映射Mapper接口
*
* @author ruoyi
* @date 2023-07-07
*/
public interface SysFormComponentMapper
{
/**
* 查询表单组件映射
*
* @param id 表单组件映射主键
* @return 表单组件映射
*/
public SysFormComponent selectSysFormComponentById(Long id);
/**
* 根据组件标识查询表单组件
*
* @param componentKey 组件标识
* @return 表单组件映射
*/
public SysFormComponent selectSysFormComponentByKey(String componentKey);
/**
* 查询表单组件映射列表
*
* @param sysFormComponent 表单组件映射
* @return 表单组件映射集合
*/
public List<SysFormComponent> selectSysFormComponentList(SysFormComponent sysFormComponent);
/**
* 查询所有表单组件列表(不分页)
*
* @return 表单组件映射集合
*/
public List<SysFormComponent> selectAllFormComponents();
/**
* 新增表单组件映射
*
* @param sysFormComponent 表单组件映射
* @return 结果
*/
public int insertSysFormComponent(SysFormComponent sysFormComponent);
/**
* 修改表单组件映射
*
* @param sysFormComponent 表单组件映射
* @return 结果
*/
public int updateSysFormComponent(SysFormComponent sysFormComponent);
/**
* 删除表单组件映射
*
* @param id 表单组件映射主键
* @return 结果
*/
public int deleteSysFormComponentById(Long id);
/**
* 批量删除表单组件映射
*
* @param ids 需要删除的数据主键集合
* @return 结果
*/
public int deleteSysFormComponentByIds(Long[] ids);
}
3. Mapper XML配置
<!-- ruoyi-system/src/main/resources/mapper/system/SysFormComponentMapper.xml -->
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.ruoyi.system.mapper.SysFormComponentMapper">
<resultMap type="SysFormComponent" id="SysFormComponentResult">
<result property="id" column="id" />
<result property="componentKey" column="component_key" />
<result property="componentName" column="component_name" />
<result property="componentPath" column="component_path" />
<result property="description" column="description" />
<result property="status" column="status" />
<result property="createBy" column="create_by" />
<result property="createTime" column="create_time" />
<result property="updateBy" column="update_by" />
<result property="updateTime" column="update_time" />
<result property="remark" column="remark" />
</resultMap>
<sql id="selectSysFormComponentVo">
select id, component_key, component_name, component_path, description, status, create_by, create_time, update_by, update_time, remark from sys_form_component
</sql>
<select id="selectSysFormComponentList" parameterType="SysFormComponent" resultMap="SysFormComponentResult">
<include refid="selectSysFormComponentVo"/>
<where>
<if test="componentKey != null and componentKey != ''"> and component_key like concat('%', #{componentKey}, '%')</if>
<if test="componentName != null and componentName != ''"> and component_name like concat('%', #{componentName}, '%')</if>
<if test="componentPath != null and componentPath != ''"> and component_path like concat('%', #{componentPath}, '%')</if>
<if test="status != null and status != ''"> and status = #{status}</if>
</where>
order by create_time desc
</select>
<select id="selectAllFormComponents" resultMap="SysFormComponentResult">
<include refid="selectSysFormComponentVo"/>
where status = '0'
order by create_time desc
</select>
<select id="selectSysFormComponentById" parameterType="Long" resultMap="SysFormComponentResult">
<include refid="selectSysFormComponentVo"/>
where id = #{id}
</select>
<select id="selectSysFormComponentByKey" parameterType="String" resultMap="SysFormComponentResult">
<include refid="selectSysFormComponentVo"/>
where component_key = #{componentKey} and status = '0'
</select>
<insert id="insertSysFormComponent" parameterType="SysFormComponent" useGeneratedKeys="true" keyProperty="id">
insert into sys_form_component
<trim prefix="(" suffix=")" suffixOverrides=",">
<if test="componentKey != null">component_key,</if>
<if test="componentName != null">component_name,</if>
<if test="componentPath != null">component_path,</if>
<if test="description != null">description,</if>
<if test="status != null">status,</if>
<if test="createBy != null">create_by,</if>
<if test="createTime != null">create_time,</if>
<if test="updateBy != null">update_by,</if>
<if test="updateTime != null">update_time,</if>
<if test="remark != null">remark,</if>
</trim>
<trim prefix="values (" suffix=")" suffixOverrides=",">
<if test="componentKey != null">#{componentKey},</if>
<if test="componentName != null">#{componentName},</if>
<if test="componentPath != null">#{componentPath},</if>
<if test="description != null">#{description},</if>
<if test="status != null">#{status},</if>
<if test="createBy != null">#{createBy},</if>
<if test="createTime != null">#{createTime},</if>
<if test="updateBy != null">#{updateBy},</if>
<if test="updateTime != null">#{updateTime},</if>
<if test="remark != null">#{remark},</if>
</trim>
</insert>
<update id="updateSysFormComponent" parameterType="SysFormComponent">
update sys_form_component
<trim prefix="SET" suffixOverrides=",">
<if test="componentKey != null">component_key = #{componentKey},</if>
<if test="componentName != null">component_name = #{componentName},</if>
<if test="componentPath != null">component_path = #{componentPath},</if>
<if test="description != null">description = #{description},</if>
<if test="status != null">status = #{status},</if>
<if test="createBy != null">create_by = #{createBy},</if>
<if test="createTime != null">create_time = #{createTime},</if>
<if test="updateBy != null">update_by = #{updateBy},</if>
<if test="updateTime != null">update_time = #{updateTime},</if>
<if test="remark != null">remark = #{remark},</if>
</trim>
where id = #{id}
</update>
<delete id="deleteSysFormComponentById" parameterType="Long">
delete from sys_form_component where id = #{id}
</delete>
<delete id="deleteSysFormComponentByIds" parameterType="String">
delete from sys_form_component where id in
<foreach item="id" collection="array" open="(" separator="," close=")">
#{id}
</foreach>
</delete>
</mapper>
4. Service接口实现
// ruoyi-flowable/src/main/java/com/ruoyi/flowable/service/ISysFormComponentService.java
package com.ruoyi.flowable.service;
import java.util.List;
import com.ruoyi.system.domain.SysFormComponent;
/**
* 表单组件映射Service接口
*
* @author ruoyi
* @date 2023-07-07
*/
public interface ISysFormComponentService
{
/**
* 查询表单组件映射
*
* @param id 表单组件映射主键
* @return 表单组件映射
*/
public SysFormComponent selectSysFormComponentById(Long id);
/**
* 根据组件标识查询表单组件
*
* @param componentKey 组件标识
* @return 表单组件映射
*/
public SysFormComponent selectSysFormComponentByKey(String componentKey);
/**
* 查询表单组件映射列表
*
* @param sysFormComponent 表单组件映射
* @return 表单组件映射集合
*/
public List<SysFormComponent> selectSysFormComponentList(SysFormComponent sysFormComponent);
/**
* 查询所有表单组件列表(不分页)
*
* @return 表单组件映射集合
*/
public List<SysFormComponent> selectAllFormComponents();
/**
* 新增表单组件映射
*
* @param sysFormComponent 表单组件映射
* @return 结果
*/
public int insertSysFormComponent(SysFormComponent sysFormComponent);
/**
* 修改表单组件映射
*
* @param sysFormComponent 表单组件映射
* @return 结果
*/
public int updateSysFormComponent(SysFormComponent sysFormComponent);
/**
* 批量删除表单组件映射
*
* @param ids 需要删除的表单组件映射主键集合
* @return 结果
*/
public int deleteSysFormComponentByIds(Long[] ids);
/**
* 删除表单组件映射信息
*
* @param id 表单组件映射主键
* @return 结果
*/
public int deleteSysFormComponentById(Long id);
}
// ruoyi-flowable/src/main/java/com/ruoyi/flowable/service/impl/SysFormComponentServiceImpl.java
package com.ruoyi.flowable.service.impl;
import java.util.List;
import com.ruoyi.common.utils.DateUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import com.ruoyi.system.mapper.SysFormComponentMapper;
import com.ruoyi.system.domain.SysFormComponent;
import com.ruoyi.flowable.service.ISysFormComponentService;
/**
* 表单组件映射Service业务层处理
*
* @author ruoyi
* @date 2023-07-07
*/
@Service
public class SysFormComponentServiceImpl implements ISysFormComponentService
{
@Autowired
private SysFormComponentMapper sysFormComponentMapper;
/**
* 查询表单组件映射
*
* @param id 表单组件映射主键
* @return 表单组件映射
*/
@Override
public SysFormComponent selectSysFormComponentById(Long id)
{
return sysFormComponentMapper.selectSysFormComponentById(id);
}
/**
* 根据组件标识查询表单组件
*
* @param componentKey 组件标识
* @return 表单组件映射
*/
@Override
public SysFormComponent selectSysFormComponentByKey(String componentKey)
{
return sysFormComponentMapper.selectSysFormComponentByKey(componentKey);
}
/**
* 查询表单组件映射列表
*
* @param sysFormComponent 表单组件映射
* @return 表单组件映射
*/
@Override
public List<SysFormComponent> selectSysFormComponentList(SysFormComponent sysFormComponent)
{
return sysFormComponentMapper.selectSysFormComponentList(sysFormComponent);
}
/**
* 查询所有表单组件列表(不分页)
*
* @return 表单组件映射集合
*/
@Override
public List<SysFormComponent> selectAllFormComponents()
{
return sysFormComponentMapper.selectAllFormComponents();
}
/**
* 新增表单组件映射
*
* @param sysFormComponent 表单组件映射
* @return 结果
*/
@Override
public int insertSysFormComponent(SysFormComponent sysFormComponent)
{
sysFormComponent.setCreateTime(DateUtils.getNowDate());
return sysFormComponentMapper.insertSysFormComponent(sysFormComponent);
}
/**
* 修改表单组件映射
*
* @param sysFormComponent 表单组件映射
* @return 结果
*/
@Override
public int updateSysFormComponent(SysFormComponent sysFormComponent)
{
sysFormComponent.setUpdateTime(DateUtils.getNowDate());
return sysFormComponentMapper.updateSysFormComponent(sysFormComponent);
}
/**
* 批量删除表单组件映射
*
* @param ids 需要删除的表单组件映射主键
* @return 结果
*/
@Override
public int deleteSysFormComponentByIds(Long[] ids)
{
return sysFormComponentMapper.deleteSysFormComponentByIds(ids);
}
/**
* 删除表单组件映射信息
*
* @param id 表单组件映射主键
* @return 结果
*/
@Override
public int deleteSysFormComponentById(Long id)
{
return sysFormComponentMapper.deleteSysFormComponentById(id);
}
}
5. Controller控制器
// ruoyi-flowable/src/main/java/com/ruoyi/flowable/controller/SysFormComponentController.java
package com.ruoyi.flowable.controller;
import java.util.List;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.PutMapping;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import com.ruoyi.common.annotation.Log;
import com.ruoyi.common.core.controller.BaseController;
import com.ruoyi.common.core.domain.AjaxResult;
import com.ruoyi.common.enums.BusinessType;
import com.ruoyi.system.domain.SysFormComponent;
import com.ruoyi.flowable.service.ISysFormComponentService;
import com.ruoyi.common.utils.poi.ExcelUtil;
import com.ruoyi.common.core.page.TableDataInfo;
/**
* 表单组件映射Controller
*
* @author ruoyi
* @date 2023-07-07
*/
@RestController
@RequestMapping("/flowable/component")
public class SysFormComponentController extends BaseController
{
@Autowired
private ISysFormComponentService sysFormComponentService;
/**
* 查询表单组件映射列表
*/
@PreAuthorize("@ss.hasPermi('flowable:component:list')")
@GetMapping("/list")
public TableDataInfo list(SysFormComponent sysFormComponent)
{
startPage();
List<SysFormComponent> list = sysFormComponentService.selectSysFormComponentList(sysFormComponent);
return getDataTable(list);
}
/**
* 获取所有表单组件列表(不分页)
*/
@GetMapping("/listAll")
public AjaxResult listAll()
{
List<SysFormComponent> list = sysFormComponentService.selectAllFormComponents();
return AjaxResult.success(list);
}
/**
* 获取表单组件映射详细信息
*/
@PreAuthorize("@ss.hasPermi('flowable:component:query')")
@GetMapping(value = "/{id}")
public AjaxResult getInfo(@PathVariable("id") Long id)
{
return AjaxResult.success(sysFormComponentService.selectSysFormComponentById(id));
}
/**
* 根据组件标识获取表单组件
*/
@GetMapping(value = "/key/{componentKey}")
public AjaxResult getByKey(@PathVariable("componentKey") String componentKey)
{
return AjaxResult.success(sysFormComponentService.selectSysFormComponentByKey(componentKey));
}
/**
* 新增表单组件映射
*/
@PreAuthorize("@ss.hasPermi('flowable:component:add')")
@Log(title = "表单组件映射", businessType = BusinessType.INSERT)
@PostMapping
public AjaxResult add(@RequestBody SysFormComponent sysFormComponent)
{
return toAjax(sysFormComponentService.insertSysFormComponent(sysFormComponent));
}
/**
* 修改表单组件映射
*/
@PreAuthorize("@ss.hasPermi('flowable:component:edit')")
@Log(title = "表单组件映射", businessType = BusinessType.UPDATE)
@PutMapping
public AjaxResult edit(@RequestBody SysFormComponent sysFormComponent)
{
return toAjax(sysFormComponentService.updateSysFormComponent(sysFormComponent));
}
/**
* 删除表单组件映射
*/
@PreAuthorize("@ss.hasPermi('flowable:component:remove')")
@Log(title = "表单组件映射", businessType = BusinessType.DELETE)
@DeleteMapping("/{ids}")
public AjaxResult remove(@PathVariable Long[] ids)
{
return toAjax(sysFormComponentService.deleteSysFormComponentByIds(ids));
}
}
前端实现
1. API接口封装
// ruoyi-ui/src/api/flowable/formComponent.js
import request from '@/utils/request'
// 查询Vue表单组件列表
export function listFormComponent(query) {
return request({
url: '/flowable/component/listAll',
method: 'get',
params: query
})
}
// 查询Vue表单组件详细
export function getFormComponent(id) {
return request({
url: '/flowable/component/' + id,
method: 'get'
})
}
// 新增Vue表单组件
export function addFormComponent(data) {
return request({
url: '/flowable/component',
method: 'post',
data: data
})
}
// 修改Vue表单组件
export function updateFormComponent(data) {
return request({
url: '/flowable/component',
method: 'put',
data: data
})
}
// 删除Vue表单组件
export function delFormComponent(id) {
return request({
url: '/flowable/component/' + id,
method: 'delete'
})
}
// 通过组件标识获取Vue表单组件信息
export function getFormComponentByKey(componentKey) {
return request({
url: '/flowable/component/key/' + componentKey,
method: 'get'
})
}
2. 表单注册中心
// ruoyi-ui/src/components/FormRegistry/register.js
/**
* Vue表单组件注册中心
* 用于管理和注册动态Vue表单组件
*/
// 表单组件存储
const formComponents = {}
/**
* 注册表单组件
* @param {string} key 组件标识
* @param {Object} component Vue组件
*/
export function registerFormComponent(key, component) {
if (!key || !component) {
console.warn('注册表单组件失败:组件标识或组件对象为空')
return
}
formComponents[key] = component
console.log(`表单组件 ${key} 已注册`)
}
/**
* 获取表单组件
* @param {string} key 组件标识
* @returns {Object|null} Vue组件
*/
export function getFormComponent(key) {
return formComponents[key] || null
}
/**
* 检查组件是否已注册
* @param {string} key 组件标识
* @returns {boolean}
*/
export function hasFormComponent(key) {
return !!formComponents[key]
}
/**
* 获取所有已注册的组件列表
* @returns {Array}
*/
export function getAllFormComponents() {
return Object.keys(formComponents).map(key => ({
key,
component: formComponents[key]
}))
}
/**
* 移除表单组件注册
* @param {string} key 组件标识
*/
export function unregisterFormComponent(key) {
if (formComponents[key]) {
delete formComponents[key]
console.log(`表单组件 ${key} 已移除注册`)
}
}
// 导出默认对象(用于Vue.use())
export default {
install(Vue) {
// 全局注册已有组件
Object.keys(formComponents).forEach(key => {
Vue.component(key, formComponents[key])
})
// 添加全局方法
Vue.prototype.$formRegistry = {
register: registerFormComponent,
get: getFormComponent,
has: hasFormComponent,
getAll: getAllFormComponents,
unregister: unregisterFormComponent
}
}
}
3. 表单渲染器组件
<!-- ruoyi-ui/src/components/FormRenderer/index.vue -->
<template>
<div class="form-renderer">
<!-- JSON动态表单渲染 -->
<template v-if="formType === 'json'">
<v-form-render
v-if="formData"
:form-data="formData"
ref="vFormRef"
:disabled="readonly"
/>
</template>
<!-- Vue自定义组件渲染 -->
<template v-else-if="formType === 'vue'">
<component
:is="formComponent"
v-if="formComponent"
:process-instance-id="processInstanceId"
:task-id="taskId"
:readonly="readonly"
:form-data="variables"
@submit="handleSubmit"
@cancel="handleCancel"
ref="customFormRef"
/>
<div v-else class="form-error">
<el-alert
title="表单组件加载失败"
:description="`无法加载表单组件: ${formKey}`"
type="error"
show-icon
/>
</div>
</template>
<!-- 未知表单类型 -->
<template v-else>
<el-alert
title="不支持的表单类型"
:description="`表单类型 '${formType}' 不被支持`"
type="warning"
show-icon
/>
</template>
</div>
</template>
<script>
import { registerFormComponent, getFormComponent } from '@/components/FormRegistry/register'
import { getFormComponentByKey } from '@/api/flowable/formComponent'
export default {
name: 'FormRenderer',
props: {
formType: {
type: String,
default: 'json'
},
formKey: {
type: String,
required: true
},
formData: {
type: Object,
default: () => ({})
},
variables: {
type: Object,
default: () => ({})
},
processInstanceId: {
type: String
},
taskId: {
type: String
},
readonly: {
type: Boolean,
default: false
}
},
data() {
return {
formComponent: null,
loading: false,
error: null
}
},
created() {
this.initForm()
},
watch: {
formKey: 'initForm',
formType: 'initForm'
},
methods: {
/**
* 初始化表单
*/
async initForm() {
if (this.formType === 'vue') {
await this.initVueForm()
}
},
/**
* 初始化Vue组件表单
*/
async initVueForm() {
if (!this.formKey) {
console.warn('表单组件标识不能为空')
return
}
this.loading = true
this.error = null
try {
// 首先检查组件是否已注册
let component = getFormComponent(this.formKey)
if (!component) {
console.log(`组件 ${this.formKey} 未注册,开始动态加载...`)
// 从数据库获取组件信息
const response = await getFormComponentByKey(this.formKey)
if (response.code === 200 && response.data) {
const componentInfo = response.data
console.log('获取到组件信息:', componentInfo)
try {
// 动态导入Vue组件
const module = await import(
/* webpackChunkName: "dynamic-form" */
`${componentInfo.componentPath}.vue`
)
component = module.default || module
// 注册组件到表单注册中心
registerFormComponent(this.formKey, component)
console.log(`组件 ${this.formKey} 动态加载成功`)
} catch (importError) {
console.error(`动态导入组件失败: ${componentInfo.componentPath}`, importError)
this.error = `无法加载组件文件: ${componentInfo.componentPath}`
return
}
} else {
console.error(`未找到组件: ${this.formKey}`)
this.error = `未找到组件配置: ${this.formKey}`
return
}
}
this.formComponent = component
} catch (error) {
console.error('初始化Vue表单失败:', error)
this.error = error.message || '表单初始化失败'
} finally {
this.loading = false
}
},
/**
* 获取表单数据
*/
async getFormData() {
if (this.formType === 'json') {
return await this.$refs.vFormRef.getFormData()
} else if (this.formType === 'vue' && this.$refs.customFormRef) {
return await this.$refs.customFormRef.getFormData()
}
throw new Error('无法获取表单数据')
},
/**
* 重置表单
*/
resetForm() {
if (this.formType === 'json' && this.$refs.vFormRef) {
this.$refs.vFormRef.resetForm()
} else if (this.formType === 'vue' && this.$refs.customFormRef && this.$refs.customFormRef.resetForm) {
this.$refs.customFormRef.resetForm()
}
},
/**
* 处理表单提交
*/
handleSubmit(formData) {
this.$emit('form-submit', formData)
},
/**
* 处理表单取消
*/
handleCancel() {
this.$emit('form-cancel')
}
}
}
</script>
<style scoped>
.form-renderer {
padding: 20px;
}
.form-error {
padding: 20px;
}
</style>
4. Vue表单管理界面
<!-- ruoyi-ui/src/views/flowable/form/vueform/index.vue -->
<template>
<div class="app-container">
<el-card class="box-card">
<div slot="header" class="clearfix">
<span>Vue组件表单配置</span>
<el-button style="float: right; padding: 3px 0" type="text" @click="handleAdd">新增表单</el-button>
</div>
<!-- Vue表单列表 -->
<el-table v-loading="loading" :data="vueFormList" border>
<el-table-column label="表单ID" align="center" prop="id" width="100" />
<el-table-column label="表单名称" align="center" prop="componentName" />
<el-table-column label="组件标识" align="center" prop="componentKey" />
<el-table-column label="组件路径" align="center" prop="componentPath" />
<el-table-column label="描述" align="center" prop="description" show-overflow-tooltip />
<el-table-column label="创建时间" align="center" prop="createTime" width="180" />
<el-table-column label="操作" align="center" class-name="small-padding fixed-width" width="280">
<template slot-scope="scope">
<el-button size="mini" type="text" icon="el-icon-view" @click="handlePreview(scope.row)">预览</el-button>
<el-button size="mini" type="text" icon="el-icon-edit" @click="handleUpdate(scope.row)">修改</el-button>
<el-button size="mini" type="text" icon="el-icon-delete" @click="handleDelete(scope.row)">删除</el-button>
</template>
</el-table-column>
</el-table>
<!-- 分页控件 -->
<pagination
v-show="total > 0"
:total="total"
:page.sync="queryParams.pageNum"
:limit.sync="queryParams.pageSize"
@pagination="getList"
/>
<!-- 添加或修改Vue表单组件 -->
<el-dialog :title="title" :visible.sync="open" width="600px" append-to-body>
<el-form ref="form" :model="form" :rules="rules" label-width="100px">
<el-form-item label="组件名称" prop="componentName">
<el-input v-model="form.componentName" placeholder="请输入组件名称" />
</el-form-item>
<el-form-item label="组件标识" prop="componentKey">
<el-input v-model="form.componentKey" placeholder="请输入组件标识,如:accident-report" />
<div class="el-form-item-msg">
<p>组件标识用于在代码中引用该表单组件,请使用小写字母和短横线</p>
</div>
</el-form-item>
<el-form-item label="组件路径" prop="componentPath">
<el-input v-model="form.componentPath" placeholder="请输入组件路径,如:@/components/CustomForms/AccidentReport" />
<div class="el-form-item-msg">
<p>组件路径是Vue文件相对于src目录的路径,不需要包含.vue扩展名</p>
</div>
</el-form-item>
<el-form-item label="描述">
<el-input v-model="form.description" type="textarea" :rows="3" placeholder="请输入组件描述" />
</el-form-item>
</el-form>
<div slot="footer" class="dialog-footer">
<el-button type="primary" @click="submitForm">确 定</el-button>
<el-button @click="cancel">取 消</el-button>
</div>
</el-dialog>
<!-- 表单预览 -->
<el-dialog :title="previewTitle" :visible.sync="previewOpen" width="80%" append-to-body>
<div class="form-preview-container">
<el-tabs v-model="previewTab">
<el-tab-pane label="表单预览" name="preview">
<form-renderer
ref="formRenderer"
:form-type="'vue'"
:form-key="previewForm.componentKey"
:variables="previewForm.variables"
:process-instance-id="'preview-proc-123'"
:task-id="'preview-task-456'"
@form-submit="handlePreviewSubmit"
@form-cancel="handlePreviewCancel"
/>
</el-tab-pane>
<el-tab-pane label="表单数据" name="data">
<el-form label-width="100px">
<el-form-item label="表单变量">
<el-input
type="textarea"
:rows="5"
v-model="previewVariablesStr"
placeholder="JSON格式的表单变量"
></el-input>
</el-form-item>
<el-form-item>
<el-button type="primary" @click="applyPreviewSettings">应用变量</el-button>
<el-button type="success" @click="getPreviewFormData">获取表单数据</el-button>
</el-form-item>
</el-form>
<el-divider content-position="center">表单提交数据</el-divider>
<pre v-if="previewResult">{{ previewResult }}</pre>
<el-empty v-else description="尚未提交数据"></el-empty>
</el-tab-pane>
</el-tabs>
</div>
<div slot="footer" class="dialog-footer">
<el-button @click="previewOpen = false">关 闭</el-button>
</div>
</el-dialog>
</el-card>
</div>
</template>
<script>
// Vue表单系统完全独立的API
import { registerFormComponent, getFormComponent } from '@/components/FormRegistry/register';
import { listFormComponent, addFormComponent, updateFormComponent, delFormComponent } from "@/api/flowable/formComponent";
import FormRenderer from '@/components/FormRenderer'
export default {
name: "VueFormConfig",
components: {
FormRenderer
},
data() {
return {
// 遮罩层
loading: true,
// 总条数
total: 0,
// Vue表单列表(独立于原始表单系统)
vueFormList: [],
// 弹出层标题
title: "",
// 是否显示弹出层
open: false,
// 预览标题
previewTitle: "",
// 是否显示预览弹出层
previewOpen: false,
// 预览表单
previewForm: {
formType: 'vue',
formKey: '',
variables: {}
},
// 预览变量字符串
previewVariablesStr: '{\n "title": "测试标题",\n "description": "测试描述"\n}',
// 预览结果
previewResult: null,
// 预览标签页
previewTab: 'preview',
// 查询参数
queryParams: {
pageNum: 1,
pageSize: 10
},
// Vue表单参数(独立数据结构)
form: {
id: null,
componentKey: null,
componentName: null,
componentPath: null,
description: null,
status: '0'
},
// Vue表单校验规则
rules: {
componentName: [
{ required: true, message: "组件名称不能为空", trigger: "blur" }
],
componentKey: [
{ required: true, message: "组件标识不能为空", trigger: "blur" }
],
componentPath: [
{ required: true, message: "组件路径不能为空", trigger: "blur" }
]
}
};
},
created() {
this.getList();
},
methods: {
/** 查询Vue表单组件列表 */
getList() {
this.loading = true;
listFormComponent(this.queryParams).then(response => {
this.vueFormList = response.data || [];
this.total = this.vueFormList.length;
this.loading = false;
}).catch(error => {
console.error('加载Vue表单列表失败:', error);
this.loading = false;
this.$message.error('加载Vue表单列表失败');
});
},
/** 取消按钮 */
cancel() {
this.open = false;
this.reset();
},
/** 表单重置 */
reset() {
this.form = {
id: null,
componentKey: null,
componentName: null,
componentPath: null,
description: null,
status: '0'
};
this.resetForm("form");
},
/** 新增按钮操作 */
handleAdd() {
this.reset();
this.open = true;
this.title = "添加Vue表单组件";
},
/** 修改按钮操作 */
handleUpdate(row) {
this.reset();
this.form = Object.assign({}, row);
this.open = true;
this.title = "修改Vue表单组件";
},
/** 预览按钮操作 */
handlePreview(row) {
this.previewOpen = true;
this.previewTitle = "预览表单:" + row.componentName;
this.previewForm = {
componentKey: row.componentKey,
variables: {}
};
try {
// 尝试解析默认变量
this.previewForm.variables = JSON.parse(this.previewVariablesStr);
} catch (error) {
console.error('变量格式错误:', error);
this.previewForm.variables = {};
}
// 检查组件是否已注册
if (!getFormComponent(row.componentKey)) {
this.$message.warning(`组件 ${row.componentKey} 尚未注册,请先确保组件文件存在于指定路径:${row.componentPath}`);
}
},
/** 删除按钮操作 */
handleDelete(row) {
this.$confirm('是否确认删除Vue表单组件"' + row.componentName + '"?', "警告", {
confirmButtonText: "确定",
cancelButtonText: "取消",
type: "warning"
}).then(() => {
// 使用独立的删除API
return this.deleteFormComponent(row.id);
}).then(() => {
this.getList();
this.$message.success("删除成功");
}).catch(() => {});
},
/** 提交按钮 */
submitForm() {
this.$refs["form"].validate(valid => {
if (valid) {
if (this.form.id != null) {
// 修改Vue表单组件
this.updateFormComponent(this.form).then(() => {
this.$message.success("修改成功");
this.open = false;
this.getList();
});
} else {
// 新增Vue表单组件
this.addFormComponent(this.form).then(() => {
this.$message.success("新增成功");
this.open = false;
this.getList();
});
}
}
});
},
/** 应用预览设置 */
applyPreviewSettings() {
try {
const variables = JSON.parse(this.previewVariablesStr);
this.previewForm.variables = variables;
this.$message.success('应用变量成功');
} catch (error) {
this.$message.error('变量格式错误: ' + error.message);
}
},
/** 获取预览表单数据 */
getPreviewFormData() {
if (this.$refs.formRenderer) {
const data = this.$refs.formRenderer.getFormData();
this.previewResult = JSON.stringify(data, null, 2);
this.$message.success('获取表单数据成功');
} else {
this.$message.error('表单组件未初始化');
}
},
/** 预览表单提交 */
handlePreviewSubmit(data) {
this.previewResult = JSON.stringify(data, null, 2);
this.$message.success('表单提交成功');
this.previewTab = 'data';
},
/** 预览表单取消 */
handlePreviewCancel() {
this.$message.info('表单操作已取消');
},
/** Vue表单组件独立的CRUD操作 */
// 新增表单组件
async addFormComponent(data) {
try {
const response = await addFormComponent(data);
if (response.code === 200) {
return response;
} else {
throw new Error(response.msg || '新增失败');
}
} catch (error) {
this.$message.error('新增失败: ' + error.message);
throw error;
}
},
// 修改表单组件
async updateFormComponent(data) {
try {
const response = await updateFormComponent(data);
if (response.code === 200) {
return response;
} else {
throw new Error(response.msg || '修改失败');
}
} catch (error) {
this.$message.error('修改失败: ' + error.message);
throw error;
}
},
// 删除表单组件
async deleteFormComponent(id) {
try {
const response = await delFormComponent(id);
if (response.code === 200) {
return response;
} else {
throw new Error(response.msg || '删除失败');
}
} catch (error) {
this.$message.error('删除失败: ' + error.message);
throw error;
}
}
}
};
</script>
<style scoped>
.form-preview-container {
min-height: 400px;
padding: 10px;
}
.el-form-item-msg {
font-size: 12px;
color: #909399;
margin-top: 5px;
}
pre {
background-color: #f5f7fa;
padding: 10px;
border-radius: 4px;
overflow: auto;
}
</style>
工作流集成
1. Flowable表单类型扩展
// ruoyi-flowable/src/main/java/com/ruoyi/flowable/service/impl/FlowTaskServiceImpl.java
/**
* 获取任务表单
*/
@Override
public AjaxResult getTaskForm(String taskId) {
Task task = taskService.createTaskQuery().taskId(taskId).singleResult();
// 获取表单类型和ID
String formKey = task.getFormKey();
String formType = getFormType(task); // 获取表单类型
Map<String, Object> result = new HashMap<>();
result.put("formKey", formKey);
result.put("formType", formType);
// 动态表单
if ("json".equals(formType)) {
SysForm sysForm = sysFormService.selectSysFormById(Long.parseLong(formKey));
if (sysForm != null) {
result.put("formContent", sysForm.getFormContent());
}
}
// Vue组件表单无需额外处理,前端会根据formKey动态加载
// 获取流程变量
Map<String, Object> variables = taskService.getVariables(taskId);
result.put("variables", variables);
return AjaxResult.success(result);
}
/**
* 获取表单类型
*/
private String getFormType(Task task) {
// 优先从Task的formType属性获取
String formType = (String) task.getProcessVariables().get("formType");
if (StringUtils.isBlank(formType)) {
// 从流程定义中获取
BpmnModel bpmnModel = repositoryService.getBpmnModel(task.getProcessDefinitionId());
FlowElement flowElement = bpmnModel.getFlowElement(task.getTaskDefinitionKey());
if (flowElement instanceof UserTask) {
UserTask userTask = (UserTask) flowElement;
formType = userTask.getAttributeValue(ProcessConstants.NAMASPASE, "formType");
}
}
return StringUtils.defaultIfBlank(formType, "json");
}
2. BPMN流程设计器扩展
<!-- 流程设计器表单选择面板 -->
<template>
<div>
<el-form label-width="80px" size="small" @submit.native.prevent>
<el-form-item label="表单类型">
<el-select v-model="formData.formType" @change="handleFormTypeChange">
<el-option label="动态表单" value="json" />
<el-option label="自定义表单" value="vue" />
</el-select>
</el-form-item>
<!-- 动态表单选择 -->
<el-form-item label="表单" v-if="formData.formType === 'json'">
<el-select v-model="formData.formKey" clearable placeholder="选择表单">
<el-option
v-for="item in jsonFormList"
:key="item.formId"
:label="item.formName"
:value="item.formId"
/>
</el-select>
</el-form-item>
<!-- 自定义组件表单选择 -->
<el-form-item label="表单组件" v-else-if="formData.formType === 'vue'">
<el-select v-model="formData.formKey" clearable placeholder="选择表单组件">
<el-option
v-for="item in vueFormList"
:key="item.componentKey"
:label="item.componentName"
:value="item.componentKey"
/>
</el-select>
</el-form-item>
</el-form>
</div>
</template>
<script>
import { listAllForm } from '@/api/flowable/form'
import { listFormComponent } from '@/api/flowable/formComponent'
export default {
name: "FormPanel",
props: {
id: {
type: String,
required: true
}
},
data() {
return {
jsonFormList: [], // 动态表单列表
vueFormList: [], // 自定义表单列表
formData: {
formType: 'json',
formKey: ''
}
}
},
created() {
this.loadJsonFormList()
this.loadVueFormList()
},
methods: {
// 表单类型变更处理
handleFormTypeChange() {
// 切换表单类型时清空表单选择
this.formData.formKey = ''
// 更新流程节点属性
this.updateElementFormProperties()
},
// 更新流程节点表单属性
updateElementFormProperties() {
const properties = {
formType: this.formData.formType,
formKey: this.formData.formKey
}
this.modelerStore.modeling.updateProperties(this.modelerStore.element, properties)
},
// 加载动态表单列表
loadJsonFormList() {
listAllForm().then(res => {
if (res.code === 200) {
this.jsonFormList = res.data.map(item => ({
...item,
formId: item.formId.toString()
}))
}
})
},
// 加载Vue表单组件列表
loadVueFormList() {
listFormComponent().then(res => {
if (res.code === 200) {
this.vueFormList = res.data || []
}
})
}
}
}
</script>
Vue表单组件开发规范
1. 组件模板
<!-- 标准Vue表单组件模板 -->
<template>
<div class="custom-form">
<el-form
ref="form"
:model="formModel"
:rules="rules"
:disabled="readonly"
label-width="120px"
>
<!-- 表单字段 -->
<el-form-item label="字段名称" prop="fieldName">
<el-input v-model="formModel.fieldName" placeholder="请输入内容" />
</el-form-item>
<!-- 更多字段... -->
</el-form>
</div>
</template>
<script>
export default {
name: 'CustomFormComponent',
props: {
// 流程实例ID
processInstanceId: {
type: String
},
// 任务ID
taskId: {
type: String
},
// 是否只读
readonly: {
type: Boolean,
default: false
},
// 表单初始数据
formData: {
type: Object,
default: () => ({})
}
},
data() {
return {
formModel: {
fieldName: ''
// 其他字段...
},
rules: {
fieldName: [
{ required: true, message: '请输入字段名称', trigger: 'blur' }
]
// 其他验证规则...
}
}
},
created() {
this.initFormData()
},
methods: {
/**
* 初始化表单数据
*/
initFormData() {
if (this.formData && Object.keys(this.formData).length > 0) {
this.formModel = { ...this.formModel, ...this.formData }
}
},
/**
* 获取表单数据 - 必须实现
* @returns {Promise<Object>}
*/
getFormData() {
return new Promise((resolve, reject) => {
this.$refs.form.validate(valid => {
if (valid) {
resolve(this.formModel)
} else {
reject(new Error('表单验证失败'))
}
})
})
},
/**
* 重置表单 - 可选实现
*/
resetForm() {
this.$refs.form.resetFields()
}
}
}
</script>
<style scoped>
.custom-form {
padding: 20px;
}
</style>
2. 必须实现的方法
- getFormData() - 必须实现,返回Promise,用于获取表单数据
- resetForm() - 可选实现,用于重置表单状态
3. 标准Props
processInstanceId
- 流程实例IDtaskId
- 任务IDreadonly
- 是否只读模式formData
- 表单初始数据
部署和测试
1. 数据库初始化
-- 执行建表语句
CREATE TABLE `sys_form_component` (
-- 表结构见上文
);
-- 插入测试数据
INSERT INTO `sys_form_component` VALUES
(1, 'test-form', '测试表单', '@/components/CustomForms/TestForm', '测试用的Vue表单组件', '0', 'admin', NOW(), 'admin', NOW(), '测试组件');
2. 创建测试组件
# 在前端项目中创建测试组件目录
mkdir -p ruoyi-ui/src/components/CustomForms
创建测试组件文件:ruoyi-ui/src/components/CustomForms/TestForm.vue
<template>
<div class="test-form">
<el-form ref="form" :model="formModel" :rules="rules" :disabled="readonly" label-width="120px">
<el-form-item label="标题" prop="title">
<el-input v-model="formModel.title" placeholder="请输入标题" />
</el-form-item>
<el-form-item label="描述" prop="description">
<el-input v-model="formModel.description" type="textarea" :rows="3" placeholder="请输入描述" />
</el-form-item>
</el-form>
</div>
</template>
<script>
export default {
name: 'TestForm',
props: {
processInstanceId: String,
taskId: String,
readonly: Boolean,
formData: { type: Object, default: () => ({}) }
},
data() {
return {
formModel: {
title: '',
description: ''
},
rules: {
title: [{ required: true, message: '请输入标题', trigger: 'blur' }]
}
}
},
created() {
this.initFormData()
},
methods: {
initFormData() {
if (this.formData && Object.keys(this.formData).length > 0) {
this.formModel = { ...this.formModel, ...this.formData }
}
},
getFormData() {
return new Promise((resolve, reject) => {
this.$refs.form.validate(valid => {
if (valid) {
resolve(this.formModel)
} else {
reject(new Error('表单验证失败'))
}
})
})
},
resetForm() {
this.$refs.form.resetFields()
}
}
}
</script>
3. 测试流程
后端测试
- 启动后端服务
- 访问API接口测试CRUD功能
前端测试
- 启动前端开发服务器
- 访问Vue表单管理页面:
/flowable/form/vueform/index
- 测试新增、编辑、删除、预览功能
工作流集成测试
- 在BPMN设计器中创建流程
- 配置用户任务使用Vue表单组件
- 部署流程并启动实例
- 测试任务表单的动态加载和提交
性能优化
1. 组件懒加载
利用Webpack的代码分割功能,Vue组件会按需加载:
// 动态导入会自动创建独立的chunk
const module = await import(
/* webpackChunkName: "dynamic-form" */
`${componentInfo.componentPath}.vue`
)
2. 组件缓存
已加载的组件会缓存在FormRegistry中,避免重复加载:
// 检查缓存
let component = getFormComponent(this.formKey)
if (!component) {
// 动态加载并缓存
component = await loadComponent()
registerFormComponent(this.formKey, component)
}
3. 预加载策略
对于常用组件,可以考虑预注册:
// 在register.js中预注册常用组件
import CommonForm from '@/components/CustomForms/CommonForm.vue'
export const formComponents = {
'common-form': CommonForm
}
故障排除
1. 常见问题
问题1:组件加载失败
- 检查组件路径是否正确
- 检查Vue文件语法是否有错误
- 查看浏览器控制台的错误信息
问题2:表单数据无法提交
- 确保组件实现了
getFormData()
方法 - 检查表单验证规则
- 查看网络请求是否正常
问题3:表单预览空白
- 检查组件是否正确导出
- 确认FormRenderer能正确接收props
- 查看Vue开发工具的组件树
2. 调试技巧
- 开启详细日志
// 在FormRenderer中添加调试日志
console.log('正在加载组件:', this.formKey)
console.log('组件路径:', componentInfo.componentPath)
- 使用Vue开发工具
- 安装Vue DevTools浏览器插件
- 检查组件状态和数据流
- 网络调试
- 使用浏览器开发工具的Network面板
- 检查API请求和响应
总结
本技术文档详细记录了Vue动态表单系统的完整实现方案,包括:
- 数据库设计 - 组件映射表的设计和初始化
- 后端实现 - 完整的MVC架构实现
- 前端实现 - 动态加载、注册中心、渲染器等核心组件
- 工作流集成 - 与Flowable的无缝集成
- 开发规范 - Vue组件的标准化开发规范
- 测试部署 - 完整的测试和部署流程
该方案实现了Vue组件的动态加载、自动注册和工作流集成,为复杂业务表单的开发提供了灵活和强大的解决方案。
技术特色
- ✅ 零配置 - 无需手动注册,自动动态加载
- ✅ 高灵活 - 支持任意目录的Vue组件文件
- ✅ 强缓存 - 智能组件缓存机制
- ✅ 易扩展 - 标准化的组件开发规范
- ✅ 完整集成 - 与Flowable工作流的深度集成
- ✅ 生产就绪 - 完整的错误处理和性能优化