快点阅读免费版
65.01MB · 2025-09-27
而且不仅数量多,数据来源也五花八门:
这意味着:同一套页面里可能同时存在字典、枚举、常量和接口四类下拉数据。如果每个地方都手写一遍 options
,不仅容易出错,还会导致:
number
/string
),导致“选不中”的诡异问题。于是我抽了一层通用模块,做成配置驱动的下拉框统一方案:只要在配置里写上 requestType + requestKey
,其余请求、缓存、并发去重、树形映射、过滤、挂载位置等繁琐工作全部自动完成。 同时,方案已适配 Element Plus
的 el-form
和 vxe-grid
的可编辑列。
正常情况下,表单配置大概是这样的:
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
的编辑列时,又要再写一遍:
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
,自动完成 请求、缓存、并发去重、树形映射、过滤、挂载。只需要在列/表单项配置中加上几个参数:
requestGORF
:来源类型(默认 form
)form
:来源是表单,options
会直接挂载在该配置项下;grid
:来源是表格列,options
会挂载在 editRender.options
;constant
:不发请求,直接使用传入的 options
常量。requestType
:数据类型enum
:请求 枚举,对应一个枚举名称;dict
:请求 字典,对应一个字典类型;interface
:请求 接口,对应某个接口方法名。requestKey
:数据源标识requestType = enum
→ requestKey
写枚举名,例如 ProductStatusEnum
;requestType = dict
→ requestKey
写字典名,例如 HX_CUSTOMER_SOURCE
;requestType = interface
→ requestKey
写接口方法名,例如 getCustomerCategoryList
。requestProps
:数据映射规则label
:显示字段,可以是字符串或数组;
value
:取值字段;
labelFormat
:当 label
为数组时,支持自定义格式化,例如:
requestProps: {
label: ['code', 'cnName', 'enName'],
value: 'code',
labelFormat: '{code}/{cnName}/{enName}'
}
requestParams
:接口参数requestType = interface
时生效,表示接口调用时的入参。requestFilter
:过滤规则支持对象式和函数式两种写法:
field + equals
:某字段等于指定值;field + include
:某字段包含在集合内;(item) => item.status === 'ENABLED'
;force
:强制刷新true
表示本次不读缓存、不写缓存;columns
上,也可以只给单个字段加上。el-form
)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 }
]
}
])
然后在页面中使用:
import { useCommunalConditions } from '@/hooks/useCommunalConditions'
const { fetchOptions } = useCommunalConditions()
onMounted(async () => {
await fetchOptions(formColumns)
})
自动加载效果:
vxe-grid
)/** 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
混用导致的“选不中”。如果同一页面同时有表单与表格下拉,可以封装一个小工具一起拉取:
// 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__
防止 旧请求覆盖新请求(避免并发乱序导致显示过期数据)// 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
,思路不一定完全适合所有项目,请按需调整。高通第六代骁龙 8 至尊版芯片踪迹曝光:代号 SM8950,小米 18 系列手机首发
【保姆级教程-从0开始开发MCP服务器】二、使用ClaudeCode连接第一个MCP服务器