动态路由调整

This commit is contained in:
JenniferW 2025-11-24 14:22:31 +08:00
parent 5052ab0e56
commit df639d040a
8 changed files with 816 additions and 794 deletions

View File

@ -1,87 +1,60 @@
// 模拟静态数据 import request from '@/utils/request'
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' }
];
// 查询菜单列表 // 查询菜单列表
export function listMenu(query) { export function listMenu(query) {
const { menuName, status } = query; return request({
let filteredMenus = [...mockMenus]; url: '/system/menu/list',
method: 'get',
if (menuName) { params: query
filteredMenus = filteredMenus.filter(menu => menu.menuName.includes(menuName)); })
}
if (status) {
filteredMenus = filteredMenus.filter(menu => menu.status === status);
}
return Promise.resolve(filteredMenus);
} }
// 查询菜单详细 // 查询菜单详细
export function getMenu(menuId) { export function getMenu(menuId) {
const menu = mockMenus.find(m => m.menuId === parseInt(menuId)); return request({
return Promise.resolve(menu || {}); url: '/system/menu/' + menuId,
method: 'get'
})
} }
// 查询菜单下拉树结构 // 查询菜单下拉树结构
export function treeselect() { export function treeselect() {
const buildTree = (parentId) => { return request({
const children = mockMenus.filter(m => m.parentId === parentId); url: '/system/menu/treeselect',
return children.map(child => ({ method: 'get'
id: child.menuId, })
label: child.menuName,
children: buildTree(child.menuId)
}));
};
return Promise.resolve(buildTree(0));
} }
// 根据角色ID查询菜单下拉树结构 // 根据角色ID查询菜单下拉树结构
export function roleMenuTreeselect(roleId) { export function roleMenuTreeselect(roleId) {
const buildTree = (parentId) => { return request({
const children = mockMenus.filter(m => m.parentId === parentId); url: '/system/menu/roleMenuTreeselect/' + roleId,
return children.map(child => ({ method: 'get'
id: child.menuId, })
label: child.menuName,
children: buildTree(child.menuId)
}));
};
return Promise.resolve({
menus: buildTree(0),
checkedKeys: [1, 2, 3, 4] // 模拟已选中的菜单
});
} }
// 新增菜单 // 新增菜单
export function addMenu(data) { export function addMenu(data) {
const newMenu = { return request({
menuId: mockMenus.length + 1, url: '/system/menu',
...data method: 'post',
}; data: data
mockMenus.push(newMenu); })
return Promise.resolve({ code: 200, msg: '新增成功' });
} }
// 修改菜单 // 修改菜单
export function updateMenu(data) { export function updateMenu(data) {
const index = mockMenus.findIndex(m => m.menuId === data.menuId); return request({
if (index !== -1) { url: '/system/menu',
mockMenus[index] = { ...mockMenus[index], ...data }; method: 'put',
} data: data
return Promise.resolve({ code: 200, msg: '修改成功' }); })
} }
// 删除菜单 // 删除菜单
export function delMenu(menuId) { export function delMenu(menuId) {
const index = mockMenus.findIndex(m => m.menuId === parseInt(menuId)); return request({
if (index !== -1) { url: '/system/menu/' + menuId,
mockMenus.splice(index, 1); method: 'delete'
} })
return Promise.resolve({ code: 200, msg: '删除成功' });
} }

View File

@ -5,9 +5,9 @@
</template> </template>
<script setup> <script setup>
const url = ref('http://doc.ruoyi.vip/ruoyi-vue'); const url = ref("http://doc.ruoyi.vip/ruoyi-vue");
function goto() { function goto() {
window.open(url.value) window.open(url.value);
} }
</script> </script>

View File

