feat(dict): add shared dictionary utilities and components

This commit is contained in:
Leo 2025-09-27 00:51:44 +08:00
parent 340eaf5bb9
commit d50611c05b
6 changed files with 315 additions and 0 deletions

View File

@ -0,0 +1,71 @@
<template>
<n-tag
v-if="option"
:type="tagType"
:style="tagStyle"
:size="size"
:bordered="!option.dictColor"
class="dict-tag"
>
<slot :option="option" :label="option.dictLabel">
{{ option.dictLabel }}
</slot>
</n-tag>
<span v-else class="dict-tag__placeholder">
<slot name="placeholder" :value="value">
{{ fallbackLabel }}
</slot>
</span>
</template>
<script setup lang="ts">
import { computed } from 'vue'
import type { TagProps } from 'naive-ui'
import { useDict } from '@/hooks'
type DictValue = string | number | boolean | null | undefined
interface DictTagProps {
dictType: string
value: DictValue
size?: NonNullable<TagProps['size']>
fallbackLabel?: string
}
const props = withDefaults(defineProps<DictTagProps>(), {
size: 'small',
fallbackLabel: '-',
})
const { getDictOption, getDictLabel } = useDict(computed(() => [props.dictType]))
const option = computed(() => getDictOption(props.dictType, props.value))
const tagType = computed<TagProps['type']>(() => {
if (!option.value)
return 'default'
return option.value.dictColor ? 'default' : (option.value.dictTag as TagProps['type']) ?? 'default'
})
const tagStyle = computed(() => {
if (!option.value?.dictColor)
return undefined
return {
borderColor: option.value.dictColor,
backgroundColor: option.value.dictColor,
color: '#fff',
} satisfies Record<string, string>
})
const fallbackLabel = computed(() => getDictLabel(props.dictType, props.value, props.fallbackLabel))
const value = computed(() => props.value)
</script>
<style scoped>
.dict-tag__placeholder {
color: var(--n-text-color);
}
</style>

View File

@ -1,2 +1,3 @@
export * from './useBoolean'
export * from './usePermission'
export * from './useDict'

75
src/hooks/useDict.ts Normal file
View File

@ -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<string[] | undefined>, 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<Record<string, DictDataOption[]>>(() => {
const result: Record<string, DictDataOption[]> = {}
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,
}
}

102
src/store/dict.ts Normal file
View File

@ -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<Record<string, DictDataOption[]>>({})
const loadingMap = ref<Record<string, boolean>>({})
const pendingMap: Record<string, Promise<DictDataOption[]>> = {}
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,
}
})

View File

@ -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) {

65
src/utils/dict.ts Normal file
View File

@ -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<string, DictDataOption>()
dictOptions.forEach(option => map.set(option.dictValue, option))
return map
}