新增:新增获取路由信息 API(默认前端动态路由处于关闭状态,可通过[页面配置]>[菜单来源于后台]开启)

1.在页面导航栏中通过[页面配置]>[菜单来源于后台]临时启用,刷新后配置失效
2.在前端项目 src/config/setting.json 中,可通过 menuFromServer 配置永久启用
This commit is contained in:
Charles7c 2023-03-09 00:06:02 +08:00
parent fb0effed9a
commit d8ceda4654
24 changed files with 260 additions and 17 deletions

View File

@ -0,0 +1,50 @@
/*
* Copyright (c) 2022-present Charles7c Authors. All Rights Reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package top.charles7c.cnadmin.auth.model.vo;
import java.io.Serializable;
import lombok.Data;
import lombok.experimental.Accessors;
import io.swagger.v3.oas.annotations.media.Schema;
/**
* 元数据信息
*
* @author Charles7c
* @since 2023/2/26 22:51
*/
@Data
@Accessors(chain = true)
@Schema(description = "元数据信息")
public class MetaVO implements Serializable {
private static final long serialVersionUID = 1L;
/**
* 菜单标题
*/
@Schema(description = "菜单标题")
private String locale;
/**
* 菜单图标
*/
@Schema(description = "菜单图标")
private String icon;
}

View File

@ -0,0 +1,72 @@
/*
* Copyright (c) 2022-present Charles7c Authors. All Rights Reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package top.charles7c.cnadmin.auth.model.vo;
import java.io.Serializable;
import java.util.List;
import lombok.Data;
import lombok.experimental.Accessors;
import io.swagger.v3.oas.annotations.media.Schema;
import com.fasterxml.jackson.annotation.JsonInclude;
/**
* 路由信息
*
* @author Charles7c
* @since 2023/2/26 22:51
*/
@Data
@Accessors(chain = true)
@Schema(description = "路由信息")
@JsonInclude(JsonInclude.Include.NON_EMPTY)
public class RouteVO implements Serializable {
private static final long serialVersionUID = 1L;
/**
* 路由地址
*/
@Schema(description = "路由地址")
private String path;
/**
* 组件名称
*/
@Schema(description = "组件名称")
private String name;
/**
* 组件路径
*/
@Schema(description = "组件路径")
private String component;
/**
* 元数据
*/
@Schema(description = "元数据")
private MetaVO meta;
/**
* 子路由列表
*/
@Schema(description = "子路由列表")
private List<RouteVO> children;
}

View File

@ -16,6 +16,10 @@
package top.charles7c.cnadmin.auth.service; package top.charles7c.cnadmin.auth.service;
import java.util.List;
import top.charles7c.cnadmin.auth.model.vo.RouteVO;
/** /**
* 登录业务接口 * 登录业务接口
* *
@ -34,4 +38,13 @@ public interface LoginService {
* @return 令牌 * @return 令牌
*/ */
String login(String username, String password); String login(String username, String password);
/**
* 构建路由树
*
* @param userId
* 用户 ID
* @return 路由树
*/
List<RouteVO> buildRouteTree(Long userId);
} }

View File

