refactor: http util and route store (#43)

This commit is contained in:
ppxb
2025-01-04 21:09:01 +08:00
committed by GitHub
parent aa14d6bdea
commit d3aeca81d8
7 changed files with 129 additions and 172 deletions

View File

@@ -63,6 +63,7 @@
"@antfu/eslint-config": "^2.16.3", "@antfu/eslint-config": "^2.16.3",
"@arco-design/web-vue": "^2.56.0", "@arco-design/web-vue": "^2.56.0",
"@types/crypto-js": "^4.2.2", "@types/crypto-js": "^4.2.2",
"@types/lodash-es": "^4.17.12",
"@types/node": "^20.2.5", "@types/node": "^20.2.5",
"@types/query-string": "^6.3.0", "@types/query-string": "^6.3.0",
"@vitejs/plugin-vue": "^5.0.4", "@vitejs/plugin-vue": "^5.0.4",

16
pnpm-lock.yaml generated
View File

@@ -150,6 +150,9 @@ importers:
'@types/crypto-js': '@types/crypto-js':
specifier: ^4.2.2 specifier: ^4.2.2
version: 4.2.2 version: 4.2.2
'@types/lodash-es':
specifier: ^4.17.12
version: 4.17.12
'@types/node': '@types/node':
specifier: ^20.2.5 specifier: ^20.2.5
version: 20.12.12 version: 20.12.12
@@ -1155,6 +1158,12 @@ packages:
'@types/linkify-it@3.0.5': '@types/linkify-it@3.0.5':
resolution: {integrity: sha512-yg6E+u0/+Zjva+buc3EIb+29XEg4wltq7cSmd4Uc2EE/1nUVmxyzpX6gUXD0V8jIrG0r7YeOGVIbYRkxeooCtw==} 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': '@types/markdown-it@13.0.9':
resolution: {integrity: sha512-1XPwR0+MgXLWfTn9gCsZ55AHOKW1WN+P9vr0PaQh5aerR9LLQXUbjfEAFhjmEmyoYFWAyuN2Mqkn40MZ4ukjBw==} resolution: {integrity: sha512-1XPwR0+MgXLWfTn9gCsZ55AHOKW1WN+P9vr0PaQh5aerR9LLQXUbjfEAFhjmEmyoYFWAyuN2Mqkn40MZ4ukjBw==}
@@ -1455,6 +1464,7 @@ packages:
acorn-import-assertions@1.9.0: acorn-import-assertions@1.9.0:
resolution: {integrity: sha512-cmMwop9x+8KFhxvKrKfPYmN6/pKTYYHBqLa0DfvVZcKMJWNyWLnaqND7dx/qn66R7ewM1UX5XMaDVP5wlVTaVA==} resolution: {integrity: sha512-cmMwop9x+8KFhxvKrKfPYmN6/pKTYYHBqLa0DfvVZcKMJWNyWLnaqND7dx/qn66R7ewM1UX5XMaDVP5wlVTaVA==}
deprecated: package has been renamed to acorn-import-attributes
peerDependencies: peerDependencies:
acorn: ^8 acorn: ^8
@@ -5703,6 +5713,12 @@ snapshots:
'@types/linkify-it@3.0.5': {} '@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': '@types/markdown-it@13.0.9':
dependencies: dependencies:
'@types/linkify-it': 3.0.5 '@types/linkify-it': 3.0.5

View File

@@ -0,0 +1,13 @@
type ImportVueFileType = typeof import('*.vue')
type ImportVueFileFnType = () => Promise<ImportVueFileType>
const moduleFiles = import.meta.glob<ImportVueFileType>('@/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<ImportVueFileFnType>)

View File

@@ -1,51 +1,36 @@
import { defineStore } from 'pinia' import { defineStore } from 'pinia'
import type { DictState, LabelValueState } from '@/types/global' import type { LabelValueState } from '@/types/global'
const storeSetup = () => { const storeSetup = () => {
const dictData = ref<DictState[]>([]) const dictData = ref(new Map<string, LabelValueState[]>())
// 设置字典 // 设置字典
const setDict = (_code: string, items: Array<LabelValueState>) => { const setDict = (code: string, items: Array<LabelValueState>) => {
if (_code !== null && _code !== '') { if (code) {
dictData.value.push({ dictData.value.set(code, items)
code: _code,
items,
})
} }
} }
// 获取字典 // 获取字典
const getDict = (_code: string) => { const getDict = (code: string) => {
if (_code == null || _code === '') { if (!code) return null
return null return dictData.value.get(code) || 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 deleteDict = (_code: string) => { const deleteDict = (code: string) => {
let bln = false
try { try {
for (let i = 0; i < dictData.value.length; i += 1) { return dictData.value.delete(code)
if (dictData.value[i].code === _code) {
dictData.value.splice(i, 1)
return true
}
}
} catch (e) { } catch (e) {
bln = false return false
} }
return bln
} }
// 清空字典 // 清空字典
const cleanDict = () => { const cleanDict = () => {
dictData.value = [] dictData.value.clear()
} }
return { return {
setDict, setDict,
getDict, getDict,

View File

@@ -4,36 +4,18 @@ import type { RouteRecordRaw } from 'vue-router'
import { mapTree, toTreeArray } from 'xe-utils' import { mapTree, toTreeArray } from 'xe-utils'
import { cloneDeep, omit } from 'lodash-es' import { cloneDeep, omit } from 'lodash-es'
import { constantRoutes, systemRoutes } from '@/router/route' import { constantRoutes, systemRoutes } from '@/router/route'
import ParentView from '@/components/ParentView/index.vue'
import { type RouteItem, getUserRoute } from '@/apis' import { type RouteItem, getUserRoute } from '@/apis'
import { transformPathToName } from '@/utils' import { transformPathToName } from '@/utils'
import { asyncRouteModules } from '@/router/asyncModules'
const Layout = () => import('@/layout/index.vue') const layoutComponentMap = {
Layout: () => import('@/layout/index.vue'),
// 匹配views里面所有的.vue文件 ParentView: () => import('@/components/ParentView/index.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
} }
/** 将component由字符串转成真正的模块 */ /** 将component由字符串转成真正的模块 */
const transformComponentView = (component: string) => { const transformComponentView = (component: string) => {
if (component === 'Layout') { return layoutComponentMap[component as keyof typeof layoutComponentMap] || asyncRouteModules[component]
return Layout as never
} else if (component === 'ParentView') {
return ParentView as never
} else {
return loadView(component) as never
}
} }
/** /**
@@ -44,16 +26,20 @@ const transformComponentView = (component: string) => {
*/ */
const formatAsyncRoutes = (menus: RouteItem[]) => { const formatAsyncRoutes = (menus: RouteItem[]) => {
if (!menus.length) return [] if (!menus.length) return []
const pathMap = new Map() const pathMap = new Map()
const routes = mapTree(menus, (item) => { return mapTree(menus, (item) => {
pathMap.set(item.id, item.path) 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) { if (item.parentId && item.type === 2 && item.permission) {
item.activeMenu = pathMap.get(item.parentId) item.activeMenu = pathMap.get(item.parentId)
} }
return { return {
path: item.path, path: item.path,
name: item.name ?? transformPathToName(item.path), name: item.name ?? transformPathToName(item.path),
@@ -68,30 +54,24 @@ const formatAsyncRoutes = (menus: RouteItem[]) => {
activeMenu: item.activeMenu, activeMenu: item.activeMenu,
}, },
} }
}) }) as unknown as RouteRecordRaw[]
return routes as RouteRecordRaw[]
} }
/** 判断路由层级是否大于 2 */ /** 判断路由层级是否大于 2 */
export const isMultipleRoute = (route: RouteRecordRaw) => { export const isMultipleRoute = (route: RouteRecordRaw) => {
const children = route.children return route.children?.some((child) => child.children?.length) ?? false
if (children?.length) {
// 只要有一个子路由的 children 长度大于 0就说明是三级及其以上路由
return children.some((child) => child.children?.length)
}
return false
} }
/** 路由降级(把三级及其以上的路由转化为二级路由) */ /** 路由降级(把三级及其以上的路由转化为二级路由) */
export const flatMultiLevelRoutes = (routes: RouteRecordRaw[]) => { export const flatMultiLevelRoutes = (routes: RouteRecordRaw[]) => {
const cloneRoutes = cloneDeep(routes) return cloneDeep(routes).map((route) => {
cloneRoutes.forEach((route) => { if (!isMultipleRoute(route)) return route
if (isMultipleRoute(route)) {
const flatRoutes = toTreeArray(route.children) return {
route.children = flatRoutes.map((i) => omit(i, 'children')) as RouteRecordRaw[] ...route,
children: toTreeArray(route.children).map((item) => omit(item, 'children')) as RouteRecordRaw[],
} }
}) })
return cloneRoutes
} }
const storeSetup = () => { const storeSetup = () => {
@@ -109,17 +89,12 @@ const storeSetup = () => {
} }
// 生成路由 // 生成路由
const generateRoutes = (): Promise<RouteRecordRaw[]> => { const generateRoutes = async (): Promise<RouteRecordRaw[]> => {
return new Promise((resolve) => { const { data } = await getUserRoute()
// 向后端请求路由数据 这个接口已经根据用户角色过滤了没权限的路由(后端根据用户角色过滤路由显得比较安全些) const asyncRoutes = formatAsyncRoutes(data)
getUserRoute().then((res) => { const flatRoutes = flatMultiLevelRoutes(cloneDeep(asyncRoutes))
const asyncRoutes = formatAsyncRoutes(res.data) setRoutes(asyncRoutes)
setRoutes(asyncRoutes) return flatRoutes
const cloneRoutes = cloneDeep(asyncRoutes)
const flatRoutes = flatMultiLevelRoutes(cloneRoutes as RouteRecordRaw[])
resolve(flatRoutes)
})
})
} }
return { return {

View File

@@ -14,10 +14,8 @@ export interface LabelValueState {
extra?: string extra?: string
} }
/** 字典类型 */ declare global{
export interface DictState { type Recordable<T = any> = Record<string, T>
code: string
items: Array<LabelValueState>
} }
/** 状态1启用2禁用 */ /** 状态1启用2禁用 */

View File

@@ -1,6 +1,6 @@
import axios from 'axios' import axios from 'axios'
import qs from 'query-string' 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 { useUserStore } from '@/stores'
import { getToken } from '@/utils/auth' import { getToken } from '@/utils/auth'
import modalErrorWrapper from '@/utils/modal-error-wrapper' import modalErrorWrapper from '@/utils/modal-error-wrapper'
@@ -34,6 +34,16 @@ const http: AxiosInstance = axios.create({
timeout: 30 * 1000, 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( http.interceptors.request.use(
(config: AxiosRequestConfig) => { (config: AxiosRequestConfig) => {
@@ -46,9 +56,7 @@ http.interceptors.request.use(
} }
return config return config
}, },
(error) => { (error) => Promise.reject(error),
return Promise.reject(error)
},
) )
// 响应拦截器 // 响应拦截器
@@ -56,11 +64,8 @@ http.interceptors.response.use(
(response: AxiosResponse) => { (response: AxiosResponse) => {
const { data } = response const { data } = response
const { success, code, msg } = data const { success, code, msg } = data
if (response.request.responseType === 'blob') {
return response if (response.request.responseType === 'blob' || success) {
}
// 成功
if (success) {
return response return response
} }
@@ -79,104 +84,68 @@ http.interceptors.response.use(
}, },
}) })
} else { } else {
// 如果错误信息长度过长,使用 Notification 进行提示 handleError(msg)
if (msg.length <= 15) {
messageErrorWrapper({
content: msg || '服务器端错误',
duration: 5 * 1000,
})
} else {
notificationErrorWrapper(msg || '服务器端错误')
}
} }
return Promise.reject(new Error(msg || '服务器端错误')) return Promise.reject(new Error(msg || '服务器端错误'))
}, },
(error) => { (error: AxiosError) => {
const response = Object.assign({}, error.response) if (!error.response) {
response handleError('网络连接失败,请检查您的网络')
&& messageErrorWrapper({ return Promise.reject(error)
content: StatusCodeMessage[response.status] || '服务器暂时未响应,请刷新页面并重试。若无法解决,请联系管理员', }
duration: 5 * 1000, const status = error.response?.status
}) const errorMsg = StatusCodeMessage[status] || '服务器暂时未响应,请刷新页面并重试。若无法解决,请联系管理员'
handleError(errorMsg)
return Promise.reject(error) return Promise.reject(error)
}, },
) )
const request = <T = unknown>(config: AxiosRequestConfig): Promise<ApiRes<T>> => { const request = async <T = unknown>(config: AxiosRequestConfig): Promise<ApiRes<T>> => {
return new Promise((resolve, reject) => { return http.request<T>(config)
http .then((res: AxiosResponse) => res.data)
.request<T>(config) .catch((err: { msg: string }) => Promise.reject(err))
.then((res: AxiosResponse) => resolve(res.data))
.catch((err: { msg: string }) => reject(err))
})
} }
const requestNative = <T = unknown>(config: AxiosRequestConfig): Promise<AxiosResponse> => { const requestNative = async <T = unknown>(config: AxiosRequestConfig): Promise<AxiosResponse> => {
return new Promise((resolve, reject) => { return http.request<T>(config)
http .then((res: AxiosResponse) => res)
.request<T>(config) .catch((err: { msg: string }) => Promise.reject(err))
.then((res: AxiosResponse) => resolve(res))
.catch((err: { msg: string }) => reject(err))
})
} }
const get = <T = any>(url: string, params?: object, config?: AxiosRequestConfig): Promise<ApiRes<T>> => { const createRequest = (method: string) => {
return request({ return <T = any>(url: string, params?: object, config?: AxiosRequestConfig): Promise<ApiRes<T>> => {
method: 'get', return request({
url, method,
params, url,
paramsSerializer: (obj) => { [method === 'get' ? 'params' : 'data']: params,
return qs.stringify(obj) ...(method === 'get'
}, ? {
...config, paramsSerializer: (obj) => qs.stringify(obj),
}) }
: {}),
...config,
})
}
} }
const post = <T = any>(url: string, params?: object, config?: AxiosRequestConfig): Promise<ApiRes<T>> => {
return request({
method: 'post',
url,
data: params,
...config,
})
}
const put = <T = any>(url: string, params?: object, config?: AxiosRequestConfig): Promise<ApiRes<T>> => {
return request({
method: 'put',
url,
data: params,
...config,
})
}
const patch = <T = any>(url: string, params?: object, config?: AxiosRequestConfig): Promise<ApiRes<T>> => {
return request({
method: 'patch',
url,
data: params,
...config,
})
}
const del = <T = any>(url: string, params?: object, config?: AxiosRequestConfig): Promise<ApiRes<T>> => {
return request({
method: 'delete',
url,
data: params,
...config,
})
}
const download = (url: string, params?: object, config?: AxiosRequestConfig): Promise<AxiosResponse> => { const download = (url: string, params?: object, config?: AxiosRequestConfig): Promise<AxiosResponse> => {
return requestNative({ return requestNative({
method: 'get', method: 'get',
url, url,
responseType: 'blob', responseType: 'blob',
params, params,
paramsSerializer: (obj) => { paramsSerializer: (obj) => qs.stringify(obj),
return qs.stringify(obj)
},
...config, ...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,
}