feat: 角色菜单授权与权限同步
Made-with: Cursor
This commit is contained in:
parent
dba6def3c5
commit
b20c00bea3
@ -49,7 +49,7 @@ public class MenuController {
|
||||
|
||||
@GetMapping("/user-menus")
|
||||
@Operation(summary = "当前用户菜单")
|
||||
@PreAuthorize("hasAuthority('menu:read')")
|
||||
@PreAuthorize("isAuthenticated()")
|
||||
public Result<List<MenuTreeVO>> userMenus(
|
||||
@AuthenticationPrincipal UserPrincipal userPrincipal) {
|
||||
Long userId = userPrincipal.getUserId();
|
||||
|
||||
@ -66,8 +66,11 @@ public class RoleController {
|
||||
@GetMapping("/{id}")
|
||||
@Operation(summary = "角色详情")
|
||||
@PreAuthorize("hasAuthority('role:read')")
|
||||
public Result<RoleDetailVO> detail(@PathVariable Long id) {
|
||||
RoleDetailVO result = roleService.detail(id);
|
||||
public Result<RoleDetailVO> detail(
|
||||
@AuthenticationPrincipal UserPrincipal userPrincipal,
|
||||
@PathVariable Long id) {
|
||||
Long tenantId = userPrincipal.getTenantId();
|
||||
RoleDetailVO result = roleService.detail(id, tenantId);
|
||||
return Result.success(result);
|
||||
}
|
||||
|
||||
@ -87,8 +90,10 @@ public class RoleController {
|
||||
@Operation(summary = "删除角色")
|
||||
@PreAuthorize("hasAuthority('role:delete')")
|
||||
public Result<Void> delete(
|
||||
@AuthenticationPrincipal UserPrincipal userPrincipal,
|
||||
@PathVariable Long id) {
|
||||
roleService.delete(id);
|
||||
Long tenantId = userPrincipal.getTenantId();
|
||||
roleService.delete(id, tenantId);
|
||||
return Result.success();
|
||||
}
|
||||
}
|
||||
|
||||
@ -26,4 +26,7 @@ public class CreateRoleDTO {
|
||||
|
||||
@Schema(description = "权限 ID 列表")
|
||||
private List<Long> permissionIds;
|
||||
|
||||
@Schema(description = "菜单 ID 列表(菜单可见性授权)")
|
||||
private List<Long> menuIds;
|
||||
}
|
||||
|
||||
@ -23,4 +23,7 @@ public class UpdateRoleDTO {
|
||||
|
||||
@Schema(description = "权限 ID 列表")
|
||||
private java.util.List<Long> permissionIds;
|
||||
|
||||
@Schema(description = "菜单 ID 列表(菜单可见性授权)")
|
||||
private java.util.List<Long> menuIds;
|
||||
}
|
||||
|
||||
@ -0,0 +1,39 @@
|
||||
package com.lesingle.creation.entity;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.IdType;
|
||||
import com.baomidou.mybatisplus.annotation.TableField;
|
||||
import com.baomidou.mybatisplus.annotation.TableId;
|
||||
import com.baomidou.mybatisplus.annotation.TableName;
|
||||
import lombok.Data;
|
||||
|
||||
import java.io.Serializable;
|
||||
|
||||
/**
|
||||
* 角色-菜单关联表实体类
|
||||
* 中间表,不使用逻辑删除和审计字段
|
||||
*/
|
||||
@Data
|
||||
@TableName("t_auth_role_menu")
|
||||
public class RoleMenu implements Serializable {
|
||||
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
/**
|
||||
* 主键 ID
|
||||
*/
|
||||
@TableId(type = IdType.AUTO)
|
||||
private Long id;
|
||||
|
||||
/**
|
||||
* 角色 ID
|
||||
*/
|
||||
@TableField("role_id")
|
||||
private Long roleId;
|
||||
|
||||
/**
|
||||
* 菜单 ID
|
||||
*/
|
||||
@TableField("menu_id")
|
||||
private Long menuId;
|
||||
}
|
||||
|
||||
@ -0,0 +1,13 @@
|
||||
package com.lesingle.creation.mapper;
|
||||
|
||||
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||
import com.lesingle.creation.entity.RoleMenu;
|
||||
import org.apache.ibatis.annotations.Mapper;
|
||||
|
||||
/**
|
||||
* 角色-菜单关联 Mapper 接口
|
||||
*/
|
||||
@Mapper
|
||||
public interface RoleMenuMapper extends BaseMapper<RoleMenu> {
|
||||
}
|
||||
|
||||
@ -39,9 +39,10 @@ public interface RoleService extends IService<Role> {
|
||||
* 根据 ID 查询角色详情
|
||||
*
|
||||
* @param id 角色 ID
|
||||
* @param tenantId 租户 ID
|
||||
* @return 角色详情 VO
|
||||
*/
|
||||
RoleDetailVO detail(Long id);
|
||||
RoleDetailVO detail(Long id, Long tenantId);
|
||||
|
||||
/**
|
||||
* 更新角色
|
||||
@ -57,8 +58,9 @@ public interface RoleService extends IService<Role> {
|
||||
* 删除角色
|
||||
*
|
||||
* @param id 角色 ID
|
||||
* @param tenantId 租户 ID
|
||||
*/
|
||||
void delete(Long id);
|
||||
void delete(Long id, Long tenantId);
|
||||
|
||||
/**
|
||||
* 获取所有角色列表
|
||||
|
||||
@ -17,8 +17,12 @@ import org.springframework.transaction.annotation.Transactional;
|
||||
import org.springframework.util.StringUtils;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
import java.util.Comparator;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
@ -33,6 +37,8 @@ public class MenuServiceImpl extends ServiceImpl<MenuMapper, Menu> implements Me
|
||||
private final UserRoleMapper userRoleMapper;
|
||||
private final RolePermissionMapper rolePermissionMapper;
|
||||
private final PermissionMapper permissionMapper;
|
||||
private final RoleMenuMapper roleMenuMapper;
|
||||
private final RoleMapper roleMapper;
|
||||
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
@ -93,30 +99,31 @@ public class MenuServiceImpl extends ServiceImpl<MenuMapper, Menu> implements Me
|
||||
.map(UserRole::getRoleId)
|
||||
.collect(Collectors.toList());
|
||||
|
||||
// 3. 查询角色关联的权限
|
||||
List<RolePermission> rolePermissions = rolePermissionMapper.selectList(
|
||||
new LambdaQueryWrapper<RolePermission>()
|
||||
.in(RolePermission::getRoleId, roleIds)
|
||||
);
|
||||
|
||||
if (rolePermissions.isEmpty()) {
|
||||
// 角色没有关联权限,返回空菜单
|
||||
log.warn("用户角色没有关联任何权限,用户 ID: {}", userId);
|
||||
return new ArrayList<>();
|
||||
// 3. 判断是否超级管理员(角色 code=super_admin 或权限 code=super_admin 兜底)
|
||||
boolean hasSuperAdmin = false;
|
||||
List<Role> roles = roleMapper.selectBatchIds(roleIds);
|
||||
if (roles != null) {
|
||||
hasSuperAdmin = roles.stream().anyMatch(r -> "super_admin".equals(r.getCode()));
|
||||
}
|
||||
|
||||
// 4. 获取权限 ID 列表
|
||||
List<Long> permissionIds = rolePermissions.stream()
|
||||
.map(RolePermission::getPermissionId)
|
||||
.distinct()
|
||||
.collect(Collectors.toList());
|
||||
if (!hasSuperAdmin) {
|
||||
// 兜底:如果权限中包含 super_admin,也视为超管
|
||||
List<RolePermission> rolePermissionsForSuper = rolePermissionMapper.selectList(
|
||||
new LambdaQueryWrapper<RolePermission>()
|
||||
.in(RolePermission::getRoleId, roleIds)
|
||||
);
|
||||
if (!rolePermissionsForSuper.isEmpty()) {
|
||||
List<Long> permissionIds = rolePermissionsForSuper.stream()
|
||||
.map(RolePermission::getPermissionId)
|
||||
.distinct()
|
||||
.collect(Collectors.toList());
|
||||
List<Permission> permissions = permissionMapper.selectBatchIds(permissionIds);
|
||||
hasSuperAdmin = permissions != null && permissions.stream()
|
||||
.anyMatch(p -> "super_admin".equals(p.getCode()));
|
||||
}
|
||||
}
|
||||
|
||||
// 5. 查询权限详情,检查是否有 super_admin 权限
|
||||
List<Permission> permissions = permissionMapper.selectBatchIds(permissionIds);
|
||||
boolean hasSuperAdmin = permissions.stream()
|
||||
.anyMatch(p -> "super_admin".equals(p.getCode()));
|
||||
|
||||
// 6. 查询菜单
|
||||
// 4. 查询菜单(超管返回所有;非超管按角色-菜单关联取并集 + 补齐祖先节点)
|
||||
LambdaQueryWrapper<Menu> wrapper = new LambdaQueryWrapper<>();
|
||||
wrapper.eq(Menu::getValidState, 1)
|
||||
.orderByAsc(Menu::getSort, Menu::getId);
|
||||
@ -127,24 +134,50 @@ public class MenuServiceImpl extends ServiceImpl<MenuMapper, Menu> implements Me
|
||||
log.info("用户是超级管理员,返回所有菜单");
|
||||
allMenus = menuMapper.selectList(wrapper);
|
||||
} else {
|
||||
// 非超管:只查询有权限的菜单
|
||||
// 获取有权限的菜单编码
|
||||
List<String> permissionCodes = permissions.stream()
|
||||
.map(Permission::getCode)
|
||||
.collect(Collectors.toList());
|
||||
|
||||
// 筛选条件:菜单权限为空(所有人都可见)或 菜单权限在用户权限列表中
|
||||
wrapper.and(w ->
|
||||
w.isNull(Menu::getPermission)
|
||||
.or()
|
||||
.in(Menu::getPermission, permissionCodes)
|
||||
// 非超管:按角色-菜单关联表控制可见性
|
||||
List<RoleMenu> roleMenus = roleMenuMapper.selectList(
|
||||
new LambdaQueryWrapper<RoleMenu>()
|
||||
.in(RoleMenu::getRoleId, roleIds)
|
||||
);
|
||||
if (roleMenus.isEmpty()) {
|
||||
log.warn("用户角色未配置任何菜单可见性,用户 ID: {}", userId);
|
||||
return new ArrayList<>();
|
||||
}
|
||||
|
||||
log.info("非超管用户,根据权限过滤菜单,权限编码数量:{}", permissionCodes.size());
|
||||
allMenus = menuMapper.selectList(wrapper);
|
||||
Set<Long> visibleMenuIds = roleMenus.stream()
|
||||
.map(RoleMenu::getMenuId)
|
||||
.collect(Collectors.toSet());
|
||||
|
||||
// 拉取全量菜单用于补齐祖先节点,然后再裁剪
|
||||
List<Menu> all = menuMapper.selectList(wrapper);
|
||||
Map<Long, Menu> menuMap = new HashMap<>();
|
||||
for (Menu m : all) {
|
||||
menuMap.put(m.getId(), m);
|
||||
}
|
||||
|
||||
Set<Long> withAncestors = new HashSet<>(visibleMenuIds);
|
||||
for (Long mid : visibleMenuIds) {
|
||||
Menu cur = menuMap.get(mid);
|
||||
while (cur != null) {
|
||||
Long pid = cur.getParentId();
|
||||
if (pid == null || pid == 0L) {
|
||||
break;
|
||||
}
|
||||
if (withAncestors.add(pid)) {
|
||||
cur = menuMap.get(pid);
|
||||
} else {
|
||||
// 已存在,继续向上也可能已处理过
|
||||
cur = menuMap.get(pid);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
allMenus = all.stream()
|
||||
.filter(m -> withAncestors.contains(m.getId()))
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
|
||||
// 7. 构建树形结构
|
||||
// 5. 构建树形结构
|
||||
return buildTree(allMenus, 0L);
|
||||
}
|
||||
|
||||
|
||||
@ -6,8 +6,12 @@ import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
|
||||
import com.lesingle.creation.common.exception.BusinessException;
|
||||
import com.lesingle.creation.dto.role.CreateRoleDTO;
|
||||
import com.lesingle.creation.dto.role.UpdateRoleDTO;
|
||||
import com.lesingle.creation.entity.Permission;
|
||||
import com.lesingle.creation.entity.Role;
|
||||
import com.lesingle.creation.entity.RoleMenu;
|
||||
import com.lesingle.creation.entity.RolePermission;
|
||||
import com.lesingle.creation.mapper.PermissionMapper;
|
||||
import com.lesingle.creation.mapper.RoleMenuMapper;
|
||||
import com.lesingle.creation.mapper.RoleMapper;
|
||||
import com.lesingle.creation.mapper.RolePermissionMapper;
|
||||
import com.lesingle.creation.service.RoleService;
|
||||
@ -33,6 +37,8 @@ public class RoleServiceImpl extends ServiceImpl<RoleMapper, Role> implements Ro
|
||||
|
||||
private final RoleMapper roleMapper;
|
||||
private final RolePermissionMapper rolePermissionMapper;
|
||||
private final PermissionMapper permissionMapper;
|
||||
private final RoleMenuMapper roleMenuMapper;
|
||||
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
@ -71,6 +77,22 @@ public class RoleServiceImpl extends ServiceImpl<RoleMapper, Role> implements Ro
|
||||
log.info("角色权限关联创建成功,权限数量:{}", dto.getPermissionIds().size());
|
||||
}
|
||||
|
||||
// 如果提供了菜单 ID,创建菜单关联(用于菜单可见性授权)
|
||||
if (!CollectionUtils.isEmpty(dto.getMenuIds())) {
|
||||
List<RoleMenu> roleMenus = dto.getMenuIds().stream()
|
||||
.distinct()
|
||||
.map(menuId -> {
|
||||
RoleMenu rm = new RoleMenu();
|
||||
rm.setRoleId(role.getId());
|
||||
rm.setMenuId(menuId);
|
||||
return rm;
|
||||
})
|
||||
.collect(Collectors.toList());
|
||||
// 批量插入:使用 MyBatis-Plus 的 insert 循环即可
|
||||
roleMenus.forEach(roleMenuMapper::insert);
|
||||
log.info("角色菜单关联创建成功,菜单数量:{}", roleMenus.size());
|
||||
}
|
||||
|
||||
return convertToDetailVO(role);
|
||||
}
|
||||
|
||||
@ -98,13 +120,16 @@ public class RoleServiceImpl extends ServiceImpl<RoleMapper, Role> implements Ro
|
||||
}
|
||||
|
||||
@Override
|
||||
public RoleDetailVO detail(Long id) {
|
||||
public RoleDetailVO detail(Long id, Long tenantId) {
|
||||
log.info("查询角色详情,角色 ID: {}", id);
|
||||
|
||||
Role role = roleMapper.selectById(id);
|
||||
if (role == null) {
|
||||
throw new BusinessException("角色不存在");
|
||||
}
|
||||
if (tenantId != null && role.getTenantId() != null && !role.getTenantId().equals(tenantId)) {
|
||||
throw new BusinessException("角色不存在或不属于当前租户");
|
||||
}
|
||||
|
||||
return convertToDetailVO(role);
|
||||
}
|
||||
@ -118,6 +143,9 @@ public class RoleServiceImpl extends ServiceImpl<RoleMapper, Role> implements Ro
|
||||
if (existingRole == null) {
|
||||
throw new BusinessException("角色不存在");
|
||||
}
|
||||
if (existingRole.getTenantId() != null && tenantId != null && !existingRole.getTenantId().equals(tenantId)) {
|
||||
throw new BusinessException("角色不存在或不属于当前租户");
|
||||
}
|
||||
|
||||
// 如果更新了 code,检查是否冲突
|
||||
if (dto.getCode() != null && !dto.getCode().isEmpty()) {
|
||||
@ -160,18 +188,42 @@ public class RoleServiceImpl extends ServiceImpl<RoleMapper, Role> implements Ro
|
||||
}
|
||||
}
|
||||
|
||||
// 如果提供了 menuIds,更新菜单关联(菜单可见性授权)
|
||||
if (dto.getMenuIds() != null) {
|
||||
LambdaQueryWrapper<RoleMenu> rmWrapper = new LambdaQueryWrapper<>();
|
||||
rmWrapper.eq(RoleMenu::getRoleId, id);
|
||||
roleMenuMapper.delete(rmWrapper);
|
||||
|
||||
if (!dto.getMenuIds().isEmpty()) {
|
||||
List<RoleMenu> roleMenus = dto.getMenuIds().stream()
|
||||
.distinct()
|
||||
.map(menuId -> {
|
||||
RoleMenu rm = new RoleMenu();
|
||||
rm.setRoleId(id);
|
||||
rm.setMenuId(menuId);
|
||||
return rm;
|
||||
})
|
||||
.collect(Collectors.toList());
|
||||
// 批量插入(使用 MyBatis-Plus 默认 insert 循环也可)
|
||||
roleMenus.forEach(roleMenuMapper::insert);
|
||||
}
|
||||
}
|
||||
|
||||
return convertToDetailVO(roleMapper.selectById(id));
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public void delete(Long id) {
|
||||
public void delete(Long id, Long tenantId) {
|
||||
log.info("删除角色,角色 ID: {}", id);
|
||||
|
||||
Role role = roleMapper.selectById(id);
|
||||
if (role == null) {
|
||||
throw new BusinessException("角色不存在");
|
||||
}
|
||||
if (role.getTenantId() != null && tenantId != null && !role.getTenantId().equals(tenantId)) {
|
||||
throw new BusinessException("角色不存在或不属于当前租户");
|
||||
}
|
||||
|
||||
// 删除角色关联
|
||||
LambdaQueryWrapper<RolePermission> wrapper = new LambdaQueryWrapper<>();
|
||||
@ -215,9 +267,41 @@ public class RoleServiceImpl extends ServiceImpl<RoleMapper, Role> implements Ro
|
||||
vo.setUpdateBy(role.getUpdateBy());
|
||||
vo.setUpdateTime(role.getUpdateTime());
|
||||
|
||||
// TODO: 获取角色权限
|
||||
vo.setPermissionIds(new ArrayList<>());
|
||||
vo.setPermissionNames(new ArrayList<>());
|
||||
// 获取角色权限(用于授权回显)
|
||||
LambdaQueryWrapper<RolePermission> rpWrapper = new LambdaQueryWrapper<>();
|
||||
rpWrapper.eq(RolePermission::getRoleId, role.getId());
|
||||
List<RolePermission> rolePermissions = rolePermissionMapper.selectList(rpWrapper);
|
||||
List<Long> permissionIds = rolePermissions == null
|
||||
? new ArrayList<>()
|
||||
: rolePermissions.stream()
|
||||
.map(RolePermission::getPermissionId)
|
||||
.distinct()
|
||||
.collect(Collectors.toList());
|
||||
vo.setPermissionIds(permissionIds);
|
||||
|
||||
if (permissionIds.isEmpty()) {
|
||||
vo.setPermissionNames(new ArrayList<>());
|
||||
} else {
|
||||
List<Permission> permissions = permissionMapper.selectBatchIds(permissionIds);
|
||||
List<String> names = permissions == null
|
||||
? new ArrayList<>()
|
||||
: permissions.stream()
|
||||
.map(Permission::getName)
|
||||
.collect(Collectors.toList());
|
||||
vo.setPermissionNames(names);
|
||||
}
|
||||
|
||||
// 获取角色菜单(用于菜单授权回显)
|
||||
LambdaQueryWrapper<RoleMenu> rmWrapper = new LambdaQueryWrapper<>();
|
||||
rmWrapper.eq(RoleMenu::getRoleId, role.getId());
|
||||
List<RoleMenu> roleMenus = roleMenuMapper.selectList(rmWrapper);
|
||||
List<Long> menuIds = roleMenus == null
|
||||
? new ArrayList<>()
|
||||
: roleMenus.stream()
|
||||
.map(RoleMenu::getMenuId)
|
||||
.distinct()
|
||||
.collect(Collectors.toList());
|
||||
vo.setMenuIds(menuIds);
|
||||
|
||||
return vo;
|
||||
}
|
||||
|
||||
@ -48,4 +48,7 @@ public class RoleDetailVO {
|
||||
|
||||
@Schema(description = "权限名称列表")
|
||||
private List<String> permissionNames;
|
||||
|
||||
@Schema(description = "菜单 ID 列表(用于菜单授权回显)")
|
||||
private List<Long> menuIds;
|
||||
}
|
||||
|
||||
@ -0,0 +1,25 @@
|
||||
-- ============================================
|
||||
-- 角色 - 菜单 关联表
|
||||
-- 用于“菜单可见性”授权(与按钮/接口权限分离)
|
||||
-- ============================================
|
||||
|
||||
CREATE TABLE IF NOT EXISTS `t_auth_role_menu` (
|
||||
`id` BIGINT NOT NULL AUTO_INCREMENT COMMENT '主键 ID',
|
||||
`role_id` BIGINT NOT NULL COMMENT '角色 ID',
|
||||
`menu_id` BIGINT NOT NULL COMMENT '菜单 ID',
|
||||
PRIMARY KEY (`id`),
|
||||
UNIQUE KEY `uk_role_menu` (`role_id`, `menu_id`),
|
||||
KEY `idx_role_id` (`role_id`),
|
||||
KEY `idx_menu_id` (`menu_id`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='角色-菜单关联表';
|
||||
|
||||
-- 初始化:为 tenant_id=1 的 super_admin 角色关联所有菜单(增量补齐)
|
||||
INSERT INTO `t_auth_role_menu` (`role_id`, `menu_id`)
|
||||
SELECT r.id, m.id
|
||||
FROM `t_auth_role` r
|
||||
JOIN `t_auth_menu` m ON m.deleted = 0
|
||||
WHERE r.tenant_id = 1
|
||||
AND r.code = 'super_admin'
|
||||
AND r.deleted = 0
|
||||
ON DUPLICATE KEY UPDATE `role_id` = VALUES(`role_id`);
|
||||
|
||||
@ -0,0 +1,26 @@
|
||||
-- ============================================
|
||||
-- 角色 - 菜单 关联表(补偿迁移)
|
||||
-- 说明:部分环境数据库 schema_version 已大于/等于 19,但实际未创建 t_auth_role_menu。
|
||||
-- 通过更高版本迁移确保表存在。
|
||||
-- ============================================
|
||||
|
||||
CREATE TABLE IF NOT EXISTS `t_auth_role_menu` (
|
||||
`id` BIGINT NOT NULL AUTO_INCREMENT COMMENT '主键 ID',
|
||||
`role_id` BIGINT NOT NULL COMMENT '角色 ID',
|
||||
`menu_id` BIGINT NOT NULL COMMENT '菜单 ID',
|
||||
PRIMARY KEY (`id`),
|
||||
UNIQUE KEY `uk_role_menu` (`role_id`, `menu_id`),
|
||||
KEY `idx_role_id` (`role_id`),
|
||||
KEY `idx_menu_id` (`menu_id`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='角色-菜单关联表';
|
||||
|
||||
-- 初始化:为 tenant_id=1 的 super_admin 角色关联所有菜单(增量补齐)
|
||||
INSERT INTO `t_auth_role_menu` (`role_id`, `menu_id`)
|
||||
SELECT r.id, m.id
|
||||
FROM `t_auth_role` r
|
||||
JOIN `t_auth_menu` m ON m.deleted = 0
|
||||
WHERE r.tenant_id = 1
|
||||
AND r.code = 'super_admin'
|
||||
AND r.deleted = 0
|
||||
ON DUPLICATE KEY UPDATE `role_id` = VALUES(`role_id`);
|
||||
|
||||
@ -0,0 +1,148 @@
|
||||
-- ============================================
|
||||
-- 补齐权限码 & 绑定超管全量权限(幂等)
|
||||
-- 目标:
|
||||
-- 1) 确保所有 Controller 上使用的 @PreAuthorize 权限码在 t_auth_permission 中存在
|
||||
-- 2) 确保 tenant_id=1 的 super_admin 角色拥有全部有效权限
|
||||
-- ============================================
|
||||
|
||||
-- 1. 补齐权限码(以 Controller 实际使用为准)
|
||||
INSERT INTO `t_auth_permission`
|
||||
(`tenant_id`, `name`, `code`, `resource`, `action`, `description`, `valid_state`)
|
||||
VALUES
|
||||
-- 菜单
|
||||
(1, '菜单创建', 'menu:create', 'menu', 'create', '创建菜单权限', 1),
|
||||
(1, '菜单查询', 'menu:read', 'menu', 'read', '查询菜单权限', 1),
|
||||
(1, '菜单更新', 'menu:update', 'menu', 'update', '更新菜单权限', 1),
|
||||
(1, '菜单删除', 'menu:delete', 'menu', 'delete', '删除菜单权限', 1),
|
||||
|
||||
-- 权限管理
|
||||
(1, '权限查询', 'permission:read', 'permission', 'read', '查询权限权限', 1),
|
||||
|
||||
-- 配置
|
||||
(1, '配置创建', 'config:create', 'config', 'create', '创建配置权限', 1),
|
||||
(1, '配置查询', 'config:read', 'config', 'read', '查询配置权限', 1),
|
||||
(1, '配置更新', 'config:update', 'config', 'update', '更新配置权限', 1),
|
||||
(1, '配置删除', 'config:delete', 'config', 'delete', '删除配置权限', 1),
|
||||
|
||||
-- 字典
|
||||
(1, '字典创建', 'dict:create', 'dict', 'create', '创建字典权限', 1),
|
||||
(1, '字典查询', 'dict:read', 'dict', 'read', '查询字典权限', 1),
|
||||
(1, '字典更新', 'dict:update', 'dict', 'update', '更新字典权限', 1),
|
||||
(1, '字典删除', 'dict:delete', 'dict', 'delete', '删除字典权限', 1),
|
||||
|
||||
-- 日志
|
||||
(1, '日志查询', 'log:read', 'log', 'read', '查询日志权限', 1),
|
||||
(1, '日志删除', 'log:delete', 'log', 'delete', '删除日志权限', 1),
|
||||
|
||||
-- 学校/组织/人员
|
||||
(1, '学校创建', 'school:create', 'school', 'create', '创建学校权限', 1),
|
||||
(1, '学校查询', 'school:read', 'school', 'read', '查询学校权限', 1),
|
||||
(1, '学校更新', 'school:update', 'school', 'update', '更新学校权限', 1),
|
||||
(1, '学校删除', 'school:delete', 'school', 'delete', '删除学校权限', 1),
|
||||
|
||||
(1, '部门创建', 'department:create', 'department', 'create', '创建部门权限', 1),
|
||||
(1, '部门查询', 'department:read', 'department', 'read', '查询部门权限', 1),
|
||||
(1, '部门更新', 'department:update', 'department', 'update', '更新部门权限', 1),
|
||||
(1, '部门删除', 'department:delete', 'department', 'delete', '删除部门权限', 1),
|
||||
|
||||
(1, '年级创建', 'grade:create', 'grade', 'create', '创建年级权限', 1),
|
||||
(1, '年级查询', 'grade:read', 'grade', 'read', '查询年级权限', 1),
|
||||
(1, '年级更新', 'grade:update', 'grade', 'update', '更新年级权限', 1),
|
||||
(1, '年级删除', 'grade:delete', 'grade', 'delete', '删除年级权限', 1),
|
||||
|
||||
(1, '班级创建', 'class:create', 'class', 'create', '创建班级权限', 1),
|
||||
(1, '班级查询', 'class:read', 'class', 'read', '查询班级权限', 1),
|
||||
(1, '班级更新', 'class:update', 'class', 'update', '更新班级权限', 1),
|
||||
(1, '班级删除', 'class:delete', 'class', 'delete', '删除班级权限', 1),
|
||||
|
||||
(1, '教师创建', 'teacher:create', 'teacher', 'create', '创建教师权限', 1),
|
||||
(1, '教师查询', 'teacher:read', 'teacher', 'read', '查询教师权限', 1),
|
||||
(1, '教师更新', 'teacher:update', 'teacher', 'update', '更新教师权限', 1),
|
||||
(1, '教师删除', 'teacher:delete', 'teacher', 'delete', '删除教师权限', 1),
|
||||
|
||||
(1, '学生创建', 'student:create', 'student', 'create', '创建学生权限', 1),
|
||||
(1, '学生查询', 'student:read', 'student', 'read', '查询学生权限', 1),
|
||||
(1, '学生更新', 'student:update', 'student', 'update', '更新学生权限', 1),
|
||||
(1, '学生删除', 'student:delete', 'student', 'delete', '删除学生权限', 1),
|
||||
|
||||
-- 活动
|
||||
(1, '活动创建', 'contest:create', 'contest', 'create', '创建活动权限', 1),
|
||||
(1, '活动查询', 'contest:read', 'contest', 'read', '查询活动权限', 1),
|
||||
(1, '活动更新', 'contest:update', 'contest', 'update', '更新活动权限', 1),
|
||||
(1, '活动发布', 'contest:publish', 'contest', 'publish', '发布活动权限', 1),
|
||||
(1, '活动删除', 'contest:delete', 'contest', 'delete', '删除活动权限', 1),
|
||||
|
||||
(1, '活动报名', 'contest:register', 'contest', 'register', '活动报名权限', 1),
|
||||
|
||||
-- 活动公告
|
||||
(1, '公告创建', 'contest:notice:create', 'contest', 'notice:create', '创建活动公告权限', 1),
|
||||
(1, '公告更新', 'contest:notice:update', 'contest', 'notice:update', '更新活动公告权限', 1),
|
||||
(1, '公告删除', 'contest:notice:delete', 'contest', 'notice:delete', '删除活动公告权限', 1),
|
||||
(1, '公告发布', 'contest:notice:publish', 'contest', 'notice:publish', '发布活动公告权限', 1),
|
||||
|
||||
-- 活动评委
|
||||
(1, '评委创建', 'contest:judge:create', 'contest', 'judge:create', '创建活动评委权限', 1),
|
||||
(1, '评委更新', 'contest:judge:update', 'contest', 'judge:update', '更新活动评委权限', 1),
|
||||
(1, '评委删除', 'contest:judge:delete', 'contest', 'judge:delete', '删除活动评委权限', 1),
|
||||
|
||||
-- 活动评审规则
|
||||
(1, '评审规则创建', 'contest:review-rule:create', 'contest', 'review-rule:create', '创建评审规则权限', 1),
|
||||
(1, '评审规则更新', 'contest:review-rule:update', 'contest', 'review-rule:update', '更新评审规则权限', 1),
|
||||
(1, '评审规则删除', 'contest:review-rule:delete', 'contest', 'review-rule:delete', '删除评审规则权限', 1),
|
||||
|
||||
-- 活动团队
|
||||
(1, '团队创建', 'contest:team:create', 'contest', 'team:create', '创建活动团队权限', 1),
|
||||
(1, '团队更新', 'contest:team:update', 'contest', 'team:update', '更新活动团队权限', 1),
|
||||
(1, '团队删除', 'contest:team:delete', 'contest', 'team:delete', '删除活动团队权限', 1),
|
||||
|
||||
-- 活动作业/作品
|
||||
(1, '作品提交', 'contest:work:submit', 'contest', 'work:submit', '提交作品权限', 1),
|
||||
(1, '作品更新', 'contest:work:update', 'contest', 'work:update', '更新作品权限', 1),
|
||||
(1, '作品删除', 'contest:work:delete', 'contest', 'work:delete', '删除作品权限', 1),
|
||||
|
||||
-- 活动评审
|
||||
(1, '评审分配', 'contest:review:assign', 'contest', 'review:assign', '分配评审权限', 1),
|
||||
(1, '评审评分', 'contest:review:score', 'contest', 'review:score', '评审评分权限', 1),
|
||||
|
||||
-- 活动评审结果
|
||||
(1, '结果创建', 'contest:result:create', 'contest', 'result:create', '创建评审结果权限', 1),
|
||||
(1, '结果更新', 'contest:result:update', 'contest', 'result:update', '更新评审结果权限', 1),
|
||||
(1, '结果发布', 'contest:result:publish', 'contest', 'result:publish', '发布评审结果权限', 1),
|
||||
(1, '结果删除', 'contest:result:delete', 'contest', 'result:delete', '删除评审结果权限', 1),
|
||||
|
||||
-- 预设评语
|
||||
(1, '预设评语创建', 'contest:preset-comment:create', 'contest', 'preset-comment:create', '创建预设评语权限', 1),
|
||||
(1, '预设评语删除', 'contest:preset-comment:delete', 'contest', 'preset-comment:delete', '删除预设评语权限', 1),
|
||||
|
||||
-- 作业(独立模块)
|
||||
(1, '作业创建', 'homework:create', 'homework', 'create', '创建作业权限', 1),
|
||||
(1, '作业更新', 'homework:update', 'homework', 'update', '更新作业权限', 1),
|
||||
(1, '作业发布', 'homework:publish', 'homework', 'publish', '发布作业权限', 1),
|
||||
(1, '作业删除', 'homework:delete', 'homework', 'delete', '删除作业权限', 1),
|
||||
(1, '作业评审', 'homework:review', 'homework', 'review', '作业评审权限', 1),
|
||||
(1, '作业评审规则创建', 'homework:review-rule:create', 'homework', 'review-rule:create', '创建作业评审规则权限', 1),
|
||||
(1, '作业评审规则更新', 'homework:review-rule:update', 'homework', 'review-rule:update', '更新作业评审规则权限', 1),
|
||||
(1, '作业评审规则删除', 'homework:review-rule:delete', 'homework', 'review-rule:delete', '删除作业评审规则权限', 1),
|
||||
|
||||
-- AI 3D
|
||||
(1, 'AI 3D 任务删除', 'ai-3d:delete', 'ai-3d', 'delete', '删除 AI 3D 任务权限', 1)
|
||||
ON DUPLICATE KEY UPDATE
|
||||
`name` = VALUES(`name`),
|
||||
`resource` = VALUES(`resource`),
|
||||
`action` = VALUES(`action`),
|
||||
`description` = VALUES(`description`),
|
||||
`valid_state` = 1,
|
||||
`deleted` = 0;
|
||||
|
||||
-- 2. 为 tenant_id=1 的 super_admin 角色补齐“全部有效权限”(增量、幂等)
|
||||
INSERT INTO `t_auth_role_permission` (`role_id`, `permission_id`)
|
||||
SELECT r.id, p.id
|
||||
FROM `t_auth_role` r
|
||||
JOIN `t_auth_permission` p ON p.tenant_id = r.tenant_id
|
||||
WHERE r.tenant_id = 1
|
||||
AND r.code = 'super_admin'
|
||||
AND r.deleted = 0
|
||||
AND p.deleted = 0
|
||||
AND p.valid_state = 1
|
||||
ON DUPLICATE KEY UPDATE `role_id` = VALUES(`role_id`);
|
||||
|
||||
@ -0,0 +1,169 @@
|
||||
-- ============================================
|
||||
-- 根据 Controller 上的 @PreAuthorize 权限码补齐权限表(幂等)
|
||||
-- 目标:
|
||||
-- 1) 确保 Controller 实际使用的 hasAuthority('xxx') 在 t_auth_permission 中存在
|
||||
-- 2) 确保 tenant_id=1 的 super_admin 角色拥有全部有效权限
|
||||
-- 说明:
|
||||
-- - 使用 ON DUPLICATE KEY UPDATE 适配 uk_tenant_code / uk_tenant_resource_action
|
||||
-- - 避免修改已发布的历史迁移,通过更高版本脚本补偿
|
||||
-- ============================================
|
||||
|
||||
INSERT INTO `t_auth_permission`
|
||||
(`tenant_id`, `name`, `code`, `resource`, `action`, `description`, `valid_state`)
|
||||
VALUES
|
||||
-- 菜单
|
||||
(1, '菜单创建', 'menu:create', 'menu', 'create', '创建菜单权限', 1),
|
||||
(1, '菜单查询', 'menu:read', 'menu', 'read', '查询菜单权限', 1),
|
||||
(1, '菜单更新', 'menu:update', 'menu', 'update', '更新菜单权限', 1),
|
||||
(1, '菜单删除', 'menu:delete', 'menu', 'delete', '删除菜单权限', 1),
|
||||
|
||||
-- 角色
|
||||
(1, '角色创建', 'role:create', 'role', 'create', '创建角色权限', 1),
|
||||
(1, '角色查询', 'role:read', 'role', 'read', '查询角色权限', 1),
|
||||
(1, '角色更新', 'role:update', 'role', 'update', '更新角色权限', 1),
|
||||
(1, '角色删除', 'role:delete', 'role', 'delete', '删除角色权限', 1),
|
||||
|
||||
-- 租户
|
||||
(1, '租户创建', 'tenant:create', 'tenant', 'create', '创建租户权限', 1),
|
||||
(1, '租户查询', 'tenant:read', 'tenant', 'read', '查询租户权限', 1),
|
||||
(1, '租户更新', 'tenant:update', 'tenant', 'update', '更新租户权限', 1),
|
||||
(1, '租户删除', 'tenant:delete', 'tenant', 'delete', '删除租户权限', 1),
|
||||
|
||||
-- 用户
|
||||
(1, '用户创建', 'user:create', 'user', 'create', '创建用户权限', 1),
|
||||
(1, '用户查询', 'user:read', 'user', 'read', '查询用户权限', 1),
|
||||
(1, '用户更新', 'user:update', 'user', 'update', '更新用户权限', 1),
|
||||
(1, '用户删除', 'user:delete', 'user', 'delete', '删除用户权限', 1),
|
||||
(1, '用户管理', 'user:manage', 'user', 'manage', '用户管理权限', 1),
|
||||
|
||||
-- 权限管理(Controller 中目前仅使用 permission:read;写操作由 super_admin 控制)
|
||||
(1, '权限查询', 'permission:read', 'permission', 'read', '查询权限权限', 1),
|
||||
|
||||
-- 配置
|
||||
(1, '配置创建', 'config:create', 'config', 'create', '创建配置权限', 1),
|
||||
(1, '配置查询', 'config:read', 'config', 'read', '查询配置权限', 1),
|
||||
(1, '配置更新', 'config:update', 'config', 'update', '更新配置权限', 1),
|
||||
(1, '配置删除', 'config:delete', 'config', 'delete', '删除配置权限', 1),
|
||||
|
||||
-- 字典
|
||||
(1, '字典创建', 'dict:create', 'dict', 'create', '创建字典权限', 1),
|
||||
(1, '字典查询', 'dict:read', 'dict', 'read', '查询字典权限', 1),
|
||||
(1, '字典更新', 'dict:update', 'dict', 'update', '更新字典权限', 1),
|
||||
(1, '字典删除', 'dict:delete', 'dict', 'delete', '删除字典权限', 1),
|
||||
|
||||
-- 日志
|
||||
(1, '日志查询', 'log:read', 'log', 'read', '查询日志权限', 1),
|
||||
(1, '日志删除', 'log:delete', 'log', 'delete', '删除日志权限', 1),
|
||||
|
||||
-- 学校/组织/人员
|
||||
(1, '学校创建', 'school:create', 'school', 'create', '创建学校权限', 1),
|
||||
(1, '学校查询', 'school:read', 'school', 'read', '查询学校权限', 1),
|
||||
(1, '学校更新', 'school:update', 'school', 'update', '更新学校权限', 1),
|
||||
(1, '学校删除', 'school:delete', 'school', 'delete', '删除学校权限', 1),
|
||||
|
||||
(1, '部门创建', 'department:create', 'department', 'create', '创建部门权限', 1),
|
||||
(1, '部门查询', 'department:read', 'department', 'read', '查询部门权限', 1),
|
||||
(1, '部门更新', 'department:update', 'department', 'update', '更新部门权限', 1),
|
||||
(1, '部门删除', 'department:delete', 'department', 'delete', '删除部门权限', 1),
|
||||
|
||||
(1, '年级创建', 'grade:create', 'grade', 'create', '创建年级权限', 1),
|
||||
(1, '年级查询', 'grade:read', 'grade', 'read', '查询年级权限', 1),
|
||||
(1, '年级更新', 'grade:update', 'grade', 'update', '更新年级权限', 1),
|
||||
(1, '年级删除', 'grade:delete', 'grade', 'delete', '删除年级权限', 1),
|
||||
|
||||
(1, '班级创建', 'class:create', 'class', 'create', '创建班级权限', 1),
|
||||
(1, '班级查询', 'class:read', 'class', 'read', '查询班级权限', 1),
|
||||
(1, '班级更新', 'class:update', 'class', 'update', '更新班级权限', 1),
|
||||
(1, '班级删除', 'class:delete', 'class', 'delete', '删除班级权限', 1),
|
||||
|
||||
(1, '教师创建', 'teacher:create', 'teacher', 'create', '创建教师权限', 1),
|
||||
(1, '教师查询', 'teacher:read', 'teacher', 'read', '查询教师权限', 1),
|
||||
(1, '教师更新', 'teacher:update', 'teacher', 'update', '更新教师权限', 1),
|
||||
(1, '教师删除', 'teacher:delete', 'teacher', 'delete', '删除教师权限', 1),
|
||||
|
||||
(1, '学生创建', 'student:create', 'student', 'create', '创建学生权限', 1),
|
||||
(1, '学生查询', 'student:read', 'student', 'read', '查询学生权限', 1),
|
||||
(1, '学生更新', 'student:update', 'student', 'update', '更新学生权限', 1),
|
||||
(1, '学生删除', 'student:delete', 'student', 'delete', '删除学生权限', 1),
|
||||
|
||||
-- 活动
|
||||
(1, '活动创建', 'contest:create', 'contest', 'create', '创建活动权限', 1),
|
||||
(1, '活动查询', 'contest:read', 'contest', 'read', '查询活动权限', 1),
|
||||
(1, '活动更新', 'contest:update', 'contest', 'update', '更新活动权限', 1),
|
||||
(1, '活动发布', 'contest:publish', 'contest', 'publish', '发布活动权限', 1),
|
||||
(1, '活动删除', 'contest:delete', 'contest', 'delete', '删除活动权限', 1),
|
||||
(1, '活动报名', 'contest:register', 'contest', 'register', '活动报名权限', 1),
|
||||
|
||||
-- 活动公告
|
||||
(1, '公告创建', 'contest:notice:create', 'contest', 'notice:create', '创建活动公告权限', 1),
|
||||
(1, '公告更新', 'contest:notice:update', 'contest', 'notice:update', '更新活动公告权限', 1),
|
||||
(1, '公告删除', 'contest:notice:delete', 'contest', 'notice:delete', '删除活动公告权限', 1),
|
||||
(1, '公告发布', 'contest:notice:publish', 'contest', 'notice:publish', '发布活动公告权限', 1),
|
||||
|
||||
-- 活动评委
|
||||
(1, '评委创建', 'contest:judge:create', 'contest', 'judge:create', '创建活动评委权限', 1),
|
||||
(1, '评委更新', 'contest:judge:update', 'contest', 'judge:update', '更新活动评委权限', 1),
|
||||
(1, '评委删除', 'contest:judge:delete', 'contest', 'judge:delete', '删除活动评委权限', 1),
|
||||
|
||||
-- 活动评审规则
|
||||
(1, '评审规则创建', 'contest:review-rule:create', 'contest', 'review-rule:create', '创建评审规则权限', 1),
|
||||
(1, '评审规则更新', 'contest:review-rule:update', 'contest', 'review-rule:update', '更新评审规则权限', 1),
|
||||
(1, '评审规则删除', 'contest:review-rule:delete', 'contest', 'review-rule:delete', '删除评审规则权限', 1),
|
||||
|
||||
-- 活动团队
|
||||
(1, '团队创建', 'contest:team:create', 'contest', 'team:create', '创建活动团队权限', 1),
|
||||
(1, '团队更新', 'contest:team:update', 'contest', 'team:update', '更新活动团队权限', 1),
|
||||
(1, '团队删除', 'contest:team:delete', 'contest', 'team:delete', '删除活动团队权限', 1),
|
||||
|
||||
-- 活动作业/作品
|
||||
(1, '作品提交', 'contest:work:submit', 'contest', 'work:submit', '提交作品权限', 1),
|
||||
(1, '作品更新', 'contest:work:update', 'contest', 'work:update', '更新作品权限', 1),
|
||||
(1, '作品删除', 'contest:work:delete', 'contest', 'work:delete', '删除作品权限', 1),
|
||||
|
||||
-- 活动评审
|
||||
(1, '评审分配', 'contest:review:assign', 'contest', 'review:assign', '分配评审权限', 1),
|
||||
(1, '评审评分', 'contest:review:score', 'contest', 'review:score', '评审评分权限', 1),
|
||||
|
||||
-- 活动评审结果
|
||||
(1, '结果创建', 'contest:result:create', 'contest', 'result:create', '创建评审结果权限', 1),
|
||||
(1, '结果更新', 'contest:result:update', 'contest', 'result:update', '更新评审结果权限', 1),
|
||||
(1, '结果发布', 'contest:result:publish', 'contest', 'result:publish', '发布评审结果权限', 1),
|
||||
(1, '结果删除', 'contest:result:delete', 'contest', 'result:delete', '删除评审结果权限', 1),
|
||||
|
||||
-- 预设评语
|
||||
(1, '预设评语创建', 'contest:preset-comment:create', 'contest', 'preset-comment:create', '创建预设评语权限', 1),
|
||||
(1, '预设评语删除', 'contest:preset-comment:delete', 'contest', 'preset-comment:delete', '删除预设评语权限', 1),
|
||||
|
||||
-- 作业(独立模块)
|
||||
(1, '作业创建', 'homework:create', 'homework', 'create', '创建作业权限', 1),
|
||||
(1, '作业更新', 'homework:update', 'homework', 'update', '更新作业权限', 1),
|
||||
(1, '作业发布', 'homework:publish', 'homework', 'publish', '发布作业权限', 1),
|
||||
(1, '作业删除', 'homework:delete', 'homework', 'delete', '删除作业权限', 1),
|
||||
(1, '作业评审', 'homework:review', 'homework', 'review', '作业评审权限', 1),
|
||||
(1, '作业评审规则创建', 'homework:review-rule:create', 'homework', 'review-rule:create', '创建作业评审规则权限', 1),
|
||||
(1, '作业评审规则更新', 'homework:review-rule:update', 'homework', 'review-rule:update', '更新作业评审规则权限', 1),
|
||||
(1, '作业评审规则删除', 'homework:review-rule:delete', 'homework', 'review-rule:delete', '删除作业评审规则权限', 1),
|
||||
|
||||
-- AI 3D
|
||||
(1, 'AI 3D 任务删除', 'ai-3d:delete', 'ai-3d', 'delete', '删除 AI 3D 任务权限', 1)
|
||||
ON DUPLICATE KEY UPDATE
|
||||
`name` = VALUES(`name`),
|
||||
`code` = VALUES(`code`),
|
||||
`resource` = VALUES(`resource`),
|
||||
`action` = VALUES(`action`),
|
||||
`description` = VALUES(`description`),
|
||||
`valid_state` = 1,
|
||||
`deleted` = 0;
|
||||
|
||||
-- 为 tenant_id=1 的 super_admin 角色补齐“全部有效权限”(增量、幂等)
|
||||
INSERT INTO `t_auth_role_permission` (`role_id`, `permission_id`)
|
||||
SELECT r.id, p.id
|
||||
FROM `t_auth_role` r
|
||||
JOIN `t_auth_permission` p ON p.tenant_id = r.tenant_id
|
||||
WHERE r.tenant_id = 1
|
||||
AND r.code = 'super_admin'
|
||||
AND r.deleted = 0
|
||||
AND p.deleted = 0
|
||||
AND p.valid_state = 1
|
||||
ON DUPLICATE KEY UPDATE `role_id` = VALUES(`role_id`);
|
||||
|
||||
@ -1,11 +1,12 @@
|
||||
<template>
|
||||
<a-config-provider :theme="themeConfig">
|
||||
<a-config-provider :theme="themeConfig" :locale="zhCN">
|
||||
<router-view />
|
||||
</a-config-provider>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ConfigProviderProps } from "ant-design-vue"
|
||||
import zhCN from "ant-design-vue/es/locale/zh_CN"
|
||||
|
||||
// 乐绘世界创想活动乐园 — 主题配置
|
||||
// 风格:活泼、艺术、少儿绘本创作
|
||||
|
||||
@ -12,7 +12,7 @@ export const authApi = {
|
||||
},
|
||||
|
||||
getUserInfo: async (): Promise<User> => {
|
||||
const response = await request.get("/api/auth/user-info");
|
||||
const response = await request.get("/api/auth/me");
|
||||
return response as unknown as User;
|
||||
},
|
||||
|
||||
|
||||
@ -11,16 +11,10 @@ export interface Role {
|
||||
updateBy?: number;
|
||||
createTime?: string;
|
||||
updateTime?: string;
|
||||
permissions?: Array<{
|
||||
id: number;
|
||||
permission: {
|
||||
id: number;
|
||||
name: string;
|
||||
code: string;
|
||||
resource: string;
|
||||
action: string;
|
||||
};
|
||||
}>;
|
||||
/** 后端返回:权限 ID 列表(用于回显/编辑) */
|
||||
permissionIds?: number[];
|
||||
/** 后端返回:菜单 ID 列表(用于回显/编辑) */
|
||||
menuIds?: number[];
|
||||
}
|
||||
|
||||
export interface CreateRoleForm {
|
||||
@ -28,6 +22,7 @@ export interface CreateRoleForm {
|
||||
code: string;
|
||||
description?: string;
|
||||
permissionIds?: number[];
|
||||
menuIds?: number[];
|
||||
}
|
||||
|
||||
export interface UpdateRoleForm {
|
||||
@ -35,15 +30,19 @@ export interface UpdateRoleForm {
|
||||
code?: string;
|
||||
description?: string;
|
||||
permissionIds?: number[];
|
||||
menuIds?: number[];
|
||||
}
|
||||
|
||||
// 获取角色列表
|
||||
export async function getRolesList(
|
||||
params: PaginationParams
|
||||
params: PaginationParams,
|
||||
): Promise<PaginationResponse<Role>> {
|
||||
const response = await request.get<any, PaginationResponse<Role>>("/api/roles", {
|
||||
params,
|
||||
});
|
||||
const response = await request.get<any, PaginationResponse<Role>>(
|
||||
"/api/roles",
|
||||
{
|
||||
params,
|
||||
},
|
||||
);
|
||||
return response;
|
||||
}
|
||||
|
||||
@ -68,7 +67,7 @@ export async function createRole(data: CreateRoleForm): Promise<Role> {
|
||||
// 更新角色
|
||||
export async function updateRole(
|
||||
id: number,
|
||||
data: UpdateRoleForm
|
||||
data: UpdateRoleForm,
|
||||
): Promise<Role> {
|
||||
const response = await request.put<any, Role>(`/api/roles/${id}`, data);
|
||||
return response;
|
||||
|
||||
@ -58,7 +58,8 @@ export function useListRequest<
|
||||
} as PaginationParams & P;
|
||||
|
||||
const response = await requestFn(params);
|
||||
dataSource.value = response.list;
|
||||
const list = (response.list ?? response.records ?? []) as T[];
|
||||
dataSource.value = list;
|
||||
pagination.total = response.total;
|
||||
} catch (error) {
|
||||
message.error(errorMessage);
|
||||
|
||||
@ -7,7 +7,7 @@ import { useAuthStore } from "@/stores/auth";
|
||||
interface PermissionDirectiveValue {
|
||||
// 权限码或权限码数组
|
||||
permission: string | string[];
|
||||
// 是否隐藏元素(默认 false,即禁用)
|
||||
// 是否隐藏元素(默认 true:无权限直接隐藏)
|
||||
hide?: boolean;
|
||||
// 是否需要所有权限(默认 false,即任一权限即可)
|
||||
all?: boolean;
|
||||
@ -73,13 +73,17 @@ function handlePermission(
|
||||
if (typeof value === "string" || Array.isArray(value)) {
|
||||
config = {
|
||||
permission: value,
|
||||
hide: modifiers.hide || false,
|
||||
// 默认隐藏;显式 .disable 才走“禁用”逻辑
|
||||
hide: modifiers.disable ? false : true,
|
||||
all: needAll,
|
||||
};
|
||||
} else {
|
||||
config = {
|
||||
permission: value.permission,
|
||||
hide: value.hide ?? modifiers.hide ?? false,
|
||||
// 默认隐藏;若显式传入 hide/disable 则尊重调用方
|
||||
hide:
|
||||
value.hide ??
|
||||
(modifiers.disable ? false : modifiers.hide ? true : true),
|
||||
all: value.all ?? needAll,
|
||||
};
|
||||
}
|
||||
|
||||
@ -1,6 +1,8 @@
|
||||
import { createApp } from "vue"
|
||||
import { createPinia } from "pinia"
|
||||
import Antd from "ant-design-vue"
|
||||
import dayjs from "dayjs"
|
||||
import "dayjs/locale/zh-cn"
|
||||
import "ant-design-vue/dist/reset.css"
|
||||
import "./styles/global.scss"
|
||||
import "./styles/theme.scss"
|
||||
@ -9,6 +11,9 @@ import router from "./router"
|
||||
import { useAuthStore } from "./stores/auth"
|
||||
import { setupPermissionDirective } from "./directives/permission"
|
||||
|
||||
// dayjs 全局中文(简体)
|
||||
dayjs.locale("zh-cn")
|
||||
|
||||
const app = createApp(App)
|
||||
const pinia = createPinia()
|
||||
|
||||
|
||||
@ -1,146 +1,277 @@
|
||||
import { defineStore } from "pinia"
|
||||
import { ref, computed } from "vue"
|
||||
import type { User, LoginForm } from "@/types/auth"
|
||||
import { authApi } from "@/api/auth"
|
||||
import { menusApi, type Menu } from "@/api/menus"
|
||||
import { getToken, setToken, removeToken, getTenantCode } from "@/utils/auth"
|
||||
import { defineStore } from "pinia";
|
||||
import { ref, computed } from "vue";
|
||||
import type { User, LoginForm } from "@/types/auth";
|
||||
import { authApi } from "@/api/auth";
|
||||
import { menusApi, type Menu } from "@/api/menus";
|
||||
import {
|
||||
getToken,
|
||||
setToken,
|
||||
removeToken,
|
||||
getTenantCode,
|
||||
setTenantId,
|
||||
removeTenantId,
|
||||
} from "@/utils/auth";
|
||||
|
||||
export const useAuthStore = defineStore("auth", () => {
|
||||
const user = ref<User | null>(null)
|
||||
const token = ref<string>(getToken() || "")
|
||||
const loading = ref<boolean>(false)
|
||||
const menus = ref<Menu[]>([])
|
||||
const user = ref<User | null>(null);
|
||||
const token = ref<string>(getToken() || "");
|
||||
const loading = ref<boolean>(false);
|
||||
const menus = ref<Menu[]>([]);
|
||||
|
||||
const isAuthenticated = computed(() => !!token.value)
|
||||
const isAuthenticated = computed(() => !!token.value);
|
||||
|
||||
// 获取当前用户的租户编码(优先从用户信息获取,其次从 URL 获取)
|
||||
const tenantCode = computed(() => {
|
||||
return user.value?.tenantCode || getTenantCode()
|
||||
})
|
||||
return user.value?.tenantCode || getTenantCode();
|
||||
});
|
||||
|
||||
// 检查是否有指定角色
|
||||
const hasRole = (role: string): boolean => {
|
||||
return user.value?.roles?.includes(role) ?? false
|
||||
}
|
||||
return user.value?.roles?.includes(role) ?? false;
|
||||
};
|
||||
|
||||
// 检查是否为超级管理员
|
||||
const isSuperAdmin = (): boolean => {
|
||||
return user.value?.roles?.includes('super_admin') ?? false
|
||||
}
|
||||
return user.value?.roles?.includes("super_admin") ?? false;
|
||||
};
|
||||
|
||||
// 检查是否有指定权限
|
||||
const hasPermission = (permission: string): boolean => {
|
||||
// 超级管理员拥有所有权限
|
||||
if (isSuperAdmin()) return true
|
||||
return user.value?.permissions?.includes(permission) ?? false
|
||||
}
|
||||
if (isSuperAdmin()) return true;
|
||||
return user.value?.permissions?.includes(permission) ?? false;
|
||||
};
|
||||
|
||||
// 检查是否有任一角色
|
||||
const hasAnyRole = (roles: string[]): boolean => {
|
||||
if (!roles || roles.length === 0) return true
|
||||
return roles.some((role) => hasRole(role))
|
||||
}
|
||||
if (!roles || roles.length === 0) return true;
|
||||
return roles.some((role) => hasRole(role));
|
||||
};
|
||||
|
||||
// 检查是否有任一权限
|
||||
const hasAnyPermission = (permissions: string[]): boolean => {
|
||||
if (!permissions || permissions.length === 0) return true
|
||||
if (!permissions || permissions.length === 0) return true;
|
||||
// 超级管理员拥有所有权限
|
||||
if (isSuperAdmin()) return true
|
||||
return permissions.some((perm) => hasPermission(perm))
|
||||
}
|
||||
if (isSuperAdmin()) return true;
|
||||
return permissions.some((perm) => hasPermission(perm));
|
||||
};
|
||||
|
||||
const login = async (form: LoginForm) => {
|
||||
const response = await authApi.login(form)
|
||||
token.value = response.token
|
||||
user.value = response.user
|
||||
const response = await authApi.login(form);
|
||||
token.value = response.token;
|
||||
user.value = response.user;
|
||||
|
||||
// 使用租户编码作为 cookie path(不再存储到 localStorage)
|
||||
if (response.user.tenantCode) {
|
||||
setToken(response.token, response.user.tenantCode)
|
||||
setToken(response.token, response.user.tenantCode);
|
||||
} else {
|
||||
setToken(response.token)
|
||||
setToken(response.token);
|
||||
}
|
||||
if (response.user.tenantId) {
|
||||
setTenantId(response.user.tenantId);
|
||||
}
|
||||
|
||||
// 登录后获取用户菜单
|
||||
await fetchUserMenus()
|
||||
return response
|
||||
}
|
||||
await fetchUserMenus();
|
||||
return response;
|
||||
};
|
||||
|
||||
const logout = async () => {
|
||||
try {
|
||||
await authApi.logout()
|
||||
await authApi.logout();
|
||||
} finally {
|
||||
token.value = ""
|
||||
token.value = "";
|
||||
// 删除 token cookie,使用当前用户的租户编码或 URL 中的租户编码
|
||||
const tenantCode = user.value?.tenantCode || getTenantCode()
|
||||
removeToken(tenantCode || undefined)
|
||||
user.value = null
|
||||
menus.value = []
|
||||
const tenantCode = user.value?.tenantCode || getTenantCode();
|
||||
removeToken(tenantCode || undefined);
|
||||
removeTenantId();
|
||||
user.value = null;
|
||||
menus.value = [];
|
||||
// 重置动态路由标记
|
||||
if (typeof window !== "undefined") {
|
||||
const { resetDynamicRoutes } = await import("@/router")
|
||||
resetDynamicRoutes()
|
||||
const { resetDynamicRoutes } = await import("@/router");
|
||||
resetDynamicRoutes();
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const fetchUserInfo = async () => {
|
||||
if (!token.value) {
|
||||
throw new Error("未登录")
|
||||
throw new Error("未登录");
|
||||
}
|
||||
|
||||
try {
|
||||
loading.value = true
|
||||
const response = await authApi.getUserInfo()
|
||||
user.value = response
|
||||
loading.value = true;
|
||||
const response = await authApi.getUserInfo();
|
||||
user.value = response;
|
||||
// 获取用户菜单
|
||||
await fetchUserMenus()
|
||||
return response
|
||||
await fetchUserMenus();
|
||||
return response;
|
||||
} catch (error) {
|
||||
// 如果获取用户信息失败,清除 token
|
||||
token.value = ""
|
||||
user.value = null
|
||||
menus.value = []
|
||||
removeToken()
|
||||
throw error
|
||||
token.value = "";
|
||||
user.value = null;
|
||||
menus.value = [];
|
||||
removeToken();
|
||||
throw error;
|
||||
} finally {
|
||||
loading.value = false
|
||||
loading.value = false;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const fetchUserMenus = async () => {
|
||||
if (!token.value) {
|
||||
return
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const userMenus = await menusApi.getUserMenus()
|
||||
menus.value = userMenus
|
||||
return userMenus
|
||||
const userMenus = await menusApi.getUserMenus();
|
||||
// 前端兜底:当数据库菜单缺失时,补齐关键入口,避免页面不可达
|
||||
// 场景:系统管理下的「角色管理」「权限管理」在数据库未配置或被误删
|
||||
const normalizedMenus = (userMenus || []) as Menu[];
|
||||
|
||||
const ensureSystemRolePermissionMenus = (list: Menu[]): Menu[] => {
|
||||
// 没有用户信息时无法判断权限,直接返回原菜单
|
||||
if (!user.value) return list;
|
||||
|
||||
const has = (
|
||||
nodes: Menu[],
|
||||
predicate: (m: Menu) => boolean,
|
||||
): boolean => {
|
||||
for (const n of nodes) {
|
||||
if (predicate(n)) return true;
|
||||
if (n.children && n.children.length && has(n.children, predicate))
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
const findSystem = (nodes: Menu[]): Menu | null => {
|
||||
for (const n of nodes) {
|
||||
// 优先按 path 匹配 system,其次按名称兜底
|
||||
if (
|
||||
n.path === "system" ||
|
||||
n.path === "/system" ||
|
||||
n.name === "系统管理"
|
||||
)
|
||||
return n;
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
const systemMenu = findSystem(list);
|
||||
|
||||
// 只有具备读取权限时才补齐入口(避免给无权限用户制造“点了进403”的假入口)
|
||||
const canReadRole = hasPermission("role:read");
|
||||
const canReadPermission = hasPermission("permission:read");
|
||||
const canReadMenu = hasPermission("menu:read");
|
||||
|
||||
// 如果用户完全没有相关权限,就不补
|
||||
if (!canReadRole && !canReadPermission && !canReadMenu) return list;
|
||||
|
||||
// 构造需要补齐的两个菜单(使用负数ID,避免与数据库ID冲突;key 生成依赖 id)
|
||||
const roleMenu: Menu = {
|
||||
id: -91001,
|
||||
name: "角色管理",
|
||||
path: "system/roles",
|
||||
icon: "Team",
|
||||
component: "system/roles/Index",
|
||||
parentId: systemMenu?.id ?? -91000,
|
||||
permission: "role:read",
|
||||
sort: 2,
|
||||
};
|
||||
|
||||
const permMenu: Menu = {
|
||||
id: -91002,
|
||||
name: "权限管理",
|
||||
path: "system/permissions",
|
||||
icon: "Lock",
|
||||
component: "system/permissions/Index",
|
||||
parentId: systemMenu?.id ?? -91000,
|
||||
permission: "permission:read",
|
||||
sort: 3,
|
||||
};
|
||||
|
||||
const menuMgmtMenu: Menu = {
|
||||
id: -91003,
|
||||
name: "菜单管理",
|
||||
path: "system/menus",
|
||||
icon: "Menu",
|
||||
component: "system/menus/Index",
|
||||
parentId: systemMenu?.id ?? -91000,
|
||||
permission: "menu:read",
|
||||
sort: 4,
|
||||
};
|
||||
|
||||
// 若系统管理本身缺失,则创建一个父级系统管理菜单承载
|
||||
const ensuredList = [...list];
|
||||
const ensuredSystem: Menu =
|
||||
systemMenu ??
|
||||
({
|
||||
id: -91000,
|
||||
name: "系统管理",
|
||||
path: "system",
|
||||
icon: "Setting",
|
||||
component: undefined,
|
||||
parentId: 0,
|
||||
permission: "super_admin",
|
||||
sort: 999,
|
||||
children: [],
|
||||
} as Menu);
|
||||
|
||||
if (!systemMenu) {
|
||||
ensuredList.push(ensuredSystem);
|
||||
} else if (!ensuredSystem.children) {
|
||||
ensuredSystem.children = [];
|
||||
}
|
||||
|
||||
const children = ensuredSystem.children || [];
|
||||
|
||||
// // 仅当缺失对应 path 时才补齐
|
||||
// if (canReadRole && !has(children, (m) => m.path === roleMenu.path)) {
|
||||
// children.push(roleMenu);
|
||||
// }
|
||||
// if (canReadPermission && !has(children, (m) => m.path === permMenu.path)) {
|
||||
// children.push(permMenu);
|
||||
// }
|
||||
// if (canReadMenu && !has(children, (m) => m.path === menuMgmtMenu.path)) {
|
||||
// children.push(menuMgmtMenu);
|
||||
// }
|
||||
|
||||
// 按 sort 排序,保持菜单稳定
|
||||
ensuredSystem.children = children.sort(
|
||||
(a, b) => (a.sort ?? 0) - (b.sort ?? 0),
|
||||
);
|
||||
|
||||
return ensuredList;
|
||||
};
|
||||
|
||||
menus.value = ensureSystemRolePermissionMenus(normalizedMenus);
|
||||
return userMenus;
|
||||
} catch (error) {
|
||||
console.error("获取用户菜单失败:", error)
|
||||
menus.value = []
|
||||
throw error
|
||||
console.error("获取用户菜单失败:", error);
|
||||
menus.value = [];
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const updateToken = (newToken: string) => {
|
||||
token.value = newToken
|
||||
token.value = newToken;
|
||||
// 使用当前用户的租户编码更新 token cookie
|
||||
const tenantCode = user.value?.tenantCode || getTenantCode()
|
||||
setToken(newToken, tenantCode || undefined)
|
||||
}
|
||||
const tenantCode = user.value?.tenantCode || getTenantCode();
|
||||
setToken(newToken, tenantCode || undefined);
|
||||
};
|
||||
|
||||
// 初始化:如果有 token 但没有用户信息,自动获取
|
||||
const initAuth = async () => {
|
||||
if (token.value && !user.value) {
|
||||
try {
|
||||
await fetchUserInfo()
|
||||
await fetchUserInfo();
|
||||
} catch (error) {
|
||||
console.error("自动获取用户信息失败:", error)
|
||||
console.error("自动获取用户信息失败:", error);
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
return {
|
||||
user,
|
||||
@ -159,5 +290,5 @@ export const useAuthStore = defineStore("auth", () => {
|
||||
fetchUserMenus,
|
||||
updateToken,
|
||||
initAuth,
|
||||
}
|
||||
})
|
||||
};
|
||||
});
|
||||
|
||||
@ -10,8 +10,20 @@ export interface PaginationParams {
|
||||
}
|
||||
|
||||
export interface PaginationResponse<T> {
|
||||
list: T[];
|
||||
/**
|
||||
* 兼容不同后端分页字段:
|
||||
* - list: 常见返回
|
||||
* - records: MyBatis-Plus / 自定义返回
|
||||
*/
|
||||
list?: T[];
|
||||
records?: T[];
|
||||
total: number;
|
||||
page: number;
|
||||
pageSize: number;
|
||||
/** 当前页:page 或 current */
|
||||
page?: number;
|
||||
current?: number;
|
||||
/** 每页大小:pageSize 或 size */
|
||||
pageSize?: number;
|
||||
size?: number;
|
||||
/** 总页数(可选) */
|
||||
pages?: number;
|
||||
}
|
||||
|
||||
@ -1,4 +1,5 @@
|
||||
const TOKEN_KEY = "token";
|
||||
const TENANT_ID_KEY = "tenantId";
|
||||
|
||||
/**
|
||||
* 从当前 URL 路径中提取租户编码
|
||||
@ -22,7 +23,7 @@ function getTenantCodeFromUrl(): string | null {
|
||||
function setCookie(
|
||||
name: string,
|
||||
value: string,
|
||||
options: { path?: string; expires?: number; maxAge?: number } = {}
|
||||
options: { path?: string; expires?: number; maxAge?: number } = {},
|
||||
): void {
|
||||
const { path = "/", expires, maxAge } = options;
|
||||
let cookieString = `${name}=${encodeURIComponent(value)}; path=${path}`;
|
||||
@ -77,7 +78,7 @@ export const getToken = (): string | null => {
|
||||
return getAllTokenCookies();
|
||||
};
|
||||
|
||||
export const setToken = (token: string, tenantCode?: string): void => {
|
||||
export const setToken = (token: string, _tenantCode?: string): void => {
|
||||
// 始终将 token 存储在根路径下,确保所有页面都能访问
|
||||
const base = import.meta.env.BASE_URL || "/";
|
||||
const basePath = base.endsWith("/") ? base.slice(0, -1) : base;
|
||||
@ -120,31 +121,35 @@ export const removeTenantCode = (): void => {
|
||||
};
|
||||
|
||||
/**
|
||||
* 获取租户ID(已废弃,不再从 localStorage 获取)
|
||||
* 保留此函数以保持兼容性,返回 null
|
||||
* 获取租户ID
|
||||
* 刷新页面时,用户信息尚未拉取前需要依赖此值补全请求头
|
||||
*/
|
||||
export const getTenantId = (): string | null => {
|
||||
// 不再从 localStorage 获取,租户ID 从用户信息中获取
|
||||
return null;
|
||||
return getCookie(TENANT_ID_KEY);
|
||||
};
|
||||
|
||||
/**
|
||||
* 设置租户ID(已废弃,不再存储到 localStorage)
|
||||
* 保留此函数以保持兼容性,但不执行任何操作
|
||||
* 设置租户ID
|
||||
*/
|
||||
export const setTenantId = (_tenantId: number | string): void => {
|
||||
// 不再存储到 localStorage,租户ID 从用户信息中获取
|
||||
const base = import.meta.env.BASE_URL || "/";
|
||||
const basePath = base.endsWith("/") ? base.slice(0, -1) : base;
|
||||
const path = basePath || "/";
|
||||
const expires = 7 * 24 * 60 * 60; // 7 天(秒)
|
||||
setCookie(TENANT_ID_KEY, String(_tenantId), { path, expires });
|
||||
};
|
||||
|
||||
/**
|
||||
* 移除租户ID(已废弃,不再从 localStorage 删除)
|
||||
* 保留此函数以保持兼容性,但不执行任何操作
|
||||
* 移除租户ID
|
||||
*/
|
||||
export const removeTenantId = (): void => {
|
||||
// 不再从 localStorage 删除,租户ID 从用户信息中获取
|
||||
const base = import.meta.env.BASE_URL || "/";
|
||||
const basePath = base.endsWith("/") ? base.slice(0, -1) : base;
|
||||
removeCookie(TENANT_ID_KEY, basePath || "/");
|
||||
};
|
||||
|
||||
export const clearAuth = (): void => {
|
||||
// 只清除 token,租户信息不再存储在 localStorage
|
||||
removeToken();
|
||||
removeTenantId();
|
||||
};
|
||||
|
||||
@ -4,7 +4,7 @@ import axios, {
|
||||
type AxiosResponse,
|
||||
} from "axios";
|
||||
import { message } from "ant-design-vue";
|
||||
import { getToken, removeToken, getTenantCode } from "./auth";
|
||||
import { getToken, removeToken, getTenantCode, getTenantId } from "./auth";
|
||||
import { useAuthStore } from "@/stores/auth";
|
||||
import router from "@/router";
|
||||
|
||||
@ -87,7 +87,7 @@ service.interceptors.request.use(
|
||||
// 租户编码从 URL 获取,租户ID 从用户信息获取
|
||||
const tenantCode = getTenantCode();
|
||||
const authStore = useAuthStore();
|
||||
const tenantId = authStore.user?.tenantId;
|
||||
const tenantId = authStore.user?.tenantId ?? getTenantId();
|
||||
|
||||
if (config.headers) {
|
||||
if (tenantCode) {
|
||||
|
||||
@ -26,8 +26,15 @@
|
||||
</a-table>
|
||||
|
||||
<!-- 新增/编辑角色弹窗 -->
|
||||
<a-modal v-model:open="modalVisible" :title="modalTitle" :confirm-loading="submitLoading" @ok="handleSubmit"
|
||||
@cancel="handleCancel" width="800px" :loading="detailLoading">
|
||||
<a-modal
|
||||
:open="modalVisible"
|
||||
:title="modalTitle"
|
||||
:confirm-loading="submitLoading"
|
||||
width="800px"
|
||||
@ok="handleSubmit"
|
||||
@cancel="handleCancel"
|
||||
@update:open="handleOpenChange"
|
||||
>
|
||||
<a-tabs v-model:activeKey="activeTab">
|
||||
<a-tab-pane key="basic" tab="基本信息">
|
||||
<a-form ref="formRef" :model="form" :rules="rules" :label-col="{ span: 6 }" :wrapper-col="{ span: 18 }">
|
||||
@ -48,7 +55,7 @@
|
||||
<div v-if="permissionsLoading" style="text-align: center; padding: 40px;">
|
||||
<a-spin size="large" />
|
||||
</div>
|
||||
<div v-else>
|
||||
<div v-else class="permission-scroll-wrapper">
|
||||
<div v-for="(group, resource) in permissionGroups" :key="resource" class="permission-group">
|
||||
<div class="permission-group-header">
|
||||
<a-checkbox :checked="isResourceAllChecked(resource)" :indeterminate="isResourceIndeterminate(resource)"
|
||||
@ -70,6 +77,25 @@
|
||||
</div>
|
||||
</div>
|
||||
</a-tab-pane>
|
||||
|
||||
<a-tab-pane key="menus" tab="菜单授权">
|
||||
<div v-if="menusLoading" style="text-align: center; padding: 40px;">
|
||||
<a-spin size="large" />
|
||||
</div>
|
||||
<div v-else class="menu-tree-wrapper">
|
||||
<a-tree
|
||||
checkable
|
||||
block-node
|
||||
default-expand-all
|
||||
v-model:checkedKeys="form.menuIds"
|
||||
:tree-data="menuTreeData"
|
||||
>
|
||||
<template #title="{ title }">
|
||||
<span>{{ title }}</span>
|
||||
</template>
|
||||
</a-tree>
|
||||
</div>
|
||||
</a-tab-pane>
|
||||
</a-tabs>
|
||||
</a-modal>
|
||||
</div>
|
||||
@ -81,6 +107,7 @@ import { message, Modal } from 'ant-design-vue'
|
||||
import type { TableColumnsType, FormInstance } from 'ant-design-vue'
|
||||
import { rolesApi, type Role, type CreateRoleForm } from '@/api/roles'
|
||||
import { permissionsApi, type Permission } from '@/api/permissions'
|
||||
import { menusApi, type Menu } from '@/api/menus'
|
||||
import { useListRequest } from '@/composables/useListRequest'
|
||||
import { useAuthStore } from '@/stores/auth'
|
||||
|
||||
@ -89,6 +116,7 @@ const authStore = useAuthStore()
|
||||
const submitLoading = ref(false)
|
||||
const detailLoading = ref(false)
|
||||
const permissionsLoading = ref(false)
|
||||
const menusLoading = ref(false)
|
||||
const modalVisible = ref(false)
|
||||
const modalTitle = ref('新增角色')
|
||||
const formRef = ref<FormInstance>()
|
||||
@ -99,7 +127,7 @@ const activeTab = ref('basic')
|
||||
const allPermissions = ref<Permission[]>([])
|
||||
const permissionGroups = computed(() => {
|
||||
const groups: Record<string, Permission[]> = {}
|
||||
allPermissions.value.forEach((permission) => {
|
||||
;(allPermissions.value || []).forEach((permission) => {
|
||||
if (!groups[permission.resource]) {
|
||||
groups[permission.resource] = []
|
||||
}
|
||||
@ -125,6 +153,7 @@ const form = reactive<CreateRoleForm>({
|
||||
code: '',
|
||||
description: '',
|
||||
permissionIds: [],
|
||||
menuIds: [],
|
||||
})
|
||||
|
||||
const rules = {
|
||||
@ -160,7 +189,7 @@ const fetchAllPermissions = async () => {
|
||||
permissionsLoading.value = true
|
||||
try {
|
||||
const response = await permissionsApi.getList({ page: 1, pageSize: 100 })
|
||||
allPermissions.value = response.list
|
||||
allPermissions.value = response.list ?? response.records ?? []
|
||||
} catch (error) {
|
||||
message.error('获取权限列表失败')
|
||||
} finally {
|
||||
@ -168,6 +197,30 @@ const fetchAllPermissions = async () => {
|
||||
}
|
||||
}
|
||||
|
||||
// 菜单相关
|
||||
const allMenus = ref<Menu[]>([])
|
||||
const menuTreeData = computed(() => {
|
||||
const build = (menus: Menu[]): any[] =>
|
||||
(menus || []).map((m) => ({
|
||||
title: m.name,
|
||||
key: m.id,
|
||||
children: m.children && m.children.length ? build(m.children) : undefined,
|
||||
}))
|
||||
return build(allMenus.value)
|
||||
})
|
||||
|
||||
const fetchAllMenus = async () => {
|
||||
menusLoading.value = true
|
||||
try {
|
||||
const response = await menusApi.getList()
|
||||
allMenus.value = response || []
|
||||
} catch (error) {
|
||||
message.error('获取菜单列表失败')
|
||||
} finally {
|
||||
menusLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 获取资源名称(中文显示)
|
||||
const getResourceName = (resource: string): string => {
|
||||
const resourceMap: Record<string, string> = {
|
||||
@ -258,6 +311,7 @@ const handleAdd = () => {
|
||||
form.code = ''
|
||||
form.description = ''
|
||||
form.permissionIds = []
|
||||
form.menuIds = []
|
||||
})
|
||||
}
|
||||
|
||||
@ -275,7 +329,9 @@ const handleEdit = async (record: Role) => {
|
||||
form.code = detail.code
|
||||
form.description = detail.description || ''
|
||||
// 回显权限
|
||||
form.permissionIds = detail.permissions?.map((rp) => rp.permission.id) || []
|
||||
form.permissionIds = detail.permissionIds || []
|
||||
// 回显菜单
|
||||
form.menuIds = detail.menuIds || []
|
||||
} catch (error) {
|
||||
message.error('获取角色详情失败')
|
||||
modalVisible.value = false
|
||||
@ -335,6 +391,15 @@ const handleSubmit = async () => {
|
||||
}
|
||||
}
|
||||
|
||||
// 统一处理弹窗开关(确保点右上角关闭也能同步状态)
|
||||
const handleOpenChange = (open: boolean) => {
|
||||
modalVisible.value = open
|
||||
if (!open) {
|
||||
formRef.value?.resetFields()
|
||||
activeTab.value = 'basic'
|
||||
}
|
||||
}
|
||||
|
||||
// 取消表单
|
||||
const handleCancel = () => {
|
||||
modalVisible.value = false
|
||||
@ -345,6 +410,7 @@ const handleCancel = () => {
|
||||
// 初始化加载权限列表
|
||||
onMounted(() => {
|
||||
fetchAllPermissions()
|
||||
fetchAllMenus()
|
||||
})
|
||||
</script>
|
||||
|
||||
@ -371,4 +437,20 @@ onMounted(() => {
|
||||
margin-left: 4px;
|
||||
}
|
||||
}
|
||||
|
||||
.permission-scroll-wrapper {
|
||||
padding: 8px 4px;
|
||||
border: 1px solid #f0f0f0;
|
||||
border-radius: 8px;
|
||||
max-height: 520px;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
.menu-tree-wrapper {
|
||||
padding: 8px 4px;
|
||||
border: 1px solid #f0f0f0;
|
||||
border-radius: 8px;
|
||||
max-height: 520px;
|
||||
overflow: auto;
|
||||
}
|
||||
</style>
|
||||
|
||||
Loading…
Reference in New Issue
Block a user