From 174f8454c305a7bc418f7ce4a470e6856879e9eb Mon Sep 17 00:00:00 2001 From: andy <1042025947@qq.com> Date: Tue, 4 Aug 2026 14:50:51 +0800 Subject: [PATCH] =?UTF-8?q?=E5=A4=84=E7=90=86vue=E9=A1=B5=E9=9D=A2?= =?UTF-8?q?=E7=99=BB=E5=BD=95=E5=A4=B1=E8=B4=A5=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/router/index.ts | 28 +++++++++--------- src/utils/request.ts | 68 ++++++++++++++++++++++++++------------------ 2 files changed, 54 insertions(+), 42 deletions(-) diff --git a/src/router/index.ts b/src/router/index.ts index ccf2040..8143e1e 100644 --- a/src/router/index.ts +++ b/src/router/index.ts @@ -238,11 +238,9 @@ const router = createRouter({ // 已添加的动态路由 const addedRouteNames = new Set() - - /** * 根据菜单动态添加新路由(只添加静态路由中没有的) - */ + */ export function addDynamicRoutes(menus: any) { //MenuInfo[] //console.log('[动态路由] 开始处理菜单:', menus) const addRoutes = (menuList: any) => { //MenuInfo[] @@ -250,22 +248,22 @@ export function addDynamicRoutes(menus: any) { //MenuInfo[] // 只处理菜单类型(type=2),且有 path if (menu.type === 2 && menu.path) { const routeName = 'Dynamic-' + menu.id - + // 检查是否已经有同路径的静态路由 const existingRoutes = router.getRoutes() const menuPath = menu.path.startsWith('/') ? menu.path.slice(1) : menu.path const pathExists = existingRoutes.some(r => r.path === '/' + menuPath || r.path === menuPath) - + if (pathExists) { // console.log(`[动态路由] 跳过(已存在): ${menuPath}`) continue } - + if (addedRouteNames.has(routeName)) { //console.log(`[动态路由] 跳过(已添加): ${menuPath}`) continue } - + // 判断是否是外链菜单 if (menu.isFrame === 1 && menu.component) { // 外链菜单,使用 iframe 组件 @@ -287,9 +285,9 @@ export function addDynamicRoutes(menus: any) { //MenuInfo[] const componentName = menu.component.startsWith('/') ? menu.component.slice(1) : menu.component const componentPath = `/src/views/${componentName}.vue` const component = modules[componentPath] - + //console.log(`[动态路由] 处理: path=${menuPath}, component=${componentPath}, 组件存在=${!!component}`) - + if (component) { router.addRoute('Layout', { path: menuPath, @@ -308,14 +306,14 @@ export function addDynamicRoutes(menus: any) { //MenuInfo[] } } } - + // 递归处理子菜单 if (menu.children && menu.children.length > 0) { addRoutes(menu.children) } } } - + addRoutes(menus) //console.log('[动态路由] 当前所有路由:', router.getRoutes().map(r => r.path)) } @@ -337,7 +335,7 @@ export function resetRouter() { router.beforeEach(async (to, _from, next) => { console.log('路由测试'); const userStore = useUserStore() - + //document.title = `${to.meta.title || ''} - CSY Admin` if (to.meta.requiresAuth === false) { next() @@ -372,7 +370,7 @@ router.beforeEach(async (to, _from, next) => { } // if (!userStore.user) { - // try { + // try { // await userStore.getInfo() // // 添加动态路由(只添加新的,不影响已有的) // addDynamicRoutes(userStore.menus) @@ -389,7 +387,7 @@ router.beforeEach(async (to, _from, next) => { // next() // return // } - + }) -export default router +export default router \ No newline at end of file diff --git a/src/utils/request.ts b/src/utils/request.ts index bab816e..7c07d4b 100644 --- a/src/utils/request.ts +++ b/src/utils/request.ts @@ -1,4 +1,7 @@ import axios, { type AxiosInstance, type AxiosRequestConfig, type AxiosResponse } from 'axios' +import { useUserStore } from '@/stores/user' + +// const BASE_API = import.meta.env.VITE_APP_BASE_API // API响应结构 interface ApiResponse { @@ -11,24 +14,12 @@ interface ApiResponse { interface CryptoConfig { enabled: boolean publicKey: string - aesKey: string + aesKey: string // AES密钥的Base64编码 } // 加密配置缓存 let cryptoConfigCache: CryptoConfig | null = null -// 从 localStorage 安全读取 token(避免循环依赖) -function getToken(): string | null { - try { - const raw = localStorage.getItem('mes-user') - if (raw) { - const data = JSON.parse(raw) - return data?.token || null - } - } catch {} - return null -} - // 获取加密配置 export async function fetchCryptoConfig(): Promise { if (cryptoConfigCache) { @@ -51,7 +42,7 @@ export function clearCryptoConfigCache() { cryptoConfigCache = null } -// 判断是否是AES加密的响应数据 +// 判断是否是AES加密的响应数据(格式:iv.encryptedData) function isAesEncryptedData(data: any): boolean { if (typeof data !== 'string') { return false @@ -60,9 +51,11 @@ function isAesEncryptedData(data: any): boolean { if (parts.length !== 2) { return false } + // 检查两部分是否都是有效的Base64,且IV长度正确 try { atob(parts[0]) atob(parts[1]) + // IV是12字节,Base64后是16字符 return parts[0].length === 16 && parts[1].length > 10 } catch { return false @@ -80,6 +73,7 @@ async function aesDecrypt(encryptedData: string, aesKeyBase64: string): Promise< const data = Uint8Array.from(atob(parts[1]), c => c.charCodeAt(0)) const keyBytes = Uint8Array.from(atob(aesKeyBase64), c => c.charCodeAt(0)) + // 导入AES密钥 const aesKey = await crypto.subtle.importKey( 'raw', keyBytes, @@ -88,6 +82,7 @@ async function aesDecrypt(encryptedData: string, aesKeyBase64: string): Promise< ['decrypt'] ) + // 解密 const decrypted = await crypto.subtle.decrypt( { name: 'AES-GCM', iv: iv }, aesKey, @@ -110,24 +105,36 @@ async function decryptResponseData(data: string): Promise { return JSON.parse(decryptedStr) } catch (error) { console.error('响应解密失败', error) + // 解密失败可能是密钥过期,清除缓存 cryptoConfigCache = null return data } } +//axios.defaults.headers["Content-Type"] = 'application/json;charset=utf-8' //"application/x-www-form-urlencoded;charset=utf-8"; //"application/json;charset=utf-8"; + // 创建axios实例 +// 后端 API 统一使用 /api 前缀 const service: AxiosInstance = axios.create({ - baseURL: '/api', + baseURL:'/api', timeout: 30000 }) -// 请求拦截器 - 直接从 localStorage 读取 token +// 请求拦截器 service.interceptors.request.use( (config) => { - const token = getToken() - if (token) { - config.headers['Authorization'] = token + const userStore = useUserStore() + if (userStore.token) { + config.headers['Authorization'] = userStore.token } + + if (config.data instanceof FormData) { + // 文件上传:不设置 JSON 请求头、不序列化,交给浏览器自动处理 multipart + return config + } + + config.headers["Content-Type"] = 'application/json;charset=utf-8' + config.data = config.data instanceof Object ? JSON.stringify(config.data) : config.data return config }, (error) => { @@ -141,25 +148,32 @@ let isLoggingOut = false // 响应拦截器 service.interceptors.response.use( async (response: AxiosResponse) => { - // blob 类型响应直接返回 + // 如果是 blob 类型响应(文件下载),直接返回 if (response.config.responseType === 'blob') { return response.data } + if (response.config.responseType === 'arraybuffer') { + // 二进制流不走code校验,直接完整返回ArrayBuffer + return response.data + } + + const res = response.data if (res.code !== 200) { + // logout 接口返回 401 时不显示错误消息(避免干扰) const isLogoutRequest = response.config.url?.includes('/auth/logout') - // 401 未授权 + // 401 未授权,弹出明确提示并跳转登录(防止重复调用) if (res.code === 401 && !isLoggingOut && !isLogoutRequest) { + window.$message?.warning('当前用户登录已过期,请重新登录') + isLoggingOut = true - window.$message?.error('当前用户登录已过期,请重新登录') - // 清除本地 token - localStorage.removeItem('mes-user') + + const userStore = useUserStore() + await userStore.logout() isLoggingOut = false - // 跳转到登录页 - window.location.href = '/login' return Promise.reject(new Error('登录已过期')) } @@ -170,7 +184,7 @@ service.interceptors.response.use( return Promise.reject(new Error(res.message || '请求失败')) } - // AES 解密 + // 检查响应数据是否是AES加密的,自动解密 if (isAesEncryptedData(res.data)) { try { return await decryptResponseData(res.data)