Commit 3491ef6c2fca2596ea06e9a132c5910e04a41ad5
1 parent
748348f0
feat(frontend): restore prototype UserDetail look (perm grid + scope tabs)
Reverses the earlier 'hide unsupported fields' decision per user request to strictly follow the prototype look. Brings back: - Full dark sub-toolbar: 新增 / 修改 / 删除 / 保存 / 取消 (separator) 功能 / 作废 / 重置密码 / 取消作废, with status chip + settings gear on the right and 已选权限 counter - 9-field 3x3 form with prototype's required-field cyan tint, 制单人 showing 保存后自动生成 placeholder in new mode - 6 perm tabs (权限组 / 客户 / 供应商 / 人员 / 工序 / 司机) - 30-row permission grid with hover, accent-soft selected row, filter - Scope tabs with master enable + 4-col grid of items - Floating vertical 帮助 button on right edge Permissions and the no-op buttons are visual-only — they don't round-trip to backend. Save still sends only sUserNo/sUserName/sUserType/sLanguage/ bCanModifyDocs (iStaffId and permissionCategoryIds intentionally omitted). Adds bespoke Primitives.tsx (Field/PrimInput/PrimSelect/PrimCheckbox/ ToolbarBtnDark/ToolbarBtnLight) so this screen renders pixel-faithful to prototype rather than picking up AntD chrome.
Showing
3 changed files
with
913 additions
and
117 deletions
frontend/src/components/Primitives.tsx
0 → 100644
| 1 | +// Bespoke primitives that mirror prototype/src/primitives.jsx exactly so | |
| 2 | +// screens like UserDetail keep pixel-level fidelity to the prototype rather | |
| 3 | +// than picking up AntD's default chrome. | |
| 4 | + | |
| 5 | +import React from "react"; | |
| 6 | + | |
| 7 | +const fieldStyles = { | |
| 8 | + label: { | |
| 9 | + width: 88, | |
| 10 | + flex: "none" as const, | |
| 11 | + textAlign: "right" as const, | |
| 12 | + paddingRight: 8, | |
| 13 | + color: "var(--text)", | |
| 14 | + fontSize: 12, | |
| 15 | + lineHeight: "26px", | |
| 16 | + whiteSpace: "nowrap" as const, | |
| 17 | + overflow: "hidden" as const, | |
| 18 | + textOverflow: "ellipsis" as const, | |
| 19 | + }, | |
| 20 | + control: { | |
| 21 | + flex: 1, | |
| 22 | + minWidth: 0, | |
| 23 | + height: "var(--input-h)", | |
| 24 | + border: "1px solid var(--border-input)", | |
| 25 | + background: "var(--bg-input)", | |
| 26 | + padding: "0 6px", | |
| 27 | + fontSize: 12, | |
| 28 | + color: "var(--text)", | |
| 29 | + borderRadius: 0, | |
| 30 | + outline: "none", | |
| 31 | + fontFamily: "inherit", | |
| 32 | + }, | |
| 33 | + controlDisabled: { background: "var(--bg-disabled)", color: "var(--text-muted)" }, | |
| 34 | + controlReq: { background: "#d4e8f7" }, // light cyan tint matching prototype screenshots | |
| 35 | +}; | |
| 36 | + | |
| 37 | +interface FieldProps { | |
| 38 | + label: string; | |
| 39 | + required?: boolean; | |
| 40 | + children: React.ReactNode; | |
| 41 | + labelWidth?: number; | |
| 42 | +} | |
| 43 | + | |
| 44 | +export function Field({ label, required, children, labelWidth }: FieldProps) { | |
| 45 | + return ( | |
| 46 | + <div style={{ display: "flex", alignItems: "center", minWidth: 0 }}> | |
| 47 | + <div | |
| 48 | + style={{ | |
| 49 | + ...fieldStyles.label, | |
| 50 | + width: labelWidth ?? fieldStyles.label.width, | |
| 51 | + ...(required ? { color: "var(--required)" } : {}), | |
| 52 | + }} | |
| 53 | + title={label} | |
| 54 | + > | |
| 55 | + {required ? <span style={{ marginRight: 2 }}>*</span> : null} | |
| 56 | + {label}: | |
| 57 | + </div> | |
| 58 | + {children} | |
| 59 | + </div> | |
| 60 | + ); | |
| 61 | +} | |
| 62 | + | |
| 63 | +interface InputProps { | |
| 64 | + value: string; | |
| 65 | + onChange?: (v: string) => void; | |
| 66 | + disabled?: boolean; | |
| 67 | + required?: boolean; | |
| 68 | + placeholder?: string; | |
| 69 | + mono?: boolean; | |
| 70 | +} | |
| 71 | + | |
| 72 | +export function PrimInput({ value, onChange, disabled, required, placeholder, mono }: InputProps) { | |
| 73 | + return ( | |
| 74 | + <input | |
| 75 | + type="text" | |
| 76 | + value={value ?? ""} | |
| 77 | + onChange={onChange ? (e) => onChange(e.target.value) : undefined} | |
| 78 | + disabled={disabled} | |
| 79 | + placeholder={placeholder} | |
| 80 | + style={{ | |
| 81 | + ...fieldStyles.control, | |
| 82 | + ...(disabled ? fieldStyles.controlDisabled : {}), | |
| 83 | + ...(required && !disabled ? fieldStyles.controlReq : {}), | |
| 84 | + ...(mono | |
| 85 | + ? { fontFamily: '"JetBrains Mono", Menlo, Consolas, monospace', fontSize: 11 } | |
| 86 | + : {}), | |
| 87 | + }} | |
| 88 | + /> | |
| 89 | + ); | |
| 90 | +} | |
| 91 | + | |
| 92 | +interface SelectOption { | |
| 93 | + value: string; | |
| 94 | + label: string; | |
| 95 | +} | |
| 96 | + | |
| 97 | +interface SelectProps { | |
| 98 | + value: string; | |
| 99 | + onChange?: (v: string) => void; | |
| 100 | + disabled?: boolean; | |
| 101 | + required?: boolean; | |
| 102 | + options: (string | SelectOption)[]; | |
| 103 | + allowEmpty?: boolean; | |
| 104 | +} | |
| 105 | + | |
| 106 | +export function PrimSelect({ | |
| 107 | + value, | |
| 108 | + onChange, | |
| 109 | + disabled, | |
| 110 | + required, | |
| 111 | + options, | |
| 112 | + allowEmpty = true, | |
| 113 | +}: SelectProps) { | |
| 114 | + return ( | |
| 115 | + <div style={{ position: "relative", flex: 1, minWidth: 0 }}> | |
| 116 | + <select | |
| 117 | + value={value ?? ""} | |
| 118 | + onChange={onChange ? (e) => onChange(e.target.value) : undefined} | |
| 119 | + disabled={disabled} | |
| 120 | + style={{ | |
| 121 | + ...fieldStyles.control, | |
| 122 | + ...(disabled ? fieldStyles.controlDisabled : {}), | |
| 123 | + ...(required && !disabled ? fieldStyles.controlReq : {}), | |
| 124 | + appearance: "none", | |
| 125 | + paddingRight: 22, | |
| 126 | + width: "100%", | |
| 127 | + }} | |
| 128 | + > | |
| 129 | + {allowEmpty ? <option value="" /> : null} | |
| 130 | + {options.map((o) => { | |
| 131 | + const v = typeof o === "string" ? o : o.value; | |
| 132 | + const l = typeof o === "string" ? o : o.label; | |
| 133 | + return ( | |
| 134 | + <option key={v} value={v}> | |
| 135 | + {l} | |
| 136 | + </option> | |
| 137 | + ); | |
| 138 | + })} | |
| 139 | + </select> | |
| 140 | + <div | |
| 141 | + style={{ | |
| 142 | + position: "absolute", | |
| 143 | + right: 6, | |
| 144 | + top: "50%", | |
| 145 | + transform: "translateY(-50%)", | |
| 146 | + color: "var(--text-faint)", | |
| 147 | + pointerEvents: "none", | |
| 148 | + fontSize: 9, | |
| 149 | + }} | |
| 150 | + > | |
| 151 | + ▼ | |
| 152 | + </div> | |
| 153 | + </div> | |
| 154 | + ); | |
| 155 | +} | |
| 156 | + | |
| 157 | +interface CheckboxProps { | |
| 158 | + checked: boolean; | |
| 159 | + onChange?: (v: boolean) => void; | |
| 160 | + disabled?: boolean; | |
| 161 | + label?: string; | |
| 162 | + size?: number; | |
| 163 | +} | |
| 164 | + | |
| 165 | +export function PrimCheckbox({ checked, onChange, disabled, label, size = 13 }: CheckboxProps) { | |
| 166 | + return ( | |
| 167 | + <label | |
| 168 | + style={{ | |
| 169 | + display: "inline-flex", | |
| 170 | + alignItems: "center", | |
| 171 | + gap: 4, | |
| 172 | + cursor: disabled ? "not-allowed" : "pointer", | |
| 173 | + userSelect: "none", | |
| 174 | + color: "var(--text)", | |
| 175 | + fontSize: 12, | |
| 176 | + }} | |
| 177 | + > | |
| 178 | + <span | |
| 179 | + onClick={disabled ? undefined : () => onChange?.(!checked)} | |
| 180 | + style={{ | |
| 181 | + width: size, | |
| 182 | + height: size, | |
| 183 | + border: "1px solid var(--border-strong)", | |
| 184 | + background: checked ? "var(--accent)" : "#fff", | |
| 185 | + borderColor: checked ? "var(--accent)" : "var(--border-strong)", | |
| 186 | + display: "inline-flex", | |
| 187 | + alignItems: "center", | |
| 188 | + justifyContent: "center", | |
| 189 | + cursor: disabled ? "not-allowed" : "pointer", | |
| 190 | + flex: "none", | |
| 191 | + opacity: disabled ? 0.6 : 1, | |
| 192 | + }} | |
| 193 | + > | |
| 194 | + {checked ? ( | |
| 195 | + <svg width={size - 4} height={size - 4} viewBox="0 0 10 10" fill="none"> | |
| 196 | + <path | |
| 197 | + d="M2 5l2 2 4-4" | |
| 198 | + stroke="#fff" | |
| 199 | + strokeWidth="1.5" | |
| 200 | + strokeLinecap="round" | |
| 201 | + strokeLinejoin="round" | |
| 202 | + /> | |
| 203 | + </svg> | |
| 204 | + ) : null} | |
| 205 | + </span> | |
| 206 | + {label != null ? <span>{label}</span> : null} | |
| 207 | + </label> | |
| 208 | + ); | |
| 209 | +} | |
| 210 | + | |
| 211 | +interface ToolbarBtnDarkProps { | |
| 212 | + children: React.ReactNode; | |
| 213 | + icon?: React.ReactNode; | |
| 214 | + onClick?: () => void; | |
| 215 | + disabled?: boolean; | |
| 216 | + danger?: boolean; | |
| 217 | +} | |
| 218 | + | |
| 219 | +export function ToolbarBtnDark({ | |
| 220 | + children, | |
| 221 | + icon, | |
| 222 | + onClick, | |
| 223 | + disabled, | |
| 224 | + danger, | |
| 225 | +}: ToolbarBtnDarkProps) { | |
| 226 | + const [hover, setHover] = React.useState(false); | |
| 227 | + return ( | |
| 228 | + <button | |
| 229 | + onClick={disabled ? undefined : onClick} | |
| 230 | + onMouseEnter={() => setHover(true)} | |
| 231 | + onMouseLeave={() => setHover(false)} | |
| 232 | + disabled={disabled} | |
| 233 | + style={{ | |
| 234 | + height: 26, | |
| 235 | + padding: "0 10px", | |
| 236 | + display: "inline-flex", | |
| 237 | + alignItems: "center", | |
| 238 | + gap: 4, | |
| 239 | + background: hover && !disabled ? "rgba(255,255,255,0.08)" : "transparent", | |
| 240 | + border: "1px solid transparent", | |
| 241 | + color: disabled | |
| 242 | + ? "var(--text-on-dark-muted)" | |
| 243 | + : danger && hover | |
| 244 | + ? "#ff8b8b" | |
| 245 | + : "var(--text-on-dark)", | |
| 246 | + cursor: disabled ? "not-allowed" : "pointer", | |
| 247 | + fontSize: 12, | |
| 248 | + borderRadius: 2, | |
| 249 | + whiteSpace: "nowrap", | |
| 250 | + fontFamily: "inherit", | |
| 251 | + }} | |
| 252 | + > | |
| 253 | + {icon ? <span style={{ display: "inline-flex" }}>{icon}</span> : null} | |
| 254 | + {children} | |
| 255 | + </button> | |
| 256 | + ); | |
| 257 | +} | |
| 258 | + | |
| 259 | +interface ToolbarBtnLightProps { | |
| 260 | + children: React.ReactNode; | |
| 261 | + icon?: React.ReactNode; | |
| 262 | + onClick?: () => void; | |
| 263 | + disabled?: boolean; | |
| 264 | + primary?: boolean; | |
| 265 | + danger?: boolean; | |
| 266 | + success?: boolean; | |
| 267 | +} | |
| 268 | + | |
| 269 | +export function ToolbarBtnLight({ | |
| 270 | + children, | |
| 271 | + icon, | |
| 272 | + onClick, | |
| 273 | + disabled, | |
| 274 | + primary, | |
| 275 | + danger, | |
| 276 | + success, | |
| 277 | +}: ToolbarBtnLightProps) { | |
| 278 | + const [hover, setHover] = React.useState(false); | |
| 279 | + let bg = "#fff", | |
| 280 | + border = "var(--border-input)", | |
| 281 | + color = "var(--text)"; | |
| 282 | + if (primary) { | |
| 283 | + bg = hover ? "#3a9ae8" : "#5cabe8"; | |
| 284 | + border = "#3a8ad6"; | |
| 285 | + color = "#fff"; | |
| 286 | + } else if (danger) { | |
| 287 | + bg = hover ? "#e85a5a" : "#f07070"; | |
| 288 | + border = "#d04141"; | |
| 289 | + color = "#fff"; | |
| 290 | + } else if (success) { | |
| 291 | + bg = hover ? "#38b777" : "#4cc488"; | |
| 292 | + border = "#27a567"; | |
| 293 | + color = "#fff"; | |
| 294 | + } else if (hover && !disabled) { | |
| 295 | + bg = "var(--accent-soft)"; | |
| 296 | + border = "var(--accent)"; | |
| 297 | + } | |
| 298 | + return ( | |
| 299 | + <button | |
| 300 | + onClick={disabled ? undefined : onClick} | |
| 301 | + onMouseEnter={() => setHover(true)} | |
| 302 | + onMouseLeave={() => setHover(false)} | |
| 303 | + disabled={disabled} | |
| 304 | + style={{ | |
| 305 | + height: "var(--tb-h)", | |
| 306 | + padding: "0 10px", | |
| 307 | + display: "inline-flex", | |
| 308 | + alignItems: "center", | |
| 309 | + gap: 4, | |
| 310 | + background: disabled ? "var(--bg-disabled)" : bg, | |
| 311 | + border: `1px solid ${disabled ? "var(--border)" : border}`, | |
| 312 | + color: disabled ? "var(--text-faint)" : color, | |
| 313 | + cursor: disabled ? "not-allowed" : "pointer", | |
| 314 | + fontSize: 12, | |
| 315 | + fontWeight: primary || danger || success ? 500 : 400, | |
| 316 | + borderRadius: 2, | |
| 317 | + whiteSpace: "nowrap", | |
| 318 | + fontFamily: "inherit", | |
| 319 | + }} | |
| 320 | + > | |
| 321 | + {icon ? <span style={{ display: "inline-flex" }}>{icon}</span> : null} | |
| 322 | + {children} | |
| 323 | + </button> | |
| 324 | + ); | |
| 325 | +} | ... | ... |
frontend/src/pages/usr/UserDetail.tsx
| 1 | 1 | import { useEffect, useState } from "react"; |
| 2 | -import { Form, Input, Select, Button, App as AntApp } from "antd"; | |
| 2 | +import { App as AntApp } from "antd"; | |
| 3 | 3 | import { |
| 4 | 4 | PlusOutlined, |
| 5 | 5 | EditOutlined, |
| 6 | 6 | DeleteOutlined, |
| 7 | 7 | SaveOutlined, |
| 8 | 8 | CloseOutlined, |
| 9 | + AppstoreOutlined, | |
| 10 | + FileExcelOutlined, | |
| 9 | 11 | KeyOutlined, |
| 12 | + RollbackOutlined, | |
| 13 | + SettingOutlined, | |
| 10 | 14 | } from "@ant-design/icons"; |
| 15 | +import { | |
| 16 | + Field, | |
| 17 | + PrimInput, | |
| 18 | + PrimSelect, | |
| 19 | + PrimCheckbox, | |
| 20 | + ToolbarBtnDark, | |
| 21 | +} from "@/components/Primitives"; | |
| 11 | 22 | import { createUser, updateUser, type UserDTO } from "@/api/user"; |
| 12 | -import { USER_TYPES, LANGUAGE_OPTIONS } from "@/utils/data"; | |
| 23 | +import { | |
| 24 | + USER_TYPES, | |
| 25 | + LANGUAGE_OPTIONS, | |
| 26 | + PERMISSION_GROUPS, | |
| 27 | + DEPARTMENTS, | |
| 28 | + SCOPE_ITEMS, | |
| 29 | +} from "@/utils/data"; | |
| 13 | 30 | import { useAppDispatch, useAppSelector } from "@/store"; |
| 14 | 31 | import { closeTab, setActiveTab } from "@/store/tabsSlice"; |
| 15 | 32 | import { fmtDateTime } from "@/utils/format"; |
| ... | ... | @@ -21,18 +38,26 @@ interface Props { |
| 21 | 38 | mode: Mode; |
| 22 | 39 | } |
| 23 | 40 | |
| 24 | -interface FormShape { | |
| 41 | +interface FormState { | |
| 25 | 42 | sUserNo: string; |
| 26 | 43 | sUserName: string; |
| 27 | - staffName?: string; | |
| 28 | - department?: string; | |
| 44 | + staffName: string; | |
| 45 | + department: string; | |
| 29 | 46 | sUserType: string; |
| 30 | 47 | sLanguage: string; |
| 31 | - bCanModifyDocs: "全部允许" | "仅本人单据" | "仅查看"; | |
| 48 | + bCanModifyDocs: boolean; | |
| 32 | 49 | } |
| 33 | 50 | |
| 51 | +const PERM_TABS = [ | |
| 52 | + { id: "groups", label: "权限组" }, | |
| 53 | + { id: "customer", label: "客户查看权限" }, | |
| 54 | + { id: "supplier", label: "供应商查看权限" }, | |
| 55 | + { id: "staff", label: "人员查看权限" }, | |
| 56 | + { id: "process", label: "工序查看权限" }, | |
| 57 | + { id: "driver", label: "司机查看权限" }, | |
| 58 | +]; | |
| 59 | + | |
| 34 | 60 | export default function UserDetail({ userId, mode: initialMode }: Props) { |
| 35 | - const [form] = Form.useForm<FormShape>(); | |
| 36 | 61 | const [mode, setMode] = useState<Mode>(initialMode); |
| 37 | 62 | const [submitting, setSubmitting] = useState(false); |
| 38 | 63 | const dispatch = useAppDispatch(); |
| ... | ... | @@ -54,56 +79,74 @@ export default function UserDetail({ userId, mode: initialMode }: Props) { |
| 54 | 79 | } |
| 55 | 80 | | undefined; |
| 56 | 81 | |
| 82 | + const initialForm: FormState = | |
| 83 | + mode === "new" || !snapshot | |
| 84 | + ? { | |
| 85 | + sUserNo: "", | |
| 86 | + sUserName: "", | |
| 87 | + staffName: "", | |
| 88 | + department: "", | |
| 89 | + sUserType: "普通用户", | |
| 90 | + sLanguage: "zh", | |
| 91 | + bCanModifyDocs: false, | |
| 92 | + } | |
| 93 | + : { | |
| 94 | + sUserNo: snapshot.sUserNo, | |
| 95 | + sUserName: snapshot.sUserName, | |
| 96 | + staffName: snapshot.staffName ?? "", | |
| 97 | + department: snapshot.department ?? "", | |
| 98 | + sUserType: snapshot.sUserType, | |
| 99 | + sLanguage: snapshot.sLanguage, | |
| 100 | + bCanModifyDocs: !!snapshot.bCanModifyDocs, | |
| 101 | + }; | |
| 102 | + | |
| 103 | + const [form, setForm] = useState<FormState>(initialForm); | |
| 104 | + 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>>({}); | |
| 110 | + | |
| 57 | 111 | useEffect(() => { |
| 58 | - if (mode === "new") { | |
| 59 | - form.setFieldsValue({ | |
| 60 | - sUserNo: "", | |
| 61 | - sUserName: "", | |
| 62 | - staffName: "", | |
| 63 | - department: "", | |
| 64 | - sUserType: "普通用户", | |
| 65 | - sLanguage: "zh", | |
| 66 | - bCanModifyDocs: "仅查看", | |
| 67 | - }); | |
| 68 | - } else if (snapshot) { | |
| 69 | - form.setFieldsValue({ | |
| 70 | - sUserNo: snapshot.sUserNo, | |
| 71 | - sUserName: snapshot.sUserName, | |
| 72 | - staffName: snapshot.staffName ?? "", | |
| 73 | - department: snapshot.department ?? "", | |
| 74 | - sUserType: snapshot.sUserType, | |
| 75 | - sLanguage: snapshot.sLanguage, | |
| 76 | - bCanModifyDocs: snapshot.bCanModifyDocs ? "全部允许" : "仅查看", | |
| 77 | - }); | |
| 78 | - } | |
| 79 | - }, [snapshot, mode, form]); | |
| 112 | + setForm(initialForm); | |
| 113 | + // eslint-disable-next-line react-hooks/exhaustive-deps | |
| 114 | + }, [snapshot, mode]); | |
| 115 | + | |
| 116 | + const set = <K extends keyof FormState>(k: K, v: FormState[K]) => | |
| 117 | + setForm((s) => ({ ...s, [k]: v })); | |
| 80 | 118 | |
| 81 | 119 | const disabled = mode === "view"; |
| 120 | + const checkedCount = Object.values(permissions).filter(Boolean).length; | |
| 82 | 121 | |
| 83 | 122 | const startEdit = () => setMode("edit"); |
| 84 | 123 | const startNew = () => setMode("new"); |
| 85 | - | |
| 86 | 124 | const cancel = () => { |
| 87 | 125 | if (mode === "new") { |
| 88 | 126 | dispatch(closeTab(activeTabId)); |
| 89 | 127 | dispatch(setActiveTab("userlist")); |
| 90 | 128 | } else { |
| 129 | + setForm(initialForm); | |
| 91 | 130 | setMode("view"); |
| 92 | 131 | } |
| 93 | 132 | }; |
| 94 | 133 | |
| 95 | 134 | const save = async () => { |
| 135 | + if (!form.sUserNo.trim() || !form.sUserName.trim() || !form.sUserType || !form.sLanguage) { | |
| 136 | + message.error("请填写必填项"); | |
| 137 | + return; | |
| 138 | + } | |
| 139 | + const dto: UserDTO = { | |
| 140 | + sUserNo: form.sUserNo, | |
| 141 | + sUserName: form.sUserName, | |
| 142 | + sUserType: form.sUserType, | |
| 143 | + sLanguage: form.sLanguage, | |
| 144 | + bCanModifyDocs: form.bCanModifyDocs, | |
| 145 | + // iStaffId omitted: prototype has no staff picker. | |
| 146 | + // permissionCategoryIds omitted: prototype permissions are by name; backend wants IDs. | |
| 147 | + }; | |
| 148 | + setSubmitting(true); | |
| 96 | 149 | try { |
| 97 | - const values = await form.validateFields(); | |
| 98 | - const dto: UserDTO = { | |
| 99 | - sUserNo: values.sUserNo, | |
| 100 | - sUserName: values.sUserName, | |
| 101 | - sUserType: values.sUserType, | |
| 102 | - sLanguage: values.sLanguage, | |
| 103 | - bCanModifyDocs: values.bCanModifyDocs === "全部允许", | |
| 104 | - // iStaffId omitted: prototype has no staff picker; backend treats as null. | |
| 105 | - }; | |
| 106 | - setSubmitting(true); | |
| 107 | 150 | if (mode === "new") { |
| 108 | 151 | await createUser(dto); |
| 109 | 152 | message.success("新增成功"); |
| ... | ... | @@ -115,7 +158,7 @@ export default function UserDetail({ userId, mode: initialMode }: Props) { |
| 115 | 158 | setMode("view"); |
| 116 | 159 | } |
| 117 | 160 | } catch { |
| 118 | - // validation or interceptor | |
| 161 | + // interceptor | |
| 119 | 162 | } finally { |
| 120 | 163 | setSubmitting(false); |
| 121 | 164 | } |
| ... | ... | @@ -128,46 +171,59 @@ export default function UserDetail({ userId, mode: initialMode }: Props) { |
| 128 | 171 | flexDirection: "column", |
| 129 | 172 | height: "100%", |
| 130 | 173 | background: "var(--bg-app)", |
| 174 | + position: "relative", | |
| 131 | 175 | }} |
| 132 | 176 | > |
| 133 | - <div className="dark-toolbar"> | |
| 134 | - <Button | |
| 135 | - icon={<PlusOutlined />} | |
| 136 | - onClick={startNew} | |
| 137 | - disabled={mode !== "view"} | |
| 138 | - size="small" | |
| 139 | - > | |
| 177 | + <div | |
| 178 | + style={{ | |
| 179 | + display: "flex", | |
| 180 | + alignItems: "center", | |
| 181 | + gap: 2, | |
| 182 | + flexWrap: "wrap", | |
| 183 | + rowGap: 4, | |
| 184 | + padding: "4px 8px", | |
| 185 | + background: "var(--bg-toolbar-dark)", | |
| 186 | + flex: "none", | |
| 187 | + borderBottom: "1px solid #1a1f2a", | |
| 188 | + }} | |
| 189 | + > | |
| 190 | + <ToolbarBtnDark icon={<PlusOutlined />} onClick={startNew} disabled={mode !== "view"}> | |
| 140 | 191 | 新增 |
| 141 | - </Button> | |
| 142 | - <Button | |
| 192 | + </ToolbarBtnDark> | |
| 193 | + <ToolbarBtnDark | |
| 143 | 194 | icon={<EditOutlined />} |
| 144 | 195 | onClick={startEdit} |
| 145 | 196 | disabled={mode !== "view" || !userId} |
| 146 | - size="small" | |
| 147 | 197 | > |
| 148 | 198 | 修改 |
| 149 | - </Button> | |
| 150 | - <Button icon={<DeleteOutlined />} disabled={mode !== "view"} danger size="small"> | |
| 199 | + </ToolbarBtnDark> | |
| 200 | + <ToolbarBtnDark icon={<DeleteOutlined />} disabled={mode !== "view"} danger> | |
| 151 | 201 | 删除 |
| 152 | - </Button> | |
| 153 | - <Button | |
| 154 | - icon={<SaveOutlined />} | |
| 155 | - onClick={save} | |
| 156 | - disabled={mode === "view"} | |
| 157 | - loading={submitting} | |
| 158 | - type="primary" | |
| 159 | - size="small" | |
| 160 | - > | |
| 202 | + </ToolbarBtnDark> | |
| 203 | + <ToolbarBtnDark icon={<SaveOutlined />} onClick={save} disabled={mode === "view" || submitting}> | |
| 161 | 204 | 保存 |
| 162 | - </Button> | |
| 163 | - <Button icon={<CloseOutlined />} onClick={cancel} disabled={mode === "view"} size="small"> | |
| 205 | + </ToolbarBtnDark> | |
| 206 | + <ToolbarBtnDark icon={<CloseOutlined />} onClick={cancel} disabled={mode === "view"}> | |
| 164 | 207 | 取消 |
| 165 | - </Button> | |
| 166 | - <span style={{ width: 1, height: 16, background: "rgba(255,255,255,0.12)", margin: "0 4px" }} /> | |
| 167 | - <Button icon={<KeyOutlined />} disabled={mode !== "view"} size="small"> | |
| 168 | - 重置密码 | |
| 169 | - </Button> | |
| 208 | + </ToolbarBtnDark> | |
| 209 | + <span | |
| 210 | + style={{ | |
| 211 | + width: 1, | |
| 212 | + height: 16, | |
| 213 | + background: "rgba(255,255,255,0.12)", | |
| 214 | + margin: "0 4px", | |
| 215 | + }} | |
| 216 | + /> | |
| 217 | + <ToolbarBtnDark icon={<AppstoreOutlined />}>功能</ToolbarBtnDark> | |
| 218 | + <ToolbarBtnDark icon={<FileExcelOutlined />}>作废</ToolbarBtnDark> | |
| 219 | + <ToolbarBtnDark icon={<KeyOutlined />}>重置密码</ToolbarBtnDark> | |
| 220 | + <ToolbarBtnDark icon={<RollbackOutlined />}>取消作废</ToolbarBtnDark> | |
| 170 | 221 | <div style={{ flex: 1 }} /> |
| 222 | + <span style={{ fontSize: 11, color: "var(--text-on-dark-muted)", marginRight: 8 }}> | |
| 223 | + 已选权限: | |
| 224 | + <span style={{ color: "#fff", fontWeight: 500 }}>{checkedCount}</span> /{" "} | |
| 225 | + {PERMISSION_GROUPS.length - 1} | |
| 226 | + </span> | |
| 171 | 227 | <span |
| 172 | 228 | style={{ |
| 173 | 229 | padding: "2px 8px", |
| ... | ... | @@ -195,67 +251,396 @@ export default function UserDetail({ userId, mode: initialMode }: Props) { |
| 195 | 251 | > |
| 196 | 252 | {mode === "view" ? "只读" : mode === "new" ? "新增中" : "编辑中"} |
| 197 | 253 | </span> |
| 254 | + <button | |
| 255 | + style={{ | |
| 256 | + marginLeft: 8, | |
| 257 | + width: 22, | |
| 258 | + height: 22, | |
| 259 | + border: "none", | |
| 260 | + background: "transparent", | |
| 261 | + color: "var(--text-on-dark-muted)", | |
| 262 | + cursor: "pointer", | |
| 263 | + }} | |
| 264 | + > | |
| 265 | + <SettingOutlined /> | |
| 266 | + </button> | |
| 198 | 267 | </div> |
| 199 | 268 | |
| 200 | 269 | <div |
| 201 | 270 | style={{ |
| 202 | - flex: 1, | |
| 203 | - overflow: "auto", | |
| 204 | 271 | background: "#fff", |
| 205 | - padding: "12px 16px", | |
| 272 | + padding: "10px 12px", | |
| 273 | + borderBottom: "1px solid var(--border)", | |
| 274 | + flex: "none", | |
| 275 | + display: "grid", | |
| 276 | + gridTemplateColumns: "repeat(auto-fit, minmax(240px, 1fr))", | |
| 277 | + rowGap: 6, | |
| 278 | + columnGap: 16, | |
| 206 | 279 | }} |
| 207 | 280 | > |
| 208 | - <Form | |
| 209 | - form={form} | |
| 210 | - layout="vertical" | |
| 211 | - disabled={disabled} | |
| 281 | + <Field label="创建时间"> | |
| 282 | + <PrimInput | |
| 283 | + value={ | |
| 284 | + mode === "new" | |
| 285 | + ? "保存后自动生成" | |
| 286 | + : fmtDateTime(snapshot?.tCreateDate) | |
| 287 | + } | |
| 288 | + disabled | |
| 289 | + mono | |
| 290 | + /> | |
| 291 | + </Field> | |
| 292 | + <Field label="制单人" required> | |
| 293 | + <PrimInput | |
| 294 | + value={mode === "new" ? "保存后自动生成" : snapshot?.sCreatedBy ?? ""} | |
| 295 | + disabled | |
| 296 | + required | |
| 297 | + /> | |
| 298 | + </Field> | |
| 299 | + <Field label="员工名" required> | |
| 300 | + <PrimInput | |
| 301 | + value={form.staffName} | |
| 302 | + onChange={(v) => set("staffName", v)} | |
| 303 | + disabled={disabled} | |
| 304 | + required | |
| 305 | + placeholder="(关联职员)" | |
| 306 | + /> | |
| 307 | + </Field> | |
| 308 | + | |
| 309 | + <Field label="用户名" required> | |
| 310 | + <PrimInput | |
| 311 | + value={form.sUserName} | |
| 312 | + onChange={(v) => set("sUserName", v)} | |
| 313 | + disabled={disabled} | |
| 314 | + required | |
| 315 | + /> | |
| 316 | + </Field> | |
| 317 | + <Field label="类型" required> | |
| 318 | + <PrimSelect | |
| 319 | + value={form.sUserType} | |
| 320 | + onChange={(v) => set("sUserType", v)} | |
| 321 | + disabled={disabled} | |
| 322 | + required | |
| 323 | + allowEmpty={false} | |
| 324 | + options={USER_TYPES} | |
| 325 | + /> | |
| 326 | + </Field> | |
| 327 | + <Field label="用户号" required> | |
| 328 | + <PrimInput | |
| 329 | + value={form.sUserNo} | |
| 330 | + onChange={(v) => set("sUserNo", v)} | |
| 331 | + disabled={disabled} | |
| 332 | + required | |
| 333 | + mono | |
| 334 | + /> | |
| 335 | + </Field> | |
| 336 | + | |
| 337 | + <Field label="部门"> | |
| 338 | + <PrimSelect | |
| 339 | + value={form.department} | |
| 340 | + onChange={(v) => set("department", v)} | |
| 341 | + disabled={disabled} | |
| 342 | + options={DEPARTMENTS} | |
| 343 | + /> | |
| 344 | + </Field> | |
| 345 | + <Field label="语言" required> | |
| 346 | + <PrimSelect | |
| 347 | + value={form.sLanguage} | |
| 348 | + onChange={(v) => set("sLanguage", v)} | |
| 349 | + disabled={disabled} | |
| 350 | + required | |
| 351 | + allowEmpty={false} | |
| 352 | + options={LANGUAGE_OPTIONS} | |
| 353 | + /> | |
| 354 | + </Field> | |
| 355 | + <Field label="单据修改权限"> | |
| 356 | + <div | |
| 357 | + style={{ | |
| 358 | + flex: 1, | |
| 359 | + height: "var(--input-h)", | |
| 360 | + display: "flex", | |
| 361 | + alignItems: "center", | |
| 362 | + paddingLeft: 4, | |
| 363 | + border: "1px solid var(--border-input)", | |
| 364 | + background: disabled ? "var(--bg-disabled)" : "var(--bg-input)", | |
| 365 | + }} | |
| 366 | + > | |
| 367 | + <PrimCheckbox | |
| 368 | + checked={form.bCanModifyDocs} | |
| 369 | + onChange={(v) => set("bCanModifyDocs", v)} | |
| 370 | + disabled={disabled} | |
| 371 | + /> | |
| 372 | + </div> | |
| 373 | + </Field> | |
| 374 | + </div> | |
| 375 | + | |
| 376 | + <div | |
| 377 | + style={{ | |
| 378 | + display: "flex", | |
| 379 | + alignItems: "stretch", | |
| 380 | + background: "#fff", | |
| 381 | + borderBottom: "1px solid var(--border)", | |
| 382 | + flex: "none", | |
| 383 | + }} | |
| 384 | + > | |
| 385 | + {PERM_TABS.map((t) => { | |
| 386 | + const active = t.id === permTab; | |
| 387 | + return ( | |
| 388 | + <div | |
| 389 | + key={t.id} | |
| 390 | + onClick={() => setPermTab(t.id)} | |
| 391 | + style={{ | |
| 392 | + padding: "6px 14px", | |
| 393 | + fontSize: 12, | |
| 394 | + cursor: "pointer", | |
| 395 | + color: active ? "var(--accent-strong)" : "var(--text)", | |
| 396 | + background: active ? "var(--accent-soft)" : "transparent", | |
| 397 | + borderTop: active ? "2px solid var(--accent)" : "2px solid transparent", | |
| 398 | + borderRight: "1px solid var(--border)", | |
| 399 | + fontWeight: active ? 500 : 400, | |
| 400 | + }} | |
| 401 | + onMouseEnter={(e) => { | |
| 402 | + if (!active) e.currentTarget.style.background = "var(--bg-row-hover)"; | |
| 403 | + }} | |
| 404 | + onMouseLeave={(e) => { | |
| 405 | + if (!active) e.currentTarget.style.background = "transparent"; | |
| 406 | + }} | |
| 407 | + > | |
| 408 | + {t.label} | |
| 409 | + </div> | |
| 410 | + ); | |
| 411 | + })} | |
| 412 | + </div> | |
| 413 | + | |
| 414 | + <div style={{ flex: 1, overflow: "auto", background: "#fff" }}> | |
| 415 | + {permTab === "groups" ? ( | |
| 416 | + <PermissionGrid | |
| 417 | + permissions={permissions} | |
| 418 | + setPermission={(g, v) => | |
| 419 | + setPermissions((s) => ({ ...s, [g]: v })) | |
| 420 | + } | |
| 421 | + disabled={disabled} | |
| 422 | + /> | |
| 423 | + ) : ( | |
| 424 | + <ScopeTab | |
| 425 | + tabKey={permTab} | |
| 426 | + enabled={!!tabPerms[permTab]} | |
| 427 | + onToggle={(v) => setTabPerms((s) => ({ ...s, [permTab]: v }))} | |
| 428 | + disabled={disabled} | |
| 429 | + /> | |
| 430 | + )} | |
| 431 | + </div> | |
| 432 | + | |
| 433 | + <div style={{ position: "absolute", right: 0, top: "50%", transform: "translateY(-50%)" }}> | |
| 434 | + <button | |
| 212 | 435 | style={{ |
| 213 | - display: "grid", | |
| 214 | - gridTemplateColumns: "repeat(auto-fit, minmax(240px, 1fr))", | |
| 215 | - rowGap: 6, | |
| 216 | - columnGap: 16, | |
| 436 | + width: 18, | |
| 437 | + height: 40, | |
| 438 | + background: "var(--accent)", | |
| 439 | + color: "#fff", | |
| 440 | + border: "none", | |
| 441 | + borderTopLeftRadius: 4, | |
| 442 | + borderBottomLeftRadius: 4, | |
| 443 | + cursor: "pointer", | |
| 444 | + fontSize: 11, | |
| 445 | + writingMode: "vertical-rl", | |
| 217 | 446 | }} |
| 218 | 447 | > |
| 219 | - <Form.Item label="创建时间"> | |
| 220 | - <Input value={fmtDateTime(snapshot?.tCreateDate)} disabled className="mono" /> | |
| 221 | - </Form.Item> | |
| 222 | - <Form.Item label="制单人"> | |
| 223 | - <Input value={snapshot?.sCreatedBy ?? ""} disabled /> | |
| 224 | - </Form.Item> | |
| 225 | - <Form.Item | |
| 226 | - label="员工名" | |
| 227 | - name="staffName" | |
| 228 | - help="新增/编辑暂不关联职员;如需关联请使用职员管理" | |
| 229 | - > | |
| 230 | - <Input disabled /> | |
| 231 | - </Form.Item> | |
| 448 | + 帮助 | |
| 449 | + </button> | |
| 450 | + </div> | |
| 451 | + </div> | |
| 452 | + ); | |
| 453 | +} | |
| 454 | + | |
| 455 | +interface PermissionGridProps { | |
| 456 | + permissions: Record<string, boolean>; | |
| 457 | + setPermission: (g: string, v: boolean) => void; | |
| 458 | + disabled?: boolean; | |
| 459 | +} | |
| 232 | 460 | |
| 233 | - <Form.Item label="用户名" name="sUserName" rules={[{ required: true }, { max: 50 }]}> | |
| 234 | - <Input /> | |
| 235 | - </Form.Item> | |
| 236 | - <Form.Item label="类型" name="sUserType" rules={[{ required: true }]}> | |
| 237 | - <Select options={USER_TYPES.map((t) => ({ value: t, label: t }))} /> | |
| 238 | - </Form.Item> | |
| 239 | - <Form.Item label="用户号" name="sUserNo" rules={[{ required: true }, { max: 50 }]}> | |
| 240 | - <Input className="mono" /> | |
| 241 | - </Form.Item> | |
| 461 | +function PermissionGrid({ permissions, setPermission, disabled }: PermissionGridProps) { | |
| 462 | + const [filter, setFilter] = useState(""); | |
| 463 | + const [hovered, setHovered] = useState<string | null>(null); | |
| 464 | + const allChecked = PERMISSION_GROUPS.every((g) => permissions[g]); | |
| 242 | 465 | |
| 243 | - <Form.Item label="部门" name="department"> | |
| 244 | - <Input disabled /> | |
| 245 | - </Form.Item> | |
| 246 | - <Form.Item label="语言" name="sLanguage" rules={[{ required: true }]}> | |
| 247 | - <Select options={LANGUAGE_OPTIONS} /> | |
| 248 | - </Form.Item> | |
| 249 | - <Form.Item label="单据修改权限" name="bCanModifyDocs"> | |
| 250 | - <Select | |
| 251 | - options={[ | |
| 252 | - { value: "全部允许", label: "全部允许" }, | |
| 253 | - { value: "仅本人单据", label: "仅本人单据" }, | |
| 254 | - { value: "仅查看", label: "仅查看" }, | |
| 255 | - ]} | |
| 466 | + return ( | |
| 467 | + <table | |
| 468 | + style={{ | |
| 469 | + borderCollapse: "collapse", | |
| 470 | + width: "100%", | |
| 471 | + fontSize: 12, | |
| 472 | + tableLayout: "fixed", | |
| 473 | + }} | |
| 474 | + > | |
| 475 | + <colgroup> | |
| 476 | + <col style={{ width: 40 }} /> | |
| 477 | + <col /> | |
| 478 | + </colgroup> | |
| 479 | + <thead> | |
| 480 | + <tr style={{ background: "var(--bg-row-zebra)", borderBottom: "1px solid var(--border)" }}> | |
| 481 | + <th | |
| 482 | + style={{ | |
| 483 | + padding: "5px 8px", | |
| 484 | + borderRight: "1px solid var(--border)", | |
| 485 | + textAlign: "center", | |
| 486 | + }} | |
| 487 | + > | |
| 488 | + <PrimCheckbox | |
| 489 | + checked={allChecked} | |
| 490 | + onChange={(v) => { | |
| 491 | + PERMISSION_GROUPS.forEach((g) => setPermission(g, v)); | |
| 492 | + }} | |
| 493 | + disabled={disabled} | |
| 256 | 494 | /> |
| 257 | - </Form.Item> | |
| 258 | - </Form> | |
| 495 | + </th> | |
| 496 | + <th style={{ padding: "5px 8px", textAlign: "left", fontWeight: 500 }}> | |
| 497 | + <span style={{ display: "inline-flex", alignItems: "center", gap: 6 }}> | |
| 498 | + 权限分类 | |
| 499 | + <input | |
| 500 | + value={filter} | |
| 501 | + onChange={(e) => setFilter(e.target.value)} | |
| 502 | + placeholder="过滤..." | |
| 503 | + style={{ | |
| 504 | + height: 20, | |
| 505 | + padding: "0 6px", | |
| 506 | + border: "1px solid var(--border-input)", | |
| 507 | + background: "#fff", | |
| 508 | + fontSize: 11, | |
| 509 | + width: 120, | |
| 510 | + fontFamily: "inherit", | |
| 511 | + outline: "none", | |
| 512 | + }} | |
| 513 | + /> | |
| 514 | + </span> | |
| 515 | + </th> | |
| 516 | + </tr> | |
| 517 | + </thead> | |
| 518 | + <tbody> | |
| 519 | + {PERMISSION_GROUPS.filter((g) => !filter || g.includes(filter)).map((g, i) => { | |
| 520 | + const isHover = hovered === g; | |
| 521 | + const checked = !!permissions[g]; | |
| 522 | + return ( | |
| 523 | + <tr | |
| 524 | + key={g} | |
| 525 | + onMouseEnter={() => setHovered(g)} | |
| 526 | + onMouseLeave={() => setHovered(null)} | |
| 527 | + onClick={disabled ? undefined : () => setPermission(g, !checked)} | |
| 528 | + style={{ | |
| 529 | + background: | |
| 530 | + isHover && !disabled | |
| 531 | + ? "var(--bg-row-hover)" | |
| 532 | + : checked | |
| 533 | + ? "var(--accent-soft)" | |
| 534 | + : i % 2 === 0 | |
| 535 | + ? "#fff" | |
| 536 | + : "var(--bg-row-zebra)", | |
| 537 | + cursor: disabled ? "default" : "pointer", | |
| 538 | + height: 24, | |
| 539 | + }} | |
| 540 | + > | |
| 541 | + <td | |
| 542 | + style={{ | |
| 543 | + padding: "3px 0", | |
| 544 | + textAlign: "center", | |
| 545 | + borderRight: "1px solid var(--border)", | |
| 546 | + borderBottom: "1px solid #f0f2f5", | |
| 547 | + }} | |
| 548 | + > | |
| 549 | + <PrimCheckbox | |
| 550 | + checked={checked} | |
| 551 | + onChange={(v) => setPermission(g, v)} | |
| 552 | + disabled={disabled} | |
| 553 | + /> | |
| 554 | + </td> | |
| 555 | + <td | |
| 556 | + style={{ | |
| 557 | + padding: "3px 8px", | |
| 558 | + borderBottom: "1px solid #f0f2f5", | |
| 559 | + color: checked ? "var(--accent-strong)" : "var(--text)", | |
| 560 | + fontWeight: checked ? 500 : 400, | |
| 561 | + }} | |
| 562 | + > | |
| 563 | + {g} | |
| 564 | + </td> | |
| 565 | + </tr> | |
| 566 | + ); | |
| 567 | + })} | |
| 568 | + </tbody> | |
| 569 | + </table> | |
| 570 | + ); | |
| 571 | +} | |
| 572 | + | |
| 573 | +interface ScopeTabProps { | |
| 574 | + tabKey: string; | |
| 575 | + enabled: boolean; | |
| 576 | + onToggle: (v: boolean) => void; | |
| 577 | + disabled?: boolean; | |
| 578 | +} | |
| 579 | + | |
| 580 | +function ScopeTab({ tabKey, enabled, onToggle, disabled }: ScopeTabProps) { | |
| 581 | + const cfg = SCOPE_ITEMS[tabKey]; | |
| 582 | + const [filter, setFilter] = useState(""); | |
| 583 | + if (!cfg) return null; | |
| 584 | + | |
| 585 | + const filtered = cfg.items.filter((it) => !filter || it.includes(filter)); | |
| 586 | + | |
| 587 | + return ( | |
| 588 | + <div style={{ padding: 12 }}> | |
| 589 | + <div | |
| 590 | + style={{ | |
| 591 | + display: "flex", | |
| 592 | + alignItems: "center", | |
| 593 | + gap: 12, | |
| 594 | + marginBottom: 10, | |
| 595 | + paddingBottom: 8, | |
| 596 | + borderBottom: "1px solid var(--border)", | |
| 597 | + }} | |
| 598 | + > | |
| 599 | + <PrimCheckbox | |
| 600 | + checked={enabled} | |
| 601 | + onChange={onToggle} | |
| 602 | + disabled={disabled} | |
| 603 | + label={`启用${cfg.label}查看权限`} | |
| 604 | + /> | |
| 605 | + <input | |
| 606 | + value={filter} | |
| 607 | + onChange={(e) => setFilter(e.target.value)} | |
| 608 | + placeholder={`筛选${cfg.label}...`} | |
| 609 | + style={{ | |
| 610 | + height: 24, | |
| 611 | + padding: "0 8px", | |
| 612 | + border: "1px solid var(--border-input)", | |
| 613 | + background: "var(--bg-input)", | |
| 614 | + fontSize: 12, | |
| 615 | + width: 200, | |
| 616 | + fontFamily: "inherit", | |
| 617 | + outline: "none", | |
| 618 | + }} | |
| 619 | + /> | |
| 620 | + <span style={{ flex: 1 }} /> | |
| 621 | + <span style={{ fontSize: 11, color: "var(--text-muted)" }}> | |
| 622 | + 共 {cfg.items.length} 个{cfg.label} | |
| 623 | + </span> | |
| 624 | + </div> | |
| 625 | + <div style={{ display: "grid", gridTemplateColumns: "repeat(4, 1fr)", gap: 6 }}> | |
| 626 | + {filtered.map((it) => ( | |
| 627 | + <label | |
| 628 | + key={it} | |
| 629 | + style={{ | |
| 630 | + display: "flex", | |
| 631 | + alignItems: "center", | |
| 632 | + gap: 6, | |
| 633 | + padding: "5px 8px", | |
| 634 | + border: "1px solid var(--border)", | |
| 635 | + background: enabled ? "#fff" : "var(--bg-disabled)", | |
| 636 | + cursor: disabled || !enabled ? "default" : "pointer", | |
| 637 | + fontSize: 12, | |
| 638 | + }} | |
| 639 | + > | |
| 640 | + <PrimCheckbox disabled={disabled || !enabled} checked={false} /> | |
| 641 | + <span style={{ color: enabled ? "var(--text)" : "var(--text-faint)" }}>{it}</span> | |
| 642 | + </label> | |
| 643 | + ))} | |
| 259 | 644 | </div> |
| 260 | 645 | </div> |
| 261 | 646 | ); | ... | ... |
frontend/src/utils/data.ts
| ... | ... | @@ -21,6 +21,92 @@ export const COMPANIES = [ |
| 21 | 21 | // Backend USER_TYPES enum: 普通用户 | 超级管理员 |
| 22 | 22 | export const USER_TYPES = ["超级管理员", "普通用户"]; |
| 23 | 23 | |
| 24 | +// Visual-only — permission grid in UserDetail. Backend persistence (by IDs) | |
| 25 | +// not yet wired; toggling these does not round-trip. | |
| 26 | +export const PERMISSION_GROUPS = [ | |
| 27 | + "默认显示(必选)", | |
| 28 | + "禁止查看价格", | |
| 29 | + "客服跟单", | |
| 30 | + "报价组员工", | |
| 31 | + "物控部员工", | |
| 32 | + "供应链 PMC", | |
| 33 | + "允许查看订单价格", | |
| 34 | + "储运部员工", | |
| 35 | + "外部供应商", | |
| 36 | + "品质部员工", | |
| 37 | + "技术中心员工", | |
| 38 | + "机修组员工", | |
| 39 | + "生产部计划员工", | |
| 40 | + "外发组员工", | |
| 41 | + "模烫车间", | |
| 42 | + "装订车间", | |
| 43 | + "粘接工车间", | |
| 44 | + "品质部管理", | |
| 45 | + "精品车间", | |
| 46 | + "人事组", | |
| 47 | + "统计组", | |
| 48 | + "机修主管", | |
| 49 | + "样品开发部员工", | |
| 50 | + "设计开发", | |
| 51 | + "总经办", | |
| 52 | + "财务部", | |
| 53 | + "销售员", | |
| 54 | + "采购员", | |
| 55 | + "仓库管理员", | |
| 56 | +]; | |
| 57 | + | |
| 58 | +// Visual-only — 部门 dropdown options on UserDetail. | |
| 59 | +export const DEPARTMENTS = [ | |
| 60 | + "工艺技术", | |
| 61 | + "印刷车间", | |
| 62 | + "机修", | |
| 63 | + "机务部", | |
| 64 | + "财务部", | |
| 65 | + "装订车间", | |
| 66 | + "总经办公室", | |
| 67 | + "总务部", | |
| 68 | + "供应链", | |
| 69 | + "质量管理部", | |
| 70 | + "模切车间", | |
| 71 | + "计划组", | |
| 72 | + "样品开发", | |
| 73 | + "设计部", | |
| 74 | + "仓库", | |
| 75 | +]; | |
| 76 | + | |
| 77 | +// Visual-only — scope tabs in UserDetail (客户/供应商/人员/工序/司机). | |
| 78 | +export const SCOPE_ITEMS: Record<string, { label: string; items: string[] }> = { | |
| 79 | + customer: { | |
| 80 | + label: "客户", | |
| 81 | + items: [ | |
| 82 | + "上海印行包装", | |
| 83 | + "锐尚文创", | |
| 84 | + "京华彩印", | |
| 85 | + "广印纸品", | |
| 86 | + "万象图文", | |
| 87 | + "联合包装", | |
| 88 | + "鼎盛印刷", | |
| 89 | + "九洲胶印", | |
| 90 | + ], | |
| 91 | + }, | |
| 92 | + supplier: { | |
| 93 | + label: "供应商", | |
| 94 | + items: ["华东油墨", "正信纸业", "宝洁化工", "日新油墨", "三鼎纸业", "鸿丰胶辊", "永利印材"], | |
| 95 | + }, | |
| 96 | + staff: { | |
| 97 | + label: "人员", | |
| 98 | + items: ["管广飞", "李斌", "孟威", "王宽明", "潘强", "杨柳"], | |
| 99 | + }, | |
| 100 | + process: { | |
| 101 | + label: "工序", | |
| 102 | + items: ["印前", "印刷", "覆膜", "模切", "装订", "胶装", "丝网", "烫金", "包装"], | |
| 103 | + }, | |
| 104 | + driver: { | |
| 105 | + label: "司机", | |
| 106 | + items: ["陈师傅", "李师傅", "王师傅", "钱师傅", "赵师傅"], | |
| 107 | + }, | |
| 108 | +}; | |
| 109 | + | |
| 24 | 110 | // Backend LANGUAGES enum: zh | en | zh-TW. Display strings for the form. |
| 25 | 111 | export const LANGUAGE_OPTIONS: { value: string; label: string }[] = [ |
| 26 | 112 | { value: "zh", label: "中文" }, | ... | ... |