feat(platform): 实现平台配置管理与权限优化
- 添加平台配置功能,支持动态设置项目名称、图标、Logo等 - 实现前端页面动态加载平台配置信息 - 完善权限控制,修复权限码格式问题 - 优化角色权限分配的安全校验机制 - 增加系统参数管理页面和相关API接口 - 更新数据库配置和MyBatis拦截器设置 - 调整安全配置,开放平台配置接口访问权限 - 重构权限查询逻辑,区分平台管理员与普通用户权限范围 - 补充实体类注解和字段定义,完善数据映射关系 - 优化登录页面样式,支持自定义背景图片和品牌信息显示master
parent
86009e2602
commit
9b721929c6
|
|
@ -58,10 +58,21 @@ com.xxx.project
|
|||
|
||||
## 四、角色与定位
|
||||
|
||||
你是一位**务实型后端开发者 Agent**,只修改后端文件,不修改前端,目标是:
|
||||
你是一位**务实型后端开发者 Agent**,目标是:
|
||||
|
||||
> 以最清晰、最朴素、最可验证的方式交付可工作的 Java 服务。
|
||||
|
||||
> 基本原则
|
||||
> 1. 生成内容必须完整、可运行、不可省略。
|
||||
> 2. 不允许伪代码。
|
||||
> 3. 不允许使用"示例代码"字样。
|
||||
> 4. 不允许省略 import。
|
||||
> 5. 不允许省略异常处理。
|
||||
> 6. 所有写操作必须考虑事务控制。
|
||||
> 7. 所有删除操作必须为逻辑删除(is_deleted)。
|
||||
> 8. 所有表必须包含:
|
||||
> - created_at TIMESTAMP(6)
|
||||
> - updated_at TIMESTAMP(6)
|
||||
> - is_deleted SMALLINT DEFAULT 0
|
||||
### 核心理念
|
||||
|
||||
* 清晰的意图胜于巧妙的代码
|
||||
|
|
@ -88,7 +99,7 @@ com.xxx.project
|
|||
2. 所有功能改动都必须更新设计文档
|
||||
3. 遵循代码风格、目录结构和 Git 工作流规则
|
||||
|
||||
中大型需求必须先创建:
|
||||
需求必须先创建:
|
||||
|
||||
`IMPLEMENTATION_PLAN.md`
|
||||
|
||||
|
|
@ -153,6 +164,22 @@ Status:
|
|||
* 根本性反思
|
||||
|
||||
---
|
||||
### 5.4. 变更同步规则
|
||||
|
||||
当数据库结构发生变更时,必须同步生成:
|
||||
|
||||
- Entity
|
||||
- Mapper
|
||||
- Service
|
||||
- Controller
|
||||
- DTO
|
||||
- VO
|
||||
- 前端类型定义
|
||||
- API 封装
|
||||
- 权限校验调整
|
||||
同步修改backend/design/db_schema.md和backend/design/db_schema_pgsql.sql
|
||||
禁止只修改数据库而不同步代码。
|
||||
|
||||
|
||||
## 六、质量关卡(DoD)
|
||||
|
||||
|
|
|
|||
|
|
@ -1,108 +0,0 @@
|
|||
# 项目设计文档(imeetingNew)
|
||||
|
||||
## 1. 项目概述
|
||||
本项目为“智能会议语音识别与总结系统”的管理后台,提供用户、角色、权限、设备与任务等管理能力。
|
||||
后端为 Java 服务,前端为后台管理 Web。 后续所有内容变更都必须同步更新该文档
|
||||
|
||||
## 2. 技术栈
|
||||
### 后端
|
||||
- Java 17
|
||||
- Spring Boot 3.x
|
||||
- Spring MVC
|
||||
- Spring Security + JWT
|
||||
- ORM: MyBatis / MyBatis-Plus
|
||||
- Database: PostgreSQL
|
||||
- Cache: Redis
|
||||
- Build: Maven
|
||||
|
||||
### 前端
|
||||
- React 18
|
||||
- TypeScript
|
||||
- Ant Design
|
||||
- React Router
|
||||
- Axios
|
||||
- Vite
|
||||
|
||||
## 3. 系统架构
|
||||
### 后端分层
|
||||
- Controller:接收请求、参数校验、返回响应
|
||||
- Service:业务编排与事务边界
|
||||
- Mapper:数据访问
|
||||
- DTO/VO:对外数据结构
|
||||
|
||||
### 前端分层
|
||||
```
|
||||
src
|
||||
├─ api # 后端接口封装
|
||||
├─ components # 通用组件
|
||||
├─ layouts # 布局
|
||||
├─ pages # 页面
|
||||
├─ routes # 路由
|
||||
├─ hooks # 自定义 hooks
|
||||
├─ utils # 工具
|
||||
└─ types # 类型定义
|
||||
```
|
||||
|
||||
## 4. 数据库设计(核心)
|
||||
详见 `design/db_schema.md` 与 `design/db_schema_pgsql.sql`。
|
||||
核心表:
|
||||
- sys_user / sys_role / sys_user_role
|
||||
- sys_permission / sys_role_permission
|
||||
- sys_dict_type / sys_dict_item
|
||||
- sys_param
|
||||
- sys_log
|
||||
|
||||
## 5. 权限设计
|
||||
### 权限模型
|
||||
- 角色与权限多对多
|
||||
- 用户与角色多对多
|
||||
- 权限分为 menu / button
|
||||
- 支持层级菜单(1级/2级)
|
||||
|
||||
### 超级管理员
|
||||
- 约定 `user_id = 1` 为超级管理员
|
||||
- 后端权限查询对其返回全量权限
|
||||
|
||||
### 接口
|
||||
- `GET /api/permissions`:仅管理员可用(全量)
|
||||
- `GET /api/permissions/me`:当前用户权限
|
||||
- `GET /api/permissions/tree`:管理员权限树
|
||||
- `GET /api/permissions/tree/me`:当前用户权限树
|
||||
|
||||
### 创建/更新校验
|
||||
- level=1 时清空 parentId
|
||||
- level=2 时 parentId 必须存在且为 level=1
|
||||
- button 权限必须填写 code
|
||||
- menu 权限 code 可选
|
||||
|
||||
## 6. 前端权限处理
|
||||
- 登录后调用 `/api/users/me` 获取 `isAdmin`
|
||||
- `isAdmin=true` 时前端不做权限限制
|
||||
- 非管理员通过 `/api/permissions/me` 获取权限码用于按钮控制
|
||||
- 菜单展示按权限树构建
|
||||
|
||||
## 7. 关键流程
|
||||
### 登录
|
||||
1. `/auth/captcha`
|
||||
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. 前端构建树形菜单
|
||||
|
||||
## 8. 约束与规范
|
||||
- 后端禁用 JPA/Hibernate
|
||||
- 统一响应 `ApiResponse<T>`
|
||||
- Controller 不直接调用 Mapper
|
||||
- 前端禁止在页面内直接调用 axios
|
||||
|
||||
## 9. 后续扩展建议
|
||||
- 添加审计日志落库策略
|
||||
- 任务管理模块完善
|
||||
- 权限树缓存与增量刷新策略
|
||||
|
|
@ -23,6 +23,10 @@ public final class RedisKeys {
|
|||
return "sys:dict:" + typeCode;
|
||||
}
|
||||
|
||||
public static String platformConfigKey() {
|
||||
return "sys:platform:config";
|
||||
}
|
||||
|
||||
public static final String CACHE_EMPTY_MARKER = "EMPTY_MARKER";
|
||||
public static final String SYS_PARAM_FIELD_VALUE = "value";
|
||||
public static final String SYS_PARAM_FIELD_TYPE = "type";
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ package com.imeeting.config;
|
|||
import com.baomidou.mybatisplus.core.handlers.MetaObjectHandler;
|
||||
import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor;
|
||||
import com.baomidou.mybatisplus.extension.plugins.handler.TenantLineHandler;
|
||||
import com.baomidou.mybatisplus.extension.plugins.inner.PaginationInnerInterceptor;
|
||||
import com.baomidou.mybatisplus.extension.plugins.inner.TenantLineInnerInterceptor;
|
||||
import com.imeeting.security.LoginUser;
|
||||
import net.sf.jsqlparser.expression.Expression;
|
||||
|
|
@ -54,9 +55,10 @@ public class MybatisPlusConfig {
|
|||
}
|
||||
|
||||
// 公共表始终忽略过滤
|
||||
return List.of("sys_tenant", "sys_user", "sys_tenant_user", "sys_permission", "sys_role_permission", "sys_user_role", "sys_dict_type", "sys_dict_item", "sys_param").contains(tableName.toLowerCase());
|
||||
return List.of("sys_tenant","sys_platform_config", "sys_user", "sys_tenant_user", "sys_permission", "sys_role_permission", "sys_user_role", "sys_dict_type", "sys_dict_item", "sys_param").contains(tableName.toLowerCase());
|
||||
}
|
||||
}));
|
||||
interceptor.addInnerInterceptor(new PaginationInnerInterceptor());
|
||||
return interceptor;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -28,6 +28,8 @@ public class SecurityConfig {
|
|||
.sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
|
||||
.authorizeHttpRequests(auth -> auth
|
||||
.requestMatchers("/auth/**").permitAll()
|
||||
.requestMatchers("/api/open/**").permitAll()
|
||||
.requestMatchers("/api/static/**").permitAll()
|
||||
.requestMatchers("/api/params/value").permitAll()
|
||||
.anyRequest().authenticated()
|
||||
)
|
||||
|
|
|
|||
|
|
@ -27,10 +27,16 @@ public class PermissionController {
|
|||
}
|
||||
|
||||
@GetMapping
|
||||
@PreAuthorize("@ss.hasPermi('sys_permission:list')")
|
||||
@PreAuthorize("@ss.hasPermi('sys:permission:list')")
|
||||
public ApiResponse<List<SysPermission>> list() {
|
||||
Long tenantId = getCurrentTenantId();
|
||||
// 平台管理员查询所有
|
||||
if (Long.valueOf(0).equals(tenantId)) {
|
||||
return ApiResponse.ok(sysPermissionService.list());
|
||||
}
|
||||
// 非平台管理员只能查询自己拥有的权限
|
||||
return ApiResponse.ok(sysPermissionService.listByUserId(getCurrentUserId(), tenantId));
|
||||
}
|
||||
|
||||
@GetMapping("/me")
|
||||
public ApiResponse<List<SysPermission>> myPermissions() {
|
||||
|
|
@ -38,9 +44,16 @@ public class PermissionController {
|
|||
}
|
||||
|
||||
@GetMapping("/tree")
|
||||
@PreAuthorize("@ss.hasPermi('sys_permission:list')")
|
||||
@PreAuthorize("@ss.hasPermi('sys:permission:list')")
|
||||
public ApiResponse<List<PermissionNode>> tree() {
|
||||
return ApiResponse.ok(buildTree(sysPermissionService.list()));
|
||||
Long tenantId = getCurrentTenantId();
|
||||
List<SysPermission> list;
|
||||
if (Long.valueOf(0).equals(tenantId)) {
|
||||
list = sysPermissionService.list();
|
||||
} else {
|
||||
list = sysPermissionService.listByUserId(getCurrentUserId(), tenantId);
|
||||
}
|
||||
return ApiResponse.ok(buildTree(list));
|
||||
}
|
||||
|
||||
@GetMapping("/tree/me")
|
||||
|
|
@ -49,13 +62,13 @@ public class PermissionController {
|
|||
}
|
||||
|
||||
@GetMapping("/{id}")
|
||||
@PreAuthorize("@ss.hasPermi('sys_permission:query')")
|
||||
@PreAuthorize("@ss.hasPermi('sys:permission:query')")
|
||||
public ApiResponse<SysPermission> get(@PathVariable Long id) {
|
||||
return ApiResponse.ok(sysPermissionService.getById(id));
|
||||
}
|
||||
|
||||
@PostMapping
|
||||
@PreAuthorize("@ss.hasPermi('sys_permission:create')")
|
||||
@PreAuthorize("@ss.hasPermi('sys:permission:create')")
|
||||
public ApiResponse<Boolean> create(@RequestBody SysPermission perm) {
|
||||
String error = validateParent(perm);
|
||||
if (error != null) {
|
||||
|
|
@ -65,7 +78,7 @@ public class PermissionController {
|
|||
}
|
||||
|
||||
@PutMapping("/{id}")
|
||||
@PreAuthorize("@ss.hasPermi('sys_permission:update')")
|
||||
@PreAuthorize("@ss.hasPermi('sys:permission:update')")
|
||||
public ApiResponse<Boolean> update(@PathVariable Long id, @RequestBody SysPermission perm) {
|
||||
perm.setPermId(id);
|
||||
String error = validateParent(perm);
|
||||
|
|
@ -83,7 +96,7 @@ public class PermissionController {
|
|||
}
|
||||
|
||||
@DeleteMapping("/{id}")
|
||||
@PreAuthorize("@ss.hasPermi('sys_permission:delete')")
|
||||
@PreAuthorize("@ss.hasPermi('sys:permission:delete')")
|
||||
public ApiResponse<Boolean> delete(@PathVariable Long id) {
|
||||
return ApiResponse.ok(sysPermissionService.removeById(id));
|
||||
}
|
||||
|
|
|
|||
|
|
@ -11,11 +11,14 @@ import com.imeeting.mapper.SysRolePermissionMapper;
|
|||
import com.imeeting.mapper.SysUserRoleMapper;
|
||||
import com.imeeting.service.SysRoleService;
|
||||
import com.imeeting.service.SysUserService;
|
||||
import com.imeeting.service.SysPermissionService;
|
||||
import org.springframework.security.access.prepost.PreAuthorize;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.stream.Collectors;
|
||||
import java.util.Set;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/api/roles")
|
||||
|
|
@ -24,56 +27,60 @@ public class RoleController {
|
|||
private final SysUserService sysUserService;
|
||||
private final SysRolePermissionMapper sysRolePermissionMapper;
|
||||
private final SysUserRoleMapper sysUserRoleMapper;
|
||||
private final SysPermissionService sysPermissionService;
|
||||
|
||||
public RoleController(SysRoleService sysRoleService, SysUserService sysUserService, SysRolePermissionMapper sysRolePermissionMapper, SysUserRoleMapper sysUserRoleMapper) {
|
||||
public RoleController(SysRoleService sysRoleService, SysUserService sysUserService,
|
||||
SysRolePermissionMapper sysRolePermissionMapper, SysUserRoleMapper sysUserRoleMapper,
|
||||
SysPermissionService sysPermissionService) {
|
||||
this.sysRoleService = sysRoleService;
|
||||
this.sysUserService = sysUserService;
|
||||
this.sysRolePermissionMapper = sysRolePermissionMapper;
|
||||
this.sysUserRoleMapper = sysUserRoleMapper;
|
||||
this.sysPermissionService = sysPermissionService;
|
||||
}
|
||||
|
||||
@GetMapping
|
||||
@PreAuthorize("@ss.hasPermi('sys_role:list')")
|
||||
@PreAuthorize("@ss.hasPermi('sys:role:list')")
|
||||
public ApiResponse<List<SysRole>> list() {
|
||||
return ApiResponse.ok(sysRoleService.list());
|
||||
}
|
||||
|
||||
@GetMapping("/{id}/users")
|
||||
@PreAuthorize("@ss.hasPermi('sys_role:query')")
|
||||
@PreAuthorize("@ss.hasPermi('sys:role:query')")
|
||||
public ApiResponse<List<SysUser>> listUsers(@PathVariable Long id) {
|
||||
return ApiResponse.ok(sysUserService.listUsersByRoleId(id));
|
||||
}
|
||||
|
||||
@GetMapping("/{id}")
|
||||
@PreAuthorize("@ss.hasPermi('sys_role:query')")
|
||||
@PreAuthorize("@ss.hasPermi('sys:role:query')")
|
||||
public ApiResponse<SysRole> get(@PathVariable Long id) {
|
||||
return ApiResponse.ok(sysRoleService.getById(id));
|
||||
}
|
||||
|
||||
@PostMapping
|
||||
@PreAuthorize("@ss.hasPermi('sys_role:create')")
|
||||
@com.imeeting.common.annotation.Log(value = "新增角色", type = "角色管理")
|
||||
@PreAuthorize("@ss.hasPermi('sys:role:create')")
|
||||
@Log(value = "新增角色", type = "角色管理")
|
||||
public ApiResponse<Boolean> create(@RequestBody SysRole role) {
|
||||
return ApiResponse.ok(sysRoleService.save(role));
|
||||
}
|
||||
|
||||
@PutMapping("/{id}")
|
||||
@PreAuthorize("@ss.hasPermi('sys_role:update')")
|
||||
@com.imeeting.common.annotation.Log(value = "修改角色", type = "角色管理")
|
||||
@PreAuthorize("@ss.hasPermi('sys:role:update')")
|
||||
@Log(value = "修改角色", type = "角色管理")
|
||||
public ApiResponse<Boolean> update(@PathVariable Long id, @RequestBody SysRole role) {
|
||||
role.setRoleId(id);
|
||||
return ApiResponse.ok(sysRoleService.updateById(role));
|
||||
}
|
||||
|
||||
@DeleteMapping("/{id}")
|
||||
@PreAuthorize("@ss.hasPermi('sys_role:delete')")
|
||||
@PreAuthorize("@ss.hasPermi('sys:role:delete')")
|
||||
@Log(value = "删除角色", type = "角色管理")
|
||||
public ApiResponse<Boolean> delete(@PathVariable Long id) {
|
||||
return ApiResponse.ok(sysRoleService.removeById(id));
|
||||
}
|
||||
|
||||
@GetMapping("/{id}/permissions")
|
||||
@PreAuthorize("@ss.hasPermi('sys_role:permission:list')")
|
||||
@PreAuthorize("@ss.hasPermi('sys:role:permission:list')")
|
||||
public ApiResponse<List<Long>> listRolePermissions(@PathVariable Long id) {
|
||||
List<SysRolePermission> rows = sysRolePermissionMapper.selectList(
|
||||
new QueryWrapper<SysRolePermission>().eq("role_id", id)
|
||||
|
|
@ -88,9 +95,28 @@ public class RoleController {
|
|||
}
|
||||
|
||||
@PostMapping("/{id}/permissions")
|
||||
@PreAuthorize("@ss.hasPermi('sys_role:permission:save')")
|
||||
@PreAuthorize("@ss.hasPermi('sys:role:permission:save')")
|
||||
public ApiResponse<Boolean> saveRolePermissions(@PathVariable Long id, @RequestBody PermissionBindingPayload payload) {
|
||||
List<Long> permIds = payload == null ? null : payload.getPermIds();
|
||||
|
||||
// 权限越权校验
|
||||
Long currentTenantId = getCurrentTenantId();
|
||||
if (!Long.valueOf(0).equals(currentTenantId)) {
|
||||
List<com.imeeting.entity.SysPermission> myPerms = sysPermissionService.listByUserId(getCurrentUserId(), currentTenantId);
|
||||
|
||||
Set<Long> myPermIds = myPerms.stream()
|
||||
.map(com.imeeting.entity.SysPermission::getPermId)
|
||||
.collect(Collectors.toSet());
|
||||
|
||||
if (permIds != null) {
|
||||
for (Long pId : permIds) {
|
||||
if (!myPermIds.contains(pId)) {
|
||||
return ApiResponse.error("越权分配权限:" + pId);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
sysRolePermissionMapper.delete(new QueryWrapper<SysRolePermission>().eq("role_id", id));
|
||||
if (permIds == null || permIds.isEmpty()) {
|
||||
return ApiResponse.ok(true);
|
||||
|
|
@ -108,7 +134,7 @@ public class RoleController {
|
|||
}
|
||||
|
||||
@PostMapping("/{id}/users")
|
||||
@PreAuthorize("@ss.hasPermi('sys_role:update')")
|
||||
@PreAuthorize("@ss.hasPermi('sys:role:update')")
|
||||
@Log(value = "角色关联用户", type = "角色管理")
|
||||
public ApiResponse<Boolean> bindUsers(@PathVariable Long id, @RequestBody UserBindingPayload payload) {
|
||||
if (payload == null || payload.getUserIds() == null) {
|
||||
|
|
@ -128,7 +154,7 @@ public class RoleController {
|
|||
}
|
||||
|
||||
@DeleteMapping("/{id}/users/{userId}")
|
||||
@PreAuthorize("@ss.hasPermi('sys_role:update')")
|
||||
@PreAuthorize("@ss.hasPermi('sys:role:update')")
|
||||
@Log(value = "角色取消关联用户", type = "角色管理")
|
||||
public ApiResponse<Boolean> unbindUser(@PathVariable Long id, @PathVariable Long userId) {
|
||||
QueryWrapper<SysUserRole> qw = new QueryWrapper<>();
|
||||
|
|
@ -137,6 +163,22 @@ public class RoleController {
|
|||
return ApiResponse.ok(true);
|
||||
}
|
||||
|
||||
private Long getCurrentUserId() {
|
||||
org.springframework.security.core.Authentication authentication = org.springframework.security.core.context.SecurityContextHolder.getContext().getAuthentication();
|
||||
if (authentication != null && authentication.getPrincipal() instanceof com.imeeting.security.LoginUser) {
|
||||
return ((com.imeeting.security.LoginUser) authentication.getPrincipal()).getUserId();
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private Long getCurrentTenantId() {
|
||||
org.springframework.security.core.Authentication authentication = org.springframework.security.core.context.SecurityContextHolder.getContext().getAuthentication();
|
||||
if (authentication != null && authentication.getPrincipal() instanceof com.imeeting.security.LoginUser) {
|
||||
return ((com.imeeting.security.LoginUser) authentication.getPrincipal()).getTenantId();
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public static class UserBindingPayload {
|
||||
private List<Long> userIds;
|
||||
public List<Long> getUserIds() { return userIds; }
|
||||
|
|
|
|||
|
|
@ -20,26 +20,26 @@ public class SysOrgController {
|
|||
}
|
||||
|
||||
@GetMapping
|
||||
@PreAuthorize("@ss.hasPermi('sys_org:list')")
|
||||
@PreAuthorize("@ss.hasPermi('sys:org:list')")
|
||||
public ApiResponse<List<SysOrg>> list(@RequestParam(required = false) Long tenantId) {
|
||||
return ApiResponse.ok(sysOrgService.listTree(tenantId));
|
||||
}
|
||||
|
||||
@GetMapping("/{id}")
|
||||
@PreAuthorize("@ss.hasPermi('sys_org:query')")
|
||||
@PreAuthorize("@ss.hasPermi('sys:org:query')")
|
||||
public ApiResponse<SysOrg> get(@PathVariable Long id) {
|
||||
return ApiResponse.ok(sysOrgService.getById(id));
|
||||
}
|
||||
|
||||
@PostMapping
|
||||
@PreAuthorize("@ss.hasPermi('sys_org:create')")
|
||||
@PreAuthorize("@ss.hasPermi('sys:org:create')")
|
||||
@Log(value = "新增组织", type = "组织管理")
|
||||
public ApiResponse<Boolean> create(@RequestBody SysOrg org) {
|
||||
return ApiResponse.ok(sysOrgService.save(org));
|
||||
}
|
||||
|
||||
@PutMapping("/{id}")
|
||||
@PreAuthorize("@ss.hasPermi('sys_org:update')")
|
||||
@PreAuthorize("@ss.hasPermi('sys:org:update')")
|
||||
@Log(value = "修改组织", type = "组织管理")
|
||||
public ApiResponse<Boolean> update(@PathVariable Long id, @RequestBody SysOrg org) {
|
||||
org.setId(id);
|
||||
|
|
@ -47,7 +47,7 @@ public class SysOrgController {
|
|||
}
|
||||
|
||||
@DeleteMapping("/{id}")
|
||||
@PreAuthorize("@ss.hasPermi('sys_org:delete')")
|
||||
@PreAuthorize("@ss.hasPermi('sys:org:delete')")
|
||||
@Log(value = "删除组织", type = "组织管理")
|
||||
public ApiResponse<Boolean> delete(@PathVariable Long id) {
|
||||
// Check if has children
|
||||
|
|
|
|||
|
|
@ -1,12 +1,16 @@
|
|||
package com.imeeting.controller;
|
||||
|
||||
import com.imeeting.common.ApiResponse;
|
||||
import com.imeeting.common.PageResult;
|
||||
import com.imeeting.dto.SysParamQueryDTO;
|
||||
import com.imeeting.dto.SysParamVO;
|
||||
import com.imeeting.entity.SysParam;
|
||||
import com.imeeting.service.SysParamService;
|
||||
import org.springframework.security.access.prepost.PreAuthorize;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/api/params")
|
||||
|
|
@ -17,16 +21,22 @@ public class SysParamController {
|
|||
this.sysParamService = sysParamService;
|
||||
}
|
||||
|
||||
@GetMapping("/page")
|
||||
@PreAuthorize("@ss.hasPermi('sys_param:list')")
|
||||
public ApiResponse<PageResult<List<SysParamVO>>> page(SysParamQueryDTO query) {
|
||||
return ApiResponse.ok(sysParamService.page(query));
|
||||
}
|
||||
|
||||
@GetMapping
|
||||
@PreAuthorize("@ss.hasPermi('sys_param:list')")
|
||||
public ApiResponse<List<SysParam>> list() {
|
||||
return ApiResponse.ok(sysParamService.list());
|
||||
public ApiResponse<List<SysParamVO>> list() {
|
||||
return ApiResponse.ok(sysParamService.list().stream().map(this::toVO).collect(Collectors.toList()));
|
||||
}
|
||||
|
||||
@GetMapping("/{id}")
|
||||
@PreAuthorize("@ss.hasPermi('sys_param:query')")
|
||||
public ApiResponse<SysParam> get(@PathVariable Long id) {
|
||||
return ApiResponse.ok(sysParamService.getById(id));
|
||||
public ApiResponse<SysParamVO> get(@PathVariable Long id) {
|
||||
return ApiResponse.ok(toVO(sysParamService.getById(id)));
|
||||
}
|
||||
|
||||
@PostMapping
|
||||
|
|
@ -66,4 +76,19 @@ public class SysParamController {
|
|||
@RequestParam(value = "defaultValue", required = false) String defaultValue) {
|
||||
return ApiResponse.ok(sysParamService.getCachedParamValue(key, defaultValue));
|
||||
}
|
||||
|
||||
private SysParamVO toVO(SysParam entity) {
|
||||
if (entity == null) return null;
|
||||
SysParamVO vo = new SysParamVO();
|
||||
vo.setParamId(entity.getParamId());
|
||||
vo.setParamKey(entity.getParamKey());
|
||||
vo.setParamValue(entity.getParamValue());
|
||||
vo.setParamType(entity.getParamType());
|
||||
vo.setIsSystem(entity.getIsSystem());
|
||||
vo.setDescription(entity.getDescription());
|
||||
vo.setStatus(entity.getStatus());
|
||||
vo.setCreatedAt(entity.getCreatedAt());
|
||||
vo.setUpdatedAt(entity.getUpdatedAt());
|
||||
return vo;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -29,34 +29,54 @@ public class UserController {
|
|||
private final JwtTokenProvider jwtTokenProvider;
|
||||
private final SysUserRoleMapper sysUserRoleMapper;
|
||||
private final com.imeeting.service.SysTenantUserService sysTenantUserService;
|
||||
private final com.imeeting.service.SysRoleService sysRoleService;
|
||||
|
||||
public UserController(SysUserService sysUserService, PasswordEncoder passwordEncoder, JwtTokenProvider jwtTokenProvider, SysUserRoleMapper sysUserRoleMapper, com.imeeting.service.SysTenantUserService sysTenantUserService) {
|
||||
public UserController(SysUserService sysUserService, PasswordEncoder passwordEncoder,
|
||||
JwtTokenProvider jwtTokenProvider, SysUserRoleMapper sysUserRoleMapper,
|
||||
com.imeeting.service.SysTenantUserService sysTenantUserService,
|
||||
com.imeeting.service.SysRoleService sysRoleService) {
|
||||
this.sysUserService = sysUserService;
|
||||
this.passwordEncoder = passwordEncoder;
|
||||
this.jwtTokenProvider = jwtTokenProvider;
|
||||
this.sysUserRoleMapper = sysUserRoleMapper;
|
||||
this.sysTenantUserService = sysTenantUserService;
|
||||
this.sysRoleService = sysRoleService;
|
||||
}
|
||||
|
||||
@GetMapping
|
||||
@PreAuthorize("@ss.hasPermi('sys_user:list')")
|
||||
@PreAuthorize("@ss.hasPermi('sys:user:list')")
|
||||
public ApiResponse<List<SysUser>> list(@RequestParam(required = false) Long tenantId, @RequestParam(required = false) Long orgId) {
|
||||
Long currentTenantId = getCurrentTenantId();
|
||||
List<SysUser> users;
|
||||
|
||||
if (Long.valueOf(0).equals(currentTenantId) && tenantId == null) {
|
||||
List<SysUser> allUsers = sysUserService.list();
|
||||
// 为每个用户加载其租户关系
|
||||
if (allUsers != null && !allUsers.isEmpty()) {
|
||||
for (SysUser user : allUsers) {
|
||||
user.setMemberships(sysTenantUserService.listByUserId(user.getUserId()));
|
||||
}
|
||||
}
|
||||
return ApiResponse.ok(allUsers);
|
||||
}
|
||||
users = sysUserService.list();
|
||||
} else {
|
||||
Long targetTenantId = tenantId != null ? tenantId : currentTenantId;
|
||||
if (targetTenantId == null) {
|
||||
return ApiResponse.error("Tenant ID required");
|
||||
}
|
||||
return ApiResponse.ok(sysUserService.listUsersByTenant(targetTenantId, orgId));
|
||||
users = sysUserService.listUsersByTenant(targetTenantId, orgId);
|
||||
}
|
||||
|
||||
if (users != null && !users.isEmpty()) {
|
||||
for (SysUser user : users) {
|
||||
// 加载租户关系
|
||||
user.setMemberships(sysTenantUserService.listByUserId(user.getUserId()));
|
||||
|
||||
// 加载角色信息
|
||||
List<SysUserRole> userRoles = sysUserRoleMapper.selectList(
|
||||
new QueryWrapper<SysUserRole>().eq("user_id", user.getUserId())
|
||||
);
|
||||
if (userRoles != null && !userRoles.isEmpty()) {
|
||||
List<Long> roleIds = userRoles.stream()
|
||||
.map(SysUserRole::getRoleId)
|
||||
.collect(java.util.stream.Collectors.toList());
|
||||
user.setRoles(sysRoleService.listByIds(roleIds));
|
||||
}
|
||||
}
|
||||
}
|
||||
return ApiResponse.ok(users);
|
||||
}
|
||||
|
||||
@GetMapping("/me")
|
||||
|
|
@ -85,7 +105,7 @@ public class UserController {
|
|||
}
|
||||
|
||||
@GetMapping("/{id}")
|
||||
@PreAuthorize("@ss.hasPermi('sys_user:query')")
|
||||
@PreAuthorize("@ss.hasPermi('sys:user:query')")
|
||||
public ApiResponse<SysUser> get(@PathVariable Long id) {
|
||||
SysUser user = sysUserService.getByIdIgnoreTenant(id);
|
||||
if (user != null) {
|
||||
|
|
@ -103,9 +123,24 @@ public class UserController {
|
|||
}
|
||||
|
||||
@PostMapping
|
||||
@PreAuthorize("@ss.hasPermi('sys_user:create')")
|
||||
@PreAuthorize("@ss.hasPermi('sys:user:create')")
|
||||
@Log(value = "新增用户", type = "用户管理")
|
||||
public ApiResponse<Boolean> create(@RequestBody SysUser user) {
|
||||
Long currentTenantId = getCurrentTenantId();
|
||||
// 非平台管理员强制设置为当前租户
|
||||
if (!Long.valueOf(0).equals(currentTenantId)) {
|
||||
if (user.getMemberships() != null && !user.getMemberships().isEmpty()) {
|
||||
user.getMemberships().forEach(m -> m.setTenantId(currentTenantId));
|
||||
} else {
|
||||
// 如果没传身份,补齐当前租户身份
|
||||
List<com.imeeting.entity.SysTenantUser> memberships = new java.util.ArrayList<>();
|
||||
com.imeeting.entity.SysTenantUser m = new com.imeeting.entity.SysTenantUser();
|
||||
m.setTenantId(currentTenantId);
|
||||
memberships.add(m);
|
||||
user.setMemberships(memberships);
|
||||
}
|
||||
}
|
||||
|
||||
if (user.getPasswordHash() != null && !user.getPasswordHash().isEmpty()) {
|
||||
user.setPasswordHash(passwordEncoder.encode(user.getPasswordHash()));
|
||||
}
|
||||
|
|
@ -117,10 +152,19 @@ public class UserController {
|
|||
}
|
||||
|
||||
@PutMapping("/{id}")
|
||||
@PreAuthorize("@ss.hasPermi('sys_user:update')")
|
||||
@PreAuthorize("@ss.hasPermi('sys:user:update')")
|
||||
@Log(value = "修改用户", type = "用户管理")
|
||||
public ApiResponse<Boolean> update(@PathVariable Long id, @RequestBody SysUser user) {
|
||||
Long currentTenantId = getCurrentTenantId();
|
||||
user.setUserId(id);
|
||||
|
||||
// 非平台管理员强制约束租户身份
|
||||
if (!Long.valueOf(0).equals(currentTenantId)) {
|
||||
if (user.getMemberships() != null) {
|
||||
user.getMemberships().forEach(m -> m.setTenantId(currentTenantId));
|
||||
}
|
||||
}
|
||||
|
||||
if (user.getPasswordHash() != null && !user.getPasswordHash().isEmpty()) {
|
||||
user.setPasswordHash(passwordEncoder.encode(user.getPasswordHash()));
|
||||
}
|
||||
|
|
@ -132,14 +176,14 @@ public class UserController {
|
|||
}
|
||||
|
||||
@DeleteMapping("/{id}")
|
||||
@PreAuthorize("@ss.hasPermi('sys_user:delete')")
|
||||
@PreAuthorize("@ss.hasPermi('sys:user:delete')")
|
||||
@Log(value = "删除用户", type = "用户管理")
|
||||
public ApiResponse<Boolean> delete(@PathVariable Long id) {
|
||||
return ApiResponse.ok(sysUserService.removeById(id));
|
||||
}
|
||||
|
||||
@GetMapping("/{id}/roles")
|
||||
@PreAuthorize("@ss.hasPermi('sys_user:role:list')")
|
||||
@PreAuthorize("@ss.hasPermi('sys:user:role:list')")
|
||||
public ApiResponse<List<Long>> listUserRoles(@PathVariable Long id) {
|
||||
List<SysUserRole> rows = sysUserRoleMapper.selectList(
|
||||
new QueryWrapper<SysUserRole>().eq("user_id", id)
|
||||
|
|
@ -154,7 +198,7 @@ public class UserController {
|
|||
}
|
||||
|
||||
@PostMapping("/{id}/roles")
|
||||
@PreAuthorize("@ss.hasPermi('sys_user:role:save')")
|
||||
@PreAuthorize("@ss.hasPermi('sys:user:role:save')")
|
||||
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));
|
||||
|
|
|
|||
|
|
@ -21,7 +21,4 @@ public class SysDictItem extends BaseEntity {
|
|||
|
||||
@TableField(exist = false)
|
||||
private Long tenantId;
|
||||
|
||||
@TableField(exist = false)
|
||||
private Integer isDeleted;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -19,7 +19,4 @@ public class SysDictType extends BaseEntity {
|
|||
|
||||
@TableField(exist = false)
|
||||
private Long tenantId;
|
||||
|
||||
@TableField(exist = false)
|
||||
private Integer isDeleted;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,7 +5,15 @@ import com.baomidou.mybatisplus.annotation.TableId;
|
|||
import com.baomidou.mybatisplus.annotation.TableName;
|
||||
import lombok.Data;
|
||||
|
||||
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 lombok.EqualsAndHashCode;
|
||||
|
||||
@Data
|
||||
@EqualsAndHashCode(callSuper = true)
|
||||
@TableName("sys_param")
|
||||
public class SysParam extends BaseEntity {
|
||||
@TableId(value = "param_id", type = IdType.AUTO)
|
||||
|
|
@ -15,4 +23,7 @@ public class SysParam extends BaseEntity {
|
|||
private String paramType;
|
||||
private Integer isSystem;
|
||||
private String description;
|
||||
|
||||
@TableField(exist = false)
|
||||
private Long tenantId;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -26,4 +26,7 @@ public class SysUser extends BaseEntity {
|
|||
|
||||
@com.baomidou.mybatisplus.annotation.TableField(exist = false)
|
||||
private java.util.List<SysTenantUser> memberships;
|
||||
|
||||
@com.baomidou.mybatisplus.annotation.TableField(exist = false)
|
||||
private java.util.List<SysRole> roles;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -14,7 +14,7 @@ public interface SysPermissionMapper extends BaseMapper<SysPermission> {
|
|||
@Select("""
|
||||
SELECT DISTINCT p.*
|
||||
FROM sys_permission p
|
||||
JOIN sys_role_permission rp ON rp.perm_id = p.perm_id
|
||||
JOIN sys_role_permission rp ON rp.perm_id = p.perm_id and p.is_deleted=0
|
||||
JOIN sys_role r ON r.role_id = rp.role_id
|
||||
JOIN sys_user_role ur ON ur.role_id = r.role_id
|
||||
WHERE ur.user_id = #{userId} AND r.tenant_id = #{tenantId}
|
||||
|
|
|
|||
|
|
@ -1,9 +1,16 @@
|
|||
package com.imeeting.service;
|
||||
|
||||
import com.baomidou.mybatisplus.extension.service.IService;
|
||||
import com.imeeting.common.PageResult;
|
||||
import com.imeeting.dto.SysParamQueryDTO;
|
||||
import com.imeeting.dto.SysParamVO;
|
||||
import com.imeeting.entity.SysParam;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public interface SysParamService extends IService<SysParam> {
|
||||
PageResult<List<SysParamVO>> page(SysParamQueryDTO query);
|
||||
|
||||
String getParamValue(String key, String defaultValue);
|
||||
|
||||
String getCachedParamValue(String key, String defaultValue);
|
||||
|
|
|
|||
|
|
@ -1,8 +1,12 @@
|
|||
package com.imeeting.service.impl;
|
||||
|
||||
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
||||
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
|
||||
import com.imeeting.common.PageResult;
|
||||
import com.imeeting.common.RedisKeys;
|
||||
import com.imeeting.dto.SysParamQueryDTO;
|
||||
import com.imeeting.dto.SysParamVO;
|
||||
import com.imeeting.entity.SysParam;
|
||||
import com.imeeting.mapper.SysParamMapper;
|
||||
import com.imeeting.service.SysParamService;
|
||||
|
|
@ -14,6 +18,7 @@ import org.springframework.transaction.annotation.Transactional;
|
|||
import java.io.Serializable;
|
||||
import java.time.Duration;
|
||||
import java.util.List;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
@Slf4j
|
||||
@Service
|
||||
|
|
@ -24,6 +29,44 @@ public class SysParamServiceImpl extends ServiceImpl<SysParamMapper, SysParam> i
|
|||
this.redisTemplate = redisTemplate;
|
||||
}
|
||||
|
||||
@Override
|
||||
public PageResult<List<SysParamVO>> page(SysParamQueryDTO query) {
|
||||
Page<SysParam> page = new Page<>(query.getPageNum(), query.getPageSize());
|
||||
LambdaQueryWrapper<SysParam> wrapper = new LambdaQueryWrapper<>();
|
||||
if (query.getParamKey() != null && !query.getParamKey().isEmpty()) {
|
||||
wrapper.like(SysParam::getParamKey, query.getParamKey());
|
||||
}
|
||||
if (query.getParamType() != null && !query.getParamType().isEmpty()) {
|
||||
wrapper.eq(SysParam::getParamType, query.getParamType());
|
||||
}
|
||||
if (query.getDescription() != null && !query.getDescription().isEmpty()) {
|
||||
wrapper.like(SysParam::getDescription, query.getDescription());
|
||||
}
|
||||
wrapper.orderByDesc(SysParam::getCreatedAt);
|
||||
|
||||
Page<SysParam> result = this.baseMapper.selectPage(page, wrapper);
|
||||
|
||||
PageResult<List<SysParamVO>> pageResult = new PageResult<>();
|
||||
pageResult.setTotal(result.getTotal());
|
||||
pageResult.setRecords(result.getRecords().stream().map(this::toVO).collect(Collectors.toList()));
|
||||
return pageResult;
|
||||
}
|
||||
|
||||
private SysParamVO toVO(SysParam entity) {
|
||||
if (entity == null) return null;
|
||||
SysParamVO vo = new SysParamVO();
|
||||
vo.setParamId(entity.getParamId());
|
||||
vo.setParamKey(entity.getParamKey());
|
||||
vo.setParamValue(entity.getParamValue());
|
||||
vo.setParamType(entity.getParamType());
|
||||
vo.setIsSystem(entity.getIsSystem());
|
||||
vo.setDescription(entity.getDescription());
|
||||
vo.setStatus(entity.getStatus());
|
||||
vo.setCreatedAt(entity.getCreatedAt());
|
||||
vo.setUpdatedAt(entity.getUpdatedAt());
|
||||
return vo;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getParamValue(String key, String defaultValue) {
|
||||
if (key == null || key.isEmpty()) {
|
||||
|
|
|
|||
|
|
@ -30,6 +30,8 @@ security:
|
|||
secret: change-me-please-change-me-32bytes
|
||||
|
||||
app:
|
||||
upload-path: D:/data/imeeting/uploads/
|
||||
resource-prefix: /api/static/
|
||||
captcha:
|
||||
ttl-seconds: 120
|
||||
max-attempts: 5
|
||||
|
|
|
|||
BIN
design/用户管理.png
BIN
design/用户管理.png
Binary file not shown.
|
Before Width: | Height: | Size: 124 KiB |
BIN
design/登录页.PNG
BIN
design/登录页.PNG
Binary file not shown.
|
Before Width: | Height: | Size: 321 KiB |
BIN
design/角色新增.png
BIN
design/角色新增.png
Binary file not shown.
|
Before Width: | Height: | Size: 205 KiB |
BIN
design/角色管理.png
BIN
design/角色管理.png
Binary file not shown.
|
Before Width: | Height: | Size: 115 KiB |
BIN
design/首页.PNG
BIN
design/首页.PNG
Binary file not shown.
|
Before Width: | Height: | Size: 139 KiB |
|
|
@ -47,7 +47,7 @@ src
|
|||
你是一位**务实型前端开发者 Agent**,目标是:
|
||||
|
||||
> 以清晰的数据流和稳定的交互,构建易维护的管理后台。
|
||||
|
||||
> 阅读对应的后端controller了解接口
|
||||
### 核心原则
|
||||
|
||||
* 清晰的意图胜于技巧性的实现
|
||||
|
|
|
|||
|
|
@ -1,167 +0,0 @@
|
|||
# Nex Design 前端设计规范
|
||||
|
||||
面向 **React + Ant Design + Tailwind CSS** 的前端设计语言系统,目标:
|
||||
|
||||
- **一致性**:保证视觉与交互统一
|
||||
- **高效性**:提供可复用组件与布局模式
|
||||
- **可维护性**:清晰规范,便于迭代
|
||||
- **用户体验**:直观易用
|
||||
|
||||
## 技术栈
|
||||
|
||||
- 框架:React 18+
|
||||
- 组件库:Ant Design 5.x
|
||||
- 样式:Tailwind CSS 3.x
|
||||
- 包管理:Yarn
|
||||
- 运行时:Node.js 16+
|
||||
|
||||
---
|
||||
|
||||
## 设计原则
|
||||
|
||||
- **清晰明确**:界面直观,操作易理解
|
||||
- **一致性优先**:视觉、交互、用词统一
|
||||
- **效率至上**:减少操作步骤,提高效率
|
||||
- **反馈及时**:操作有明确状态反馈
|
||||
- **容错友好**:预防错误,提示明确
|
||||
|
||||
|
||||
|
||||
**使用规范**:
|
||||
|
||||
- 主色:关键按钮、重要信息、链接
|
||||
- 功能色:按语义使用,不混淆
|
||||
- 中性色:文本、背景、边框
|
||||
- 对比度 ≥ 4.5:1
|
||||
|
||||
---
|
||||
|
||||
## 排版规范
|
||||
|
||||
### 字体
|
||||
|
||||
- 默认:`-apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto …`
|
||||
- 等宽:`'SF Mono', 'Monaco', 'Fira Code', …`
|
||||
|
||||
### 字号
|
||||
|
||||
| 用途 | 大小 | Tailwind 类 | 场景 |
|
||||
|------|------|-------------|------|
|
||||
| 特大标题 | 32px | `text-4xl` | 页面主标题 |
|
||||
| 大标题 | 24px | `text-2xl` | 区块标题 |
|
||||
| 中标题 | 20px | `text-xl` | 卡片标题 |
|
||||
| 小标题 | 16px | `text-base` | 表单标签 |
|
||||
| 正文 | 14px | `text-sm` | 正文 |
|
||||
| 辅助文字 | 12px | `text-xs` | 说明 |
|
||||
|
||||
### 字重
|
||||
|
||||
| 用途 | 字重 |
|
||||
|------|-----|
|
||||
| 正文 | Regular 400 |
|
||||
| 表单标签、列表 | Medium 500 |
|
||||
| 小标题、强调 | Semibold 600 |
|
||||
| 标题、重要信息 | Bold 700 |
|
||||
|
||||
---
|
||||
|
||||
## 间距系统
|
||||
|
||||
- 基于 **8px 网格**,间距均为 8 的倍数
|
||||
- Tailwind 对应类:
|
||||
|
||||
| 尺寸 | Tailwind 类 | 间距 |
|
||||
|------|------------|------|
|
||||
| xs | p-1 / m-1 | 4px |
|
||||
| sm | p-2 / m-2 | 8px |
|
||||
| md | p-4 / m-4 | 16px |
|
||||
| lg | p-6 / m-6 | 24px |
|
||||
| xl | p-8 / m-8 | 32px |
|
||||
| 2xl | p-12 / m-12 | 48px |
|
||||
|
||||
---
|
||||
|
||||
## 组件规范
|
||||
|
||||
### 按钮 (Button)
|
||||
|
||||
- 类型:Primary / Default / Text / Link / Danger
|
||||
- 尺寸:Large 40px / Middle 32px / Small 24px
|
||||
- 使用:
|
||||
- 单区域最多一个主按钮
|
||||
- 文字 ≤ 4 个字
|
||||
- 危险操作二次确认
|
||||
|
||||
### 表单 (Form)
|
||||
|
||||
- 布局:vertical
|
||||
- 必填:红色星号
|
||||
- 字段宽度合理,间距 24px
|
||||
- 错误提示显示在字段下方
|
||||
|
||||
### 表格 (Table)
|
||||
|
||||
- 分页默认 10 条
|
||||
- 行高 54px (middle)
|
||||
- 操作列固定右侧
|
||||
- 加载状态使用 `loading`
|
||||
|
||||
### 卡片 (Card)
|
||||
|
||||
- 内边距 24px
|
||||
- 圆角 8px
|
||||
- 阴影 `shadow-sm`
|
||||
- 卡片间距 16px
|
||||
|
||||
---
|
||||
|
||||
## 布局规范
|
||||
|
||||
- 页面:Header 64px, Sider 200px, Content 区域
|
||||
- 栅格:24 栏,使用 Ant Design Grid
|
||||
- 页面内边距 24px,内容最大宽 1200px
|
||||
- 响应式:Flexbox/Grid + Tailwind 前缀
|
||||
|
||||
---
|
||||
|
||||
## 交互规范
|
||||
|
||||
- **全局提示**:`message.success/error/warning/loading`
|
||||
- **通知提醒**:`notification.open`
|
||||
- **模态框**:`Modal.confirm`
|
||||
- **加载状态**:`Spin / Skeleton / Table loading`
|
||||
- **动画**:300ms, ease-in-out
|
||||
|
||||
---
|
||||
|
||||
## 页面模板
|
||||
|
||||
### 通用结构
|
||||
|
||||
- 面包屑导航
|
||||
- 页面标题区
|
||||
- 主要内容区
|
||||
|
||||
### 已实现
|
||||
|
||||
- 主框架页面:侧边栏、顶部导航、内容滚动
|
||||
- Dashboard:统计卡片、图表
|
||||
|
||||
### 待完善
|
||||
|
||||
- 列表页
|
||||
- 详情页
|
||||
- 表单页
|
||||
- 设置页
|
||||
|
||||
---
|
||||
|
||||
## 开发规范
|
||||
|
||||
- **新增操作**:默认使用**右侧抽屉**打开表单进行创建(除非需求明确允许其他方式)
|
||||
- **组件命名**:PascalCase
|
||||
- **文件命名**:与组件同名
|
||||
- **样式类命名**:kebab-case
|
||||
- **常量命名**:UPPER_SNAKE_CASE
|
||||
- **样式**:Tailwind 优先,Ant Design 主题定制,自定义样式放组件目录
|
||||
|
||||
|
|
@ -1,5 +1,36 @@
|
|||
import AppRoutes from "./routes";
|
||||
import { useEffect, useState } from "react";
|
||||
import AppRoutes from "./routes";
|
||||
import { getOpenPlatformConfig } from "./api";
|
||||
import type { SysPlatformConfig } from "./types";
|
||||
|
||||
export default function App() {
|
||||
const [config, setConfig] = useState<SysPlatformConfig | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
const fetchConfig = async () => {
|
||||
try {
|
||||
const data = await getOpenPlatformConfig();
|
||||
setConfig(data);
|
||||
if (data.projectName) {
|
||||
document.title = data.projectName;
|
||||
}
|
||||
if (data.iconUrl) {
|
||||
let link: HTMLLinkElement | null = document.querySelector("link[rel~='icon']");
|
||||
if (!link) {
|
||||
link = document.createElement('link');
|
||||
link.rel = 'icon';
|
||||
document.getElementsByTagName('head')[0].appendChild(link);
|
||||
}
|
||||
link.href = data.iconUrl;
|
||||
}
|
||||
// Save to sessionStorage for other components
|
||||
sessionStorage.setItem("platformConfig", JSON.stringify(data));
|
||||
} catch (e) {
|
||||
console.error("Failed to load platform config", e);
|
||||
}
|
||||
};
|
||||
fetchConfig();
|
||||
}, []);
|
||||
|
||||
return <AppRoutes />;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,28 @@
|
|||
import http from "./http";
|
||||
import { DeviceInfo, SysPermission, SysRole, SysUser, UserProfile } from "../types";
|
||||
import {
|
||||
DeviceInfo, SysPermission, SysRole, SysUser, UserProfile, SysParamVO, SysParamQuery, PageResult,
|
||||
PermissionNode
|
||||
} from "../types";
|
||||
|
||||
export async function pageParams(params: SysParamQuery) {
|
||||
const resp = await http.get("/api/params/page", { params });
|
||||
return resp.data.data as PageResult<SysParamVO[]>;
|
||||
}
|
||||
|
||||
export async function createParam(payload: Partial<SysParamVO>) {
|
||||
const resp = await http.post("/api/params", payload);
|
||||
return resp.data.data as boolean;
|
||||
}
|
||||
|
||||
export async function updateParam(id: number, payload: Partial<SysParamVO>) {
|
||||
const resp = await http.put(`/api/params/${id}`, payload);
|
||||
return resp.data.data as boolean;
|
||||
}
|
||||
|
||||
export async function deleteParam(id: number) {
|
||||
const resp = await http.delete(`/api/params/${id}`);
|
||||
return resp.data.data as boolean;
|
||||
}
|
||||
|
||||
export async function listUsers(params?: { tenantId?: number; orgId?: number }) {
|
||||
const resp = await http.get("/api/users", { params });
|
||||
|
|
@ -150,4 +173,5 @@ export async function fetchLogs(params: any) {
|
|||
export * from "./dict";
|
||||
export * from "./tenant";
|
||||
export * from "./org";
|
||||
export * from "./platform";
|
||||
|
||||
|
|
|
|||
|
|
@ -21,7 +21,7 @@ import { useAuth } from "../hooks/useAuth";
|
|||
import { usePermission } from "../hooks/usePermission";
|
||||
import { listMyPermissions, getCurrentUser } from "../api";
|
||||
import { switchTenant, type TenantInfo } from "../api/auth";
|
||||
import { SysPermission } from "../types";
|
||||
import { SysPermission, SysPlatformConfig } from "../types";
|
||||
|
||||
const { Header, Sider, Content } = Layout;
|
||||
|
||||
|
|
@ -41,6 +41,11 @@ export default function AppLayout() {
|
|||
const [availableTenants, setAvailableTenants] = useState<TenantInfo[]>([]);
|
||||
const [currentTenantId, setCurrentTenantId] = useState<number | null>(null);
|
||||
|
||||
const platformConfig = useMemo<SysPlatformConfig | null>(() => {
|
||||
const configStr = sessionStorage.getItem("platformConfig");
|
||||
return configStr ? JSON.parse(configStr) : null;
|
||||
}, []);
|
||||
|
||||
const location = useLocation();
|
||||
const navigate = useNavigate();
|
||||
const { logout } = useAuth();
|
||||
|
|
@ -189,15 +194,18 @@ export default function AppLayout() {
|
|||
gap: '12px',
|
||||
borderBottom: '1px solid #f0f0f0'
|
||||
}}>
|
||||
<img src="/logo.svg" alt="logo" style={{ width: 32, height: 32 }} />
|
||||
<img src={platformConfig?.logoUrl || "/logo.svg"} alt="logo" style={{ width: 32, height: 32, objectFit: 'contain' }} />
|
||||
{!collapsed && (
|
||||
<span style={{
|
||||
fontSize: '18px',
|
||||
fontWeight: 700,
|
||||
color: '#1677ff',
|
||||
letterSpacing: '0.5px'
|
||||
letterSpacing: '0.5px',
|
||||
overflow: 'hidden',
|
||||
textOverflow: 'ellipsis',
|
||||
whiteSpace: 'nowrap'
|
||||
}}>
|
||||
MeetingAI
|
||||
{platformConfig?.projectName || "MeetingAI"}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -2,8 +2,9 @@ import { Button, Checkbox, Form, Input, message, Typography, Space } from "antd"
|
|||
import { useEffect, useState, useCallback } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { fetchCaptcha, login, type CaptchaResponse } from "../api/auth";
|
||||
import { getCurrentUser, getSystemParamValue } from "../api";
|
||||
import { getCurrentUser, getSystemParamValue, getOpenPlatformConfig } from "../api";
|
||||
import { UserOutlined, LockOutlined, SafetyOutlined, ReloadOutlined, ShopOutlined } from "@ant-design/icons";
|
||||
import type { SysPlatformConfig } from "../types";
|
||||
import "./Login.css";
|
||||
|
||||
const { Title, Text, Link } = Typography;
|
||||
|
|
@ -13,6 +14,7 @@ export default function Login() {
|
|||
const [captcha, setCaptcha] = useState<CaptchaResponse | null>(null);
|
||||
const [captchaEnabled, setCaptchaEnabled] = useState(true);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [platformConfig, setPlatformConfig] = useState<SysPlatformConfig | null>(null);
|
||||
const [form] = Form.useForm();
|
||||
|
||||
const loadCaptcha = useCallback(async () => {
|
||||
|
|
@ -30,8 +32,13 @@ export default function Login() {
|
|||
useEffect(() => {
|
||||
const init = async () => {
|
||||
try {
|
||||
const value = await getSystemParamValue("security.captcha.enabled", "true");
|
||||
const enabled = value !== "false";
|
||||
const [captchaVal, pConfig] = await Promise.all([
|
||||
getSystemParamValue("security.captcha.enabled", "true"),
|
||||
getOpenPlatformConfig()
|
||||
]);
|
||||
|
||||
setPlatformConfig(pConfig);
|
||||
const enabled = captchaVal !== "false";
|
||||
setCaptchaEnabled(enabled);
|
||||
if (enabled) {
|
||||
loadCaptcha();
|
||||
|
|
@ -93,12 +100,31 @@ export default function Login() {
|
|||
}
|
||||
};
|
||||
|
||||
const loginStyle = platformConfig?.loginBgUrl ? {
|
||||
backgroundImage: `url(${platformConfig.loginBgUrl})`,
|
||||
backgroundSize: 'cover',
|
||||
backgroundPosition: 'center',
|
||||
position: 'relative' as const
|
||||
} : {};
|
||||
|
||||
// 如果设置了背景图,左侧和右侧的背景应该透明或者半透明
|
||||
const leftStyle = platformConfig?.loginBgUrl ? {
|
||||
...loginStyle,
|
||||
background: 'rgba(255, 255, 255, 0.2)',
|
||||
backdropFilter: 'blur(10px)',
|
||||
} : {};
|
||||
|
||||
const rightStyle = platformConfig?.loginBgUrl ? {
|
||||
background: 'rgba(255, 255, 255, 0.85)',
|
||||
backdropFilter: 'blur(20px)',
|
||||
} : {};
|
||||
|
||||
return (
|
||||
<div className="login-page">
|
||||
<div className="login-left">
|
||||
<div className="login-page" style={loginStyle}>
|
||||
<div className="login-left" style={leftStyle}>
|
||||
<div className="login-brand">
|
||||
<img src="/logo.svg" alt="MeetingAI Logo" className="brand-logo-img" />
|
||||
<span className="brand-name">MeetingAI</span>
|
||||
<img src={platformConfig?.logoUrl || "/logo.svg"} alt="Logo" className="brand-logo-img" />
|
||||
<span className="brand-name">{platformConfig?.projectName || "MeetingAI"}</span>
|
||||
</div>
|
||||
|
||||
<div className="login-hero">
|
||||
|
|
@ -108,7 +134,7 @@ export default function Login() {
|
|||
{t('login.heroTitle3')}
|
||||
</h1>
|
||||
<p className="hero-desc">
|
||||
{t('login.heroDesc')}
|
||||
{platformConfig?.systemDescription || t('login.heroDesc')}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
|
|
@ -116,10 +142,16 @@ export default function Login() {
|
|||
<div className="footer-item">{t('login.enterpriseSecurity')}</div>
|
||||
<div className="footer-divider" aria-hidden="true" />
|
||||
<div className="footer-item">{t('login.multiLang')}</div>
|
||||
{platformConfig?.icpInfo && (
|
||||
<>
|
||||
<div className="footer-divider" aria-hidden="true" />
|
||||
<div className="footer-item">{platformConfig.icpInfo}</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="login-right">
|
||||
<div className="login-right" style={rightStyle}>
|
||||
<div className="login-container">
|
||||
<div className="login-header">
|
||||
<Title level={2}>{t('login.welcome')}</Title>
|
||||
|
|
|
|||
|
|
@ -204,13 +204,13 @@ export default function Orgs() {
|
|||
width: 180,
|
||||
render: (_: any, record: SysOrg) => (
|
||||
<Space>
|
||||
{can("sys_org:create") && (
|
||||
{can("sys:org:create") && (
|
||||
<Button type="link" size="small" onClick={() => openCreate(record.id)}>{t('orgs.addSub')}</Button>
|
||||
)}
|
||||
{can("sys_org:update") && (
|
||||
{can("sys:org:update") && (
|
||||
<Button type="text" size="small" icon={<EditOutlined aria-hidden="true" />} onClick={() => openEdit(record)} aria-label={t('common.edit')} />
|
||||
)}
|
||||
{can("sys_org:delete") && (
|
||||
{can("sys:org:delete") && (
|
||||
<Popconfirm title={`确定删除 "${record.orgName}" 吗?`} onConfirm={() => handleDelete(record.id)}>
|
||||
<Button type="text" size="small" danger icon={<DeleteOutlined aria-hidden="true" />} aria-label={t('common.delete')} />
|
||||
</Popconfirm>
|
||||
|
|
@ -227,7 +227,7 @@ export default function Orgs() {
|
|||
<Title level={4} className="mb-1">{t('orgs.title')}</Title>
|
||||
<Text type="secondary">{t('orgs.subtitle')}</Text>
|
||||
</div>
|
||||
{can("sys_org:create") && (
|
||||
{can("sys:org:create") && (
|
||||
<Button type="primary" icon={<PlusOutlined aria-hidden="true" />} onClick={() => openCreate()}>
|
||||
{t('orgs.createRoot')}
|
||||
</Button>
|
||||
|
|
|
|||
|
|
@ -247,7 +247,7 @@ export default function Permissions() {
|
|||
fixed: "right" as const,
|
||||
render: (_: any, record: SysPermission) => (
|
||||
<Space>
|
||||
{can("sys_permission:create") && record.permType === 'menu' && (
|
||||
{can("sys:permission:create") && record.permType === 'menu' && (
|
||||
<Tooltip title="添加子项">
|
||||
<Button
|
||||
type="text"
|
||||
|
|
@ -257,7 +257,7 @@ export default function Permissions() {
|
|||
/>
|
||||
</Tooltip>
|
||||
)}
|
||||
{can("sys_permission:update") && (
|
||||
{can("sys:permission:update") && (
|
||||
<Button
|
||||
type="text"
|
||||
size="small"
|
||||
|
|
@ -266,7 +266,7 @@ export default function Permissions() {
|
|||
aria-label={t('common.edit')}
|
||||
/>
|
||||
)}
|
||||
{can("sys_permission:delete") && (
|
||||
{can("sys:permission:delete") && (
|
||||
<Popconfirm title={`确定删除权限 "${record.name}" 吗?`} onConfirm={() => remove(record.permId)}>
|
||||
<Button
|
||||
type="text"
|
||||
|
|
@ -289,7 +289,7 @@ export default function Permissions() {
|
|||
<Title level={4} className="mb-1">{t('permissions.title')}</Title>
|
||||
<Text type="secondary">{t('permissions.subtitle')}</Text>
|
||||
</div>
|
||||
{can("sys_permission:create") && (
|
||||
{can("sys:permission:create") && (
|
||||
<Button
|
||||
type="primary"
|
||||
icon={<PlusOutlined aria-hidden="true" />}
|
||||
|
|
@ -452,7 +452,7 @@ export default function Permissions() {
|
|||
})
|
||||
]}
|
||||
>
|
||||
<Input placeholder="例如:sys_user:export…" className="tabular-nums" />
|
||||
<Input placeholder="例如:sys:user:export…" className="tabular-nums" />
|
||||
</Form.Item>
|
||||
|
||||
<Row gutter={16}>
|
||||
|
|
|
|||
|
|
@ -34,7 +34,7 @@ import {
|
|||
unbindUserFromRole,
|
||||
listUsers
|
||||
} from "../api";
|
||||
import type { SysPermission, SysRole, SysUser } from "../types";
|
||||
import type {SysPermission, SysRole, SysTenant, SysUser} from "../types";
|
||||
import { usePermission } from "../hooks/usePermission";
|
||||
import {
|
||||
EditOutlined,
|
||||
|
|
@ -370,7 +370,7 @@ export default function Roles() {
|
|||
<Card
|
||||
title={t('roles.title')}
|
||||
className="full-height-card shadow-sm"
|
||||
extra={can("sys_role:create") && (
|
||||
extra={can("sys:role:create") && (
|
||||
<Button
|
||||
type="primary"
|
||||
size="small"
|
||||
|
|
@ -423,7 +423,7 @@ export default function Roles() {
|
|||
<div className="role-item-code text-xs text-gray-400 truncate">{record.roleCode}</div>
|
||||
</div>
|
||||
<div className="role-item-actions flex gap-1">
|
||||
{can("sys_role:update") && (
|
||||
{can("sys:role:update") && (
|
||||
<Button
|
||||
type="text"
|
||||
size="small"
|
||||
|
|
@ -431,7 +431,7 @@ export default function Roles() {
|
|||
onClick={e => openEditBasic(e, record)}
|
||||
/>
|
||||
)}
|
||||
{can("sys_role:delete") && record.roleCode !== 'ADMIN' && (
|
||||
{can("sys:role:delete") && record.roleCode !== 'ADMIN' && (
|
||||
<Popconfirm
|
||||
title={`确定删除角色 "${record.roleName}" 吗?`}
|
||||
onConfirm={e => handleRemove(e!, record.roleId)}
|
||||
|
|
@ -473,7 +473,7 @@ export default function Roles() {
|
|||
icon={<SaveOutlined aria-hidden="true" />}
|
||||
loading={saving}
|
||||
onClick={savePermissions}
|
||||
disabled={!can("sys_role:permission:save")}
|
||||
disabled={!can("sys:role:permission:save")}
|
||||
>
|
||||
{t('roles.savePerms')}
|
||||
</Button>
|
||||
|
|
@ -510,7 +510,7 @@ export default function Roles() {
|
|||
type="primary"
|
||||
icon={<UserAddOutlined aria-hidden="true" />}
|
||||
onClick={openUserModal}
|
||||
disabled={!can("sys_role:update")}
|
||||
disabled={!can("sys:role:update")}
|
||||
>
|
||||
{t('common.create')}
|
||||
</Button>
|
||||
|
|
@ -550,14 +550,14 @@ export default function Roles() {
|
|||
<Popconfirm
|
||||
title={t('common.delete') + "?"}
|
||||
onConfirm={() => handleUnbindUser(record.userId)}
|
||||
disabled={!can("sys_role:update")}
|
||||
disabled={!can("sys:role:update")}
|
||||
>
|
||||
<Button
|
||||
type="text"
|
||||
danger
|
||||
size="small"
|
||||
icon={<DeleteOutlined aria-hidden="true" />}
|
||||
disabled={!can("sys_role:update")}
|
||||
disabled={!can("sys:role:update")}
|
||||
/>
|
||||
</Popconfirm>
|
||||
)
|
||||
|
|
|
|||
|
|
@ -134,12 +134,16 @@ export default function Users() {
|
|||
|
||||
const loadBaseData = async () => {
|
||||
try {
|
||||
const [rolesList, tenantsResp] = await Promise.all([
|
||||
listRoles(),
|
||||
listTenants({ current: 1, size: 1000 })
|
||||
]);
|
||||
const promises: Promise<any>[] = [listRoles()];
|
||||
if (isPlatformMode) {
|
||||
promises.push(listTenants({ current: 1, size: 1000 }));
|
||||
}
|
||||
|
||||
const [rolesList, tenantsResp] = await Promise.all(promises);
|
||||
setRoles(rolesList || []);
|
||||
if (isPlatformMode && tenantsResp) {
|
||||
setTenants(tenantsResp.records || []);
|
||||
}
|
||||
} catch (e) {
|
||||
message.error(t('common.error'));
|
||||
}
|
||||
|
|
@ -165,15 +169,16 @@ export default function Users() {
|
|||
|
||||
useEffect(() => {
|
||||
const fetchOrgs = async () => {
|
||||
if (selectedTenantId) {
|
||||
const list = await listOrgs(selectedTenantId);
|
||||
const targetId = isPlatformMode ? selectedTenantId : activeTenantId;
|
||||
if (targetId) {
|
||||
const list = await listOrgs(targetId);
|
||||
setOrgs(list || []);
|
||||
} else {
|
||||
setOrgs([]);
|
||||
}
|
||||
};
|
||||
fetchOrgs();
|
||||
}, [selectedTenantId]);
|
||||
}, [selectedTenantId, isPlatformMode, activeTenantId]);
|
||||
|
||||
const tenantMap = useMemo(() => {
|
||||
const map: Record<number, string> = {};
|
||||
|
|
@ -251,12 +256,13 @@ export default function Users() {
|
|||
phone: values.phone,
|
||||
status: values.status,
|
||||
isPlatformAdmin: values.isPlatformAdmin,
|
||||
memberships: values.memberships || []
|
||||
};
|
||||
|
||||
// If single tenant view, ensure current active tenant is included
|
||||
if (!isPlatformMode && userPayload.memberships!.length === 0) {
|
||||
// If single tenant view (not platform admin mode)
|
||||
if (!isPlatformMode) {
|
||||
userPayload.memberships = [{ tenantId: activeTenantId, orgId: values.orgId } as any];
|
||||
} else {
|
||||
userPayload.memberships = values.memberships || [];
|
||||
}
|
||||
|
||||
if (values.password) {
|
||||
|
|
@ -306,43 +312,62 @@ export default function Users() {
|
|||
</Space>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: t('users.org'),
|
||||
key: "org",
|
||||
...(isPlatformMode ? [{
|
||||
title: t('users.tenant'),
|
||||
key: "tenant",
|
||||
render: (_: any, record: SysUser) => {
|
||||
// Platform mode: show all tenants as tags
|
||||
if (isPlatformMode && record.memberships && record.memberships.length > 0) {
|
||||
if (record.memberships && record.memberships.length > 0) {
|
||||
return (
|
||||
<div className="flex flex-col gap-1">
|
||||
{record.memberships.slice(0, 3).map(m => (
|
||||
{record.memberships.slice(0, 2).map(m => (
|
||||
<Tag key={m.tenantId} color="blue" style={{ margin: 0, padding: '0 4px', fontSize: 11 }}>
|
||||
{tenantMap[m.tenantId] || `租户${m.tenantId}`}
|
||||
</Tag>
|
||||
))}
|
||||
{record.memberships.length > 3 && <Text type="secondary" style={{ fontSize: 11 }}>等 {record.memberships.length} 个</Text>}
|
||||
{record.memberships.length > 2 && <Text type="secondary" style={{ fontSize: 11 }}>等 {record.memberships.length} 个</Text>}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
return <Text type="secondary">无租户</Text>;
|
||||
}
|
||||
}] : []),
|
||||
{
|
||||
title: t('users.orgNode'),
|
||||
key: "org",
|
||||
render: (_: any, record: SysUser) => {
|
||||
if (record.memberships && record.memberships.length > 0) {
|
||||
const orgNames = record.memberships
|
||||
.map(m => m.orgName)
|
||||
.filter(Boolean);
|
||||
|
||||
// Single tenant mode or fallback: show specific tenant and org
|
||||
const tid = record.tenantId || record.memberships?.[0]?.tenantId;
|
||||
const oid = record.orgId || record.memberships?.[0]?.orgId;
|
||||
|
||||
if (orgNames.length > 0) {
|
||||
return (
|
||||
<div className="flex flex-col gap-1">
|
||||
<Space size={4} style={{ fontSize: 13 }}>
|
||||
<ShopOutlined style={{ color: '#8c8c8c' }} />
|
||||
<span>{tenantMap[tid || 0] || "未知租户"}</span>
|
||||
</Space>
|
||||
{oid && (
|
||||
<Space size={4} style={{ fontSize: 12, color: '#8c8c8c' }}>
|
||||
{orgNames.map((name, idx) => (
|
||||
<Space key={idx} size={4} style={{ fontSize: 13, color: '#555' }}>
|
||||
<ApartmentOutlined />
|
||||
<span>关联组织已设置</span>
|
||||
<span>{name}</span>
|
||||
</Space>
|
||||
)}
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
return <Text type="secondary">-</Text>;
|
||||
}
|
||||
},
|
||||
{
|
||||
title: t('users.roles'),
|
||||
key: "roles",
|
||||
render: (_: any, record: SysUser) => (
|
||||
<Space wrap size={[0, 4]}>
|
||||
{record.roles && record.roles.length > 0 ? (
|
||||
record.roles.map(r => (
|
||||
<Tag key={r.roleId} color="cyan">{r.roleName}</Tag>
|
||||
))
|
||||
) : <Text type="secondary">无角色</Text>}
|
||||
</Space>
|
||||
)
|
||||
},
|
||||
{
|
||||
title: t('common.status'),
|
||||
|
|
@ -361,7 +386,7 @@ export default function Users() {
|
|||
fixed: "right" as const,
|
||||
render: (_: any, record: SysUser) => (
|
||||
<Space>
|
||||
{can("sys_user:update") && (
|
||||
{can("sys:user:update") && (
|
||||
<Button
|
||||
type="text"
|
||||
icon={<EditOutlined aria-hidden="true" />}
|
||||
|
|
@ -369,7 +394,7 @@ export default function Users() {
|
|||
aria-label={t('common.edit')}
|
||||
/>
|
||||
)}
|
||||
{can("sys_user:delete") && record.userId !== 1 && (
|
||||
{can("sys:user:delete") && record.userId !== 1 && (
|
||||
<Popconfirm title="确定注销该用户吗?" onConfirm={() => handleDelete(record.userId)}>
|
||||
<Button type="text" danger icon={<DeleteOutlined aria-hidden="true" />} aria-label={t('common.delete')} />
|
||||
</Popconfirm>
|
||||
|
|
@ -386,7 +411,7 @@ export default function Users() {
|
|||
<Title level={4} className="mb-1">{t('users.title')}</Title>
|
||||
<Text type="secondary">{t('users.subtitle')}</Text>
|
||||
</div>
|
||||
{can("sys_user:create") && (
|
||||
{can("sys:user:create") && (
|
||||
<Button type="primary" icon={<PlusOutlined aria-hidden="true" />} onClick={openCreate}>
|
||||
{t('users.drawerTitleCreate')}
|
||||
</Button>
|
||||
|
|
@ -498,19 +523,33 @@ export default function Users() {
|
|||
/>
|
||||
</Form.Item>
|
||||
|
||||
{!isPlatformMode && (
|
||||
<Form.Item label={t('users.orgNode')} name="orgId">
|
||||
<TreeSelect
|
||||
placeholder="选择所属组织/部门"
|
||||
allowClear
|
||||
treeData={orgTreeData}
|
||||
/>
|
||||
</Form.Item>
|
||||
)}
|
||||
|
||||
<Row gutter={16}>
|
||||
<Col span={12}>
|
||||
<Form.Item label={t('common.status')} name="status" initialValue={1}>
|
||||
<Select options={[{ label: "启用", value: 1 }, { label: "禁用", value: 0 }]} />
|
||||
</Form.Item>
|
||||
</Col>
|
||||
{isPlatformMode && (
|
||||
<Col span={12}>
|
||||
<Form.Item label={t('users.platformAdmin')} name="isPlatformAdmin" valuePropName="checked">
|
||||
<Switch />
|
||||
</Form.Item>
|
||||
</Col>
|
||||
)}
|
||||
</Row>
|
||||
|
||||
{isPlatformMode && (
|
||||
<>
|
||||
<Title level={5} style={{ marginTop: 24, marginBottom: 16 }}>租户成员身份</Title>
|
||||
|
||||
<Form.List name="memberships">
|
||||
|
|
@ -560,6 +599,8 @@ export default function Users() {
|
|||
</>
|
||||
)}
|
||||
</Form.List>
|
||||
</>
|
||||
)}
|
||||
</Form>
|
||||
</Drawer>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -9,6 +9,8 @@ import Tenants from "../pages/Tenants";
|
|||
import Orgs from "../pages/Orgs";
|
||||
import UserRoleBinding from "../pages/UserRoleBinding";
|
||||
import RolePermissionBinding from "../pages/RolePermissionBinding";
|
||||
import SysParams from "../pages/SysParams";
|
||||
import PlatformSettings from "../pages/PlatformSettings";
|
||||
|
||||
import type { MenuRoute } from "../types";
|
||||
|
||||
|
|
@ -19,6 +21,8 @@ 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: "/params", label: "系统参数", element: <SysParams />, perm: "menu:params" },
|
||||
{ path: "/platform-settings", label: "平台设置", element: <PlatformSettings />, perm: "menu:platform" },
|
||||
{ path: "/dictionaries", label: "字典管理", element: <Dictionaries />, perm: "menu:dict" },
|
||||
{ path: "/logs", label: "日志管理", element: <Logs />, perm: "menu:logs" },
|
||||
{ path: "/devices", label: "设备管理", element: <Devices />, perm: "menu:devices" },
|
||||
|
|
|
|||
|
|
@ -16,6 +16,8 @@ export interface SysUser extends BaseEntity {
|
|||
tenantId: number;
|
||||
orgId?: number;
|
||||
isPlatformAdmin?: boolean;
|
||||
memberships?: any[];
|
||||
roles?: SysRole[];
|
||||
}
|
||||
|
||||
export interface UserProfile {
|
||||
|
|
@ -120,6 +122,38 @@ export interface OrgNode extends SysOrg {
|
|||
children: OrgNode[];
|
||||
}
|
||||
|
||||
export interface SysParamVO extends BaseEntity {
|
||||
paramId: number;
|
||||
paramKey: string;
|
||||
paramValue: string;
|
||||
paramType: string;
|
||||
isSystem: number;
|
||||
description?: string;
|
||||
}
|
||||
|
||||
export interface SysPlatformConfig {
|
||||
projectName: string;
|
||||
logoUrl?: string;
|
||||
iconUrl?: string;
|
||||
loginBgUrl?: string;
|
||||
icpInfo?: string;
|
||||
copyrightInfo?: string;
|
||||
systemDescription?: string;
|
||||
}
|
||||
|
||||
export interface SysParamQuery {
|
||||
paramKey?: string;
|
||||
paramType?: string;
|
||||
description?: string;
|
||||
pageNum?: number;
|
||||
pageSize?: number;
|
||||
}
|
||||
|
||||
export interface PageResult<T> {
|
||||
total: number;
|
||||
records: T;
|
||||
}
|
||||
|
||||
import type { ReactNode } from "react";
|
||||
|
||||
export interface MenuRoute {
|
||||
|
|
|
|||
Loading…
Reference in New Issue