feat: 角色菜单授权与权限同步
Made-with: Cursor
This commit is contained in:
parent
dba6def3c5
commit
b20c00bea3
@ -49,7 +49,7 @@ public class MenuController {
|
|||||||
|
|
||||||
@GetMapping("/user-menus")
|
@GetMapping("/user-menus")
|
||||||
@Operation(summary = "当前用户菜单")
|
@Operation(summary = "当前用户菜单")
|
||||||
@PreAuthorize("hasAuthority('menu:read')")
|
@PreAuthorize("isAuthenticated()")
|
||||||
public Result<List<MenuTreeVO>> userMenus(
|
public Result<List<MenuTreeVO>> userMenus(
|
||||||
@AuthenticationPrincipal UserPrincipal userPrincipal) {
|
@AuthenticationPrincipal UserPrincipal userPrincipal) {
|
||||||
Long userId = userPrincipal.getUserId();
|
Long userId = userPrincipal.getUserId();
|
||||||
|
|||||||
@ -66,8 +66,11 @@ public class RoleController {
|
|||||||
@GetMapping("/{id}")
|
@GetMapping("/{id}")
|
||||||
@Operation(summary = "角色详情")
|
@Operation(summary = "角色详情")
|
||||||
@PreAuthorize("hasAuthority('role:read')")
|
@PreAuthorize("hasAuthority('role:read')")
|
||||||
public Result<RoleDetailVO> detail(@PathVariable Long id) {
|
public Result<RoleDetailVO> detail(
|
||||||
RoleDetailVO result = roleService.detail(id);
|
@AuthenticationPrincipal UserPrincipal userPrincipal,
|
||||||
|
@PathVariable Long id) {
|
||||||
|
Long tenantId = userPrincipal.getTenantId();
|
||||||
|
RoleDetailVO result = roleService.detail(id, tenantId);
|
||||||
return Result.success(result);
|
return Result.success(result);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -87,8 +90,10 @@ public class RoleController {
|
|||||||
@Operation(summary = "删除角色")
|
@Operation(summary = "删除角色")
|
||||||
@PreAuthorize("hasAuthority('role:delete')")
|
@PreAuthorize("hasAuthority('role:delete')")
|
||||||
public Result<Void> delete(
|
public Result<Void> delete(
|
||||||
|
@AuthenticationPrincipal UserPrincipal userPrincipal,
|
||||||
@PathVariable Long id) {
|
@PathVariable Long id) {
|
||||||
roleService.delete(id);
|
Long tenantId = userPrincipal.getTenantId();
|
||||||
|
roleService.delete(id, tenantId);
|
||||||
return Result.success();
|
return Result.success();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -26,4 +26,7 @@ public class CreateRoleDTO {
|
|||||||
|
|
||||||
@Schema(description = "权限 ID 列表")
|
@Schema(description = "权限 ID 列表")
|
||||||
private List<Long> permissionIds;
|
private List<Long> permissionIds;
|
||||||
|
|
||||||
|
@Schema(description = "菜单 ID 列表(菜单可见性授权)")
|
||||||
|
private List<Long> menuIds;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -23,4 +23,7 @@ public class UpdateRoleDTO {
|
|||||||
|
|
||||||
@Schema(description = "权限 ID 列表")
|
@Schema(description = "权限 ID 列表")
|
||||||
private java.util.List<Long> permissionIds;
|
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 查询角色详情
|
* 根据 ID 查询角色详情
|
||||||
*
|
*
|
||||||
* @param id 角色 ID
|
* @param id 角色 ID
|
||||||
|
* @param tenantId 租户 ID
|
||||||
* @return 角色详情 VO
|
* @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 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 org.springframework.util.StringUtils;
|
||||||
|
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.HashSet;
|
||||||
import java.util.Comparator;
|
import java.util.Comparator;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.Set;
|
||||||
import java.util.stream.Collectors;
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -33,6 +37,8 @@ public class MenuServiceImpl extends ServiceImpl<MenuMapper, Menu> implements Me
|
|||||||
private final UserRoleMapper userRoleMapper;
|
private final UserRoleMapper userRoleMapper;
|
||||||
private final RolePermissionMapper rolePermissionMapper;
|
private final RolePermissionMapper rolePermissionMapper;
|
||||||
private final PermissionMapper permissionMapper;
|
private final PermissionMapper permissionMapper;
|
||||||
|
private final RoleMenuMapper roleMenuMapper;
|
||||||
|
private final RoleMapper roleMapper;
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@Transactional(rollbackFor = Exception.class)
|
@Transactional(rollbackFor = Exception.class)
|
||||||
@ -93,30 +99,31 @@ public class MenuServiceImpl extends ServiceImpl<MenuMapper, Menu> implements Me
|
|||||||
.map(UserRole::getRoleId)
|
.map(UserRole::getRoleId)
|
||||||
.collect(Collectors.toList());
|
.collect(Collectors.toList());
|
||||||
|
|
||||||
// 3. 查询角色关联的权限
|
// 3. 判断是否超级管理员(角色 code=super_admin 或权限 code=super_admin 兜底)
|
||||||
List<RolePermission> rolePermissions = rolePermissionMapper.selectList(
|
boolean hasSuperAdmin = false;
|
||||||
new LambdaQueryWrapper<RolePermission>()
|
List<Role> roles = roleMapper.selectBatchIds(roleIds);
|
||||||
.in(RolePermission::getRoleId, roleIds)
|
if (roles != null) {
|
||||||
);
|
hasSuperAdmin = roles.stream().anyMatch(r -> "super_admin".equals(r.getCode()));
|
||||||
|
|
||||||
if (rolePermissions.isEmpty()) {
|
|
||||||
// 角色没有关联权限,返回空菜单
|
|
||||||
log.warn("用户角色没有关联任何权限,用户 ID: {}", userId);
|
|
||||||
return new ArrayList<>();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 4. 获取权限 ID 列表
|
if (!hasSuperAdmin) {
|
||||||
List<Long> permissionIds = rolePermissions.stream()
|
// 兜底:如果权限中包含 super_admin,也视为超管
|
||||||
.map(RolePermission::getPermissionId)
|
List<RolePermission> rolePermissionsForSuper = rolePermissionMapper.selectList(
|
||||||
.distinct()
|
new LambdaQueryWrapper<RolePermission>()
|
||||||
.collect(Collectors.toList());
|
.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 权限
|
// 4. 查询菜单(超管返回所有;非超管按角色-菜单关联取并集 + 补齐祖先节点)
|
||||||
List<Permission> permissions = permissionMapper.selectBatchIds(permissionIds);
|
|
||||||
boolean hasSuperAdmin = permissions.stream()
|
|
||||||
.anyMatch(p -> "super_admin".equals(p.getCode()));
|
|
||||||
|
|
||||||
// 6. 查询菜单
|
|
||||||
LambdaQueryWrapper<Menu> wrapper = new LambdaQueryWrapper<>();
|
LambdaQueryWrapper<Menu> wrapper = new LambdaQueryWrapper<>();
|
||||||
wrapper.eq(Menu::getValidState, 1)
|
wrapper.eq(Menu::getValidState, 1)
|
||||||
.orderByAsc(Menu::getSort, Menu::getId);
|
.orderByAsc(Menu::getSort, Menu::getId);
|
||||||
@ -127,24 +134,50 @@ public class MenuServiceImpl extends ServiceImpl<MenuMapper, Menu> implements Me
|
|||||||
log.info("用户是超级管理员,返回所有菜单");
|
log.info("用户是超级管理员,返回所有菜单");
|
||||||
allMenus = menuMapper.selectList(wrapper);
|
allMenus = menuMapper.selectList(wrapper);
|
||||||
} else {
|
} else {
|
||||||
// 非超管:只查询有权限的菜单
|
// 非超管:按角色-菜单关联表控制可见性
|
||||||
// 获取有权限的菜单编码
|
List<RoleMenu> roleMenus = roleMenuMapper.selectList(
|
||||||
List<String> permissionCodes = permissions.stream()
|
new LambdaQueryWrapper<RoleMenu>()
|
||||||
.map(Permission::getCode)
|
.in(RoleMenu::getRoleId, roleIds)
|
||||||
.collect(Collectors.toList());
|
|
||||||
|
|
||||||
// 筛选条件:菜单权限为空(所有人都可见)或 菜单权限在用户权限列表中
|
|
||||||
wrapper.and(w ->
|
|
||||||
w.isNull(Menu::getPermission)
|
|
||||||
.or()
|
|
||||||
.in(Menu::getPermission, permissionCodes)
|
|
||||||
);
|
);
|
||||||
|
if (roleMenus.isEmpty()) {
|
||||||
|
log.warn("用户角色未配置任何菜单可见性,用户 ID: {}", userId);
|
||||||
|
return new ArrayList<>();
|
||||||
|
}
|
||||||
|
|
||||||
log.info("非超管用户,根据权限过滤菜单,权限编码数量:{}", permissionCodes.size());
|
Set<Long> visibleMenuIds = roleMenus.stream()
|
||||||
allMenus = menuMapper.selectList(wrapper);
|
.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);
|
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.common.exception.BusinessException;
|
||||||
import com.lesingle.creation.dto.role.CreateRoleDTO;
|
import com.lesingle.creation.dto.role.CreateRoleDTO;
|
||||||
import com.lesingle.creation.dto.role.UpdateRoleDTO;
|
import com.lesingle.creation.dto.role.UpdateRoleDTO;
|
||||||
|
import com.lesingle.creation.entity.Permission;
|
||||||
import com.lesingle.creation.entity.Role;
|
import com.lesingle.creation.entity.Role;
|
||||||
|
import com.lesingle.creation.entity.RoleMenu;
|
||||||
import com.lesingle.creation.entity.RolePermission;
|
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.RoleMapper;
|
||||||
import com.lesingle.creation.mapper.RolePermissionMapper;
|
import com.lesingle.creation.mapper.RolePermissionMapper;
|
||||||
import com.lesingle.creation.service.RoleService;
|
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 RoleMapper roleMapper;
|
||||||
private final RolePermissionMapper rolePermissionMapper;
|
private final RolePermissionMapper rolePermissionMapper;
|
||||||
|
private final PermissionMapper permissionMapper;
|
||||||
|
private final RoleMenuMapper roleMenuMapper;
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@Transactional(rollbackFor = Exception.class)
|
@Transactional(rollbackFor = Exception.class)
|
||||||
@ -71,6 +77,22 @@ public class RoleServiceImpl extends ServiceImpl<RoleMapper, Role> implements Ro
|
|||||||
log.info("角色权限关联创建成功,权限数量:{}", dto.getPermissionIds().size());
|
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);
|
return convertToDetailVO(role);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -98,13 +120,16 @@ public class RoleServiceImpl extends ServiceImpl<RoleMapper, Role> implements Ro
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public RoleDetailVO detail(Long id) {
|
public RoleDetailVO detail(Long id, Long tenantId) {
|
||||||
log.info("查询角色详情,角色 ID: {}", id);
|
log.info("查询角色详情,角色 ID: {}", id);
|
||||||
|
|
||||||
Role role = roleMapper.selectById(id);
|
Role role = roleMapper.selectById(id);
|
||||||
if (role == null) {
|
if (role == null) {
|
||||||
throw new BusinessException("角色不存在");
|
throw new BusinessException("角色不存在");
|
||||||
}
|
}
|
||||||
|
if (tenantId != null && role.getTenantId() != null && !role.getTenantId().equals(tenantId)) {
|
||||||
|
throw new BusinessException("角色不存在或不属于当前租户");
|
||||||
|
}
|
||||||
|
|
||||||
return convertToDetailVO(role);
|
return convertToDetailVO(role);
|
||||||
}
|
}
|
||||||
@ -118,6 +143,9 @@ public class RoleServiceImpl extends ServiceImpl<RoleMapper, Role> implements Ro
|
|||||||
if (existingRole == null) {
|
if (existingRole == null) {
|
||||||
throw new BusinessException("角色不存在");
|
throw new BusinessException("角色不存在");
|
||||||
}
|
}
|
||||||
|
if (existingRole.getTenantId() != null && tenantId != null && !existingRole.getTenantId().equals(tenantId)) {
|
||||||
|
throw new BusinessException("角色不存在或不属于当前租户");
|
||||||
|
}
|
||||||
|
|
||||||
// 如果更新了 code,检查是否冲突
|
// 如果更新了 code,检查是否冲突
|
||||||
if (dto.getCode() != null && !dto.getCode().isEmpty()) {
|
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));
|
return convertToDetailVO(roleMapper.selectById(id));
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@Transactional(rollbackFor = Exception.class)
|
@Transactional(rollbackFor = Exception.class)
|
||||||
public void delete(Long id) {
|
public void delete(Long id, Long tenantId) {
|
||||||
log.info("删除角色,角色 ID: {}", id);
|
log.info("删除角色,角色 ID: {}", id);
|
||||||
|
|
||||||
Role role = roleMapper.selectById(id);
|
Role role = roleMapper.selectById(id);
|
||||||
if (role == null) {
|
if (role == null) {
|
||||||
throw new BusinessException("角色不存在");
|
throw new BusinessException("角色不存在");
|
||||||
}
|
}
|
||||||
|
if (role.getTenantId() != null && tenantId != null && !role.getTenantId().equals(tenantId)) {
|
||||||
|
throw new BusinessException("角色不存在或不属于当前租户");
|
||||||
|
}
|
||||||
|
|
||||||
// 删除角色关联
|
// 删除角色关联
|
||||||
LambdaQueryWrapper<RolePermission> wrapper = new LambdaQueryWrapper<>();
|
LambdaQueryWrapper<RolePermission> wrapper = new LambdaQueryWrapper<>();
|
||||||
@ -215,9 +267,41 @@ public class RoleServiceImpl extends ServiceImpl<RoleMapper, Role> implements Ro
|
|||||||
vo.setUpdateBy(role.getUpdateBy());
|
vo.setUpdateBy(role.getUpdateBy());
|
||||||
vo.setUpdateTime(role.getUpdateTime());
|
vo.setUpdateTime(role.getUpdateTime());
|
||||||
|
|
||||||
// TODO: 获取角色权限
|
// 获取角色权限(用于授权回显)
|
||||||
vo.setPermissionIds(new ArrayList<>());
|
LambdaQueryWrapper<RolePermission> rpWrapper = new LambdaQueryWrapper<>();
|
||||||
vo.setPermissionNames(new ArrayList<>());
|
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;
|
return vo;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -48,4 +48,7 @@ public class RoleDetailVO {
|
|||||||
|
|
||||||
@Schema(description = "权限名称列表")
|
@Schema(description = "权限名称列表")
|
||||||
private List<String> permissionNames;
|
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>
|
<template>
|
||||||
<a-config-provider :theme="themeConfig">
|
<a-config-provider :theme="themeConfig" :locale="zhCN">
|
||||||
<router-view />
|
<router-view />
|
||||||
</a-config-provider>
|
</a-config-provider>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ConfigProviderProps } from "ant-design-vue"
|
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> => {
|
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;
|
return response as unknown as User;
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|||||||
@ -11,16 +11,10 @@ export interface Role {
|
|||||||
updateBy?: number;
|
updateBy?: number;
|
||||||
createTime?: string;
|
createTime?: string;
|
||||||
updateTime?: string;
|
updateTime?: string;
|
||||||
permissions?: Array<{
|
/** 后端返回:权限 ID 列表(用于回显/编辑) */
|
||||||
id: number;
|
permissionIds?: number[];
|
||||||
permission: {
|
/** 后端返回:菜单 ID 列表(用于回显/编辑) */
|
||||||
id: number;
|
menuIds?: number[];
|
||||||
name: string;
|
|
||||||
code: string;
|
|
||||||
resource: string;
|
|
||||||
action: string;
|
|
||||||
};
|
|
||||||
}>;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface CreateRoleForm {
|
export interface CreateRoleForm {
|
||||||
@ -28,6 +22,7 @@ export interface CreateRoleForm {
|
|||||||
code: string;
|
code: string;
|
||||||
description?: string;
|
description?: string;
|
||||||
permissionIds?: number[];
|
permissionIds?: number[];
|
||||||
|
menuIds?: number[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface UpdateRoleForm {
|
export interface UpdateRoleForm {
|
||||||
@ -35,15 +30,19 @@ export interface UpdateRoleForm {
|
|||||||
code?: string;
|
code?: string;
|
||||||
description?: string;
|
description?: string;
|
||||||
permissionIds?: number[];
|
permissionIds?: number[];
|
||||||
|
menuIds?: number[];
|
||||||
}
|
}
|
||||||
|
|
||||||
// 获取角色列表
|
// 获取角色列表
|
||||||
export async function getRolesList(
|
export async function getRolesList(
|
||||||
params: PaginationParams
|
params: PaginationParams,
|
||||||
): Promise<PaginationResponse<Role>> {
|
): Promise<PaginationResponse<Role>> {
|
||||||
const response = await request.get<any, PaginationResponse<Role>>("/api/roles", {
|
const response = await request.get<any, PaginationResponse<Role>>(
|
||||||
params,
|
"/api/roles",
|
||||||
});
|
{
|
||||||
|
params,
|
||||||
|
},
|
||||||
|
);
|
||||||
return response;
|
return response;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -68,7 +67,7 @@ export async function createRole(data: CreateRoleForm): Promise<Role> {
|
|||||||
// 更新角色
|
// 更新角色
|
||||||
export async function updateRole(
|
export async function updateRole(
|
||||||
id: number,
|
id: number,
|
||||||
data: UpdateRoleForm
|
data: UpdateRoleForm,
|
||||||
): Promise<Role> {
|
): Promise<Role> {
|
||||||
const response = await request.put<any, Role>(`/api/roles/${id}`, data);
|
const response = await request.put<any, Role>(`/api/roles/${id}`, data);
|
||||||
return response;
|
return response;
|
||||||
|
|||||||
@ -58,7 +58,8 @@ export function useListRequest<
|
|||||||
} as PaginationParams & P;
|
} as PaginationParams & P;
|
||||||
|
|
||||||
const response = await requestFn(params);
|
const response = await requestFn(params);
|
||||||
dataSource.value = response.list;
|
const list = (response.list ?? response.records ?? []) as T[];
|
||||||
|
dataSource.value = list;
|
||||||
pagination.total = response.total;
|
pagination.total = response.total;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
message.error(errorMessage);
|
message.error(errorMessage);
|
||||||
|
|||||||
@ -7,7 +7,7 @@ import { useAuthStore } from "@/stores/auth";
|
|||||||
interface PermissionDirectiveValue {
|
interface PermissionDirectiveValue {
|
||||||
// 权限码或权限码数组
|
// 权限码或权限码数组
|
||||||
permission: string | string[];
|
permission: string | string[];
|
||||||
// 是否隐藏元素(默认 false,即禁用)
|
// 是否隐藏元素(默认 true:无权限直接隐藏)
|
||||||
hide?: boolean;
|
hide?: boolean;
|
||||||
// 是否需要所有权限(默认 false,即任一权限即可)
|
// 是否需要所有权限(默认 false,即任一权限即可)
|
||||||
all?: boolean;
|
all?: boolean;
|
||||||
@ -73,13 +73,17 @@ function handlePermission(
|
|||||||
if (typeof value === "string" || Array.isArray(value)) {
|
if (typeof value === "string" || Array.isArray(value)) {
|
||||||
config = {
|
config = {
|
||||||
permission: value,
|
permission: value,
|
||||||
hide: modifiers.hide || false,
|
// 默认隐藏;显式 .disable 才走“禁用”逻辑
|
||||||
|
hide: modifiers.disable ? false : true,
|
||||||
all: needAll,
|
all: needAll,
|
||||||
};
|
};
|
||||||
} else {
|
} else {
|
||||||
config = {
|
config = {
|
||||||
permission: value.permission,
|
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,
|
all: value.all ?? needAll,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,6 +1,8 @@
|
|||||||
import { createApp } from "vue"
|
import { createApp } from "vue"
|
||||||
import { createPinia } from "pinia"
|
import { createPinia } from "pinia"
|
||||||
import Antd from "ant-design-vue"
|
import Antd from "ant-design-vue"
|
||||||
|
import dayjs from "dayjs"
|
||||||
|
import "dayjs/locale/zh-cn"
|
||||||
import "ant-design-vue/dist/reset.css"
|
import "ant-design-vue/dist/reset.css"
|
||||||
import "./styles/global.scss"
|
import "./styles/global.scss"
|
||||||
import "./styles/theme.scss"
|
import "./styles/theme.scss"
|
||||||
@ -9,6 +11,9 @@ import router from "./router"
|
|||||||
import { useAuthStore } from "./stores/auth"
|
import { useAuthStore } from "./stores/auth"
|
||||||
import { setupPermissionDirective } from "./directives/permission"
|
import { setupPermissionDirective } from "./directives/permission"
|
||||||
|
|
||||||
|
// dayjs 全局中文(简体)
|
||||||
|
dayjs.locale("zh-cn")
|
||||||
|
|
||||||
const app = createApp(App)
|
const app = createApp(App)
|
||||||
const pinia = createPinia()
|
const pinia = createPinia()
|
||||||
|
|
||||||
|
|||||||
@ -1,146 +1,277 @@
|
|||||||
import { defineStore } from "pinia"
|
import { defineStore } from "pinia";
|
||||||
import { ref, computed } from "vue"
|
import { ref, computed } from "vue";
|
||||||
import type { User, LoginForm } from "@/types/auth"
|
import type { User, LoginForm } from "@/types/auth";
|
||||||
import { authApi } from "@/api/auth"
|
import { authApi } from "@/api/auth";
|
||||||
import { menusApi, type Menu } from "@/api/menus"
|
import { menusApi, type Menu } from "@/api/menus";
|
||||||
import { getToken, setToken, removeToken, getTenantCode } from "@/utils/auth"
|
import {
|
||||||
|
getToken,
|
||||||
|
setToken,
|
||||||
|
removeToken,
|
||||||
|
getTenantCode,
|
||||||
|
setTenantId,
|
||||||
|
removeTenantId,
|
||||||
|
} from "@/utils/auth";
|
||||||
|
|
||||||
export const useAuthStore = defineStore("auth", () => {
|
export const useAuthStore = defineStore("auth", () => {
|
||||||
const user = ref<User | null>(null)
|
const user = ref<User | null>(null);
|
||||||
const token = ref<string>(getToken() || "")
|
const token = ref<string>(getToken() || "");
|
||||||
const loading = ref<boolean>(false)
|
const loading = ref<boolean>(false);
|
||||||
const menus = ref<Menu[]>([])
|
const menus = ref<Menu[]>([]);
|
||||||
|
|
||||||
const isAuthenticated = computed(() => !!token.value)
|
const isAuthenticated = computed(() => !!token.value);
|
||||||
|
|
||||||
// 获取当前用户的租户编码(优先从用户信息获取,其次从 URL 获取)
|
// 获取当前用户的租户编码(优先从用户信息获取,其次从 URL 获取)
|
||||||
const tenantCode = computed(() => {
|
const tenantCode = computed(() => {
|
||||||
return user.value?.tenantCode || getTenantCode()
|
return user.value?.tenantCode || getTenantCode();
|
||||||
})
|
});
|
||||||
|
|
||||||
// 检查是否有指定角色
|
// 检查是否有指定角色
|
||||||
const hasRole = (role: string): boolean => {
|
const hasRole = (role: string): boolean => {
|
||||||
return user.value?.roles?.includes(role) ?? false
|
return user.value?.roles?.includes(role) ?? false;
|
||||||
}
|
};
|
||||||
|
|
||||||
// 检查是否为超级管理员
|
// 检查是否为超级管理员
|
||||||
const isSuperAdmin = (): boolean => {
|
const isSuperAdmin = (): boolean => {
|
||||||
return user.value?.roles?.includes('super_admin') ?? false
|
return user.value?.roles?.includes("super_admin") ?? false;
|
||||||
}
|
};
|
||||||
|
|
||||||
// 检查是否有指定权限
|
// 检查是否有指定权限
|
||||||
const hasPermission = (permission: string): boolean => {
|
const hasPermission = (permission: string): boolean => {
|
||||||
// 超级管理员拥有所有权限
|
// 超级管理员拥有所有权限
|
||||||
if (isSuperAdmin()) return true
|
if (isSuperAdmin()) return true;
|
||||||
return user.value?.permissions?.includes(permission) ?? false
|
return user.value?.permissions?.includes(permission) ?? false;
|
||||||
}
|
};
|
||||||
|
|
||||||
// 检查是否有任一角色
|
// 检查是否有任一角色
|
||||||
const hasAnyRole = (roles: string[]): boolean => {
|
const hasAnyRole = (roles: string[]): boolean => {
|
||||||
if (!roles || roles.length === 0) return true
|
if (!roles || roles.length === 0) return true;
|
||||||
return roles.some((role) => hasRole(role))
|
return roles.some((role) => hasRole(role));
|
||||||
}
|
};
|
||||||
|
|
||||||
// 检查是否有任一权限
|
// 检查是否有任一权限
|
||||||
const hasAnyPermission = (permissions: string[]): boolean => {
|
const hasAnyPermission = (permissions: string[]): boolean => {
|
||||||
if (!permissions || permissions.length === 0) return true
|
if (!permissions || permissions.length === 0) return true;
|
||||||
// 超级管理员拥有所有权限
|
// 超级管理员拥有所有权限
|
||||||
if (isSuperAdmin()) return true
|
if (isSuperAdmin()) return true;
|
||||||
return permissions.some((perm) => hasPermission(perm))
|
return permissions.some((perm) => hasPermission(perm));
|
||||||
}
|
};
|
||||||
|
|
||||||
const login = async (form: LoginForm) => {
|
const login = async (form: LoginForm) => {
|
||||||
const response = await authApi.login(form)
|
const response = await authApi.login(form);
|
||||||
token.value = response.token
|
token.value = response.token;
|
||||||
user.value = response.user
|
user.value = response.user;
|
||||||
|
|
||||||
// 使用租户编码作为 cookie path(不再存储到 localStorage)
|
// 使用租户编码作为 cookie path(不再存储到 localStorage)
|
||||||
if (response.user.tenantCode) {
|
if (response.user.tenantCode) {
|
||||||
setToken(response.token, response.user.tenantCode)
|
setToken(response.token, response.user.tenantCode);
|
||||||
} else {
|
} else {
|
||||||
setToken(response.token)
|
setToken(response.token);
|
||||||
|
}
|
||||||
|
if (response.user.tenantId) {
|
||||||
|
setTenantId(response.user.tenantId);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 登录后获取用户菜单
|
// 登录后获取用户菜单
|
||||||
await fetchUserMenus()
|
await fetchUserMenus();
|
||||||
return response
|
return response;
|
||||||
}
|
};
|
||||||
|
|
||||||
const logout = async () => {
|
const logout = async () => {
|
||||||
try {
|
try {
|
||||||
await authApi.logout()
|
await authApi.logout();
|
||||||
} finally {
|
} finally {
|
||||||
token.value = ""
|
token.value = "";
|
||||||
// 删除 token cookie,使用当前用户的租户编码或 URL 中的租户编码
|
// 删除 token cookie,使用当前用户的租户编码或 URL 中的租户编码
|
||||||
const tenantCode = user.value?.tenantCode || getTenantCode()
|
const tenantCode = user.value?.tenantCode || getTenantCode();
|
||||||
removeToken(tenantCode || undefined)
|
removeToken(tenantCode || undefined);
|
||||||
user.value = null
|
removeTenantId();
|
||||||
menus.value = []
|
user.value = null;
|
||||||
|
menus.value = [];
|
||||||
// 重置动态路由标记
|
// 重置动态路由标记
|
||||||
if (typeof window !== "undefined") {
|
if (typeof window !== "undefined") {
|
||||||
const { resetDynamicRoutes } = await import("@/router")
|
const { resetDynamicRoutes } = await import("@/router");
|
||||||
resetDynamicRoutes()
|
resetDynamicRoutes();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
|
|
||||||
const fetchUserInfo = async () => {
|
const fetchUserInfo = async () => {
|
||||||
if (!token.value) {
|
if (!token.value) {
|
||||||
throw new Error("未登录")
|
throw new Error("未登录");
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
loading.value = true
|
loading.value = true;
|
||||||
const response = await authApi.getUserInfo()
|
const response = await authApi.getUserInfo();
|
||||||
user.value = response
|
user.value = response;
|
||||||
// 获取用户菜单
|
// 获取用户菜单
|
||||||
await fetchUserMenus()
|
await fetchUserMenus();
|
||||||
return response
|
return response;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
// 如果获取用户信息失败,清除 token
|
// 如果获取用户信息失败,清除 token
|
||||||
token.value = ""
|
token.value = "";
|
||||||
user.value = null
|
user.value = null;
|
||||||
menus.value = []
|
menus.value = [];
|
||||||
removeToken()
|
removeToken();
|
||||||
throw error
|
throw error;
|
||||||
} finally {
|
} finally {
|
||||||
loading.value = false
|
loading.value = false;
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
|
|
||||||
const fetchUserMenus = async () => {
|
const fetchUserMenus = async () => {
|
||||||
if (!token.value) {
|
if (!token.value) {
|
||||||
return
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const userMenus = await menusApi.getUserMenus()
|
const userMenus = await menusApi.getUserMenus();
|
||||||
menus.value = userMenus
|
// 前端兜底:当数据库菜单缺失时,补齐关键入口,避免页面不可达
|
||||||
return userMenus
|
// 场景:系统管理下的「角色管理」「权限管理」在数据库未配置或被误删
|
||||||
|
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) {
|
} catch (error) {
|
||||||
console.error("获取用户菜单失败:", error)
|
console.error("获取用户菜单失败:", error);
|
||||||
menus.value = []
|
menus.value = [];
|
||||||
throw error
|
throw error;
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
|
|
||||||
const updateToken = (newToken: string) => {
|
const updateToken = (newToken: string) => {
|
||||||
token.value = newToken
|
token.value = newToken;
|
||||||
// 使用当前用户的租户编码更新 token cookie
|
// 使用当前用户的租户编码更新 token cookie
|
||||||
const tenantCode = user.value?.tenantCode || getTenantCode()
|
const tenantCode = user.value?.tenantCode || getTenantCode();
|
||||||
setToken(newToken, tenantCode || undefined)
|
setToken(newToken, tenantCode || undefined);
|
||||||
}
|
};
|
||||||
|
|
||||||
// 初始化:如果有 token 但没有用户信息,自动获取
|
// 初始化:如果有 token 但没有用户信息,自动获取
|
||||||
const initAuth = async () => {
|
const initAuth = async () => {
|
||||||
if (token.value && !user.value) {
|
if (token.value && !user.value) {
|
||||||
try {
|
try {
|
||||||
await fetchUserInfo()
|
await fetchUserInfo();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("自动获取用户信息失败:", error)
|
console.error("自动获取用户信息失败:", error);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
|
|
||||||
return {
|
return {
|
||||||
user,
|
user,
|
||||||
@ -159,5 +290,5 @@ export const useAuthStore = defineStore("auth", () => {
|
|||||||
fetchUserMenus,
|
fetchUserMenus,
|
||||||
updateToken,
|
updateToken,
|
||||||
initAuth,
|
initAuth,
|
||||||
}
|
};
|
||||||
})
|
});
|
||||||
|
|||||||
@ -10,8 +10,20 @@ export interface PaginationParams {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export interface PaginationResponse<T> {
|
export interface PaginationResponse<T> {
|
||||||
list: T[];
|
/**
|
||||||
|
* 兼容不同后端分页字段:
|
||||||
|
* - list: 常见返回
|
||||||
|
* - records: MyBatis-Plus / 自定义返回
|
||||||
|
*/
|
||||||
|
list?: T[];
|
||||||
|
records?: T[];
|
||||||
total: number;
|
total: number;
|
||||||
page: number;
|
/** 当前页:page 或 current */
|
||||||
pageSize: number;
|
page?: number;
|
||||||
|
current?: number;
|
||||||
|
/** 每页大小:pageSize 或 size */
|
||||||
|
pageSize?: number;
|
||||||
|
size?: number;
|
||||||
|
/** 总页数(可选) */
|
||||||
|
pages?: number;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,4 +1,5 @@
|
|||||||
const TOKEN_KEY = "token";
|
const TOKEN_KEY = "token";
|
||||||
|
const TENANT_ID_KEY = "tenantId";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 从当前 URL 路径中提取租户编码
|
* 从当前 URL 路径中提取租户编码
|
||||||
@ -22,7 +23,7 @@ function getTenantCodeFromUrl(): string | null {
|
|||||||
function setCookie(
|
function setCookie(
|
||||||
name: string,
|
name: string,
|
||||||
value: string,
|
value: string,
|
||||||
options: { path?: string; expires?: number; maxAge?: number } = {}
|
options: { path?: string; expires?: number; maxAge?: number } = {},
|
||||||
): void {
|
): void {
|
||||||
const { path = "/", expires, maxAge } = options;
|
const { path = "/", expires, maxAge } = options;
|
||||||
let cookieString = `${name}=${encodeURIComponent(value)}; path=${path}`;
|
let cookieString = `${name}=${encodeURIComponent(value)}; path=${path}`;
|
||||||
@ -77,7 +78,7 @@ export const getToken = (): string | null => {
|
|||||||
return getAllTokenCookies();
|
return getAllTokenCookies();
|
||||||
};
|
};
|
||||||
|
|
||||||
export const setToken = (token: string, tenantCode?: string): void => {
|
export const setToken = (token: string, _tenantCode?: string): void => {
|
||||||
// 始终将 token 存储在根路径下,确保所有页面都能访问
|
// 始终将 token 存储在根路径下,确保所有页面都能访问
|
||||||
const base = import.meta.env.BASE_URL || "/";
|
const base = import.meta.env.BASE_URL || "/";
|
||||||
const basePath = base.endsWith("/") ? base.slice(0, -1) : base;
|
const basePath = base.endsWith("/") ? base.slice(0, -1) : base;
|
||||||
@ -120,31 +121,35 @@ export const removeTenantCode = (): void => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 获取租户ID(已废弃,不再从 localStorage 获取)
|
* 获取租户ID
|
||||||
* 保留此函数以保持兼容性,返回 null
|
* 刷新页面时,用户信息尚未拉取前需要依赖此值补全请求头
|
||||||
*/
|
*/
|
||||||
export const getTenantId = (): string | null => {
|
export const getTenantId = (): string | null => {
|
||||||
// 不再从 localStorage 获取,租户ID 从用户信息中获取
|
return getCookie(TENANT_ID_KEY);
|
||||||
return null;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 设置租户ID(已废弃,不再存储到 localStorage)
|
* 设置租户ID
|
||||||
* 保留此函数以保持兼容性,但不执行任何操作
|
|
||||||
*/
|
*/
|
||||||
export const setTenantId = (_tenantId: number | string): void => {
|
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 => {
|
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 => {
|
export const clearAuth = (): void => {
|
||||||
// 只清除 token,租户信息不再存储在 localStorage
|
// 只清除 token,租户信息不再存储在 localStorage
|
||||||
removeToken();
|
removeToken();
|
||||||
|
removeTenantId();
|
||||||
};
|
};
|
||||||
|
|||||||
@ -4,7 +4,7 @@ import axios, {
|
|||||||
type AxiosResponse,
|
type AxiosResponse,
|
||||||
} from "axios";
|
} from "axios";
|
||||||
import { message } from "ant-design-vue";
|
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 { useAuthStore } from "@/stores/auth";
|
||||||
import router from "@/router";
|
import router from "@/router";
|
||||||
|
|
||||||
@ -87,7 +87,7 @@ service.interceptors.request.use(
|
|||||||
// 租户编码从 URL 获取,租户ID 从用户信息获取
|
// 租户编码从 URL 获取,租户ID 从用户信息获取
|
||||||
const tenantCode = getTenantCode();
|
const tenantCode = getTenantCode();
|
||||||
const authStore = useAuthStore();
|
const authStore = useAuthStore();
|
||||||
const tenantId = authStore.user?.tenantId;
|
const tenantId = authStore.user?.tenantId ?? getTenantId();
|
||||||
|
|
||||||
if (config.headers) {
|
if (config.headers) {
|
||||||
if (tenantCode) {
|
if (tenantCode) {
|
||||||
|
|||||||
@ -26,8 +26,15 @@
|
|||||||
</a-table>
|
</a-table>
|
||||||
|
|
||||||
<!-- 新增/编辑角色弹窗 -->
|
<!-- 新增/编辑角色弹窗 -->
|
||||||
<a-modal v-model:open="modalVisible" :title="modalTitle" :confirm-loading="submitLoading" @ok="handleSubmit"
|
<a-modal
|
||||||
@cancel="handleCancel" width="800px" :loading="detailLoading">
|
:open="modalVisible"
|
||||||
|
:title="modalTitle"
|
||||||
|
:confirm-loading="submitLoading"
|
||||||
|
width="800px"
|
||||||
|
@ok="handleSubmit"
|
||||||
|
@cancel="handleCancel"
|
||||||
|
@update:open="handleOpenChange"
|
||||||
|
>
|
||||||
<a-tabs v-model:activeKey="activeTab">
|
<a-tabs v-model:activeKey="activeTab">
|
||||||
<a-tab-pane key="basic" tab="基本信息">
|
<a-tab-pane key="basic" tab="基本信息">
|
||||||
<a-form ref="formRef" :model="form" :rules="rules" :label-col="{ span: 6 }" :wrapper-col="{ span: 18 }">
|
<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;">
|
<div v-if="permissionsLoading" style="text-align: center; padding: 40px;">
|
||||||
<a-spin size="large" />
|
<a-spin size="large" />
|
||||||
</div>
|
</div>
|
||||||
<div v-else>
|
<div v-else class="permission-scroll-wrapper">
|
||||||
<div v-for="(group, resource) in permissionGroups" :key="resource" class="permission-group">
|
<div v-for="(group, resource) in permissionGroups" :key="resource" class="permission-group">
|
||||||
<div class="permission-group-header">
|
<div class="permission-group-header">
|
||||||
<a-checkbox :checked="isResourceAllChecked(resource)" :indeterminate="isResourceIndeterminate(resource)"
|
<a-checkbox :checked="isResourceAllChecked(resource)" :indeterminate="isResourceIndeterminate(resource)"
|
||||||
@ -70,6 +77,25 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</a-tab-pane>
|
</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-tabs>
|
||||||
</a-modal>
|
</a-modal>
|
||||||
</div>
|
</div>
|
||||||
@ -81,6 +107,7 @@ import { message, Modal } from 'ant-design-vue'
|
|||||||
import type { TableColumnsType, FormInstance } from 'ant-design-vue'
|
import type { TableColumnsType, FormInstance } from 'ant-design-vue'
|
||||||
import { rolesApi, type Role, type CreateRoleForm } from '@/api/roles'
|
import { rolesApi, type Role, type CreateRoleForm } from '@/api/roles'
|
||||||
import { permissionsApi, type Permission } from '@/api/permissions'
|
import { permissionsApi, type Permission } from '@/api/permissions'
|
||||||
|
import { menusApi, type Menu } from '@/api/menus'
|
||||||
import { useListRequest } from '@/composables/useListRequest'
|
import { useListRequest } from '@/composables/useListRequest'
|
||||||
import { useAuthStore } from '@/stores/auth'
|
import { useAuthStore } from '@/stores/auth'
|
||||||
|
|
||||||
@ -89,6 +116,7 @@ const authStore = useAuthStore()
|
|||||||
const submitLoading = ref(false)
|
const submitLoading = ref(false)
|
||||||
const detailLoading = ref(false)
|
const detailLoading = ref(false)
|
||||||
const permissionsLoading = ref(false)
|
const permissionsLoading = ref(false)
|
||||||
|
const menusLoading = ref(false)
|
||||||
const modalVisible = ref(false)
|
const modalVisible = ref(false)
|
||||||
const modalTitle = ref('新增角色')
|
const modalTitle = ref('新增角色')
|
||||||
const formRef = ref<FormInstance>()
|
const formRef = ref<FormInstance>()
|
||||||
@ -99,7 +127,7 @@ const activeTab = ref('basic')
|
|||||||
const allPermissions = ref<Permission[]>([])
|
const allPermissions = ref<Permission[]>([])
|
||||||
const permissionGroups = computed(() => {
|
const permissionGroups = computed(() => {
|
||||||
const groups: Record<string, Permission[]> = {}
|
const groups: Record<string, Permission[]> = {}
|
||||||
allPermissions.value.forEach((permission) => {
|
;(allPermissions.value || []).forEach((permission) => {
|
||||||
if (!groups[permission.resource]) {
|
if (!groups[permission.resource]) {
|
||||||
groups[permission.resource] = []
|
groups[permission.resource] = []
|
||||||
}
|
}
|
||||||
@ -125,6 +153,7 @@ const form = reactive<CreateRoleForm>({
|
|||||||
code: '',
|
code: '',
|
||||||
description: '',
|
description: '',
|
||||||
permissionIds: [],
|
permissionIds: [],
|
||||||
|
menuIds: [],
|
||||||
})
|
})
|
||||||
|
|
||||||
const rules = {
|
const rules = {
|
||||||
@ -160,7 +189,7 @@ const fetchAllPermissions = async () => {
|
|||||||
permissionsLoading.value = true
|
permissionsLoading.value = true
|
||||||
try {
|
try {
|
||||||
const response = await permissionsApi.getList({ page: 1, pageSize: 100 })
|
const response = await permissionsApi.getList({ page: 1, pageSize: 100 })
|
||||||
allPermissions.value = response.list
|
allPermissions.value = response.list ?? response.records ?? []
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
message.error('获取权限列表失败')
|
message.error('获取权限列表失败')
|
||||||
} finally {
|
} 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 getResourceName = (resource: string): string => {
|
||||||
const resourceMap: Record<string, string> = {
|
const resourceMap: Record<string, string> = {
|
||||||
@ -258,6 +311,7 @@ const handleAdd = () => {
|
|||||||
form.code = ''
|
form.code = ''
|
||||||
form.description = ''
|
form.description = ''
|
||||||
form.permissionIds = []
|
form.permissionIds = []
|
||||||
|
form.menuIds = []
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -275,7 +329,9 @@ const handleEdit = async (record: Role) => {
|
|||||||
form.code = detail.code
|
form.code = detail.code
|
||||||
form.description = detail.description || ''
|
form.description = detail.description || ''
|
||||||
// 回显权限
|
// 回显权限
|
||||||
form.permissionIds = detail.permissions?.map((rp) => rp.permission.id) || []
|
form.permissionIds = detail.permissionIds || []
|
||||||
|
// 回显菜单
|
||||||
|
form.menuIds = detail.menuIds || []
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
message.error('获取角色详情失败')
|
message.error('获取角色详情失败')
|
||||||
modalVisible.value = false
|
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 = () => {
|
const handleCancel = () => {
|
||||||
modalVisible.value = false
|
modalVisible.value = false
|
||||||
@ -345,6 +410,7 @@ const handleCancel = () => {
|
|||||||
// 初始化加载权限列表
|
// 初始化加载权限列表
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
fetchAllPermissions()
|
fetchAllPermissions()
|
||||||
|
fetchAllMenus()
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
@ -371,4 +437,20 @@ onMounted(() => {
|
|||||||
margin-left: 4px;
|
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>
|
</style>
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user