表单数据页动态列与动态查询条件实现文档
一、整体架构概述
┌─────────────────────────────────────────────────────────────┐
│ form-data/index.vue │
├─────────────────────────────────────────────────────────────┤
│ useFormDataCenter (Composable) │
│ ├── formList - 表单列表 │
│ ├── currentFormId - 当前选中表单ID │
│ ├── formConfigCache - 表单配置缓存 │
│ ├── queryFields - [计算属性] 动态查询字段 │
│ ├── tableColumns - [计算属性] 动态表格列 │
│ ├── tableData - 表格数据 │
│ ├── queryParams - 查询参数 │
│ └── loadRemoteOptions - 加载远程选项 │
└─────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────┐
│ FormQuery 组件 │
│ └── 根据 fields 动态渲染查询表单项 │
└─────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────┐
│ vxe-grid 表格 │
│ └── 根据 tableColumns 动态渲染列 │
└─────────────────────────────────────────────────────────────┘
二、动态列实现
2.1 核心思路
通过表单配置动态生成列配置数组,而非写死列定义。
2.2 实现代码
// useFormDataCenter.ts
// 1. 表格列配置 - 计算属性
const tableColumns = computed(() => {
if (!currentFormId.value || !formConfigCache.value[currentFormId.value]) return []
const fields = formConfigCache.value[currentFormId.value].fields || []
// 关键:根据 isShowInTable === '1' 筛选需要显示的字段
const dynamicColumns = fields
.filter((field: any) => field.isShowInTable === '1')
.map((field: any) => {
const col: any = {
field: field.field, // 列字段名(对应数据key)
title: String(field.title), // 列标题
minWidth: field.minWidth || 120
}
// 2. 为特定类型添加格式化器
const formatter = getColumnFormatter(field)
if (formatter) {
col.formatter = formatter
}
return col
})
// 3. 追加操作列(固定在右侧)
dynamicColumns.push({
title: '操作',
width: 150,
fixed: 'right',
slots: { default: 'operate' } // 使用插槽渲染自定义内容
})
return dynamicColumns
})
2.3 模板中使用
<vxe-grid
ref="tableRef"
:data="tableData"
:columns="tableColumns" <!-- 动态列配置 -->
>
<!-- 操作列插槽 -->
<template #operate="{ row }">
<vxe-button type="text" @click="viewData(row)">详情</vxe-button>
<vxe-button type="text" @click="removeData(row)">删除</vxe-button>
</template>
</vxe-grid>
三、动态查询条件实现
3.1 核心思路
筛选设置了 isSearchCondition === '1' 的字段,映射为查询组件配置。
3.2 实现代码
// 1. 查询字段配置 - 计算属性
const queryFields = computed(() => {
if (!currentFormId.value || !formConfigCache.value[currentFormId.value]) return []
const fields = formConfigCache.value[currentFormId.value].fields || []
return fields
.filter((field: any) => field.isSearchCondition === '1') // 关键筛选条件
.map((field: any) => {
let type = field.type
let multiple = undefined
// 类型适配:checkbox/radio 转 select
if (type === 'checkbox') {
type = 'select'
multiple = true
} else if (type === 'radio') {
type = 'select'
multiple = false
}
return {
field: field.field, // 字段名
label: field.title, // 字段标签
type, // 组件类型
options: field.options, // 选项数据
props: field.props, // 其他属性
multiple // 是否多选
}
})
})
<FormQuery
v-if="queryFields.length > 0"
:fields="queryFields"
v-model="queryParams"
@query="handleQuery"
@reset="handleReset"
/>
<template>
<div class="form-query">
<el-form :model="modelValue" :inline="true" @submit.prevent="handleQuery">
<el-form-item v-for="field in fields" :key="field.field" :label="field.label">
<!-- 文本输入 -->
<el-input
v-if="field.type === 'input' || !field.type"
v-model="modelValue[field.field]"
placeholder="请输入xxx"
clearable
/>
<!-- 下拉选择(静态/远程选项) -->
<el-select
v-else-if="field.type === 'select' || field.type === 'checkbox' || field.type === 'radio'"
v-model="modelValue[field.field]"
:multiple="field.multiple"
clearable
>
<el-option
v-for="opt in field.options"
:key="opt.value"
:label="opt.label"
:value="opt.value"
/>
</el-select>
<!-- 日期选择 -->
<el-date-picker
v-else-if="field.type === 'date'"
v-model="modelValue[field.field]"
type="date"
clearable
/>
<!-- 日期范围 -->
<el-date-picker
v-else-if="field.type === 'dateRange'"
v-model="modelValue[field.field]"
type="daterange"
/>
<!-- 时间选择 -->
<el-time-picker
v-else-if="field.type === 'time'"
v-model="modelValue[field.field]"
clearable
/>
</el-form-item>
<el-form-item>
<el-button type="primary" @click="handleQuery">查询</el-button>
<el-button @click="handleReset">重置</el-button>
</el-form-item>
</el-form>
</div>
</template>
四、Label 回显核心逻辑
4.1 表格列格式化器
// 根据字段类型返回对应的格式化函数
function getColumnFormatter(field: any) {
const { type, options } = field
switch (type) {
// ==================== 静态选项 ====================
case 'select':
case 'radio':
case 'userSelect':
if (!options || !options.length) return undefined
return ({ cellValue }) => {
if (isEmpty(cellValue)) return '-'
return findLabel(options, cellValue) // 静态options匹配
}
case 'checkbox':
if (!options || !options.length) return undefined
return ({ cellValue }) => {
if (isEmpty(cellValue)) return '-'
return formatCheckbox(options, cellValue) // 多选逗号拼接
}
// ==================== 树形选项 ====================
case 'departmentPicker':
if (!options || !options.length) return undefined
return ({ cellValue }) => {
if (isEmpty(cellValue)) return '-'
return findLabelFromTree(options, cellValue) || '-'
}
// ==================== 日期时间 ====================
case 'datePicker':
case 'timePicker':
case 'dateTimePicker':
return ({ cellValue }) => {
if (isEmpty(cellValue)) return '-'
return formatDate(cellValue)
}
// ==================== 文件上传 ====================
case 'upload':
return ({ cellValue }) => {
if (isEmpty(cellValue)) return '-'
return formatUpload(cellValue)
}
// ==================== 布尔/数值 ====================
case 'switch':
return ({ cellValue }) => {
if (isEmpty(cellValue)) return '-'
return cellValue ? '是' : '否'
}
case 'rate':
return ({ cellValue }) => {
if (isEmpty(cellValue)) return '-'
return '⭐'.repeat(cellValue) || '-'
}
case 'inputNumber':
case 'slider':
return ({ cellValue }) => {
if (isEmpty(cellValue)) return '-'
return String(cellValue)
}
default:
return undefined // input等直接显示原始值
}
}
4.2 静态选项回显
// formDataFormatter.ts
export function findLabel(options: any[], value: any): string {
if (!options || !options.length) return String(value)
const found = options.find((opt: any) => opt.value === value)
return found ? found.label : String(value) // 未找到时返回原值
}
export function formatCheckbox(options: any[], value: any): string {
if (!value) return '-'
const values = Array.isArray(value) ? value : [value]
return values.map(v => findLabel(options, v)).join(', ')
}
4.3 树形选项回显(部门选择器)
export function findLabelFromTree(options: any[], value: any): string | null {
if (!options || !value) return null
for (const opt of options) {
if (opt.value === value) return opt.label
if (opt.children) {
const found = findLabelFromTree(opt.children, value)
if (found) return found
}
}
return null
}
4.4 远程选项回显
关键点:先加载远程选项,再渲染表格
// 1. 加载远程选项
async function loadRemoteOptions(fields: any[], formId: number | string) {
if (!fields || !fields.length) return
// 筛选需要远程加载的字段
const remoteFields = fields.filter((field: any) => {
const optionType = field.props?._optionType
// optionType === '1' 表示远程选项
return (optionType === '1' || optionType === 1) && field.effect?.fetch?.action
})
if (!remoteFields.length) return
await Promise.all(
remoteFields.map(async (field: any) => {
const fetch = field.effect.fetch
try {
// 发起远程请求
const res = await request({
url: fetch.action,
method: (fetch.method || 'get').toLowerCase(),
params: fetch.query,
data: fetch.data
})
// 解析选项数据(支持自定义parse函数)
let list = res
if (fetch.parse && typeof fetch.parse === 'string') {
const fn = new Function('res', fetch.parse)
list = fn(res)
}
if (!Array.isArray(list)) {
console.warn(`[${field.title}] 远程选项解析结果不是数组`)
return
}
// 关键:将远程数据挂载到 field.options
field.options = list
} catch (error) {
console.warn(`加载字段 [${field.title}] 远程选项失败:`, error)
}
})
)
}
// 2. 选择表单时调用
const selectForm = async (formId, formQueryCode) => {
currentFormId.value = formId
// 首次选择,需要获取配置并加载远程选项
if (!formConfigCache.value[formId]) {
const res = await getDesignerFormConfig({ id: formId })
formConfigCache.value[formId] = { fields: res.data }
// ⭐ 关键:加载远程选项后再设置表格列
await loadRemoteOptions(formConfigCache.value[formId].fields, formId)
}
await fetchTableData()
}
五、可能的坑及解决方案
坑1:远程选项加载时机问题
| 问题 | 后端返回的 options 为空或 optionType 未正确设置,导致回显失败 |
|---|
| 原因 | loadRemoteOptions 未被调用,或远程数据加载晚于表格渲染 |
| 解决 | - 确保 optionType === '1' 条件正确<br>- 使用 await loadRemoteOptions() 等待加载完成<br>- 添加 loading 状态防止闪烁 |
// ⭐ 推荐:在 selectForm 中 await 等待远程选项加载完成
const selectForm = async (formId, formQueryCode) => {
// ...
if (!formConfigCache.value[formId]) {
const res = await getDesignerFormConfig({ id: formId })
formConfigCache.value[formId] = { fields: res.data }
// 等待远程选项加载完成
await loadRemoteOptions(formConfigCache.value[formId].fields, formId)
}
// ...
}
坑2:选项数据与表格列使用不同的对象引用
| 问题 | 修改了 field.options,但 tableColumns 使用的仍是旧引用 |
|---|
| 原因 | computed 属性缓存了 fields 数组的初次解析结果 |
| 解决 | 将 options 直接修改到原 field 对象上(引用传递),或强制刷新 |
// ✅ 正确做法:直接修改 field.options(引用传递)
field.options = list // 触发响应式更新
// ❌ 错误做法:创建新数组
field.options = [...list] // 破坏引用关系
坑3:静态选项与远程选项混淆
| 问题 | 某些字段同时配置了静态 options 和远程加载逻辑 |
|---|
| 解决 | 优先使用远程数据覆盖静态 options |
// 判断逻辑:optionType === '1' 优先
const optionType = field.props?._optionType
if (optionType === '1') {
// 使用远程数据
} else {
// 使用静态 options
}
坑4:多选组件值类型不匹配
| 问题 | checkbox 回显时,单元格显示空白或 undefined |
|---|
| 原因 | 数据库存储的是 JSON 字符串,但代码按数组处理 |
| 解决 | 统一值类型处理 |
export function formatCheckbox(options: any[], value: any): string {
if (!value) return '-'
// 统一转为数组处理
let values
if (typeof value === 'string') {
try {
values = JSON.parse(value) // 解析 JSON 字符串
} catch {
values = [value]
}
} else {
values = Array.isArray(value) ? value : [value]
}
return values.map(v => findLabel(options, v)).join(', ')
}
坑5:部门选择器树形结构递归查找性能
| 问题 | 树形层级深时,递归查找效率低 |
|---|
| 解决 | 使用 Map 缓存或预先构建 value->label 映射 |
// 优化:预先构建 Map
const labelMap = new Map()
function buildMap(options: any[]) {
for (const opt of options) {
labelMap.set(opt.value, opt.label)
if (opt.children) buildMap(opt.children)
}
}
export function findLabelFromTreeFast(options: any[], value: any): string | null {
buildMap(options)
return labelMap.get(value) || null
}
坑6:查询参数空值过滤
| 问题 | 空字符串或空数组导致后端查询报错或返回异常 |
|---|
| 解决 | 提交前过滤空值 |
const fetchTableData = async () => {
// 过滤空值
const filteredParams: Record<string, any> = {}
Object.entries(queryParams.value).forEach(([key, value]) => {
if (
value !== '' &&
value !== null &&
value !== undefined &&
!(Array.isArray(value) && value.length === 0)
) {
filteredParams[key] = value
}
})
const params = {
pageNum: pagination.value.pageNum,
pageSize: pagination.value.pageSize,
formData: filteredParams // 只传有值的参数
}
const res = await getFormDataByFormId(curQueryDataFormId.value, params)
// ...
}
坑7:动态列与 vxe-grid 高度计算问题
| 问题 | 表格出现滚动条异常或高度计算错误 |
|---|
| 原因 | 动态列切换时表格未重新计算尺寸 |
| 解决 | 列配置变化后调用 recalculate() |
// 切换表单后手动触发重新计算
const selectForm = async (formId, formQueryCode) => {
// ...
await fetchTableData()
// 等待 DOM 更新后重新计算
nextTick(() => {
tableRef.value?.recalculate()
})
}
// 同时监听窗口 resize
window.addEventListener('resize', () => {
tableRef.value?.recalculate()
})
| 问题 | 切换表单后,查询组件的选项未更新 |
|---|
| 原因 | queryFields computed 未响应 field.options 的变化 |
| 解决 | 确保 options 引用变化时触发 computed 重新计算 |
// queryFields 中直接使用 field.options(引用传递)
const queryFields = computed(() => {
const fields = formConfigCache.value[currentFormId.value]?.fields || []
return fields
.filter((field: any) => field.isSearchCondition === '1')
.map((field: any) => ({
field: field.field,
label: field.title,
type: field.type,
options: field.options, // 直接引用,远程加载后会更新
multiple: field.multiple
}))
})
六、完整数据流程图
┌──────────────────────────────────────────────────────────────────────────┐
│ 用户选择表单 │
└──────────────────────────────────────────────────────────────────────────┘
│
▼
┌──────────────────────────────────────────────────────────────────────────┐
│ 1. getDesignerFormConfig() 获取表单配置 │
│ - 包含 fields[] 数组,每个 field 有 type、options、isShowInTable 等 │
└──────────────────────────────────────────────────────────────────────────┘
│
▼
┌──────────────────────────────────────────────────────────────────────────┐
│ 2. loadRemoteOptions() 加载远程选项 │
│ - 筛选 optionType === '1' 的字段 │
│ - 请求远程 API 获取选项数据 │
│ - 将结果赋值给 field.options │
└──────────────────────────────────────────────────────────────────────────┘
│
┌───────────────┴───────────────┐
▼ ▼
┌───────────────────┐ ┌───────────────────┐
│ queryFields │ │ tableColumns │
│ (计算属性) │ │ (计算属性) │
│ │ │ │
│ 筛选 isSearchCon- │ │ 筛选 isShowInTable │
│ dition === '1' │ │ === '1' │
│ │ │ │
│ 输出: │ │ 输出: │
│ - field │ │ - field │
│ - label │ │ - title │
│ - type │ │ - formatter │
│ - options ←───────┼───────────┼─ options ←────────┘
│ - multiple │ │
└───────────────────┘ └───────────────────┘
│ │
▼ ▼
┌───────────────────┐ ┌───────────────────┐
│ FormQuery 组件 │ │ vxe-grid 表格 │
│ │ │ │
│ 渲染动态表单项 │ │ 渲染动态列 │
│ - el-input │ │ - formatter 回显 │
│ - el-select │ │ - 静态/远程选项 │
│ - el-date-picker │ │ - 操作列插槽 │
└───────────────────┘ └───────────────────┘
│ │
▼ ▼
┌───────────────────────────────────────────────────┐
│ 用户点击查询 → fetchTableData() │
│ - 过滤空值后的 queryParams │
│ - 携带分页参数请求后端 │
└───────────────────────────────────────────────────┘
七、字段配置结构参考
// 表单字段配置(后端返回的 fields 数组中每个元素)
interface FieldConfig {
field: string // 字段名(英文标识)
title: string // 字段标题(中文显示)
type: string // 字段类型(input/select/checkbox/datePicker等)
options?: Array<{ // 静态选项(仅 select/checkbox/radio 等需要)
label: string
value: any
}>
props?: {
_optionType?: '0' | '1' | 0 | 1 // 0=静态 1=远程
// ... 其他组件属性
}
effect?: {
fetch?: {
action: string // 远程API地址
method?: string // 请求方法
query?: object // URL参数
data?: object // body参数
parse?: string // JS表达式,如 'res.data.list'
}
}
isShowInTable?: '0' | '1' // 是否在表格中显示
isSearchCondition?: '0' | '1' // 是否作为查询条件
minWidth?: number // 表格列最小宽度
}
八、关键要点总结
| 序号 | 要点 | 说明 |
|---|
| 1 | 动态列 | tableColumns computed 根据 isShowInTable === '1' 筛选字段 |
| 2 | 动态查询条件 | queryFields computed 根据 isSearchCondition === '1' 筛选字段 |
| 3 | 静态选项回显 | findLabel() 从 options 数组匹配 value → label |
| 4 | 远程选项回显 | 先 loadRemoteOptions() 加载数据到 field.options,再渲染表格 |
| 5 | 多选回显 | formatCheckbox() 将值数组转为逗号拼接的标签字符串 |
| 6 | 树形回显 | findLabelFromTree() 递归查找树形结构的 label |
| 7 | 时序保证 | await loadRemoteOptions() 确保选项加载后再渲染 |
| 8 | 空值过滤 | 查询前过滤 ''、null、空数组 |
| 9 | 引用传递 | 修改 field.options 时保持原引用以触发响应式更新 |
| 10 | 表格重算 | 动态列变化后调用 tableRef.value?.recalculate() |