feat(shared): 添加操作帮助面板组件
- 实现 ActionHelpPanel 组件,提供操作详情和帮助信息展示 - 添加完整的 CSS 样式文件,支持响应式布局和主题适配 - 集成 Ant Design 的 Drawer 和 Collapse 组件 - 支持当前操作和所有可用操作的分类展示 - 实现操作步骤、注意事项、快捷键等功能说明 - 添加图标、标签、权限要求等信息展示 - 支持操作列表点击切换和实时预览功能master
parent
bf537d6074
commit
78e77cf260
|
|
@ -87,6 +87,11 @@ src
|
||||||
2. `/auth/login`
|
2. `/auth/login`
|
||||||
3. `/api/users/me`(获取用户信息与 isAdmin)
|
3. `/api/users/me`(获取用户信息与 isAdmin)
|
||||||
|
|
||||||
|
### 验证码开关(系统参数)
|
||||||
|
- 系统参数 `security.captcha.enabled` 控制验证码是否启用(true/false)
|
||||||
|
- 系统启动时加载 `sys_param` 到 Redis Hash:`sys:param:{paramKey}`(字段:value/type)
|
||||||
|
- 前端登录页根据系统参数决定是否展示验证码
|
||||||
|
|
||||||
### 权限菜单渲染
|
### 权限菜单渲染
|
||||||
1. `/api/permissions/me` 获取权限列表
|
1. `/api/permissions/me` 获取权限列表
|
||||||
2. 前端构建树形菜单
|
2. 前端构建树形菜单
|
||||||
|
|
@ -101,4 +106,3 @@ src
|
||||||
- 添加审计日志落库策略
|
- 添加审计日志落库策略
|
||||||
- 任务管理模块完善
|
- 任务管理模块完善
|
||||||
- 权限树缓存与增量刷新策略
|
- 权限树缓存与增量刷新策略
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -9,9 +9,7 @@ public class DeviceCodeRequest {
|
||||||
private String username;
|
private String username;
|
||||||
@NotBlank
|
@NotBlank
|
||||||
private String password;
|
private String password;
|
||||||
@NotBlank
|
|
||||||
private String captchaId;
|
private String captchaId;
|
||||||
@NotBlank
|
|
||||||
private String captchaCode;
|
private String captchaCode;
|
||||||
private String deviceName;
|
private String deviceName;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -9,9 +9,7 @@ public class LoginRequest {
|
||||||
private String username;
|
private String username;
|
||||||
@NotBlank
|
@NotBlank
|
||||||
private String password;
|
private String password;
|
||||||
@NotBlank
|
|
||||||
private String captchaId;
|
private String captchaId;
|
||||||
@NotBlank
|
|
||||||
private String captchaCode;
|
private String captchaCode;
|
||||||
private String deviceCode;
|
private String deviceCode;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -14,4 +14,11 @@ public final class RedisKeys {
|
||||||
public static String refreshTokenKey(Long userId, String deviceCode) {
|
public static String refreshTokenKey(Long userId, String deviceCode) {
|
||||||
return "refresh:" + userId + ":" + deviceCode;
|
return "refresh:" + userId + ":" + deviceCode;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static String sysParamKey(String paramKey) {
|
||||||
|
return "sys:param:" + paramKey;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static final String SYS_PARAM_FIELD_VALUE = "value";
|
||||||
|
public static final String SYS_PARAM_FIELD_TYPE = "type";
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -26,6 +26,7 @@ public class SecurityConfig {
|
||||||
.sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
|
.sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
|
||||||
.authorizeHttpRequests(auth -> auth
|
.authorizeHttpRequests(auth -> auth
|
||||||
.requestMatchers("/auth/**").permitAll()
|
.requestMatchers("/auth/**").permitAll()
|
||||||
|
.requestMatchers("/api/params/value").permitAll()
|
||||||
.anyRequest().authenticated()
|
.anyRequest().authenticated()
|
||||||
)
|
)
|
||||||
.addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class);
|
.addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class);
|
||||||
|
|
|
||||||
|
|
@ -8,6 +8,8 @@ import com.imeeting.auth.dto.RefreshRequest;
|
||||||
import com.imeeting.auth.dto.TokenResponse;
|
import com.imeeting.auth.dto.TokenResponse;
|
||||||
import com.imeeting.common.ApiResponse;
|
import com.imeeting.common.ApiResponse;
|
||||||
import com.imeeting.common.RedisKeys;
|
import com.imeeting.common.RedisKeys;
|
||||||
|
import com.imeeting.common.SysParamKeys;
|
||||||
|
import com.imeeting.service.SysParamService;
|
||||||
import com.imeeting.service.AuthService;
|
import com.imeeting.service.AuthService;
|
||||||
import com.wf.captcha.SpecCaptcha;
|
import com.wf.captcha.SpecCaptcha;
|
||||||
import jakarta.validation.Valid;
|
import jakarta.validation.Valid;
|
||||||
|
|
@ -24,18 +26,24 @@ public class AuthController {
|
||||||
private final AuthService authService;
|
private final AuthService authService;
|
||||||
private final StringRedisTemplate stringRedisTemplate;
|
private final StringRedisTemplate stringRedisTemplate;
|
||||||
private final JwtTokenProvider jwtTokenProvider;
|
private final JwtTokenProvider jwtTokenProvider;
|
||||||
|
private final SysParamService sysParamService;
|
||||||
|
|
||||||
@Value("${app.captcha.ttl-seconds:120}")
|
@Value("${app.captcha.ttl-seconds:120}")
|
||||||
private long captchaTtlSeconds;
|
private long captchaTtlSeconds;
|
||||||
|
|
||||||
public AuthController(AuthService authService, StringRedisTemplate stringRedisTemplate, JwtTokenProvider jwtTokenProvider) {
|
public AuthController(AuthService authService, StringRedisTemplate stringRedisTemplate,
|
||||||
|
JwtTokenProvider jwtTokenProvider, SysParamService sysParamService) {
|
||||||
this.authService = authService;
|
this.authService = authService;
|
||||||
this.stringRedisTemplate = stringRedisTemplate;
|
this.stringRedisTemplate = stringRedisTemplate;
|
||||||
this.jwtTokenProvider = jwtTokenProvider;
|
this.jwtTokenProvider = jwtTokenProvider;
|
||||||
|
this.sysParamService = sysParamService;
|
||||||
}
|
}
|
||||||
|
|
||||||
@GetMapping("/captcha")
|
@GetMapping("/captcha")
|
||||||
public ApiResponse<CaptchaResponse> captcha() {
|
public ApiResponse<CaptchaResponse> captcha() {
|
||||||
|
if (!isCaptchaEnabled()) {
|
||||||
|
return ApiResponse.error("Captcha disabled");
|
||||||
|
}
|
||||||
SpecCaptcha captcha = new SpecCaptcha(130, 48, 4);
|
SpecCaptcha captcha = new SpecCaptcha(130, 48, 4);
|
||||||
String code = captcha.text();
|
String code = captcha.text();
|
||||||
String imageBase64 = captcha.toBase64();
|
String imageBase64 = captcha.toBase64();
|
||||||
|
|
@ -75,4 +83,9 @@ public class AuthController {
|
||||||
authService.logout(userId, deviceCode);
|
authService.logout(userId, deviceCode);
|
||||||
return ApiResponse.ok(null);
|
return ApiResponse.ok(null);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private boolean isCaptchaEnabled() {
|
||||||
|
String value = sysParamService.getCachedParamValue(SysParamKeys.CAPTCHA_ENABLED, "true");
|
||||||
|
return Boolean.parseBoolean(value);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,19 +1,25 @@
|
||||||
package com.imeeting.controller;
|
package com.imeeting.controller;
|
||||||
|
|
||||||
|
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
|
||||||
import com.imeeting.common.ApiResponse;
|
import com.imeeting.common.ApiResponse;
|
||||||
import com.imeeting.entity.SysRole;
|
import com.imeeting.entity.SysRole;
|
||||||
|
import com.imeeting.entity.SysRolePermission;
|
||||||
|
import com.imeeting.mapper.SysRolePermissionMapper;
|
||||||
import com.imeeting.service.SysRoleService;
|
import com.imeeting.service.SysRoleService;
|
||||||
import org.springframework.web.bind.annotation.*;
|
import org.springframework.web.bind.annotation.*;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
@RestController
|
@RestController
|
||||||
@RequestMapping("/api/roles")
|
@RequestMapping("/api/roles")
|
||||||
public class RoleController {
|
public class RoleController {
|
||||||
private final SysRoleService sysRoleService;
|
private final SysRoleService sysRoleService;
|
||||||
|
private final SysRolePermissionMapper sysRolePermissionMapper;
|
||||||
|
|
||||||
public RoleController(SysRoleService sysRoleService) {
|
public RoleController(SysRoleService sysRoleService, SysRolePermissionMapper sysRolePermissionMapper) {
|
||||||
this.sysRoleService = sysRoleService;
|
this.sysRoleService = sysRoleService;
|
||||||
|
this.sysRolePermissionMapper = sysRolePermissionMapper;
|
||||||
}
|
}
|
||||||
|
|
||||||
@GetMapping
|
@GetMapping
|
||||||
|
|
@ -41,4 +47,49 @@ public class RoleController {
|
||||||
public ApiResponse<Boolean> delete(@PathVariable Long id) {
|
public ApiResponse<Boolean> delete(@PathVariable Long id) {
|
||||||
return ApiResponse.ok(sysRoleService.removeById(id));
|
return ApiResponse.ok(sysRoleService.removeById(id));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@GetMapping("/{id}/permissions")
|
||||||
|
public ApiResponse<List<Long>> listRolePermissions(@PathVariable Long id) {
|
||||||
|
List<SysRolePermission> rows = sysRolePermissionMapper.selectList(
|
||||||
|
new QueryWrapper<SysRolePermission>().eq("role_id", id)
|
||||||
|
);
|
||||||
|
List<Long> permIds = new ArrayList<>();
|
||||||
|
for (SysRolePermission row : rows) {
|
||||||
|
if (row.getPermId() != null) {
|
||||||
|
permIds.add(row.getPermId());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return ApiResponse.ok(permIds);
|
||||||
|
}
|
||||||
|
|
||||||
|
@PostMapping("/{id}/permissions")
|
||||||
|
public ApiResponse<Boolean> saveRolePermissions(@PathVariable Long id, @RequestBody PermissionBindingPayload payload) {
|
||||||
|
List<Long> permIds = payload == null ? null : payload.getPermIds();
|
||||||
|
sysRolePermissionMapper.delete(new QueryWrapper<SysRolePermission>().eq("role_id", id));
|
||||||
|
if (permIds == null || permIds.isEmpty()) {
|
||||||
|
return ApiResponse.ok(true);
|
||||||
|
}
|
||||||
|
for (Long permId : permIds) {
|
||||||
|
if (permId == null) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
SysRolePermission item = new SysRolePermission();
|
||||||
|
item.setRoleId(id);
|
||||||
|
item.setPermId(permId);
|
||||||
|
sysRolePermissionMapper.insert(item);
|
||||||
|
}
|
||||||
|
return ApiResponse.ok(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static class PermissionBindingPayload {
|
||||||
|
private List<Long> permIds;
|
||||||
|
|
||||||
|
public List<Long> getPermIds() {
|
||||||
|
return permIds;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setPermIds(List<Long> permIds) {
|
||||||
|
this.permIds = permIds;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -28,17 +28,36 @@ public class SysParamController {
|
||||||
|
|
||||||
@PostMapping
|
@PostMapping
|
||||||
public ApiResponse<Boolean> create(@RequestBody SysParam param) {
|
public ApiResponse<Boolean> create(@RequestBody SysParam param) {
|
||||||
return ApiResponse.ok(sysParamService.save(param));
|
boolean saved = sysParamService.save(param);
|
||||||
|
if (saved) {
|
||||||
|
sysParamService.syncParamToCache(param);
|
||||||
|
}
|
||||||
|
return ApiResponse.ok(saved);
|
||||||
}
|
}
|
||||||
|
|
||||||
@PutMapping("/{id}")
|
@PutMapping("/{id}")
|
||||||
public ApiResponse<Boolean> update(@PathVariable Long id, @RequestBody SysParam param) {
|
public ApiResponse<Boolean> update(@PathVariable Long id, @RequestBody SysParam param) {
|
||||||
param.setParamId(id);
|
param.setParamId(id);
|
||||||
return ApiResponse.ok(sysParamService.updateById(param));
|
boolean updated = sysParamService.updateById(param);
|
||||||
|
if (updated) {
|
||||||
|
sysParamService.syncParamToCache(param);
|
||||||
|
}
|
||||||
|
return ApiResponse.ok(updated);
|
||||||
}
|
}
|
||||||
|
|
||||||
@DeleteMapping("/{id}")
|
@DeleteMapping("/{id}")
|
||||||
public ApiResponse<Boolean> delete(@PathVariable Long id) {
|
public ApiResponse<Boolean> delete(@PathVariable Long id) {
|
||||||
return ApiResponse.ok(sysParamService.removeById(id));
|
SysParam param = sysParamService.getById(id);
|
||||||
|
boolean removed = sysParamService.removeById(id);
|
||||||
|
if (removed && param != null) {
|
||||||
|
sysParamService.deleteParamCache(param.getParamKey());
|
||||||
|
}
|
||||||
|
return ApiResponse.ok(removed);
|
||||||
|
}
|
||||||
|
|
||||||
|
@GetMapping("/value")
|
||||||
|
public ApiResponse<String> getValue(@RequestParam("key") String key,
|
||||||
|
@RequestParam(value = "defaultValue", required = false) String defaultValue) {
|
||||||
|
return ApiResponse.ok(sysParamService.getCachedParamValue(key, defaultValue));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -3,12 +3,16 @@ package com.imeeting.controller;
|
||||||
import com.imeeting.auth.JwtTokenProvider;
|
import com.imeeting.auth.JwtTokenProvider;
|
||||||
import com.imeeting.common.ApiResponse;
|
import com.imeeting.common.ApiResponse;
|
||||||
import com.imeeting.dto.UserProfile;
|
import com.imeeting.dto.UserProfile;
|
||||||
|
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
|
||||||
import com.imeeting.entity.SysUser;
|
import com.imeeting.entity.SysUser;
|
||||||
|
import com.imeeting.entity.SysUserRole;
|
||||||
|
import com.imeeting.mapper.SysUserRoleMapper;
|
||||||
import com.imeeting.service.SysUserService;
|
import com.imeeting.service.SysUserService;
|
||||||
import io.jsonwebtoken.Claims;
|
import io.jsonwebtoken.Claims;
|
||||||
import org.springframework.security.crypto.password.PasswordEncoder;
|
import org.springframework.security.crypto.password.PasswordEncoder;
|
||||||
import org.springframework.web.bind.annotation.*;
|
import org.springframework.web.bind.annotation.*;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
@RestController
|
@RestController
|
||||||
|
|
@ -17,11 +21,13 @@ public class UserController {
|
||||||
private final SysUserService sysUserService;
|
private final SysUserService sysUserService;
|
||||||
private final PasswordEncoder passwordEncoder;
|
private final PasswordEncoder passwordEncoder;
|
||||||
private final JwtTokenProvider jwtTokenProvider;
|
private final JwtTokenProvider jwtTokenProvider;
|
||||||
|
private final SysUserRoleMapper sysUserRoleMapper;
|
||||||
|
|
||||||
public UserController(SysUserService sysUserService, PasswordEncoder passwordEncoder, JwtTokenProvider jwtTokenProvider) {
|
public UserController(SysUserService sysUserService, PasswordEncoder passwordEncoder, JwtTokenProvider jwtTokenProvider, SysUserRoleMapper sysUserRoleMapper) {
|
||||||
this.sysUserService = sysUserService;
|
this.sysUserService = sysUserService;
|
||||||
this.passwordEncoder = passwordEncoder;
|
this.passwordEncoder = passwordEncoder;
|
||||||
this.jwtTokenProvider = jwtTokenProvider;
|
this.jwtTokenProvider = jwtTokenProvider;
|
||||||
|
this.sysUserRoleMapper = sysUserRoleMapper;
|
||||||
}
|
}
|
||||||
|
|
||||||
@GetMapping
|
@GetMapping
|
||||||
|
|
@ -77,6 +83,39 @@ public class UserController {
|
||||||
return ApiResponse.ok(sysUserService.removeById(id));
|
return ApiResponse.ok(sysUserService.removeById(id));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@GetMapping("/{id}/roles")
|
||||||
|
public ApiResponse<List<Long>> listUserRoles(@PathVariable Long id) {
|
||||||
|
List<SysUserRole> rows = sysUserRoleMapper.selectList(
|
||||||
|
new QueryWrapper<SysUserRole>().eq("user_id", id)
|
||||||
|
);
|
||||||
|
List<Long> roleIds = new ArrayList<>();
|
||||||
|
for (SysUserRole row : rows) {
|
||||||
|
if (row.getRoleId() != null) {
|
||||||
|
roleIds.add(row.getRoleId());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return ApiResponse.ok(roleIds);
|
||||||
|
}
|
||||||
|
|
||||||
|
@PostMapping("/{id}/roles")
|
||||||
|
public ApiResponse<Boolean> saveUserRoles(@PathVariable Long id, @RequestBody RoleBindingPayload payload) {
|
||||||
|
List<Long> roleIds = payload == null ? null : payload.getRoleIds();
|
||||||
|
sysUserRoleMapper.delete(new QueryWrapper<SysUserRole>().eq("user_id", id));
|
||||||
|
if (roleIds == null || roleIds.isEmpty()) {
|
||||||
|
return ApiResponse.ok(true);
|
||||||
|
}
|
||||||
|
for (Long roleId : roleIds) {
|
||||||
|
if (roleId == null) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
SysUserRole item = new SysUserRole();
|
||||||
|
item.setUserId(id);
|
||||||
|
item.setRoleId(roleId);
|
||||||
|
sysUserRoleMapper.insert(item);
|
||||||
|
}
|
||||||
|
return ApiResponse.ok(true);
|
||||||
|
}
|
||||||
|
|
||||||
private Long resolveUserId(String authorization) {
|
private Long resolveUserId(String authorization) {
|
||||||
if (authorization == null || !authorization.startsWith("Bearer ")) {
|
if (authorization == null || !authorization.startsWith("Bearer ")) {
|
||||||
return null;
|
return null;
|
||||||
|
|
@ -85,4 +124,16 @@ public class UserController {
|
||||||
Claims claims = jwtTokenProvider.parseToken(token);
|
Claims claims = jwtTokenProvider.parseToken(token);
|
||||||
return claims.get("userId", Long.class);
|
return claims.get("userId", Long.class);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static class RoleBindingPayload {
|
||||||
|
private List<Long> roleIds;
|
||||||
|
|
||||||
|
public List<Long> getRoleIds() {
|
||||||
|
return roleIds;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setRoleIds(List<Long> roleIds) {
|
||||||
|
this.roleIds = roleIds;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
package com.imeeting.dto;
|
package com.imeeting.dto;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||||
import lombok.Data;
|
import lombok.Data;
|
||||||
|
|
||||||
@Data
|
@Data
|
||||||
|
|
@ -10,5 +11,6 @@ public class UserProfile {
|
||||||
private String email;
|
private String email;
|
||||||
private String phone;
|
private String phone;
|
||||||
private Integer status;
|
private Integer status;
|
||||||
|
@JsonProperty("isAdmin")
|
||||||
private boolean isAdmin;
|
private boolean isAdmin;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,15 +1,25 @@
|
||||||
package com.imeeting.entity;
|
package com.imeeting.entity;
|
||||||
|
|
||||||
|
import com.baomidou.mybatisplus.annotation.FieldFill;
|
||||||
import com.baomidou.mybatisplus.annotation.IdType;
|
import com.baomidou.mybatisplus.annotation.IdType;
|
||||||
|
import com.baomidou.mybatisplus.annotation.TableField;
|
||||||
import com.baomidou.mybatisplus.annotation.TableId;
|
import com.baomidou.mybatisplus.annotation.TableId;
|
||||||
import com.baomidou.mybatisplus.annotation.TableName;
|
import com.baomidou.mybatisplus.annotation.TableName;
|
||||||
import lombok.Data;
|
import lombok.Data;
|
||||||
|
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
|
||||||
@Data
|
@Data
|
||||||
@TableName("sys_role_permission")
|
@TableName("sys_role_permission")
|
||||||
public class SysRolePermission extends BaseEntity {
|
public class SysRolePermission {
|
||||||
@TableId(value = "id", type = IdType.AUTO)
|
@TableId(value = "id", type = IdType.AUTO)
|
||||||
private Long id;
|
private Long id;
|
||||||
private Long roleId;
|
private Long roleId;
|
||||||
private Long permId;
|
private Long permId;
|
||||||
|
|
||||||
|
@TableField(fill = FieldFill.INSERT)
|
||||||
|
private LocalDateTime createdAt;
|
||||||
|
|
||||||
|
@TableField(fill = FieldFill.INSERT_UPDATE)
|
||||||
|
private LocalDateTime updatedAt;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,15 +1,25 @@
|
||||||
package com.imeeting.entity;
|
package com.imeeting.entity;
|
||||||
|
|
||||||
|
import com.baomidou.mybatisplus.annotation.FieldFill;
|
||||||
import com.baomidou.mybatisplus.annotation.IdType;
|
import com.baomidou.mybatisplus.annotation.IdType;
|
||||||
|
import com.baomidou.mybatisplus.annotation.TableField;
|
||||||
import com.baomidou.mybatisplus.annotation.TableId;
|
import com.baomidou.mybatisplus.annotation.TableId;
|
||||||
import com.baomidou.mybatisplus.annotation.TableName;
|
import com.baomidou.mybatisplus.annotation.TableName;
|
||||||
import lombok.Data;
|
import lombok.Data;
|
||||||
|
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
|
||||||
@Data
|
@Data
|
||||||
@TableName("sys_user_role")
|
@TableName("sys_user_role")
|
||||||
public class SysUserRole extends BaseEntity {
|
public class SysUserRole {
|
||||||
@TableId(value = "id", type = IdType.AUTO)
|
@TableId(value = "id", type = IdType.AUTO)
|
||||||
private Long id;
|
private Long id;
|
||||||
private Long userId;
|
private Long userId;
|
||||||
private Long roleId;
|
private Long roleId;
|
||||||
|
|
||||||
|
@TableField(fill = FieldFill.INSERT)
|
||||||
|
private LocalDateTime createdAt;
|
||||||
|
|
||||||
|
@TableField(fill = FieldFill.INSERT_UPDATE)
|
||||||
|
private LocalDateTime updatedAt;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -5,4 +5,12 @@ import com.imeeting.entity.SysParam;
|
||||||
|
|
||||||
public interface SysParamService extends IService<SysParam> {
|
public interface SysParamService extends IService<SysParam> {
|
||||||
String getParamValue(String key, String defaultValue);
|
String getParamValue(String key, String defaultValue);
|
||||||
|
|
||||||
|
String getCachedParamValue(String key, String defaultValue);
|
||||||
|
|
||||||
|
void syncParamToCache(SysParam param);
|
||||||
|
|
||||||
|
void deleteParamCache(String key);
|
||||||
|
|
||||||
|
void syncAllToCache();
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,7 @@ import com.imeeting.auth.JwtTokenProvider;
|
||||||
import com.imeeting.auth.dto.LoginRequest;
|
import com.imeeting.auth.dto.LoginRequest;
|
||||||
import com.imeeting.auth.dto.TokenResponse;
|
import com.imeeting.auth.dto.TokenResponse;
|
||||||
import com.imeeting.common.RedisKeys;
|
import com.imeeting.common.RedisKeys;
|
||||||
|
import com.imeeting.common.SysParamKeys;
|
||||||
import com.imeeting.entity.Device;
|
import com.imeeting.entity.Device;
|
||||||
import com.imeeting.entity.SysUser;
|
import com.imeeting.entity.SysUser;
|
||||||
import com.imeeting.service.AuthService;
|
import com.imeeting.service.AuthService;
|
||||||
|
|
@ -51,7 +52,9 @@ public class AuthServiceImpl implements AuthService {
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public TokenResponse login(LoginRequest request) {
|
public TokenResponse login(LoginRequest request) {
|
||||||
validateCaptcha(request.getCaptchaId(), request.getCaptchaCode());
|
if (isCaptchaEnabled()) {
|
||||||
|
validateCaptcha(request.getCaptchaId(), request.getCaptchaCode());
|
||||||
|
}
|
||||||
|
|
||||||
SysUser user = sysUserService.getOne(new LambdaQueryWrapper<SysUser>()
|
SysUser user = sysUserService.getOne(new LambdaQueryWrapper<SysUser>()
|
||||||
.eq(SysUser::getUsername, request.getUsername())
|
.eq(SysUser::getUsername, request.getUsername())
|
||||||
|
|
@ -118,7 +121,9 @@ public class AuthServiceImpl implements AuthService {
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public String createDeviceCode(LoginRequest request, String deviceName) {
|
public String createDeviceCode(LoginRequest request, String deviceName) {
|
||||||
validateCaptcha(request.getCaptchaId(), request.getCaptchaCode());
|
if (isCaptchaEnabled()) {
|
||||||
|
validateCaptcha(request.getCaptchaId(), request.getCaptchaCode());
|
||||||
|
}
|
||||||
|
|
||||||
SysUser user = sysUserService.getOne(new LambdaQueryWrapper<SysUser>()
|
SysUser user = sysUserService.getOne(new LambdaQueryWrapper<SysUser>()
|
||||||
.eq(SysUser::getUsername, request.getUsername())
|
.eq(SysUser::getUsername, request.getUsername())
|
||||||
|
|
@ -138,6 +143,12 @@ public class AuthServiceImpl implements AuthService {
|
||||||
}
|
}
|
||||||
|
|
||||||
private void validateCaptcha(String captchaId, String captchaCode) {
|
private void validateCaptcha(String captchaId, String captchaCode) {
|
||||||
|
if (captchaId == null || captchaId.isEmpty()) {
|
||||||
|
throw new IllegalArgumentException("验证码不能为空");
|
||||||
|
}
|
||||||
|
if (captchaCode == null || captchaCode.isEmpty()) {
|
||||||
|
throw new IllegalArgumentException("验证码不能为空");
|
||||||
|
}
|
||||||
String key = RedisKeys.captchaKey(captchaId);
|
String key = RedisKeys.captchaKey(captchaId);
|
||||||
String stored = stringRedisTemplate.opsForValue().get(key);
|
String stored = stringRedisTemplate.opsForValue().get(key);
|
||||||
if (stored == null) {
|
if (stored == null) {
|
||||||
|
|
@ -164,6 +175,11 @@ public class AuthServiceImpl implements AuthService {
|
||||||
stringRedisTemplate.delete(attemptsKey);
|
stringRedisTemplate.delete(attemptsKey);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private boolean isCaptchaEnabled() {
|
||||||
|
String value = sysParamService.getCachedParamValue(SysParamKeys.CAPTCHA_ENABLED, "true");
|
||||||
|
return Boolean.parseBoolean(value);
|
||||||
|
}
|
||||||
|
|
||||||
private TokenResponse issueTokens(SysUser user, String deviceCode, long accessMinutes, long refreshDays) {
|
private TokenResponse issueTokens(SysUser user, String deviceCode, long accessMinutes, long refreshDays) {
|
||||||
Map<String, Object> accessClaims = new HashMap<>();
|
Map<String, Object> accessClaims = new HashMap<>();
|
||||||
accessClaims.put("tokenType", "access");
|
accessClaims.put("tokenType", "access");
|
||||||
|
|
|
||||||
|
|
@ -2,16 +2,67 @@ package com.imeeting.service.impl;
|
||||||
|
|
||||||
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||||
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
|
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
|
||||||
|
import com.imeeting.common.RedisKeys;
|
||||||
import com.imeeting.entity.SysParam;
|
import com.imeeting.entity.SysParam;
|
||||||
import com.imeeting.mapper.SysParamMapper;
|
import com.imeeting.mapper.SysParamMapper;
|
||||||
import com.imeeting.service.SysParamService;
|
import com.imeeting.service.SysParamService;
|
||||||
|
import org.springframework.data.redis.core.StringRedisTemplate;
|
||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
@Service
|
@Service
|
||||||
public class SysParamServiceImpl extends ServiceImpl<SysParamMapper, SysParam> implements SysParamService {
|
public class SysParamServiceImpl extends ServiceImpl<SysParamMapper, SysParam> implements SysParamService {
|
||||||
|
private final StringRedisTemplate stringRedisTemplate;
|
||||||
|
|
||||||
|
public SysParamServiceImpl(StringRedisTemplate stringRedisTemplate) {
|
||||||
|
this.stringRedisTemplate = stringRedisTemplate;
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public String getParamValue(String key, String defaultValue) {
|
public String getParamValue(String key, String defaultValue) {
|
||||||
SysParam param = getOne(new LambdaQueryWrapper<SysParam>().eq(SysParam::getParamKey, key));
|
SysParam param = getOne(new LambdaQueryWrapper<SysParam>().eq(SysParam::getParamKey, key));
|
||||||
return param == null ? defaultValue : param.getParamValue();
|
return param == null ? defaultValue : param.getParamValue();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getCachedParamValue(String key, String defaultValue) {
|
||||||
|
if (key == null || key.isEmpty()) {
|
||||||
|
return defaultValue;
|
||||||
|
}
|
||||||
|
Object value = stringRedisTemplate.opsForHash().get(RedisKeys.sysParamKey(key), RedisKeys.SYS_PARAM_FIELD_VALUE);
|
||||||
|
if (value != null) {
|
||||||
|
return String.valueOf(value);
|
||||||
|
}
|
||||||
|
return getParamValue(key, defaultValue);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void syncParamToCache(SysParam param) {
|
||||||
|
if (param == null || param.getParamKey() == null || param.getParamKey().isEmpty()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
Map<String, String> data = new HashMap<>();
|
||||||
|
data.put(RedisKeys.SYS_PARAM_FIELD_VALUE, param.getParamValue() == null ? "" : param.getParamValue());
|
||||||
|
data.put(RedisKeys.SYS_PARAM_FIELD_TYPE, param.getParamType() == null ? "" : param.getParamType());
|
||||||
|
stringRedisTemplate.opsForHash().putAll(RedisKeys.sysParamKey(param.getParamKey()), data);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void deleteParamCache(String key) {
|
||||||
|
if (key == null || key.isEmpty()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
stringRedisTemplate.delete(RedisKeys.sysParamKey(key));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void syncAllToCache() {
|
||||||
|
List<SysParam> params = list();
|
||||||
|
for (SysParam param : params) {
|
||||||
|
syncParamToCache(param);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -11,6 +11,7 @@ spring:
|
||||||
host: 10.100.51.51
|
host: 10.100.51.51
|
||||||
port: 6379
|
port: 6379
|
||||||
password: Unis@123
|
password: Unis@123
|
||||||
|
database: 15
|
||||||
|
|
||||||
mybatis-plus:
|
mybatis-plus:
|
||||||
configuration:
|
configuration:
|
||||||
|
|
|
||||||
|
|
@ -35,3 +35,20 @@ Tests:
|
||||||
|
|
||||||
Status:
|
Status:
|
||||||
- In Progress
|
- In Progress
|
||||||
|
|
||||||
|
## Stage 3: User-Role and Role-Permission Binding Pages
|
||||||
|
|
||||||
|
Goal:
|
||||||
|
- Add admin pages for binding users to roles and roles to permissions.
|
||||||
|
|
||||||
|
Success Criteria:
|
||||||
|
- Two new pages available at /user-roles and /role-permissions.
|
||||||
|
- Role and permission selection UI with save actions wired to API endpoints.
|
||||||
|
|
||||||
|
Tests:
|
||||||
|
- Select user and roles, then save.
|
||||||
|
- Select role and permissions, then save.
|
||||||
|
- Verify behavior when APIs are missing or return errors.
|
||||||
|
|
||||||
|
Status:
|
||||||
|
- In Progress
|
||||||
|
|
|
||||||
|
|
@ -167,6 +167,7 @@
|
||||||
|
|
||||||
## 开发规范
|
## 开发规范
|
||||||
|
|
||||||
|
- **新增操作**:默认使用**右侧抽屉**打开表单进行创建(除非需求明确允许其他方式)
|
||||||
- **组件命名**:PascalCase
|
- **组件命名**:PascalCase
|
||||||
- **文件命名**:与组件同名
|
- **文件命名**:与组件同名
|
||||||
- **样式类命名**:kebab-case
|
- **样式类命名**:kebab-case
|
||||||
|
|
|
||||||
|
|
@ -15,16 +15,16 @@ export interface TokenResponse {
|
||||||
export interface LoginPayload {
|
export interface LoginPayload {
|
||||||
username: string;
|
username: string;
|
||||||
password: string;
|
password: string;
|
||||||
captchaId: string;
|
captchaId?: string;
|
||||||
captchaCode: string;
|
captchaCode?: string;
|
||||||
deviceCode?: string;
|
deviceCode?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface DeviceCodePayload {
|
export interface DeviceCodePayload {
|
||||||
username: string;
|
username: string;
|
||||||
password: string;
|
password: string;
|
||||||
captchaId: string;
|
captchaId?: string;
|
||||||
captchaCode: string;
|
captchaCode?: string;
|
||||||
deviceName?: string;
|
deviceName?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -47,6 +47,11 @@ export async function listPermissions() {
|
||||||
return resp.data.data as SysPermission[];
|
return resp.data.data as SysPermission[];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function getSystemParamValue(key: string, defaultValue?: string) {
|
||||||
|
const resp = await http.get("/api/params/value", { params: { key, defaultValue } });
|
||||||
|
return resp.data.data as string;
|
||||||
|
}
|
||||||
|
|
||||||
export async function listMyPermissions() {
|
export async function listMyPermissions() {
|
||||||
const resp = await http.get("/api/permissions/me");
|
const resp = await http.get("/api/permissions/me");
|
||||||
return resp.data.data as SysPermission[];
|
return resp.data.data as SysPermission[];
|
||||||
|
|
@ -92,3 +97,23 @@ export async function deleteDevice(id: number) {
|
||||||
return resp.data.data as boolean;
|
return resp.data.data as boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function listUserRoles(userId: number) {
|
||||||
|
const resp = await http.get(`/api/users/${userId}/roles`);
|
||||||
|
return resp.data.data as number[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function saveUserRoles(userId: number, roleIds: number[]) {
|
||||||
|
const resp = await http.post(`/api/users/${userId}/roles`, { roleIds });
|
||||||
|
return resp.data.data as boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function listRolePermissions(roleId: number) {
|
||||||
|
const resp = await http.get(`/api/roles/${roleId}/permissions`);
|
||||||
|
return resp.data.data as number[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function saveRolePermissions(roleId: number, permIds: number[]) {
|
||||||
|
const resp = await http.post(`/api/roles/${roleId}/permissions`, { permIds });
|
||||||
|
return resp.data.data as boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
import { Button, Form, Input, Modal, Popconfirm, Space, Table, Tag, Select, InputNumber } from "antd";
|
import { Button, Form, Input, Drawer, Popconfirm, Space, Table, Tag, Select, InputNumber } from "antd";
|
||||||
import { useEffect, useMemo, useState } from "react";
|
import { useEffect, useMemo, useState } from "react";
|
||||||
import { createDevice, deleteDevice, listDevices, updateDevice } from "../api";
|
import { createDevice, deleteDevice, listDevices, updateDevice } from "../api";
|
||||||
import type { DeviceInfo } from "../types";
|
import type { DeviceInfo } from "../types";
|
||||||
|
|
@ -135,12 +135,18 @@ export default function Devices() {
|
||||||
]}
|
]}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<Modal
|
<Drawer
|
||||||
title={editing ? "编辑设备" : "新增设备"}
|
title={editing ? "编辑设备" : "新增设备"}
|
||||||
open={open}
|
open={open}
|
||||||
onOk={submit}
|
onClose={() => setOpen(false)}
|
||||||
onCancel={() => setOpen(false)}
|
width={420}
|
||||||
destroyOnClose
|
destroyOnClose
|
||||||
|
footer={
|
||||||
|
<Space style={{ width: "100%", justifyContent: "flex-end" }}>
|
||||||
|
<Button onClick={() => setOpen(false)}>取消</Button>
|
||||||
|
<Button type="primary" onClick={submit}>确认</Button>
|
||||||
|
</Space>
|
||||||
|
}
|
||||||
>
|
>
|
||||||
<Form form={form} layout="vertical">
|
<Form form={form} layout="vertical">
|
||||||
<Form.Item label="用户ID" name="userId" rules={[{ required: true }]}>
|
<Form.Item label="用户ID" name="userId" rules={[{ required: true }]}>
|
||||||
|
|
@ -156,7 +162,7 @@ export default function Devices() {
|
||||||
<Select options={[{ value: 1, label: "启用" }, { value: 0, label: "禁用" }]} />
|
<Select options={[{ value: 1, label: "启用" }, { value: 0, label: "禁用" }]} />
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
</Form>
|
</Form>
|
||||||
</Modal>
|
</Drawer>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,17 +1,21 @@
|
||||||
import { Button, Checkbox, Form, Input, message, Typography } from "antd";
|
import { Button, Checkbox, Form, Input, message, Typography } from "antd";
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import { fetchCaptcha, login, type CaptchaResponse } from "../api/auth";
|
import { fetchCaptcha, login, type CaptchaResponse } from "../api/auth";
|
||||||
import { getCurrentUser } from "../api";
|
import { getCurrentUser, getSystemParamValue } from "../api";
|
||||||
import "./Login.css";
|
import "./Login.css";
|
||||||
|
|
||||||
const { Title, Text, Link } = Typography;
|
const { Title, Text, Link } = Typography;
|
||||||
|
|
||||||
export default function Login() {
|
export default function Login() {
|
||||||
const [captcha, setCaptcha] = useState<CaptchaResponse | null>(null);
|
const [captcha, setCaptcha] = useState<CaptchaResponse | null>(null);
|
||||||
|
const [captchaEnabled, setCaptchaEnabled] = useState(true);
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
const [form] = Form.useForm();
|
const [form] = Form.useForm();
|
||||||
|
|
||||||
const loadCaptcha = async () => {
|
const loadCaptcha = async () => {
|
||||||
|
if (!captchaEnabled) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
try {
|
try {
|
||||||
const data = await fetchCaptcha();
|
const data = await fetchCaptcha();
|
||||||
setCaptcha(data);
|
setCaptcha(data);
|
||||||
|
|
@ -21,7 +25,20 @@ export default function Login() {
|
||||||
};
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
loadCaptcha();
|
const init = async () => {
|
||||||
|
try {
|
||||||
|
const value = await getSystemParamValue("security.captcha.enabled", "true");
|
||||||
|
const enabled = value !== "false";
|
||||||
|
setCaptchaEnabled(enabled);
|
||||||
|
if (enabled) {
|
||||||
|
loadCaptcha();
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
setCaptchaEnabled(true);
|
||||||
|
loadCaptcha();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
init();
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const onFinish = async (values: any) => {
|
const onFinish = async (values: any) => {
|
||||||
|
|
@ -30,8 +47,8 @@ export default function Login() {
|
||||||
const data = await login({
|
const data = await login({
|
||||||
username: values.username,
|
username: values.username,
|
||||||
password: values.password,
|
password: values.password,
|
||||||
captchaId: captcha?.captchaId || "",
|
captchaId: captchaEnabled ? captcha?.captchaId : undefined,
|
||||||
captchaCode: values.captchaCode
|
captchaCode: captchaEnabled ? values.captchaCode : undefined
|
||||||
});
|
});
|
||||||
localStorage.setItem("accessToken", data.accessToken);
|
localStorage.setItem("accessToken", data.accessToken);
|
||||||
localStorage.setItem("refreshToken", data.refreshToken);
|
localStorage.setItem("refreshToken", data.refreshToken);
|
||||||
|
|
@ -46,7 +63,9 @@ export default function Login() {
|
||||||
window.location.href = "/";
|
window.location.href = "/";
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
message.error(e.message || "登录失败");
|
message.error(e.message || "登录失败");
|
||||||
loadCaptcha();
|
if (captchaEnabled) {
|
||||||
|
loadCaptcha();
|
||||||
|
}
|
||||||
} finally {
|
} finally {
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
}
|
}
|
||||||
|
|
@ -109,22 +128,24 @@ export default function Login() {
|
||||||
<Input.Password size="large" placeholder="请输入密码" />
|
<Input.Password size="large" placeholder="请输入密码" />
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
|
|
||||||
<Form.Item
|
{captchaEnabled && (
|
||||||
label="验证码"
|
<Form.Item
|
||||||
name="captchaCode"
|
label="验证码"
|
||||||
rules={[{ required: true, message: "请输入验证码" }]}
|
name="captchaCode"
|
||||||
>
|
rules={[{ required: true, message: "请输入验证码" }]}
|
||||||
<div className="captcha-wrapper">
|
>
|
||||||
<Input size="large" placeholder="验证码" />
|
<div className="captcha-wrapper">
|
||||||
<div className="captcha-image-container" onClick={loadCaptcha}>
|
<Input size="large" placeholder="验证码" />
|
||||||
{captcha ? (
|
<div className="captcha-image-container" onClick={loadCaptcha}>
|
||||||
<img src={captcha.imageBase64} alt="captcha" />
|
{captcha ? (
|
||||||
) : (
|
<img src={captcha.imageBase64} alt="captcha" />
|
||||||
<div className="captcha-placeholder" />
|
) : (
|
||||||
)}
|
<div className="captcha-placeholder" />
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</Form.Item>
|
||||||
</Form.Item>
|
)}
|
||||||
|
|
||||||
<div className="login-extra">
|
<div className="login-extra">
|
||||||
<Form.Item name="remember" valuePropName="checked" noStyle>
|
<Form.Item name="remember" valuePropName="checked" noStyle>
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
import { Button, Form, Input, Modal, Popconfirm, Space, Table, Tag, Select, InputNumber } from "antd";
|
import { Button, Form, Input, Drawer, Popconfirm, Space, Table, Tag, Select, InputNumber } from "antd";
|
||||||
import { useEffect, useMemo, useState } from "react";
|
import { useEffect, useMemo, useState } from "react";
|
||||||
import { createPermission, deletePermission, listMyPermissions, updatePermission } from "../api";
|
import { createPermission, deletePermission, listMyPermissions, updatePermission } from "../api";
|
||||||
import type { SysPermission } from "../types";
|
import type { SysPermission } from "../types";
|
||||||
|
|
@ -188,12 +188,18 @@ export default function Permissions() {
|
||||||
]}
|
]}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<Modal
|
<Drawer
|
||||||
title={editing ? "编辑权限" : "新增权限"}
|
title={editing ? "编辑权限" : "新增权限"}
|
||||||
open={open}
|
open={open}
|
||||||
onOk={submit}
|
onClose={() => setOpen(false)}
|
||||||
onCancel={() => setOpen(false)}
|
width={520}
|
||||||
destroyOnClose
|
destroyOnClose
|
||||||
|
footer={
|
||||||
|
<Space style={{ width: "100%", justifyContent: "flex-end" }}>
|
||||||
|
<Button onClick={() => setOpen(false)}>取消</Button>
|
||||||
|
<Button type="primary" onClick={submit}>确认</Button>
|
||||||
|
</Space>
|
||||||
|
}
|
||||||
>
|
>
|
||||||
<Form
|
<Form
|
||||||
form={form}
|
form={form}
|
||||||
|
|
@ -268,7 +274,7 @@ export default function Permissions() {
|
||||||
<Input.TextArea rows={3} />
|
<Input.TextArea rows={3} />
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
</Form>
|
</Form>
|
||||||
</Modal>
|
</Drawer>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,161 +1,348 @@
|
||||||
import { Button, Form, Input, Modal, Popconfirm, Space, Table, Tag, Select } from "antd";
|
import { Button, Drawer, Form, Input, message, Tag, Typography, Tree } from "antd";
|
||||||
|
import type { DataNode } from "antd/es/tree";
|
||||||
import { useEffect, useMemo, useState } from "react";
|
import { useEffect, useMemo, useState } from "react";
|
||||||
import { createRole, deleteRole, listRoles, updateRole } from "../api";
|
import {
|
||||||
import type { SysRole } from "../types";
|
createRole,
|
||||||
|
listPermissions,
|
||||||
|
listRolePermissions,
|
||||||
|
listRoles,
|
||||||
|
saveRolePermissions,
|
||||||
|
updateRole
|
||||||
|
} from "../api";
|
||||||
|
import type { SysPermission, SysRole } from "../types";
|
||||||
import { usePermission } from "../hooks/usePermission";
|
import { usePermission } from "../hooks/usePermission";
|
||||||
|
import { EditOutlined, PlusOutlined, SafetyCertificateOutlined } from "@ant-design/icons";
|
||||||
|
import "./Roles.css";
|
||||||
|
|
||||||
|
const { Title, Text } = Typography;
|
||||||
|
|
||||||
|
const DEFAULT_STATUS = 1;
|
||||||
|
|
||||||
|
type PermissionNode = SysPermission & { key: number; children?: PermissionNode[] };
|
||||||
|
|
||||||
|
const buildPermissionTree = (list: SysPermission[]): PermissionNode[] => {
|
||||||
|
const active = list.filter((p) => p.status !== 0);
|
||||||
|
const map = new Map<number, PermissionNode>();
|
||||||
|
const roots: PermissionNode[] = [];
|
||||||
|
|
||||||
|
active.forEach((item) => {
|
||||||
|
map.set(item.permId, { ...item, key: item.permId, children: [] });
|
||||||
|
});
|
||||||
|
|
||||||
|
map.forEach((node) => {
|
||||||
|
if (node.parentId && map.has(node.parentId)) {
|
||||||
|
map.get(node.parentId)!.children!.push(node);
|
||||||
|
} else {
|
||||||
|
roots.push(node);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const sortNodes = (nodes: PermissionNode[]) => {
|
||||||
|
nodes.sort((a, b) => (a.sortOrder || 0) - (b.sortOrder || 0));
|
||||||
|
nodes.forEach((n) => n.children && sortNodes(n.children));
|
||||||
|
};
|
||||||
|
sortNodes(roots);
|
||||||
|
return roots;
|
||||||
|
};
|
||||||
|
|
||||||
|
const toTreeData = (nodes: PermissionNode[]): DataNode[] =>
|
||||||
|
nodes.map((node) => ({
|
||||||
|
key: node.permId,
|
||||||
|
title: (
|
||||||
|
<span className="role-permission-node">
|
||||||
|
<span>{node.name}</span>
|
||||||
|
{node.permType === "button" && <Tag color="blue">按钮</Tag>}
|
||||||
|
</span>
|
||||||
|
),
|
||||||
|
children: node.children ? toTreeData(node.children) : undefined
|
||||||
|
}));
|
||||||
|
|
||||||
|
const generateRoleCode = () => `ROLE-${Date.now().toString(36).toUpperCase()}`;
|
||||||
|
|
||||||
export default function Roles() {
|
export default function Roles() {
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
|
const [saving, setSaving] = useState(false);
|
||||||
const [data, setData] = useState<SysRole[]>([]);
|
const [data, setData] = useState<SysRole[]>([]);
|
||||||
const [query, setQuery] = useState({ roleCode: "", roleName: "" });
|
const [permissions, setPermissions] = useState<SysPermission[]>([]);
|
||||||
const [pagination, setPagination] = useState({ current: 1, pageSize: 10 });
|
const [rolePermMap, setRolePermMap] = useState<Record<number, number[]>>({});
|
||||||
const [open, setOpen] = useState(false);
|
const [drawerOpen, setDrawerOpen] = useState(false);
|
||||||
const [editing, setEditing] = useState<SysRole | null>(null);
|
const [editing, setEditing] = useState<SysRole | null>(null);
|
||||||
|
const [selectedPermIds, setSelectedPermIds] = useState<number[]>([]);
|
||||||
const [form] = Form.useForm();
|
const [form] = Form.useForm();
|
||||||
const { can } = usePermission();
|
const { can } = usePermission();
|
||||||
|
|
||||||
const load = async () => {
|
const permissionMap = useMemo(() => {
|
||||||
|
const map = new Map<number, SysPermission>();
|
||||||
|
permissions.forEach((p) => map.set(p.permId, p));
|
||||||
|
return map;
|
||||||
|
}, [permissions]);
|
||||||
|
|
||||||
|
const permissionTreeData = useMemo(
|
||||||
|
() => toTreeData(buildPermissionTree(permissions)),
|
||||||
|
[permissions]
|
||||||
|
);
|
||||||
|
|
||||||
|
const loadPermissions = async () => {
|
||||||
|
try {
|
||||||
|
const list = await listPermissions();
|
||||||
|
setPermissions(list || []);
|
||||||
|
} catch (e) {
|
||||||
|
setPermissions([]);
|
||||||
|
message.error("加载权限失败,请确认有管理员权限");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const loadRolePermissions = async (roles: SysRole[]) => {
|
||||||
|
const entries = await Promise.all(
|
||||||
|
roles.map(async (role) => {
|
||||||
|
try {
|
||||||
|
const ids = await listRolePermissions(role.roleId);
|
||||||
|
const normalized = (ids || []).map((id) => Number(id)).filter((id) => !Number.isNaN(id));
|
||||||
|
return [role.roleId, normalized] as const;
|
||||||
|
} catch (e) {
|
||||||
|
return [role.roleId, []] as const;
|
||||||
|
}
|
||||||
|
})
|
||||||
|
);
|
||||||
|
setRolePermMap(Object.fromEntries(entries));
|
||||||
|
};
|
||||||
|
|
||||||
|
const loadRoles = async () => {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
try {
|
try {
|
||||||
const list = await listRoles();
|
const list = await listRoles();
|
||||||
setData(list || []);
|
const roles = list || [];
|
||||||
|
setData(roles);
|
||||||
|
await loadPermissions();
|
||||||
|
await loadRolePermissions(roles);
|
||||||
} finally {
|
} finally {
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
load();
|
loadRoles();
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const filtered = useMemo(() => {
|
|
||||||
return data.filter((r) => {
|
|
||||||
const hitCode = query.roleCode ? r.roleCode?.includes(query.roleCode) : true;
|
|
||||||
const hitName = query.roleName ? r.roleName?.includes(query.roleName) : true;
|
|
||||||
return hitCode && hitName;
|
|
||||||
});
|
|
||||||
}, [data, query]);
|
|
||||||
|
|
||||||
const pageData = useMemo(() => {
|
|
||||||
const start = (pagination.current - 1) * pagination.pageSize;
|
|
||||||
return filtered.slice(start, start + pagination.pageSize);
|
|
||||||
}, [filtered, pagination]);
|
|
||||||
|
|
||||||
const openCreate = () => {
|
const openCreate = () => {
|
||||||
setEditing(null);
|
setEditing(null);
|
||||||
|
setSelectedPermIds([]);
|
||||||
form.resetFields();
|
form.resetFields();
|
||||||
setOpen(true);
|
setDrawerOpen(true);
|
||||||
};
|
};
|
||||||
|
|
||||||
const openEdit = (record: SysRole) => {
|
const openEdit = (record: SysRole) => {
|
||||||
setEditing(record);
|
setEditing(record);
|
||||||
form.setFieldsValue(record);
|
setSelectedPermIds(rolePermMap[record.roleId] || []);
|
||||||
setOpen(true);
|
form.setFieldsValue({
|
||||||
|
roleName: record.roleName,
|
||||||
|
remark: record.remark
|
||||||
|
});
|
||||||
|
setDrawerOpen(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (editing) {
|
||||||
|
setSelectedPermIds(rolePermMap[editing.roleId] || []);
|
||||||
|
}
|
||||||
|
}, [editing, rolePermMap]);
|
||||||
|
|
||||||
|
const handleClose = () => {
|
||||||
|
setDrawerOpen(false);
|
||||||
};
|
};
|
||||||
|
|
||||||
const submit = async () => {
|
const submit = async () => {
|
||||||
const values = await form.validateFields();
|
try {
|
||||||
const payload: Partial<SysRole> = {
|
const values = await form.validateFields();
|
||||||
roleCode: values.roleCode,
|
setSaving(true);
|
||||||
roleName: values.roleName,
|
const payload: Partial<SysRole> = {
|
||||||
remark: values.remark,
|
roleCode: editing?.roleCode || generateRoleCode(),
|
||||||
status: values.status
|
roleName: values.roleName,
|
||||||
};
|
remark: values.remark,
|
||||||
if (editing) {
|
status: editing?.status ?? DEFAULT_STATUS
|
||||||
await updateRole(editing.roleId, payload);
|
};
|
||||||
} else {
|
let roleId = editing?.roleId;
|
||||||
await createRole(payload);
|
if (editing) {
|
||||||
|
await updateRole(editing.roleId, payload);
|
||||||
|
} else {
|
||||||
|
await createRole(payload);
|
||||||
|
}
|
||||||
|
|
||||||
|
const list = await listRoles();
|
||||||
|
const roles = list || [];
|
||||||
|
setData(roles);
|
||||||
|
if (!roleId) {
|
||||||
|
roleId = roles.find((r) => r.roleCode === payload.roleCode)?.roleId;
|
||||||
|
}
|
||||||
|
if (roleId) {
|
||||||
|
await saveRolePermissions(roleId, selectedPermIds);
|
||||||
|
}
|
||||||
|
await loadRolePermissions(roles);
|
||||||
|
setDrawerOpen(false);
|
||||||
|
message.success(editing ? "角色已更新" : "角色已创建");
|
||||||
|
} catch (e) {
|
||||||
|
if (e instanceof Error && e.message) {
|
||||||
|
message.error(e.message);
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
setSaving(false);
|
||||||
}
|
}
|
||||||
setOpen(false);
|
|
||||||
load();
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const remove = async (id: number) => {
|
const renderRolePermissions = (role: SysRole) => {
|
||||||
await deleteRole(id);
|
const permIds = rolePermMap[role.roleId] || [];
|
||||||
load();
|
const perms = permIds
|
||||||
|
.map((id) => permissionMap.get(id))
|
||||||
|
.filter((p): p is SysPermission => Boolean(p));
|
||||||
|
const preview = perms.slice(0, 3);
|
||||||
|
const totalCount = permIds.length;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<div className="role-permission-summary">
|
||||||
|
<span>权限概览</span>
|
||||||
|
<span className="role-permission-badge">{`${totalCount}个权限`}</span>
|
||||||
|
</div>
|
||||||
|
<div className="role-permission-tags">
|
||||||
|
{preview.length ? (
|
||||||
|
preview.map((p) => (
|
||||||
|
<Tag key={p.permId} className="role-permission-tag">
|
||||||
|
{p.name}
|
||||||
|
</Tag>
|
||||||
|
))
|
||||||
|
) : (
|
||||||
|
<Tag className="role-permission-tag">
|
||||||
|
{totalCount ? "已选权限" : "暂无权限"}
|
||||||
|
</Tag>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div className="roles-page">
|
||||||
<Space style={{ marginBottom: 16 }}>
|
<div className="roles-header">
|
||||||
<Input
|
<div>
|
||||||
placeholder="角色编码"
|
<Title level={4} className="roles-title">
|
||||||
value={query.roleCode}
|
系统角色权限
|
||||||
onChange={(e) => setQuery({ ...query, roleCode: e.target.value })}
|
</Title>
|
||||||
/>
|
<Text type="secondary" className="roles-subtitle">
|
||||||
<Input
|
设置系统中不同角色的访问权限和操作边界
|
||||||
placeholder="角色名称"
|
</Text>
|
||||||
value={query.roleName}
|
</div>
|
||||||
onChange={(e) => setQuery({ ...query, roleName: e.target.value })}
|
|
||||||
/>
|
|
||||||
{can("sys_role:create") && (
|
{can("sys_role:create") && (
|
||||||
<Button type="primary" onClick={openCreate}>新增</Button>
|
<Button type="primary" icon={<PlusOutlined />} onClick={openCreate}>
|
||||||
|
添加角色
|
||||||
|
</Button>
|
||||||
)}
|
)}
|
||||||
</Space>
|
</div>
|
||||||
|
|
||||||
<Table
|
<div className="roles-grid">
|
||||||
rowKey="roleId"
|
{data.map((role) => (
|
||||||
loading={loading}
|
<div key={role.roleId} className="role-card">
|
||||||
dataSource={pageData}
|
<div className="role-card-header">
|
||||||
pagination={{
|
<div className="role-icon">
|
||||||
current: pagination.current,
|
<SafetyCertificateOutlined />
|
||||||
pageSize: pagination.pageSize,
|
</div>
|
||||||
total: filtered.length,
|
{can("sys_role:update") && (
|
||||||
onChange: (current, pageSize) => setPagination({ current, pageSize })
|
<Button
|
||||||
}}
|
type="text"
|
||||||
columns={[
|
className="role-edit-btn"
|
||||||
{ title: "ID", dataIndex: "roleId" },
|
icon={<EditOutlined />}
|
||||||
{ title: "编码", dataIndex: "roleCode" },
|
onClick={() => openEdit(role)}
|
||||||
{ title: "名称", dataIndex: "roleName" },
|
/>
|
||||||
{ title: "备注", dataIndex: "remark" },
|
)}
|
||||||
{
|
</div>
|
||||||
title: "状态",
|
|
||||||
dataIndex: "status",
|
|
||||||
render: (v) => (v === 1 ? <Tag color="green">启用</Tag> : <Tag color="red">禁用</Tag>)
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: "操作",
|
|
||||||
render: (_, record) => (
|
|
||||||
<Space>
|
|
||||||
{can("sys_role:update") && <Button onClick={() => openEdit(record)}>编辑</Button>}
|
|
||||||
{can("sys_role:delete") && (
|
|
||||||
<Popconfirm title="确认删除?" onConfirm={() => remove(record.roleId)}>
|
|
||||||
<Button danger>删除</Button>
|
|
||||||
</Popconfirm>
|
|
||||||
)}
|
|
||||||
</Space>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
]}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<Modal
|
<div className="role-main">
|
||||||
title={editing ? "编辑角色" : "新增角色"}
|
<div className="role-name">{role.roleName}</div>
|
||||||
open={open}
|
<div className="role-id">{`ID: ${role.roleCode || role.roleId}`}</div>
|
||||||
onOk={submit}
|
</div>
|
||||||
onCancel={() => setOpen(false)}
|
|
||||||
destroyOnClose
|
{renderRolePermissions(role)}
|
||||||
|
|
||||||
|
<div className="role-footer">
|
||||||
|
<span>最后同步</span>
|
||||||
|
<span>{role.updatedAt ? "刚刚" : "刚刚"}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
{!data.length && !loading && (
|
||||||
|
<div className="roles-empty">暂无角色</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Drawer
|
||||||
|
open={drawerOpen}
|
||||||
|
onClose={handleClose}
|
||||||
|
width={420}
|
||||||
|
closable
|
||||||
|
title={
|
||||||
|
<div className="role-drawer-title">
|
||||||
|
<div className="role-drawer-icon">
|
||||||
|
<SafetyCertificateOutlined />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<div className="role-drawer-heading">
|
||||||
|
{editing ? "编辑角色" : "创建新角色"}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
footer={
|
||||||
|
<div className="role-drawer-footer">
|
||||||
|
<Button type="link" className="role-drawer-cancel" onClick={handleClose}>
|
||||||
|
取消更改
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
type="primary"
|
||||||
|
className="role-drawer-submit"
|
||||||
|
loading={saving}
|
||||||
|
onClick={submit}
|
||||||
|
>
|
||||||
|
确认并同步到系统
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
>
|
>
|
||||||
<Form form={form} layout="vertical">
|
<Form form={form} layout="vertical" className="role-form">
|
||||||
<Form.Item label="角色编码" name="roleCode" rules={[{ required: true }]}>
|
<Form.Item
|
||||||
<Input disabled={!!editing} />
|
label="角色名称"
|
||||||
|
name="roleName"
|
||||||
|
rules={[{ required: true, message: "请输入角色名称" }]}
|
||||||
|
>
|
||||||
|
<Input placeholder="例如:Auditor" />
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
<Form.Item label="角色名称" name="roleName" rules={[{ required: true }]}>
|
<Form.Item label="描述" name="remark">
|
||||||
<Input />
|
<Input placeholder="该角色的职责描述..." />
|
||||||
</Form.Item>
|
|
||||||
<Form.Item label="备注" name="remark">
|
|
||||||
<Input.TextArea rows={3} />
|
|
||||||
</Form.Item>
|
|
||||||
<Form.Item label="状态" name="status" initialValue={1}>
|
|
||||||
<Select
|
|
||||||
options={[
|
|
||||||
{ value: 1, label: "启用" },
|
|
||||||
{ value: 0, label: "禁用" }
|
|
||||||
]}
|
|
||||||
/>
|
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
</Form>
|
</Form>
|
||||||
</Modal>
|
|
||||||
|
<div className="role-permission-section">
|
||||||
|
<div className="role-permission-group-title">
|
||||||
|
<span className="role-permission-group-icon">
|
||||||
|
<SafetyCertificateOutlined />
|
||||||
|
</span>
|
||||||
|
<span>权限选择</span>
|
||||||
|
</div>
|
||||||
|
<div className="role-permission-tree">
|
||||||
|
<Tree
|
||||||
|
checkable
|
||||||
|
selectable={false}
|
||||||
|
checkStrictly={false}
|
||||||
|
treeData={permissionTreeData}
|
||||||
|
checkedKeys={selectedPermIds}
|
||||||
|
onCheck={(keys) => {
|
||||||
|
const raw = Array.isArray(keys) ? keys : keys.checked;
|
||||||
|
const normalized = (raw as Array<string | number>).map((k) => Number(k));
|
||||||
|
setSelectedPermIds(normalized.filter((id) => !Number.isNaN(id)));
|
||||||
|
}}
|
||||||
|
defaultExpandAll
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Drawer>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
import { Button, Form, Input, Modal, Popconfirm, Space, Table, Tag, Select } from "antd";
|
import { Button, Drawer, Form, Input, Popconfirm, Space, Table, Tag, Select } from "antd";
|
||||||
import { useMemo, useState, useEffect } from "react";
|
import { useMemo, useState, useEffect } from "react";
|
||||||
import { createUser, deleteUser, listUsers, updateUser } from "../api";
|
import { createUser, deleteUser, listUsers, updateUser } from "../api";
|
||||||
import type { SysUser } from "../types";
|
import type { SysUser } from "../types";
|
||||||
|
|
@ -140,12 +140,18 @@ export default function Users() {
|
||||||
]}
|
]}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<Modal
|
<Drawer
|
||||||
title={editing ? "编辑用户" : "新增用户"}
|
title={editing ? "编辑用户" : "新增用户"}
|
||||||
open={open}
|
open={open}
|
||||||
onOk={submit}
|
onClose={() => setOpen(false)}
|
||||||
onCancel={() => setOpen(false)}
|
width={420}
|
||||||
destroyOnClose
|
destroyOnClose
|
||||||
|
footer={
|
||||||
|
<Space style={{ width: "100%", justifyContent: "flex-end" }}>
|
||||||
|
<Button onClick={() => setOpen(false)}>取消</Button>
|
||||||
|
<Button type="primary" onClick={submit}>确认</Button>
|
||||||
|
</Space>
|
||||||
|
}
|
||||||
>
|
>
|
||||||
<Form form={form} layout="vertical">
|
<Form form={form} layout="vertical">
|
||||||
<Form.Item label="用户名" name="username" rules={[{ required: true }]}>
|
<Form.Item label="用户名" name="username" rules={[{ required: true }]}>
|
||||||
|
|
@ -172,7 +178,7 @@ export default function Users() {
|
||||||
/>
|
/>
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
</Form>
|
</Form>
|
||||||
</Modal>
|
</Drawer>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,8 @@ import Users from "../pages/Users";
|
||||||
import Roles from "../pages/Roles";
|
import Roles from "../pages/Roles";
|
||||||
import Permissions from "../pages/Permissions";
|
import Permissions from "../pages/Permissions";
|
||||||
import Devices from "../pages/Devices";
|
import Devices from "../pages/Devices";
|
||||||
|
import UserRoleBinding from "../pages/UserRoleBinding";
|
||||||
|
import RolePermissionBinding from "../pages/RolePermissionBinding";
|
||||||
|
|
||||||
import type { MenuRoute } from "../types";
|
import type { MenuRoute } from "../types";
|
||||||
|
|
||||||
|
|
@ -11,5 +13,7 @@ export const menuRoutes: MenuRoute[] = [
|
||||||
{ path: "/users", label: "用户管理", element: <Users />, perm: "menu:users" },
|
{ path: "/users", label: "用户管理", element: <Users />, perm: "menu:users" },
|
||||||
{ path: "/roles", label: "角色管理", element: <Roles />, perm: "menu:roles" },
|
{ path: "/roles", label: "角色管理", element: <Roles />, perm: "menu:roles" },
|
||||||
{ path: "/permissions", label: "权限管理", element: <Permissions />, perm: "menu:permissions" },
|
{ path: "/permissions", label: "权限管理", element: <Permissions />, perm: "menu:permissions" },
|
||||||
{ path: "/devices", label: "设备管理", element: <Devices />, perm: "menu:devices" }
|
{ path: "/devices", label: "设备管理", element: <Devices />, perm: "menu:devices" },
|
||||||
|
{ path: "/user-roles", label: "用户角色绑定", element: <UserRoleBinding />, perm: "menu:user-roles" },
|
||||||
|
{ path: "/role-permissions", label: "角色权限绑定", element: <RolePermissionBinding />, perm: "menu:role-permissions" }
|
||||||
];
|
];
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue