背景
很久没有写前端的文章了,最近做项目的时候碰到这样一些问题:在 表格 和 表单 页面里,下拉框的数量非常多。
而且不仅数量多,数据来源也五花八门:
- 有些下拉框使用的是全局的 字典 或 枚举,例如「客户状态」、「性别」、「订单来源」;
- 有些下拉框则是 页面特有的固定常量,比如「是否启用」、「优先级」;
- 还有一些下拉框需要通过调用 接口 动态获取,例如「行业列表」、「港口信息」、「归属用户」。
这意味着:同一套页面里可能同时存在字典、枚举、常量和接口四类下拉数据 。如果每个地方都手写一遍 options
,不仅容易出错,还会导致:
- 重复代码多、维护困难;
- 相同数据多处请求、性能浪费;
- 不同组件挂载方式不一致,接入繁琐;
- 值类型不统一(
number
/string
),导致"选不中"的诡异问题。
于是我抽了一层通用模块,做成配置驱动 的下拉框统一方案:只要在配置里写上 requestType + requestKey
,其余请求、缓存、并发去重、树形映射、过滤、挂载位置等繁琐工作全部自动完成。 同时,方案已适配 Element Plus
的 el-form
和 vxe-grid
的可编辑列。
普通写法:重复且易错
正常情况下,表单配置大概是这样的:
JavaScript
const queryFormColumns = [
{
prop: 'customerCode',
label: '客户编号',
valueType: 'input'
},
{
prop: 'categoryId',
label: '客户类型',
valueType: 'select',
fieldProps: { filterable: true },
options: [
{ label: '意向客户', value: '0' },
{ label: '无效客户', value: '1' },
{ label: '成交客户', value: '2' },
{ label: '失败客户', value: '3' }
]
},
{
prop: 'sourceCode',
label: '客户来源',
valueType: 'select',
fieldProps: { filterable: true },
options: [
{ label: '广交会', value: '0' },
{ label: '互联网', value: '1' },
{ label: '社交媒体', value: '2' },
{ label: '广告', value: '3' }
]
}
]
表格里配置 vxe-grid
的编辑列时,又要再写一遍:
JavaScript
const gridColumns = [
{
field: 'customerCode',
title: '客户编号',
width: 150
},
{
field: 'name',
title: '客户名称',
width: 100,
showOverflow: true
},
{
field: 'categoryName',
title: '客户类型',
width: 100,
editRender: {
name: 'select',
options: [
{ label: '广交会', value: '0' },
{ label: '互联网', value: '1' },
{ label: '社交媒体', value: '2' },
{ label: '广告', value: '3' }
]
}
},
{
field: 'sourceCode',
title: '客户来源',
width: 100,
editRender: {
name: 'select',
options: [
{ label: '广交会', value: '0' },
{ label: '互联网', value: '1' },
{ label: '社交媒体', value: '2' },
{ label: '广告', value: '3' }
]
}
},
{ field: 'action', title: '操作', fixed: 'right' }
]
同一份数据写了两次,而且一旦接口返回格式变化、字段多语言切换、过滤逻辑调整......都要逐一修改,非常麻烦。
统一方案:配置驱动 + 自动处理
核心思路是:
- 在配置里声明 数据来源(枚举 / 字典 / 接口 / 常量);
- 封装通用方法
fetchOptions
,自动完成 请求、缓存、并发去重、树形映射、过滤、挂载。
只需要在列/表单项配置中加上几个参数:
1. requestGORF
:来源类型(默认 form
)
form
:来源是表单,options
会直接挂载在该配置项下;grid
:来源是表格列,options
会挂载在editRender.options
;constant
:不发请求,直接使用传入的options
常量。
2. requestType
:数据类型
enum
:请求 枚举,对应一个枚举名称;dict
:请求 字典,对应一个字典类型;interface
:请求 接口,对应某个接口方法名。
3. requestKey
:数据源标识
- 当
requestType = enum
→requestKey
写枚举名,例如ProductStatusEnum
; - 当
requestType = dict
→requestKey
写字典名,例如HX_CUSTOMER_SOURCE
; - 当
requestType = interface
→requestKey
写接口方法名,例如getCustomerCategoryList
。
4. requestProps
:数据映射规则
-
label
:显示字段,可以是字符串或数组; -
value
:取值字段; -
labelFormat
:当label
为数组时,支持自定义格式化,例如:JavaScriptrequestProps: { label: ['code', 'cnName', 'enName'], value: 'code', labelFormat: '{code}/{cnName}/{enName}' }
5. requestParams
:接口参数
- 仅当
requestType = interface
时生效,表示接口调用时的入参。
6. requestFilter
:过滤规则
支持对象式和函数式两种写法:
field + equals
:某字段等于指定值;field + include
:某字段包含在集合内;- 自定义函数:
(item) => item.status === 'ENABLED'
; - 支持树形过滤(命中节点及祖先节点会被保留)。
7. force
:强制刷新
true
表示本次不读缓存、不写缓存;- 可以配置在 整个
columns
上,也可以只给单个字段加上。
配置示例
表单(el-form
)
JavaScript
const formColumns = reactive([
// 1. 普通输入框(不需要下拉)
{
prop: 'customerCode',
label: '客户编号',
valueType: 'input'
},
// 2. 使用枚举(requestType = enum)
{
prop: 'status',
label: '客户状态',
valueType: 'select',
requestGORF: 'form',
requestType: 'enum',
requestKey: 'CustomerStatusEnum', // 枚举名
requestProps: { label: 'label', value: 'value' }
},
// 3. 使用字典(requestType = dict)
{
prop: 'source',
label: '客户来源',
valueType: 'select',
requestGORF: 'form',
requestType: 'dict',
requestKey: 'HX_CUSTOMER_SOURCE' // 字典类型
},
// 4. 使用接口(requestType = interface)
{
prop: 'industryCode',
label: '所属行业',
valueType: 'select',
requestGORF: 'form',
requestType: 'interface',
requestKey: 'getIndustryList', // 接口方法名
requestParams: { level: 0 }, // 接口参数
requestProps: { label: 'industryName', value: 'industryCode' },
requestFilter: { field: 'status', equals: true } // 过滤:只要启用状态
},
// 5. 使用常量(requestGORF = constant)
{
prop: 'level',
label: '客户等级',
valueType: 'select',
requestGORF: 'constant', // 不请求,直接用 options
options: [
{ label: '普通', value: 1 },
{ label: 'VIP', value: 2 },
{ label: '至尊', value: 3 }
]
}
])
然后在页面中使用:
JavaScript
import { useCommunalConditions } from '@/hooks/useCommunalConditions'
const { fetchOptions } = useCommunalConditions()
onMounted(async () => {
await fetchOptions(formColumns)
})
自动加载效果:
- 状态 → 自动从枚举取;
- 客户来源 → 自动从字典取;
- 所属行业 → 自动请求接口并过滤;
- 客户等级 → 直接使用常量。
表格(vxe-grid
)
JavaScript
/** vxe-grid 列配置(支持自动拉取并写入 editRender.options) */
const gridColumns = reactive([
// 普通展示列(无下拉)
{ field: 'customerCode', title: '客户编号', width: 140, fixed: 'left' },
{ field: 'name', title: '客户名称', width: 160, showOverflow: true },
// A. 枚举 enum:用作可编辑下拉(写入 editRender.options)
{
field: 'statusCode',
title: '客户状态',
width: 120,
requestGORF: 'grid',
requestType: 'enum',
requestKey: 'CustomerStatusEnum', // 枚举名
requestProps: { label: 'label', value: 'value' },
editRender: { name: 'ElSelect', props: { clearable: true } }
},
// B. 字典 dict:一把梭注入 editRender.options
{
field: 'sourceCode',
title: '客户来源',
width: 140,
requestGORF: 'grid',
requestType: 'dict',
requestKey: 'HX_CUSTOMER_SOURCE', // 字典类型
editRender: { name: 'ElSelect', props: { filterable: true } }
},
// C. 接口 interface:带参数、带过滤、强制刷新
{
field: 'industryCode',
title: '所属行业',
width: 160,
requestGORF: 'grid',
requestType: 'interface',
requestKey: 'getIndustryList', // 接口方法名 communalApi.getIndustryList
requestParams: { level: 0 }, // 接口入参
requestProps: { label: 'industryName', value: 'industryCode' },
requestFilter: { field: 'status', equals: true }, // 只保留启用项
force: true, // 本列不走缓存,实时拉取
editRender: { name: 'ElSelect', props: { filterable: true, clearable: true } }
},
// D. 接口 + labelFormat:多字段拼接显示
{
field: 'destinationPort',
title: '目的港',
width: 220,
requestGORF: 'grid',
requestType: 'interface',
requestKey: 'getPortList',
requestProps: {
label: ['code', 'cnName', 'enName'], // 多字段
value: 'code',
labelFormat: '{code}/{cnName}/{enName}' // 自定义展示格式
},
editRender: { name: 'ElSelect', props: { filterable: true } }
},
// E. 常量 constant:无需请求,直接使用 options
{
field: 'level',
title: '客户等级',
width: 140,
requestGORF: 'constant',
editRender: { name: 'ElSelect' },
options: [
{ label: '普通', value: '1' },
{ label: 'VIP', value: '2' },
{ label: '至尊', value: '3' }
]
},
{ field: 'action', title: '操作', width: 160, fixed: 'right' }
])
说明要点
requestGORF: 'grid'
→ 模块会将最终下拉选项写入editRender.options
,vxe-grid
直接可用。- 强制刷新 :
force: true
适合人员/组织/行业等变动频繁的数据,跳过缓存取最新。 labelFormat
:当label
为数组时可配模板(如{code}/{cnName}/{enName}
),提升可读性。- 类型统一 :模块已将
value
字符串化,避免由于number
/string
混用导致的"选不中"。
扩展:表单 + 表格一次性初始化
如果同一页面同时有表单与表格下拉,可以封装一个小工具一起拉取:
JavaScript
// hooks/useCommunalConditions.ts
export function useInitOptions(...groups: any[][]) {
const { fetchOptions } = useCommunalConditions()
onMounted(() => {
groups.forEach(g => fetchOptions(g))
})
}
// 页面中
import { formColumns } from './form-columns'
import { gridColumns } from './grid-columns'
useInitOptions(formColumns, gridColumns)
这样就实现了 "配置即数据源" 的一体化体验:
- 表单(
el-form
)的select
自动填充options
; - 表格(
vxe-grid
)的可编辑列自动填充editRender.options
; - 统一享受 缓存 / 并发去重 / 过滤 /
labelFormat
/ 强制刷新 等能力。
功能亮点
-
三类来源一把梭
enum
:调枚举接口,转成options
dict
:调字典工具getStrDictOptions
interface
:调任意API
方法,支持传参
-
缓存 & 并发去重
sessionStorage
缓存下拉数据,默认 10 分钟过期- 同一个下拉同时多处触发时,只请求一次,结果共享
-
树形 & 过滤
- 自动支持树形结构递归挂载
- 支持
requestFilter
(对象式/函数式),树形过滤会保留祖先节点
-
挂载位置自动适配
el-form
→options
vxe-grid
→editRender.options
- 开发者无需处理组件差异
-
类型统一 & 错误兜底
- 所有
value
统一字符串化,杜绝"选不中" - 接口失败写入空数组,不会卡死页面;配合
commLoading
可做加载态
- 所有
-
版本号保护(并发安全)
- 通过
__version__
防止 旧请求覆盖新请求(避免并发乱序导致显示过期数据)
- 通过
缓存的数据结构(示意)

