feat(dict): add shared dictionary utilities and components
This commit is contained in:
parent
340eaf5bb9
commit
d50611c05b
71
src/components/common/DictTag.vue
Normal file
71
src/components/common/DictTag.vue
Normal 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>
|
||||
@ -1,2 +1,3 @@
|
||||
export * from './useBoolean'
|
||||
export * from './usePermission'
|
||||
export * from './useDict'
|
||||
|
||||
75
src/hooks/useDict.ts
Normal file
75
src/hooks/useDict.ts
Normal 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
102
src/store/dict.ts
Normal 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,
|
||||
}
|
||||
})
|
||||
@ -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
65
src/utils/dict.ts
Normal 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
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user