新增:新增获取路由信息 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;
import java.util.List;
import top.charles7c.cnadmin.auth.model.vo.RouteVO;
/**
* 登录业务接口
*
@ -34,4 +38,13 @@ public interface LoginService {
* @return 令牌
*/
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;
import java.util.ArrayList;
import java.util.List;
import java.util.Set;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import cn.dev33.satoken.stp.StpUtil;
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.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.MenuTypeEnum;
import top.charles7c.cnadmin.common.model.dto.LoginUser;
import top.charles7c.cnadmin.common.util.ExceptionUtils;
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.validate.CheckUtils;
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.MenuService;
import top.charles7c.cnadmin.system.service.RoleService;
import top.charles7c.cnadmin.system.service.UserService;
@ -49,6 +65,7 @@ public class LoginServiceImpl implements LoginService {
private final UserService userService;
private final DeptService deptService;
private final RoleService roleService;
private final MenuService menuService;
private final PermissionService permissionService;
@Override
@ -70,4 +87,42 @@ public class LoginServiceImpl implements LoginService {
// 返回令牌
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;
import java.util.List;
import java.util.Set;
import org.apache.ibatis.annotations.Param;
@ -39,4 +40,13 @@ public interface MenuMapper extends BaseMapper<MenuDO> {
* @return 权限码集合
*/
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;
import java.util.List;
import java.util.Set;
import top.charles7c.cnadmin.common.base.BaseService;
@ -39,4 +40,13 @@ public interface MenuService extends BaseService<MenuVO, MenuVO, MenuQuery, Menu
* @return 权限码集合
*/
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.transaction.annotation.Transactional;
import cn.hutool.core.bean.BeanUtil;
import top.charles7c.cnadmin.common.base.BaseServiceImpl;
import top.charles7c.cnadmin.common.enums.DisEnableStatusEnum;
import top.charles7c.cnadmin.common.util.validate.CheckUtils;
@ -77,6 +79,14 @@ public class MenuServiceImpl extends BaseServiceImpl<MenuMapper, MenuDO, MenuVO,
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 r.`status` = 1
</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>

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -7,7 +7,7 @@ const UserCenter: AppRouteRecordRaw = {
component: DEFAULT_LAYOUT,
meta: {
locale: 'menu.user',
icon: 'icon-user',
icon: 'user',
requiresAuth: true,
},
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 { RouteRecordNormalized } from 'vue-router';
import defaultSettings from '@/config/settings.json';
import { getMenuList } from '@/api/auth/login';
import { listRoute } from '@/api/auth/login';
import { AppState } from './types';
const useAppStore = defineStore('app', {
@ -52,7 +52,7 @@ const useAppStore = defineStore('app', {
content: 'loading',
closable: true,
});
const { data } = await getMenuList();
const { data } = await listRoute();
this.serverMenu = data;
notifyInstance = Notification.success({
id: 'menuNotice',

View File

@ -16,6 +16,8 @@
package top.charles7c.cnadmin.webapi.controller.auth;
import java.util.List;
import lombok.RequiredArgsConstructor;
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.vo.LoginVO;
import top.charles7c.cnadmin.auth.model.vo.RouteVO;
import top.charles7c.cnadmin.auth.model.vo.UserInfoVO;
import top.charles7c.cnadmin.auth.service.LoginService;
import top.charles7c.cnadmin.common.constant.CacheConsts;
@ -93,4 +96,12 @@ public class LoginController {
UserInfoVO userInfoVO = BeanUtil.copyProperties(loginUser, UserInfoVO.class);
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);
}
}