Commit 3beaa61ae870b4f7435bede82d23a4cdca84c5c9
1 parent
2f4874be
feat(staff-picker): add staff search endpoint and UserDetail typeahead
Backend: - StaffSearchVO (iIncrement / sStaffNo / sStaffName / sDepartment) - StaffMapper.searchActive(keyword, limit) — LIKE on name + staff no - StaffController GET /api/usr/staffs?keyword=&limit= - UserListVO + UserMapper.xml: expose iStaffId so edit mode preserves the binding when user doesn't change 员工名 Frontend: - api/staff.ts → searchStaff() - StaffPicker.tsx: input + dropdown with debounced search, '已绑定' badge when an iStaffId is bound, click outside to close, search icon - UserDetail: replace 员工名 PrimInput with StaffPicker; track iStaffId in form state; send to backend on save (no longer omitted)
Showing
9 changed files
with
309 additions
and
4 deletions
backend/src/main/java/com/xly/erp/module/usr/controller/StaffController.java
0 → 100644
| 1 | +package com.xly.erp.module.usr.controller; | ||
| 2 | + | ||
| 3 | +import com.xly.erp.common.response.Result; | ||
| 4 | +import com.xly.erp.module.usr.mapper.StaffMapper; | ||
| 5 | +import com.xly.erp.module.usr.vo.StaffSearchVO; | ||
| 6 | +import org.springframework.web.bind.annotation.GetMapping; | ||
| 7 | +import org.springframework.web.bind.annotation.RequestMapping; | ||
| 8 | +import org.springframework.web.bind.annotation.RequestParam; | ||
| 9 | +import org.springframework.web.bind.annotation.RestController; | ||
| 10 | + | ||
| 11 | +import java.util.List; | ||
| 12 | + | ||
| 13 | +@RestController | ||
| 14 | +@RequestMapping("/api/usr") | ||
| 15 | +public class StaffController { | ||
| 16 | + | ||
| 17 | + private static final int DEFAULT_LIMIT = 20; | ||
| 18 | + private static final int MAX_LIMIT = 50; | ||
| 19 | + | ||
| 20 | + private final StaffMapper staffMapper; | ||
| 21 | + | ||
| 22 | + public StaffController(StaffMapper staffMapper) { | ||
| 23 | + this.staffMapper = staffMapper; | ||
| 24 | + } | ||
| 25 | + | ||
| 26 | + @GetMapping("/staffs") | ||
| 27 | + public Result<List<StaffSearchVO>> search(@RequestParam(required = false) String keyword, | ||
| 28 | + @RequestParam(required = false) Integer limit) { | ||
| 29 | + int n = (limit == null || limit < 1) ? DEFAULT_LIMIT : Math.min(limit, MAX_LIMIT); | ||
| 30 | + String kw = keyword == null ? "" : keyword.trim(); | ||
| 31 | + return Result.ok(staffMapper.searchActive(kw, n)); | ||
| 32 | + } | ||
| 33 | +} |
backend/src/main/java/com/xly/erp/module/usr/mapper/StaffMapper.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.StaffSearchVO; | ||
| 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; |
| 6 | 7 | ||
| 8 | +import java.util.List; | ||
| 9 | + | ||
| 7 | @Mapper | 10 | @Mapper |
| 8 | public interface StaffMapper { | 11 | public interface StaffMapper { |
| 9 | 12 | ||
| @@ -13,4 +16,19 @@ public interface StaffMapper { | @@ -13,4 +16,19 @@ public interface StaffMapper { | ||
| 13 | default boolean existsActiveById(Integer id) { | 16 | default boolean existsActiveById(Integer id) { |
| 14 | return findActiveStaffFlag(id) != null; | 17 | return findActiveStaffFlag(id) != null; |
| 15 | } | 18 | } |
| 19 | + | ||
| 20 | + @Select(""" | ||
| 21 | + <script> | ||
| 22 | + SELECT iIncrement, sStaffNo, sStaffName, sDepartment | ||
| 23 | + FROM tStaff | ||
| 24 | + WHERE bDeleted = 0 | ||
| 25 | + <if test='keyword != null and keyword != ""'> | ||
| 26 | + AND (sStaffName LIKE CONCAT('%', #{keyword}, '%') | ||
| 27 | + OR sStaffNo LIKE CONCAT('%', #{keyword}, '%')) | ||
| 28 | + </if> | ||
| 29 | + ORDER BY sStaffName ASC | ||
| 30 | + LIMIT #{limit} | ||
| 31 | + </script> | ||
| 32 | + """) | ||
| 33 | + List<StaffSearchVO> searchActive(@Param("keyword") String keyword, @Param("limit") Integer limit); | ||
| 16 | } | 34 | } |
backend/src/main/java/com/xly/erp/module/usr/vo/StaffSearchVO.java
0 → 100644
| 1 | +package com.xly.erp.module.usr.vo; | ||
| 2 | + | ||
| 3 | +import com.fasterxml.jackson.annotation.JsonProperty; | ||
| 4 | + | ||
| 5 | +public class StaffSearchVO { | ||
| 6 | + | ||
| 7 | + @JsonProperty("iIncrement") | ||
| 8 | + private Integer iIncrement; | ||
| 9 | + | ||
| 10 | + @JsonProperty("sStaffNo") | ||
| 11 | + private String sStaffNo; | ||
| 12 | + | ||
| 13 | + @JsonProperty("sStaffName") | ||
| 14 | + private String sStaffName; | ||
| 15 | + | ||
| 16 | + @JsonProperty("sDepartment") | ||
| 17 | + private String sDepartment; | ||
| 18 | + | ||
| 19 | + public Integer getIIncrement() { return iIncrement; } | ||
| 20 | + public void setIIncrement(Integer iIncrement) { this.iIncrement = iIncrement; } | ||
| 21 | + public String getSStaffNo() { return sStaffNo; } | ||
| 22 | + public void setSStaffNo(String sStaffNo) { this.sStaffNo = sStaffNo; } | ||
| 23 | + public String getSStaffName() { return sStaffName; } | ||
| 24 | + public void setSStaffName(String sStaffName) { this.sStaffName = sStaffName; } | ||
| 25 | + public String getSDepartment() { return sDepartment; } | ||
| 26 | + public void setSDepartment(String sDepartment) { this.sDepartment = sDepartment; } | ||
| 27 | +} |
backend/src/main/java/com/xly/erp/module/usr/vo/UserListVO.java
| @@ -12,6 +12,9 @@ public class UserListVO { | @@ -12,6 +12,9 @@ public class UserListVO { | ||
| 12 | @JsonProperty("sUserName") | 12 | @JsonProperty("sUserName") |
| 13 | private String sUserName; | 13 | private String sUserName; |
| 14 | 14 | ||
| 15 | + @JsonProperty("iStaffId") | ||
| 16 | + private Integer iStaffId; | ||
| 17 | + | ||
| 15 | @JsonProperty("staffName") | 18 | @JsonProperty("staffName") |
| 16 | private String staffName; | 19 | private String staffName; |
| 17 | 20 | ||
| @@ -43,6 +46,8 @@ public class UserListVO { | @@ -43,6 +46,8 @@ public class UserListVO { | ||
| 43 | public void setIIncrement(Integer iIncrement) { this.iIncrement = iIncrement; } | 46 | public void setIIncrement(Integer iIncrement) { this.iIncrement = iIncrement; } |
| 44 | public String getSUserName() { return sUserName; } | 47 | public String getSUserName() { return sUserName; } |
| 45 | public void setSUserName(String sUserName) { this.sUserName = sUserName; } | 48 | public void setSUserName(String sUserName) { this.sUserName = sUserName; } |
| 49 | + public Integer getIStaffId() { return iStaffId; } | ||
| 50 | + public void setIStaffId(Integer iStaffId) { this.iStaffId = iStaffId; } | ||
| 46 | public String getStaffName() { return staffName; } | 51 | public String getStaffName() { return staffName; } |
| 47 | public void setStaffName(String staffName) { this.staffName = staffName; } | 52 | public void setStaffName(String staffName) { this.staffName = staffName; } |
| 48 | public String getSUserNo() { return sUserNo; } | 53 | public String getSUserNo() { return sUserNo; } |
backend/src/main/resources/mapper/usr/UserMapper.xml
| @@ -6,6 +6,7 @@ | @@ -6,6 +6,7 @@ | ||
| 6 | <sql id="baseSelectColumns"> | 6 | <sql id="baseSelectColumns"> |
| 7 | u.iIncrement AS iIncrement, | 7 | u.iIncrement AS iIncrement, |
| 8 | u.sUserName AS sUserName, | 8 | u.sUserName AS sUserName, |
| 9 | + u.iStaffId AS iStaffId, | ||
| 9 | s.sStaffName AS staffName, | 10 | s.sStaffName AS staffName, |
| 10 | u.sUserNo AS sUserNo, | 11 | u.sUserNo AS sUserNo, |
| 11 | s.sDepartment AS department, | 12 | s.sDepartment AS department, |
frontend/src/api/staff.ts
0 → 100644
| 1 | +import { request } from "./client"; | ||
| 2 | + | ||
| 3 | +export interface StaffSearchVO { | ||
| 4 | + iIncrement: number; | ||
| 5 | + sStaffNo: string; | ||
| 6 | + sStaffName: string; | ||
| 7 | + sDepartment: string | null; | ||
| 8 | +} | ||
| 9 | + | ||
| 10 | +export function searchStaff(keyword?: string, limit = 20): Promise<StaffSearchVO[]> { | ||
| 11 | + return request<StaffSearchVO[]>({ | ||
| 12 | + url: "/usr/staffs", | ||
| 13 | + method: "GET", | ||
| 14 | + params: { keyword: keyword ?? "", limit }, | ||
| 15 | + }); | ||
| 16 | +} |
frontend/src/api/user.ts
| @@ -3,6 +3,7 @@ import { request } from "./client"; | @@ -3,6 +3,7 @@ import { request } from "./client"; | ||
| 3 | export interface UserListVO { | 3 | export interface UserListVO { |
| 4 | iIncrement: number; | 4 | iIncrement: number; |
| 5 | sUserName: string; | 5 | sUserName: string; |
| 6 | + iStaffId: number | null; | ||
| 6 | staffName: string | null; | 7 | staffName: string | null; |
| 7 | sUserNo: string; | 8 | sUserNo: string; |
| 8 | department: string | null; | 9 | department: string | null; |
frontend/src/components/StaffPicker.tsx
0 → 100644
| 1 | +import { useEffect, useRef, useState } from "react"; | ||
| 2 | +import { SearchOutlined } from "@ant-design/icons"; | ||
| 3 | +import { searchStaff, type StaffSearchVO } from "@/api/staff"; | ||
| 4 | + | ||
| 5 | +interface Props { | ||
| 6 | + value: string; | ||
| 7 | + staffId: number | null; | ||
| 8 | + onChange: (name: string, staffId: number | null) => void; | ||
| 9 | + disabled?: boolean; | ||
| 10 | + required?: boolean; | ||
| 11 | + placeholder?: string; | ||
| 12 | +} | ||
| 13 | + | ||
| 14 | +const fieldControl: React.CSSProperties = { | ||
| 15 | + flex: 1, | ||
| 16 | + minWidth: 0, | ||
| 17 | + height: "var(--input-h)", | ||
| 18 | + border: "1px solid var(--border-input)", | ||
| 19 | + background: "var(--bg-input)", | ||
| 20 | + padding: "0 26px 0 6px", | ||
| 21 | + fontSize: 12, | ||
| 22 | + color: "var(--text)", | ||
| 23 | + borderRadius: 0, | ||
| 24 | + outline: "none", | ||
| 25 | + fontFamily: "inherit", | ||
| 26 | +}; | ||
| 27 | + | ||
| 28 | +export default function StaffPicker({ | ||
| 29 | + value, | ||
| 30 | + staffId, | ||
| 31 | + onChange, | ||
| 32 | + disabled, | ||
| 33 | + required, | ||
| 34 | + placeholder, | ||
| 35 | +}: Props) { | ||
| 36 | + const [open, setOpen] = useState(false); | ||
| 37 | + const [items, setItems] = useState<StaffSearchVO[]>([]); | ||
| 38 | + const [loading, setLoading] = useState(false); | ||
| 39 | + const wrapRef = useRef<HTMLDivElement>(null); | ||
| 40 | + const debounce = useRef<number | null>(null); | ||
| 41 | + | ||
| 42 | + useEffect(() => { | ||
| 43 | + function onDocClick(e: MouseEvent) { | ||
| 44 | + if (wrapRef.current && !wrapRef.current.contains(e.target as Node)) { | ||
| 45 | + setOpen(false); | ||
| 46 | + } | ||
| 47 | + } | ||
| 48 | + document.addEventListener("mousedown", onDocClick); | ||
| 49 | + return () => document.removeEventListener("mousedown", onDocClick); | ||
| 50 | + }, []); | ||
| 51 | + | ||
| 52 | + const fetchSuggestions = async (kw: string) => { | ||
| 53 | + setLoading(true); | ||
| 54 | + try { | ||
| 55 | + const list = await searchStaff(kw, 20); | ||
| 56 | + setItems(list); | ||
| 57 | + } catch { | ||
| 58 | + setItems([]); | ||
| 59 | + } finally { | ||
| 60 | + setLoading(false); | ||
| 61 | + } | ||
| 62 | + }; | ||
| 63 | + | ||
| 64 | + const onFocus = () => { | ||
| 65 | + if (disabled) return; | ||
| 66 | + setOpen(true); | ||
| 67 | + void fetchSuggestions(value); | ||
| 68 | + }; | ||
| 69 | + | ||
| 70 | + const onInputChange = (next: string) => { | ||
| 71 | + // Typing clears any previously bound staff id; user must re-select | ||
| 72 | + onChange(next, null); | ||
| 73 | + setOpen(true); | ||
| 74 | + if (debounce.current) window.clearTimeout(debounce.current); | ||
| 75 | + debounce.current = window.setTimeout(() => fetchSuggestions(next), 200); | ||
| 76 | + }; | ||
| 77 | + | ||
| 78 | + const select = (s: StaffSearchVO) => { | ||
| 79 | + onChange(s.sStaffName, s.iIncrement); | ||
| 80 | + setOpen(false); | ||
| 81 | + }; | ||
| 82 | + | ||
| 83 | + return ( | ||
| 84 | + <div | ||
| 85 | + ref={wrapRef} | ||
| 86 | + style={{ position: "relative", flex: 1, minWidth: 0, display: "flex", alignItems: "center" }} | ||
| 87 | + > | ||
| 88 | + <input | ||
| 89 | + type="text" | ||
| 90 | + value={value} | ||
| 91 | + onChange={(e) => onInputChange(e.target.value)} | ||
| 92 | + onFocus={onFocus} | ||
| 93 | + disabled={disabled} | ||
| 94 | + placeholder={placeholder} | ||
| 95 | + style={{ | ||
| 96 | + ...fieldControl, | ||
| 97 | + ...(disabled ? { background: "var(--bg-disabled)", color: "var(--text-muted)" } : {}), | ||
| 98 | + ...(required && !disabled ? { background: "#d4e8f7" } : {}), | ||
| 99 | + }} | ||
| 100 | + /> | ||
| 101 | + <span | ||
| 102 | + style={{ | ||
| 103 | + position: "absolute", | ||
| 104 | + right: 6, | ||
| 105 | + top: "50%", | ||
| 106 | + transform: "translateY(-50%)", | ||
| 107 | + color: "var(--text-faint)", | ||
| 108 | + pointerEvents: "none", | ||
| 109 | + fontSize: 12, | ||
| 110 | + }} | ||
| 111 | + > | ||
| 112 | + <SearchOutlined /> | ||
| 113 | + </span> | ||
| 114 | + {staffId != null && !disabled && ( | ||
| 115 | + <span | ||
| 116 | + style={{ | ||
| 117 | + position: "absolute", | ||
| 118 | + right: 22, | ||
| 119 | + top: "50%", | ||
| 120 | + transform: "translateY(-50%)", | ||
| 121 | + fontSize: 9, | ||
| 122 | + color: "#fff", | ||
| 123 | + background: "var(--success)", | ||
| 124 | + padding: "1px 4px", | ||
| 125 | + borderRadius: 2, | ||
| 126 | + pointerEvents: "none", | ||
| 127 | + }} | ||
| 128 | + title={`已绑定职员 ID ${staffId}`} | ||
| 129 | + > | ||
| 130 | + 已绑定 | ||
| 131 | + </span> | ||
| 132 | + )} | ||
| 133 | + {open && !disabled && ( | ||
| 134 | + <div | ||
| 135 | + style={{ | ||
| 136 | + position: "absolute", | ||
| 137 | + top: "calc(100% + 1px)", | ||
| 138 | + left: 0, | ||
| 139 | + right: 0, | ||
| 140 | + zIndex: 50, | ||
| 141 | + background: "#fff", | ||
| 142 | + border: "1px solid var(--border-input)", | ||
| 143 | + boxShadow: "0 2px 8px rgba(0,0,0,0.08)", | ||
| 144 | + maxHeight: 240, | ||
| 145 | + overflow: "auto", | ||
| 146 | + }} | ||
| 147 | + > | ||
| 148 | + {loading && ( | ||
| 149 | + <div style={{ padding: "8px 12px", fontSize: 12, color: "var(--text-faint)" }}> | ||
| 150 | + 加载中… | ||
| 151 | + </div> | ||
| 152 | + )} | ||
| 153 | + {!loading && items.length === 0 && ( | ||
| 154 | + <div style={{ padding: "8px 12px", fontSize: 12, color: "var(--text-faint)" }}> | ||
| 155 | + 没有匹配的职员 | ||
| 156 | + </div> | ||
| 157 | + )} | ||
| 158 | + {!loading && | ||
| 159 | + items.map((s) => ( | ||
| 160 | + <div | ||
| 161 | + key={s.iIncrement} | ||
| 162 | + onMouseDown={(e) => { | ||
| 163 | + e.preventDefault(); | ||
| 164 | + select(s); | ||
| 165 | + }} | ||
| 166 | + style={{ | ||
| 167 | + padding: "6px 12px", | ||
| 168 | + fontSize: 12, | ||
| 169 | + cursor: "pointer", | ||
| 170 | + display: "flex", | ||
| 171 | + alignItems: "center", | ||
| 172 | + gap: 8, | ||
| 173 | + borderBottom: "1px solid #f0f2f5", | ||
| 174 | + }} | ||
| 175 | + onMouseEnter={(e) => { | ||
| 176 | + e.currentTarget.style.background = "var(--bg-row-hover)"; | ||
| 177 | + }} | ||
| 178 | + onMouseLeave={(e) => { | ||
| 179 | + e.currentTarget.style.background = "transparent"; | ||
| 180 | + }} | ||
| 181 | + > | ||
| 182 | + <span style={{ color: "var(--text)" }}>{s.sStaffName}</span> | ||
| 183 | + <span style={{ color: "var(--text-faint)", fontSize: 11 }} className="mono"> | ||
| 184 | + {s.sStaffNo} | ||
| 185 | + </span> | ||
| 186 | + <span style={{ flex: 1 }} /> | ||
| 187 | + <span style={{ color: "var(--text-muted)", fontSize: 11 }}> | ||
| 188 | + {s.sDepartment ?? ""} | ||
| 189 | + </span> | ||
| 190 | + </div> | ||
| 191 | + ))} | ||
| 192 | + </div> | ||
| 193 | + )} | ||
| 194 | + </div> | ||
| 195 | + ); | ||
| 196 | +} |
frontend/src/pages/usr/UserDetail.tsx
| @@ -19,6 +19,7 @@ import { | @@ -19,6 +19,7 @@ import { | ||
| 19 | PrimCheckbox, | 19 | PrimCheckbox, |
| 20 | ToolbarBtnDark, | 20 | ToolbarBtnDark, |
| 21 | } from "@/components/Primitives"; | 21 | } from "@/components/Primitives"; |
| 22 | +import StaffPicker from "@/components/StaffPicker"; | ||
| 22 | import { createUser, updateUser, type UserDTO } from "@/api/user"; | 23 | import { createUser, updateUser, type UserDTO } from "@/api/user"; |
| 23 | import { | 24 | import { |
| 24 | USER_TYPES, | 25 | USER_TYPES, |
| @@ -41,6 +42,7 @@ interface Props { | @@ -41,6 +42,7 @@ interface Props { | ||
| 41 | interface FormState { | 42 | interface FormState { |
| 42 | sUserNo: string; | 43 | sUserNo: string; |
| 43 | sUserName: string; | 44 | sUserName: string; |
| 45 | + iStaffId: number | null; | ||
| 44 | staffName: string; | 46 | staffName: string; |
| 45 | department: string; | 47 | department: string; |
| 46 | sUserType: string; | 48 | sUserType: string; |
| @@ -69,6 +71,7 @@ export default function UserDetail({ userId, mode: initialMode }: Props) { | @@ -69,6 +71,7 @@ export default function UserDetail({ userId, mode: initialMode }: Props) { | ||
| 69 | | { | 71 | | { |
| 70 | sUserNo: string; | 72 | sUserNo: string; |
| 71 | sUserName: string; | 73 | sUserName: string; |
| 74 | + iStaffId: number | null; | ||
| 72 | staffName: string | null; | 75 | staffName: string | null; |
| 73 | department: string | null; | 76 | department: string | null; |
| 74 | sUserType: string; | 77 | sUserType: string; |
| @@ -84,6 +87,7 @@ export default function UserDetail({ userId, mode: initialMode }: Props) { | @@ -84,6 +87,7 @@ export default function UserDetail({ userId, mode: initialMode }: Props) { | ||
| 84 | ? { | 87 | ? { |
| 85 | sUserNo: "", | 88 | sUserNo: "", |
| 86 | sUserName: "", | 89 | sUserName: "", |
| 90 | + iStaffId: null, | ||
| 87 | staffName: "", | 91 | staffName: "", |
| 88 | department: "", | 92 | department: "", |
| 89 | sUserType: "普通用户", | 93 | sUserType: "普通用户", |
| @@ -93,6 +97,7 @@ export default function UserDetail({ userId, mode: initialMode }: Props) { | @@ -93,6 +97,7 @@ export default function UserDetail({ userId, mode: initialMode }: Props) { | ||
| 93 | : { | 97 | : { |
| 94 | sUserNo: snapshot.sUserNo, | 98 | sUserNo: snapshot.sUserNo, |
| 95 | sUserName: snapshot.sUserName, | 99 | sUserName: snapshot.sUserName, |
| 100 | + iStaffId: snapshot.iStaffId, | ||
| 96 | staffName: snapshot.staffName ?? "", | 101 | staffName: snapshot.staffName ?? "", |
| 97 | department: snapshot.department ?? "", | 102 | department: snapshot.department ?? "", |
| 98 | sUserType: snapshot.sUserType, | 103 | sUserType: snapshot.sUserType, |
| @@ -139,10 +144,10 @@ export default function UserDetail({ userId, mode: initialMode }: Props) { | @@ -139,10 +144,10 @@ export default function UserDetail({ userId, mode: initialMode }: Props) { | ||
| 139 | const dto: UserDTO = { | 144 | const dto: UserDTO = { |
| 140 | sUserNo: form.sUserNo, | 145 | sUserNo: form.sUserNo, |
| 141 | sUserName: form.sUserName, | 146 | sUserName: form.sUserName, |
| 147 | + iStaffId: form.iStaffId, | ||
| 142 | sUserType: form.sUserType, | 148 | sUserType: form.sUserType, |
| 143 | sLanguage: form.sLanguage, | 149 | sLanguage: form.sLanguage, |
| 144 | bCanModifyDocs: form.bCanModifyDocs, | 150 | bCanModifyDocs: form.bCanModifyDocs, |
| 145 | - // iStaffId omitted: prototype has no staff picker. | ||
| 146 | // permissionCategoryIds omitted: prototype permissions are by name; backend wants IDs. | 151 | // permissionCategoryIds omitted: prototype permissions are by name; backend wants IDs. |
| 147 | }; | 152 | }; |
| 148 | setSubmitting(true); | 153 | setSubmitting(true); |
| @@ -297,12 +302,15 @@ export default function UserDetail({ userId, mode: initialMode }: Props) { | @@ -297,12 +302,15 @@ export default function UserDetail({ userId, mode: initialMode }: Props) { | ||
| 297 | /> | 302 | /> |
| 298 | </Field> | 303 | </Field> |
| 299 | <Field label="员工名" required> | 304 | <Field label="员工名" required> |
| 300 | - <PrimInput | 305 | + <StaffPicker |
| 301 | value={form.staffName} | 306 | value={form.staffName} |
| 302 | - onChange={(v) => set("staffName", v)} | 307 | + staffId={form.iStaffId} |
| 308 | + onChange={(name, id) => | ||
| 309 | + setForm((s) => ({ ...s, staffName: name, iStaffId: id })) | ||
| 310 | + } | ||
| 303 | disabled={disabled} | 311 | disabled={disabled} |
| 304 | required | 312 | required |
| 305 | - placeholder="(关联职员)" | 313 | + placeholder="输入员工姓名搜索" |
| 306 | /> | 314 | /> |
| 307 | </Field> | 315 | </Field> |
| 308 | 316 |