第四十二节:全局字典封装(后端字典,下拉选择复用)
目标:一次封装,所有页面直接使用字典,不用每个页面重复写请求
后端接口规范(若依风格,国内后台最通用)
- 获取全部字典类型列表:
GET /system/dict/type/list- 根据字典类型编码获取字典项:
GET /system/dict/data/{dictType}
例如:/system/dict/data/sys_user_status
返回:{ code:200, data: [ {label:'启用', value:'1'}, {label:'禁用', value:'0'} ] }
整体思路
- api 层:封装字典请求函数
- composables/useDict.js:字典逻辑封装 + 全局缓存(避免重复请求同一个字典)
- utils/dictUtil.js:工具函数,根据 value 查找字典 label(表格渲染用)
- mock/index.js:配置模拟接口,本地开发调试
- 页面使用:一行代码加载字典,下拉框、表格直接渲染
1. api/dict.js 接口层
import request from '@/utils/request'
/**
* 根据字典类型,获取字典数据项
* @param {string} dictType 字典编码,例如 sys_user_status
*/
export function getDictData(dictType) {
return request({
url: `/system/dict/data/${dictType}`,
method: 'get'
})
}
2. composables/useDict.js(核心组合式钩子,全局缓存)
import { ref } from 'vue'
import { getDictData } from '@/api/dict'
// 全局缓存,整个项目共用,只请求一次同一个字典
const dictCache = new Map()
export function useDict() {
/**
* 加载字典,带缓存
* @param {string} dictType
* @returns Promise<Array> 字典数组 [{label, value}]
*/
const loadDict = async (dictType) => {
// 如果缓存已经存在,直接返回缓存,不再发请求
if (dictCache.has(dictType)) {
return dictCache.get(dictType)
}
try {
const res = await getDictData(dictType)
// ✅ 兜底!防止接口返回 null / undefined,永远返回数组
const dictList = Array.isArray(res.data) ? res.data : []
// 存入全局缓存
dictCache.set(dictType, dictList)
return dictList
} catch (err) {
console.error('字典加载失败:', dictType, err)
// 接口报错,返回空数组,防止页面崩溃
return []
}
}
return {
loadDict
}
}
3. utils/dictUtil.js 字典工具类(表格根据 value 反查 label)
export default {
/**
* 根据字典数组 和 value,获取对应的label文本
* @param {Array} dictList 字典数组 [{label,value}]
* @param {string|number} value 后端返回的值
* @returns string label
*/
getDictLabel(dictList, value) {
if (!Array.isArray(dictList)) return ''
const item = dictList.find(i => i.value === value)
return item ? item.label : ''
}
}
4. mock/index.js 添加字典模拟接口
// 根据字典类型获取字典项
'/system/dict/data/sys_user_status': () => {
return {
code: 200,
data: [
{ label: '启用', value: '1' },
{ label: '禁用', value: '0' }
]
}
},
'/system/dict/data/sys_gender': () => {
return {
code: 200,
data: [
{ label: '男', value: '1' },
{ label: '女', value: '0' }
]
}
},
5. 页面如何使用(你的用户页面示例)
<template>
<PageCardView title="用户">
<!-- 状态下拉筛选 -->
<el-select v-model="queryParams.status" placeholder="请选择状态">
<el-option
v-for="item in statusDict"
:key="item.value"
:label="item.label"
:value="item.value"
/>
</el-select>
<el-table :data="tableData">
<el-table-column label="状态">
<template #default="scope">
{{ dictUtil.getDictLabel(statusDict, scope.row.status) }}
</template>
</el-table-column>
</el-table>
</PageCardView>
</template>
<script setup>
import { ref, onMounted } from 'vue'
import { useDict } from '@/composables/useDict'
import dictUtil from '@/utils/dictUtil'
import PageCardView from '@/components/common/PageCardView.vue'
const { loadDict } = useDict()
const statusDict = ref([])
const queryParams = ref({ status: '' })
onMounted(async () => {
// 加载 sys_user_status 字典
statusDict.value = await loadDict('sys_user_status')
})
</script>
6. 整套方案核心特性(重点)
- 全局缓存 Map:同一个字典,页面多次打开只请求一次,提升性能
- 多层兜底防护 :接口异常、返回 null,全部返回空数组,杜绝
Cannot use 'in' operator报错 - 解耦:api、composable、util、mock 分层,结构清晰
- 复用 :任意页面,只需要
const { loadDict } = useDict()即可加载任意字典 - 符合国内后台(若依)开发习惯,后端接口不用改动
7. 扩展:一次性加载多个字典
onMounted(async () => {
// 并行加载多个字典
const [statusList, genderList] = await Promise.all([
loadDict('sys_user_status'),
loadDict('sys_gender')
])
statusDict.value = statusList
genderDict.value = genderList
})
8. 常见踩坑点(你刚才遇到的坑)
✅ 坑 1:接口地址不匹配 → mock 路由和前端请求 URL 必须一模一样
✅ 坑 2:后端返回 null,没有兜底 → useDict 内部强制转数组
✅ 坑 3:字典数据结构不对,后端返回[{dictType, options}]而不是[{label,value}] → 修改 mock / 后端返回格式
✅ 坑 4:忘记缓存,页面切换反复请求字典 → Map 全局缓存
现在我们可以分步操作
- 先把上面 4 段核心文件代码全部替换
- mock 添加字典接口
- 修改用户页面调用
loadDict('sys_user_status') - 重启项目,测试,报错消失