From df639d040ad73f2c74745c2f9a138532cf522d59 Mon Sep 17 00:00:00 2001
From: JenniferW <1627055433@qq.com>
Date: Mon, 24 Nov 2025 14:22:31 +0800
Subject: [PATCH] =?UTF-8?q?=E5=8A=A8=E6=80=81=E8=B7=AF=E7=94=B1=E8=B0=83?=
=?UTF-8?q?=E6=95=B4?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
src/api/system/menu.js | 91 ++----
src/components/RuoYi/Doc/index.vue | 4 +-
src/layout/index.vue | 247 +++++++--------
src/permission.js | 144 +++++----
src/router/index.js | 308 +++++++------------
src/store/modules/permission.js | 350 ++++++++++++++--------
src/utils/ruoyi.js | 462 +++++++++++++++--------------
src/views/system/menu/index.vue | 4 +-
8 files changed, 816 insertions(+), 794 deletions(-)
diff --git a/src/api/system/menu.js b/src/api/system/menu.js
index 96aff6d..97258ee 100644
--- a/src/api/system/menu.js
+++ b/src/api/system/menu.js
@@ -1,87 +1,60 @@
-// 模拟静态数据
-const mockMenus = [
- { menuId: 1, menuName: '系统管理', parentId: 0, orderNum: 1, path: 'system', component: 'Layout', status: '0' },
- { menuId: 2, menuName: '用户管理', parentId: 1, orderNum: 1, path: 'user', component: 'system/user/index', status: '0' },
- { menuId: 3, menuName: '角色管理', parentId: 1, orderNum: 2, path: 'role', component: 'system/role/index', status: '0' },
- { menuId: 4, menuName: '菜单管理', parentId: 1, orderNum: 3, path: 'menu', component: 'system/menu/index', status: '0' }
-];
+import request from '@/utils/request'
// 查询菜单列表
export function listMenu(query) {
- const { menuName, status } = query;
- let filteredMenus = [...mockMenus];
-
- if (menuName) {
- filteredMenus = filteredMenus.filter(menu => menu.menuName.includes(menuName));
- }
- if (status) {
- filteredMenus = filteredMenus.filter(menu => menu.status === status);
- }
-
- return Promise.resolve(filteredMenus);
+ return request({
+ url: '/system/menu/list',
+ method: 'get',
+ params: query
+ })
}
// 查询菜单详细
export function getMenu(menuId) {
- const menu = mockMenus.find(m => m.menuId === parseInt(menuId));
- return Promise.resolve(menu || {});
+ return request({
+ url: '/system/menu/' + menuId,
+ method: 'get'
+ })
}
// 查询菜单下拉树结构
export function treeselect() {
- const buildTree = (parentId) => {
- const children = mockMenus.filter(m => m.parentId === parentId);
- return children.map(child => ({
- id: child.menuId,
- label: child.menuName,
- children: buildTree(child.menuId)
- }));
- };
-
- return Promise.resolve(buildTree(0));
+ return request({
+ url: '/system/menu/treeselect',
+ method: 'get'
+ })
}
// 根据角色ID查询菜单下拉树结构
export function roleMenuTreeselect(roleId) {
- const buildTree = (parentId) => {
- const children = mockMenus.filter(m => m.parentId === parentId);
- return children.map(child => ({
- id: child.menuId,
- label: child.menuName,
- children: buildTree(child.menuId)
- }));
- };
-
- return Promise.resolve({
- menus: buildTree(0),
- checkedKeys: [1, 2, 3, 4] // 模拟已选中的菜单
- });
+ return request({
+ url: '/system/menu/roleMenuTreeselect/' + roleId,
+ method: 'get'
+ })
}
// 新增菜单
export function addMenu(data) {
- const newMenu = {
- menuId: mockMenus.length + 1,
- ...data
- };
- mockMenus.push(newMenu);
- return Promise.resolve({ code: 200, msg: '新增成功' });
+ return request({
+ url: '/system/menu',
+ method: 'post',
+ data: data
+ })
}
// 修改菜单
export function updateMenu(data) {
- const index = mockMenus.findIndex(m => m.menuId === data.menuId);
- if (index !== -1) {
- mockMenus[index] = { ...mockMenus[index], ...data };
- }
- return Promise.resolve({ code: 200, msg: '修改成功' });
+ return request({
+ url: '/system/menu',
+ method: 'put',
+ data: data
+ })
}
// 删除菜单
export function delMenu(menuId) {
- const index = mockMenus.findIndex(m => m.menuId === parseInt(menuId));
- if (index !== -1) {
- mockMenus.splice(index, 1);
- }
- return Promise.resolve({ code: 200, msg: '删除成功' });
+ return request({
+ url: '/system/menu/' + menuId,
+ method: 'delete'
+ })
}
\ No newline at end of file
diff --git a/src/components/RuoYi/Doc/index.vue b/src/components/RuoYi/Doc/index.vue
index ace6d47..90448d2 100644
--- a/src/components/RuoYi/Doc/index.vue
+++ b/src/components/RuoYi/Doc/index.vue
@@ -5,9 +5,9 @@
\ No newline at end of file
diff --git a/src/layout/index.vue b/src/layout/index.vue
index 5b15f20..be71c9d 100644
--- a/src/layout/index.vue
+++ b/src/layout/index.vue
@@ -1,121 +1,128 @@
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/src/permission.js b/src/permission.js
index 4796f93..b7d1498 100644
--- a/src/permission.js
+++ b/src/permission.js
@@ -1,61 +1,83 @@
-import router from './router'
-import { ElMessage } from 'element-plus'
-import NProgress from 'nprogress'
-import 'nprogress/nprogress.css'
-import { getToken } from '@/utils/auth'
-import { isHttp, isPathMatch } from '@/utils/validate'
-import { isRelogin } from '@/utils/request'
-import useUserStore from '@/store/modules/user'
-import useSettingsStore from '@/store/modules/settings'
-
-NProgress.configure({ showSpinner: false })
-
-const whiteList = ['/login', '/register']
-
-const isWhiteList = (path) => {
- return whiteList.some(pattern => isPathMatch(pattern, path))
-}
-
-router.beforeEach((to, from, next) => {
- NProgress.start()
- if (getToken()) {
- to.meta.title && useSettingsStore().setTitle(to.meta.title)
- /* has token*/
- if (to.path === '/login') {
- next({ path: '/' })
- NProgress.done()
- } else if (isWhiteList(to.path)) {
- next()
- } else {
- if (useUserStore().roles.length === 0) {
- isRelogin.show = true
- // 判断当前用户是否已拉取完user_info信息
- useUserStore().getInfo().then(() => {
- isRelogin.show = false
- // 不再动态加载路由,直接使用静态路由
- next({ ...to, replace: true })
- }).catch(err => {
- useUserStore().logOut().then(() => {
- ElMessage.error(err)
- next({ path: '/' })
- })
- })
- } else {
- next()
- }
- }
- } else {
- // 没有token
- if (isWhiteList(to.path)) {
- // 在免登录白名单,直接进入
- next()
- } else {
- next(`/login?redirect=${to.fullPath}`) // 否则全部重定向到登录页
- NProgress.done()
- }
- }
-})
-
-router.afterEach(() => {
- NProgress.done()
-})
+import router from './router'
+import { ElMessage } from 'element-plus'
+import NProgress from 'nprogress'
+import 'nprogress/nprogress.css'
+import { getToken } from '@/utils/auth'
+import { isHttp, isPathMatch } from '@/utils/validate'
+import { isRelogin } from '@/utils/request'
+import useUserStore from '@/store/modules/user'
+import useSettingsStore from '@/store/modules/settings'
+import usePermissionStore from '@/store/modules/permission'
+
+NProgress.configure({ showSpinner: false })
+
+const whiteList = ['/login', '/register']
+
+const isWhiteList = (path) => {
+ return whiteList.some(pattern => isPathMatch(pattern, path))
+}
+
+router.beforeEach((to, from, next) => {
+ NProgress.start()
+ if (getToken()) {
+ to.meta.title && useSettingsStore().setTitle(to.meta.title)
+ /* has token*/
+ if (to.path === '/login') {
+ next({ path: '/' })
+ NProgress.done()
+ } else if (isWhiteList(to.path)) {
+ next()
+ } else {
+ if (useUserStore().roles.length === 0) {
+ isRelogin.show = true
+ // 判断当前用户是否已拉取完user_info信息
+ useUserStore().getInfo().then(() => {
+ isRelogin.show = false
+ // 动态加载路由
+ const permissionStore = usePermissionStore()
+ permissionStore.generateRoutes(useUserStore().roles).then(() => {
+ // 动态路由加载完成后,重新跳转到目标路由
+ next({ ...to, replace: true })
+ }).catch((error) => {
+ console.error('路由加载失败:', error)
+ ElMessage.error(error?.message || '路由加载失败,请刷新页面重试')
+ next({ path: '/' })
+ })
+ }).catch(err => {
+ useUserStore().logOut().then(() => {
+ ElMessage.error(err)
+ next({ path: '/' })
+ })
+ })
+ } else {
+ // 已获取用户信息,检查路由是否已加载
+ const permissionStore = usePermissionStore()
+ if (permissionStore.addRoutes.length === 0) {
+ // 路由未加载,重新加载
+ permissionStore.generateRoutes(useUserStore().roles).then(() => {
+ next({ ...to, replace: true })
+ }).catch((error) => {
+ console.error('路由加载失败:', error)
+ ElMessage.error(error?.message || '路由加载失败,请刷新页面重试')
+ next({ path: '/' })
+ })
+ } else {
+ next()
+ }
+ }
+ }
+ } else {
+ // 没有token
+ if (isWhiteList(to.path)) {
+ // 在免登录白名单,直接进入
+ next()
+ } else {
+ next(`/login?redirect=${to.fullPath}`) // 否则全部重定向到登录页
+ NProgress.done()
+ }
+ }
+})
+
+router.afterEach(() => {
+ NProgress.done()
+})
diff --git a/src/router/index.js b/src/router/index.js
index 8e1551e..71e8312 100644
--- a/src/router/index.js
+++ b/src/router/index.js
@@ -1,203 +1,105 @@
-import { createWebHistory, createRouter } from 'vue-router'
-/* Layout */
-import Layout from '@/layout/index.vue'
-
-/**
- * Note: 路由配置项
- *
- * hidden: true // 当设置 true 的时候该路由不会再侧边栏出现 如401,login等页面,或者如一些编辑页面/edit/1
- * alwaysShow: true // 当你一个路由下面的 children 声明的路由大于1个时,自动会变成嵌套的模式--如组件页面
- * // 只有一个时,会将那个子路由当做根路由显示在侧边栏--如引导页面
- * // 若你想不管路由下面的 children 声明的个数都显示你的根路由
- * // 你可以设置 alwaysShow: true,这样它就会忽略之前定义的规则,一直显示根路由
- * redirect: noRedirect // 当设置 noRedirect 的时候该路由在面包屑导航中不可被点击
- * name:'router-name' // 设定路由的名字,一定要填写不然使用时会出现各种问题
- * query: '{"id": 1, "name": "ry"}' // 访问路由的默认传递参数
- * roles: ['admin', 'common'] // 访问路由的角色权限
- * permissions: ['a:a:a', 'b:b:b'] // 访问路由的菜单权限
- * meta : {
- noCache: true // 如果设置为true,则不会被 缓存(默认 false)
- title: 'title' // 设置该路由在侧边栏和面包屑中展示的名字
- icon: 'svg-name' // 设置该路由的图标,对应路径src/assets/icons/svg
- breadcrumb: false // 如果设置为false,则不会在breadcrumb面包屑中显示
- activeMenu: '/system/user' // 当路由设置了该属性,则会高亮相对应的侧边栏。
- }
- */
-
-// 公共路由
-export const constantRoutes = [
- {
- path: '/redirect',
- component: Layout,
- hidden: true,
- children: [
- {
- path: '/redirect/:path(.*)',
- component: () => import('@/views/redirect/index.vue')
- }
- ]
- },
- {
- path: '/login',
- component: () => import('@/views/login.vue'),
- hidden: true
- },
- {
- path: '/register',
- component: () => import('@/views/register.vue'),
- hidden: true
- },
- {
- path: "/:pathMatch(.*)*",
- component: () => import('@/views/error/404.vue'),
- hidden: true
- },
- {
- path: '/401',
- component: () => import('@/views/error/401.vue'),
- hidden: true
- },
- {
- path: '',
- component: Layout,
- redirect: '/order/intention',
- children: [
- {
- path: '/index',
- component: () => import('@/views/index.vue'),
- name: 'Index',
- meta: { title: '首页', icon: 'dashboard', affix: true }
- }
- ]
- },
- {
- path: '/user',
- component: Layout,
- hidden: true,
- redirect: 'noRedirect',
- name: 'UserCenter',
- meta: { title: '个人中心', icon: 'user' },
- children: [
- {
- path: 'profile',
- component: () => import('@/views/user/profile/index.vue'),
- name: 'Profile',
- meta: { title: '个人中心', icon: 'user' }
- }
- ]
- },
- {
- path: '/order',
- component: Layout,
- hidden: false,
- name: 'Order',
- meta: { title: '订单管理', icon: 'shopping' },
- children: [
- {
- path: 'intention',
- component: () => import('@/views/order/intention/index.vue'),
- name: 'Intention',
- meta: { title: '订单/意向单', icon: 'form' }
- },
- {
- path: 'create',
- component: () => import('@/views/order/intention/create.vue'),
- name: 'IntentionCreate',
- meta: { title: '创建订单/意向单', icon: 'edit', hidden: true }
- },
- {
- path: 'search',
- component: () => import('@/views/order/intention/search.vue'),
- name: 'IntentionSearch',
- meta: { title: '品号查询', icon: 'search', hidden: true }
- }
- ]
- },
- // 以下为原动态路由,现在直接合并到静态路由中
- {
- path: '/system/user-auth',
- component: Layout,
- hidden: true,
- permissions: ['system:user:edit'],
- children: [
- {
- path: 'role/:userId(\\d+)',
- component: () => import('@/views/system/user/authRole.vue'),
- name: 'AuthRole',
- meta: { title: '分配角色', activeMenu: '/system/user' }
- }
- ]
- },
- {
- path: '/system/role-auth',
- component: Layout,
- hidden: true,
- permissions: ['system:role:edit'],
- children: [
- {
- path: 'user/:roleId(\\d+)',
- component: () => import('@/views/system/role/authUser.vue'),
- name: 'AuthUser',
- meta: { title: '分配用户', activeMenu: '/system/role' }
- }
- ]
- },
- {
- path: '/system/dict-data',
- component: Layout,
- hidden: true,
- permissions: ['system:dict:list'],
- children: [
- {
- path: 'index/:dictId(\\d+)',
- component: () => import('@/views/system/dict/data.vue'),
- name: 'Data',
- meta: { title: '字典数据', activeMenu: '/system/dict' }
- }
- ]
- },
- {
- path: '/monitor/job-log',
- component: Layout,
- hidden: true,
- permissions: ['monitor:job:list'],
- children: [
- {
- path: 'index/:jobId(\\d+)',
- component: () => import('@/views/monitor/job/log.vue'),
- name: 'JobLog',
- meta: { title: '调度日志', activeMenu: '/monitor/job' }
- }
- ]
- },
- {
- path: '/tool/gen-edit',
- component: Layout,
- hidden: true,
- permissions: ['tool:gen:edit'],
- children: [
- {
- path: 'index/:tableId(\\d+)',
- component: () => import('@/views/tool/gen/editTable.vue'),
- name: 'GenEdit',
- meta: { title: '修改生成配置', activeMenu: '/tool/gen' }
- }
- ]
- }
-];
-
-// 动态路由已合并到静态路由中,不再需要
-// export const dynamicRoutes = [ ... ];
-
-const router = createRouter({
- history: createWebHistory(),
- routes: constantRoutes,
- scrollBehavior(to, from, savedPosition) {
- if (savedPosition) {
- return savedPosition
- }
- return { top: 0 }
- },
-});
-
-export default router;
+import { createWebHistory, createRouter } from 'vue-router'
+/* Layout */
+import Layout from '@/layout/index.vue'
+
+/**
+ * Note: 路由配置项
+ *
+ * hidden: true // 当设置 true 的时候该路由不会再侧边栏出现 如401,login等页面,或者如一些编辑页面/edit/1
+ * alwaysShow: true // 当你一个路由下面的 children 声明的路由大于1个时,自动会变成嵌套的模式--如组件页面
+ * // 只有一个时,会将那个子路由当做根路由显示在侧边栏--如引导页面
+ * // 若你想不管路由下面的 children 声明的个数都显示你的根路由
+ * // 你可以设置 alwaysShow: true,这样它就会忽略之前定义的规则,一直显示根路由
+ * redirect: noRedirect // 当设置 noRedirect 的时候该路由在面包屑导航中不可被点击
+ * name:'router-name' // 设定路由的名字,一定要填写不然使用时会出现各种问题
+ * query: '{"id": 1, "name": "ry"}' // 访问路由的默认传递参数
+ * roles: ['admin', 'common'] // 访问路由的角色权限
+ * permissions: ['a:a:a', 'b:b:b'] // 访问路由的菜单权限
+ * meta : {
+ noCache: true // 如果设置为true,则不会被 缓存(默认 false)
+ title: 'title' // 设置该路由在侧边栏和面包屑中展示的名字
+ icon: 'svg-name' // 设置该路由的图标,对应路径src/assets/icons/svg
+ breadcrumb: false // 如果设置为false,则不会在breadcrumb面包屑中显示
+ activeMenu: '/system/user' // 当路由设置了该属性,则会高亮相对应的侧边栏。
+ }
+ */
+
+// 公共路由
+export const constantRoutes = [
+ {
+ path: '/redirect',
+ component: Layout,
+ hidden: true,
+ children: [
+ {
+ path: '/redirect/:path(.*)',
+ component: () => import('@/views/redirect/index.vue')
+ }
+ ]
+ },
+ {
+ path: '/login',
+ component: () => import('@/views/login.vue'),
+ hidden: true
+ },
+ {
+ path: '/register',
+ component: () => import('@/views/register.vue'),
+ hidden: true
+ },
+ {
+ path: "/:pathMatch(.*)*",
+ component: () => import('@/views/error/404.vue'),
+ hidden: true
+ },
+ {
+ path: '/401',
+ component: () => import('@/views/error/401.vue'),
+ hidden: true
+ },
+ {
+ path: '',
+ component: Layout,
+ redirect: 'index',
+ children: [
+ {
+ path: '/index',
+ component: () => import('@/views/index.vue'),
+ name: 'Index',
+ meta: { title: '首页', icon: 'dashboard', affix: true }
+ }
+ ]
+ },
+ {
+ path: '/user',
+ component: Layout,
+ hidden: true,
+ redirect: 'noRedirect',
+ name: 'UserCenter',
+ meta: { title: '个人中心', icon: 'user' },
+ children: [
+ {
+ path: 'profile',
+ component: () => import('@/views/user/profile/index.vue'),
+ name: 'Profile',
+ meta: { title: '个人中心', icon: 'user' }
+ }
+ ]
+ }
+ // 业务路由(订单管理、系统管理等)将从后端动态获取
+];
+
+// 动态路由将从后端接口获取,不再在此处定义
+
+const router = createRouter({
+ history: createWebHistory(),
+ routes: constantRoutes,
+ scrollBehavior(to, from, savedPosition) {
+ if (savedPosition) {
+ return savedPosition
+ }
+ return { top: 0 }
+ },
+});
+
+export default router;
diff --git a/src/store/modules/permission.js b/src/store/modules/permission.js
index 77b1805..790ec7c 100644
--- a/src/store/modules/permission.js
+++ b/src/store/modules/permission.js
@@ -1,119 +1,231 @@
-import auth from '@/plugins/auth'
-import router, { constantRoutes } from '@/router'
-// 不再需要动态路由,移除 dynamicRoutes 和 getRouters
-// import { getRouters } from '@/api/menu'
-import Layout from '@/layout/index'
-import ParentView from '@/components/ParentView'
-import InnerLink from '@/layout/components/InnerLink'
-
-// 匹配views里面所有的.vue文件
-const modules = import.meta.glob('./../../views/**/*.vue')
-
-const usePermissionStore = defineStore(
- 'permission',
- {
- state: () => ({
- routes: [],
- addRoutes: [],
- defaultRoutes: [],
- topbarRouters: [],
- sidebarRouters: []
- }),
- actions: {
- setRoutes(routes) {
- this.addRoutes = routes
- this.routes = constantRoutes.concat(routes)
- },
- setDefaultRoutes(routes) {
- this.defaultRoutes = constantRoutes.concat(routes)
- },
- setTopbarRoutes(routes) {
- this.topbarRouters = routes
- },
- setSidebarRouters(routes) {
- this.sidebarRouters = routes
- },
- generateRoutes(roles) {
- return new Promise(resolve => {
- // 不再调用后端接口,直接使用静态路由
- const staticRoutes = constantRoutes
- this.setRoutes(staticRoutes)
- this.setSidebarRouters(staticRoutes)
- this.setDefaultRoutes(staticRoutes)
- this.setTopbarRoutes(staticRoutes)
- resolve(staticRoutes)
- })
- }
- }
- })
-
-// 遍历后台传来的路由字符串,转换为组件对象
-function filterAsyncRouter(asyncRouterMap, lastRouter = false, type = false) {
- return asyncRouterMap.filter(route => {
- if (type && route.children) {
- route.children = filterChildren(route.children)
- }
- if (route.component) {
- // Layout ParentView 组件特殊处理
- if (route.component === 'Layout') {
- route.component = Layout
- } else if (route.component === 'ParentView') {
- route.component = ParentView
- } else if (route.component === 'InnerLink') {
- route.component = InnerLink
- } else {
- route.component = loadView(route.component)
- }
- }
- if (route.children != null && route.children && route.children.length) {
- route.children = filterAsyncRouter(route.children, route, type)
- } else {
- delete route['children']
- delete route['redirect']
- }
- return true
- })
-}
-
-function filterChildren(childrenMap, lastRouter = false) {
- var children = []
- childrenMap.forEach(el => {
- el.path = lastRouter ? lastRouter.path + '/' + el.path : el.path
- if (el.children && el.children.length && el.component === 'ParentView') {
- children = children.concat(filterChildren(el.children, el))
- } else {
- children.push(el)
- }
- })
- return children
-}
-
-// 动态路由遍历,验证是否具备权限
-export function filterDynamicRoutes(routes) {
- const res = []
- routes.forEach(route => {
- if (route.permissions) {
- if (auth.hasPermiOr(route.permissions)) {
- res.push(route)
- }
- } else if (route.roles) {
- if (auth.hasRoleOr(route.roles)) {
- res.push(route)
- }
- }
- })
- return res
-}
-
-export const loadView = (view) => {
- let res;
- for (const path in modules) {
- const dir = path.split('views/')[1].split('.vue')[0];
- if (dir === view) {
- res = () => modules[path]();
- }
- }
- return res
-}
-
-export default usePermissionStore
+import auth from '@/plugins/auth'
+import router, { constantRoutes } from '@/router'
+import { getRouters } from '@/api/menu'
+import Layout from '@/layout/index'
+import ParentView from '@/components/ParentView'
+import InnerLink from '@/layout/components/InnerLink'
+import { isHttp } from '@/utils/validate'
+
+// 匹配views里面所有的.vue文件
+const modules = import.meta.glob('./../../views/**/*.vue')
+
+const usePermissionStore = defineStore(
+ 'permission',
+ {
+ state: () => ({
+ routes: [],
+ addRoutes: [],
+ defaultRoutes: [],
+ topbarRouters: [],
+ sidebarRouters: []
+ }),
+ actions: {
+ setRoutes(routes) {
+ this.addRoutes = routes
+ this.routes = constantRoutes.concat(routes)
+ },
+ setDefaultRoutes(routes) {
+ this.defaultRoutes = constantRoutes.concat(routes)
+ },
+ setTopbarRoutes(routes) {
+ this.topbarRouters = routes
+ },
+ setSidebarRouters(routes) {
+ this.sidebarRouters = routes
+ },
+ generateRoutes(roles) {
+ return new Promise((resolve, reject) => {
+ // 调用后端接口获取动态路由
+ getRouters().then(res => {
+ // 确保 res.data 是数组
+ if (!res.data || !Array.isArray(res.data)) {
+ console.error('路由数据格式错误:', res.data)
+ reject(new Error('路由数据格式错误,期望数组'))
+ return
+ }
+
+ try {
+ const sdata = JSON.parse(JSON.stringify(res.data))
+ const rdata = JSON.parse(JSON.stringify(res.data))
+ const defaultData = JSON.parse(JSON.stringify(res.data))
+ const sidebarRoutes = filterAsyncRouter(sdata)
+ const rewriteRoutes = filterAsyncRouter(rdata, false, true)
+ const defaultRoutes = filterAsyncRouter(defaultData)
+
+ // 过滤权限路由
+ const accessedRoutes = filterDynamicRoutes(rewriteRoutes)
+
+ // 调试:打印路由信息
+ console.log('动态路由加载成功,路由数量:', accessedRoutes.length)
+ if (accessedRoutes.length > 0) {
+ console.log('第一个路由示例:', accessedRoutes[0])
+ }
+
+ this.setRoutes(accessedRoutes)
+ this.setSidebarRouters(constantRoutes.concat(sidebarRoutes))
+ this.setDefaultRoutes(constantRoutes.concat(defaultRoutes))
+ this.setTopbarRoutes(constantRoutes.concat(sidebarRoutes))
+
+ // 动态添加路由到router
+ accessedRoutes.forEach(route => {
+ router.addRoute(route)
+ })
+
+ resolve(accessedRoutes)
+ } catch (error) {
+ console.error('路由处理错误:', error)
+ reject(error)
+ }
+ }).catch(error => {
+ console.error('获取路由失败:', error)
+ reject(error)
+ })
+ })
+ }
+ }
+ })
+
+// 遍历后台传来的路由字符串,转换为组件对象
+function filterAsyncRouter(asyncRouterMap, lastRouter = false, type = false) {
+ // 确保 asyncRouterMap 是数组
+ if (!Array.isArray(asyncRouterMap)) {
+ console.error('filterAsyncRouter: 期望数组,但收到:', asyncRouterMap)
+ return []
+ }
+
+ return asyncRouterMap.filter(route => {
+ // 处理外部链接路径:如果 path 是外部链接,需要转换为有效的内部路径
+ if (route.path && isHttp(route.path)) {
+ // 确保 meta.link 保存实际的外部链接
+ if (!route.meta) {
+ route.meta = {}
+ }
+ // 保存原始的外部链接到 meta.link
+ const originalPath = route.path
+ route.meta.link = originalPath
+
+ // 将外部链接路径转换为内部路径
+ // 使用简单的路径格式:/iframe/域名,避免路径冲突
+ const domain = originalPath.replace(/^https?:\/\//, '').replace(/\/.*$/, '')
+ const timestamp = Date.now()
+ const encodedPath = '/iframe/' + encodeURIComponent(domain) + '-' + timestamp
+
+ // 将 path 转换为有效的内部路径
+ route.path = encodedPath
+ } else if (!lastRouter && route.path && !route.path.startsWith('/')) {
+ // 只对顶级路由(lastRouter 为 false)确保路径以 "/" 开头
+ // 子路由的路径会在 filterChildren 中处理
+ route.path = '/' + route.path
+ }
+
+ if (route.component) {
+ // 检查是否有外部链接
+ if (route.meta && route.meta.link && (route.meta.link.startsWith('http://') || route.meta.link.startsWith('https://'))) {
+ route.component = InnerLink
+ }
+ // Layout ParentView 组件特殊处理
+ else if (route.component === 'Layout') {
+ route.component = Layout
+ } else if (route.component === 'ParentView') {
+ route.component = ParentView
+ } else if (route.component === 'InnerLink') {
+ route.component = InnerLink
+ } else {
+ const component = loadView(route.component)
+ if (!component) {
+ console.warn('找不到组件:', route.component)
+ }
+ route.component = component || (() => import('@/views/error/404.vue'))
+ }
+ }
+ if (route.children != null && route.children && route.children.length) {
+ // 如果 type 为 true,先使用 filterChildren 处理子路由路径
+ if (type) {
+ route.children = filterChildren(route.children, route)
+ }
+ // 然后递归处理子路由
+ route.children = filterAsyncRouter(route.children, route, type)
+ } else {
+ delete route['children']
+ delete route['redirect']
+ }
+ return true
+ })
+}
+
+function filterChildren(childrenMap, lastRouter = false) {
+ var children = []
+ childrenMap.forEach(el => {
+ if (lastRouter) {
+ // 处理子路由路径:如果子路由路径以 / 开头,需要去掉,因为要拼接父路由路径
+ let childPath = el.path || ''
+ if (childPath.startsWith('/')) {
+ childPath = childPath.substring(1)
+ }
+ // 拼接父路由路径和子路由路径
+ const parentPath = lastRouter.path || ''
+ // 确保父路径以 / 结尾(如果父路径不是根路径)
+ const separator = parentPath.endsWith('/') ? '' : '/'
+ el.path = parentPath + separator + childPath
+ }
+ // 确保最终路径以 / 开头(如果不是空路径)
+ if (el.path && !el.path.startsWith('/')) {
+ el.path = '/' + el.path
+ }
+ if (el.children && el.children.length && el.component === 'ParentView') {
+ children = children.concat(filterChildren(el.children, el))
+ } else {
+ children.push(el)
+ }
+ })
+ return children
+}
+
+// 动态路由遍历,验证是否具备权限
+export function filterDynamicRoutes(routes) {
+ const res = []
+ routes.forEach(route => {
+ if (route.permissions) {
+ if (auth.hasPermiOr(route.permissions)) {
+ res.push(route)
+ }
+ } else if (route.roles) {
+ if (auth.hasRoleOr(route.roles)) {
+ res.push(route)
+ }
+ } else {
+ // 没有权限控制的路由,直接通过
+ res.push(route)
+ }
+ })
+ return res
+}
+
+export const loadView = (view) => {
+ if (!view) {
+ return null
+ }
+
+ // 处理外部链接
+ if (view.startsWith('http://') || view.startsWith('https://')) {
+ return null
+ }
+
+ let res = null;
+ for (const path in modules) {
+ const dir = path.split('views/')[1]?.split('.vue')[0];
+ if (dir && dir === view) {
+ res = () => modules[path]();
+ break;
+ }
+ }
+
+ if (!res) {
+ console.warn(`loadView: 找不到组件 "${view}",请检查路径是否正确`)
+ }
+
+ return res
+}
+
+export default usePermissionStore
+
diff --git a/src/utils/ruoyi.js b/src/utils/ruoyi.js
index 7aa9ab6..34c0487 100644
--- a/src/utils/ruoyi.js
+++ b/src/utils/ruoyi.js
@@ -1,228 +1,234 @@
-/**
- * 通用js方法封装处理
- * Copyright (c) 2019 ruoyi
- */
-
-// 日期格式化
-export function parseTime(time, pattern) {
- if (arguments.length === 0 || !time) {
- return null
- }
- const format = pattern || '{y}-{m}-{d} {h}:{i}:{s}'
- let date
- if (typeof time === 'object') {
- date = time
- } else {
- if ((typeof time === 'string') && (/^[0-9]+$/.test(time))) {
- time = parseInt(time)
- } else if (typeof time === 'string') {
- time = time.replace(new RegExp(/-/gm), '/').replace('T', ' ').replace(new RegExp(/\.[\d]{3}/gm), '');
- }
- if ((typeof time === 'number') && (time.toString().length === 10)) {
- time = time * 1000
- }
- date = new Date(time)
- }
- const formatObj = {
- y: date.getFullYear(),
- m: date.getMonth() + 1,
- d: date.getDate(),
- h: date.getHours(),
- i: date.getMinutes(),
- s: date.getSeconds(),
- a: date.getDay()
- }
- const time_str = format.replace(/{(y|m|d|h|i|s|a)+}/g, (result, key) => {
- let value = formatObj[key]
- // Note: getDay() returns 0 on Sunday
- if (key === 'a') { return ['日', '一', '二', '三', '四', '五', '六'][value] }
- if (result.length > 0 && value < 10) {
- value = '0' + value
- }
- return value || 0
- })
- return time_str
-}
-
-// 表单重置
-export function resetForm(refName) {
- if (this.$refs[refName]) {
- this.$refs[refName].resetFields();
- }
-}
-
-// 添加日期范围
-export function addDateRange(params, dateRange, propName) {
- let search = params;
- search.params = typeof (search.params) === 'object' && search.params !== null && !Array.isArray(search.params) ? search.params : {};
- dateRange = Array.isArray(dateRange) ? dateRange : [];
- if (typeof (propName) === 'undefined') {
- search.params['beginTime'] = dateRange[0];
- search.params['endTime'] = dateRange[1];
- } else {
- search.params['begin' + propName] = dateRange[0];
- search.params['end' + propName] = dateRange[1];
- }
- return search;
-}
-
-// 回显数据字典
-export function selectDictLabel(datas, value) {
- if (value === undefined) {
- return "";
- }
- var actions = [];
- Object.keys(datas).some((key) => {
- if (datas[key].value == ('' + value)) {
- actions.push(datas[key].label);
- return true;
- }
- })
- if (actions.length === 0) {
- actions.push(value);
- }
- return actions.join('');
-}
-
-// 回显数据字典(字符串、数组)
-export function selectDictLabels(datas, value, separator) {
- if (value === undefined || value.length ===0) {
- return "";
- }
- if (Array.isArray(value)) {
- value = value.join(",");
- }
- var actions = [];
- var currentSeparator = undefined === separator ? "," : separator;
- var temp = value.split(currentSeparator);
- Object.keys(value.split(currentSeparator)).some((val) => {
- var match = false;
- Object.keys(datas).some((key) => {
- if (datas[key].value == ('' + temp[val])) {
- actions.push(datas[key].label + currentSeparator);
- match = true;
- }
- })
- if (!match) {
- actions.push(temp[val] + currentSeparator);
- }
- })
- return actions.join('').substring(0, actions.join('').length - 1);
-}
-
-// 字符串格式化(%s )
-export function sprintf(str) {
- var args = arguments, flag = true, i = 1;
- str = str.replace(/%s/g, function () {
- var arg = args[i++];
- if (typeof arg === 'undefined') {
- flag = false;
- return '';
- }
- return arg;
- });
- return flag ? str : '';
-}
-
-// 转换字符串,undefined,null等转化为""
-export function parseStrEmpty(str) {
- if (!str || str == "undefined" || str == "null") {
- return "";
- }
- return str;
-}
-
-// 数据合并
-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;
-};
-
-/**
- * 构造树型结构数据
- * @param {*} data 数据源
- * @param {*} id id字段 默认 'id'
- * @param {*} parentId 父节点字段 默认 'parentId'
- * @param {*} children 孩子节点字段 默认 'children'
- */
-export function handleTree(data, id, parentId, children) {
- let config = {
- id: id || 'id',
- parentId: parentId || 'parentId',
- childrenList: children || 'children'
- };
-
- var childrenListMap = {};
- var tree = [];
- for (let d of data) {
- let id = d[config.id];
- childrenListMap[id] = d;
- if (!d[config.childrenList]) {
- d[config.childrenList] = [];
- }
- }
-
- for (let d of data) {
- let parentId = d[config.parentId]
- let parentObj = childrenListMap[parentId]
- if (!parentObj) {
- tree.push(d);
- } else {
- parentObj[config.childrenList].push(d)
- }
- }
- return tree;
-}
-
-/**
-* 参数处理
-* @param {*} params 参数
-*/
-export function tansParams(params) {
- let result = ''
- for (const propName of Object.keys(params)) {
- const value = params[propName];
- var part = encodeURIComponent(propName) + "=";
- if (value !== null && value !== "" && typeof (value) !== "undefined") {
- if (typeof value === 'object') {
- for (const key of Object.keys(value)) {
- if (value[key] !== null && value[key] !== "" && typeof (value[key]) !== 'undefined') {
- let params = propName + '[' + key + ']';
- var subPart = encodeURIComponent(params) + "=";
- result += subPart + encodeURIComponent(value[key]) + "&";
- }
- }
- } else {
- result += part + encodeURIComponent(value) + "&";
- }
- }
- }
- return result
-}
-
-// 返回项目路径
-export function getNormalPath(p) {
- if (p.length === 0 || !p || p == 'undefined') {
- return p
- };
- let res = p.replace('//', '/')
- if (res[res.length - 1] === '/') {
- return res.slice(0, res.length - 1)
- }
- return res
-}
-
-// 验证是否为blob格式
-export function blobValidate(data) {
- return data.type !== 'application/json'
-}
+/**
+ * 通用js方法封装处理
+ * Copyright (c) 2019 ruoyi
+ */
+
+// 日期格式化
+export function parseTime(time, pattern) {
+ if (arguments.length === 0 || !time) {
+ return null
+ }
+ const format = pattern || '{y}-{m}-{d} {h}:{i}:{s}'
+ let date
+ if (typeof time === 'object') {
+ date = time
+ } else {
+ if ((typeof time === 'string') && (/^[0-9]+$/.test(time))) {
+ time = parseInt(time)
+ } else if (typeof time === 'string') {
+ time = time.replace(new RegExp(/-/gm), '/').replace('T', ' ').replace(new RegExp(/\.[\d]{3}/gm), '');
+ }
+ if ((typeof time === 'number') && (time.toString().length === 10)) {
+ time = time * 1000
+ }
+ date = new Date(time)
+ }
+ const formatObj = {
+ y: date.getFullYear(),
+ m: date.getMonth() + 1,
+ d: date.getDate(),
+ h: date.getHours(),
+ i: date.getMinutes(),
+ s: date.getSeconds(),
+ a: date.getDay()
+ }
+ const time_str = format.replace(/{(y|m|d|h|i|s|a)+}/g, (result, key) => {
+ let value = formatObj[key]
+ // Note: getDay() returns 0 on Sunday
+ if (key === 'a') { return ['日', '一', '二', '三', '四', '五', '六'][value] }
+ if (result.length > 0 && value < 10) {
+ value = '0' + value
+ }
+ return value || 0
+ })
+ return time_str
+}
+
+// 表单重置
+export function resetForm(refName) {
+ if (this.$refs[refName]) {
+ this.$refs[refName].resetFields();
+ }
+}
+
+// 添加日期范围
+export function addDateRange(params, dateRange, propName) {
+ let search = params;
+ search.params = typeof (search.params) === 'object' && search.params !== null && !Array.isArray(search.params) ? search.params : {};
+ dateRange = Array.isArray(dateRange) ? dateRange : [];
+ if (typeof (propName) === 'undefined') {
+ search.params['beginTime'] = dateRange[0];
+ search.params['endTime'] = dateRange[1];
+ } else {
+ search.params['begin' + propName] = dateRange[0];
+ search.params['end' + propName] = dateRange[1];
+ }
+ return search;
+}
+
+// 回显数据字典
+export function selectDictLabel(datas, value) {
+ if (value === undefined) {
+ return "";
+ }
+ var actions = [];
+ Object.keys(datas).some((key) => {
+ if (datas[key].value == ('' + value)) {
+ actions.push(datas[key].label);
+ return true;
+ }
+ })
+ if (actions.length === 0) {
+ actions.push(value);
+ }
+ return actions.join('');
+}
+
+// 回显数据字典(字符串、数组)
+export function selectDictLabels(datas, value, separator) {
+ if (value === undefined || value.length ===0) {
+ return "";
+ }
+ if (Array.isArray(value)) {
+ value = value.join(",");
+ }
+ var actions = [];
+ var currentSeparator = undefined === separator ? "," : separator;
+ var temp = value.split(currentSeparator);
+ Object.keys(value.split(currentSeparator)).some((val) => {
+ var match = false;
+ Object.keys(datas).some((key) => {
+ if (datas[key].value == ('' + temp[val])) {
+ actions.push(datas[key].label + currentSeparator);
+ match = true;
+ }
+ })
+ if (!match) {
+ actions.push(temp[val] + currentSeparator);
+ }
+ })
+ return actions.join('').substring(0, actions.join('').length - 1);
+}
+
+// 字符串格式化(%s )
+export function sprintf(str) {
+ var args = arguments, flag = true, i = 1;
+ str = str.replace(/%s/g, function () {
+ var arg = args[i++];
+ if (typeof arg === 'undefined') {
+ flag = false;
+ return '';
+ }
+ return arg;
+ });
+ return flag ? str : '';
+}
+
+// 转换字符串,undefined,null等转化为""
+export function parseStrEmpty(str) {
+ if (!str || str == "undefined" || str == "null") {
+ return "";
+ }
+ return str;
+}
+
+// 数据合并
+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;
+};
+
+/**
+ * 构造树型结构数据
+ * @param {*} data 数据源
+ * @param {*} id id字段 默认 'id'
+ * @param {*} parentId 父节点字段 默认 'parentId'
+ * @param {*} children 孩子节点字段 默认 'children'
+ */
+export function handleTree(data, id, parentId, children) {
+ // 检查 data 是否为数组
+ if (!data || !Array.isArray(data)) {
+ console.warn('handleTree: 期望数组,但收到:', data)
+ return []
+ }
+
+ let config = {
+ id: id || 'id',
+ parentId: parentId || 'parentId',
+ childrenList: children || 'children'
+ };
+
+ var childrenListMap = {};
+ var tree = [];
+ for (let d of data) {
+ let id = d[config.id];
+ childrenListMap[id] = d;
+ if (!d[config.childrenList]) {
+ d[config.childrenList] = [];
+ }
+ }
+
+ for (let d of data) {
+ let parentId = d[config.parentId]
+ let parentObj = childrenListMap[parentId]
+ if (!parentObj) {
+ tree.push(d);
+ } else {
+ parentObj[config.childrenList].push(d)
+ }
+ }
+ return tree;
+}
+
+/**
+* 参数处理
+* @param {*} params 参数
+*/
+export function tansParams(params) {
+ let result = ''
+ for (const propName of Object.keys(params)) {
+ const value = params[propName];
+ var part = encodeURIComponent(propName) + "=";
+ if (value !== null && value !== "" && typeof (value) !== "undefined") {
+ if (typeof value === 'object') {
+ for (const key of Object.keys(value)) {
+ if (value[key] !== null && value[key] !== "" && typeof (value[key]) !== 'undefined') {
+ let params = propName + '[' + key + ']';
+ var subPart = encodeURIComponent(params) + "=";
+ result += subPart + encodeURIComponent(value[key]) + "&";
+ }
+ }
+ } else {
+ result += part + encodeURIComponent(value) + "&";
+ }
+ }
+ }
+ return result
+}
+
+// 返回项目路径
+export function getNormalPath(p) {
+ if (p.length === 0 || !p || p == 'undefined') {
+ return p
+ };
+ let res = p.replace('//', '/')
+ if (res[res.length - 1] === '/') {
+ return res.slice(0, res.length - 1)
+ }
+ return res
+}
+
+// 验证是否为blob格式
+export function blobValidate(data) {
+ return data.type !== 'application/json'
+}
diff --git a/src/views/system/menu/index.vue b/src/views/system/menu/index.vue
index 2d15eec..462a3bc 100644
--- a/src/views/system/menu/index.vue
+++ b/src/views/system/menu/index.vue
@@ -289,7 +289,7 @@