Commit fe5a6531cf4e2680051a1301a729a226a9436d0a

Authored by zichun
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
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&lt;User&gt; { @@ -31,4 +31,6 @@ public interface UserMapper extends BaseMapper&lt;User&gt; {
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&lt;UserListPage&gt; { @@ -47,6 +58,17 @@ export function listUsers(params: UserListParams = {}): Promise&lt;UserListPage&gt; {
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 );