这段时间设计了系统字典功能, 特定写一遍文字记录一下.
后端
表结构设计
先贴上 sql 可直接执行
create table sys_dictionary
(
id int auto_increment
primary key,
name varchar(64) not null,
dict_type int not null comment '字典类型',
sort int default 0 not null comment '排序',
default_flag tinyint(1) default 0 not null comment '默认',
remark varchar(128) default '' not null comment '备注',
meta varchar(128) default '' not null comment '元数据',
del tinyint(1) default 0 not null comment '删除标记',
create_time timestamp default CURRENT_TIMESTAMP not null comment '创建时间',
update_time timestamp default CURRENT_TIMESTAMP not null on update CURRENT_TIMESTAMP comment '更新时间'
)
comment '系统字典表' engine = InnoDB;
笔者对上面字段进行说明
字段类型主要是为了将字段进行归类
排序为了字段类型在页面上显示顺序
备注主要对这个字段值进行解释
元数据这个字段提供给前端显示使用, 比如字体颜色/字体/图标 登录
删除标记主要解决因为这里删除是为了用户不能再使用这个字典生成新的数据,之前的使用这个字典数据不受影响
这里你会发现大多数字典表 value 这里并没有出现, 这个是因为在没有实际常见. 为了设计而设计一个 value 后面只会使程序变得不易维护所以我们这里字典值采用 id 作为 value 值
dictType 为了后端方便可以建立一个 enum 类进行维护.
接口设计
字典列表接口
/dict/list
其他修改删除接口都多比较简单, 这里就不举例了
前端
前端这里使用的 vue3 技术, 下面主要是字典组件的封装
pinia 状态管理字典
export const useDictionaryStore = defineStore('widgetDictionaryStore', () => {
const allDistTree = ref<Record<number, DictionaryItemResponse[]>>({});
/**
* 处理字典返回值
* @param dictList
*/
function handleRes(dictList: DictionaryItemResponse[]) {
return dictList.map(item => {
if (item.meta) {
// @ts-ignore
item.meta = JSON.parse(item.meta);
}
return item;
});
}
/**
* 加载字典
* @param dictType
*/
async function loadDistData(dictType: null | WidgetDictType = null) {
const data = await getAllDictionary({ dictType })
if (dictType) {
// 局部更新
allDistTree.value[dictType] = handleRes(data);
} else {
// 全局更新
allDistTree.value = groupBy(handleRes(data), (item) => item.dictType);
}
}
function getDictItemInner(dictType: WidgetDictType, value: number) {
const searchDictItems = allDistTree.value[dictType] || [];
const item = searchDictItems.find(item => item.id === value)
if (item) {
return item;
}
}
/**
* 按字典类型获取字典列表
* @param dictType
*/
async function getDistList(dictType: WidgetDictType) {
const dictItem = allDistTree.value[dictType];
if (dictItem) {
return dictItem;
}
await loadDistData(dictType);
return allDistTree.value[dictType];
}
/**
* 获取字典项
* @param dictType
* @param value
*/
async function getDictItem(dictType: WidgetDictType, value: number) {
const dictItem = getDictItemInner(dictType, value)
if (!dictItem) {
return dictItem;
}
// 重新加载
await loadDistData(dictType)
return getDictItemInner(dictType, value);
}
return {
getDictItem,
loadDistData,
getDistList,
allDistTree
}
});
问:上面 用 Record<number, DictionaryItemResponse[]> 方式来管理字典是为什么?
答: 是因为字典的使用方式 99% 多少通过类型获取字典列表的方式使用, 所以使用这种存储结构会使按字典类型获取字典列表检索的复杂度降低到 0(1) .
字典项显示组件
字典显示组件,是字典组件最基础的组件之一
<script lang="ts" setup>
import type { DictionaryNameProps } from '@/components/Distionary/index';
import { ref, watch } from 'vue'
import { useWidgetDictionaryStore } from '@/stores/WidgetDictionaryStore'
const props = defineProps();
const dict = ref();
const dictionaryStore = useDictionaryStore()
// 监控字典值变动
watch(() => props.distValue, async (newValue) => {
dict.value = await dictionaryStore.getDictItem(props.distType, newValue);
})
</script>
<template>
<slot name="default" v-if="dict" :dict="dict">
<span>{{dict.name}}</span>
</slot>
<slot name="noFound" v-else></slot>
</template>
这个是字典项基础组件主要提供给字典单项显示功能,
使用者可以自定义字典渲染样式, 我们提供了 default 插槽
当字典值没有找到时我们提供了noFound 插槽
属性
Slots 插槽
使用
<DictionaryName :dict-type="..">
<template #default="{dict}">
<div>{{ dict.name }}</div>
</template>
</DictionaryName>
字典列表组件
提供字典列表显示, 是字典组件最基础的组件之一。
<script lang="js" setup>
import { ref, onMounted } from 'vue'
import { useDictionaryStore } from "@/stores";
const props = defineProps({
dictType: {
type: Number,
},
setDictList: {
type: Function,
default: (val) => {},
}
})
const dictList = ref([])
const dictionaryStore = useDictionaryStore()
function addDictItem(data) {
dictList.value.push(data);
props.setDictList(dictList.value)
}
async function loadDictList() {
dictList.value = await dictionaryStore.getDistList(props.dictType, true)
props.setDictList(dictList.value);
}
defineExpose({
loadDictList,
addDictItem,
})
onMounted(async () => {
await loadDictList();
})
</script>
<template>
<slot v-for="(dict, index) in dictList"
:key="dict.value"
:dict="dict"
:index="index">
<span>{{dict.name}}</span>
</slot>
</template>
属性
Slots 插槽
Exposes
使用
<template>
<DictionaryName
:dict-type=""
:value="">
<template #default="{dict}">
<div>
<el-tag>{{dict.name}}</el-tag>
</div>
</template>
</DictionaryName>
</template>
字典下拉列表组件
因为字典选择大多数会出现在 select 中, 我们这里选用 el-select 利用上面提供的组件封装一下字典下拉列表组件, 这个组件支持单选多选自动创建字典
<script lang="js" setup>
import { watchEffect, ref, computed, watch, onMounted } from 'vue';
import DictionaryListDisplay from '../DictionaryListDisplay/index.vue'
import { SaveDictionary } from "@/api/dictionaryModuleApi";
const props = defineProps({
className: {
type: Array,
default: ''
},
style: {
type: Object,
default: () => ({})
},
dictType: {
type: Number,
},
selectProps: {
type: Object,
default: () => ({})
},
disableAutoCreate: {
type: Boolean,
default: false
},
modelValue: {
default: () => {}
},
createDict: {
type: Function,
default: null
}
});
const emit = defineEmits([
'update:modelValue',
'change',
'focus',
'blur'
])
const multipleEnvSingle = computed(() => props.selectProps.multiple && !(props.modelValue instanceof Array))
function calcSelect(val) {
return multipleEnvSingle.value ? [val].filter(item => item !== '') : val
}
const selectValue = ref(calcSelect(props.modelValue));
watch(() => props.modelValue, (newVal) => {
selectValue.value = calcSelect(newVal);
})
const selectRef = ref();
onMounted(() => {
console.log('selectValue', selectValue.value)
})
const allDict = ref([]);
async function handleAutoCreate(val) {
const lastVal = val[val.length - 1]
const index = allDict.value.findIndex(({name, value}) =>
lastVal.toString() === value.toString() || name === lastVal.toString())
if (index === -1) {
const data = props.createDict
? props.createDict(lastVal)
: await SaveDictionary({
dictType: props.dictType,
name: lastVal,
});
await dictionaryListDisplayRef.value.addDictItem(data);
setTimeout(() => {
val[val.length - 1] = data.value
change(val)
})
}
return val;
}
function change(val) {
if (val && val.length && multipleEnvSingle.value) {
if (val instanceof Array && val.length > 1) {
val[0] = val.pop()
}
val = val[0]
} else if (multipleEnvSingle.value) {
val = ""
}
selectRef.value.blur()
selectRef.value.focus()
emit('change', val)
emit('update:modelValue', val)
}
async function onChange(val) {
if (val && val.length
&& !props.disableAutoCreate && props.selectProps.allowCreate) {
val = await handleAutoCreate(val)
}
change(val)
}
async function onFocus() {
emit('focus')
await dictionaryListDisplayRef.value.loadDictList()
}
async function onBlur() {
// 这里需要延迟是因为需要时间去创建这个字典
setTimeout(() => {
emit('blur')
}, 200)
}
const dictionaryListDisplayRef = ref();
defineExpose({
dictionaryListDisplayRef,
selectRef,
})
</script>
<template>
<el-select ref="selectRef"
:style="style"
:class="className"
@focus="onFocus"
@blur="onBlur"
v-bind="selectProps"
:model-value="selectValue"
@change="onChange">
<DictionaryListDisplay ref="dictionaryListDisplayRef"
:dict-type="dictType"
:set-dict-list="(val) => allDict = val">
<template #default="{dict}">
<slot name="option" :dict="dict">
<el-option :label="dict.name" :key="dict.id" :value="dict.value" />
</slot>
</template>
</DictionaryListDisplay>
<!--region el-select 的插槽-->
<template #tag>
<slot name="tag" />
</template>
<template #empty>
<slot name="empty" />
</template>
<template #loading>
<slot name="loading" />
</template>
<!--endregion-->
</el-select>
</template>
这里组件封装时候也继承了原来 el-select 全部的数据而且并使用 el-select 原有的属性判断是否是多选, 用户无需额外传递新的参数进来. 又因为我这边有要求单选要显示 tag 样式所以这块组件单独封装 value 值为数组, 如果你不需要可以直接将值绑定进来