动态路由调整

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

@ -22,6 +22,7 @@ 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 settingsStore = useSettingsStore()
const theme = computed(() => settingsStore.theme); const theme = computed(() => settingsStore.theme);
@ -68,7 +69,13 @@ function setLayout() {
} }
onMounted(() => { onMounted(() => {
permissionStore.generateRoutes() //
if (permissionStore.addRoutes.length === 0) {
const userStore = useUserStore()
if (userStore.roles.length > 0) {
permissionStore.generateRoutes(userStore.roles)
}
}
}) })
</script> </script>

View File

@ -7,6 +7,7 @@ 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 })
@ -32,18 +33,39 @@ router.beforeEach((to, from, next) => {
// 判断当前用户是否已拉取完user_info信息 // 判断当前用户是否已拉取完user_info信息
useUserStore().getInfo().then(() => { useUserStore().getInfo().then(() => {
isRelogin.show = false isRelogin.show = false
// 不再动态加载路由,直接使用静态路由 // 动态加载路由
const permissionStore = usePermissionStore()
permissionStore.generateRoutes(useUserStore().roles).then(() => {
// 动态路由加载完成后,重新跳转到目标路由
next({ ...to, replace: true }) next({ ...to, replace: true })
}).catch((error) => {
console.error('路由加载失败:', error)
ElMessage.error(error?.message || '路由加载失败,请刷新页面重试')
next({ path: '/' })
})
}).catch(err => { }).catch(err => {
useUserStore().logOut().then(() => { useUserStore().logOut().then(() => {
ElMessage.error(err) ElMessage.error(err)
next({ path: '/' }) 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 { } else {
next() next()
} }
} }
}
} else { } else {
// 没有token // 没有token
if (isWhiteList(to.path)) { if (isWhiteList(to.path)) {

View File

@ -60,7 +60,7 @@ export const constantRoutes = [
{ {
path: '', path: '',
component: Layout, component: Layout,
redirect: '/order/intention', redirect: 'index',
children: [ children: [
{ {
path: '/index', path: '/index',
@ -85,109 +85,11 @@ export const constantRoutes = [
meta: { title: '个人中心', icon: 'user' } 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({ const router = createRouter({
history: createWebHistory(), history: createWebHistory(),

View File

@ -1,10 +1,10 @@
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')
@ -34,14 +34,52 @@ const usePermissionStore = defineStore(
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))
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)
})
}) })
} }
} }
@ -49,23 +87,63 @@ const usePermissionStore = defineStore(
// 遍历后台传来的路由字符串,转换为组件对象 // 遍历后台传来的路由字符串,转换为组件对象
function filterAsyncRouter(asyncRouterMap, lastRouter = false, type = false) { function filterAsyncRouter(asyncRouterMap, lastRouter = false, type = false) {
return asyncRouterMap.filter(route => { // 确保 asyncRouterMap 是数组
if (type && route.children) { if (!Array.isArray(asyncRouterMap)) {
route.children = filterChildren(route.children) 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.component) {
// 检查是否有外部链接
if (route.meta && route.meta.link && (route.meta.link.startsWith('http://') || route.meta.link.startsWith('https://'))) {
route.component = InnerLink
}
// Layout ParentView 组件特殊处理 // Layout ParentView 组件特殊处理
if (route.component === 'Layout') { else if (route.component === 'Layout') {
route.component = Layout route.component = Layout
} else if (route.component === 'ParentView') { } else if (route.component === 'ParentView') {
route.component = ParentView route.component = ParentView
} else if (route.component === 'InnerLink') { } else if (route.component === 'InnerLink') {
route.component = InnerLink route.component = InnerLink
} else { } else {
route.component = loadView(route.component) 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) { 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) route.children = filterAsyncRouter(route.children, route, type)
} else { } else {
delete route['children'] delete route['children']
@ -78,7 +156,22 @@ function filterAsyncRouter(asyncRouterMap, lastRouter = false, type = false) {
function filterChildren(childrenMap, lastRouter = false) { function filterChildren(childrenMap, lastRouter = false) {
var children = [] var children = []
childrenMap.forEach(el => { childrenMap.forEach(el => {
el.path = lastRouter ? lastRouter.path + '/' + el.path : el.path 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') { if (el.children && el.children.length && el.component === 'ParentView') {
children = children.concat(filterChildren(el.children, el)) children = children.concat(filterChildren(el.children, el))
} else { } else {
@ -100,20 +193,39 @@ export function filterDynamicRoutes(routes) {
if (auth.hasRoleOr(route.roles)) { if (auth.hasRoleOr(route.roles)) {
res.push(route) res.push(route)
} }
} else {
// 没有权限控制的路由,直接通过
res.push(route)
} }
}) })
return res return res
} }
export const loadView = (view) => { export const loadView = (view) => {
let res; if (!view) {
return null
}
// 处理外部链接
if (view.startsWith('http://') || view.startsWith('https://')) {
return null
}
let res = null;
for (const path in modules) { for (const path in modules) {
const dir = path.split('views/')[1].split('.vue')[0]; const dir = path.split('views/')[1]?.split('.vue')[0];
if (dir === view) { if (dir && dir === view) {
res = () => modules[path](); res = () => modules[path]();
break;
} }
} }
if (!res) {
console.warn(`loadView: 找不到组件 "${view}",请检查路径是否正确`)
}
return res return res
} }
export default usePermissionStore export default usePermissionStore

View File

@ -156,6 +156,12 @@ export function mergeRecursive(source, target) {
* @param {*} children 孩子节点字段 默认 'children' * @param {*} children 孩子节点字段 默认 'children'
*/ */
export function handleTree(data, id, parentId, children) { export function handleTree(data, id, parentId, children) {
// 检查 data 是否为数组
if (!data || !Array.isArray(data)) {
console.warn('handleTree: 期望数组,但收到:', data)
return []
}
let config = { let config = {
id: id || 'id', id: id || 'id',
parentId: parentId || 'parentId', parentId: parentId || 'parentId',

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);