告别重复代码!Vue3 中后台下拉框统一加载方案(自动缓存、去重、过滤、适配表单与表格)

背景

很久没有写前端的文章了,最近做项目的时候碰到这样一些问题:在 表格表单 页面里,下拉框的数量非常多。

而且不仅数量多,数据来源也五花八门

  • 有些下拉框使用的是全局的 字典枚举,例如「客户状态」、「性别」、「订单来源」;
  • 有些下拉框则是 页面特有的固定常量,比如「是否启用」、「优先级」;
  • 还有一些下拉框需要通过调用 接口 动态获取,例如「行业列表」、「港口信息」、「归属用户」。

这意味着:同一套页面里可能同时存在字典、枚举、常量和接口四类下拉数据 。如果每个地方都手写一遍 options,不仅容易出错,还会导致:

  • 重复代码多、维护困难;
  • 相同数据多处请求、性能浪费;
  • 不同组件挂载方式不一致,接入繁琐;
  • 值类型不统一(number/string),导致"选不中"的诡异问题。

于是我抽了一层通用模块,做成配置驱动 的下拉框统一方案:只要在配置里写上 requestType + requestKey,其余请求、缓存、并发去重、树形映射、过滤、挂载位置等繁琐工作全部自动完成。 同时,方案已适配 Element Plusel-formvxe-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 = enumrequestKey 写枚举名,例如 ProductStatusEnum
  • requestType = dictrequestKey 写字典名,例如 HX_CUSTOMER_SOURCE
  • requestType = interfacerequestKey 写接口方法名,例如 getCustomerCategoryList
4. requestProps:数据映射规则
  • label:显示字段,可以是字符串或数组;

  • value:取值字段;

  • labelFormat:当 label 为数组时,支持自定义格式化,例如:

    JavaScript 复制代码
    requestProps: { 
      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.optionsvxe-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 / 强制刷新 等能力。

功能亮点

  1. 三类来源一把梭

    • enum:调枚举接口,转成 options
    • dict:调字典工具 getStrDictOptions
    • interface:调任意 API 方法,支持传参
  2. 缓存 & 并发去重

    • sessionStorage 缓存下拉数据,默认 10 分钟过期
    • 同一个下拉同时多处触发时,只请求一次,结果共享
  3. 树形 & 过滤

    • 自动支持树形结构递归挂载
    • 支持 requestFilter(对象式/函数式),树形过滤会保留祖先节点
  4. 挂载位置自动适配

    • el-formoptions
    • vxe-grideditRender.options
    • 开发者无需处理组件差异
  5. 类型统一 & 错误兜底

    • 所有 value 统一字符串化,杜绝"选不中"
    • 接口失败写入空数组,不会卡死页面;配合 commLoading 可做加载态
  6. 版本号保护(并发安全)

    • 通过 __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-formvxe-grid,思路不一定完全适合所有项目,请按需调整。
  • 如果你的业务场景里有一些特殊需求,欢迎在此基础上进行优化或扩展。
  • 如果你在使用中遇到问题,或者需要本文所涉及的完整代码示例,可以在评论区留言 ,或者私信联系我
  • 如果你有更好的思路或改进方案,也非常欢迎在评论区分享,一起交流探讨 🚀
相关推荐
weixin_448119943 小时前
在vscode中,在powershell 下,如何进入子目录?
前端·ide·vscode
Hilaku3 小时前
前端开发,为什么容易被边缘化?
前端·javascript·面试
訾博ZiBo3 小时前
Vue3组件通信的方法有哪些?
前端·vue.js
砺能3 小时前
JavaScript 截取 HTML 生成图片
前端·javascript
Nan_Shu_6143 小时前
学习:uniapp全栈微信小程序vue3后台 (25)
前端·学习·微信小程序·小程序·uni-app
徐小夕3 小时前
pxcharts-vue-max 多维表格编辑器:支持大数据渲染 + 二次开发
前端·vue.js·算法
吃饺子不吃馅3 小时前
如何让AntV X6 的连线“动”起来:实现流动效果?
前端·css·svg
袁煦丞3 小时前
WebSocket实时通信不卡顿:cpolar内网穿透实验室第503个成功挑战
前端·程序员·远程工作
解道Jdon3 小时前
IntelliJ IDEA全流程智能支持Java 25新特性
javascript·reactjs