diff --git a/admin/src/api/index.vue b/admin/src/api/index.vue deleted file mode 100644 index 649b9ac..0000000 --- a/admin/src/api/index.vue +++ /dev/null @@ -1,306 +0,0 @@ - - - diff --git a/admin/src/components/DictData/index.js b/admin/src/components/DictData/index.js new file mode 100644 index 0000000..8faffbd --- /dev/null +++ b/admin/src/components/DictData/index.js @@ -0,0 +1,98 @@ +import Vue from 'vue' +import store from '@/store' +import DataDict from '@/utils/dict' +import { getDicts as getDicts, getBatchDicts } from '@/api/system/dict/data' + +function searchDictByKey(dict, key) { + if (key == null && key == "") { + return null + } + try { + for (let i = 0; i < dict.length; i++) { + if (dict[i].key == key) { + return dict[i].value + } + } + } catch (e) { + return null + } +} + +function install() { + let waiting = []; + let isRequesting = false; + let timer = null; // 定时器 + + Vue.use(DataDict, { + metas: { + '*': { + labelField: 'dictLabel', + valueField: 'dictValue', + request(dictMeta) { + const storeDict = searchDictByKey(store.getters.dict, dictMeta.type) + if (storeDict) { + return new Promise(resolve => { resolve(storeDict) }) + } else { + return new Promise((resolve, reject) => { + waiting.push({ + type: dictMeta.type, + resolve, + reject, + }); + if (timer || isRequesting) { + return; + } + const startRequest = () => { + const requesting = [...waiting]; + waiting = []; + timer = null; + isRequesting = true; + // 批量获取字典 + getBatchDicts(requesting.map(item => item.type)).then(res => { + const datas = res.data || {}; + requesting.forEach(item => { + store.dispatch('dict/setDict', { key: item.type, value: datas[item.type] || [] }); + item.resolve(datas[item.type] || []); + const wIndex = waiting.findIndex(witem => witem.type == item.type); + if (wIndex > -1) { + waiting[wIndex].resolve(datas[item.type] || []); + waiting.splice(wIndex, 1); + } + }); + isRequesting = false; + if (waiting.length > 0) { + startRequest(); + } + }).catch(error => { + requesting.forEach(item => { + item.reject(error) + }); + isRequesting = false; + if (waiting.length > 0) { + startRequest(); + } + }); + } + + timer = setTimeout(() => { + startRequest(); + }, 300); + + + // getDicts(dictMeta.type).then(res => { + // store.dispatch('dict/setDict', { key: dictMeta.type, value: res.data }) + // resolve(res.data) + // }).catch(error => { + // reject(error) + // }) + }) + } + }, + }, + }, + }) +} + +export default { + install, +} \ No newline at end of file diff --git a/admin/src/components/DictTag/index.vue b/admin/src/components/DictTag/index.vue new file mode 100644 index 0000000..69ffc8c --- /dev/null +++ b/admin/src/components/DictTag/index.vue @@ -0,0 +1,82 @@ + + + + diff --git a/admin/src/store/getters.js b/admin/src/store/getters.js index 8d70256..198e729 100644 --- a/admin/src/store/getters.js +++ b/admin/src/store/getters.js @@ -11,6 +11,7 @@ const getters = { sidebar: state => state.app.sidebar, size: state => state.app.size, + dict: state => state.dict.dict, device: state => state.app.device, visitedViews: state => state.tagsView.visitedViews, cachedViews: state => state.tagsView.cachedViews, diff --git a/admin/src/store/modules/dict.js b/admin/src/store/modules/dict.js new file mode 100644 index 0000000..2f31044 --- /dev/null +++ b/admin/src/store/modules/dict.js @@ -0,0 +1,51 @@ +const state = { + dict: new Array(), + dictData: {} +} +const mutations = { + SET_DICT: (state, { key, value }) => { + if (key !== null && key !== "") { + state.dict.push({ + key: key, + value: value + }) + } + }, + REMOVE_DICT: (state, key) => { + try { + for (let i = 0; i < state.dict.length; i++) { + if (state.dict[i].key == key) { + state.dict.splice(i, i) + return true + } + } + } catch (e) { + } + }, + CLEAN_DICT: (state) => { + state.dict = new Array() + } +} + +const actions = { + // 设置字典 + setDict({ commit }, data) { + commit('SET_DICT', data) + }, + // 删除字典 + removeDict({ commit }, key) { + commit('REMOVE_DICT', key) + }, + // 清空字典 + cleanDict({ commit }) { + commit('CLEAN_DICT') + } +} + +export default { + namespaced: true, + state, + mutations, + actions +} + diff --git a/admin/src/utils/dict/Dict.js b/admin/src/utils/dict/Dict.js new file mode 100644 index 0000000..5dea086 --- /dev/null +++ b/admin/src/utils/dict/Dict.js @@ -0,0 +1,84 @@ +import Vue from 'vue' +import { mergeRecursive } from "@/utils"; +import DictMeta from './DictMeta' +import DictData from './DictData' + +const DEFAULT_DICT_OPTIONS = { + types: [], +} + +/** + * @classdesc 字典 + * @property {Object} label 标签对象,内部属性名为字典类型名称 + * @property {Object} dict 字段数组,内部属性名为字典类型名称 + * @property {Array.} _dictMetas 字典元数据数组 + */ +export default class Dict { + constructor() { + this.owner = null; + this.label = {}; + this.type = {}; + } + + init(options) { + if (options instanceof Array) { + options = { types: options } + } + const opts = mergeRecursive(DEFAULT_DICT_OPTIONS, options) + if (opts.types === undefined) { + throw new Error('need dict types') + } + const ps = [] + this._dictMetas = opts.types.map(t => DictMeta.parse(t)) + this._dictMetas.forEach(dictMeta => { + const type = dictMeta.type + Vue.set(this.label, type, {}) + Vue.set(this.type, type, []) + if (dictMeta.lazy) { + // console.log(`dict of ${type} will be loaded lazily`); + return + } + ps.push(loadDict(this, dictMeta)); + }) + return Promise.all(ps) + } + + /** + * 重新加载字典 + * @param {String} type 字典类型 + */ + reloadDict(type) { + const dictMeta = this._dictMetas.find(e => e.type === type) + if (dictMeta === undefined) { + return Promise.reject(`the dict meta of ${type} was not found`) + } + return loadDict(this, dictMeta) + } +} + +/** + * 加载字典 + * @param {Dict} dict 字典 + * @param {DictMeta} dictMeta 字典元数据 + * @returns {Promise} + */ +function loadDict(dict, dictMeta) { + return dictMeta.request(dictMeta) + .then(response => { + const type = dictMeta.type + let dicts = dictMeta.responseConverter(response, dictMeta) + // console.log('设置字典', dicts); + if (!(dicts instanceof Array)) { + console.error('the return of responseConverter must be Array.') + dicts = [] + } else if (dicts.filter(d => d instanceof DictData).length !== dicts.length) { + console.error('the type of elements in dicts must be DictData') + dicts = [] + } + dict.type[type].splice(0, Number.MAX_SAFE_INTEGER, ...dicts) + dicts.forEach(d => { + Vue.set(dict.label[type], d.value, d.label) + }) + return dicts + }) +} diff --git a/admin/src/utils/dict/DictConverter.js b/admin/src/utils/dict/DictConverter.js new file mode 100644 index 0000000..ab93b5b --- /dev/null +++ b/admin/src/utils/dict/DictConverter.js @@ -0,0 +1,17 @@ +import DictOptions from './DictOptions' +import DictData from './DictData' +// 查找字典key value正确字段 +export default function(dict, dictMeta) { + const label = determineDictField(dict, dictMeta.labelField, ...DictOptions.DEFAULT_LABEL_FIELDS) + const value = determineDictField(dict, dictMeta.valueField, ...DictOptions.DEFAULT_VALUE_FIELDS) + return new DictData(dict[label], dict[value], dict) +} + +/** + * 确定字典字段 + * @param {DictData} dict + * @param {...String} fields + */ +function determineDictField(dict, ...fields) { + return fields.find(f => Object.prototype.hasOwnProperty.call(dict, f)) +} diff --git a/admin/src/utils/dict/DictData.js b/admin/src/utils/dict/DictData.js new file mode 100644 index 0000000..5b07cee --- /dev/null +++ b/admin/src/utils/dict/DictData.js @@ -0,0 +1,15 @@ +/** + * @classdesc 字典数据 + * @property {String} label 标签 + * @property {*} value 标签 + * @property {Object} raw 原始数据 + */ +export default class DictData { + constructor(label, value, raw) { + this.label = label + this.value = value + this.sort = raw?.dictSort + this.code = raw?.dictCode + this.raw = raw; //保存原始数据 + } +} diff --git a/admin/src/utils/dict/DictMeta.js b/admin/src/utils/dict/DictMeta.js new file mode 100644 index 0000000..3d2249c --- /dev/null +++ b/admin/src/utils/dict/DictMeta.js @@ -0,0 +1,39 @@ +import { mergeRecursive } from "@/utils"; +import DictOptions from './DictOptions' + +/** + * @classdesc 字典元数据 + * @property {String} type 类型 + * @property {Function} request 请求 + * @property {String} label 标签字段 + * @property {String} value 值字段 + */ +export default class DictMeta { + constructor(options) { + this.type = options.type + this.request = options.request + this.responseConverter = options.responseConverter + this.labelField = options.labelField + this.valueField = options.valueField + this.lazy = options.lazy === true + } +} + + +/** + * 解析字典元数据 + * @param {Object} options + * @returns {DictMeta} + */ +DictMeta.parse= function(options) { + let opts = null + if (typeof options === 'string') { + opts = DictOptions.metas[options] || {} + opts.type = options + } else if (typeof options === 'object') { + opts = options + } + opts = mergeRecursive(DictOptions.metas['*'], opts); + // console.log('解析结果:', opts) + return new DictMeta(opts) +} diff --git a/admin/src/utils/dict/DictOptions.js b/admin/src/utils/dict/DictOptions.js new file mode 100644 index 0000000..441f4f9 --- /dev/null +++ b/admin/src/utils/dict/DictOptions.js @@ -0,0 +1,52 @@ +import { mergeRecursive } from "@/utils"; +import dictConverter from './DictConverter' + +export const options = { + metas: { + '*': { + /** + * 字典请求,方法签名为function(dictMeta: DictMeta): Promise + */ + request: (dictMeta) => { + console.log(`load dict ${dictMeta.type}`) + return Promise.resolve([]) + }, + /** + * 字典响应数据转换器,方法签名为function(response: Object, dictMeta: DictMeta): DictData + */ + responseConverter, + // 转化后属性 + labelField: 'label', + valueField: 'value', + }, + }, + /** + * 默认标签字段 + */ + DEFAULT_LABEL_FIELDS: ['label', 'name', 'title'], + /** + * 默认值字段 + */ + DEFAULT_VALUE_FIELDS: ['value', 'id', 'uid', 'key'], +} + +/** + * 映射字典 + * @param {Object} response 字典数据 + * @param {DictMeta} dictMeta 字典元数据 + * @returns {DictData} + */ +function responseConverter(response, dictMeta) { + const dicts = response.content instanceof Array ? response.content : response + if (dicts === undefined) { + console.warn(`no dict data of "${dictMeta.type}" found in the response`) + return [] + } + return dicts.map(d => dictConverter(d, dictMeta)) +} + +export function mergeOptions(src) { + mergeRecursive(options, src) +} + +export default options diff --git a/admin/src/utils/dict/index.js b/admin/src/utils/dict/index.js new file mode 100644 index 0000000..215eb9e --- /dev/null +++ b/admin/src/utils/dict/index.js @@ -0,0 +1,33 @@ +import Dict from './Dict' +import { mergeOptions } from './DictOptions' + +export default function(Vue, options) { + mergeOptions(options) + Vue.mixin({ + data() { + if (this.$options === undefined || this.$options.dicts === undefined || this.$options.dicts === null) { + return {} + } + const dict = new Dict() + dict.owner = this + return { + dict + } + }, + created() { + if (!(this.dict instanceof Dict)) { + return + } + options.onCreated && options.onCreated(this.dict) + this.dict.init(this.$options.dicts).then(() => { + options.onReady && options.onReady(this.dict) + this.$nextTick(() => { + this.$emit('dictReady', this.dict) + if (this.$options.methods && this.$options.methods.onDictReady instanceof Function) { + this.$options.methods.onDictReady.call(this, this.dict) + } + }) + }) + }, + }) +} diff --git a/admin/src/utils/index.js b/admin/src/utils/index.js index 3f51a68..07a6ae5 100644 --- a/admin/src/utils/index.js +++ b/admin/src/utils/index.js @@ -431,3 +431,18 @@ export function isWriteOff() { } } +// 数据合并 +export function mergeRecursive(source, target) { + for (var p in target) { + try { + if (target[p].constructor == Object) { + source[p] = mergeRecursive(source[p], target[p]); + } else { + source[p] = target[p]; + } + } catch (e) { + source[p] = target[p]; + } + } + return source; +}; \ No newline at end of file diff --git a/admin/src/utils/parsing.js b/admin/src/utils/parsing.js index a0a6caf..f9adf54 100644 --- a/admin/src/utils/parsing.js +++ b/admin/src/utils/parsing.js @@ -169,52 +169,36 @@ export function handleTree(data, id, parentId, children) { * 路由数据遍历 * */ - export function formatRoutes(routerArr){ - let arr = [],obj = {}; - routerArr.forEach(tmp => { - obj = { - id:tmp.id, - pid:tmp.pid, - name:tmp.name, - url:tmp.component, - path:'/' + tmp.pid + '/', - perms:tmp.perms, - child:tmp.childList.length ? tmp.childList.map(item=>{ - return { - id:item.id, - pid:item.pid, - name:item.name, - url:item.component, - path:'/' + tmp.pid + '/' + item.pid + '/', - perms:item.perms, - extra:item.icon, - child:item.childList.length ? item.childList.map(item1=>{ - return { - id:item1.id, - pid:item1.pid, - name:item1.name, - url:item1.component, - path:'/' + tmp.pid + '/' + item.pid + '/' + item1.pid + '/', - perms:item1.perms, - extra:item1.icon, - child:item1.childList.length ? item1.childList.map(item2=>{ - return { - id:item2.id, - pid:item2.pid, - name:item2.name, - url:item2.component, - path:'/' + tmp.pid + '/' + item.pid + '/' + item1.pid + '/' + item2.pid + '/', - perms:item2.perms, - extra:item2.icon, - } - }) : [] - } - }) : [] - } - }) : [], - extra:tmp.icon, +export function formatRoutes(routerArr) { + /** + * 递归处理单个路由项 + * @param {Object} routeItem 路由项 + * @param {String} parentPath 父级路径,用于构建完整路径 + * @returns {Object} 格式化后的路由对象 + */ + function formatRouteItem(routeItem, parentPath = '') { + const currentPath = parentPath ? `${parentPath}${routeItem.pid}/` : `/${routeItem.pid}/`; + + const formattedRoute = { + id: routeItem.id, + pid: routeItem.pid, + name: routeItem.name, + url: routeItem.component, + path: currentPath, + perms: routeItem.perms, + child: [], + extra: routeItem.icon + }; + + // 递归处理子路由 + if (routeItem.childList && routeItem.childList.length > 0) { + formattedRoute.child = routeItem.childList.map(child => + formatRouteItem(child, currentPath) + ); } - arr.push(obj); - }) - return arr; - } \ No newline at end of file + + return formattedRoute; + } + + return routerArr.map(item => formatRouteItem(item)); +} \ No newline at end of file