@ -16,23 +16,39 @@
package top.charles7c.cnadmin.auth.service.impl; package top.charles7c.cnadmin.auth.service.impl;
import java.util.ArrayList;
import java.util.List;
import java.util.Set;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import cn.dev33.satoken.stp.StpUtil; import cn.dev33.satoken.stp.StpUtil;
import cn.hutool.core.bean.BeanUtil; import cn.hutool.core.bean.BeanUtil;
import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.lang.tree.Tree;
import cn.hutool.core.lang.tree.TreeNodeConfig;
import top.charles7c.cnadmin.auth.model.vo.MetaVO;
import top.charles7c.cnadmin.auth.model.vo.RouteVO;
import top.charles7c.cnadmin.auth.service.LoginService; import top.charles7c.cnadmin.auth.service.LoginService;
import top.charles7c.cnadmin.auth.service.PermissionService; import top.charles7c.cnadmin.auth.service.PermissionService;
import top.charles7c.cnadmin.common.annotation.TreeField;
import top.charles7c.cnadmin.common.constant.SysConsts;
import top.charles7c.cnadmin.common.enums.DisEnableStatusEnum; import top.charles7c.cnadmin.common.enums.DisEnableStatusEnum;
import top.charles7c.cnadmin.common.enums.MenuTypeEnum;
import top.charles7c.cnadmin.common.model.dto.LoginUser; import top.charles7c.cnadmin.common.model.dto.LoginUser;
import top.charles7c.cnadmin.common.util.ExceptionUtils; import top.charles7c.cnadmin.common.util.ExceptionUtils;
import top.charles7c.cnadmin.common.util.SecureUtils; import top.charles7c.cnadmin.common.util.SecureUtils;
import top.charles7c.cnadmin.common.util.TreeUtils;
import top.charles7c.cnadmin.common.util.helper.LoginHelper; import top.charles7c.cnadmin.common.util.helper.LoginHelper;
import top.charles7c.cnadmin.common.util.validate.CheckUtils; import top.charles7c.cnadmin.common.util.validate.CheckUtils;
import top.charles7c.cnadmin.system.model.entity.UserDO; import top.charles7c.cnadmin.system.model.entity.UserDO;
import top.charles7c.cnadmin.system.model.query.MenuQuery;
import top.charles7c.cnadmin.system.model.vo.MenuVO;
import top.charles7c.cnadmin.system.service.DeptService; import top.charles7c.cnadmin.system.service.DeptService;
import top.charles7c.cnadmin.system.service.MenuService;
import top.charles7c.cnadmin.system.service.RoleService; import top.charles7c.cnadmin.system.service.RoleService;
import top.charles7c.cnadmin.system.service.UserService; import top.charles7c.cnadmin.system.service.UserService;
@ -49,6 +65,7 @@ public class LoginServiceImpl implements LoginService {
private final UserService userService; private final UserService userService;
private final DeptService deptService; private final DeptService deptService;
private final RoleService roleService; private final RoleService roleService;
private final MenuService menuService;
private final PermissionService permissionService; private final PermissionService permissionService;
@Override @Override
@ -70,4 +87,42 @@ public class LoginServiceImpl implements LoginService {
// 返回令牌 // 返回令牌
return StpUtil.getTokenValue(); return StpUtil.getTokenValue();
} }
@Override
public List<RouteVO> buildRouteTree(Long userId) {
Set<String> roleSet = permissionService.listRoleCodeByUserId(userId);
if (CollUtil.isEmpty(roleSet)) {
return new ArrayList<>(0);
}
// 查询菜单列表
List<MenuVO> menuList;
if (roleSet.contains(SysConsts.ADMIN_ROLE_CODE)) {
MenuQuery menuQuery = new MenuQuery();
menuQuery.setStatus(DisEnableStatusEnum.ENABLE.getValue());
menuList = menuService.list(menuQuery, null);
} else {
menuList = menuService.listByUserId(userId);
}
menuList.removeIf(m -> MenuTypeEnum.BUTTON.equals(m.getType()));
// 构建路由树
TreeField treeField = MenuVO.class.getDeclaredAnnotation(TreeField.class);
TreeNodeConfig treeNodeConfig = TreeUtils.genTreeNodeConfig(treeField);
List<Tree<Long>> treeList = TreeUtils.build(menuList, treeNodeConfig, (m, tree) -> {
tree.setId(m.getId());
tree.setParentId(m.getParentId());
tree.setName(m.getTitle());
tree.setWeight(m.getSort());
tree.putExtra("path", m.getPath());
tree.putExtra("name", m.getName());
tree.putExtra("component", m.getComponent());
MetaVO metaVO = new MetaVO();
metaVO.setLocale(m.getTitle());
metaVO.setIcon(m.getIcon());
tree.putExtra("meta", metaVO);
});
return BeanUtil.copyToList(treeList, RouteVO.class);
}
} }

