188 lines
6.0 KiB
JavaScript
188 lines
6.0 KiB
JavaScript
import { defineStore } from 'pinia'
|
||
import { ref } from 'vue'
|
||
import { getDictOptions, getDictOptionsBatch, getAllDictOptions } from '@/api/dict'
|
||
|
||
/**
|
||
* 字典 Store(平台端)
|
||
*
|
||
* 全部字典数据都来自数据库,前端不维护编码清单:
|
||
* - 登录后调用一次 loadAll(),把库里所有「启用」字典读进缓存;
|
||
* - 页面按库里真实的 dict_code 取值即可(字符串直接写,无需常量文件);
|
||
* - 之后在字典管理页新增的字典,第一次被引用时会按需自动加载。
|
||
*
|
||
* 用法:
|
||
* const dictStore = useDictStore()
|
||
* dictStore.getDictLabel('payment_order_status', row.status) // 文案
|
||
* dictStore.getDictTagType('payment_order_status', row.status) // 标签色
|
||
* dictStore.getSelectOptions('payment_channel') // 下拉选项
|
||
*/
|
||
export const useDictStore = defineStore('dict', () => {
|
||
/** dict_code -> 字典项数组(渲染依赖它,必须是响应式的) */
|
||
const dictMap = ref({})
|
||
|
||
/** 已加载过的编码(仅用于去重,不参与渲染) */
|
||
let loaded = {}
|
||
/** 正在请求中的编码 -> Promise(并发调用复用同一个请求) */
|
||
const pending = new Map()
|
||
/** 是否已完成全量预热 */
|
||
let allLoaded = false
|
||
let allPending = null
|
||
|
||
function setItems(code, list) {
|
||
dictMap.value = { ...dictMap.value, [code]: Array.isArray(list) ? list : [] }
|
||
}
|
||
|
||
function itemsOf(code) {
|
||
return dictMap.value[code] || []
|
||
}
|
||
|
||
/** 把入参规范成去重后的编码数组 */
|
||
function normalizeCodes(codes) {
|
||
return [...new Set((codes || []).map((c) => String(c || '').trim()).filter(Boolean))]
|
||
}
|
||
|
||
/**
|
||
* 加载字典(带缓存与并发去重)。
|
||
* 请求失败也会标记为已加载,避免页面反复重试;需要重试请先 clearCache。
|
||
*/
|
||
async function load(codes) {
|
||
const list = normalizeCodes(codes)
|
||
const missing = list.filter((c) => !loaded[c] && !pending.has(c))
|
||
|
||
if (missing.length) {
|
||
const promise = (async () => {
|
||
if (missing.length === 1) {
|
||
// 单个字典走专用接口
|
||
const code = missing[0]
|
||
try {
|
||
const res = await getDictOptions(code)
|
||
setItems(code, res?.code === 200 ? res.data?.list || [] : [])
|
||
} catch {
|
||
setItems(code, [])
|
||
}
|
||
} else {
|
||
// 多个字典合并成一次请求
|
||
try {
|
||
const res = await getDictOptionsBatch(missing)
|
||
const dicts = res?.code === 200 ? res.data?.dicts || {} : {}
|
||
missing.forEach((code) => setItems(code, dicts[code] || []))
|
||
} catch {
|
||
missing.forEach((code) => setItems(code, []))
|
||
}
|
||
}
|
||
})().finally(() => {
|
||
missing.forEach((code) => {
|
||
loaded[code] = true
|
||
pending.delete(code)
|
||
})
|
||
})
|
||
|
||
missing.forEach((code) => pending.set(code, promise))
|
||
}
|
||
|
||
await Promise.all(list.map((c) => pending.get(c) || Promise.resolve()))
|
||
return list.flatMap((c) => itemsOf(c))
|
||
}
|
||
|
||
/**
|
||
* 全量预热:一次请求把库里所有启用字典读进缓存(登录后调用一次即可)。
|
||
* 之后再新增的字典仍会走 load() 按需加载。
|
||
*/
|
||
function loadAll() {
|
||
if (allLoaded) return Promise.resolve()
|
||
if (allPending) return allPending
|
||
|
||
allPending = (async () => {
|
||
try {
|
||
const res = await getAllDictOptions()
|
||
const dicts = res?.code === 200 ? res.data?.dicts || {} : {}
|
||
const next = { ...dictMap.value }
|
||
Object.entries(dicts).forEach(([code, list]) => {
|
||
loaded[code] = true
|
||
next[code] = Array.isArray(list) ? list : []
|
||
})
|
||
dictMap.value = next
|
||
} catch {
|
||
// 预热失败不影响使用:后续按需加载
|
||
} finally {
|
||
allLoaded = true
|
||
allPending = null
|
||
}
|
||
})()
|
||
return allPending
|
||
}
|
||
|
||
/** 同步触发加载(不等待),用于组件 setup/onMounted 里“先渲染后填充” */
|
||
function ensure(code) {
|
||
const list = normalizeCodes([code])
|
||
if (list.some((c) => !loaded[c] && !pending.has(c))) {
|
||
load(list)
|
||
}
|
||
}
|
||
|
||
/** 同步取字典项;未加载时顺带触发加载,拿到后由响应式自动刷新 */
|
||
function getDictItemsSync(code) {
|
||
ensure(code)
|
||
return itemsOf(code)
|
||
}
|
||
|
||
/** 值 → 字典项(值按字符串比较,兼容 1 / '1' 混用) */
|
||
function getDictItem(code, value) {
|
||
if (value === undefined || value === null || value === '') return null
|
||
const key = String(value)
|
||
return itemsOf(code).find((it) => String(it.item_value) === key) || null
|
||
}
|
||
|
||
/** 值 → 文案;取不到时返回 fallback,未传则原样返回取值 */
|
||
function getDictLabel(code, value, fallback) {
|
||
const hit = getDictItem(code, value)
|
||
if (hit) return hit.item_label
|
||
if (fallback !== undefined) return fallback
|
||
return value === undefined || value === null ? '' : String(value)
|
||
}
|
||
|
||
/** 值 → el-tag 的 type;取不到时返回 fallback(默认 info) */
|
||
function getDictTagType(code, value, fallback = 'info') {
|
||
const hit = getDictItem(code, value)
|
||
return hit && hit.tag_type ? hit.tag_type : fallback
|
||
}
|
||
|
||
/** 值 → 下拉选项 { label, value },供 el-select / el-option 直接使用 */
|
||
function getSelectOptions(code) {
|
||
return getDictItemsSync(code).map((it) => ({ label: it.item_label, value: it.item_value, tag_type: it.tag_type }))
|
||
}
|
||
|
||
/** 预加载多个字典(一次批量请求) */
|
||
function preloadDicts(codes) {
|
||
return load(codes)
|
||
}
|
||
|
||
/** 清空缓存:传编码清单个,不传清全部(同时允许重新全量预热) */
|
||
function clearCache(code) {
|
||
if (code) {
|
||
delete loaded[code]
|
||
const next = { ...dictMap.value }
|
||
delete next[code]
|
||
dictMap.value = next
|
||
return
|
||
}
|
||
loaded = {}
|
||
dictMap.value = {}
|
||
allLoaded = false
|
||
}
|
||
|
||
return {
|
||
dictMap,
|
||
ensure,
|
||
loadAll,
|
||
getDictItems: load,
|
||
getDictItemsSync,
|
||
getDictItem,
|
||
getDictLabel,
|
||
getDictTagType,
|
||
getSelectOptions,
|
||
preloadDicts,
|
||
clearCache
|
||
}
|
||
})
|