鬼故事
46.88MB · 2025-10-29
而且不仅数量多,数据来源也五花八门:
这意味着:同一套页面里可能同时存在字典、枚举、常量和接口四类下拉数据。如果每个地方都手写一遍 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:调枚举接口,转成 optionsdict:调字典工具 getStrDictOptionsinterface:调任意 API 方法,支持传参缓存 & 并发去重
sessionStorage 缓存下拉数据,默认 10 分钟过期树形 & 过滤
requestFilter(对象式/函数式),树形过滤会保留祖先节点挂载位置自动适配
el-form → optionsvxe-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,思路不一定完全适合所有项目,请按需调整。
2025-10-30
国产类魂游戏《明末:渊虚之羽》今日上线,Steam 国区 248 元起
2025-10-30
国产游戏《明末:渊虚之羽》首发 Steam“差评如潮”:被指灾难级优化、环国区永久降价