View File

@ -16,6 +16,7 @@
package top.charles7c.cnadmin.system.mapper; package top.charles7c.cnadmin.system.mapper;
import java.util.List;
import java.util.Set; import java.util.Set;
import org.apache.ibatis.annotations.Param; import org.apache.ibatis.annotations.Param;
@ -39,4 +40,13 @@ public interface MenuMapper extends BaseMapper<MenuDO> {
* @return 权限码集合 * @return 权限码集合
*/ */
Set<String> selectPermissionByUserId(@Param("userId") Long userId); Set<String> selectPermissionByUserId(@Param("userId") Long userId);
/**
* 根据用户 ID 查询
*
* @param userId
* 用户 ID
* @return 菜单列表
*/
List<MenuDO> selectListByUserId(@Param("userId") Long userId);
} }

View File

@ -16,6 +16,7 @@
package top.charles7c.cnadmin.system.service; package top.charles7c.cnadmin.system.service;
import java.util.List;
import java.util.Set; import java.util.Set;
import top.charles7c.cnadmin.common.base.BaseService; import top.charles7c.cnadmin.common.base.BaseService;
@ -39,4 +40,13 @@ public interface MenuService extends BaseService<MenuVO, MenuVO, MenuQuery, Menu
* @return 权限码集合 * @return 权限码集合
*/ */
Set<String> listPermissionByUserId(Long userId); Set<String> listPermissionByUserId(Long userId);
/**
* 根据用户 ID 查询
*
* @param userId
* 用户 ID
* @return 菜单列表
*/
List<MenuVO> listByUserId(Long userId);
} }

View File

