DictTag 字典标签
基于字典数据的标签组件,支持多种显示样式和自动数据获取。
📋 基础用法
简单使用
vue
<template>
<div>
<!-- 用户状态标签 -->
<DictTag
dict-type="sys_user_status"
:value="userStatus"
/>
<!-- 性别标签 -->
<DictTag
dict-type="sys_user_sex"
:value="userSex"
type="info"
/>
<!-- 是否标签 -->
<DictTag
dict-type="sys_yes_no"
:value="isEnabled"
:show-value="false"
/>
</div>
</template>
<script setup lang="ts">
const userStatus = ref('0') // 0-正常 1-停用
const userSex = ref('1') // 0-男 1-女 2-未知
const isEnabled = ref('Y') // Y-是 N-否
</script>多值显示
vue
<template>
<div>
<!-- 单个值 -->
<DictTag
dict-type="sys_user_status"
value="0"
/>
<!-- 多个值 -->
<DictTag
dict-type="sys_user_status"
:value="['0', '1']"
separator="、"
/>
<!-- 数组对象 -->
<DictTag
dict-type="sys_role"
:value="userRoles"
value-key="roleId"
label-key="roleName"
/>
</div>
</template>
<script setup lang="ts">
const userRoles = ref([
{ roleId: '1', roleName: '管理员' },
{ roleId: '2', roleName: '普通用户' }
])
</script>🎯 组件实现
DictTag 组件
vue
<!-- components/DictTag/index.vue -->
<template>
<span class="dict-tag">
<!-- 单个标签 -->
<template v-if="!isMultiple">
<el-tag
v-if="displayItem"
:type="getTagType(displayItem)"
:size="size"
:effect="effect"
:round="round"
:closable="closable"
:disable-transitions="disableTransitions"
:color="getTagColor(displayItem)"
:class="getTagClass(displayItem)"
@close="handleClose"
>
<slot :item="displayItem" :value="currentValue">
{{ formatDisplayText(displayItem) }}
</slot>
</el-tag>
<span v-else class="dict-tag-empty">
{{ emptyText }}
</span>
</template>
<!-- 多个标签 -->
<template v-else>
<template v-if="displayItems.length > 0">
<el-tag
v-for="(item, index) in displayItems"
:key="getItemKey(item, index)"
:type="getTagType(item)"
:size="size"
:effect="effect"
:round="round"
:closable="closable"
:disable-transitions="disableTransitions"
:color="getTagColor(item)"
:class="getTagClass(item)"
@close="() => handleClose(item, index)"
>
<slot :item="item" :value="getItemValue(item)" :index="index">
{{ formatDisplayText(item) }}
</slot>
</el-tag>
<span v-if="separator && index < displayItems.length - 1" class="separator">
{{ separator }}
</span>
</template>
<span v-else class="dict-tag-empty">
{{ emptyText }}
</span>
</template>
</span>
</template>
<script setup lang="ts">
import { useDict } from '@/composables/use-dict'
interface DictData {
dictCode: number
dictSort: number
dictLabel: string
dictValue: string
dictType: string
cssClass?: string
listClass?: string
isDefault: 'Y' | 'N'
status: '0' | '1'
remark?: string
}
interface Props {
// 字典配置
dictType: string
value?: string | number | string[] | number[] | any[]
// 多值配置
valueKey?: string
labelKey?: string
separator?: string
// 显示配置
showValue?: boolean
showLabel?: boolean
emptyText?: string
// 标签样式
type?: 'success' | 'info' | 'warning' | 'danger'
size?: 'large' | 'default' | 'small'
effect?: 'dark' | 'light' | 'plain'
round?: boolean
closable?: boolean
disableTransitions?: boolean
// 自定义样式
colorMapping?: Record<string, string>
typeMapping?: Record<string, string>
classMapping?: Record<string, string>
}
interface Emits {
(e: 'close', item: any, index?: number): void
}
const props = withDefaults(defineProps<Props>(), {
showValue: false,
showLabel: true,
emptyText: '-',
type: 'primary',
size: 'default',
effect: 'light',
round: false,
closable: false,
disableTransitions: false,
separator: ' '
})
const emit = defineEmits<Emits>()
// 获取字典数据
const { dictData, loading } = useDict(props.dictType)
// 当前值处理
const currentValue = computed(() => {
if (props.value === null || props.value === undefined) {
return null
}
return props.value
})
// 是否多值
const isMultiple = computed(() => {
return Array.isArray(currentValue.value)
})
// 获取字典项
const getDictItem = (value: string | number): DictData | null => {
if (loading.value || !dictData.value[props.dictType]) {
return null
}
const items = dictData.value[props.dictType]
return items.find(item => item.dictValue === String(value)) || null
}
// 单个显示项
const displayItem = computed(() => {
if (isMultiple.value || currentValue.value === null) {
return null
}
// 如果是对象,直接返回
if (typeof currentValue.value === 'object') {
return currentValue.value
}
// 查找字典项
return getDictItem(currentValue.value)
})
// 多个显示项
const displayItems = computed(() => {
if (!isMultiple.value || !currentValue.value) {
return []
}
const values = currentValue.value as any[]
return values.map(val => {
// 如果是对象,直接返回
if (typeof val === 'object') {
return val
}
// 查找字典项
return getDictItem(val)
}).filter(Boolean)
})
// 获取项的键值
const getItemKey = (item: any, index: number): string => {
if (!item) return String(index)
if (props.valueKey && item[props.valueKey]) {
return String(item[props.valueKey])
}
if (item.dictValue) {
return item.dictValue
}
return String(index)
}
// 获取项的值
const getItemValue = (item: any): string => {
if (!item) return ''
if (props.valueKey && item[props.valueKey]) {
return String(item[props.valueKey])
}
if (item.dictValue) {
return item.dictValue
}
return String(item)
}
// 格式化显示文本
const formatDisplayText = (item: any): string => {
if (!item) return props.emptyText
let text = ''
// 获取标签文本
if (props.showLabel) {
if (props.labelKey && item[props.labelKey]) {
text = item[props.labelKey]
} else if (item.dictLabel) {
text = item.dictLabel
} else {
text = String(item)
}
}
// 获取值文本
if (props.showValue) {
const value = getItemValue(item)
if (text) {
text += ` (${value})`
} else {
text = value
}
}
return text || props.emptyText
}
// 获取标签类型
const getTagType = (item: any): string => {
if (!item) return props.type
const value = getItemValue(item)
// 自定义类型映射
if (props.typeMapping && props.typeMapping[value]) {
return props.typeMapping[value]
}
// 根据字典配置的listClass
if (item.listClass) {
const classMap: Record<string, string> = {
'primary': 'primary',
'success': 'success',
'info': 'info',
'warning': 'warning',
'danger': 'danger',
'default': 'info'
}
return classMap[item.listClass] || props.type
}
// 默认类型映射(可根据业务调整)
const defaultTypeMap: Record<string, string> = {
'0': 'success', // 正常
'1': 'danger', // 停用
'Y': 'success', // 是
'N': 'info', // 否
}
return defaultTypeMap[value] || props.type
}
// 获取标签颜色
const getTagColor = (item: any): string | undefined => {
if (!item) return undefined
const value = getItemValue(item)
// 自定义颜色映射
if (props.colorMapping && props.colorMapping[value]) {
return props.colorMapping[value]
}
// 根据字典配置的cssClass
if (item.cssClass) {
// 这里可以根据cssClass返回对应的颜色
return undefined
}
return undefined
}
// 获取标签CSS类
const getTagClass = (item: any): string => {
if (!item) return ''
const value = getItemValue(item)
const classes: string[] = []
// 自定义类映射
if (props.classMapping && props.classMapping[value]) {
classes.push(props.classMapping[value])
}
// 字典配置的CSS类
if (item.cssClass) {
classes.push(item.cssClass)
}
// 字典配置的List类
if (item.listClass) {
classes.push(`dict-${item.listClass}`)
}
return classes.join(' ')
}
// 关闭处理
const handleClose = (item?: any, index?: number) => {
emit('close', item, index)
}
</script>🔧 增强功能
字典选择器
vue
<!-- components/DictSelect/index.vue -->
<template>
<el-select
v-model="currentValue"
v-bind="$attrs"
:loading="loading"
:placeholder="placeholder"
:clearable="clearable"
:filterable="filterable"
@change="handleChange"
>
<el-option
v-for="item in options"
:key="item.dictValue"
:label="item.dictLabel"
:value="item.dictValue"
:disabled="item.status === '1'"
>
<div class="dict-option">
<span class="option-label">{{ item.dictLabel }}</span>
<el-tag
v-if="showTag"
:type="getOptionType(item)"
size="small"
class="option-tag"
>
{{ item.dictValue }}
</el-tag>
</div>
</el-option>
<template v-if="showEmpty && options.length === 0" #empty>
<div class="empty-data">
<el-icon><DocumentRemove /></el-icon>
<span>暂无数据</span>
</div>
</template>
</el-select>
</template>
<script setup lang="ts">
interface Props {
dictType: string
modelValue?: string | number
placeholder?: string
clearable?: boolean
filterable?: boolean
showTag?: boolean
showEmpty?: boolean
}
interface Emits {
(e: 'update:modelValue', value: string | number | undefined): void
(e: 'change', value: string | number | undefined, item?: DictData): void
}
const props = withDefaults(defineProps<Props>(), {
placeholder: '请选择',
clearable: true,
filterable: true,
showTag: false,
showEmpty: true
})
const emit = defineEmits<Emits>()
// 获取字典数据
const { dictData, loading } = useDict(props.dictType)
// 选项列表
const options = computed(() => {
if (loading.value || !dictData.value[props.dictType]) {
return []
}
return dictData.value[props.dictType]
.filter(item => item.status === '0') // 只显示正常状态
.sort((a, b) => a.dictSort - b.dictSort) // 按排序字段排序
})
// 双向绑定
const currentValue = computed({
get: () => props.modelValue,
set: (value) => emit('update:modelValue', value)
})
// 获取选项类型
const getOptionType = (item: DictData): string => {
const typeMap: Record<string, string> = {
'0': 'success',
'1': 'danger',
'Y': 'success',
'N': 'info'
}
return typeMap[item.dictValue] || 'info'
}
// 值变化处理
const handleChange = (value: string | number | undefined) => {
const selectedItem = options.value.find(item => item.dictValue === String(value))
emit('change', value, selectedItem)
}
</script>字典单选框组
vue
<!-- components/DictRadio/index.vue -->
<template>
<el-radio-group
v-model="currentValue"
v-bind="$attrs"
@change="handleChange"
>
<component
:is="radioComponent"
v-for="item in options"
:key="item.dictValue"
:value="item.dictValue"
:disabled="item.status === '1'"
:border="border"
>
{{ item.dictLabel }}
</component>
</el-radio-group>
</template>
<script setup lang="ts">
interface Props {
dictType: string
modelValue?: string | number
radioType?: 'radio' | 'button'
border?: boolean
}
interface Emits {
(e: 'update:modelValue', value: string | number | undefined): void
(e: 'change', value: string | number | undefined, item?: DictData): void
}
const props = withDefaults(defineProps<Props>(), {
radioType: 'radio',
border: false
})
const emit = defineEmits<Emits>()
// 获取字典数据
const { dictData, loading } = useDict(props.dictType)
// 单选框组件类型
const radioComponent = computed(() => {
return props.radioType === 'button' ? 'el-radio-button' : 'el-radio'
})
// 选项列表
const options = computed(() => {
if (loading.value || !dictData.value[props.dictType]) {
return []
}
return dictData.value[props.dictType]
.filter(item => item.status === '0')
.sort((a, b) => a.dictSort - b.dictSort)
})
// 双向绑定
const currentValue = computed({
get: () => props.modelValue,
set: (value) => emit('update:modelValue', value)
})
// 值变化处理
const handleChange = (value: string | number | undefined) => {
const selectedItem = options.value.find(item => item.dictValue === String(value))
emit('change', value, selectedItem)
}
</script>字典复选框组
vue
<!-- components/DictCheckbox/index.vue -->
<template>
<el-checkbox-group
v-model="currentValue"
v-bind="$attrs"
@change="handleChange"
>
<component
:is="checkboxComponent"
v-for="item in options"
:key="item.dictValue"
:value="item.dictValue"
:disabled="item.status === '1'"
:border="border"
>
{{ item.dictLabel }}
</component>
</el-checkbox-group>
</template>
<script setup lang="ts">
interface Props {
dictType: string
modelValue?: (string | number)[]
checkboxType?: 'checkbox' | 'button'
border?: boolean
}
interface Emits {
(e: 'update:modelValue', value: (string | number)[]): void
(e: 'change', value: (string | number)[], items: DictData[]): void
}
const props = withDefaults(defineProps<Props>(), {
checkboxType: 'checkbox',
border: false,
modelValue: () => []
})
const emit = defineEmits<Emits>()
// 获取字典数据
const { dictData, loading } = useDict(props.dictType)
// 复选框组件类型
const checkboxComponent = computed(() => {
return props.checkboxType === 'button' ? 'el-checkbox-button' : 'el-checkbox'
})
// 选项列表
const options = computed(() => {
if (loading.value || !dictData.value[props.dictType]) {
return []
}
return dictData.value[props.dictType]
.filter(item => item.status === '0')
.sort((a, b) => a.dictSort - b.dictSort)
})
// 双向绑定
const currentValue = computed({
get: () => props.modelValue || [],
set: (value) => emit('update:modelValue', value)
})
// 值变化处理
const handleChange = (value: (string | number)[]) => {
const selectedItems = options.value.filter(item =>
value.includes(item.dictValue)
)
emit('change', value, selectedItems)
}
</script>📊 使用示例
表格中使用
vue
<template>
<el-table :data="tableData">
<el-table-column prop="username" label="用户名" />
<el-table-column prop="status" label="状态">
<template #default="{ row }">
<DictTag
dict-type="sys_user_status"
:value="row.status"
/>
</template>
</el-table-column>
<el-table-column prop="sex" label="性别">
<template #default="{ row }">
<DictTag
dict-type="sys_user_sex"
:value="row.sex"
type="info"
size="small"
/>
</template>
</el-table-column>
<el-table-column prop="roles" label="角色">
<template #default="{ row }">
<DictTag
dict-type="sys_role"
:value="row.roleIds"
separator="、"
/>
</template>
</el-table-column>
</el-table>
</template>表单中使用
vue
<template>
<el-form :model="formData">
<el-form-item label="状态" prop="status">
<DictSelect
v-model="formData.status"
dict-type="sys_user_status"
placeholder="请选择状态"
/>
</el-form-item>
<el-form-item label="性别" prop="sex">
<DictRadio
v-model="formData.sex"
dict-type="sys_user_sex"
radio-type="button"
/>
</el-form-item>
<el-form-item label="权限" prop="permissions">
<DictCheckbox
v-model="formData.permissions"
dict-type="sys_permission"
checkbox-type="button"
/>
</el-form-item>
</el-form>
</template>❓ 常见问题
1. 字典标签显示为空或显示原始值
问题描述
字典标签组件渲染后显示为空白或只显示原始的字典值(如 "0"、"1"),而不是对应的中文标签文本。
问题原因
- 字典类型名称配置错误,与后端不匹配
- 字典数据尚未加载完成,组件已经渲染
- 字典值类型不匹配(数字与字符串)
- 字典数据接口返回格式不符合预期
解决方案
vue
<template>
<div>
<!-- 方案1: 添加加载状态判断 -->
<DictTag
v-if="dictLoaded"
dict-type="sys_user_status"
:value="userStatus"
/>
<el-skeleton v-else :rows="1" animated />
<!-- 方案2: 使用 v-show 保持 DOM 结构 -->
<div v-show="!loading">
<DictTag
dict-type="sys_user_status"
:value="userStatus"
/>
</div>
<!-- 方案3: 确保值类型统一为字符串 -->
<DictTag
dict-type="sys_user_status"
:value="String(userStatus)"
/>
</div>
</template>
<script setup lang="ts">
import { ref, onMounted } from 'vue'
import { useDictStore } from '@/stores/dict'
const userStatus = ref('0')
const dictLoaded = ref(false)
const loading = ref(true)
const dictStore = useDictStore()
onMounted(async () => {
// 预加载字典数据
await dictStore.loadDict('sys_user_status')
dictLoaded.value = true
loading.value = false
})
</script>调试技巧
typescript
// 检查字典数据是否正确加载
const { dictData, loading } = useDict('sys_user_status')
watch(dictData, (newData) => {
console.log('字典数据:', newData)
console.log('字典类型:', Object.keys(newData))
}, { immediate: true })
// 检查值类型
console.log('当前值:', userStatus.value, typeof userStatus.value)2. 字典数据加载与组件渲染不同步
问题描述
在页面初次加载时,字典组件先渲染后才获取到字典数据,导致短暂的空白或闪烁现象。
问题原因
- 字典数据通过异步请求获取,存在网络延迟
- 组件挂载时机早于数据返回时机
- 未使用合适的加载状态控制
解决方案
vue
<template>
<div class="dict-container">
<!-- 方案1: 全局字典预加载 -->
<Suspense>
<template #default>
<DictTag dict-type="sys_user_status" :value="status" />
</template>
<template #fallback>
<el-skeleton :rows="1" animated />
</template>
</Suspense>
<!-- 方案2: 使用 loading 状态 -->
<div class="dict-wrapper">
<template v-if="!dictLoading">
<DictTag dict-type="sys_user_status" :value="status" />
</template>
<el-skeleton v-else class="dict-skeleton" :rows="1" animated />
</div>
<!-- 方案3: 批量预加载多个字典 -->
<div v-if="allDictsLoaded">
<DictTag dict-type="sys_user_status" :value="status" />
<DictTag dict-type="sys_user_sex" :value="sex" />
<DictTag dict-type="sys_yes_no" :value="enabled" />
</div>
</div>
</template>
<script setup lang="ts">
import { ref, onMounted } from 'vue'
import { useDictStore } from '@/stores/dict'
const status = ref('0')
const sex = ref('1')
const enabled = ref('Y')
const dictStore = useDictStore()
const dictLoading = ref(true)
const allDictsLoaded = ref(false)
onMounted(async () => {
// 批量预加载字典
await Promise.all([
dictStore.loadDict('sys_user_status'),
dictStore.loadDict('sys_user_sex'),
dictStore.loadDict('sys_yes_no')
])
dictLoading.value = false
allDictsLoaded.value = true
})
</script>
<style scoped>
.dict-skeleton {
display: inline-block;
width: 60px;
height: 22px;
}
</style>应用级预加载
typescript
// main.ts 或 App.vue
import { useDictStore } from '@/stores/dict'
const app = createApp(App)
// 应用启动时预加载常用字典
app.mount('#app')
const dictStore = useDictStore()
dictStore.preloadDicts([
'sys_user_status',
'sys_user_sex',
'sys_yes_no',
'sys_normal_disable'
])3. 多值字典标签分隔符显示异常
问题描述
当传入数组值时,多个标签之间的分隔符位置不正确或根本不显示。
问题原因
- 分隔符渲染逻辑位于 v-for 循环内部但条件判断有误
- index 变量作用域问题
- 分隔符与标签样式冲突
解决方案
vue
<template>
<div class="multi-dict-tags">
<!-- 方案1: 使用 template 包裹 -->
<template v-for="(item, index) in displayItems" :key="getItemKey(item, index)">
<el-tag
:type="getTagType(item)"
:size="size"
>
{{ formatDisplayText(item) }}
</el-tag>
<span
v-if="separator && index < displayItems.length - 1"
class="dict-separator"
>
{{ separator }}
</span>
</template>
<!-- 方案2: 使用 CSS 伪元素 -->
<div class="tags-with-separator">
<el-tag
v-for="(item, index) in displayItems"
:key="getItemKey(item, index)"
:type="getTagType(item)"
class="dict-tag-item"
>
{{ formatDisplayText(item) }}
</el-tag>
</div>
<!-- 方案3: 使用 join 方法处理纯文本场景 -->
<span v-if="textOnly" class="dict-text">
{{ displayItems.map(item => formatDisplayText(item)).join(separator) }}
</span>
</div>
</template>
<script setup lang="ts">
interface Props {
value: string[]
separator?: string
textOnly?: boolean
}
const props = withDefaults(defineProps<Props>(), {
separator: '、',
textOnly: false
})
</script>
<style scoped>
.dict-separator {
margin: 0 4px;
color: var(--el-text-color-secondary);
}
/* 使用 CSS 实现分隔符 */
.tags-with-separator .dict-tag-item:not(:last-child)::after {
content: '、';
margin: 0 4px;
color: var(--el-text-color-secondary);
}
/* 或使用 gap 属性 */
.tags-with-separator {
display: inline-flex;
flex-wrap: wrap;
gap: 8px;
}
</style>4. 字典标签颜色与后端配置不一致
问题描述
后端配置的字典标签颜色(listClass 或 cssClass)在前端没有正确显示,或者显示的颜色与预期不符。
问题原因
- 后端 listClass 值与 Element Plus 的 type 值不完全匹配
- 自定义 cssClass 未在前端定义对应样式
- 颜色映射逻辑缺失或不完整
解决方案
vue
<template>
<DictTag
dict-type="sys_user_status"
:value="status"
:type-mapping="customTypeMapping"
:color-mapping="customColorMapping"
:class-mapping="customClassMapping"
/>
</template>
<script setup lang="ts">
const status = ref('0')
// 自定义类型映射(覆盖 listClass)
const customTypeMapping = {
'0': 'success',
'1': 'danger',
'2': 'warning',
'3': 'info'
}
// 自定义颜色映射(十六进制颜色)
const customColorMapping = {
'0': '#67c23a',
'1': '#f56c6c',
'2': '#e6a23c',
'3': '#909399'
}
// 自定义 CSS 类映射
const customClassMapping = {
'0': 'status-normal',
'1': 'status-disabled',
'2': 'status-pending'
}
</script>
<style>
/* 全局样式文件中定义 */
.status-normal {
--el-tag-bg-color: rgba(103, 194, 58, 0.1);
--el-tag-border-color: rgba(103, 194, 58, 0.2);
--el-tag-text-color: #67c23a;
}
.status-disabled {
--el-tag-bg-color: rgba(245, 108, 108, 0.1);
--el-tag-border-color: rgba(245, 108, 108, 0.2);
--el-tag-text-color: #f56c6c;
}
.status-pending {
--el-tag-bg-color: rgba(230, 162, 60, 0.1);
--el-tag-border-color: rgba(230, 162, 60, 0.2);
--el-tag-text-color: #e6a23c;
}
</style>组件内完善颜色映射逻辑
typescript
// 在 DictTag 组件中完善 getTagType 方法
const getTagType = (item: any): string => {
if (!item) return props.type
const value = getItemValue(item)
// 优先使用自定义类型映射
if (props.typeMapping && props.typeMapping[value]) {
return props.typeMapping[value]
}
// 后端 listClass 到 Element Plus type 的映射
if (item.listClass) {
const listClassMap: Record<string, string> = {
'primary': 'primary',
'success': 'success',
'info': 'info',
'warning': 'warning',
'danger': 'danger',
'default': 'info',
// 兼容可能的其他值
'green': 'success',
'red': 'danger',
'yellow': 'warning',
'blue': 'primary',
'gray': 'info'
}
return listClassMap[item.listClass] || props.type
}
return props.type
}5. 字典选择器与表单验证冲突
问题描述
DictSelect 组件在表单中使用时,清空选择后验证不触发,或者验证消息显示位置不正确。
问题原因
- el-select 的 change 事件与 el-form-item 的 validate 触发时机不匹配
- clearable 清空时未正确触发 blur 事件
- 自定义组件未正确透传验证相关属性
解决方案
vue
<template>
<el-form
ref="formRef"
:model="formData"
:rules="formRules"
label-width="100px"
>
<el-form-item label="用户状态" prop="status">
<DictSelect
v-model="formData.status"
dict-type="sys_user_status"
placeholder="请选择状态"
@change="handleStatusChange"
@clear="handleStatusClear"
/>
</el-form-item>
<el-form-item label="用户性别" prop="sex">
<DictRadio
v-model="formData.sex"
dict-type="sys_user_sex"
@change="() => validateField('sex')"
/>
</el-form-item>
</el-form>
</template>
<script setup lang="ts">
import { ref, reactive } from 'vue'
import type { FormInstance, FormRules } from 'element-plus'
const formRef = ref<FormInstance>()
const formData = reactive({
status: '',
sex: ''
})
const formRules: FormRules = {
status: [
{ required: true, message: '请选择用户状态', trigger: ['change', 'blur'] }
],
sex: [
{ required: true, message: '请选择用户性别', trigger: 'change' }
]
}
// 手动触发字段验证
const validateField = (field: string) => {
formRef.value?.validateField(field)
}
// 状态变化时触发验证
const handleStatusChange = (value: string | undefined) => {
// 延迟验证确保值已更新
nextTick(() => {
validateField('status')
})
}
// 清空时触发验证
const handleStatusClear = () => {
nextTick(() => {
validateField('status')
})
}
</script>在 DictSelect 组件中增强验证支持
vue
<!-- components/DictSelect/index.vue -->
<template>
<el-select
v-model="currentValue"
v-bind="$attrs"
:validate-event="validateEvent"
@change="handleChange"
@clear="handleClear"
@blur="handleBlur"
>
<!-- 选项内容 -->
</el-select>
</template>
<script setup lang="ts">
interface Props {
// ... 其他属性
validateEvent?: boolean
}
interface Emits {
(e: 'update:modelValue', value: string | number | undefined): void
(e: 'change', value: string | number | undefined, item?: DictData): void
(e: 'clear'): void
(e: 'blur', event: FocusEvent): void
}
const props = withDefaults(defineProps<Props>(), {
validateEvent: true
})
const emit = defineEmits<Emits>()
const handleClear = () => {
emit('update:modelValue', undefined)
emit('clear')
}
const handleBlur = (event: FocusEvent) => {
emit('blur', event)
}
</script>6. 字典数据缓存与更新同步问题
问题描述
后台管理员修改了字典数据后,前端页面仍然显示旧的字典值,需要刷新页面才能看到最新数据。
问题原因
- 字典数据被缓存在 Pinia store 中,没有自动更新机制
- 多个浏览器标签页之间缓存不同步
- 没有设置合理的缓存过期策略
解决方案
typescript
// stores/dict.ts
import { defineStore } from 'pinia'
interface DictCacheItem {
data: DictData[]
timestamp: number
expireTime: number
}
export const useDictStore = defineStore('dict', {
state: () => ({
dictCache: new Map<string, DictCacheItem>(),
defaultExpireTime: 5 * 60 * 1000 // 5分钟过期
}),
actions: {
// 获取字典数据(带缓存检查)
async getDict(dictType: string, forceRefresh = false): Promise<DictData[]> {
const cached = this.dictCache.get(dictType)
const now = Date.now()
// 检查缓存是否有效
if (!forceRefresh && cached && now < cached.timestamp + cached.expireTime) {
return cached.data
}
// 从服务器获取最新数据
const data = await this.fetchDictFromServer(dictType)
// 更新缓存
this.dictCache.set(dictType, {
data,
timestamp: now,
expireTime: this.defaultExpireTime
})
return data
},
// 强制刷新指定字典
async refreshDict(dictType: string): Promise<DictData[]> {
return this.getDict(dictType, true)
},
// 刷新所有字典
async refreshAllDicts(): Promise<void> {
const dictTypes = Array.from(this.dictCache.keys())
await Promise.all(dictTypes.map(type => this.refreshDict(type)))
},
// 清除指定字典缓存
clearDictCache(dictType: string): void {
this.dictCache.delete(dictType)
},
// 清除所有缓存
clearAllCache(): void {
this.dictCache.clear()
}
}
})跨标签页同步
typescript
// composables/use-dict-sync.ts
import { onMounted, onUnmounted } from 'vue'
import { useDictStore } from '@/stores/dict'
export function useDictSync() {
const dictStore = useDictStore()
let channel: BroadcastChannel | null = null
onMounted(() => {
// 创建广播通道
channel = new BroadcastChannel('dict-sync-channel')
// 监听其他标签页的更新通知
channel.onmessage = (event) => {
const { type, dictType } = event.data
if (type === 'dict-updated') {
// 刷新指定字典
dictStore.refreshDict(dictType)
} else if (type === 'dict-all-updated') {
// 刷新所有字典
dictStore.refreshAllDicts()
}
}
})
onUnmounted(() => {
channel?.close()
})
// 通知其他标签页字典已更新
const notifyDictUpdate = (dictType?: string) => {
channel?.postMessage({
type: dictType ? 'dict-updated' : 'dict-all-updated',
dictType
})
}
return {
notifyDictUpdate
}
}在管理页面使用
vue
<template>
<div class="dict-management">
<!-- 字典管理表单 -->
<el-button @click="handleSave">保存</el-button>
</div>
</template>
<script setup lang="ts">
import { useDictSync } from '@/composables/use-dict-sync'
import { useDictStore } from '@/stores/dict'
const dictStore = useDictStore()
const { notifyDictUpdate } = useDictSync()
const handleSave = async () => {
// 保存字典数据到后端
await saveDictData()
// 刷新本地缓存
await dictStore.refreshDict('sys_user_status')
// 通知其他标签页更新
notifyDictUpdate('sys_user_status')
ElMessage.success('保存成功')
}
</script>7. 虚拟列表中字典标签的性能问题
问题描述
在大数据量表格(使用虚拟滚动)中,每行都有字典标签组件,滚动时出现卡顿或标签闪烁。
问题原因
- 每个 DictTag 组件都独立调用 useDict,造成重复的响应式开销
- 虚拟滚动快速创建/销毁组件,字典查找频繁
- 没有对字典查找结果进行缓存优化
解决方案
vue
<template>
<el-table-v2
:columns="columns"
:data="tableData"
:width="800"
:height="600"
>
<template #cell="{ column, row }">
<template v-if="column.key === 'status'">
<!-- 使用优化版本的字典标签 -->
<DictTagOptimized
:dict-map="statusDictMap"
:value="row.status"
/>
</template>
</template>
</el-table-v2>
</template>
<script setup lang="ts">
import { computed, onMounted } from 'vue'
import { useDictStore } from '@/stores/dict'
const dictStore = useDictStore()
// 预先获取并转换为 Map 结构
const statusDictMap = ref<Map<string, DictData>>(new Map())
onMounted(async () => {
const dictList = await dictStore.getDict('sys_user_status')
// 转换为 Map 以实现 O(1) 查找
statusDictMap.value = new Map(
dictList.map(item => [item.dictValue, item])
)
})
</script>优化版 DictTag 组件
vue
<!-- components/DictTagOptimized/index.vue -->
<template>
<el-tag
v-if="dictItem"
:type="tagType"
:size="size"
>
{{ dictItem.dictLabel }}
</el-tag>
<span v-else class="dict-tag-empty">{{ emptyText }}</span>
</template>
<script setup lang="ts">
import { computed } from 'vue'
interface Props {
dictMap: Map<string, DictData>
value: string | number
size?: 'large' | 'default' | 'small'
emptyText?: string
}
const props = withDefaults(defineProps<Props>(), {
size: 'default',
emptyText: '-'
})
// 直接从 Map 中查找,O(1) 时间复杂度
const dictItem = computed(() => {
return props.dictMap.get(String(props.value)) || null
})
const tagType = computed(() => {
if (!dictItem.value) return 'info'
const typeMap: Record<string, string> = {
'primary': 'primary',
'success': 'success',
'info': 'info',
'warning': 'warning',
'danger': 'danger'
}
return typeMap[dictItem.value.listClass || ''] || 'info'
})
</script>使用 memoization 优化
typescript
// utils/dict-memo.ts
import { memoize } from 'lodash-es'
// 缓存字典查找结果
export const memoizedGetDictLabel = memoize(
(dictType: string, value: string, dictData: Record<string, DictData[]>) => {
const items = dictData[dictType]
if (!items) return null
return items.find(item => item.dictValue === value)?.dictLabel || null
},
// 自定义缓存 key
(dictType, value, dictData) => `${dictType}:${value}:${Object.keys(dictData).length}`
)
// 清除缓存
export const clearDictMemoCache = () => {
memoizedGetDictLabel.cache.clear?.()
}8. 字典组件的国际化支持问题
问题描述
系统需要支持多语言,但字典标签只显示中文,切换语言后字典内容没有变化。
问题原因
- 字典数据存储在后端,默认只有中文标签
- 前端没有建立字典值与国际化 key 的映射关系
- 语言切换时没有重新加载对应语言的字典
解决方案
vue
<template>
<DictTag
dict-type="sys_user_status"
:value="status"
:label-formatter="formatDictLabel"
/>
</template>
<script setup lang="ts">
import { useI18n } from 'vue-i18n'
const { t, locale } = useI18n()
// 格式化字典标签(支持国际化)
const formatDictLabel = (item: DictData) => {
// 优先使用国际化 key
const i18nKey = `dict.${item.dictType}.${item.dictValue}`
const translated = t(i18nKey)
// 如果翻译 key 存在则使用翻译,否则使用原始标签
return translated !== i18nKey ? translated : item.dictLabel
}
</script>国际化语言文件配置
typescript
// locales/zh-CN.ts
export default {
dict: {
sys_user_status: {
'0': '正常',
'1': '停用'
},
sys_user_sex: {
'0': '男',
'1': '女',
'2': '未知'
}
}
}
// locales/en-US.ts
export default {
dict: {
sys_user_status: {
'0': 'Normal',
'1': 'Disabled'
},
sys_user_sex: {
'0': 'Male',
'1': 'Female',
'2': 'Unknown'
}
}
}封装支持国际化的字典组件
vue
<!-- components/DictTagI18n/index.vue -->
<template>
<el-tag
v-if="displayItem"
:type="getTagType(displayItem)"
:size="size"
>
{{ getLocalizedLabel(displayItem) }}
</el-tag>
<span v-else class="dict-tag-empty">{{ emptyText }}</span>
</template>
<script setup lang="ts">
import { computed } from 'vue'
import { useI18n } from 'vue-i18n'
import { useDict } from '@/composables/use-dict'
interface Props {
dictType: string
value: string | number
size?: 'large' | 'default' | 'small'
emptyText?: string
useI18n?: boolean
}
const props = withDefaults(defineProps<Props>(), {
size: 'default',
emptyText: '-',
useI18n: true
})
const { t, te } = useI18n()
const { dictData, loading } = useDict(props.dictType)
const displayItem = computed(() => {
if (loading.value || !dictData.value[props.dictType]) {
return null
}
const items = dictData.value[props.dictType]
return items.find(item => item.dictValue === String(props.value)) || null
})
const getLocalizedLabel = (item: DictData): string => {
if (!props.useI18n) {
return item.dictLabel
}
const i18nKey = `dict.${item.dictType}.${item.dictValue}`
// 检查翻译 key 是否存在
if (te(i18nKey)) {
return t(i18nKey)
}
// 回退到原始标签
return item.dictLabel
}
const getTagType = (item: DictData): string => {
const typeMap: Record<string, string> = {
'primary': 'primary',
'success': 'success',
'info': 'info',
'warning': 'warning',
'danger': 'danger'
}
return typeMap[item.listClass || ''] || 'info'
}
</script>监听语言变化刷新字典
typescript
// composables/use-dict-i18n.ts
import { watch } from 'vue'
import { useI18n } from 'vue-i18n'
import { useDictStore } from '@/stores/dict'
export function useDictI18n() {
const { locale } = useI18n()
const dictStore = useDictStore()
// 监听语言变化
watch(locale, async (newLocale) => {
// 清除旧缓存
dictStore.clearAllCache()
// 如果后端支持多语言字典,可以携带语言参数重新请求
// await dictStore.loadDictsByLocale(newLocale)
// 或者触发组件重新渲染
dictStore.$patch({ refreshKey: Date.now() })
})
}DictTag及相关字典组件为Vue3应用提供了完整的字典数据展示和选择解决方案,支持多种样式配置和自动数据获取,提升了开发效率和用户体验。
