OA通用报销申请 —— 物理表绑定配置实施指南
本文档以「OA通用报销申请」为例,详细说明如何在 RuoYi-Office 改造后的表单设计器中配置物理表绑定,实现表单数据存储到指定数据库表。
一、需求概述
1.1 表单字段清单
| 区域 | 字段名 | 组件类型 | 物理表列名 | 表 |
|---|---|---|---|---|
| 表头 | 无采购订单付款 | 单选框 (是/否) | no_po_payment | 主表 |
| 表头 | 是否网购 | 单选框 (是/否) | is_online_purchase | 主表 |
| 表头 | 公司主体 | 下拉选择 | company | 主表 |
| 表头 | 报销类型 | 多选框 | expense_type | 主表 |
| 表头 | 项目分类 | 单选框 | project_category | 主表 |
| 表头 | 报销总额 | 数字输入框(自动合计) | total_amount | 主表 |
| 表头 | 相关附件 | 文件上传 | attachment | 主表 |
| 表头 | 附件图片 | 图片上传 | attachment_image | 主表 |
| 表头 | 备注 | 多行文本 | remark | 主表 |
| 明细 | 报销项 | 下拉选择(可编辑) | expense_item | 明细表 |
| 明细 | 报销金额(元) | 数字(可编辑) | amount | 明细表 |
| 明细 | 子项目名称 | 文本(可编辑) | sub_project_name | 明细表 |
| 明细 | 子项目编号 | 文本(可编辑) | sub_project_no | 明细表 |
| 明细 | 备注 | 文本(可编辑) | item_remark | 明细表 |
| 明细 | 附件编号 | 文本(可编辑) | attachment_no | 明细表 |
| 明细 | 招待说明 | 文本(可编辑) | entertain_desc | 明细表 |
1.2 物理表结构
- 主表:
oa_expense_apply— 报销申请主信息 - 明细表:
oa_expense_apply_item— 报销明细行 - 明细表外键:
apply_id— 关联oa_expense_apply.id
二、前置条件
确保以下条件已满足:
- 物理表已创建(
oa_expense_apply和oa_expense_apply_item) - 后端服务已启动,V2 API 可用(
/bpm/form-data/v2/*、/bpm/form-data/table-schema) - 前端开发服务已启动(
pnpm dev:antd)
三、配置步骤
步骤 1:打开表单设计器
访问:http://localhost:5801/bpm/manager/form/edit?id=35&type=edit
如果是新建表单,从「流程设置 → 流程表单 → 新增」进入
步骤 2:设计表单布局
使用 FormCreate Designer 的拖拽功能构建表单:
2.1 表头区域
| 操作 | 组件 | 配置 |
|---|---|---|
| 拖入「三列栅格」 | fcRow + 3个col | 第一行放单选框组 |
| col-1 拖入「单选框」 | radio | 标题=无采购订单付款,field=no_po_payment,选项: 是=1, 否=0 |
| col-2 拖入「单选框」 | radio | 标题=是否网购,field=is_online_purchase,选项: 是=1, 否=0 |
| 拖入「下拉选择器」 | select | 标题=公司主体,field=company |
| 拖入「三列栅格」 | fcRow | 第二行 |
| col-1 拖入「多选框」 | checkbox | 标题=报销类型,field=expense_type,选项: 日常报销, 差旅报销, 零星采购报销 |
| col-2 拖入「单选框」 | radio | 标题=项目分类,field=project_category,选项: 外部项目, 科研项目, 内部项目 |
| col-3 拖入「数字输入框」 | inputNumber | 标题=报销总额,field=total_amount |
关键:将每个组件的
field属性手动改为物理表列名! 这样提交时api.formData()返回的 key 就是物理列名,后端可以直接存入数据库。
2.2 明细区域 (Vxe数据表格)
拖入「Vxe数据表格」组件,配置如下:
列配置(prop 使用物理列名):
| 列标题 | prop | 编辑类型 | 宽度 |
|---|---|---|---|
| 报销项 | expense_item | select | 150 |
| 报销金额 | amount | number | 120 |
| 子项目名称 | sub_project_name | input | 150 |
| 子项目编号 | sub_project_no | input | 120 |
| 备注 | item_remark | textarea | 200 |
| 附件编号 | attachment_no | input | 100 |
| 招待说明 | entertain_desc | textarea | 200 |
操作按钮配置:
重要:VxeDataTable 的数据来源请选择 「全局数据源」 →
physicalDetail。 系统会在创建场景自动注入初始空行,在详情场景加载物理表行数据。
新增按钮 click(兼容全局数据源 & 静态数据两种模式):
function click(scope, api) {
var rule = api.getRule('ref_VxeTable名称');
if (!rule || !rule.props) return;
var gk = rule.props.globalDataKey;
var key = typeof gk === 'string' ? gk : (gk && gk.key);
if (key) {
var gd = api.options.globalData[key];
if (gd) {
var rows = gd.data || [];
var newRow = {};
if (rows.length > 0) {
var keys = Object.keys(rows[0]);
for (var i = 0; i < keys.length; i++) newRow[keys[i]] = '';
}
gd.data = rows.concat([newRow]);
rule.props.globalDataKey = { key: key };
}
} else {
var d = rule.props.data || [];
var nr = {};
if (d.length > 0) {
var ks = Object.keys(d[0]);
for (var j = 0; j < ks.length; j++) nr[ks[j]] = '';
}
rule.props.data = d.concat([nr]);
}
}注意:新增行的 key 从已有行数据中复制(
Object.keys(rows[0])),确保与物理表列名格式一致(下划线命名)。不要从column[i].prop读取,因为 form-create 可能将其驼峰化。
删除按钮 click(兼容全局数据源 & 静态数据两种模式):
function click(scope, api) {
var rule = api.getRule('ref_VxeTable名称');
if (!rule || !rule.props) return;
var gk = rule.props.globalDataKey;
var key = typeof gk === 'string' ? gk : (gk && gk.key);
if (key) {
var gd = api.options.globalData[key];
if (gd && Array.isArray(gd.data)) {
gd.data = gd.data.filter(function(item) { return item !== scope.row; });
rule.props.globalDataKey = { key: key };
}
} else if (Array.isArray(rule.props.data)) {
rule.props.data = rule.props.data.filter(function(item) { return item !== scope.row; });
}
}原理说明:全局数据源模式下
rule.props.data无效(组件从api.options.globalData[key].data读取数据)。脚本先修改globalData[key].data,然后通过rule.props.globalDataKey = { key: key }创建新对象引用来触发组件的 watcher,让表格自动重新加载数据。
2.3 附件和备注
| 操作 | 组件 | 配置 |
|---|---|---|
| 拖入「文件上传」 | FileUpload | 标题=相关附件,field=attachment |
| 拖入「图片上传」 | ImageUpload | 标题=附件图片,field=attachment_image |
| 拖入「多行输入框」 | textarea | 标题=备注,field=remark |
步骤 3:保存并配置物理表绑定
- 点击设计器顶部「保存」按钮
- 在弹出的「修改流程表单」窗口中填写:
| 字段 | 值 |
|---|---|
| 表单名称 | OA通用报销申请 |
| 单据类型编码 | OA900 |
| 表单分类 | 绑定物理表 |
| 绑定主表 | oa_expense_apply |
| 主键字段 | id |
| 明细表 | oa_expense_apply_item |
| 明细表外键 | apply_id |
- 点击「刷新数据源」按钮 — 系统会调用
/bpm/form-data/table-schemaAPI 获取物理表列信息,注入到设计器的全局数据源($globalData) - 点击「确认」保存
步骤 4:数据绑定配置(可选增强)
保存后返回设计器。此时设计器的全局数据源已注入,你可以通过右侧面板的「数据绑定」进一步配置:
4.1 组件数据绑定
- 选中一个组件(如「报销总额」)
- 点击右侧面板的 「数据绑定」 标签
- 在变量列表中展开
$globalData → physicalTable - 你会看到物理表的列名:
no_po_payment,is_online_purchase,total_amount等 - 将
$globalData.physicalTable.total_amount绑定到「设置组件的值」
注意:如果组件的
field已经是物理列名(步骤2中设置),则数据绑定是可选的。field 名即为物理列名时,提交/加载可以直接工作。
4.2 VxeDataTable 数据源绑定
- 选中 Vxe数据表格组件
- 右侧面板「属性配置」找到「数据来源」下拉
- 选择 「全局数据源」
- 选择
physicalDetail - 确保每个列的
prop值与明细表列名一致
注意:全局数据源模式是推荐方式。系统已做如下自动处理:
- 创建场景:
physicalDetail.data自动注入一行空行,确保表格可见且操作按钮可用- 详情场景:从物理表加载的行数据自动注入到
physicalDetail.data,表格自动显示已有数据- 按钮脚本:新增/删除操作
globalData[key].data,通过globalDataKey引用变化触发表格刷新
步骤 5:全局数据源配置(高级)
如需在设计器顶部「全局数据源」面板手动配置远程数据源:
- 点击设计器工具栏右上角 「设置」图标 → 全局数据源
- 已有
physicalTable和physicalDetail两个静态数据源(由「刷新数据源」自动注入) - 如需远程数据源,可配置:
- 数据源名:
physicalTable - 类型:远程数据
- 请求链接:
/admin-api/bpm/form-data/v2/get?formId=35&businessKey= - 请求方式:GET
- 数据解析:
function(res) { return res.data; }
- 数据源名:
步骤 6:发布流程
- 进入「流程设置 → 流程模型」
- 找到对应流程模型,关联此表单
- 点击「发布」
- 验证发布成功
四、运行时数据流
4.1 发起流程(创建)
用户填写表单 → 点击「提交」
→ 前端 saveAndStartProcessV2()
→ POST /bpm/form-data/v2/save-and-start
body: {
formId: 35,
processDefinitionId: "xxx",
formData: { ← 主表数据(物理列名为 key)
no_po_payment: "1",
is_online_purchase: "0",
company: "xxx",
total_amount: 3196
},
detailRows: [ ← 明细表数据
{ expense_item: "办公用品", amount: 360, sub_project_name: "行政管理", ... },
{ expense_item: "差旅交通", amount: 1280, ... }
]
}
→ 后端写入 oa_expense_apply + oa_expense_apply_item
→ 发起 Flowable 流程实例(businessKey = 主表PK)
→ 返回 processInstanceId4.2 查看详情(加载)
打开流程详情
→ 前端 getApprovalDetail()
→ 判断 formCategory === 'bound'
→ GET /bpm/form-data/v2/get?formId=35&businessKey=1
→ GET /bpm/form-data/v2/detail-list?formId=35&businessKey=1
→ 将数据注入 formOptions.globalData
→ form-create $loadData 自动绑定值到组件
→ coverValue() 强制刷新(兼容保障)4.3 撤回后重新提交(更新)
用户修改表单 → 点击「提交」
→ 前端 updateFormDataV2()
→ PUT /bpm/form-data/v2/update
body: { formId: 35, businessKey: "1", formData: {...} }
→ 后端 UPDATE oa_expense_apply SET ... WHERE id = 1五、关键配置要点总结
field 命名策略
| 方案 | 说明 | 推荐度 |
|---|---|---|
| 方案A(推荐): field = 物理列名 | 组件 field 直接设为 total_amount 等,提交时无需映射 | ★★★★★ |
| 方案B: field 自动生成 + $loadData 绑定 | field 用系统生成的ID,通过数据绑定映射物理列,需额外映射逻辑 | ★★★ |
强烈推荐方案A:设计表单时将每个组件的 field 直接改为对应的物理表列名。
VxeDataTable 列 prop 命名
明细表格每列的 prop 必须与 oa_expense_apply_item 表的列名一致:
prop: "expense_item" → 对应 expense_item 列
prop: "amount" → 对应 amount 列
prop: "sub_project_name" → 对应 sub_project_name 列类型注意事项
| 物理表类型 | 组件类型 | 注意 |
|---|---|---|
| TINYINT | radio (是/否) | 选项 value 用字符串 "1"/"0",后端会做类型转换 |
| DECIMAL | inputNumber | 直接兼容 |
| VARCHAR | input/select/checkbox | 直接兼容 |
| TEXT | textarea | 直接兼容 |
| VARCHAR(JSON) | FileUpload/ImageUpload | 文件列表序列化为 JSON 字符串 |
六、与旧版方案对比
| 维度 | 旧方案 (V1) | 新方案 (V2) |
|---|---|---|
| 字段映射 | 保存弹窗手动填映射表 | field 直接用物理列名,无需映射 |
| 配置体验 | 表格中手填列名(反人类) | 右侧面板可视化绑定 |
| API | /bpm/form-data/get (字段ID为key) | /bpm/form-data/v2/get (物理列名为key) |
| 明细表 | 通用逻辑猜测 | 独立 API /v2/detail-list |
| 前端代码 | 大量手动 coverValue + 类型转换 | form-create 原生 $loadData 机制 |
| 向后兼容 | - | 旧 V1 API 保留,两套并存 |
七、常见问题排查
Q: 保存弹窗看不到物理表列表?
A: 确保后端 getSchemaTableList API(codegen 模块)正常工作,且传入了 dataSourceConfigId: 0
Q: 刷新数据源后变量列表为空?
A: 检查 /bpm/form-data/table-schema?tableName=oa_expense_apply 是否返回数据。确认物理表已创建。
Q: 发起流程后物理表没有数据?
A: 检查前端是否走了 V2 路径(saveAndStartProcessV2),确认 formCategory === 'bound'。查看浏览器网络请求是否调用了 /bpm/form-data/v2/save-and-start。
Q: 详情页不显示物理表数据?
A: 检查 businessKey 是否正确传递。如果是旧流程实例(businessKey 为 null),系统会通过 getBusinessKeyByProcessInstanceId 降级查找。
Q: 单选框/下拉框值未选中?
A: 检查数据类型。数据库 TINYINT 返回数字 1,但 radio 选项的 value 是字符串 "1",系统自动处理该转换。如果仍有问题,确保组件选项的 value 类型与物理表返回的数据类型匹配。
Q: VxeDataTable 新增按钮点击无反应?
A: 常见原因:
- 全局数据源模式下脚本写了
rule.props.data:全局数据源模式下组件不读rule.props.data,需操作api.options.globalData[key].data,并通过rule.props.globalDataKey = { key: key }触发刷新。参考上方「操作按钮配置」的完整脚本。 ref名称不匹配:确认api.getRule('ref_Fbn4mnlgovp8hrc')中的名称与设计器中 VxeDataTable 的「组件编号」一致。- 未选全局数据源:如果数据来源选的「静态数据」而非「全局数据源」,则脚本中的
rule.props.data = ...concat(...)写法即可(无需操作 globalData)。
