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 './useBoolean'
|
||||||
export * from './usePermission'
|
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 './auth'
|
||||||
export * from './router'
|
export * from './router'
|
||||||
export * from './tab'
|
export * from './tab'
|
||||||
|
export * from './dict'
|
||||||
|
|
||||||
// 安装pinia全局状态库
|
// 安装pinia全局状态库
|
||||||
export function installPinia(app: App) {
|
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