diff --git a/backend/static/image/group.jpg b/backend/static/image/group.jpg index 8e14219d..2bc7c1bc 100644 Binary files a/backend/static/image/group.jpg and b/backend/static/image/group.jpg differ diff --git a/frontend/src/layouts/components/Menu/MixTopMenu.vue b/frontend/src/layouts/components/Menu/MixTopMenu.vue index 0709f53b..5982fb01 100644 --- a/frontend/src/layouts/components/Menu/MixTopMenu.vue +++ b/frontend/src/layouts/components/Menu/MixTopMenu.vue @@ -58,31 +58,51 @@ const topMenus = ref([]); // 处理后的顶部菜单列表 - 智能显示唯一子菜单的标题 const processedTopMenus = computed(() => { - return topMenus.value.map((route) => { - // 如果路由设置了 alwaysShow=true,或者没有子菜单,直接返回原路由 - if (route.meta?.alwaysShow || !route.children || route.children.length === 0) { + return topMenus.value + .map((route) => { + // 如果路由本身设置了hidden=true,直接返回null + if (route.meta?.hidden === true) { + return null; + } + + // 如果路由设置了 alwaysShow=true,或者没有子菜单,直接返回原路由 + if (route.meta?.alwaysShow || !route.children || route.children.length === 0) { + return route; + } + + // 过滤出非隐藏的子菜单 + const visibleChildren = route.children.filter((child) => !child.meta?.hidden); + + // 如果没有可见子菜单,返回null + if (visibleChildren.length === 0) { + return null; + } + + // 如果只有一个非隐藏的子菜单,显示子菜单的信息 + if (visibleChildren.length === 1) { + const onlyChild = visibleChildren[0]; + // 检查子菜单是否应该显示在顶部菜单 + if (onlyChild.meta?.hidden !== true) { + return { + ...route, + meta: { + ...route.meta, + title: onlyChild.meta?.title || route.meta?.title, + icon: onlyChild.meta?.icon || route.meta?.icon, + }, + // 使用子菜单的path确保路由匹配正确 + path: onlyChild.path + }; + } + // 如果子菜单应该隐藏,则不显示该顶级菜单 + return null; + } + + // 其他情况返回原路由 return route; - } - - // 过滤出非隐藏的子菜单 - const visibleChildren = route.children.filter((child) => !child.meta?.hidden); - - // 如果只有一个非隐藏的子菜单,显示子菜单的信息 - if (visibleChildren.length === 1) { - const onlyChild = visibleChildren[0]; - return { - ...route, - meta: { - ...route.meta, - title: onlyChild.meta?.title || route.meta?.title, - icon: onlyChild.meta?.icon || route.meta?.icon, - }, - }; - } - - // 其他情况返回原路由 - return route; - }); + }) + // 过滤掉null值 + .filter((route) => route !== null); }); /** @@ -99,15 +119,18 @@ const handleMenuSelect = (routePath: string) => { * @param skipNavigation 是否跳过导航(路由变化时为true,点击菜单时为false) */ const updateMenuState = (topMenuPath: string, skipNavigation = false) => { - // 不相同才更新,避免重复操作 - if (topMenuPath !== appStore.activeTopMenuPath) { + // 确保路径有效且不相同才更新,避免重复操作 + if (topMenuPath && topMenuPath !== appStore.activeTopMenuPath) { appStore.activeTopMenu(topMenuPath); // 设置激活的顶部菜单 - permissionStore.updateSideMenu(topMenuPath); // 更新左侧菜单 + // 只有当路由映射表中存在该路径时才更新侧边菜单 + if (permissionStore.routePathMap && permissionStore.routePathMap[topMenuPath]) { + permissionStore.updateSideMenu(topMenuPath); // 更新左侧菜单 + } } // 如果是点击菜单且状态已变更,才进行导航 - if (!skipNavigation) { - navigateToFirstLeftMenu(permissionStore.sideMenuRoutes); // 跳转到左侧第一个菜单 + if (!skipNavigation && topMenuPath === appStore.activeTopMenuPath) { + navigateToFirstLeftMenu(permissionStore.sideMenuRoutes || []); // 跳转到左侧第一个菜单 } }; diff --git a/frontend/src/layouts/components/NavBar/components/NavbarActions.vue b/frontend/src/layouts/components/NavBar/components/NavbarActions.vue index fd77b114..7eb78275 100644 --- a/frontend/src/layouts/components/NavBar/components/NavbarActions.vue +++ b/frontend/src/layouts/components/NavBar/components/NavbarActions.vue @@ -22,58 +22,59 @@ - - - - - + + + + + + diff --git a/frontend/src/layouts/views/MixLayout.vue b/frontend/src/layouts/views/MixLayout.vue index fd3171aa..3f9dbe5c 100644 --- a/frontend/src/layouts/views/MixLayout.vue +++ b/frontend/src/layouts/views/MixLayout.vue @@ -109,33 +109,24 @@ function resolvePath(routePath: string) { } if (routePath.startsWith("/")) { - return activeTopMenuPath.value + routePath; + return routePath; } - return `${activeTopMenuPath.value}/${routePath}`; + return `${routePath}`; } -// 监听路由变化,确保左侧菜单能随TagsView切换而正确激活 +// 优化后的路由监听逻辑,仅在顶级路径实际变化时更新菜单 +let prevTopMenuPath = ''; + watch( () => route.path, (newPath) => { - console.log("📍 Route changed in MixLayout:", newPath); - // 获取顶级路径 - const topMenuPath = + const topMenuPath = newPath.split("/").filter(Boolean).length > 1 ? newPath.match(/^\/[^/]+/)?.[0] || "/" : "/"; - // 如果当前路径属于当前激活的顶部菜单 - if (newPath.startsWith(activeTopMenuPath.value)) { - console.log("📍 Route is under active top menu, ensuring menu item is activated"); - } - // 如果路径改变了顶级菜单,确保顶部菜单和左侧菜单都更新 - else if (topMenuPath !== activeTopMenuPath.value) { - console.log( - "📍 Top menu changed, updating active menu from:", - activeTopMenuPath.value, - "to:", - topMenuPath - ); + // 仅在顶级路径实际变化时才执行更新 + if (topMenuPath !== prevTopMenuPath) { + prevTopMenuPath = topMenuPath; // 主动更新顶部菜单和左侧菜单 const appStore = useAppStore(); @@ -212,7 +203,6 @@ watch( flex-shrink: 0; align-items: center; height: 100%; - padding: 0 16px; } } diff --git a/frontend/src/store/modules/permission.store.ts b/frontend/src/store/modules/permission.store.ts index 6eecc267..d3cc91d5 100644 --- a/frontend/src/store/modules/permission.store.ts +++ b/frontend/src/store/modules/permission.store.ts @@ -136,15 +136,37 @@ export const usePermissionStore = defineStore("permission", () => { } } + // 用于存储路由路径到路由项的映射,提高查找效率 + const routePathMap = ref>({}); + + // 当routes更新时,同步更新routePathMap + watch(routes, (newRoutes) => { + const newMap: Record = {}; + const buildMap = (routes: RouteRecordRaw[]) => { + routes.forEach(route => { + if (route.path) newMap[route.path] = route; + if (route.children) buildMap(route.children); + }); + }; + buildMap(newRoutes); + routePathMap.value = newMap; + }, { immediate: true }); + /** * 根据父菜单路径设置侧边菜单 * * @param parentPath 父菜单的路径,用于查找对应的菜单项 */ const updateSideMenu = (parentPath: string) => { - const matchedItem = routes.value.find((item) => item.path === parentPath); + // 使用映射表进行O(1)时间复杂度的查找 + const matchedItem = routePathMap.value[parentPath]; if (matchedItem && matchedItem.children) { - sideMenuRoutes.value = matchedItem.children; + // 只有当子菜单发生变化时才更新,避免不必要的重渲染 + if (JSON.stringify(matchedItem.children) !== JSON.stringify(sideMenuRoutes.value)) { + sideMenuRoutes.value = matchedItem.children; + } + } else { + sideMenuRoutes.value = []; } }; @@ -166,6 +188,7 @@ export const usePermissionStore = defineStore("permission", () => { routes.value = [...constantRoutes]; sideMenuRoutes.value = []; routesLoaded.value = false; + routePathMap.value = {}; }; return { @@ -175,6 +198,7 @@ export const usePermissionStore = defineStore("permission", () => { generateRoutes, updateSideMenu, resetRouter, + routePathMap, }; }); diff --git a/frontend/src/utils/request.ts b/frontend/src/utils/request.ts index 7fd303af..239040e0 100644 --- a/frontend/src/utils/request.ts +++ b/frontend/src/utils/request.ts @@ -60,7 +60,30 @@ httpRequest.interceptors.response.use((response: AxiosResponse) => return response; }, async (error: AxiosError) => { + console.log(error); + const data = error.response?.data; + + // 处理blob类型的错误响应 + if (error.response?.config.responseType === 'blob' && error.response.data instanceof Blob) { + try { + // 将blob转换为JSON + const text = await new Response(error.response.data).text(); + const jsonData: ApiResponse = JSON.parse(text); + + if (jsonData.code === ResultEnum.ERROR) { + ElMessage.error(jsonData.msg || "请求错误"); + return Promise.reject(new Error(jsonData.msg || "请求错误")); + } else if (jsonData.code === ResultEnum.EXCEPTION) { + ElMessage.error(jsonData.msg || "服务异常"); + return Promise.reject(new Error(jsonData.msg || "服务异常")); + } + } catch (e) { + // 如果无法解析为JSON,则使用默认错误处理 + ElMessage.error(error.message || "请求异常"); + return Promise.reject(new Error(error.message || "请求异常")); + } + } if (data?.status_code === ResultEnum.ACCESS_TOKEN_INVALID) { await redirectToLogin("登录已过期,请重新登录"); diff --git a/frontend/src/views/monitor/job/index.vue b/frontend/src/views/monitor/job/index.vue index 9ac35d94..db6de1b7 100644 --- a/frontend/src/views/monitor/job/index.vue +++ b/frontend/src/views/monitor/job/index.vue @@ -800,6 +800,7 @@ async function handleExport() { type: "warning", }) .then(async () => { + let downloadUrl = ""; try { loading.value = true; @@ -811,9 +812,7 @@ async function handleExport() { const fileType = "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet;charset=utf-8"; const blob = new Blob([fileData], { type: fileType }); - - // 从响应头获取文件名 - const downloadUrl = window.URL.createObjectURL(blob); + downloadUrl = window.URL.createObjectURL(blob); const downloadLink = document.createElement("a"); downloadLink.href = downloadUrl; @@ -821,12 +820,15 @@ async function handleExport() { document.body.appendChild(downloadLink); downloadLink.click(); - ElMessage.success('导出成功'); + document.body.removeChild(downloadLink); - window.URL.revokeObjectURL(downloadUrl); } catch (error: any) { - console.error("导出错误:", error); + // 错误信息已经在响应拦截器中处理并显示 + console.error('导出失败:', error); } finally { + if (downloadUrl) { + window.URL.revokeObjectURL(downloadUrl); + } loading.value = false; } }) diff --git a/frontend/src/views/system/config/index.vue b/frontend/src/views/system/config/index.vue index bf6a3d3f..953996fd 100644 --- a/frontend/src/views/system/config/index.vue +++ b/frontend/src/views/system/config/index.vue @@ -406,20 +406,17 @@ async function handleExport() { cancelButtonText: '取消', type: 'warning' }).then(async () => { + let downloadUrl = ''; try { loading.value = true; - ElMessage.warning('正在导出数据,请稍候...'); - const response = await ConfigAPI.exportConfig(queryFormData); const fileData = response.data; const fileName = decodeURI(response.headers["content-disposition"].split(";")[1].split("=")[1]); const fileType = "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet;charset=utf-8"; const blob = new Blob([fileData], { type: fileType }); - - // 从响应头获取文件名 - const downloadUrl = window.URL.createObjectURL(blob); + downloadUrl = window.URL.createObjectURL(blob); const downloadLink = document.createElement("a"); downloadLink.href = downloadUrl; @@ -427,13 +424,15 @@ async function handleExport() { document.body.appendChild(downloadLink); downloadLink.click(); - ElMessage.success('导出成功'); + document.body.removeChild(downloadLink); - window.URL.revokeObjectURL(downloadUrl); } catch (error: any) { - ElMessage.error('文件处理失败', error.message); - console.error('导出错误:', error); + // 错误信息已经在响应拦截器中处理并显示 + console.error('导出失败:', error); } finally { + if (downloadUrl) { + window.URL.revokeObjectURL(downloadUrl); + } loading.value = false; } }).catch(() => { diff --git a/frontend/src/views/system/dict/components/DataDrawer.vue b/frontend/src/views/system/dict/components/DataDrawer.vue index 2d19df90..82fad394 100644 --- a/frontend/src/views/system/dict/components/DataDrawer.vue +++ b/frontend/src/views/system/dict/components/DataDrawer.vue @@ -452,10 +452,9 @@ async function handleExport() { cancelButtonText: '取消', type: 'warning' }).then(async () => { + let downloadUrl = ''; try { - loading.value = true; - - ElMessage.warning('正在导出数据,请稍候...'); + loading.value = true; const response = await DictAPI.exportDictData(queryFormData); const fileData = response.data; @@ -463,9 +462,7 @@ async function handleExport() { const fileType = "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet;charset=utf-8"; const blob = new Blob([fileData], { type: fileType }); - - // 从响应头获取文件名 - const downloadUrl = window.URL.createObjectURL(blob); + downloadUrl = window.URL.createObjectURL(blob); const downloadLink = document.createElement("a"); downloadLink.href = downloadUrl; @@ -473,13 +470,15 @@ async function handleExport() { document.body.appendChild(downloadLink); downloadLink.click(); - ElMessage.success('导出成功'); + document.body.removeChild(downloadLink); - window.URL.revokeObjectURL(downloadUrl); } catch (error: any) { - ElMessage.error('文件处理失败', error.message); - console.error('导出错误:', error); + // 错误信息已经在响应拦截器中处理并显示 + console.error('导出失败:', error); } finally { + if (downloadUrl) { + window.URL.revokeObjectURL(downloadUrl); + } loading.value = false; } }).catch(() => { diff --git a/frontend/src/views/system/dict/index.vue b/frontend/src/views/system/dict/index.vue index 609252b9..250e101b 100644 --- a/frontend/src/views/system/dict/index.vue +++ b/frontend/src/views/system/dict/index.vue @@ -427,20 +427,17 @@ async function handleExport() { cancelButtonText: '取消', type: 'warning' }).then(async () => { + let downloadUrl = ''; try { loading.value = true; - ElMessage.warning('正在导出数据,请稍候...'); - const response = await DictAPI.exportDictType(queryFormData); const fileData = response.data; const fileName = decodeURI(response.headers["content-disposition"].split(";")[1].split("=")[1]); const fileType = "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet;charset=utf-8"; const blob = new Blob([fileData], { type: fileType }); - - // 从响应头获取文件名 - const downloadUrl = window.URL.createObjectURL(blob); + downloadUrl = window.URL.createObjectURL(blob); const downloadLink = document.createElement("a"); downloadLink.href = downloadUrl; @@ -448,13 +445,15 @@ async function handleExport() { document.body.appendChild(downloadLink); downloadLink.click(); - ElMessage.success('导出成功'); + document.body.removeChild(downloadLink); - window.URL.revokeObjectURL(downloadUrl); } catch (error: any) { - ElMessage.error('文件处理失败', error.message); - console.error('导出错误:', error); + // 错误信息已经在响应拦截器中处理并显示 + console.error('导出失败:', error); } finally { + if (downloadUrl) { + window.URL.revokeObjectURL(downloadUrl); + } loading.value = false; } }).catch(() => { diff --git a/frontend/src/views/system/log/index.vue b/frontend/src/views/system/log/index.vue index b78fff20..69c5b26e 100644 --- a/frontend/src/views/system/log/index.vue +++ b/frontend/src/views/system/log/index.vue @@ -353,10 +353,9 @@ async function handleExport() { cancelButtonText: '取消', type: 'warning' }).then(async () => { + let downloadUrl = ''; try { - loading.value = true; - - ElMessage.warning('正在导出数据,请稍候...'); + loading.value = true; const response = await LogAPI.exportLog(queryFormData); const fileData = response.data; @@ -364,9 +363,7 @@ async function handleExport() { const fileType = "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet;charset=utf-8"; const blob = new Blob([fileData], { type: fileType }); - - // 从响应头获取文件名 - const downloadUrl = window.URL.createObjectURL(blob); + downloadUrl = window.URL.createObjectURL(blob); const downloadLink = document.createElement("a"); downloadLink.href = downloadUrl; @@ -374,13 +371,15 @@ async function handleExport() { document.body.appendChild(downloadLink); downloadLink.click(); - ElMessage.success('导出成功'); + document.body.removeChild(downloadLink); - window.URL.revokeObjectURL(downloadUrl); } catch (error: any) { - ElMessage.error('文件处理失败', error.message); - console.error('导出错误:', error); + // 错误信息已经在响应拦截器中处理并显示 + console.error('导出失败:', error); } finally { + if (downloadUrl) { + window.URL.revokeObjectURL(downloadUrl); + } loading.value = false; } }).catch(() => { diff --git a/frontend/src/views/system/notice/index.vue b/frontend/src/views/system/notice/index.vue index f355dcf7..cd6ce930 100644 --- a/frontend/src/views/system/notice/index.vue +++ b/frontend/src/views/system/notice/index.vue @@ -455,10 +455,9 @@ async function handleExport() { cancelButtonText: '取消', type: 'warning' }).then(async () => { + let downloadUrl = ''; try { - loading.value = true; - - ElMessage.warning('正在导出数据,请稍候...'); + loading.value = true; const response = await NoticeAPI.exportNotice(queryFormData); const fileData = response.data; @@ -466,9 +465,7 @@ async function handleExport() { const fileType = "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet;charset=utf-8"; const blob = new Blob([fileData], { type: fileType }); - - // 从响应头获取文件名 - const downloadUrl = window.URL.createObjectURL(blob); + downloadUrl = window.URL.createObjectURL(blob); const downloadLink = document.createElement("a"); downloadLink.href = downloadUrl; @@ -476,13 +473,15 @@ async function handleExport() { document.body.appendChild(downloadLink); downloadLink.click(); - ElMessage.success('导出成功'); + document.body.removeChild(downloadLink); - window.URL.revokeObjectURL(downloadUrl); } catch (error: any) { - ElMessage.error('文件处理失败', error.message); - console.error('导出错误:', error); + // 错误信息已经在响应拦截器中处理并显示 + console.error('导出失败:', error); } finally { + if (downloadUrl) { + window.URL.revokeObjectURL(downloadUrl); + } loading.value = false; } }).catch(() => { diff --git a/frontend/src/views/system/position/index.vue b/frontend/src/views/system/position/index.vue index 41fabf45..6c21f877 100644 --- a/frontend/src/views/system/position/index.vue +++ b/frontend/src/views/system/position/index.vue @@ -405,20 +405,17 @@ async function handleExport() { cancelButtonText: '取消', type: 'warning' }).then(async () => { + let downloadUrl = ''; try { loading.value = true; - ElMessage.warning('正在导出数据,请稍候...'); - const response = await PositionAPI.exportPosition(queryFormData); const fileData = response.data; const fileName = decodeURI(response.headers["content-disposition"].split(";")[1].split("=")[1]); const fileType = "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet;charset=utf-8"; const blob = new Blob([fileData], { type: fileType }); - - // 从响应头获取文件名 - const downloadUrl = window.URL.createObjectURL(blob); + downloadUrl = window.URL.createObjectURL(blob); const downloadLink = document.createElement("a"); downloadLink.href = downloadUrl; @@ -426,13 +423,15 @@ async function handleExport() { document.body.appendChild(downloadLink); downloadLink.click(); - ElMessage.success('导出成功'); + document.body.removeChild(downloadLink); - window.URL.revokeObjectURL(downloadUrl); } catch (error: any) { - ElMessage.error('文件处理失败', error.message); - console.error('导出错误:', error); + // 错误信息已经在响应拦截器中处理并显示 + console.error('导出失败:', error); } finally { + if (downloadUrl) { + window.URL.revokeObjectURL(downloadUrl); + } loading.value = false; } }).catch(() => { diff --git a/frontend/src/views/system/role/index.vue b/frontend/src/views/system/role/index.vue index b9c2e771..458d118c 100644 --- a/frontend/src/views/system/role/index.vue +++ b/frontend/src/views/system/role/index.vue @@ -432,10 +432,9 @@ async function handleExport() { cancelButtonText: '取消', type: 'warning' }).then(async () => { + let downloadUrl = ''; try { - loading.value = true; - - ElMessage.warning('正在导出数据,请稍候...'); + loading.value = true; const response = await RoleAPI.exportRole(queryFormData); const fileData = response.data; @@ -443,9 +442,7 @@ async function handleExport() { const fileType = "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet;charset=utf-8"; const blob = new Blob([fileData], { type: fileType }); - - // 从响应头获取文件名 - const downloadUrl = window.URL.createObjectURL(blob); + downloadUrl = window.URL.createObjectURL(blob); const downloadLink = document.createElement("a"); downloadLink.href = downloadUrl; @@ -453,13 +450,15 @@ async function handleExport() { document.body.appendChild(downloadLink); downloadLink.click(); - ElMessage.success('导出成功'); + document.body.removeChild(downloadLink); - window.URL.revokeObjectURL(downloadUrl); } catch (error: any) { - ElMessage.error('文件处理失败', error.message); - console.error('导出错误:', error); + // 错误信息已经在响应拦截器中处理并显示 + console.error('导出失败:', error); } finally { + if (downloadUrl) { + window.URL.revokeObjectURL(downloadUrl); + } loading.value = false; } }).catch(() => { diff --git a/frontend/src/views/system/user/index.vue b/frontend/src/views/system/user/index.vue index b1f74871..a609736b 100644 --- a/frontend/src/views/system/user/index.vue +++ b/frontend/src/views/system/user/index.vue @@ -588,19 +588,17 @@ async function handleExport() { cancelButtonText: '取消', type: 'warning' }).then(async () => { + let downloadUrl = ''; try { loading.value = true; - ElMessage.warning('正在导出数据,请稍候...'); - const response = await UserAPI.exportUser(queryFormData); const fileData = response.data; - const fileName = decodeURI(response.headers["content-disposition"].split(";")[1].split("=")[1]); + const fileName = decodeURI(response.headers["content-disposition"].split("; ")[1].split("=")[1]); const fileType = "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet;charset=utf-8"; const blob = new Blob([fileData], { type: fileType }); - - const downloadUrl = window.URL.createObjectURL(blob); + downloadUrl = window.URL.createObjectURL(blob); const downloadLink = document.createElement("a"); downloadLink.href = downloadUrl; @@ -608,16 +606,18 @@ async function handleExport() { document.body.appendChild(downloadLink); downloadLink.click(); - ElMessage.success('导出成功'); + document.body.removeChild(downloadLink); - window.URL.revokeObjectURL(downloadUrl); - } catch (error: any) { - ElMessage.error('文件处理失败', error.message); - console.error('导出错误:', error); + // 错误信息已经在响应拦截器中处理并显示 + console.error('导出失败:', error); } finally { + if (downloadUrl) { + window.URL.revokeObjectURL(downloadUrl); + } loading.value = false; } + }).catch(() => { ElMessageBox.close(); });