# FE-04 用户信息单据 — 实现规格(前端) > 阶段:前端(frontend)。作用域限定 `frontend/` 下的页面 / 组件 / 路由 / store / api / 样式。 > SSoT 引用:需求卡片 `docs/01-需求清单/USR-用户管理/REQ-USR-001.md`(增加用户)+ `docs/01-需求清单/USR-用户管理/REQ-USR-002.md`(修改用户);原型 `prototype/erp.html`(`
` 区域:`.toolbar` / `.form-grid` / `.tabs-row` / `.perm-list`,布局与交互权威,含 `setUserDetailMode('new')` 新增/编辑切换脚本);API 契约 `docs/05-API接口契约.md` § REQ-USR-001(`POST /api/usr/users`)+ § REQ-USR-002(`PUT /api/usr/users/{id}`)+ § REQ-USR-003(`GET /api/usr/users`,编辑预填复用,见 § 8 D4);技术规范 `docs/04-技术规范.md` § 零(技术栈)/ § 2.1 目录约定 / § 2.2 状态管理 / § 2.3 请求封装 / § 2.4 错误处理;Design Tokens `src/styles/tokens.css`;承载外壳 FE-02 规格 `docs/superpowers/specs/2026-06-01-FE-02.md`(`AppLayout` + 标签栈「用户信息单据」+ `RequireAuth` 守卫 + 路由 `/usr/users/new` 与 `/usr/users/:id`);列表来源 FE-03 规格 `docs/superpowers/specs/2026-06-01-FE-03.md`(「新增」按钮 → `/usr/users/new`、双击行 → `/usr/users/:id`);登录态/请求基建 FE-01 规格 `docs/superpowers/specs/2026-06-01-FE-01.md`(`authSlice` + `request.ts` + token 持久化键 `xly_erp_token`,及「支撑读端点供下拉预加载」先例 `GET /api/usr/companies`)。 > 本规格只消费已锁定事实。用户名全局唯一校验、用户名格式(3-20 位字母数字下划线)、密码 BCrypt 初始化(默认 666666)、权限组授权多对多写入与全量覆盖、职员/权限 id 存在性校验、管理员权限判定、审计字段(制单人/创建时间)系统生成等业务真伪裁决全部在后端(见 REQ-USR-001 / REQ-USR-002 后端规格)。前端只负责:按新增/编辑模式渲染单据表单、预填默认值或原值、采集字段、做轻量必填/格式前置校验(减少无效请求)、提交后按响应码反馈、成功后回流列表并刷新。本 FE 同时承载**新增**(`POST`)与**修改**(`PUT`)两种写场景,共用同一单据组件按 `mode` 分支。 --- ## 1. 关联 REQ + 关联原型 | 维度 | 内容 | |---|---| | 业务功能 | FE-04 用户信息单据(新增/修改用户表单 + 权限组勾选,对接 `POST /api/usr/users` 与 `PUT /api/usr/users/{id}`) | | 关联 REQ(增) | REQ-USR-001 增加用户。表 1 输入字段:创建时间(系统生成/只读,预加载=页面加载、默认当前日期)、制单人(系统生成/只读,默认当前登录用户)、员工名(下拉单选,来源职员表,可选)、用户号(手工输入,必填,关联职员后自动带出)、用户名(手工输入,必填,关联职员后自动带出)、类型(下拉单选,普通用户/超级管理员,默认普通用户)、语言(下拉单选,中文/英文/繁体)、单据修改权限(复选框,默认否)、密码(系统生成不显示,默认 666666)。表 2 权限组:复选框 + 权限分类(页面加载预取)。输出表 1:用户号。 | | 关联 REQ(改) | REQ-USR-002 修改用户。表 1 字段同上但「预加载=页面加载时」「默认值=原值」(编辑预填);用户名作为唯一标识不可修改;密码不在本接口修改。表 2 权限组:复选框预加载原值(已授权项勾选)。输出表 1:用户 id。 | | 关联原型 | `prototype/erp.html` → `
`:`.toolbar`(新增/修改/删除/保存/取消/功能/作废/重置密码/取消作废 + 设置齿轮)、`.form-grid`(3 列表单网格:创建时间只读 / 制单人只读 / 员工名下拉 / 用户名输入 / 类型下拉 / 语言下拉 / 用户号输入 / 单据修改权限复选框)、`.tabs-row`(权限页签:权限组 / 客户查看权限 / 供应商查看权限 / 人员查看权限 / 工序查看权限 / 司机查看权限)、`.perm-list`(权限分类列表,每行复选框 + 权限分类名 + 表头排序图标);脚本 `setUserDetailMode('new')` 在新增态清空各字段、制单人显示「保存后自动生成」、清除权限勾选。 | | 路由 | 新增 `/usr/users/new`;修改 `/usr/users/:id`(React Router v6,受保护区子路由,由 FE-02 `AppLayout` + `RequireAuth` 包裹,挂载于「用户信息单据」标签的 ``)。入口:FE-03 工具栏「新增」→ `navigate('/usr/users/new')`;FE-03 双击行 → `navigate('/usr/users/'+row.id)`;FE-02 导航/常用操作的「新增用户」入口 → `/usr/users/new`。 | | 落地组件目录 | 页面 `frontend/src/pages/usr/UserDetail/`(`index.tsx` 单据容器 + 子组件 `UserDetailToolbar.tsx` / `UserBasicForm.tsx`(表单网格)/ `PermissionTabs.tsx`(权限页签条)/ `PermissionGroupList.tsx`(权限分类勾选列表));接口走 `frontend/src/api/usrApi.ts`(新增 `createUser` / `updateUser` / `getUserDetail` / `listEmployees` / `listPermissions` 方法)+ `frontend/src/api/request.ts`(FE-01 已建);类型集中 `frontend/src/api/types.ts`(`UserCreateReq` / `UserUpdateReq` / `EmployeeOption` / `PermissionItem` / 复用 `UserVO`);单据态就近用页面 hook(不进全局 store,见 § 8 D7)。 | > 原型 `#screen-userdetail` 为纯静态 HTML + 内联 demo 脚本:`.form-grid` 各字段值写死(`f-ctime`/`f-creator`/`f-empname`/`f-username`/`f-userno`/`f-type`/`f-lang`),`setUserDetailMode('new')` 仅做 DOM 文本清空,`.perm-list` 由静态 `perms` 数组渲染、复选框无真实勾选态,工具栏按钮(保存/取消等)无后端动作。本规格按 React + AntD 5 复刻其**布局与交互语义**,但表单数据、下拉数据源、权限勾选、保存提交改为真实对接后端。原型 `.tabs-row` 中除「权限组」外的 5 个查看权限页签(客户/供应商/人员/工序/司机)在 docs/01 REQ-USR-001/002 卡片与 docs/05 契约中**均无对应字段或端点**,按占位处理(见 § 8 D9);工具栏「删除/作废/重置密码/取消作废/功能」同样无对应 REQ/端点,按占位处理(见 § 8 D8)。 --- ## 2. 组件树(按区域分块,推导自 prototype DOM) 页面根 `UserDetailPage`(路由 `/usr/users/new` 与 `/usr/users/:id` 共用,渲染于 FE-02 `AppLayout` 的 `` 内),纵向结构对应原型 `#screen-userdetail`(工具栏 / 表单网格 / 权限页签条 / 权限分类列表): ``` UserDetailPage(单据容器,对应 #screen-userdetail;按路由判定 mode:有 :id → 'edit',/new → 'create') ├── UserDetailToolbar(对应 .toolbar:深色工具条) │ ├── BtnSave(对应「保存」:校验通过后提交 create/update,见 § 5 BR12) │ ├── BtnCancel(对应「取消」:放弃编辑回到列表 /usr/users,见 § 5 BR13) │ ├── BtnNew(对应「新增」:在单据内切到 create 模式 / 跳 /usr/users/new,见 § 5 BR14) │ ├── BtnDelete / BtnVoid / BtnResetPwd / BtnUnvoid / BtnFunc(对应「删除/作废/重置密码/取消作废/功能」:占位,无后端端点,见 § 8 D8) │ └── GearSetting(对应 .gear「⚙」:偏好设置占位,见 § 8 D8) ├── UserBasicForm(对应 .form-grid:3 列表单网格,AntD Form) │ ├── FieldCreateTime(对应 #f-ctime .field.readonly:创建时间,只读;create 态空/「保存后自动生成」,edit 态回填原值,见 § 5 BR1) │ ├── FieldCreator(对应 #f-creator .field.readonly:制单人,只读;create 态「保存后自动生成」,edit 态回填原值 sCreator,见 § 5 BR2) │ ├── FieldEmployee(对应 #f-empname .field.with-caret:员工名下拉单选,options 来自 GET /api/usr/employees,可空,选中联动带出用户号/用户名,见 § 5 BR5) │ ├── FieldUserName(对应 #f-username input:用户名,必填;create 可编辑,edit 只读不可改(唯一标识),见 § 5 BR3) │ ├── FieldUserType(对应 #f-type .field.with-caret:类型下拉单选,普通用户/超级管理员,create 默认普通用户,见 § 5 BR6) │ ├── FieldLanguage(对应 #f-lang .field.with-caret:语言下拉单选,中文/英文/繁体,见 § 5 BR7) │ ├── FieldUserNo(对应 #f-userno input:用户号,必填,关联职员后可自动带出,见 § 5 BR4/BR5) │ └── FieldCanModifyBill(对应 .form-cell .cb「单据修改权限」:复选框,默认否,见 § 5 BR8) │ (密码字段不在 UI 呈现:create 由后端默认 666666 初始化,edit 不改密码;对应卡片「密码=系统生成/不显示」,见 § 5 BR9) ├── PermissionTabs(对应 .tabs-row:权限页签条) │ ├── TabPermGroup(对应「权限组」active 页签:本 FE 唯一有数据的页签,承载权限分类勾选) │ └── TabPlaceholder[](对应「客户查看权限/供应商查看权限/人员查看权限/工序查看权限/司机查看权限」:占位页签,无 REQ/端点,见 § 8 D9) └── PermissionGroupList(对应 .perm-list:权限分类勾选列表) ├── HeadRow(对应 .perm-row.head:表头「☐ 权限分类 ⇅」,含全选复选框 + 排序图标占位) └── PermRow[](对应 .perm-row:每行复选框 + 权限分类名;数据来自 GET /api/usr/permissions;勾选集合 → 提交 permissionIds,见 § 5 BR10/BR11) ``` - 控件选型(依据 `docs/04 § 零` `frontend.ui_lib = Ant Design 5.x`): - 工具栏 → 自定义深色 `div` 条 + AntD `Button`(`type="text"`,白字图标按钮,复刻原型 `.tb-btn`),「保存」`type="primary"`(或主操作高亮)配 `SaveOutlined`、「新增」`PlusCircleOutlined`、「取消」`CloseCircleOutlined`、设置 `SettingOutlined`;占位按钮(删除/作废/重置密码/取消作废/功能)渲染但 `disabled` 或点击 `message.info("功能开发中")`(见 § 8 D8)。 - 表单网格 → AntD `Form`(`layout` 自定义为 3 列网格,复刻原型 `.form-grid grid-template-columns:repeat(3,1fr)`),各字段为 `Form.Item`;必填项标 `required`(红色 `*` 复刻原型 `.lbl.req::before`)。 - 员工名 / 类型 / 语言 → `Select`(单选);员工名 options 来自 `GET /api/usr/employees`,类型/语言 options 为固定枚举(见 § 5 BR6/BR7,不杜撰端点)。 - 用户名 / 用户号 → `Input`(用户名 edit 态 `disabled`,见 BR3)。 - 单据修改权限 → `Checkbox`。 - 创建时间 / 制单人 → 只读展示(`Input disabled` 或纯文本 `div`,复刻原型 `.field.readonly`)。 - 权限页签 → AntD `Tabs`(仅「权限组」页签有内容,其余占位页签 `disabled` 或空态,见 § 8 D9)。 - 权限分类列表 → AntD `Checkbox.Group` + 列表行(复刻原型 `.perm-list .perm-row`),表头「全选」用 `Checkbox`(`indeterminate` 表达半选);数据来自 `GET /api/usr/permissions`,勾选集合即提交的 `permissionIds`。 - 页面在受保护区内渲染(无 token 由 FE-02 `RequireAuth` 拦截重定向,本页不重复守卫)。顶栏 / 标签栈属 FE-02,本页只提供内容区;保存成功/取消后由本页 `navigate('/usr/users')` 回流列表(标签联动属 FE-02,见 § 8 D6)。 --- ## 3. 页面状态机(≥5 态) 单据态以「初始化加载 / 正常编辑 / 表单提交中 / 提交失败 / 提交成功 / 预填或下拉取数失败」表达。`mode` 由路由判定(`/usr/users/new` → `create`,`/usr/users/:id` → `edit`);页面 hook 持有 `{ mode, formValues, employees, permissions, checkedPermissionIds, loading, submitting, error }`: | 状态 | 触发时机 | UI 表现 | |---|---|---| | `initialLoading`(初始化加载中 / loading) | 页面挂载即预取下拉数据:`GET /api/usr/employees`(员工名)+ `GET /api/usr/permissions`(权限分类);edit 态额外取该用户详情(见 § 8 D4)。对应卡片「预加载=页面加载时」 | 单据区 AntD `Spin` 遮罩或骨架;表单字段禁用直至数据就绪;工具栏「保存」禁用 | | `editing`(正常编辑 / 正常态) | 下拉数据就绪、(edit 态)原值回填完成、表单未提交 | 表单字段按 mode 可编辑(create 全部可编辑+默认值预填;edit 用户名只读、其余回填原值可改);权限列表渲染并按已授权回勾(edit);「保存」可点 | | `submitting`(表单提交中 / 提交中) | 点击「保存」且前置校验通过,调 `POST`(create)/ `PUT`(edit)期间 | 「保存」按钮置 `loading` 并禁用(防重复提交,见 § 5 BR15);表单整体置 `disabled`/遮罩;其余工具栏按钮禁用 | | `submitError`(提交失败) | 接口返回非 0 `code`(40001/40901/40301/40401)或网络/超时/5xx | 按 § 4 错误码表在表单内(就近字段)或全局 `message.error` 展示文案;用户名冲突(40901)就近高亮用户名字段(docs/04 § 2.4「表单提交错误就近在表单展示」);保留已填值可修正重试;按钮恢复可点 | | `submitSuccess`(提交成功) | 接口返回 `code=0`,返回 `data.id` | `message.success`(create:「用户创建成功」/ edit:「保存成功」);按 BR16 回流:`navigate('/usr/users')` 回到列表并触发列表刷新(标签联动属 FE-02);或留在单据切到 edit 态(取舍见 § 8 D6) | | `loadError`(预填/下拉取数失败) | `GET /api/usr/employees`、`GET /api/usr/permissions` 或 edit 详情取数失败(非 0 / 网络异常) | 对应区域显错误占位 + 「加载失败,点击重试」入口(员工/权限下拉空 + 重试;edit 详情失败给整页重试或返回列表);`message.error` 文案见 § 4;被动 401 由 `request.ts` 拦截器统一跳 `/login`(docs/04 § 2.4) | > 状态以页面本地 hook(`useState`/自定义 `useUserDetail`)表达:`{ mode, formValues, employees, permissions, checkedPermissionIds, loading, submitting, error }`;不进全局 `store`(单据态为页面就近态,docs/04 § 2.2「跨页面共享的才进 store」,见 § 8 D7)。`authResolving`/`unauthenticated` 由 FE-02 守卫承担,本页不重复建态。 --- ## 4. 消费的后端端点(对齐 docs/05) | 端点 | 方法 | 触发时机 | 请求参数 | 成功响应 | 失败处理 | |---|---|---|---|---|---| | `/api/usr/users` | POST | create 态点击「保存」且校验通过 | JSON body `{ sUserName, sUserNo?, iEmployeeId?, sUserType, sLanguage, iCanModifyBill?(0/1), permissionIds?(number[]), initialPassword?(默认666666,前端不显式传,由后端默认) }`(对齐 docs/05 § REQ-USR-001) | `Result<{ id:number }>`,`code=0`,返回新建用户主键 id | 非 0 `code` 按错误码表反馈;用户名冲突就近高亮 | | `/api/usr/users/{id}` | PUT | edit 态点击「保存」且校验通过 | 路径 `id`;JSON body `{ sUserNo?, iEmployeeId?, sUserType, sLanguage, iCanModifyBill?(0/1), iIsVoid?(0/1), permissionIds?(number[],全量覆盖语义) }`(sUserName 不可改、密码不在本接口改;对齐 docs/05 § REQ-USR-002) | `Result<{ id:number }>`,`code=0`,返回被修改用户 id | 非 0 `code` 按错误码表反馈 | | `/api/usr/users` | GET | edit 态预填该用户原值(复用 REQ-USR-003 列表查询,按用户名/号精确匹配定位单条,见 § 8 D4) | query `{ queryField, matchType:等于, queryValue, pageNum:1, pageSize:1 }`(对齐 docs/05 § REQ-USR-003) | `Result>`,取 `records[0]` 作为原值(`UserVO` 不含密码) | 取数失败 → 整页重试或返回列表 + `message.error` | | `/api/usr/employees` | GET | 页面挂载(员工名下拉预加载) | 无参(或可选关键字,MVP 无参全量) | `Result>`,`code=0`(映射为 `EmployeeOption{ value=iIncrement, label=sEmployeeName }`) | 取数失败 → 员工 `Select` 空 + 重试入口 + `message.error("员工列表加载失败")`(端点来源见 § 8 D1) | | `/api/usr/permissions` | GET | 页面挂载(权限组列表预加载) | 无参(MVP 全量) | `Result>`,`code=0`(映射为 `PermissionItem{ id=iIncrement, name=sPermissionName, category=sPermissionCategory }`) | 取数失败 → 权限列表空 + 重试入口 + `message.error("权限列表加载失败")`(端点来源见 § 8 D2) | 请求 / 响应约定(依据 `docs/04 § 2.3 / § 2.4`): - 统一走 `frontend/src/api/request.ts` 的 Axios 实例(FE-01 已建:`baseURL=/api`,开发期经 Vite proxy 转发到后端端口 `config-vars.yaml backend.http_port=5172`;请求拦截器注入 `Authorization: Bearer `,token 取 localStorage 键 `xly_erp_token`)。 - 响应拦截器统一拆 `Result`:`code=0` 取 `data`;非 0 `code` 抛业务错误(默认弹 `message.error`,本页对可就近展示的字段错误覆盖为表单内提示);`401` 统一跳 `/login`(docs/04 § 2.4)。本页方法集中在 `usrApi.ts`(`createUser` / `updateUser` / `getUserDetail` / `listEmployees` / `listPermissions`),页面不直接散用 axios(docs/04 § 2.3)。 #### 错误码表(对齐 docs/05 § REQ-USR-001 / § REQ-USR-002) | code | 含义 | 前端文案 | 处理 | |---|---|---|---| | `0` | 成功 | —(`message.success` + 回流列表) | create:「用户创建成功」/ edit:「保存成功」 | | `40001` | 参数校验失败(字段格式/必填/枚举越界/关联 id 不存在) | 「提交信息有误,请检查后重试」(能定位到具体字段时就近提示,如用户名格式「用户名须为 3-20 位字母数字下划线」) | `message.error` + 就近字段高亮;保留已填值,正常情况下前端前置校验已拦截,此为兜底 | | `40901` | 用户名已存在(sUserName 全局唯一冲突,仅 create) | 「用户名已存在,请更换」 | 用户名字段就近 `Form.Item` 报错高亮 + `message.error`;聚焦用户名框 | | `40401` | 用户不存在(id 无对应记录,仅 edit) | 「该用户不存在或已被删除」 | `message.error` + 提供「返回列表」入口(数据已不在) | | `40301` | 无权限(非管理员调用) | 「无权限执行此操作」 | `message.error`;正常情况下入口仅对管理员开放,此为后端兜底 | | 网络/超时/5xx | 请求异常 | 「保存失败,请稍后重试」 | 响应拦截器兜底 `message.error`;按钮恢复可点 | | `401` | 登录失效(被动) | 「登录已失效,请重新登录」 | 由 `request.ts` 拦截器统一跳 `/login`(docs/04 § 2.4),本页不单独处理 | > create 态可能命中 40001/40901/40301;edit 态可能命中 40001/40401/40301(无 40901,用户名不可改)。前端按 mode 仅订阅相关码,未列码归入「保存失败」兜底。 --- ## 5. 业务规则前端复刻清单(逐条) | # | 规则 | 触发时机 | 前端报错 / 反馈文案 | 来源 | |---|---|---|---|---| | BR1 | 创建时间为系统生成、只读;create 态显示空/「保存后自动生成」,edit 态回填原值 `tCreateDate`,前端均不提交该字段 | 渲染单据 | —(只读展示,不参与提交) | REQ-USR-001/002 表 1「创建时间=系统生成/只读,保存后自动生成」;原型 `#f-ctime .readonly` + `setUserDetailMode('new')` 置空 | | BR2 | 制单人为系统生成、只读;create 态显示「保存后自动生成」,edit 态回填原值 `sCreator`,前端不提交 | 渲染单据 | —(只读展示,不提交) | REQ-USR-001/002 表 1「制单人=系统生成/只读,默认当前登录用户/原值」;原型 `#f-creator` + 新增态文本「保存后自动生成」 | | BR3 | 用户名必填;create 态可手工输入(前端前置校验 3-20 位字母数字下划线),edit 态只读不可改(唯一标识) | create 输入 / edit 渲染 | create 格式不符:「用户名须为 3-20 位字母数字下划线」;为空:「请输入用户名」 | REQ-USR-001 表 1「用户名=必填/手工输入」+ docs/05 § REQ-USR-001「3-20 位字母数字下划线,全局唯一」;REQ-USR-002 跨字段「sUserName 不可修改」 | | BR4 | 用户号必填,手工输入 | 提交校验 | 为空:「请输入用户号」 | REQ-USR-001/002 表 1「用户号=必填/手工输入」 | | BR5 | 选择「员工名」后自动带出用户号/用户名(前端联动预填,用户仍可改) | 员工名下拉选中 | —(联动填充,无报错) | REQ-USR-001/002 表 1「关联职员选择后自动输入员工姓名」;REQ-USR-001 后端规格 D3「自动带出属前端交互」 | | BR6 | 类型下拉单选,取值仅普通用户/超级管理员;create 默认「普通用户」,edit 回填原值 | 渲染下拉 / 提交校验 | 越界由 `Select` 限制(无自由输入);必填为空:「请选择类型」 | REQ-USR-001 表 1「类型=必填/下拉/默认普通用户」;REQ-USR-002「默认原值」;docs/05 枚举 {普通用户,超级管理员} | | BR7 | 语言下拉单选,取值仅中文/英文/繁体;必填 | 渲染下拉 / 提交校验 | 必填为空:「请选择语言」 | REQ-USR-001/002 表 1「语言=必填/下拉/中文,英文,繁体」 | | BR8 | 单据修改权限复选框,默认否(create)/ 原值(edit);提交为 0/1 | 渲染 / 提交 | —(布尔,无报错) | REQ-USR-001/002 表 1「单据修改权限=布尔/复选框/默认否」+ docs/05 `iCanModifyBill(0/1)` | | BR9 | 密码不在 UI 呈现:create 由后端默认 666666 初始化(前端不显式传),edit 不修改密码 | 提交 | —(前端不采集/不显示密码) | REQ-USR-001 表 1「密码=系统生成/不显示/默认 666666」;REQ-USR-002 跨字段「密码不在该接口修改」 | | BR10 | 权限组:列表项来自后端权限分类,复选框勾选集合即提交的 `permissionIds`;edit 态按已授权回勾 | 渲染权限列表 / 提交 | —(勾选集合,无报错;取数失败见 § 4 loadError) | REQ-USR-001/002 表 2「权限组复选框 + 权限分类」;docs/05 `permissionIds(number[])` | | BR11 | edit 态权限为全量覆盖语义:提交的 `permissionIds` 即该用户最终授权全集(不勾即取消授权;空集清空) | edit 保存 | —(前端提交当前勾选全集,覆盖语义由后端执行) | REQ-USR-002 后端规格 D4「permissionIds 全量覆盖」+ § 3 规则 8 | | BR12 | 点击「保存」:前置必填/格式校验通过后,create 调 `POST /api/usr/users`、edit 调 `PUT /api/usr/users/{id}` | 点击「保存」 | 校验不通过:就近字段报错不发请求;通过后按 § 4 反馈 | 原型 `.toolbar`「保存」;docs/05 § REQ-USR-001/002 | | BR13 | 点击「取消」:放弃当前编辑,回到用户列表 `/usr/users`(不提交) | 点击「取消」 | —(`navigate('/usr/users')`;有未保存改动时可二次确认,见 § 8 D5) | 原型 `.toolbar`「取消」 | | BR14 | 点击「新增」:在单据内切到 create 模式(或 `navigate('/usr/users/new')`),清空各字段、制单人显示「保存后自动生成」、清除权限勾选 | 点击「新增」 | —(复刻 `setUserDetailMode('new')`) | 原型 `.toolbar`「新增」+ `setUserDetailMode('new')` 脚本 | | BR15 | 提交进行中禁用「保存」按钮并置 loading,防重复提交 | submitting 态 | —(按钮 `loading`+`disabled`) | docs/04 § 2.4 错误处理 / 通用表单防重(登记 § 8 D5) | | BR16 | 保存成功后回流用户列表并触发列表刷新(保证新增/修改实时反映在列表,对应验收) | submitSuccess | `message.success` 后 `navigate('/usr/users')` | REQ-USR-001 验收「提交合法数据后用户记录出现在列表」;REQ-USR-002 验收「修改后立即反映在用户列表」 | | BR17 | edit 态进入时按路由 `:id` 预填该用户原值(基本字段 + 已授权权限回勾) | edit 页面挂载 | 取数失败按 § 4 loadError | REQ-USR-002 表 1「预加载=页面加载时/默认值=原值」;表 2「预加载原值」 | > 本页只做**字段采集 + 轻量前置校验 + 写提交 + 反馈回流**;用户名唯一性/格式终判、枚举与外键存在性、权限多对多写入与全量覆盖、密码 BCrypt 初始化、管理员权限、审计字段生成等真伪裁决全部由后端在 `POST /api/usr/users` / `PUT /api/usr/users/{id}` 内完成。前端前置校验仅为减少无效请求与即时反馈,不替代后端校验、不预判结果、不杜撰业务端点。 --- ## 6. 字段定义与提交映射(对齐 REQ 表 1/表 2 + docs/05 请求体) | 表单字段(中文标签,对应原型) | 提交字段(docs/05) | mode 行为 | 渲染/校验说明 | |---|---|---|---| | 创建时间 | —(不提交) | create 空/占位;edit 回填 `tCreateDate` | 只读(BR1) | | 制单人 | —(不提交) | create「保存后自动生成」;edit 回填 `sCreator` | 只读(BR2) | | 员工名 | `iEmployeeId`(可选) | create 可选空;edit 回填原值 | `Select`,options 来自 `GET /api/usr/employees`(`value=iIncrement`,`label=sEmployeeName`);选中联动带出用户号/用户名(BR5) | | 用户名 | `sUserName`(create 必传;edit 不传) | create 可编辑必填;edit 只读不可改 | `Input`,前置校验 3-20 位字母数字下划线(BR3) | | 用户号 | `sUserNo`(可选,本 UI 必填采集) | create/edit 均可编辑,可由员工名联动带出 | `Input`,前端必填(BR4),后端契约可空 | | 类型 | `sUserType`(必填) | create 默认普通用户;edit 回填原值 | `Select`,枚举 {普通用户,超级管理员}(BR6) | | 语言 | `sLanguage`(必填) | create 必选;edit 回填原值 | `Select`,枚举 {中文,英文,繁体}(BR7) | | 单据修改权限 | `iCanModifyBill`(0/1) | create 默认 0;edit 回填原值 | `Checkbox`(BR8) | | (作废,仅 edit 可选) | `iIsVoid`(0/1,仅 PUT) | edit 可改(控制启停用);create 不提交 | docs/05 § REQ-USR-002 含 `iIsVoid`;原型工具栏「作废/取消作废」语义映射到此布尔(见 § 8 D8),MVP 可作为表单内启停用开关或暂随占位按钮,登记 D8 | | 权限组勾选 | `permissionIds`(number[]) | create 勾选集;edit 全量覆盖集 | `Checkbox.Group`,项来自 `GET /api/usr/permissions`(`value=iIncrement`,`label=sPermissionName`/`sPermissionCategory`)(BR10/BR11) | | (密码) | `initialPassword`(create 可选,前端不传由后端默认 666666) | 不在 UI | 不采集(BR9) | > 输出:create 返回 `data.id`(卡片输出表 1「用户号」语义上对应新建用户主键,前端用于回流/提示);edit 返回 `data.id`(被改用户)。前端不直接展示返回 id,仅用于成功反馈与列表刷新定位。 --- ## 7. Design Tokens 引用清单(`src/styles/tokens.css`,仅 `var(--color-*)`) > 约束:组件样式只用 `var(--color-*)`,禁止硬编码 hex/rgba;色值冲突时 `tokens.css` 优先于 `prototype/`(原型内联 `:root` 变量为 demo 私有,不作色值 SSoT)。AntD 主题色经 FE-02/FE-01 已配置的 `ConfigProvider` 对齐 `--color-primary`。 | 用途 | Token | 备注 | |---|---|---| | 主操作(保存按钮 / 激活页签下划线 / 链接强调) | `var(--color-primary)` | 对应原型「保存」主操作、`.tabs-row .tb.active` 蓝色下划线;同时作为 AntD `colorPrimary` | | 页面 / 内容区基础背景 | `var(--color-bg-base)` | 单据内容区浅灰底(外壳 Outlet 内) | | 表单网格 / 权限列表背景(白) | `var(--color-form-bg-edit)` | 对应原型 `.form-grid` / `.perm-list` 白底(原型 `--panel`/`#fff`) | | 可编辑输入框背景(用户名/用户号/下拉) | `var(--color-form-bg-edit)` | 可编辑字段底(原型 `--field-bg` 映射到表单可编辑 token) | | 只读字段背景(创建时间/制单人) | `var(--color-form-bg-readonly)` | 对应原型 `.field.readonly`(原型 `--field-bg-readonly`) | | 下拉项 hover 背景 | `var(--color-form-bg-hover)` | 员工名/类型/语言 `Select` 选项 hover(tokens 注「仅下拉框使用」) | | 表单文字 / 字段值 | `var(--color-form-fg)` | 输入与展示文字 | | 权限列表表头背景 | `var(--color-table-header-bg)` | 对应原型 `.perm-row.head` 表头底(原型 `--header-bg`) | | 权限列表表头文字 | `var(--color-table-header-fg)` | 「权限分类」表头文字 | | 权限行 hover 背景 | `var(--color-table-row-bg-hover)` | 权限行悬停高亮(复刻原型行 hover 浅色) | | 通用文字 / 标签文字 | `var(--color-text)` | 字段标签、页签文字 | | 次要文字 / 占位提示 | `var(--color-text-secondary)` | 占位页签、占位按钮、空态提示、设置齿轮 | | 必填星号 / 用户名冲突等校验错误 | `var(--color-error)` | 必填 `*`(复刻原型 `.lbl.req` 红 `--label`)、`message.error`、字段就近报错强调 | | 边框 / 分隔线 / 网格线 | `var(--color-border)` | 表单网格分隔、权限行下边线、页签条下边线(原型 `--border`) | | 警告提示 | `var(--color-warning)` | 未保存改动二次确认等轻提示(D5) | | 成功提示 | `var(--color-success)` | `message.success`(创建/保存成功) | > 工具栏 `.toolbar` 的深色底(原型 `--toolbar-bg:#2c2f36`)在 `tokens.css` 中**无对应语义 token**;与 FE-02/FE-03 一致,本规格将单据工具条深色底作为**页面局部装饰样式**保留在 `UserDetail` 的 scoped 样式里(不新增全局 token、不挪用语义 token)。该深色仅承载工具条容器视觉,不承载状态语义,不违反「语义色只用 token」约束(见 § 8 D10)。原型必填标签红色 `--label:#f04848` 在 tokens 中映射到 `var(--color-error)`(语义=校验错误/必填强调),以 tokens.css 为准。 --- ## 8. 自主决策记录(decisions) | # | 问题 | 选择 | 依据 | 置信度 | |---|---|---|---|---| | D1 | 员工名下拉数据从哪个端点取(docs/05 主清单与 REQ-USR-001/002 后端规格均只列写端点 `POST`/`PUT`,未定义员工读端点) | 消费支撑只读端点 `GET /api/usr/employees`(返回 `usr_employee` 的 iIncrement/sEmployeeName/sEmployeeNo/部门),映射为员工名下拉 options | REQ-USR-001/002 表 1 明确「员工名=下拉单选/显示来源=职员表/预加载=页面加载时」,必须有读源;docs/03 `usr_employee` 定义了 sEmployeeName 且注「用户『员工名』下拉来源」;沿用 FE-01 § 8 D1 既有先例(下拉所需的支撑只读端点由对应后端 REQ 补齐,如 `GET /api/usr/companies`);不杜撰参数、仅按职员表字段定义最小读端点,留待后端实现期补契约/或后端 REQ 补齐 | medium | | D2 | 权限组列表数据从哪个端点取(docs/05 未定义权限读端点) | 消费支撑只读端点 `GET /api/usr/permissions`(返回 `usr_permission` 的 iIncrement/sPermissionName/sPermissionCategory),映射为权限分类勾选列表 | REQ-USR-001/002 表 2「权限组复选框 + 权限分类/预加载=页面加载时」必须有读源;docs/03 `usr_permission` 定义 sPermissionName/sPermissionCategory 且注「对应新增/修改用户界面『权限组』」;提交字段 `permissionIds` 需先有可选权限项;与 D1 同先例处理,不杜撰额外语义 | medium | | D3 | 「权限分类」列表是按分类聚合展示还是逐权限项展示 | MVP 按后端返回的权限项逐行展示(每行一个可勾选权限),列名「权限分类」沿用原型表头;若后端按分类聚合返回则按分类分组渲染(实现期对齐响应结构) | docs/03 `usr_permission` 注「权限粒度(按分类/按具体功能点)待确认」为 DB 文档遗留占位(标注「需用户审阅」),非本前端可消解;原型 `.perm-list` 逐行渲染 `perms` 数组(扁平项),故 MVP 取逐行展示最贴合原型;提交 `permissionIds` 为权限项 id 集合(docs/05 `permissionIds(number[])`),与逐项勾选一致 | medium | | D4 | edit 态预填用户原值从哪个端点取(docs/05 无单用户详情 GET 端点,仅列表 `GET /api/usr/users`) | 复用 REQ-USR-003 列表端点 `GET /api/usr/users`,以「等于」匹配 + pageSize=1 按 :id 对应的用户名/用户号定位取 `records[0]`;不杜撰新的 `GET /api/usr/users/{id}` 详情端点 | docs/05 仅定义列表查询端点,无单据详情端点;硬约束禁止编造后端端点;列表 `UserVO` 已含编辑所需基本字段(不含密码,正符合「密码不在编辑接口」);FE-03 双击行携带的 `id` 与列表行数据可经路由 state 传递时甚至无需重新取数(见备注)。备注:FE-03 双击进入时可经 React Router `navigate(..., {state:{user}})` 把行数据带入 edit 预填,避免二次请求;二者择一实现期定,列渲染语义不变 | medium | | D5 | 「保存」防重复提交 / 「取消」未保存改动是否二次确认 | 提交中禁用「保存」并置 loading(BR15);「取消」时若表单有未保存改动弹 AntD `Modal.confirm`「放弃未保存的修改?」,无改动直接返回 | 通用表单交互安全实践(防重复写、防误丢编辑),不与任何业务规则冲突;非硬性 REQ 要求,故登记 | medium | | D6 | 保存成功后留在单据还是回流列表 | 回流用户列表 `/usr/users` 并触发列表刷新(`message.success` 后 `navigate`),由 FE-02 标签栈联动(关闭/切回列表标签语义属 FE-02) | REQ-USR-001/002 验收均要求「记录出现/反映在用户列表」,回流列表最直接满足验收且与原型「单据隶属列表标签下」结构一致(FE-02 BR5/BR6 标签联动);留在单据需额外切 edit 态,MVP 取回流更简 | medium | | D7 | 单据态放页面本地 hook 还是全局 store | 页面本地 hook(`useUserDetail`,含 mode/formValues/employees/permissions/checkedPermissionIds/loading/submitting/error),不进全局 store | docs/04 § 2.2「服务端数据优先就近在页面用 hooks 拉取,跨页面共享的才进 store」;单据数据仅本页用,无跨页共享需求;与 FE-03 D6 一致 | high | | D8 | 工具栏「删除/作废/重置密码/取消作废/功能」按钮(原型有,但 docs/05/REQ 无对应端点)如何处理 | 占位渲染(`disabled` 或点击 `message.info("功能开发中")`),不杜撰后端端点;其中「作废/取消作废」语义对应 `iIsVoid` 0/1,MVP 可在 edit 表单内以启停用开关承载(提交 PUT 的 `iIsVoid`),其余删除/重置密码/功能纯占位 | docs/05 仅定义 create/update/list/login 端点,无删除/重置密码/作废独立端点(硬约束禁止编造);`iIsVoid` 是 PUT 已有字段(docs/05 § REQ-USR-002),故「作废」可经 PUT 实现而无需新端点;其余按钮无任何端点旁证,占位化最小侵入 | medium | | D9 | 权限页签条除「权限组」外的 5 个查看权限页签(客户/供应商/人员/工序/司机)如何处理 | 占位页签(`disabled` 或空态「功能开发中」),不实现、不取数 | REQ-USR-001/002 卡片仅定义「权限组」(表 2),其余查看权限在 docs/01/docs/03/docs/05 均无字段/表/端点;原型为静态 demo 页签(无脚本);占位化不杜撰功能 | high | | D10 | 工具栏深色底色值来源(tokens.css 无工具条深色 token) | 作为页面局部装饰样式保留在 `UserDetail` scoped 样式,不新增全局 token、不挪用语义 token;语义色严格走 token | tokens.css 仅定义语义/状态色,无工具条品牌深色 token;深色底纯装饰无状态语义,局部化最小侵入;与 FE-02/FE-03 一致 | medium | | D11 | 用户号契约可空但本 UI 是否必填 | 本 UI 按 REQ 卡片表 1「用户号=必填」前端必填采集;docs/05 标 `sUserNo(可选)` 为后端宽容接收(联动带出/为空存 null),二者不冲突 | REQ-USR-001/002 表 1 明确「用户号 必填=是」(业务期望填写);docs/05/后端 D3 标可选是后端容错(按传入值落库);前端按业务卡片必填、后端按契约兜底,取更严的前端必填满足业务期望 | medium | > 本规格不含后端实现细节(用户名唯一性/格式终判、枚举与外键存在性校验、权限多对多写入与全量覆盖、密码 BCrypt 初始化、管理员权限、审计字段生成、数据访问、库表迁移等均不在前端作用域);除按 D1/D2/D4 消费/复用支撑读端点外不杜撰任何写端点;登录态/请求基建/路由守卫/标签栈复用 FE-01 + FE-02 + FE-03 已落地资产,所有写操作真伪由后端在 `POST /api/usr/users` / `PUT /api/usr/users/{id}` 调用时裁决。D1/D2 的支撑只读端点(员工/权限下拉数据源)须在后端编码期于 REQ-USR-001/002 后端实现内补齐契约(如同 FE-01 的 `GET /api/usr/companies` 先例),否则前端下拉无数据来源——此为跨阶段待对齐项,记于此供编码阶段消化。