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 @@