关键代码
JavaScript
// hooks/useCommunalConditions.ts
export function useCommunalConditions() {
const commLoading = ref(false)
const fetchOptions = async (columns: Column[], { force = false } = {}): Promise<Column[]> => {
commLoading.value = true
const handleColumnItem = async (col: Column): Promise<void> => {
// 递归处理 children
if (col.children?.length) {
await Promise.all(col.children.map(handleColumnItem))
return
}
const {
requestGORF = 'form',
requestType,
requestKey,
requestProps = { label: 'label', value: 'value' },
requestParams = {},
requestFilter
} = col
// 跳过常量或无请求配置
if (requestGORF === 'constant' || !requestType || !requestKey) return
const isForceColumn = !!(force || col.force)
col.__version__ = (col.__version__ || 0) + 1 // 版本号自增
const myVersion = col.__version__
const k = cacheKey(requestType, requestKey, requestParams)
// 尝试从缓存读取"原始数据 raw"
let rawOptions: any[] = []
// 强刷:不读缓存也不写缓存、不走 inflight
if (isForceColumn) {
try {
rawOptions = await fetchRawByType(requestType, requestKey, requestParams)
} catch (err) {
console.error(`获取下拉数据失败 [${requestType} - ${requestKey}]`, err)
rawOptions = []
}
} else {
// 非强刷:先读缓存
const cached = getOptionsCache(requestType, requestKey, requestParams) // 与 get 对齐
if (cached && isArray(cached) && cached.length) {
rawOptions = cached
} else {
// miss -> inflight 去重
let p = inflight.get(k)
if (!p) {
p = fetchRawByType(requestType, requestKey, requestParams)
.then((raw) => {
// 仅非强刷才写缓存(覆盖旧值)
setOptionsCache(requestType, requestKey, raw, requestParams, 600)
return raw
})
.finally(() => {
inflight.delete(k)
})
inflight.set(k, p)
}
try {
rawOptions = await p
} catch (err) {
console.error(`获取下拉数据失败 [${requestType} - ${requestKey}]`, err)
rawOptions = []
}
}
}
// 过滤 raw -> rawFiltered
const predicate = toPredicate(requestFilter)
const rawFiltered = predicate
? (looksLikeTree(rawOptions) ? filterTree(rawOptions, predicate) : filterArray(rawOptions, predicate))
: rawOptions
// 基于当列的 requestProps 做映射(不会污染缓存的 raw)
const mappedOptions = mapOptionsByProps(rawFiltered, requestProps)
// 挂载 options 到指定位置(并发版本保护)
if (col.__version__ === myVersion) {
if (requestGORF === 'grid') {
col.editRender = col.editRender || {}
col.editRender.options = mappedOptions
} else {
col.options = mappedOptions
}
if (col.valueType === 'treeSelect' || col.valueType === 'tree-select') {
col.fieldProps = col.fieldProps || {}
col.fieldProps.data = mappedOptions
}
}
// 清除本次 force 标记,避免下次重复强刷
if (col.force) delete col.force
}
try {
await Promise.all((columns || []).map(handleColumnItem))
} finally {
commLoading.value = false
}
return columns
}
return { fetchOptions, commLoading }
}
效果图

总结
只要在配置里声明 requestType + requestKey
,其它工作(请求 / 缓存 / 去重 / 过滤 / 树形 / 强刷)都由模块接管。
这样做能让下拉框真正做到:一套配置,跑通全局。
提示 & 交流
- 本文方案基于 已封装好的可配置式
el-form
与vxe-grid
,思路不一定完全适合所有项目,请按需调整。 - 如果你的业务场景里有一些特殊需求,欢迎在此基础上进行优化或扩展。
- 如果你在使用中遇到问题,或者需要本文所涉及的完整代码示例,可以在评论区留言 ,或者私信联系我。
- 如果你有更好的思路或改进方案,也非常欢迎在评论区分享,一起交流探讨 🚀