@ -1,121 +1,128 @@
<template> <template>
<div :class="classObj" class="app-wrapper" :style="{ '--current-color': theme }"> <div :class="classObj" class="app-wrapper" :style="{ '--current-color': theme }">
<div v-if="device === 'mobile' && sidebar.opened" class="drawer-bg" @click="handleClickOutside"/> <div v-if="device === 'mobile' && sidebar.opened" class="drawer-bg" @click="handleClickOutside"/>
<sidebar v-if="!sidebar.hide" class="sidebar-container" /> <sidebar v-if="!sidebar.hide" class="sidebar-container" />
<div :class="{ hasTagsView: needTagsView, sidebarHide: sidebar.hide }" class="main-container"> <div :class="{ hasTagsView: needTagsView, sidebarHide: sidebar.hide }" class="main-container">
<div :class="{ 'fixed-header': fixedHeader }"> <div :class="{ 'fixed-header': fixedHeader }">
<navbar @setLayout="setLayout" /> <navbar @setLayout="setLayout" />
<tags-view v-if="needTagsView" /> <tags-view v-if="needTagsView" />
</div> </div>
<app-main /> <app-main />
<settings ref="settingRef" /> <settings ref="settingRef" />
</div> </div>
</div> </div>
</template> </template>
<script setup> <script setup>
import { useWindowSize } from '@vueuse/core' import { useWindowSize } from '@vueuse/core'
import Sidebar from './components/Sidebar/index.vue' import Sidebar from './components/Sidebar/index.vue'
import { AppMain, Navbar, Settings, TagsView } from './components' import { AppMain, Navbar, Settings, TagsView } from './components'
import defaultSettings from '@/settings' import defaultSettings from '@/settings'
import useAppStore from '@/store/modules/app' import useAppStore from '@/store/modules/app'
import useSettingsStore from '@/store/modules/settings' import useSettingsStore from '@/store/modules/settings'
import usePermissionStore from '@/store/modules/permission' import usePermissionStore from '@/store/modules/permission'
import useUserStore from '@/store/modules/user'
const settingsStore = useSettingsStore()
const theme = computed(() => settingsStore.theme); const settingsStore = useSettingsStore()
const sideTheme = computed(() => settingsStore.sideTheme); const theme = computed(() => settingsStore.theme);
const sidebar = computed(() => useAppStore().sidebar); const sideTheme = computed(() => settingsStore.sideTheme);
const device = computed(() => useAppStore().device); const sidebar = computed(() => useAppStore().sidebar);
const needTagsView = computed(() => settingsStore.tagsView); const device = computed(() => useAppStore().device);
const fixedHeader = computed(() => settingsStore.fixedHeader); const needTagsView = computed(() => settingsStore.tagsView);
const fixedHeader = computed(() => settingsStore.fixedHeader);
const classObj = computed(() => ({
hideSidebar: !sidebar.value.opened, const classObj = computed(() => ({
openSidebar: sidebar.value.opened, hideSidebar: !sidebar.value.opened,
withoutAnimation: sidebar.value.withoutAnimation, openSidebar: sidebar.value.opened,
mobile: device.value === 'mobile' withoutAnimation: sidebar.value.withoutAnimation,
})) mobile: device.value === 'mobile'
}))
const { width, height } = useWindowSize();
const WIDTH = 992; // refer to Bootstrap's responsive design const { width, height } = useWindowSize();
const WIDTH = 992; // refer to Bootstrap's responsive design
const permissionStore = usePermissionStore()
const permissionStore = usePermissionStore()
watch(() => device.value, () => {
if (device.value === 'mobile' && sidebar.value.opened) { watch(() => device.value, () => {
useAppStore().closeSideBar({ withoutAnimation: false }) if (device.value === 'mobile' && sidebar.value.opened) {
} useAppStore().closeSideBar({ withoutAnimation: false })
}) }
})
watchEffect(() => {
if (width.value - 1 < WIDTH) { watchEffect(() => {
useAppStore().toggleDevice('mobile') if (width.value - 1 < WIDTH) {
useAppStore().closeSideBar({ withoutAnimation: true }) useAppStore().toggleDevice('mobile')
} else { useAppStore().closeSideBar({ withoutAnimation: true })
useAppStore().toggleDevice('desktop') } else {
} useAppStore().toggleDevice('desktop')
}) }
})
function handleClickOutside() {
useAppStore().closeSideBar({ withoutAnimation: false }) function handleClickOutside() {
} useAppStore().closeSideBar({ withoutAnimation: false })
}
const settingRef = ref(null);
function setLayout() { const settingRef = ref(null);
settingRef.value.openSetting(); function setLayout() {
} settingRef.value.openSetting();
}
onMounted(() => {
permissionStore.generateRoutes() onMounted(() => {
}) //
</script> if (permissionStore.addRoutes.length === 0) {
const userStore = useUserStore()
<style lang="scss" scoped> if (userStore.roles.length > 0) {
@import "@/assets/styles/mixin.scss"; permissionStore.generateRoutes(userStore.roles)
@import "@/assets/styles/variables.module.scss"; }
}
.app-wrapper { })
@include clearfix; </script>
position: relative;
height: 100%; <style lang="scss" scoped>
width: 100%; @import "@/assets/styles/mixin.scss";
@import "@/assets/styles/variables.module.scss";
&.mobile.openSidebar {
position: fixed; .app-wrapper {
top: 0; @include clearfix;
} position: relative;
} height: 100%;
width: 100%;
.drawer-bg {
background: #000; &.mobile.openSidebar {
opacity: 0.3; position: fixed;
width: 100%; top: 0;
top: 0; }
height: 100%; }
position: absolute;
z-index: 999; .drawer-bg {
} background: #000;
opacity: 0.3;
.fixed-header { width: 100%;
position: fixed; top: 0;
top: 0; height: 100%;
right: 0; position: absolute;
z-index: 9; z-index: 999;
width: calc(100% - #{$base-sidebar-width}); }
transition: width 0.28s;
} .fixed-header {
position: fixed;
.hideSidebar .fixed-header { top: 0;
width: calc(100% - 54px); right: 0;
} z-index: 9;
width: calc(100% - #{$base-sidebar-width});
.sidebarHide .fixed-header { transition: width 0.28s;
width: 100%; }
}
.hideSidebar .fixed-header {
.mobile .fixed-header { width: calc(100% - 54px);
width: 100%; }
}
.sidebarHide .fixed-header {
width: 100%;
}
.mobile .fixed-header {
width: 100%;
}
</style> </style>

View File

@ -1,61 +1,83 @@
import router from './router' import router from './router'
import { ElMessage } from 'element-plus' import { ElMessage } from 'element-plus'
import NProgress from 'nprogress' import NProgress from 'nprogress'
import 'nprogress/nprogress.css' import 'nprogress/nprogress.css'
import { getToken } from '@/utils/auth' import { getToken } from '@/utils/auth'
import { isHttp, isPathMatch } from '@/utils/validate' import { isHttp, isPathMatch } from '@/utils/validate'
import { isRelogin } from '@/utils/request' import { isRelogin } from '@/utils/request'
import useUserStore from '@/store/modules/user' import useUserStore from '@/store/modules/user'
import useSettingsStore from '@/store/modules/settings' import useSettingsStore from '@/store/modules/settings'
import usePermissionStore from '@/store/modules/permission'
NProgress.configure({ showSpinner: false })
NProgress.configure({ showSpinner: false })
const whiteList = ['/login', '/register']
const whiteList = ['/login', '/register']
const isWhiteList = (path) => {
return whiteList.some(pattern => isPathMatch(pattern, path)) const isWhiteList = (path) => {
} return whiteList.some(pattern => isPathMatch(pattern, path))
}
router.beforeEach((to, from, next) => {
NProgress.start() router.beforeEach((to, from, next) => {
if (getToken()) { NProgress.start()
to.meta.title && useSettingsStore().setTitle(to.meta.title) if (getToken()) {
/* has token*/ to.meta.title && useSettingsStore().setTitle(to.meta.title)
if (to.path === '/login') { /* has token*/
next({ path: '/' }) if (to.path === '/login') {
NProgress.done() next({ path: '/' })
} else if (isWhiteList(to.path)) { NProgress.done()
next() } else if (isWhiteList(to.path)) {
} else { next()
if (useUserStore().roles.length === 0) { } else {
isRelogin.show = true if (useUserStore().roles.length === 0) {
// 判断当前用户是否已拉取完user_info信息 isRelogin.show = true
useUserStore().getInfo().then(() => { // 判断当前用户是否已拉取完user_info信息
isRelogin.show = false useUserStore().getInfo().then(() => {
// 不再动态加载路由,直接使用静态路由 isRelogin.show = false
next({ ...to, replace: true }) // 动态加载路由
}).catch(err => { const permissionStore = usePermissionStore()
useUserStore().logOut().then(() => { permissionStore.generateRoutes(useUserStore().roles).then(() => {
ElMessage.error(err) // 动态路由加载完成后,重新跳转到目标路由
next({ path: '/' }) next({ ...to, replace: true })
}) }).catch((error) => {
}) console.error('路由加载失败:', error)
} else { ElMessage.error(error?.message || '路由加载失败,请刷新页面重试')
next() next({ path: '/' })
} })
} }).catch(err => {
} else { useUserStore().logOut().then(() => {
// 没有token ElMessage.error(err)
if (isWhiteList(to.path)) { next({ path: '/' })
// 在免登录白名单,直接进入 })
next() })
} else { } else {
next(`/login?redirect=${to.fullPath}`) // 否则全部重定向到登录页 // 已获取用户信息,检查路由是否已加载
NProgress.done() const permissionStore = usePermissionStore()
} if (permissionStore.addRoutes.length === 0) {
} // 路由未加载,重新加载
}) permissionStore.generateRoutes(useUserStore().roles).then(() => {
next({ ...to, replace: true })
router.afterEach(() => { }).catch((error) => {
NProgress.done() 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()
})

View File

@ -1,203 +1,105 @@
import { createWebHistory, createRouter } from 'vue-router' import { createWebHistory, createRouter } from 'vue-router'
/* Layout */ /* Layout */
import Layout from '@/layout/index.vue' import Layout from '@/layout/index.vue'
/** /**
* Note: 路由配置项 * Note: 路由配置项
* *
* hidden: true // 当设置 true 的时候该路由不会再侧边栏出现 如401login等页面或者如一些编辑页面/edit/1 * hidden: true // 当设置 true 的时候该路由不会再侧边栏出现 如401login等页面或者如一些编辑页面/edit/1
* alwaysShow: true // 当你一个路由下面的 children 声明的路由大于1个时自动会变成嵌套的模式--如组件页面 * alwaysShow: true // 当你一个路由下面的 children 声明的路由大于1个时自动会变成嵌套的模式--如组件页面
* // 只有一个时,会将那个子路由当做根路由显示在侧边栏--如引导页面 * // 只有一个时,会将那个子路由当做根路由显示在侧边栏--如引导页面
* // 若你想不管路由下面的 children 声明的个数都显示你的根路由 * // 若你想不管路由下面的 children 声明的个数都显示你的根路由
* // 你可以设置 alwaysShow: true这样它就会忽略之前定义的规则一直显示根路由 * // 你可以设置 alwaysShow: true这样它就会忽略之前定义的规则一直显示根路由
* redirect: noRedirect // 当设置 noRedirect 的时候该路由在面包屑导航中不可被点击 * redirect: noRedirect // 当设置 noRedirect 的时候该路由在面包屑导航中不可被点击
* name:'router-name' // 设定路由的名字,一定要填写不然使用<keep-alive>时会出现各种问题 * name:'router-name' // 设定路由的名字,一定要填写不然使用<keep-alive>时会出现各种问题
* query: '{"id": 1, "name": "ry"}' // 访问路由的默认传递参数 * query: '{"id": 1, "name": "ry"}' // 访问路由的默认传递参数
* roles: ['admin', 'common'] // 访问路由的角色权限 * roles: ['admin', 'common'] // 访问路由的角色权限
* permissions: ['a:a:a', 'b:b:b'] // 访问路由的菜单权限 * permissions: ['a:a:a', 'b:b:b'] // 访问路由的菜单权限
* meta : { * meta : {
noCache: true // 如果设置为true则不会被 <keep-alive> 缓存(默认 false) noCache: true // 如果设置为true则不会被 <keep-alive> 缓存(默认 false)
title: 'title' // 设置该路由在侧边栏和面包屑中展示的名字 title: 'title' // 设置该路由在侧边栏和面包屑中展示的名字
icon: 'svg-name' // 设置该路由的图标对应路径src/assets/icons/svg icon: 'svg-name' // 设置该路由的图标对应路径src/assets/icons/svg
breadcrumb: false // 如果设置为false则不会在breadcrumb面包屑中显示 breadcrumb: false // 如果设置为false则不会在breadcrumb面包屑中显示
activeMenu: '/system/user' // 当路由设置了该属性,则会高亮相对应的侧边栏。 activeMenu: '/system/user' // 当路由设置了该属性,则会高亮相对应的侧边栏。
} }
*/ */
// 公共路由 // 公共路由
export const constantRoutes = [ export const constantRoutes = [
{ {
path: '/redirect', path: '/redirect',
component: Layout, component: Layout,
hidden: true, hidden: true,
children: [ children: [
{ {
path: '/redirect/:path(.*)', path: '/redirect/:path(.*)',
component: () => import('@/views/redirect/index.vue') component: () => import('@/views/redirect/index.vue')
} }
] ]
}, },
{ {
path: '/login', path: '/login',
component: () => import('@/views/login.vue'), component: () => import('@/views/login.vue'),
hidden: true hidden: true
}, },
{ {
path: '/register', path: '/register',
component: () => import('@/views/register.vue'), component: () => import('@/views/register.vue'),
hidden: true hidden: true
}, },
{ {
path: "/:pathMatch(.*)*", path: "/:pathMatch(.*)*",
component: () => import('@/views/error/404.vue'), component: () => import('@/views/error/404.vue'),
hidden: true hidden: true
}, },
{ {
path: '/401', path: '/401',
component: () => import('@/views/error/401.vue'), component: () => import('@/views/error/401.vue'),
hidden: true hidden: true
}, },
{ {
path: '', path: '',
component: Layout, component: Layout,
redirect: '/order/intention', redirect: 'index',
children: [ children: [
{ {
path: '/index', path: '/index',
component: () => import('@/views/index.vue'), component: () => import('@/views/index.vue'),
name: 'Index', name: 'Index',
meta: { title: '首页', icon: 'dashboard', affix: true } meta: { title: '首页', icon: 'dashboard', affix: true }
} }
] ]
}, },
{ {
path: '/user', path: '/user',
component: Layout, component: Layout,
hidden: true, hidden: true,
redirect: 'noRedirect', redirect: 'noRedirect',
name: 'UserCenter', name: 'UserCenter',
meta: { title: '个人中心', icon: 'user' }, meta: { title: '个人中心', icon: 'user' },
children: [ children: [
{ {
path: 'profile', path: 'profile',
component: () => import('@/views/user/profile/index.vue'), component: () => import('@/views/user/profile/index.vue'),
name: 'Profile', name: 'Profile',
meta: { title: '个人中心', icon: 'user' } meta: { title: '个人中心', icon: 'user' }
} }
] ]
}, }
{ // 业务路由(订单管理、系统管理等)将从后端动态获取
path: '/order', ];
component: Layout,
hidden: false, // 动态路由将从后端接口获取,不再在此处定义
name: 'Order',
meta: { title: '订单管理', icon: 'shopping' }, const router = createRouter({
children: [ history: createWebHistory(),
{ routes: constantRoutes,
path: 'intention', scrollBehavior(to, from, savedPosition) {
component: () => import('@/views/order/intention/index.vue'), if (savedPosition) {
name: 'Intention', return savedPosition
meta: { title: '订单/意向单', icon: 'form' } }
}, return { top: 0 }
{ },
path: 'create', });
component: () => import('@/views/order/intention/create.vue'),
name: 'IntentionCreate', export default router;
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;

View File

@ -1,119 +1,231 @@
import auth from '@/plugins/auth' import auth from '@/plugins/auth'
import router, { constantRoutes } from '@/router' import router, { constantRoutes } from '@/router'
// 不再需要动态路由,移除 dynamicRoutes 和 getRouters import { getRouters } from '@/api/menu'
// import { getRouters } from '@/api/menu' import Layout from '@/layout/index'
import Layout from '@/layout/index' import ParentView from '@/components/ParentView'
import ParentView from '@/components/ParentView' import InnerLink from '@/layout/components/InnerLink'
import InnerLink from '@/layout/components/InnerLink' import { isHttp } from '@/utils/validate'
// 匹配views里面所有的.vue文件 // 匹配views里面所有的.vue文件
const modules = import.meta.glob('./../../views/**/*.vue') const modules = import.meta.glob('./../../views/**/*.vue')
const usePermissionStore = defineStore( const usePermissionStore = defineStore(
'permission', 'permission',
{ {
state: () => ({ state: () => ({
routes: [], routes: [],
addRoutes: [], addRoutes: [],
defaultRoutes: [], defaultRoutes: [],
topbarRouters: [], topbarRouters: [],
sidebarRouters: [] sidebarRouters: []
}), }),
actions: { actions: {
setRoutes(routes) { setRoutes(routes) {
this.addRoutes = routes this.addRoutes = routes
this.routes = constantRoutes.concat(routes) this.routes = constantRoutes.concat(routes)
}, },
setDefaultRoutes(routes) { setDefaultRoutes(routes) {
this.defaultRoutes = constantRoutes.concat(routes) this.defaultRoutes = constantRoutes.concat(routes)
}, },
setTopbarRoutes(routes) { setTopbarRoutes(routes) {
this.topbarRouters = routes this.topbarRouters = routes
}, },
setSidebarRouters(routes) { setSidebarRouters(routes) {
this.sidebarRouters = routes this.sidebarRouters = routes
}, },
generateRoutes(roles) { generateRoutes(roles) {
return new Promise(resolve => { return new Promise((resolve, reject) => {
// 不再调用后端接口,直接使用静态路由 // 调用后端接口获取动态路由
const staticRoutes = constantRoutes getRouters().then(res => {
this.setRoutes(staticRoutes) // 确保 res.data 是数组
this.setSidebarRouters(staticRoutes) if (!res.data || !Array.isArray(res.data)) {
this.setDefaultRoutes(staticRoutes) console.error('路由数据格式错误:', res.data)
this.setTopbarRoutes(staticRoutes) reject(new Error('路由数据格式错误,期望数组'))
resolve(staticRoutes) 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))
function filterAsyncRouter(asyncRouterMap, lastRouter = false, type = false) { const sidebarRoutes = filterAsyncRouter(sdata)
return asyncRouterMap.filter(route => { const rewriteRoutes = filterAsyncRouter(rdata, false, true)
if (type && route.children) { const defaultRoutes = filterAsyncRouter(defaultData)
route.children = filterChildren(route.children)
} // 过滤权限路由
if (route.component) { const accessedRoutes = filterDynamicRoutes(rewriteRoutes)
// Layout ParentView 组件特殊处理
if (route.component === 'Layout') { // 调试:打印路由信息
route.component = Layout console.log('动态路由加载成功,路由数量:', accessedRoutes.length)
} else if (route.component === 'ParentView') { if (accessedRoutes.length > 0) {
route.component = ParentView console.log('第一个路由示例:', accessedRoutes[0])
} else if (route.component === 'InnerLink') { }
route.component = InnerLink
} else { this.setRoutes(accessedRoutes)
route.component = loadView(route.component) this.setSidebarRouters(constantRoutes.concat(sidebarRoutes))
} this.setDefaultRoutes(constantRoutes.concat(defaultRoutes))
} this.setTopbarRoutes(constantRoutes.concat(sidebarRoutes))
if (route.children != null && route.children && route.children.length) {
route.children = filterAsyncRouter(route.children, route, type) // 动态添加路由到router
} else { accessedRoutes.forEach(route => {
delete route['children'] router.addRoute(route)
delete route['redirect'] })
}
return true resolve(accessedRoutes)
}) } catch (error) {
} console.error('路由处理错误:', error)
reject(error)
function filterChildren(childrenMap, lastRouter = false) { }
var children = [] }).catch(error => {
childrenMap.forEach(el => { console.error('获取路由失败:', error)
el.path = lastRouter ? lastRouter.path + '/' + el.path : el.path reject(error)
if (el.children && el.children.length && el.component === 'ParentView') { })
children = children.concat(filterChildren(el.children, el)) })
} else { }
children.push(el) }
} })
})
return children // 遍历后台传来的路由字符串,转换为组件对象
} function filterAsyncRouter(asyncRouterMap, lastRouter = false, type = false) {
// 确保 asyncRouterMap 是数组
// 动态路由遍历,验证是否具备权限 if (!Array.isArray(asyncRouterMap)) {
export function filterDynamicRoutes(routes) { console.error('filterAsyncRouter: 期望数组,但收到:', asyncRouterMap)
const res = [] return []
routes.forEach(route => { }
if (route.permissions) {
if (auth.hasPermiOr(route.permissions)) { return asyncRouterMap.filter(route => {
res.push(route) // 处理外部链接路径:如果 path 是外部链接,需要转换为有效的内部路径
} if (route.path && isHttp(route.path)) {
} else if (route.roles) { // 确保 meta.link 保存实际的外部链接
if (auth.hasRoleOr(route.roles)) { if (!route.meta) {
res.push(route) route.meta = {}
} }
} // 保存原始的外部链接到 meta.link
}) const originalPath = route.path
return res route.meta.link = originalPath
}
// 将外部链接路径转换为内部路径
export const loadView = (view) => { // 使用简单的路径格式:/iframe/域名,避免路径冲突
let res; const domain = originalPath.replace(/^https?:\/\//, '').replace(/\/.*$/, '')
for (const path in modules) { const timestamp = Date.now()
const dir = path.split('views/')[1].split('.vue')[0]; const encodedPath = '/iframe/' + encodeURIComponent(domain) + '-' + timestamp
if (dir === view) {
res = () => modules[path](); // 将 path 转换为有效的内部路径
} route.path = encodedPath
} } else if (!lastRouter && route.path && !route.path.startsWith('/')) {
return res // 只对顶级路由lastRouter 为 false确保路径以 "/" 开头
} // 子路由的路径会在 filterChildren 中处理
route.path = '/' + route.path
export default usePermissionStore }
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

View File

@ -1,228 +1,234 @@
/** /**
* 通用js方法封装处理 * 通用js方法封装处理
* Copyright (c) 2019 ruoyi * Copyright (c) 2019 ruoyi
*/ */
// 日期格式化 // 日期格式化
export function parseTime(time, pattern) { export function parseTime(time, pattern) {
if (arguments.length === 0 || !time) { if (arguments.length === 0 || !time) {
return null return null
} }
const format = pattern || '{y}-{m}-{d} {h}:{i}:{s}' const format = pattern || '{y}-{m}-{d} {h}:{i}:{s}'
let date let date
if (typeof time === 'object') { if (typeof time === 'object') {
date = time date = time
} else { } else {
if ((typeof time === 'string') && (/^[0-9]+$/.test(time))) { if ((typeof time === 'string') && (/^[0-9]+$/.test(time))) {
time = parseInt(time) time = parseInt(time)
} else if (typeof time === 'string') { } else if (typeof time === 'string') {
time = time.replace(new RegExp(/-/gm), '/').replace('T', ' ').replace(new RegExp(/\.[\d]{3}/gm), ''); time = time.replace(new RegExp(/-/gm), '/').replace('T', ' ').replace(new RegExp(/\.[\d]{3}/gm), '');
} }
if ((typeof time === 'number') && (time.toString().length === 10)) { if ((typeof time === 'number') && (time.toString().length === 10)) {
time = time * 1000 time = time * 1000
} }
date = new Date(time) date = new Date(time)
} }
const formatObj = { const formatObj = {
y: date.getFullYear(), y: date.getFullYear(),
m: date.getMonth() + 1, m: date.getMonth() + 1,
d: date.getDate(), d: date.getDate(),
h: date.getHours(), h: date.getHours(),
i: date.getMinutes(), i: date.getMinutes(),
s: date.getSeconds(), s: date.getSeconds(),
a: date.getDay() a: date.getDay()
} }
const time_str = format.replace(/{(y|m|d|h|i|s|a)+}/g, (result, key) => { const time_str = format.replace(/{(y|m|d|h|i|s|a)+}/g, (result, key) => {
let value = formatObj[key] let value = formatObj[key]
// Note: getDay() returns 0 on Sunday // Note: getDay() returns 0 on Sunday
if (key === 'a') { return ['日', '一', '二', '三', '四', '五', '六'][value] } if (key === 'a') { return ['日', '一', '二', '三', '四', '五', '六'][value] }
if (result.length > 0 && value < 10) { if (result.length > 0 && value < 10) {
value = '0' + value value = '0' + value
} }
return value || 0 return value || 0
}) })
return time_str return time_str
} }
// 表单重置 // 表单重置
export function resetForm(refName) { export function resetForm(refName) {
if (this.$refs[refName]) { if (this.$refs[refName]) {
this.$refs[refName].resetFields(); this.$refs[refName].resetFields();
} }
} }
// 添加日期范围 // 添加日期范围
export function addDateRange(params, dateRange, propName) { export function addDateRange(params, dateRange, propName) {
let search = params; let search = params;
search.params = typeof (search.params) === 'object' && search.params !== null && !Array.isArray(search.params) ? search.params : {}; search.params = typeof (search.params) === 'object' && search.params !== null && !Array.isArray(search.params) ? search.params : {};
dateRange = Array.isArray(dateRange) ? dateRange : []; dateRange = Array.isArray(dateRange) ? dateRange : [];
if (typeof (propName) === 'undefined') { if (typeof (propName) === 'undefined') {
search.params['beginTime'] = dateRange[0]; search.params['beginTime'] = dateRange[0];
search.params['endTime'] = dateRange[1]; search.params['endTime'] = dateRange[1];
} else { } else {
search.params['begin' + propName] = dateRange[0]; search.params['begin' + propName] = dateRange[0];
search.params['end' + propName] = dateRange[1]; search.params['end' + propName] = dateRange[1];
} }
return search; return search;
} }
// 回显数据字典 // 回显数据字典
export function selectDictLabel(datas, value) { export function selectDictLabel(datas, value) {
if (value === undefined) { if (value === undefined) {
return ""; return "";
} }
var actions = []; var actions = [];
Object.keys(datas).some((key) => { Object.keys(datas).some((key) => {
if (datas[key].value == ('' + value)) { if (datas[key].value == ('' + value)) {
actions.push(datas[key].label); actions.push(datas[key].label);
return true; return true;
} }
}) })
if (actions.length === 0) { if (actions.length === 0) {
actions.push(value); actions.push(value);
} }
return actions.join(''); return actions.join('');
} }
// 回显数据字典(字符串、数组) // 回显数据字典(字符串、数组)
export function selectDictLabels(datas, value, separator) { export function selectDictLabels(datas, value, separator) {
if (value === undefined || value.length ===0) { if (value === undefined || value.length ===0) {
return ""; return "";
} }
if (Array.isArray(value)) { if (Array.isArray(value)) {
value = value.join(","); value = value.join(",");
} }
var actions = []; var actions = [];
var currentSeparator = undefined === separator ? "," : separator; var currentSeparator = undefined === separator ? "," : separator;
var temp = value.split(currentSeparator); var temp = value.split(currentSeparator);
Object.keys(value.split(currentSeparator)).some((val) => { Object.keys(value.split(currentSeparator)).some((val) => {
var match = false; var match = false;
Object.keys(datas).some((key) => { Object.keys(datas).some((key) => {
if (datas[key].value == ('' + temp[val])) { if (datas[key].value == ('' + temp[val])) {
actions.push(datas[key].label + currentSeparator); actions.push(datas[key].label + currentSeparator);
match = true; match = true;
} }
}) })
if (!match) { if (!match) {
actions.push(temp[val] + currentSeparator); actions.push(temp[val] + currentSeparator);
} }
}) })
return actions.join('').substring(0, actions.join('').length - 1); return actions.join('').substring(0, actions.join('').length - 1);
} }
// 字符串格式化(%s ) // 字符串格式化(%s )
export function sprintf(str) { export function sprintf(str) {
var args = arguments, flag = true, i = 1; var args = arguments, flag = true, i = 1;
str = str.replace(/%s/g, function () { str = str.replace(/%s/g, function () {
var arg = args[i++]; var arg = args[i++];
if (typeof arg === 'undefined') { if (typeof arg === 'undefined') {
flag = false; flag = false;
return ''; return '';
} }
return arg; return arg;
}); });
return flag ? str : ''; return flag ? str : '';
} }
// 转换字符串undefined,null等转化为"" // 转换字符串undefined,null等转化为""
export function parseStrEmpty(str) { export function parseStrEmpty(str) {
if (!str || str == "undefined" || str == "null") { if (!str || str == "undefined" || str == "null") {
return ""; return "";
} }
return str; return str;
} }
// 数据合并 // 数据合并
export function mergeRecursive(source, target) { export function mergeRecursive(source, target) {
for (var p in target) { for (var p in target) {
try { try {
if (target[p].constructor == Object) { if (target[p].constructor == Object) {
source[p] = mergeRecursive(source[p], target[p]); source[p] = mergeRecursive(source[p], target[p]);
} else { } else {
source[p] = target[p]; source[p] = target[p];
} }
} catch (e) { } catch (e) {
source[p] = target[p]; source[p] = target[p];
} }
} }
return source; return source;
}; };
/** /**
* 构造树型结构数据 * 构造树型结构数据
* @param {*} data 数据源 * @param {*} data 数据源
* @param {*} id id字段 默认 'id' * @param {*} id id字段 默认 'id'
* @param {*} parentId 父节点字段 默认 'parentId' * @param {*} parentId 父节点字段 默认 'parentId'
* @param {*} children 孩子节点字段 默认 'children' * @param {*} children 孩子节点字段 默认 'children'
*/ */
export function handleTree(data, id, parentId, children) { export function handleTree(data, id, parentId, children) {
let config = { // 检查 data 是否为数组
id: id || 'id', if (!data || !Array.isArray(data)) {
parentId: parentId || 'parentId', console.warn('handleTree: 期望数组,但收到:', data)
childrenList: children || 'children' return []
}; }
var childrenListMap = {}; let config = {
var tree = []; id: id || 'id',
for (let d of data) { parentId: parentId || 'parentId',
let id = d[config.id]; childrenList: children || 'children'
childrenListMap[id] = d; };
if (!d[config.childrenList]) {
d[config.childrenList] = []; var childrenListMap = {};
} var tree = [];
} for (let d of data) {
let id = d[config.id];
for (let d of data) { childrenListMap[id] = d;
let parentId = d[config.parentId] if (!d[config.childrenList]) {
let parentObj = childrenListMap[parentId] d[config.childrenList] = [];
if (!parentObj) { }
tree.push(d); }
} else {
parentObj[config.childrenList].push(d) for (let d of data) {
} let parentId = d[config.parentId]
} let parentObj = childrenListMap[parentId]
return tree; if (!parentObj) {
} tree.push(d);
} else {
/** parentObj[config.childrenList].push(d)
* 参数处理 }
* @param {*} params 参数 }
*/ return tree;
export function tansParams(params) { }
let result = ''
for (const propName of Object.keys(params)) { /**
const value = params[propName]; * 参数处理
var part = encodeURIComponent(propName) + "="; * @param {*} params 参数
if (value !== null && value !== "" && typeof (value) !== "undefined") { */
if (typeof value === 'object') { export function tansParams(params) {
for (const key of Object.keys(value)) { let result = ''
if (value[key] !== null && value[key] !== "" && typeof (value[key]) !== 'undefined') { for (const propName of Object.keys(params)) {
let params = propName + '[' + key + ']'; const value = params[propName];
var subPart = encodeURIComponent(params) + "="; var part = encodeURIComponent(propName) + "=";
result += subPart + encodeURIComponent(value[key]) + "&"; if (value !== null && value !== "" && typeof (value) !== "undefined") {
} if (typeof value === 'object') {
} for (const key of Object.keys(value)) {
} else { if (value[key] !== null && value[key] !== "" && typeof (value[key]) !== 'undefined') {
result += part + encodeURIComponent(value) + "&"; let params = propName + '[' + key + ']';
} var subPart = encodeURIComponent(params) + "=";
} result += subPart + encodeURIComponent(value[key]) + "&";
} }
return result }
} } else {
result += part + encodeURIComponent(value) + "&";
// 返回项目路径 }
export function getNormalPath(p) { }
if (p.length === 0 || !p || p == 'undefined') { }
return p return result
}; }
let res = p.replace('//', '/')
if (res[res.length - 1] === '/') { // 返回项目路径
return res.slice(0, res.length - 1) export function getNormalPath(p) {
} if (p.length === 0 || !p || p == 'undefined') {
return res return p
} };
let res = p.replace('//', '/')
// 验证是否为blob格式 if (res[res.length - 1] === '/') {
export function blobValidate(data) { return res.slice(0, res.length - 1)
return data.type !== 'application/json' }
} return res
}
// 验证是否为blob格式
export function blobValidate(data) {
return data.type !== 'application/json'
}

View File

@ -289,7 +289,7 @@
</template> </template>
<script setup name="Menu"> <script setup name="Menu">
import { addMenu, delMenu, getMenu, listMenu, updateMenu } from "@/api/system/menu"; import { addMenu, delMenu, getMenu, listMenu, treeselect, updateMenu } from "@/api/system/menu";
import SvgIcon from "@/components/SvgIcon"; import SvgIcon from "@/components/SvgIcon";
import IconSelect from "@/components/IconSelect"; import IconSelect from "@/components/IconSelect";
@ -333,7 +333,7 @@ function getList() {
/** 查询菜单下拉树结构 */ /** 查询菜单下拉树结构 */
function getTreeselect() { function getTreeselect() {
menuOptions.value = []; menuOptions.value = [];
listMenu().then(response => { treeselect().then(response => {
const menu = { menuId: 0, menuName: "主类目", children: [] }; const menu = { menuId: 0, menuName: "主类目", children: [] };
menu.children = proxy.handleTree(response.data, "menuId"); menu.children = proxy.handleTree(response.data, "menuId");
menuOptions.value.push(menu); menuOptions.value.push(menu);