Commit fe5a6531cf4e2680051a1301a729a226a9436d0a
1 parent
f1e20216
feat(user): wire user detail + permission categories endpoints to UserDetail page
- Backend: add GET /usr/users/{id} detail endpoint that returns the user row plus its permissionCategoryIds
- Backend: add GET /usr/permission-categories listing for the permission grid (active categories only)
- Frontend: UserDetail consumes both endpoints to populate edit form and the permission grid
Showing
11 changed files
with
212 additions
and
53 deletions
backend/src/main/java/com/xly/erp/module/usr/controller/UserController.java
| @@ -4,6 +4,8 @@ import com.xly.erp.common.response.Result; | @@ -4,6 +4,8 @@ import com.xly.erp.common.response.Result; | ||
| 4 | import com.xly.erp.module.usr.dto.CreateUserDTO; | 4 | import com.xly.erp.module.usr.dto.CreateUserDTO; |
| 5 | import com.xly.erp.module.usr.dto.UpdateUserDTO; | 5 | import com.xly.erp.module.usr.dto.UpdateUserDTO; |
| 6 | import com.xly.erp.module.usr.service.UserService; | 6 | import com.xly.erp.module.usr.service.UserService; |
| 7 | +import com.xly.erp.module.usr.vo.PermissionCategoryVO; | ||
| 8 | +import com.xly.erp.module.usr.vo.UserListVO; | ||
| 7 | import jakarta.validation.Valid; | 9 | import jakarta.validation.Valid; |
| 8 | import org.springframework.web.bind.annotation.GetMapping; | 10 | import org.springframework.web.bind.annotation.GetMapping; |
| 9 | import org.springframework.web.bind.annotation.PathVariable; | 11 | import org.springframework.web.bind.annotation.PathVariable; |
| @@ -14,6 +16,7 @@ import org.springframework.web.bind.annotation.RequestMapping; | @@ -14,6 +16,7 @@ import org.springframework.web.bind.annotation.RequestMapping; | ||
| 14 | import org.springframework.web.bind.annotation.RequestParam; | 16 | import org.springframework.web.bind.annotation.RequestParam; |
| 15 | import org.springframework.web.bind.annotation.RestController; | 17 | import org.springframework.web.bind.annotation.RestController; |
| 16 | 18 | ||
| 19 | +import java.util.List; | ||
| 17 | import java.util.Map; | 20 | import java.util.Map; |
| 18 | 21 | ||
| 19 | @RestController | 22 | @RestController |
| @@ -46,4 +49,14 @@ public class UserController { | @@ -46,4 +49,14 @@ public class UserController { | ||
| 46 | @RequestParam(required = false) Integer pageSize) { | 49 | @RequestParam(required = false) Integer pageSize) { |
| 47 | return Result.ok(userService.list(field, match, value, pageNum, pageSize)); | 50 | return Result.ok(userService.list(field, match, value, pageNum, pageSize)); |
| 48 | } | 51 | } |
| 52 | + | ||
| 53 | + @GetMapping("/users/{id}") | ||
| 54 | + public Result<UserListVO> detail(@PathVariable Integer id) { | ||
| 55 | + return Result.ok(userService.detail(id)); | ||
| 56 | + } | ||
| 57 | + | ||
| 58 | + @GetMapping("/permission-categories") | ||
| 59 | + public Result<List<PermissionCategoryVO>> permissionCategories() { | ||
| 60 | + return Result.ok(userService.listPermissionCategories()); | ||
| 61 | + } | ||
| 49 | } | 62 | } |
backend/src/main/java/com/xly/erp/module/usr/mapper/PermissionCategoryMapper.java
| 1 | package com.xly.erp.module.usr.mapper; | 1 | package com.xly.erp.module.usr.mapper; |
| 2 | 2 | ||
| 3 | +import com.xly.erp.module.usr.vo.PermissionCategoryVO; | ||
| 3 | import org.apache.ibatis.annotations.Mapper; | 4 | import org.apache.ibatis.annotations.Mapper; |
| 4 | import org.apache.ibatis.annotations.Param; | 5 | import org.apache.ibatis.annotations.Param; |
| 5 | import org.apache.ibatis.annotations.Select; | 6 | import org.apache.ibatis.annotations.Select; |
| @@ -15,4 +16,10 @@ public interface PermissionCategoryMapper { | @@ -15,4 +16,10 @@ public interface PermissionCategoryMapper { | ||
| 15 | + "<foreach collection='ids' item='id' open='(' separator=',' close=')'>#{id}</foreach>" | 16 | + "<foreach collection='ids' item='id' open='(' separator=',' close=')'>#{id}</foreach>" |
| 16 | + "</script>") | 17 | + "</script>") |
| 17 | int countActiveByIds(@Param("ids") List<Integer> ids); | 18 | int countActiveByIds(@Param("ids") List<Integer> ids); |
| 19 | + | ||
| 20 | + @Select("SELECT iIncrement, sCategoryCode, sCategoryName, iParentId, iSortOrder " | ||
| 21 | + + "FROM tPermissionCategory " | ||
| 22 | + + "WHERE bDeleted = 0 " | ||
| 23 | + + "ORDER BY iSortOrder ASC, iIncrement ASC") | ||
| 24 | + List<PermissionCategoryVO> listActive(); | ||
| 18 | } | 25 | } |
backend/src/main/java/com/xly/erp/module/usr/mapper/UserMapper.java
| @@ -31,4 +31,6 @@ public interface UserMapper extends BaseMapper<User> { | @@ -31,4 +31,6 @@ public interface UserMapper extends BaseMapper<User> { | ||
| 31 | long countWithFilter(@Param("field") String field, | 31 | long countWithFilter(@Param("field") String field, |
| 32 | @Param("matchOp") String matchOp, | 32 | @Param("matchOp") String matchOp, |
| 33 | @Param("value") Object value); | 33 | @Param("value") Object value); |
| 34 | + | ||
| 35 | + UserListVO selectDetailById(@Param("id") Integer id); | ||
| 34 | } | 36 | } |
backend/src/main/java/com/xly/erp/module/usr/mapper/UserPermissionMapper.java
| @@ -4,9 +4,15 @@ import com.baomidou.mybatisplus.core.mapper.BaseMapper; | @@ -4,9 +4,15 @@ import com.baomidou.mybatisplus.core.mapper.BaseMapper; | ||
| 4 | import com.xly.erp.module.usr.entity.UserPermission; | 4 | import com.xly.erp.module.usr.entity.UserPermission; |
| 5 | import org.apache.ibatis.annotations.Delete; | 5 | import org.apache.ibatis.annotations.Delete; |
| 6 | import org.apache.ibatis.annotations.Param; | 6 | import org.apache.ibatis.annotations.Param; |
| 7 | +import org.apache.ibatis.annotations.Select; | ||
| 8 | + | ||
| 9 | +import java.util.List; | ||
| 7 | 10 | ||
| 8 | public interface UserPermissionMapper extends BaseMapper<UserPermission> { | 11 | public interface UserPermissionMapper extends BaseMapper<UserPermission> { |
| 9 | 12 | ||
| 10 | @Delete("DELETE FROM tUserPermission WHERE iUserId = #{userId}") | 13 | @Delete("DELETE FROM tUserPermission WHERE iUserId = #{userId}") |
| 11 | int deleteByUserId(@Param("userId") Integer userId); | 14 | int deleteByUserId(@Param("userId") Integer userId); |
| 15 | + | ||
| 16 | + @Select("SELECT iCategoryId FROM tUserPermission WHERE iUserId = #{userId} ORDER BY iIncrement ASC") | ||
| 17 | + List<Integer> selectCategoryIdsByUserId(@Param("userId") Integer userId); | ||
| 12 | } | 18 | } |
backend/src/main/java/com/xly/erp/module/usr/service/UserService.java
| @@ -4,7 +4,10 @@ import com.xly.erp.module.usr.dto.CreateUserDTO; | @@ -4,7 +4,10 @@ import com.xly.erp.module.usr.dto.CreateUserDTO; | ||
| 4 | import com.xly.erp.module.usr.dto.LoginDTO; | 4 | import com.xly.erp.module.usr.dto.LoginDTO; |
| 5 | import com.xly.erp.module.usr.dto.UpdateUserDTO; | 5 | import com.xly.erp.module.usr.dto.UpdateUserDTO; |
| 6 | import com.xly.erp.module.usr.vo.LoginVO; | 6 | import com.xly.erp.module.usr.vo.LoginVO; |
| 7 | +import com.xly.erp.module.usr.vo.PermissionCategoryVO; | ||
| 8 | +import com.xly.erp.module.usr.vo.UserListVO; | ||
| 7 | 9 | ||
| 10 | +import java.util.List; | ||
| 8 | import java.util.Map; | 11 | import java.util.Map; |
| 9 | 12 | ||
| 10 | public interface UserService { | 13 | public interface UserService { |
| @@ -14,5 +17,9 @@ public interface UserService { | @@ -14,5 +17,9 @@ public interface UserService { | ||
| 14 | 17 | ||
| 15 | Map<String, Object> list(String field, String match, String value, Integer pageNum, Integer pageSize); | 18 | Map<String, Object> list(String field, String match, String value, Integer pageNum, Integer pageSize); |
| 16 | 19 | ||
| 20 | + UserListVO detail(Integer id); | ||
| 21 | + | ||
| 22 | + List<PermissionCategoryVO> listPermissionCategories(); | ||
| 23 | + | ||
| 17 | LoginVO login(LoginDTO dto); | 24 | LoginVO login(LoginDTO dto); |
| 18 | } | 25 | } |
backend/src/main/java/com/xly/erp/module/usr/service/impl/UserServiceImpl.java
| @@ -11,6 +11,7 @@ import com.xly.erp.module.usr.dto.UpdateUserDTO; | @@ -11,6 +11,7 @@ import com.xly.erp.module.usr.dto.UpdateUserDTO; | ||
| 11 | import com.xly.erp.module.usr.entity.User; | 11 | import com.xly.erp.module.usr.entity.User; |
| 12 | import com.xly.erp.module.usr.security.LoginAttemptStore; | 12 | import com.xly.erp.module.usr.security.LoginAttemptStore; |
| 13 | import com.xly.erp.module.usr.vo.LoginVO; | 13 | import com.xly.erp.module.usr.vo.LoginVO; |
| 14 | +import com.xly.erp.module.usr.vo.PermissionCategoryVO; | ||
| 14 | import com.xly.erp.module.usr.vo.UserBriefVO; | 15 | import com.xly.erp.module.usr.vo.UserBriefVO; |
| 15 | import com.xly.erp.module.usr.vo.UserListVO; | 16 | import com.xly.erp.module.usr.vo.UserListVO; |
| 16 | import com.xly.erp.module.usr.entity.UserPermission; | 17 | import com.xly.erp.module.usr.entity.UserPermission; |
| @@ -259,6 +260,23 @@ public class UserServiceImpl implements UserService { | @@ -259,6 +260,23 @@ public class UserServiceImpl implements UserService { | ||
| 259 | return result; | 260 | return result; |
| 260 | } | 261 | } |
| 261 | 262 | ||
| 263 | + @Override | ||
| 264 | + @Transactional(readOnly = true) | ||
| 265 | + public UserListVO detail(Integer id) { | ||
| 266 | + UserListVO vo = userMapper.selectDetailById(id); | ||
| 267 | + if (vo == null || Boolean.TRUE.equals(vo.getBDeleted())) { | ||
| 268 | + throw new BizException(40400, "用户不存在或已删除"); | ||
| 269 | + } | ||
| 270 | + vo.setPermissionCategoryIds(userPermissionMapper.selectCategoryIdsByUserId(id)); | ||
| 271 | + return vo; | ||
| 272 | + } | ||
| 273 | + | ||
| 274 | + @Override | ||
| 275 | + @Transactional(readOnly = true) | ||
| 276 | + public List<PermissionCategoryVO> listPermissionCategories() { | ||
| 277 | + return permissionCategoryMapper.listActive(); | ||
| 278 | + } | ||
| 279 | + | ||
| 262 | private Integer parseBoolean(String v) { | 280 | private Integer parseBoolean(String v) { |
| 263 | return switch (v.toLowerCase()) { | 281 | return switch (v.toLowerCase()) { |
| 264 | case "true", "1" -> 1; | 282 | case "true", "1" -> 1; |
backend/src/main/java/com/xly/erp/module/usr/vo/PermissionCategoryVO.java
0 → 100644
| 1 | +package com.xly.erp.module.usr.vo; | ||
| 2 | + | ||
| 3 | +import com.fasterxml.jackson.annotation.JsonProperty; | ||
| 4 | + | ||
| 5 | +public class PermissionCategoryVO { | ||
| 6 | + | ||
| 7 | + @JsonProperty("iIncrement") | ||
| 8 | + private Integer iIncrement; | ||
| 9 | + | ||
| 10 | + @JsonProperty("sCategoryCode") | ||
| 11 | + private String sCategoryCode; | ||
| 12 | + | ||
| 13 | + @JsonProperty("sCategoryName") | ||
| 14 | + private String sCategoryName; | ||
| 15 | + | ||
| 16 | + @JsonProperty("iParentId") | ||
| 17 | + private Integer iParentId; | ||
| 18 | + | ||
| 19 | + @JsonProperty("iSortOrder") | ||
| 20 | + private Integer iSortOrder; | ||
| 21 | + | ||
| 22 | + public Integer getIIncrement() { return iIncrement; } | ||
| 23 | + public void setIIncrement(Integer iIncrement) { this.iIncrement = iIncrement; } | ||
| 24 | + public String getSCategoryCode() { return sCategoryCode; } | ||
| 25 | + public void setSCategoryCode(String sCategoryCode) { this.sCategoryCode = sCategoryCode; } | ||
| 26 | + public String getSCategoryName() { return sCategoryName; } | ||
| 27 | + public void setSCategoryName(String sCategoryName) { this.sCategoryName = sCategoryName; } | ||
| 28 | + public Integer getIParentId() { return iParentId; } | ||
| 29 | + public void setIParentId(Integer iParentId) { this.iParentId = iParentId; } | ||
| 30 | + public Integer getISortOrder() { return iSortOrder; } | ||
| 31 | + public void setISortOrder(Integer iSortOrder) { this.iSortOrder = iSortOrder; } | ||
| 32 | +} |
backend/src/main/java/com/xly/erp/module/usr/vo/UserListVO.java
| @@ -3,6 +3,7 @@ package com.xly.erp.module.usr.vo; | @@ -3,6 +3,7 @@ package com.xly.erp.module.usr.vo; | ||
| 3 | import com.fasterxml.jackson.annotation.JsonProperty; | 3 | import com.fasterxml.jackson.annotation.JsonProperty; |
| 4 | 4 | ||
| 5 | import java.time.LocalDateTime; | 5 | import java.time.LocalDateTime; |
| 6 | +import java.util.List; | ||
| 6 | 7 | ||
| 7 | public class UserListVO { | 8 | public class UserListVO { |
| 8 | 9 | ||
| @@ -30,6 +31,12 @@ public class UserListVO { | @@ -30,6 +31,12 @@ public class UserListVO { | ||
| 30 | @JsonProperty("sLanguage") | 31 | @JsonProperty("sLanguage") |
| 31 | private String sLanguage; | 32 | private String sLanguage; |
| 32 | 33 | ||
| 34 | + @JsonProperty("bCanModifyDocs") | ||
| 35 | + private Boolean bCanModifyDocs; | ||
| 36 | + | ||
| 37 | + @JsonProperty("permissionCategoryIds") | ||
| 38 | + private List<Integer> permissionCategoryIds; | ||
| 39 | + | ||
| 33 | @JsonProperty("bDeleted") | 40 | @JsonProperty("bDeleted") |
| 34 | private Boolean bDeleted; | 41 | private Boolean bDeleted; |
| 35 | 42 | ||
| @@ -58,6 +65,10 @@ public class UserListVO { | @@ -58,6 +65,10 @@ public class UserListVO { | ||
| 58 | public void setSUserType(String sUserType) { this.sUserType = sUserType; } | 65 | public void setSUserType(String sUserType) { this.sUserType = sUserType; } |
| 59 | public String getSLanguage() { return sLanguage; } | 66 | public String getSLanguage() { return sLanguage; } |
| 60 | public void setSLanguage(String sLanguage) { this.sLanguage = sLanguage; } | 67 | public void setSLanguage(String sLanguage) { this.sLanguage = sLanguage; } |
| 68 | + public Boolean getBCanModifyDocs() { return bCanModifyDocs; } | ||
| 69 | + public void setBCanModifyDocs(Boolean bCanModifyDocs) { this.bCanModifyDocs = bCanModifyDocs; } | ||
| 70 | + public List<Integer> getPermissionCategoryIds() { return permissionCategoryIds; } | ||
| 71 | + public void setPermissionCategoryIds(List<Integer> permissionCategoryIds) { this.permissionCategoryIds = permissionCategoryIds; } | ||
| 61 | public Boolean getBDeleted() { return bDeleted; } | 72 | public Boolean getBDeleted() { return bDeleted; } |
| 62 | public void setBDeleted(Boolean bDeleted) { this.bDeleted = bDeleted; } | 73 | public void setBDeleted(Boolean bDeleted) { this.bDeleted = bDeleted; } |
| 63 | public LocalDateTime getTLastLoginDate() { return tLastLoginDate; } | 74 | public LocalDateTime getTLastLoginDate() { return tLastLoginDate; } |
backend/src/main/resources/mapper/usr/UserMapper.xml
| @@ -12,6 +12,7 @@ | @@ -12,6 +12,7 @@ | ||
| 12 | s.sDepartment AS department, | 12 | s.sDepartment AS department, |
| 13 | u.sUserType AS sUserType, | 13 | u.sUserType AS sUserType, |
| 14 | u.sLanguage AS sLanguage, | 14 | u.sLanguage AS sLanguage, |
| 15 | + u.bCanModifyDocs AS bCanModifyDocs, | ||
| 15 | u.bDeleted AS bDeleted, | 16 | u.bDeleted AS bDeleted, |
| 16 | u.tLastLoginDate AS tLastLoginDate, | 17 | u.tLastLoginDate AS tLastLoginDate, |
| 17 | u.sCreatedBy AS sCreatedBy, | 18 | u.sCreatedBy AS sCreatedBy, |
| @@ -46,4 +47,12 @@ | @@ -46,4 +47,12 @@ | ||
| 46 | <include refid="filterClause"/> | 47 | <include refid="filterClause"/> |
| 47 | </select> | 48 | </select> |
| 48 | 49 | ||
| 50 | + <select id="selectDetailById" resultType="com.xly.erp.module.usr.vo.UserListVO"> | ||
| 51 | + SELECT <include refid="baseSelectColumns"/> | ||
| 52 | + FROM tUser u | ||
| 53 | + LEFT JOIN tStaff s ON s.iIncrement = u.iStaffId AND s.bDeleted = 0 | ||
| 54 | + WHERE u.iIncrement = #{id} | ||
| 55 | + LIMIT 1 | ||
| 56 | + </select> | ||
| 57 | + | ||
| 49 | </mapper> | 58 | </mapper> |
frontend/src/api/user.ts
| @@ -9,12 +9,22 @@ export interface UserListVO { | @@ -9,12 +9,22 @@ export interface UserListVO { | ||
| 9 | department: string | null; | 9 | department: string | null; |
| 10 | sUserType: string; | 10 | sUserType: string; |
| 11 | sLanguage: string; | 11 | sLanguage: string; |
| 12 | + bCanModifyDocs?: boolean; | ||
| 13 | + permissionCategoryIds?: number[]; | ||
| 12 | bDeleted: boolean; | 14 | bDeleted: boolean; |
| 13 | tLastLoginDate: string | null; | 15 | tLastLoginDate: string | null; |
| 14 | sCreatedBy: string | null; | 16 | sCreatedBy: string | null; |
| 15 | tCreateDate: string; | 17 | tCreateDate: string; |
| 16 | } | 18 | } |
| 17 | 19 | ||
| 20 | +export interface PermissionCategoryVO { | ||
| 21 | + iIncrement: number; | ||
| 22 | + sCategoryCode: string; | ||
| 23 | + sCategoryName: string; | ||
| 24 | + iParentId: number | null; | ||
| 25 | + iSortOrder: number; | ||
| 26 | +} | ||
| 27 | + | ||
| 18 | export interface UserListPage { | 28 | export interface UserListPage { |
| 19 | records: UserListVO[]; | 29 | records: UserListVO[]; |
| 20 | total: number; | 30 | total: number; |
| @@ -37,6 +47,7 @@ export interface UserDTO { | @@ -37,6 +47,7 @@ export interface UserDTO { | ||
| 37 | sUserType: string; | 47 | sUserType: string; |
| 38 | sLanguage: string; | 48 | sLanguage: string; |
| 39 | bCanModifyDocs?: boolean; | 49 | bCanModifyDocs?: boolean; |
| 50 | + permissionCategoryIds?: number[]; | ||
| 40 | } | 51 | } |
| 41 | 52 | ||
| 42 | export function listUsers(params: UserListParams = {}): Promise<UserListPage> { | 53 | export function listUsers(params: UserListParams = {}): Promise<UserListPage> { |
| @@ -47,6 +58,17 @@ export function listUsers(params: UserListParams = {}): Promise<UserListPage> { | @@ -47,6 +58,17 @@ export function listUsers(params: UserListParams = {}): Promise<UserListPage> { | ||
| 47 | }); | 58 | }); |
| 48 | } | 59 | } |
| 49 | 60 | ||
| 61 | +export function getUser(id: number): Promise<UserListVO> { | ||
| 62 | + return request<UserListVO>({ url: `/usr/users/${id}`, method: "GET" }); | ||
| 63 | +} | ||
| 64 | + | ||
| 65 | +export function listPermissionCategories(): Promise<PermissionCategoryVO[]> { | ||
| 66 | + return request<PermissionCategoryVO[]>({ | ||
| 67 | + url: "/usr/permission-categories", | ||
| 68 | + method: "GET", | ||
| 69 | + }); | ||
| 70 | +} | ||
| 71 | + | ||
| 50 | export function createUser(dto: UserDTO): Promise<{ iIncrement: number; sUserNo: string }> { | 72 | export function createUser(dto: UserDTO): Promise<{ iIncrement: number; sUserNo: string }> { |
| 51 | return request({ url: "/usr/users", method: "POST", data: dto }); | 73 | return request({ url: "/usr/users", method: "POST", data: dto }); |
| 52 | } | 74 | } |
frontend/src/pages/usr/UserDetail.tsx
| @@ -20,11 +20,18 @@ import { | @@ -20,11 +20,18 @@ import { | ||
| 20 | ToolbarBtnDark, | 20 | ToolbarBtnDark, |
| 21 | } from "@/components/Primitives"; | 21 | } from "@/components/Primitives"; |
| 22 | import StaffPicker from "@/components/StaffPicker"; | 22 | import StaffPicker from "@/components/StaffPicker"; |
| 23 | -import { createUser, updateUser, type UserDTO } from "@/api/user"; | 23 | +import { |
| 24 | + createUser, | ||
| 25 | + getUser, | ||
| 26 | + listPermissionCategories, | ||
| 27 | + updateUser, | ||
| 28 | + type PermissionCategoryVO, | ||
| 29 | + type UserDTO, | ||
| 30 | + type UserListVO, | ||
| 31 | +} from "@/api/user"; | ||
| 24 | import { | 32 | import { |
| 25 | USER_TYPES, | 33 | USER_TYPES, |
| 26 | LANGUAGE_OPTIONS, | 34 | LANGUAGE_OPTIONS, |
| 27 | - PERMISSION_GROUPS, | ||
| 28 | SCOPE_ITEMS, | 35 | SCOPE_ITEMS, |
| 29 | } from "@/utils/data"; | 36 | } from "@/utils/data"; |
| 30 | import { useAppDispatch, useAppSelector } from "@/store"; | 37 | import { useAppDispatch, useAppSelector } from "@/store"; |
| @@ -65,22 +72,14 @@ export default function UserDetail({ userId, mode: initialMode }: Props) { | @@ -65,22 +72,14 @@ export default function UserDetail({ userId, mode: initialMode }: Props) { | ||
| 65 | const tabs = useAppSelector((s) => s.tabs.tabs); | 72 | const tabs = useAppSelector((s) => s.tabs.tabs); |
| 66 | const activeTabId = useAppSelector((s) => s.tabs.activeTabId); | 73 | const activeTabId = useAppSelector((s) => s.tabs.activeTabId); |
| 67 | const tab = tabs.find((t) => t.id === activeTabId); | 74 | const tab = tabs.find((t) => t.id === activeTabId); |
| 68 | - const snapshot = tab?.meta?.snapshot as | ||
| 69 | - | { | ||
| 70 | - sUserNo: string; | ||
| 71 | - sUserName: string; | ||
| 72 | - iStaffId: number | null; | ||
| 73 | - staffName: string | null; | ||
| 74 | - sUserType: string; | ||
| 75 | - sLanguage: string; | ||
| 76 | - bCanModifyDocs?: boolean; | ||
| 77 | - tCreateDate?: string; | ||
| 78 | - sCreatedBy?: string; | ||
| 79 | - } | ||
| 80 | - | undefined; | 75 | + const snapshot = tab?.meta?.snapshot as UserListVO | undefined; |
| 76 | + const [detail, setDetail] = useState<UserListVO | undefined>(snapshot); | ||
| 77 | + const currentSnapshot = detail ?? snapshot; | ||
| 78 | + const [permissionCategories, setPermissionCategories] = useState<PermissionCategoryVO[]>([]); | ||
| 79 | + const [permissionCategoryIds, setPermissionCategoryIds] = useState<number[]>([]); | ||
| 81 | 80 | ||
| 82 | - const initialForm: FormState = | ||
| 83 | - mode === "new" || !snapshot | 81 | + const buildForm = (snap?: UserListVO): FormState => |
| 82 | + mode === "new" || !snap | ||
| 84 | ? { | 83 | ? { |
| 85 | sUserNo: "", | 84 | sUserNo: "", |
| 86 | sUserName: "", | 85 | sUserName: "", |
| @@ -91,33 +90,54 @@ export default function UserDetail({ userId, mode: initialMode }: Props) { | @@ -91,33 +90,54 @@ export default function UserDetail({ userId, mode: initialMode }: Props) { | ||
| 91 | bCanModifyDocs: false, | 90 | bCanModifyDocs: false, |
| 92 | } | 91 | } |
| 93 | : { | 92 | : { |
| 94 | - sUserNo: snapshot.sUserNo, | ||
| 95 | - sUserName: snapshot.sUserName, | ||
| 96 | - iStaffId: snapshot.iStaffId, | ||
| 97 | - staffName: snapshot.staffName ?? "", | ||
| 98 | - sUserType: snapshot.sUserType, | ||
| 99 | - sLanguage: snapshot.sLanguage, | ||
| 100 | - bCanModifyDocs: !!snapshot.bCanModifyDocs, | 93 | + sUserNo: snap.sUserNo, |
| 94 | + sUserName: snap.sUserName, | ||
| 95 | + iStaffId: snap.iStaffId, | ||
| 96 | + staffName: snap.staffName ?? "", | ||
| 97 | + sUserType: snap.sUserType, | ||
| 98 | + sLanguage: snap.sLanguage, | ||
| 99 | + bCanModifyDocs: !!snap.bCanModifyDocs, | ||
| 101 | }; | 100 | }; |
| 102 | 101 | ||
| 103 | - const [form, setForm] = useState<FormState>(initialForm); | 102 | + const [form, setForm] = useState<FormState>(() => buildForm(currentSnapshot)); |
| 104 | const [permTab, setPermTab] = useState("groups"); | 103 | const [permTab, setPermTab] = useState("groups"); |
| 105 | - // Visual-only — not persisted to backend. | ||
| 106 | - const [permissions, setPermissions] = useState<Record<string, boolean>>(() => | ||
| 107 | - Object.fromEntries(PERMISSION_GROUPS.map((g) => [g, false])) | ||
| 108 | - ); | ||
| 109 | const [tabPerms, setTabPerms] = useState<Record<string, boolean>>({}); | 104 | const [tabPerms, setTabPerms] = useState<Record<string, boolean>>({}); |
| 110 | 105 | ||
| 111 | useEffect(() => { | 106 | useEffect(() => { |
| 112 | - setForm(initialForm); | 107 | + setDetail(snapshot); |
| 108 | + }, [snapshot]); | ||
| 109 | + | ||
| 110 | + useEffect(() => { | ||
| 111 | + void listPermissionCategories() | ||
| 112 | + .then(setPermissionCategories) | ||
| 113 | + .catch(() => { | ||
| 114 | + // interceptor already showed message | ||
| 115 | + }); | ||
| 116 | + }, []); | ||
| 117 | + | ||
| 118 | + useEffect(() => { | ||
| 119 | + if (mode === "new" || !userId) return; | ||
| 120 | + void getUser(userId) | ||
| 121 | + .then((u) => { | ||
| 122 | + setDetail(u); | ||
| 123 | + setPermissionCategoryIds(u.permissionCategoryIds ?? []); | ||
| 124 | + }) | ||
| 125 | + .catch(() => { | ||
| 126 | + // interceptor already showed message | ||
| 127 | + }); | ||
| 128 | + }, [mode, userId]); | ||
| 129 | + | ||
| 130 | + useEffect(() => { | ||
| 131 | + setForm(buildForm(currentSnapshot)); | ||
| 132 | + setPermissionCategoryIds(mode === "new" ? [] : currentSnapshot?.permissionCategoryIds ?? []); | ||
| 113 | // eslint-disable-next-line react-hooks/exhaustive-deps | 133 | // eslint-disable-next-line react-hooks/exhaustive-deps |
| 114 | - }, [snapshot, mode]); | 134 | + }, [currentSnapshot, mode]); |
| 115 | 135 | ||
| 116 | const set = <K extends keyof FormState>(k: K, v: FormState[K]) => | 136 | const set = <K extends keyof FormState>(k: K, v: FormState[K]) => |
| 117 | setForm((s) => ({ ...s, [k]: v })); | 137 | setForm((s) => ({ ...s, [k]: v })); |
| 118 | 138 | ||
| 119 | const disabled = mode === "view"; | 139 | const disabled = mode === "view"; |
| 120 | - const checkedCount = Object.values(permissions).filter(Boolean).length; | 140 | + const checkedCount = permissionCategoryIds.length; |
| 121 | 141 | ||
| 122 | const startEdit = () => setMode("edit"); | 142 | const startEdit = () => setMode("edit"); |
| 123 | const startNew = () => setMode("new"); | 143 | const startNew = () => setMode("new"); |
| @@ -126,7 +146,8 @@ export default function UserDetail({ userId, mode: initialMode }: Props) { | @@ -126,7 +146,8 @@ export default function UserDetail({ userId, mode: initialMode }: Props) { | ||
| 126 | dispatch(closeTab(activeTabId)); | 146 | dispatch(closeTab(activeTabId)); |
| 127 | dispatch(setActiveTab("userlist")); | 147 | dispatch(setActiveTab("userlist")); |
| 128 | } else { | 148 | } else { |
| 129 | - setForm(initialForm); | 149 | + setForm(buildForm(currentSnapshot)); |
| 150 | + setPermissionCategoryIds(currentSnapshot?.permissionCategoryIds ?? []); | ||
| 130 | setMode("view"); | 151 | setMode("view"); |
| 131 | } | 152 | } |
| 132 | }; | 153 | }; |
| @@ -143,7 +164,7 @@ export default function UserDetail({ userId, mode: initialMode }: Props) { | @@ -143,7 +164,7 @@ export default function UserDetail({ userId, mode: initialMode }: Props) { | ||
| 143 | sUserType: form.sUserType, | 164 | sUserType: form.sUserType, |
| 144 | sLanguage: form.sLanguage, | 165 | sLanguage: form.sLanguage, |
| 145 | bCanModifyDocs: form.bCanModifyDocs, | 166 | bCanModifyDocs: form.bCanModifyDocs, |
| 146 | - // permissionCategoryIds omitted: prototype permissions are by name; backend wants IDs. | 167 | + permissionCategoryIds, |
| 147 | }; | 168 | }; |
| 148 | setSubmitting(true); | 169 | setSubmitting(true); |
| 149 | try { | 170 | try { |
| @@ -154,6 +175,9 @@ export default function UserDetail({ userId, mode: initialMode }: Props) { | @@ -154,6 +175,9 @@ export default function UserDetail({ userId, mode: initialMode }: Props) { | ||
| 154 | dispatch(setActiveTab("userlist")); | 175 | dispatch(setActiveTab("userlist")); |
| 155 | } else if (userId) { | 176 | } else if (userId) { |
| 156 | await updateUser(userId, dto); | 177 | await updateUser(userId, dto); |
| 178 | + const refreshed = await getUser(userId); | ||
| 179 | + setDetail(refreshed); | ||
| 180 | + setPermissionCategoryIds(refreshed.permissionCategoryIds ?? []); | ||
| 157 | message.success("保存成功"); | 181 | message.success("保存成功"); |
| 158 | setMode("view"); | 182 | setMode("view"); |
| 159 | } | 183 | } |
| @@ -222,7 +246,7 @@ export default function UserDetail({ userId, mode: initialMode }: Props) { | @@ -222,7 +246,7 @@ export default function UserDetail({ userId, mode: initialMode }: Props) { | ||
| 222 | <span style={{ fontSize: 11, color: "var(--text-on-dark-muted)", marginRight: 8 }}> | 246 | <span style={{ fontSize: 11, color: "var(--text-on-dark-muted)", marginRight: 8 }}> |
| 223 | 已选权限: | 247 | 已选权限: |
| 224 | <span style={{ color: "#fff", fontWeight: 500 }}>{checkedCount}</span> /{" "} | 248 | <span style={{ color: "#fff", fontWeight: 500 }}>{checkedCount}</span> /{" "} |
| 225 | - {PERMISSION_GROUPS.length - 1} | 249 | + {permissionCategories.length} |
| 226 | </span> | 250 | </span> |
| 227 | <span | 251 | <span |
| 228 | style={{ | 252 | style={{ |
| @@ -283,7 +307,7 @@ export default function UserDetail({ userId, mode: initialMode }: Props) { | @@ -283,7 +307,7 @@ export default function UserDetail({ userId, mode: initialMode }: Props) { | ||
| 283 | value={ | 307 | value={ |
| 284 | mode === "new" | 308 | mode === "new" |
| 285 | ? "保存后自动生成" | 309 | ? "保存后自动生成" |
| 286 | - : fmtDateTime(snapshot?.tCreateDate) | 310 | + : fmtDateTime(currentSnapshot?.tCreateDate) |
| 287 | } | 311 | } |
| 288 | disabled | 312 | disabled |
| 289 | mono | 313 | mono |
| @@ -291,7 +315,7 @@ export default function UserDetail({ userId, mode: initialMode }: Props) { | @@ -291,7 +315,7 @@ export default function UserDetail({ userId, mode: initialMode }: Props) { | ||
| 291 | </Field> | 315 | </Field> |
| 292 | <Field label="制单人" required> | 316 | <Field label="制单人" required> |
| 293 | <PrimInput | 317 | <PrimInput |
| 294 | - value={mode === "new" ? "保存后自动生成" : snapshot?.sCreatedBy ?? ""} | 318 | + value={mode === "new" ? "保存后自动生成" : currentSnapshot?.sCreatedBy ?? ""} |
| 295 | disabled | 319 | disabled |
| 296 | required | 320 | required |
| 297 | /> | 321 | /> |
| @@ -415,9 +439,12 @@ export default function UserDetail({ userId, mode: initialMode }: Props) { | @@ -415,9 +439,12 @@ export default function UserDetail({ userId, mode: initialMode }: Props) { | ||
| 415 | <div style={{ flex: 1, overflow: "auto", background: "#fff" }}> | 439 | <div style={{ flex: 1, overflow: "auto", background: "#fff" }}> |
| 416 | {permTab === "groups" ? ( | 440 | {permTab === "groups" ? ( |
| 417 | <PermissionGrid | 441 | <PermissionGrid |
| 418 | - permissions={permissions} | ||
| 419 | - setPermission={(g, v) => | ||
| 420 | - setPermissions((s) => ({ ...s, [g]: v })) | 442 | + categories={permissionCategories} |
| 443 | + selectedIds={permissionCategoryIds} | ||
| 444 | + setPermission={(id, v) => | ||
| 445 | + setPermissionCategoryIds((s) => | ||
| 446 | + v ? Array.from(new Set([...s, id])) : s.filter((x) => x !== id) | ||
| 447 | + ) | ||
| 421 | } | 448 | } |
| 422 | disabled={disabled} | 449 | disabled={disabled} |
| 423 | /> | 450 | /> |
| @@ -454,15 +481,20 @@ export default function UserDetail({ userId, mode: initialMode }: Props) { | @@ -454,15 +481,20 @@ export default function UserDetail({ userId, mode: initialMode }: Props) { | ||
| 454 | } | 481 | } |
| 455 | 482 | ||
| 456 | interface PermissionGridProps { | 483 | interface PermissionGridProps { |
| 457 | - permissions: Record<string, boolean>; | ||
| 458 | - setPermission: (g: string, v: boolean) => void; | 484 | + categories: PermissionCategoryVO[]; |
| 485 | + selectedIds: number[]; | ||
| 486 | + setPermission: (id: number, v: boolean) => void; | ||
| 459 | disabled?: boolean; | 487 | disabled?: boolean; |
| 460 | } | 488 | } |
| 461 | 489 | ||
| 462 | -function PermissionGrid({ permissions, setPermission, disabled }: PermissionGridProps) { | 490 | +function PermissionGrid({ categories, selectedIds, setPermission, disabled }: PermissionGridProps) { |
| 463 | const [filter, setFilter] = useState(""); | 491 | const [filter, setFilter] = useState(""); |
| 464 | const [hovered, setHovered] = useState<string | null>(null); | 492 | const [hovered, setHovered] = useState<string | null>(null); |
| 465 | - const allChecked = PERMISSION_GROUPS.every((g) => permissions[g]); | 493 | + const selected = new Set(selectedIds); |
| 494 | + const visibleCategories = categories.filter( | ||
| 495 | + (c) => !filter || c.sCategoryName.includes(filter) || c.sCategoryCode.includes(filter) | ||
| 496 | + ); | ||
| 497 | + const allChecked = categories.length > 0 && categories.every((c) => selected.has(c.iIncrement)); | ||
| 466 | 498 | ||
| 467 | return ( | 499 | return ( |
| 468 | <table | 500 | <table |
| @@ -489,7 +521,7 @@ function PermissionGrid({ permissions, setPermission, disabled }: PermissionGrid | @@ -489,7 +521,7 @@ function PermissionGrid({ permissions, setPermission, disabled }: PermissionGrid | ||
| 489 | <PrimCheckbox | 521 | <PrimCheckbox |
| 490 | checked={allChecked} | 522 | checked={allChecked} |
| 491 | onChange={(v) => { | 523 | onChange={(v) => { |
| 492 | - PERMISSION_GROUPS.forEach((g) => setPermission(g, v)); | 524 | + categories.forEach((c) => setPermission(c.iIncrement, v)); |
| 493 | }} | 525 | }} |
| 494 | disabled={disabled} | 526 | disabled={disabled} |
| 495 | /> | 527 | /> |
| @@ -517,15 +549,15 @@ function PermissionGrid({ permissions, setPermission, disabled }: PermissionGrid | @@ -517,15 +549,15 @@ function PermissionGrid({ permissions, setPermission, disabled }: PermissionGrid | ||
| 517 | </tr> | 549 | </tr> |
| 518 | </thead> | 550 | </thead> |
| 519 | <tbody> | 551 | <tbody> |
| 520 | - {PERMISSION_GROUPS.filter((g) => !filter || g.includes(filter)).map((g, i) => { | ||
| 521 | - const isHover = hovered === g; | ||
| 522 | - const checked = !!permissions[g]; | 552 | + {visibleCategories.map((c, i) => { |
| 553 | + const isHover = hovered === c.sCategoryCode; | ||
| 554 | + const checked = selected.has(c.iIncrement); | ||
| 523 | return ( | 555 | return ( |
| 524 | <tr | 556 | <tr |
| 525 | - key={g} | ||
| 526 | - onMouseEnter={() => setHovered(g)} | 557 | + key={c.iIncrement} |
| 558 | + onMouseEnter={() => setHovered(c.sCategoryCode)} | ||
| 527 | onMouseLeave={() => setHovered(null)} | 559 | onMouseLeave={() => setHovered(null)} |
| 528 | - onClick={disabled ? undefined : () => setPermission(g, !checked)} | 560 | + onClick={disabled ? undefined : () => setPermission(c.iIncrement, !checked)} |
| 529 | style={{ | 561 | style={{ |
| 530 | background: | 562 | background: |
| 531 | isHover && !disabled | 563 | isHover && !disabled |
| @@ -549,7 +581,7 @@ function PermissionGrid({ permissions, setPermission, disabled }: PermissionGrid | @@ -549,7 +581,7 @@ function PermissionGrid({ permissions, setPermission, disabled }: PermissionGrid | ||
| 549 | > | 581 | > |
| 550 | <PrimCheckbox | 582 | <PrimCheckbox |
| 551 | checked={checked} | 583 | checked={checked} |
| 552 | - onChange={(v) => setPermission(g, v)} | 584 | + onChange={(v) => setPermission(c.iIncrement, v)} |
| 553 | disabled={disabled} | 585 | disabled={disabled} |
| 554 | /> | 586 | /> |
| 555 | </td> | 587 | </td> |
| @@ -561,7 +593,7 @@ function PermissionGrid({ permissions, setPermission, disabled }: PermissionGrid | @@ -561,7 +593,7 @@ function PermissionGrid({ permissions, setPermission, disabled }: PermissionGrid | ||
| 561 | fontWeight: checked ? 500 : 400, | 593 | fontWeight: checked ? 500 : 400, |
| 562 | }} | 594 | }} |
| 563 | > | 595 | > |
| 564 | - {g} | 596 | + {c.sCategoryName} |
| 565 | </td> | 597 | </td> |
| 566 | </tr> | 598 | </tr> |
| 567 | ); | 599 | ); |