diff --git a/src/components/common/DictTag.vue b/src/components/common/DictTag.vue new file mode 100644 index 0000000..7df1ae5 --- /dev/null +++ b/src/components/common/DictTag.vue @@ -0,0 +1,71 @@ + + + + + diff --git a/src/hooks/index.ts b/src/hooks/index.ts index a0eb588..efec91d 100644 --- a/src/hooks/index.ts +++ b/src/hooks/index.ts @@ -1,2 +1,3 @@ export * from './useBoolean' export * from './usePermission' +export * from './useDict' diff --git a/src/hooks/useDict.ts b/src/hooks/useDict.ts new file mode 100644 index 0000000..2a39d2e --- /dev/null +++ b/src/hooks/useDict.ts @@ -0,0 +1,75 @@ +import type { MaybeRef } from 'vue' +import { computed, unref, watch } from 'vue' +import { toSelectOptions } from '@/utils/dict' +import { useDictStore } from '@/store' +import type { DictDataOption } from '@/service/api/system/dict' + +interface UseDictOptions { + /** 是否在创建时立即加载,默认为 true */ + immediate?: boolean + /** 监听类型变化时是否强制刷新 */ + force?: boolean +} + +export function useDict(dictTypes: MaybeRef, options: UseDictOptions = {}) { + const dictStore = useDictStore() + + const normalizedTypes = computed(() => { + const value = unref(dictTypes) ?? [] + return value.filter((item): item is string => Boolean(item)) + }) + + const load = async (force = false) => { + const types = normalizedTypes.value + if (!types.length) + return + + await dictStore.fetchDicts(types, force) + } + + watch( + normalizedTypes, + (types) => { + if (!types.length) + return + void dictStore.fetchDicts(types, options.force ?? false) + }, + { immediate: options.immediate ?? true }, + ) + + const dictOptions = computed>(() => { + const result: Record = {} + normalizedTypes.value.forEach((type) => { + result[type] = dictStore.getDictOptions(type) + }) + return result + }) + + const isLoading = computed(() => normalizedTypes.value.some(type => dictStore.isLoading(type))) + + const getDictLabel = (dictType: string, value: unknown, fallback?: string) => + dictStore.getDictLabel(dictType, value, fallback) + + const getDictOption = (dictType: string, value: unknown) => + dictStore.getDictOption(dictType, value) + + const getSelectOptions = (dictType: string) => toSelectOptions(dictStore.getDictOptions(dictType)) + + const reload = async (targetTypes?: string[]) => { + const types = targetTypes && targetTypes.length ? targetTypes : normalizedTypes.value + if (!types.length) + return + + await dictStore.fetchDicts(types, true) + } + + return { + dictOptions, + isLoading, + load, + reload, + getDictLabel, + getDictOption, + getSelectOptions, + } +} diff --git a/src/store/dict.ts b/src/store/dict.ts new file mode 100644 index 0000000..a42ce0c --- /dev/null +++ b/src/store/dict.ts @@ -0,0 +1,102 @@ +import { getDictDataByType } from '@/service/api/system/dict' +import type { DictDataOption } from '@/service/api/system/dict' + +type DictValue = string | number | boolean | null | undefined + +export const useDictStore = defineStore('dict-store', () => { + const dictMap = ref>({}) + const loadingMap = ref>({}) + const pendingMap: Record> = {} + + function getDictOptions(dictType: string) { + return dictMap.value[dictType] ?? [] + } + + function getDictOption(dictType: string, value: DictValue) { + if (value === undefined || value === null) + return undefined + + const target = String(value) + return getDictOptions(dictType).find(option => option.dictValue === target) + } + + function getDictLabel(dictType: string, value: DictValue, fallback?: string) { + const option = getDictOption(dictType, value) + if (option) + return option.dictLabel + + if (fallback !== undefined) + return fallback + + if (value === undefined || value === null || value === '') + return '' + + return String(value) + } + + async function fetchDict(dictType: string, force = false) { + if (!dictType) + return [] as DictDataOption[] + + if (!force && dictMap.value[dictType]) + return dictMap.value[dictType] + + if (!force && pendingMap[dictType]) + return pendingMap[dictType] + + const promise = (async () => { + loadingMap.value[dictType] = true + try { + const { isSuccess, data } = await getDictDataByType(dictType) + if (isSuccess && Array.isArray(data)) + dictMap.value[dictType] = data + else if (!dictMap.value[dictType]) + dictMap.value[dictType] = [] + + return dictMap.value[dictType] + } + finally { + loadingMap.value[dictType] = false + delete pendingMap[dictType] + } + })() + + if (!force) + pendingMap[dictType] = promise + + return promise + } + + async function fetchDicts(dictTypes: string[], force = false) { + const uniqueTypes = Array.from(new Set(dictTypes.filter(Boolean))) + await Promise.all(uniqueTypes.map(type => fetchDict(type, force))) + } + + function isLoading(dictType: string) { + return Boolean(loadingMap.value[dictType]) + } + + function invalidate(dictType?: string) { + if (dictType) { + delete dictMap.value[dictType] + delete loadingMap.value[dictType] + delete pendingMap[dictType] + } + else { + dictMap.value = {} + loadingMap.value = {} + Object.keys(pendingMap).forEach(key => delete pendingMap[key]) + } + } + + return { + dictMap, + fetchDict, + fetchDicts, + getDictOptions, + getDictOption, + getDictLabel, + isLoading, + invalidate, + } +}) diff --git a/src/store/index.ts b/src/store/index.ts index acb3543..1df836f 100644 --- a/src/store/index.ts +++ b/src/store/index.ts @@ -5,6 +5,7 @@ export * from './app/index' export * from './auth' export * from './router' export * from './tab' +export * from './dict' // 安装pinia全局状态库 export function installPinia(app: App) { diff --git a/src/utils/dict.ts b/src/utils/dict.ts new file mode 100644 index 0000000..33f373e --- /dev/null +++ b/src/utils/dict.ts @@ -0,0 +1,65 @@ +import type { DictDataOption } from '@/service/api/system/dict' + +export type DictValue = string | number | boolean | null | undefined + +/** + * 将字典选项转换为 Naive UI Select 所需结构 + */ +export function toSelectOptions(dictOptions: DictDataOption[] = []) { + return dictOptions.map(option => ({ + label: option.dictLabel, + value: option.dictValue, + })) +} + +/** + * 通过字典值获取对应的完整选项对象 + */ +export function findDictOption(dictOptions: DictDataOption[] = [], value: DictValue) { + if (value === undefined || value === null) + return undefined + + const target = String(value) + return dictOptions.find(option => option.dictValue === target) +} + +/** + * 获取字典标签,找不到时返回默认值或原始值 + */ +export function findDictLabel(dictOptions: DictDataOption[] = [], value: DictValue, fallback?: string) { + const target = findDictOption(dictOptions, value) + if (target) + return target.dictLabel + + if (fallback !== undefined) + return fallback + + if (value === undefined || value === null || value === '') + return '' + + return String(value) +} + +/** + * 获取字典颜色配置 + */ +export function findDictColor(dictOptions: DictDataOption[] = [], value: DictValue) { + const target = findDictOption(dictOptions, value) + if (!target) + return undefined + + return { + tag: target.dictTag, + color: target.dictColor, + label: target.dictLabel, + } +} + +/** + * 将字典数组转为值 -> 选项的 Map,方便重复查询 + */ +export function createDictMap(dictOptions: DictDataOption[] = []) { + const map = new Map() + dictOptions.forEach(option => map.set(option.dictValue, option)) + return map +}