feat(shared): 添加操作帮助面板组件
- 实现 ActionHelpPanel 组件,提供操作详情和帮助信息展示 - 添加完整的 CSS 样式文件,支持响应式布局和主题适配 - 集成 Ant Design 的 Drawer 和 Collapse 组件 - 支持当前操作和所有可用操作的分类展示 - 实现操作步骤、注意事项、快捷键等功能说明 - 添加图标、标签、权限要求等信息展示 - 支持操作列表点击切换和实时预览功能master
parent
bf537d6074
commit
78e77cf260
|
|
@ -87,6 +87,11 @@ src
|
|||
2. `/auth/login`
|
||||
3. `/api/users/me`(获取用户信息与 isAdmin)
|
||||
|
||||
### 验证码开关(系统参数)
|
||||
- 系统参数 `security.captcha.enabled` 控制验证码是否启用(true/false)
|
||||
- 系统启动时加载 `sys_param` 到 Redis Hash:`sys:param:{paramKey}`(字段:value/type)
|
||||
- 前端登录页根据系统参数决定是否展示验证码
|
||||
|
||||
### 权限菜单渲染
|
||||
1. `/api/permissions/me` 获取权限列表
|
||||
2. 前端构建树形菜单
|
||||
|
|
@ -101,4 +106,3 @@ src
|
|||
- 添加审计日志落库策略
|
||||
- 任务管理模块完善
|
||||
- 权限树缓存与增量刷新策略
|
||||
|
||||
|
|
|
|||
|
|
@ -9,9 +9,7 @@ public class DeviceCodeRequest {
|
|||
private String username;
|
||||
@NotBlank
|
||||
private String password;
|
||||
@NotBlank
|
||||
private String captchaId;
|
||||
@NotBlank
|
||||
private String captchaCode;
|
||||
private String deviceName;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -9,9 +9,7 @@ public class LoginRequest {
|
|||
private String username;
|
||||
@NotBlank
|
||||
private String password;
|
||||
@NotBlank
|
||||
private String captchaId;
|
||||
@NotBlank
|
||||
private String captchaCode;
|
||||
private String deviceCode;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -14,4 +14,11 @@ public final class RedisKeys {
|
|||
public static String refreshTokenKey(Long userId, String 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))
|
||||
.authorizeHttpRequests(auth -> auth
|
||||
.requestMatchers("/auth/**").permitAll()
|
||||
.requestMatchers("/api/params/value").permitAll()
|
||||
.anyRequest().authenticated()
|
||||
)
|
||||
.addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class);
|
||||
|
|
|
|||
|
|
@ -8,6 +8,8 @@ import com.imeeting.auth.dto.RefreshRequest;
|
|||
import com.imeeting.auth.dto.TokenResponse;
|
||||
import com.imeeting.common.ApiResponse;
|
||||
import com.imeeting.common.RedisKeys;
|
||||
import com.imeeting.common.SysParamKeys;
|
||||
import com.imeeting.service.SysParamService;
|
||||
import com.imeeting.service.AuthService;
|
||||
import com.wf.captcha.SpecCaptcha;
|
||||
import jakarta.validation.Valid;
|
||||
|
|
@ -24,18 +26,24 @@ public class AuthController {
|
|||
private final AuthService authService;
|
||||
private final StringRedisTemplate stringRedisTemplate;
|
||||
private final JwtTokenProvider jwtTokenProvider;
|
||||
private final SysParamService sysParamService;
|
||||
|
||||
@Value("${app.captcha.ttl-seconds:120}")
|
||||
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.stringRedisTemplate = stringRedisTemplate;
|
||||
this.jwtTokenProvider = jwtTokenProvider;
|
||||
this.sysParamService = sysParamService;
|
||||
}
|
||||
|
||||
@GetMapping("/captcha")
|
||||
public ApiResponse<CaptchaResponse> captcha() {
|
||||
if (!isCaptchaEnabled()) {
|
||||
return ApiResponse.error("Captcha disabled");
|
||||
}
|
||||
SpecCaptcha captcha = new SpecCaptcha(130, 48, 4);
|
||||
String code = captcha.text();
|
||||
String imageBase64 = captcha.toBase64();
|
||||
|
|
@ -75,4 +83,9 @@ public class AuthController {
|
|||
authService.logout(userId, deviceCode);
|
||||
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;
|
||||
|
||||
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
|
||||
import com.imeeting.common.ApiResponse;
|
||||
import com.imeeting.entity.SysRole;
|
||||
import com.imeeting.entity.SysRolePermission;
|
||||
import com.imeeting.mapper.SysRolePermissionMapper;
|
||||
import com.imeeting.service.SysRoleService;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/api/roles")
|
||||
public class RoleController {
|
||||
private final SysRoleService sysRoleService;
|
||||
private final SysRolePermissionMapper sysRolePermissionMapper;
|
||||
|
||||
public RoleController(SysRoleService sysRoleService) {
|
||||
public RoleController(SysRoleService sysRoleService, SysRolePermissionMapper sysRolePermissionMapper) {
|
||||
this.sysRoleService = sysRoleService;
|
||||
this.sysRolePermissionMapper = sysRolePermissionMapper;
|
||||
}
|
||||
|
||||
@GetMapping
|
||||
|
|
@ -41,4 +47,49 @@ public class RoleController {
|
|||
public ApiResponse<Boolean> delete(@PathVariable Long 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
|
||||
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}")
|
||||
public ApiResponse<Boolean> update(@PathVariable Long id, @RequestBody SysParam param) {
|
||||
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}")
|
||||
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.common.ApiResponse;
|
||||
import com.imeeting.dto.UserProfile;
|
||||
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
|
||||
import com.imeeting.entity.SysUser;
|
||||
import com.imeeting.entity.SysUserRole;
|
||||
import com.imeeting.mapper.SysUserRoleMapper;
|
||||
import com.imeeting.service.SysUserService;
|
||||
import io.jsonwebtoken.Claims;
|
||||
import org.springframework.security.crypto.password.PasswordEncoder;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
@RestController
|
||||
|
|
@ -17,11 +21,13 @@ public class UserController {
|
|||
private final SysUserService sysUserService;
|
||||
private final PasswordEncoder passwordEncoder;
|
||||
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.passwordEncoder = passwordEncoder;
|
||||
this.jwtTokenProvider = jwtTokenProvider;
|
||||
this.sysUserRoleMapper = sysUserRoleMapper;
|
||||
}
|
||||
|
||||
@GetMapping
|
||||
|
|
@ -77,6 +83,39 @@ public class UserController {
|
|||
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) {
|
||||
if (authorization == null || !authorization.startsWith("Bearer ")) {
|
||||
return null;
|
||||
|
|
@ -85,4 +124,16 @@ public class UserController {
|
|||
Claims claims = jwtTokenProvider.parseToken(token);
|
||||
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;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
import lombok.Data;
|
||||
|
||||
@Data
|
||||
|
|
@ -10,5 +11,6 @@ public class UserProfile {
|
|||
private String email;
|
||||
private String phone;
|
||||
private Integer status;
|
||||
@JsonProperty("isAdmin")
|
||||
private boolean isAdmin;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,15 +1,25 @@
|
|||
package com.imeeting.entity;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.FieldFill;
|
||||
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.time.LocalDateTime;
|
||||
|
||||
@Data
|
||||
@TableName("sys_role_permission")
|
||||
public class SysRolePermission extends BaseEntity {
|
||||
public class SysRolePermission {
|
||||
@TableId(value = "id", type = IdType.AUTO)
|
||||
private Long id;
|
||||
private Long roleId;
|
||||
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;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.FieldFill;
|
||||
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.time.LocalDateTime;
|
||||
|
||||
@Data
|
||||
@TableName("sys_user_role")
|
||||
public class SysUserRole extends BaseEntity {
|
||||
public class SysUserRole {
|
||||
@TableId(value = "id", type = IdType.AUTO)
|
||||
private Long id;
|
||||
private Long userId;
|
||||
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> {
|
||||
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.TokenResponse;
|
||||
import com.imeeting.common.RedisKeys;
|
||||
import com.imeeting.common.SysParamKeys;
|
||||
import com.imeeting.entity.Device;
|
||||
import com.imeeting.entity.SysUser;
|
||||
import com.imeeting.service.AuthService;
|
||||
|
|
@ -51,7 +52,9 @@ public class AuthServiceImpl implements AuthService {
|
|||
|
||||
@Override
|
||||
public TokenResponse login(LoginRequest request) {
|
||||
if (isCaptchaEnabled()) {
|
||||
validateCaptcha(request.getCaptchaId(), request.getCaptchaCode());
|
||||
}
|
||||
|
||||
SysUser user = sysUserService.getOne(new LambdaQueryWrapper<SysUser>()
|
||||
.eq(SysUser::getUsername, request.getUsername())
|
||||
|
|
@ -118,7 +121,9 @@ public class AuthServiceImpl implements AuthService {
|
|||
|
||||
@Override
|
||||
public String createDeviceCode(LoginRequest request, String deviceName) {
|
||||
if (isCaptchaEnabled()) {
|
||||
validateCaptcha(request.getCaptchaId(), request.getCaptchaCode());
|
||||
}
|
||||
|
||||
SysUser user = sysUserService.getOne(new LambdaQueryWrapper<SysUser>()
|
||||
.eq(SysUser::getUsername, request.getUsername())
|
||||
|
|
@ -138,6 +143,12 @@ public class AuthServiceImpl implements AuthService {
|
|||
}
|
||||
|
||||
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 stored = stringRedisTemplate.opsForValue().get(key);
|
||||
if (stored == null) {
|
||||
|
|
@ -164,6 +175,11 @@ public class AuthServiceImpl implements AuthService {
|
|||
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) {
|
||||
Map<String, Object> accessClaims = new HashMap<>();
|
||||
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.extension.service.impl.ServiceImpl;
|
||||
import com.imeeting.common.RedisKeys;
|
||||
import com.imeeting.entity.SysParam;
|
||||
import com.imeeting.mapper.SysParamMapper;
|
||||
import com.imeeting.service.SysParamService;
|
||||
import org.springframework.data.redis.core.StringRedisTemplate;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
@Service
|
||||
public class SysParamServiceImpl extends ServiceImpl<SysParamMapper, SysParam> implements SysParamService {
|
||||
private final StringRedisTemplate stringRedisTemplate;
|
||||
|
||||
public SysParamServiceImpl(StringRedisTemplate stringRedisTemplate) {
|
||||
this.stringRedisTemplate = stringRedisTemplate;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getParamValue(String key, String defaultValue) {
|
||||
SysParam param = getOne(new LambdaQueryWrapper<SysParam>().eq(SysParam::getParamKey, key));
|
||||
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
|
||||
port: 6379
|
||||
password: Unis@123
|
||||
database: 15
|
||||
|
||||
mybatis-plus:
|
||||
configuration:
|
||||
|
|
|
|||
|
|
@ -35,3 +35,20 @@ Tests:
|
|||
|
||||
Status:
|
||||
- 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
|
||||
- **文件命名**:与组件同名
|
||||
- **样式类命名**:kebab-case
|
||||
|
|
|
|||
|
|
@ -15,16 +15,16 @@ export interface TokenResponse {
|
|||
export interface LoginPayload {
|
||||
username: string;
|
||||
password: string;
|
||||
captchaId: string;
|
||||
captchaCode: string;
|
||||
captchaId?: string;
|
||||
captchaCode?: string;
|
||||
deviceCode?: string;
|
||||
}
|
||||
|
||||
export interface DeviceCodePayload {
|
||||
username: string;
|
||||
password: string;
|
||||
captchaId: string;
|
||||
captchaCode: string;
|
||||
captchaId?: string;
|
||||
captchaCode?: string;
|
||||
deviceName?: string;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -47,6 +47,11 @@ export async function listPermissions() {
|
|||
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() {
|
||||
const resp = await http.get("/api/permissions/me");
|
||||
return resp.data.data as SysPermission[];
|
||||
|
|
@ -92,3 +97,23 @@ export async function deleteDevice(id: number) {
|
|||
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 { createDevice, deleteDevice, listDevices, updateDevice } from "../api";
|
||||
import type { DeviceInfo } from "../types";
|
||||
|
|
@ -135,12 +135,18 @@ export default function Devices() {
|
|||
]}
|
||||
/>
|
||||
|
||||
<Modal
|
||||
<Drawer
|
||||
title={editing ? "编辑设备" : "新增设备"}
|
||||
open={open}
|
||||
onOk={submit}
|
||||
onCancel={() => setOpen(false)}
|
||||
onClose={() => setOpen(false)}
|
||||
width={420}
|
||||
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.Item label="用户ID" name="userId" rules={[{ required: true }]}>
|
||||
|
|
@ -156,7 +162,7 @@ export default function Devices() {
|
|||
<Select options={[{ value: 1, label: "启用" }, { value: 0, label: "禁用" }]} />
|
||||
</Form.Item>
|
||||
</Form>
|
||||
</Modal>
|
||||
</Drawer>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,17 +1,21 @@
|
|||
import { Button, Checkbox, Form, Input, message, Typography } from "antd";
|
||||
import { useEffect, useState } from "react";
|
||||
import { fetchCaptcha, login, type CaptchaResponse } from "../api/auth";
|
||||
import { getCurrentUser } from "../api";
|
||||
import { getCurrentUser, getSystemParamValue } from "../api";
|
||||
import "./Login.css";
|
||||
|
||||
const { Title, Text, Link } = Typography;
|
||||
|
||||
export default function Login() {
|
||||
const [captcha, setCaptcha] = useState<CaptchaResponse | null>(null);
|
||||
const [captchaEnabled, setCaptchaEnabled] = useState(true);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [form] = Form.useForm();
|
||||
|
||||
const loadCaptcha = async () => {
|
||||
if (!captchaEnabled) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const data = await fetchCaptcha();
|
||||
setCaptcha(data);
|
||||
|
|
@ -21,7 +25,20 @@ export default function Login() {
|
|||
};
|
||||
|
||||
useEffect(() => {
|
||||
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) => {
|
||||
|
|
@ -30,8 +47,8 @@ export default function Login() {
|
|||
const data = await login({
|
||||
username: values.username,
|
||||
password: values.password,
|
||||
captchaId: captcha?.captchaId || "",
|
||||
captchaCode: values.captchaCode
|
||||
captchaId: captchaEnabled ? captcha?.captchaId : undefined,
|
||||
captchaCode: captchaEnabled ? values.captchaCode : undefined
|
||||
});
|
||||
localStorage.setItem("accessToken", data.accessToken);
|
||||
localStorage.setItem("refreshToken", data.refreshToken);
|
||||
|
|
@ -46,7 +63,9 @@ export default function Login() {
|
|||
window.location.href = "/";
|
||||
} catch (e: any) {
|
||||
message.error(e.message || "登录失败");
|
||||
if (captchaEnabled) {
|
||||
loadCaptcha();
|
||||
}
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
|
|
@ -109,6 +128,7 @@ export default function Login() {
|
|||
<Input.Password size="large" placeholder="请输入密码" />
|
||||
</Form.Item>
|
||||
|
||||
{captchaEnabled && (
|
||||
<Form.Item
|
||||
label="验证码"
|
||||
name="captchaCode"
|
||||
|
|
@ -125,6 +145,7 @@ export default function Login() {
|
|||
</div>
|
||||
</div>
|
||||
</Form.Item>
|
||||
)}
|
||||
|
||||
<div className="login-extra">
|
||||
<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 { createPermission, deletePermission, listMyPermissions, updatePermission } from "../api";
|
||||
import type { SysPermission } from "../types";
|
||||
|
|
@ -188,12 +188,18 @@ export default function Permissions() {
|
|||
]}
|
||||
/>
|
||||
|
||||
<Modal
|
||||
<Drawer
|
||||
title={editing ? "编辑权限" : "新增权限"}
|
||||
open={open}
|
||||
onOk={submit}
|
||||
onCancel={() => setOpen(false)}
|
||||
onClose={() => setOpen(false)}
|
||||
width={520}
|
||||
destroyOnClose
|
||||
footer={
|
||||
<Space style={{ width: "100%", justifyContent: "flex-end" }}>
|
||||
<Button onClick={() => setOpen(false)}>取消</Button>
|
||||
<Button type="primary" onClick={submit}>确认</Button>
|
||||
</Space>
|
||||
}
|
||||
>
|
||||
<Form
|
||||
form={form}
|
||||
|
|
@ -268,7 +274,7 @@ export default function Permissions() {
|
|||
<Input.TextArea rows={3} />
|
||||
</Form.Item>
|
||||
</Form>
|
||||
</Modal>
|
||||
</Drawer>
|
||||
</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 { createRole, deleteRole, listRoles, updateRole } from "../api";
|
||||
import type { SysRole } from "../types";
|
||||
import {
|
||||
createRole,
|
||||
listPermissions,
|
||||
listRolePermissions,
|
||||
listRoles,
|
||||
saveRolePermissions,
|
||||
updateRole
|
||||
} from "../api";
|
||||
import type { SysPermission, SysRole } from "../types";
|
||||
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() {
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [saving, setSaving] = useState(false);
|
||||
const [data, setData] = useState<SysRole[]>([]);
|
||||
const [query, setQuery] = useState({ roleCode: "", roleName: "" });
|
||||
const [pagination, setPagination] = useState({ current: 1, pageSize: 10 });
|
||||
const [open, setOpen] = useState(false);
|
||||
const [permissions, setPermissions] = useState<SysPermission[]>([]);
|
||||
const [rolePermMap, setRolePermMap] = useState<Record<number, number[]>>({});
|
||||
const [drawerOpen, setDrawerOpen] = useState(false);
|
||||
const [editing, setEditing] = useState<SysRole | null>(null);
|
||||
const [selectedPermIds, setSelectedPermIds] = useState<number[]>([]);
|
||||
const [form] = Form.useForm();
|
||||
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);
|
||||
try {
|
||||
const list = await listRoles();
|
||||
setData(list || []);
|
||||
const roles = list || [];
|
||||
setData(roles);
|
||||
await loadPermissions();
|
||||
await loadRolePermissions(roles);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
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 = () => {
|
||||
setEditing(null);
|
||||
setSelectedPermIds([]);
|
||||
form.resetFields();
|
||||
setOpen(true);
|
||||
setDrawerOpen(true);
|
||||
};
|
||||
|
||||
const openEdit = (record: SysRole) => {
|
||||
setEditing(record);
|
||||
form.setFieldsValue(record);
|
||||
setOpen(true);
|
||||
setSelectedPermIds(rolePermMap[record.roleId] || []);
|
||||
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 () => {
|
||||
try {
|
||||
const values = await form.validateFields();
|
||||
setSaving(true);
|
||||
const payload: Partial<SysRole> = {
|
||||
roleCode: values.roleCode,
|
||||
roleCode: editing?.roleCode || generateRoleCode(),
|
||||
roleName: values.roleName,
|
||||
remark: values.remark,
|
||||
status: values.status
|
||||
status: editing?.status ?? DEFAULT_STATUS
|
||||
};
|
||||
let roleId = editing?.roleId;
|
||||
if (editing) {
|
||||
await updateRole(editing.roleId, payload);
|
||||
} else {
|
||||
await createRole(payload);
|
||||
}
|
||||
setOpen(false);
|
||||
load();
|
||||
|
||||
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);
|
||||
}
|
||||
};
|
||||
|
||||
const remove = async (id: number) => {
|
||||
await deleteRole(id);
|
||||
load();
|
||||
const renderRolePermissions = (role: SysRole) => {
|
||||
const permIds = rolePermMap[role.roleId] || [];
|
||||
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 (
|
||||
<div className="roles-page">
|
||||
<div className="roles-header">
|
||||
<div>
|
||||
<Space style={{ marginBottom: 16 }}>
|
||||
<Input
|
||||
placeholder="角色编码"
|
||||
value={query.roleCode}
|
||||
onChange={(e) => setQuery({ ...query, roleCode: e.target.value })}
|
||||
/>
|
||||
<Input
|
||||
placeholder="角色名称"
|
||||
value={query.roleName}
|
||||
onChange={(e) => setQuery({ ...query, roleName: e.target.value })}
|
||||
/>
|
||||
<Title level={4} className="roles-title">
|
||||
系统角色权限
|
||||
</Title>
|
||||
<Text type="secondary" className="roles-subtitle">
|
||||
设置系统中不同角色的访问权限和操作边界
|
||||
</Text>
|
||||
</div>
|
||||
{can("sys_role:create") && (
|
||||
<Button type="primary" onClick={openCreate}>新增</Button>
|
||||
<Button type="primary" icon={<PlusOutlined />} onClick={openCreate}>
|
||||
添加角色
|
||||
</Button>
|
||||
)}
|
||||
</Space>
|
||||
</div>
|
||||
|
||||
<Table
|
||||
rowKey="roleId"
|
||||
loading={loading}
|
||||
dataSource={pageData}
|
||||
pagination={{
|
||||
current: pagination.current,
|
||||
pageSize: pagination.pageSize,
|
||||
total: filtered.length,
|
||||
onChange: (current, pageSize) => setPagination({ current, pageSize })
|
||||
}}
|
||||
columns={[
|
||||
{ title: "ID", dataIndex: "roleId" },
|
||||
{ title: "编码", dataIndex: "roleCode" },
|
||||
{ title: "名称", dataIndex: "roleName" },
|
||||
{ title: "备注", dataIndex: "remark" },
|
||||
{
|
||||
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>
|
||||
<div className="roles-grid">
|
||||
{data.map((role) => (
|
||||
<div key={role.roleId} className="role-card">
|
||||
<div className="role-card-header">
|
||||
<div className="role-icon">
|
||||
<SafetyCertificateOutlined />
|
||||
</div>
|
||||
{can("sys_role:update") && (
|
||||
<Button
|
||||
type="text"
|
||||
className="role-edit-btn"
|
||||
icon={<EditOutlined />}
|
||||
onClick={() => openEdit(role)}
|
||||
/>
|
||||
)}
|
||||
</Space>
|
||||
)
|
||||
</div>
|
||||
|
||||
<div className="role-main">
|
||||
<div className="role-name">{role.roleName}</div>
|
||||
<div className="role-id">{`ID: ${role.roleCode || role.roleId}`}</div>
|
||||
</div>
|
||||
|
||||
{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>
|
||||
}
|
||||
]}
|
||||
/>
|
||||
|
||||
<Modal
|
||||
title={editing ? "编辑角色" : "新增角色"}
|
||||
open={open}
|
||||
onOk={submit}
|
||||
onCancel={() => setOpen(false)}
|
||||
destroyOnClose
|
||||
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}
|
||||
>
|
||||
<Form form={form} layout="vertical">
|
||||
<Form.Item label="角色编码" name="roleCode" rules={[{ required: true }]}>
|
||||
<Input disabled={!!editing} />
|
||||
确认并同步到系统
|
||||
</Button>
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<Form form={form} layout="vertical" className="role-form">
|
||||
<Form.Item
|
||||
label="角色名称"
|
||||
name="roleName"
|
||||
rules={[{ required: true, message: "请输入角色名称" }]}
|
||||
>
|
||||
<Input placeholder="例如:Auditor" />
|
||||
</Form.Item>
|
||||
<Form.Item label="角色名称" name="roleName" rules={[{ required: true }]}>
|
||||
<Input />
|
||||
</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 label="描述" name="remark">
|
||||
<Input placeholder="该角色的职责描述..." />
|
||||
</Form.Item>
|
||||
</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>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 { createUser, deleteUser, listUsers, updateUser } from "../api";
|
||||
import type { SysUser } from "../types";
|
||||
|
|
@ -140,12 +140,18 @@ export default function Users() {
|
|||
]}
|
||||
/>
|
||||
|
||||
<Modal
|
||||
<Drawer
|
||||
title={editing ? "编辑用户" : "新增用户"}
|
||||
open={open}
|
||||
onOk={submit}
|
||||
onCancel={() => setOpen(false)}
|
||||
onClose={() => setOpen(false)}
|
||||
width={420}
|
||||
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.Item label="用户名" name="username" rules={[{ required: true }]}>
|
||||
|
|
@ -172,7 +178,7 @@ export default function Users() {
|
|||
/>
|
||||
</Form.Item>
|
||||
</Form>
|
||||
</Modal>
|
||||
</Drawer>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,6 +3,8 @@ import Users from "../pages/Users";
|
|||
import Roles from "../pages/Roles";
|
||||
import Permissions from "../pages/Permissions";
|
||||
import Devices from "../pages/Devices";
|
||||
import UserRoleBinding from "../pages/UserRoleBinding";
|
||||
import RolePermissionBinding from "../pages/RolePermissionBinding";
|
||||
|
||||
import type { MenuRoute } from "../types";
|
||||
|
||||
|
|
@ -11,5 +13,7 @@ export const menuRoutes: MenuRoute[] = [
|
|||
{ path: "/users", label: "用户管理", element: <Users />, perm: "menu:users" },
|
||||
{ path: "/roles", label: "角色管理", element: <Roles />, perm: "menu:roles" },
|
||||
{ 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