不废话,先看功能效果
什么是"动态表单设计"?
拖拽式表单设计器——左侧组件面板、中间画布、右侧属性面板,和市面上很多低代码工具别无二致。但如果只是把它当成一个"拖拖拽拽画表单"的工具,那就大大低估了它。
它真正做的事情,是把一个业务模块的整个生命周期——界面、数据库、接口、菜单、权限、编号规则、打印模板——全部浓缩成一份 JSON 元数据。你拖动一个"单行文本"组件到画布上,同时发生的其实是四件事:表单界面上多了一个输入框、数据库表里多了一个字段的定义、列表视图里多了一列、甚至后端 CRUD 的 Go 结构体里多了一个成员。
为什么这样设计、灵活在哪里、能带来哪些效率和质量的提升?
一、整体结构:三栏布局背后的分层
设计器的骨架是经典的"三栏 + 一栏头"结构,位于 design.vue:
- Header:保存提交、清空、复制为新设计、导入/导出 JSON、查看 Go 结构体;
- Left:组件面板,按"容器 / 表单组件 / 业务组件 / 信息展示"分组,支持搜索;
- Center:画布,所见即所得的栅格表单;
- Right:三个页签——组件设置(当前选中组件的属性)、表单全局设置(数据存储、菜单、UI、脚本)、字段列表(树形总览所有字段,点一下就能跳转编辑)。
这个布局本身平平无奇,真正的精髓在于数据流:三栏之间没有任何直接的组件通信,它们全部读写同一个响应式状态对象states.formMeta(service.ts)。左栏拖入组件,是往formMeta.formItemList里 push 一个对象;画布就是把这个数组渲染出来;右栏修改的属性,直接双向绑定到数组里的同一个对象。数据结构即界面,界面即数据结构——这是理解整套设计的钥匙。
二、设计思想:一个 JSON 驱动一切
1. 元数据驱动:单一事实来源(Single Source of Truth)
这套系统最核心的思想是:表单不是页面,是模型;设计表单就是建模。
保存时,整个设计被序列化成一个巨大的 JSON(metaStr)存进数据库,里面包含了这个业务模块的一切:
formMeta ├── tableName 数据库表名 ├── formProps 表单全局外观(布局/尺寸/间距) ├── formItemList 字段数组,每个字段自带 8 个维度的配置 │ ├── component { name, props, rules, events, slots, vif, vshow } │ ├── formItemProps { label, field, required, tooltip, ... } │ ├── dbColumn { size, default, notNull, index, uniqueIndex, comment } │ ├── fieldOther { 表格列行为、查询条件、虚拟字段、子表列设置 } │ ├── vtable 表格视图的列级配置 │ └── fdLvl 字段权限等级 ├── menu 菜单定义(名称/图标/路由/应用/上级菜单) ├── funcs 表单生命周期函数(JS) ├── before_add... Go 服务端钩子 └── codenPrefix 流水号编号规则一个普通字段在 design-data.ts 中的"模板"同时包含 UI 属性(component.props)、校验规则(rules)、事件(events)、插槽(slots)、数据库列定义(dbColumn)、视图行为(fieldOther)、权限等级(fdLvl)。一次配置,七个维度同时生效——这就是"单一事实来源"的含义:表单、表格、查询筛选、打印、数据库、后端结构体之间不存在"各写一份、互相漂移"的可能,因为它们本来就是同一份数据。
2. 设计时即运行时:所见即所得,而且"所得即所存"
很多表单设计器有一个通病:设计器里用一套"假组件"渲染预览,运行时用另一套"真组件"渲染表单,两边总有细微差异。
这里没有这个问题。画布(center.vue + form-item.vue)渲染的就是真实的yq-*/a-*组件,通过 Vue 的动态组件机制<component :is>按名字实例化;而运行时表单(form-display.vue)读取同一份formItemList,用同一套组件、同样的动态组件机制渲染。设计器里看到的,就是用户将来用到的;设计器里绑定的 v-model 字段名,就是将来数据库里的列名。设计态和运行态共享同一份元数据和同一套渲染逻辑,这是"设计即所得"最彻底的实现方式。
3. 渐进式复杂度:为所有人设计,也为开发者保留"逃生舱"
这是我认为最有深度的设计思想之一。它把能力分成了五个台阶,每一级都为下一级留了口子,而不是把用户逼在某一层:
- 可视化配置:点选、拖拽、填属性——业务人员即可完成 80% 的工作;
- 组件级事件脚本:每个组件自带事件表(
change、focus、pressEnter……),在 events-modal.vue 中用 Monaco 编辑器直接写 JavaScript,代码里有states、OBJ、userStore可用——联动的、计算型的交互逻辑不再需要改代码发布; - 表单级生命周期函数:
onBeforeMount、onMounted、getDataAfter、saveBefore等表单全局函数(form-vue-funcs.vue); - 服务端 Go 钩子:
before_add、before_edit、before_submit等九个服务端钩子(hooks-modal.vue),在数据库事务中、系统默认逻辑之前执行,可以做任何后端才能做的事——这是很多低代码平台最缺失的一环,它们要么没有服务端扩展,要么服务端扩展要改代码重启; - 完全脱离平台:"查看 go struct" 一键把设计生成标准 Go 结构体代码(go-struct-modal.vue 与 service.ts 中的
GenGoStructCode),将来要自建工程、微服务拆分,模型资产可以直接带走。
第五级的"逃生舱"特别值得强调:它意味着这套低代码方案不会绑架你——平台可以快速起量,平台外的演进路径也是通的。设计一次,平台内外都留了路。
4. 前后端契约一体化:设计器即数据库设计器,也是接口设计器
传统的开发流程里,前端表单、数据库 DDL、后端 struct、接口文档是四份独立产出,靠人肉保持同步。这里把契约"折叠"进了组件定义:
- 每个组件模板自带
dbColumn定义,前端设置字段长度、默认值、非空、索引、唯一索引、注释,后端在保存时用 GORMAutoMigrate自动建表、自动增删字段(tools/design.form.go、tools/db.migrate.go); - 后端甚至用
dynamicstruct库在运行时根据 metaStr 动态拼装 Go 结构体,再交给 GORM 做 CRUD 和一对多关联(子表单)的预加载——也就是说,这张表在后端根本没有一个静态的 Go model,结构体是"即时生成"的; - 而前端那些
yq-select、yq-tree-select组件通过doctypeTableName引用其他表单模型,自动完成表间关联的取值和展示。
"设计器里加一个字段"和"数据库里加一列 + 后端模型加一个成员 + 接口文档更新"被合并成了同一个动作。这是效率提升的根本来源,后面还会展开。
5. 约束即质量:把"防呆"做进设计流程
质量不是靠事后测试堆出来的,而是在设计动作发生的瞬间就拦截错误。这套设计器在多个环节内置了护栏:
- 命名规范强制:字段名必须以英文开头、只能包含英文/数字/下划线(right-comp-setting.vue 中的
handleFieldChange),系统保留字段(id、uid、coden、status、parent_id、children等)直接拒绝; - 表名黑名单:
adminuser、adminmenu、admindoctype等 20 多个系统表名不允许占用(right-form-setting.vue); - 组合冲突拦截:子表单里不能再放子表单、文件存储模式不能有子表单组件、树形结构不能有子表、子表单不能有虚拟字段、设成子表单前要检查已有组件——这些"看似能设计、实则无法落地"的模型,在拖拽或勾选的那一刻就被拦下并给出原因(service.ts 的
handleClone等); - 提交前校验:表名、菜单名为必填,校验失败自动展开右侧面板并定位到错误处;
- 危险操作二次确认:清空画布、提交保存、修改已发布表单的表名都有确认或警告。
三、灵活性:从"够用"到"几乎无死角"
组件体系的四分类与全覆盖
FormMetaList组件注册表把组件分成四类(CompType):
- 容器(Container):分组标签
yq-tabs、折叠面板yq-accordion,可以内嵌子字段组,实现"一张长表单"到"分区块表单"的升级; - 表单项(FormItem):单行/多行文本、数字、密码、富文本、验证码、单选框、复选框组、开关、评分器、滑动条、日期、下拉选择、下拉树、文件上传、中国省市区、颜色选择器、一维码、二维码等 20 余种;
- 业务组件(Business):子表单
yq-sub-table(一对多明细,自带合计/计数/平均汇总行、货币格式化)、选择用户(直接对接用户表); - 信息展示(Display):标题、静态文字、分割线、图片、时间轴、进度条、步骤条、按钮、提示——这些不落库,纯展示与排版。
三级配置通道:没有"配置不了"的属性
每个组件的属性配置有三条通道,按需递进:
- 专用配置面板:30 多个组件的每个核心属性都有专门的配置界面(
InputCfg、YqSubTableCfg、YqTabsCfg……),对小白友好; - 事件与插槽的代码编辑:组件事件用 Monaco 写 JS,插槽(
slots)配置用 JS 对象字面量编辑,可以注入自定义图标、自定义内容; - Monaco JSON 兜底:任何没有专用面板的组件(以及所有未覆盖的属性),直接落到一个 JSON 编辑器——任何属性都能改,不存在"面板没做出来就改不了"的死角。这看似简单,实则是低代码工具"天花板"问题的标准解法:面板是快捷方式,JSON 是全集。
一个字段的"第二人生":fieldOther 与视图行为
灵活性还体现在一个字段不止服务于表单。fieldOther里的配置让它同时服务其他视图:
queryHide:是否出现在数据视图的查询条件里;isVirtual:虚拟字段——不生成数据库列,仅用于前端交互展示(比如临时计算值),子表单中禁用以保证数据模型干净;subColumn:当本表单作为子表单嵌入别的表单时,这个字段在子表里如何显示——隐藏、禁用、只读、固定左右、宽度、对齐、是否可排序、是否支持批量编辑。同一个字段定义,在"当表头"和"当明细"两种身份下各有各的行为配置;fdLvl:字段级权限等级,当用户的字段等级低于该值时,运行时直接不渲染这个字段(HasFdLvl检查),实现了"同一张表单,不同角色看到不同字段"的行内权限。
存储与形态的多样性
- 存储类型:
db(数据库存储,配autoMigrate自动建表)或file(文件存储,适合存配置类单据),由业务场景决定; - 树形结构:勾选后自动注入
parent_id字段组件,自动生成树形表格; - 子表单:作为一对多关系中的"多"方被嵌入其他表单;
- 多数据源:可以为每张表指定不同的数据库数据源;
- 编号规则:
codenPrefix+ 六种codenRule(纯自增、带分隔符、年月日 + 自增)自由组合出流水号。
模型资产的流转
设计好的表单可以导出为 JSON 文件,也能批量导入(支持一次导入多个模型文件、导入前预览、自动清掉 id 关联),还能一键复制为新设计。这意味着表单模型可以跨环境迁移、纳入 Git 版本管理、在团队间共享——模型真正成了可管理的资产,而不是锁死在某个环境里的私有配置。
四、好处优势
对业务/实施人员:门槛足够低。拖拽、填属性、配校验、挂菜单,就能交付一个完整的增删改查模块,不需要理解路由、接口、数据库。
对开发者:从"重复造轮子"变成"只写差异"。列表页、表单页、增删改查接口、建表语句、菜单注册这些固定动作全部消失,精力集中在真正的业务逻辑上——而且这些逻辑有地方放:组件事件(JS)、表单函数(JS)、服务端钩子(Go)。不会出现"平台很好用,但有个特殊需求死活实现不了"的困境。
对系统架构:一致性是最大的隐性收益。表单、表格、查询、打印、后端模型共享一份元数据,杜绝了传统开发中"前端字段和后端字段对不上""列表列和表单字段不一致"这类最耗时又最常见的 bug。同时系统维护成本大幅下降:一个业务模块上线后,调整字段、修改校验、变更界面,全部是配置变更而非代码变更,不需要走完整的上线流程。
对数据资产:字段的dbColumn(长度、索引、注释)沉淀在模型里,数据库结构永远有"文档";导出的 JSON 是天然的设计文档;生成的 Go 结构体是天然的接口契约。模型、代码、数据库三者的可追溯性比手写代码时代更强。
五、效率提升:从"几天"到"几十分钟"
效率提升来自"合并动作"和"自动化",具体拆开看:
- 一拖五:拖一个组件 = 前端表单 + 数据库列 + 列表列 + 查询条件 + 后端模型同时就位。传统流程中这五件事横跨前端、后端、DBA,需要多次沟通与联调;现在是一个动作,并且不会不一致;
- 自动建表:
autoMigrate让每次保存自动生成/更新表结构,没有 DDL 脚本、没有手工在数据库里执行、没有"忘了加字段"的生产事故; - 菜单自动生成:填写表名后,菜单名(
xxx-curdtable)、路由(/xxx)、编号前缀自动带出;保存后菜单自动注册、自动刷新当前用户的菜单树; - 数据视图自动生成:字段定义自带
vtable配置,列表视图的列自动从表单模型生成,之后还可以用独立的数据管理视图继续微调; - 批量导入/导出与复制:一个成熟模型可以瞬间复制变体;多个模型可以打包迁移;
- 脚本内联,热更新:组件事件、表单函数、Go 钩子的修改都是保存即生效,没有编译、打包、发版的等待;
- 打印模板联动:表单设计的同时可绑定/生成打印模板(hiprint 或 pdfmake 两种引擎),从列表页可直接跳转打印设计。
综合来看,一个标准业务模块(含主子表、校验、联动、权限)从立项到可用,传统方式以"天"计;在这套体系里,熟练的设计者以"小时"甚至"分钟"计。更关键的是迭代效率:需求变更(加字段、改校验、调界面)从"改三处代码 + 发布"变成"改一处配置 + 保存",这是低代码方案最核心的价值。
六、质量提升:质量不是测出来的,是设计出来的
- 校验规则内建:每个组件自带
rules结构,必填、长度、格式校验在建模时配置,运行时统一走表单校验,且支持scrollToFirstError自动定位。校验逻辑和字段定义同生共死,不会出现"前端忘了校验"的遗漏; - 命名与结构的一致性:字段名、表名的强制规则(英文开头、下划线分隔、保留字黑名单)保证整个数据库命名风格统一、无歧义——团队越大,这个收益越明显;
- 源头拦截无效设计:子表单嵌套、文件存储与子表冲突、树形与子表冲突、虚拟字段越界等在建模时即被阻止,而不是等运行到一半才报错。模型永远是"可落地的模型";
- 权限内建:
fdLvl字段级权限让"敏感字段对低权限角色隐藏"成为模型配置的一部分,而不是散落在各处的v-if判断——安全策略跟着模型走,不会漏; - 事务化的服务端钩子:
before_*钩子明确运行在事务中、默认逻辑之前,业务校验、数据篡改防护、跨表联动都有确定的执行语义和原子性保证,且每个钩子都附带完整注释的示例代码(hooks-modal-data.js); - 可评审、可回滚:JSON 模型文件可以进入版本管理,设计变更可以 diff、可以评审、可以一键还原旧模型——配置变更第一次获得了和代码变更同等的治理能力;
- 防误操作:清空确认、提交确认、修改已发布表名的红色警告("请慎重!修改表名,容易出错"),以及保存后的 toast 反馈,降低了设计者本人成为质量风险源的概率。
七、结语
回过头看,这套动态表单设计器做的其实是一件很"软件工程"的事:把"建模"这个动作从代码里显性化出来。
传统开发中,模型活在开发者的脑子里和散落的代码里,每一次需求变更都要经历"理解现状 → 改多处代码 → 联调 → 发布"的漫长回路。而这套设计把模型变成了一份可见、可拖拽、可复用、可迁移、可脚本化、可生成代码的资产,让"改模型"成为一个低风险、低成本的日常动作。
它同时回答了两个容易被低代码平台忽略的问题:"复杂的特殊需求怎么办?"(渐进式五级台阶,一直到脱离平台生成 Go 代码)和"质量怎么保证?"(约束前置、契约一体化、模型治理)。这种"元数据驱动 + 真实组件渲染 + 前后端契约折叠 + 逃生舱设计"的组合,值得任何正在做或想要做低代码平台的人仔细研究。