diff --git a/package.json b/package.json index b52f2bc..7b8d002 100644 --- a/package.json +++ b/package.json @@ -63,6 +63,7 @@ "@antfu/eslint-config": "^2.16.3", "@arco-design/web-vue": "^2.56.0", "@types/crypto-js": "^4.2.2", + "@types/lodash-es": "^4.17.12", "@types/node": "^20.2.5", "@types/query-string": "^6.3.0", "@vitejs/plugin-vue": "^5.0.4", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 9de94ba..78deafa 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -150,6 +150,9 @@ importers: '@types/crypto-js': specifier: ^4.2.2 version: 4.2.2 + '@types/lodash-es': + specifier: ^4.17.12 + version: 4.17.12 '@types/node': specifier: ^20.2.5 version: 20.12.12 @@ -1155,6 +1158,12 @@ packages: '@types/linkify-it@3.0.5': resolution: {integrity: sha512-yg6E+u0/+Zjva+buc3EIb+29XEg4wltq7cSmd4Uc2EE/1nUVmxyzpX6gUXD0V8jIrG0r7YeOGVIbYRkxeooCtw==} + '@types/lodash-es@4.17.12': + resolution: {integrity: sha512-0NgftHUcV4v34VhXm8QBSftKVXtbkBG3ViCjs6+eJ5a6y6Mi/jiFGPc1sC7QK+9BFhWrURE3EOggmWaSxL9OzQ==} + + '@types/lodash@4.17.13': + resolution: {integrity: sha512-lfx+dftrEZcdBPczf9d0Qv0x+j/rfNCMuC6OcfXmO8gkfeNAY88PgKUbvG56whcN23gc27yenwF6oJZXGFpYxg==} + '@types/markdown-it@13.0.9': resolution: {integrity: sha512-1XPwR0+MgXLWfTn9gCsZ55AHOKW1WN+P9vr0PaQh5aerR9LLQXUbjfEAFhjmEmyoYFWAyuN2Mqkn40MZ4ukjBw==} @@ -1455,6 +1464,7 @@ packages: acorn-import-assertions@1.9.0: resolution: {integrity: sha512-cmMwop9x+8KFhxvKrKfPYmN6/pKTYYHBqLa0DfvVZcKMJWNyWLnaqND7dx/qn66R7ewM1UX5XMaDVP5wlVTaVA==} + deprecated: package has been renamed to acorn-import-attributes peerDependencies: acorn: ^8 @@ -5703,6 +5713,12 @@ snapshots: '@types/linkify-it@3.0.5': {} + '@types/lodash-es@4.17.12': + dependencies: + '@types/lodash': 4.17.13 + + '@types/lodash@4.17.13': {} + '@types/markdown-it@13.0.9': dependencies: '@types/linkify-it': 3.0.5 diff --git a/src/router/asyncModules.ts b/src/router/asyncModules.ts new file mode 100644 index 0000000..4f2a444 --- /dev/null +++ b/src/router/asyncModules.ts @@ -0,0 +1,13 @@ +type ImportVueFileType = typeof import('*.vue') +type ImportVueFileFnType = () => Promise + +const moduleFiles = import.meta.glob('@/views/**/*.vue') + +export const asyncRouteModules = Object.entries(moduleFiles).reduce((routes, [url, importFn]) => { + if (!/\/(views\/login|components)\//.test(url)) { + const path = url.replace('/src/views/', '').replace('.vue', '') + routes[path] = importFn + } + + return routes +}, {} as Recordable) diff --git a/src/stores/modules/dict.ts b/src/stores/modules/dict.ts index 5878495..d1d7fa9 100644 --- a/src/stores/modules/dict.ts +++ b/src/stores/modules/dict.ts @@ -1,51 +1,36 @@ import { defineStore } from 'pinia' -import type { DictState, LabelValueState } from '@/types/global' +import type { LabelValueState } from '@/types/global' const storeSetup = () => { - const dictData = ref([]) + const dictData = ref(new Map()) // 设置字典 - const setDict = (_code: string, items: Array) => { - if (_code !== null && _code !== '') { - dictData.value.push({ - code: _code, - items, - }) + const setDict = (code: string, items: Array) => { + if (code) { + dictData.value.set(code, items) } } // 获取字典 - const getDict = (_code: string) => { - if (_code == null || _code === '') { - return null - } - for (let i = 0; i < dictData.value.length; i += 1) { - if (dictData.value[i].code === _code) { - return dictData.value[i].items - } - } - return null + const getDict = (code: string) => { + if (!code) return null + return dictData.value.get(code) || null } // 删除字典 - const deleteDict = (_code: string) => { - let bln = false + const deleteDict = (code: string) => { try { - for (let i = 0; i < dictData.value.length; i += 1) { - if (dictData.value[i].code === _code) { - dictData.value.splice(i, 1) - return true - } - } + return dictData.value.delete(code) } catch (e) { - bln = false + return false } - return bln } + // 清空字典 const cleanDict = () => { - dictData.value = [] + dictData.value.clear() } + return { setDict, getDict, diff --git a/src/stores/modules/route.ts b/src/stores/modules/route.ts index 78a683a..826e69f 100644 --- a/src/stores/modules/route.ts +++ b/src/stores/modules/route.ts @@ -4,36 +4,18 @@ import type { RouteRecordRaw } from 'vue-router' import { mapTree, toTreeArray } from 'xe-utils' import { cloneDeep, omit } from 'lodash-es' import { constantRoutes, systemRoutes } from '@/router/route' -import ParentView from '@/components/ParentView/index.vue' import { type RouteItem, getUserRoute } from '@/apis' import { transformPathToName } from '@/utils' +import { asyncRouteModules } from '@/router/asyncModules' -const Layout = () => import('@/layout/index.vue') - -// 匹配views里面所有的.vue文件 -const modules = import.meta.glob('@/views/**/*.vue') - -/** 加载模块 */ -export const loadView = (view: string) => { - let res - for (const path in modules) { - const dir = path.split('views/')[1].split('.vue')[0] - if (dir === view) { - res = () => modules[path]() - } - } - return res +const layoutComponentMap = { + Layout: () => import('@/layout/index.vue'), + ParentView: () => import('@/components/ParentView/index.vue'), } /** 将component由字符串转成真正的模块 */ const transformComponentView = (component: string) => { - if (component === 'Layout') { - return Layout as never - } else if (component === 'ParentView') { - return ParentView as never - } else { - return loadView(component) as never - } + return layoutComponentMap[component as keyof typeof layoutComponentMap] || asyncRouteModules[component] } /** @@ -44,16 +26,20 @@ const transformComponentView = (component: string) => { */ const formatAsyncRoutes = (menus: RouteItem[]) => { if (!menus.length) return [] + const pathMap = new Map() - const routes = mapTree(menus, (item) => { + return mapTree(menus, (item) => { pathMap.set(item.id, item.path) - if (item.children && item.children.length) { - item.children.sort((a, b) => (a?.sort ?? 0) - (b?.sort ?? 0)) // 排序 + + if (item.children?.length) { + item.children.sort((a, b) => (a?.sort ?? 0) - (b?.sort ?? 0)) } + // 部分子菜单,例如:通知公告新增、查看详情,需要选中其父菜单 if (item.parentId && item.type === 2 && item.permission) { item.activeMenu = pathMap.get(item.parentId) } + return { path: item.path, name: item.name ?? transformPathToName(item.path), @@ -68,30 +54,24 @@ const formatAsyncRoutes = (menus: RouteItem[]) => { activeMenu: item.activeMenu, }, } - }) - return routes as RouteRecordRaw[] + }) as unknown as RouteRecordRaw[] } /** 判断路由层级是否大于 2 */ export const isMultipleRoute = (route: RouteRecordRaw) => { - const children = route.children - if (children?.length) { - // 只要有一个子路由的 children 长度大于 0,就说明是三级及其以上路由 - return children.some((child) => child.children?.length) - } - return false + return route.children?.some((child) => child.children?.length) ?? false } /** 路由降级(把三级及其以上的路由转化为二级路由) */ export const flatMultiLevelRoutes = (routes: RouteRecordRaw[]) => { - const cloneRoutes = cloneDeep(routes) - cloneRoutes.forEach((route) => { - if (isMultipleRoute(route)) { - const flatRoutes = toTreeArray(route.children) - route.children = flatRoutes.map((i) => omit(i, 'children')) as RouteRecordRaw[] + return cloneDeep(routes).map((route) => { + if (!isMultipleRoute(route)) return route + + return { + ...route, + children: toTreeArray(route.children).map((item) => omit(item, 'children')) as RouteRecordRaw[], } }) - return cloneRoutes } const storeSetup = () => { @@ -109,17 +89,12 @@ const storeSetup = () => { } // 生成路由 - const generateRoutes = (): Promise => { - return new Promise((resolve) => { - // 向后端请求路由数据 这个接口已经根据用户角色过滤了没权限的路由(后端根据用户角色过滤路由显得比较安全些) - getUserRoute().then((res) => { - const asyncRoutes = formatAsyncRoutes(res.data) - setRoutes(asyncRoutes) - const cloneRoutes = cloneDeep(asyncRoutes) - const flatRoutes = flatMultiLevelRoutes(cloneRoutes as RouteRecordRaw[]) - resolve(flatRoutes) - }) - }) + const generateRoutes = async (): Promise => { + const { data } = await getUserRoute() + const asyncRoutes = formatAsyncRoutes(data) + const flatRoutes = flatMultiLevelRoutes(cloneDeep(asyncRoutes)) + setRoutes(asyncRoutes) + return flatRoutes } return { diff --git a/src/types/global.d.ts b/src/types/global.d.ts index 47168f4..45ece5a 100644 --- a/src/types/global.d.ts +++ b/src/types/global.d.ts @@ -14,10 +14,8 @@ export interface LabelValueState { extra?: string } -/** 字典类型 */ -export interface DictState { - code: string - items: Array +declare global{ + type Recordable = Record } /** 状态(1:启用;2:禁用) */ diff --git a/src/utils/http.ts b/src/utils/http.ts index e87de31..8feb0d1 100644 --- a/src/utils/http.ts +++ b/src/utils/http.ts @@ -1,6 +1,6 @@ import axios from 'axios' import qs from 'query-string' -import type { AxiosInstance, AxiosRequestConfig, AxiosResponse } from 'axios' +import type { AxiosError, AxiosInstance, AxiosRequestConfig, AxiosResponse } from 'axios' import { useUserStore } from '@/stores' import { getToken } from '@/utils/auth' import modalErrorWrapper from '@/utils/modal-error-wrapper' @@ -34,6 +34,16 @@ const http: AxiosInstance = axios.create({ timeout: 30 * 1000, }) +const handleError = (msg: string) => { + if (msg.length >= 15) { + return notificationErrorWrapper(msg || '服务器端错误') + } + return messageErrorWrapper({ + content: msg || '服务器端错误', + duration: 5 * 1000, + }) +} + // 请求拦截器 http.interceptors.request.use( (config: AxiosRequestConfig) => { @@ -46,9 +56,7 @@ http.interceptors.request.use( } return config }, - (error) => { - return Promise.reject(error) - }, + (error) => Promise.reject(error), ) // 响应拦截器 @@ -56,11 +64,8 @@ http.interceptors.response.use( (response: AxiosResponse) => { const { data } = response const { success, code, msg } = data - if (response.request.responseType === 'blob') { - return response - } - // 成功 - if (success) { + + if (response.request.responseType === 'blob' || success) { return response } @@ -79,104 +84,68 @@ http.interceptors.response.use( }, }) } else { - // 如果错误信息长度过长,使用 Notification 进行提示 - if (msg.length <= 15) { - messageErrorWrapper({ - content: msg || '服务器端错误', - duration: 5 * 1000, - }) - } else { - notificationErrorWrapper(msg || '服务器端错误') - } + handleError(msg) } return Promise.reject(new Error(msg || '服务器端错误')) }, - (error) => { - const response = Object.assign({}, error.response) - response - && messageErrorWrapper({ - content: StatusCodeMessage[response.status] || '服务器暂时未响应,请刷新页面并重试。若无法解决,请联系管理员', - duration: 5 * 1000, - }) + (error: AxiosError) => { + if (!error.response) { + handleError('网络连接失败,请检查您的网络') + return Promise.reject(error) + } + const status = error.response?.status + const errorMsg = StatusCodeMessage[status] || '服务器暂时未响应,请刷新页面并重试。若无法解决,请联系管理员' + handleError(errorMsg) return Promise.reject(error) }, ) -const request = (config: AxiosRequestConfig): Promise> => { - return new Promise((resolve, reject) => { - http - .request(config) - .then((res: AxiosResponse) => resolve(res.data)) - .catch((err: { msg: string }) => reject(err)) - }) +const request = async (config: AxiosRequestConfig): Promise> => { + return http.request(config) + .then((res: AxiosResponse) => res.data) + .catch((err: { msg: string }) => Promise.reject(err)) } -const requestNative = (config: AxiosRequestConfig): Promise => { - return new Promise((resolve, reject) => { - http - .request(config) - .then((res: AxiosResponse) => resolve(res)) - .catch((err: { msg: string }) => reject(err)) - }) +const requestNative = async (config: AxiosRequestConfig): Promise => { + return http.request(config) + .then((res: AxiosResponse) => res) + .catch((err: { msg: string }) => Promise.reject(err)) } -const get = (url: string, params?: object, config?: AxiosRequestConfig): Promise> => { - return request({ - method: 'get', - url, - params, - paramsSerializer: (obj) => { - return qs.stringify(obj) - }, - ...config, - }) +const createRequest = (method: string) => { + return (url: string, params?: object, config?: AxiosRequestConfig): Promise> => { + return request({ + method, + url, + [method === 'get' ? 'params' : 'data']: params, + ...(method === 'get' + ? { + paramsSerializer: (obj) => qs.stringify(obj), + } + : {}), + ...config, + }) + } } -const post = (url: string, params?: object, config?: AxiosRequestConfig): Promise> => { - return request({ - method: 'post', - url, - data: params, - ...config, - }) -} - -const put = (url: string, params?: object, config?: AxiosRequestConfig): Promise> => { - return request({ - method: 'put', - url, - data: params, - ...config, - }) -} - -const patch = (url: string, params?: object, config?: AxiosRequestConfig): Promise> => { - return request({ - method: 'patch', - url, - data: params, - ...config, - }) -} - -const del = (url: string, params?: object, config?: AxiosRequestConfig): Promise> => { - return request({ - method: 'delete', - url, - data: params, - ...config, - }) -} const download = (url: string, params?: object, config?: AxiosRequestConfig): Promise => { return requestNative({ method: 'get', url, responseType: 'blob', params, - paramsSerializer: (obj) => { - return qs.stringify(obj) - }, + paramsSerializer: (obj) => qs.stringify(obj), ...config, }) } -export default { get, post, put, patch, del, request, requestNative, download } + +export default { + get: createRequest('get'), + post: createRequest('post'), + put: createRequest('put'), + patch: createRequest('patch'), + del: createRequest('delete'), + request, + requestNative, + download, +}