@ -23,6 +23,8 @@ import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional; import org.springframework.transaction.annotation.Transactional;
import cn.hutool.core.bean.BeanUtil;
import top.charles7c.cnadmin.common.base.BaseServiceImpl; import top.charles7c.cnadmin.common.base.BaseServiceImpl;
import top.charles7c.cnadmin.common.enums.DisEnableStatusEnum; import top.charles7c.cnadmin.common.enums.DisEnableStatusEnum;
import top.charles7c.cnadmin.common.util.validate.CheckUtils; import top.charles7c.cnadmin.common.util.validate.CheckUtils;
@ -77,6 +79,14 @@ public class MenuServiceImpl extends BaseServiceImpl<MenuMapper, MenuDO, MenuVO,
return baseMapper.selectPermissionByUserId(userId); return baseMapper.selectPermissionByUserId(userId);
} }
@Override
public List<MenuVO> listByUserId(Long userId) {
List<MenuDO> menuList = baseMapper.selectListByUserId(userId);
List<MenuVO> list = BeanUtil.copyToList(menuList, MenuVO.class);
list.forEach(this::fill);
return list;
}
/** /**
* 检查名称是否存在 * 检查名称是否存在
* *

View File

@ -13,4 +13,16 @@
AND m.`status` = 1 AND m.`status` = 1
AND r.`status` = 1 AND r.`status` = 1
</select> </select>
<select id="selectListByUserId" resultType="top.charles7c.cnadmin.system.model.entity.MenuDO">
SELECT m.*
FROM `sys_menu` as m
LEFT JOIN `sys_role_menu` as rm ON rm.`menu_id` = m.`id`
LEFT JOIN `sys_role` as r ON r.`id` = rm.`role_id`
LEFT JOIN `sys_user_role` as sur ON sur.`role_id` = r.`id`
LEFT JOIN `sys_user` as u ON u.`id` = sur.`user_id`
WHERE u.`id` = #{userId}
AND m.`status` = 1
AND r.`status` = 1
</select>
</mapper> </mapper>

View File

@ -27,6 +27,6 @@ export function getUserInfo() {
return axios.get<UserState>(`${BASE_URL}/user/info`); return axios.get<UserState>(`${BASE_URL}/user/info`);
} }
export function getMenuList() { export function listRoute() {
return axios.get<RouteRecordNormalized[]>('/api/user/menu'); return axios.get<RouteRecordNormalized[]>(`${BASE_URL}/route`);
} }

View File

@ -95,7 +95,7 @@
_route.forEach((element) => { _route.forEach((element) => {
// This is demo, modify nodes as needed // This is demo, modify nodes as needed
const icon = element?.meta?.icon const icon = element?.meta?.icon
? () => h(compile(`<${element?.meta?.icon}/>`)) ? () => h(compile(`<icon-${element?.meta?.icon}/>`))
: null; : null;
const node = const node =
element?.children && element?.children.length !== 0 ? ( element?.children && element?.children.length !== 0 ? (

View File

@ -3,7 +3,7 @@ export default {
name: 'ArcoWebsite', name: 'ArcoWebsite',
meta: { meta: {
locale: 'menu.arcoWebsite', locale: 'menu.arcoWebsite',
icon: 'icon-link', icon: 'link',
requiresAuth: true, requiresAuth: true,
order: 106, order: 106,
}, },

View File

@ -3,7 +3,7 @@ export default {
name: 'GitHub', name: 'GitHub',
meta: { meta: {
locale: 'menu.github', locale: 'menu.github',
icon: 'icon-github', icon: 'github',
requiresAuth: true, requiresAuth: true,
order: 107, order: 107,
}, },

View File

@ -8,7 +8,7 @@ const EXCEPTION: AppRouteRecordRaw = {
meta: { meta: {
locale: 'menu.exception', locale: 'menu.exception',
requiresAuth: true, requiresAuth: true,
icon: 'icon-exclamation-circle', icon: 'exclamation-circle',
order: 104, order: 104,
}, },
children: [ children: [

View File

@ -7,7 +7,7 @@ const FORM: AppRouteRecordRaw = {
component: DEFAULT_LAYOUT, component: DEFAULT_LAYOUT,
meta: { meta: {
locale: 'menu.form', locale: 'menu.form',
icon: 'icon-bookmark', icon: 'bookmark',
requiresAuth: true, requiresAuth: true,
order: 101, order: 101,
}, },

View File

@ -8,7 +8,7 @@ const LIST: AppRouteRecordRaw = {
meta: { meta: {
locale: 'menu.list', locale: 'menu.list',
requiresAuth: true, requiresAuth: true,
icon: 'icon-list', icon: 'list',
order: 100, order: 100,
}, },
children: [ children: [

View File

@ -8,7 +8,7 @@ const PROFILE: AppRouteRecordRaw = {
meta: { meta: {
locale: 'menu.profile', locale: 'menu.profile',
requiresAuth: true, requiresAuth: true,
icon: 'icon-file', icon: 'file',
order: 102, order: 102,
}, },
children: [ children: [

View File

@ -7,7 +7,7 @@ const RESULT: AppRouteRecordRaw = {
component: DEFAULT_LAYOUT, component: DEFAULT_LAYOUT,
meta: { meta: {
locale: 'menu.result', locale: 'menu.result',
icon: 'icon-check-circle', icon: 'check-circle',
requiresAuth: true, requiresAuth: true,
order: 103, order: 103,
}, },

View File

@ -8,7 +8,7 @@ const VISUALIZATION: AppRouteRecordRaw = {
meta: { meta: {
locale: 'menu.visualization', locale: 'menu.visualization',
requiresAuth: true, requiresAuth: true,
icon: 'icon-bar-chart', icon: 'bar-chart',
order: 105, order: 105,
}, },
children: [ children: [

View File

@ -9,7 +9,7 @@ const DASHBOARD: AppRouteRecordRaw = {
meta: { meta: {
locale: 'menu.dashboard', locale: 'menu.dashboard',
requiresAuth: true, requiresAuth: true,
icon: 'icon-dashboard', icon: 'dashboard',
order: 0, order: 0,
hideChildrenInMenu: true, hideChildrenInMenu: true,
}, },

View File

@ -7,7 +7,7 @@ const Monitor: AppRouteRecordRaw = {
component: DEFAULT_LAYOUT, component: DEFAULT_LAYOUT,
meta: { meta: {
locale: 'menu.monitor', locale: 'menu.monitor',
icon: 'icon-computer', icon: 'computer',
requiresAuth: true, requiresAuth: true,
order: 2, order: 2,
}, },

View File

@ -7,7 +7,7 @@ const System: AppRouteRecordRaw = {
component: DEFAULT_LAYOUT, component: DEFAULT_LAYOUT,
meta: { meta: {
locale: 'menu.system', locale: 'menu.system',
icon: 'icon-settings', icon: 'settings',
requiresAuth: true, requiresAuth: true,
order: 1, order: 1,
}, },

View File

@ -7,7 +7,7 @@ const UserCenter: AppRouteRecordRaw = {
component: DEFAULT_LAYOUT, component: DEFAULT_LAYOUT,
meta: { meta: {
locale: 'menu.user', locale: 'menu.user',
icon: 'icon-user', icon: 'user',
requiresAuth: true, requiresAuth: true,
}, },
children: [ children: [

View File

@ -3,7 +3,7 @@ import { Notification } from '@arco-design/web-vue';
import type { NotificationReturn } from '@arco-design/web-vue/es/notification/interface'; import type { NotificationReturn } from '@arco-design/web-vue/es/notification/interface';
import type { RouteRecordNormalized } from 'vue-router'; import type { RouteRecordNormalized } from 'vue-router';
import defaultSettings from '@/config/settings.json'; import defaultSettings from '@/config/settings.json';
import { getMenuList } from '@/api/auth/login'; import { listRoute } from '@/api/auth/login';
import { AppState } from './types'; import { AppState } from './types';
const useAppStore = defineStore('app', { const useAppStore = defineStore('app', {
@ -52,7 +52,7 @@ const useAppStore = defineStore('app', {
content: 'loading', content: 'loading',
closable: true, closable: true,
}); });
const { data } = await getMenuList(); const { data } = await listRoute();
this.serverMenu = data; this.serverMenu = data;
notifyInstance = Notification.success({ notifyInstance = Notification.success({
id: 'menuNotice', id: 'menuNotice',

View File

@ -16,6 +16,8 @@
package top.charles7c.cnadmin.webapi.controller.auth; package top.charles7c.cnadmin.webapi.controller.auth;
import java.util.List;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Operation;
@ -32,6 +34,7 @@ import cn.hutool.core.bean.BeanUtil;
import top.charles7c.cnadmin.auth.model.request.LoginRequest; import top.charles7c.cnadmin.auth.model.request.LoginRequest;
import top.charles7c.cnadmin.auth.model.vo.LoginVO; import top.charles7c.cnadmin.auth.model.vo.LoginVO;
import top.charles7c.cnadmin.auth.model.vo.RouteVO;
import top.charles7c.cnadmin.auth.model.vo.UserInfoVO; import top.charles7c.cnadmin.auth.model.vo.UserInfoVO;
import top.charles7c.cnadmin.auth.service.LoginService; import top.charles7c.cnadmin.auth.service.LoginService;
import top.charles7c.cnadmin.common.constant.CacheConsts; import top.charles7c.cnadmin.common.constant.CacheConsts;
@ -93,4 +96,12 @@ public class LoginController {
UserInfoVO userInfoVO = BeanUtil.copyProperties(loginUser, UserInfoVO.class); UserInfoVO userInfoVO = BeanUtil.copyProperties(loginUser, UserInfoVO.class);
return R.ok(userInfoVO); return R.ok(userInfoVO);
} }
@Operation(summary = "获取路由信息", description = "获取登录用户的路由信息")
@GetMapping("/route")
public R<List<RouteVO>> listMenu() {
Long userId = LoginHelper.getUserId();
List<RouteVO> routeTree = loginService.buildRouteTree(userId);
return R.ok(routeTree);
}
} }