fix(pc): 找回被 #feedback 吞掉的表单校验错误 + 收集任务文案精简
naive 的 NFormItem 一旦用了 #feedback 插槽,renderExplains 就永不渲染
(children || feedback ? feedback : …),校验错误被完全遮住 —— 用户看到的是
"红框 + 一句无关的正常提示",现象上等价于"点了保存没反应"。RoleFormModal 里
早就记录过这个机制,但只在自己那个文件绕开了,没固化成约定,于是扩散到 7 处。
- 7 处 validate() 的 catch 改成 message.error(firstError(errors) ?? 兜底)
(login / org / role / user / task 建改任务 / 材料规则 / 表单设计)
- ⚠️ firstError 必须摊平后再取:naive reject 出来的是嵌套数组,
errors[0].message 恒为 undefined,兜底文案会把每条错误都吞成"请检查表单填写"
- 批次号在"修改"时置灰(task_code = 批次号 + 4 位序号,且同时是文件目录名,
而服务端 update 无条件写 batch_no、没有"编码已生成"的保护)
- 删掉「本期范围」区块(收集模式 / 认定类型恒定,请求体里仍写死 1)
- 清掉教材式描述:extras_json / extra_ 前缀 / 幂等 / 接收能力 / 识别结果只用于提示
- 规则沉淀进 frontend-pc/AGENTS.md 第 10 条与交接摘要 §一·四十
This commit is contained in:
+159
@@ -3325,5 +3325,164 @@ CI/CD 与 test/prod 环境 → 压测 → 用户手册与客服话术 → 缩略
|
||||
「页面内页签用工具条分段按钮,写明高度链的原因」;
|
||||
根 `AGENTS.md` 的接线检查说明补第 ⑩ 项。
|
||||
|
||||
## §一·四十 收集任务页面「描述」精简 + 一处长期静默的故障(2026-10)
|
||||
|
||||
**起因**:用户要求「收集任务页面表单描述要简洁直观」,并点名
|
||||
「新建收集任务」里的 **本期范围** 区块(`本期只做 学校收集 + 自主认定:收集模式与认定类型由系统固定,界面上不开放其它取值。`)
|
||||
不该出现在表单里;同时要求审查收集任务的全部页面。
|
||||
|
||||
### 一、删掉「本期范围」区块(决定 + 理由)
|
||||
|
||||
`TaskFormModal.vue` 里删掉了 **`NDivider` + 两个 `disabled` 的 `NInput`(收集模式 / 认定类型)+ 一整块 `NAlert`**,
|
||||
原位只留一段注释。三个理由,缺一个都会被后来者"改回去":
|
||||
|
||||
1. 它的信息是**恒定的**(`collectMode=1` / `certType=1` 由后端 `TaskService.assertScopeSupported` 强制),
|
||||
对建任务的人零决策价值;
|
||||
2. 它是 **PRD 口径**,不是界面文案 —— 真正该放的地方是 `docs/01-需求规格说明书`;
|
||||
3. ⚠️ 保持它只会**暗示系统支持切换模式**,而范围收敛已经定了不设二期。
|
||||
|
||||
⚠️ **请求体里那两行写死不能跟着删**:`confirm()` 仍必须带 `collectMode: 1` / `certType: 1`
|
||||
(`TaskService.java:323-332` 的 `assertScopeSupported` 对 `create`(:248) 与 `update`(:351) 都调,
|
||||
传别的值返回 **40044**)。这是本轮最容易"顺手删错"的地方。
|
||||
|
||||
### 二、本轮最要紧的发现:`#feedback` 会**完全遮住**校验错误(长期静默故障)
|
||||
|
||||
**机制**(naive **2.45.3**,`frontend-pc/node_modules/naive-ui/es/form/src/FormItem.mjs:416`):
|
||||
|
||||
```js
|
||||
const feedbackNodes = children || feedback ? (…) : this.renderExplains.length ? this.renderExplains.map(…) : null
|
||||
```
|
||||
|
||||
`#feedback` 插槽存在时 `renderExplains` **永不渲染** —— 而 `renderExplains` 就是校验错误所在。
|
||||
所以一个字段只要写了提示语,它的**必填/格式错误就一个字都显示不出来**,
|
||||
用户看到的是「红框 + 一句无关的正常提示」→ 现象上等价于「点了保存没反应」。
|
||||
|
||||
**为什么以前没被发现**:写出这句话的人(`RoleFormModal.vue:90-97` 的注释)已经知道这个机制,
|
||||
但**只在自己那个文件里绕开了**,没有固化成约定,也没有人扫过全仓 → 另外 7 处继续踩。
|
||||
|
||||
**⚠️ 并且这个插槽不接收任何参数**(同文件 `:412` 用的是 `_utils/vue/resolve-slot.mjs:23-25` 的
|
||||
`resolveWrappedSlot(slot, wrapper)`,即 `wrapper(slot())` —— **零参数**):
|
||||
所以**不存在**「`#feedback="{ status }"` 里顺手把错误渲染出来」这种优雅修法。
|
||||
想内联渲染错误只有一条路:给 `NFormItem` 的 **`feedback` prop 传渲染函数**(`FormItem.mjs:420-425` 的分支),
|
||||
那要自写包装组件 + `h()` 手绘插槽 + 自行转发 `label`/`path`/`validationStatus`,
|
||||
而本仓**没有任何 `.tsx` 文件**。→ 列为独立一批(见末节「显式不做⑤」)。
|
||||
|
||||
**本轮的修法(7 处,与 `TenantEditModal.vue:119-139` 的既有写法同形)**:
|
||||
`catch (errors) { message.error(firstError(errors) ?? '请检查表单填写'); return }`
|
||||
|
||||
| 文件 | 行 | 该表单里被遮住的错误 |
|
||||
|---|---|---|
|
||||
| `frontend-pc/src/views/login/index.vue` | `firstError` :127、`catch` :142 | 「租户码」必填(`#feedback` 那句常驻提示) |
|
||||
| `frontend-pc/src/views/task/TaskFormModal.vue` | `firstError` :205、`catch` :225 | 批次号格式、填报截止必填、命名模板必填 |
|
||||
| `frontend-pc/src/views/task/TaskMaterialDrawer.vue` | `firstError` :369、`catch` :380 | 材料类型 / 格式 / 体积 / 份数 |
|
||||
| `frontend-pc/src/views/task/TaskFormSchemaDrawer.vue` | `firstError` :437、`catch` :447 | 字段标识格式(`extra_` 前缀)与重名 |
|
||||
| `frontend-pc/src/views/user/UserFormModal.vue` | `firstError` :200、`catch` :210 | 手机号 / 角色类 |
|
||||
| `frontend-pc/src/views/org/OrgFormModal.vue` | `firstError` :124、`catch` :134 | 组织类型必填 |
|
||||
| `frontend-pc/src/views/role/RoleFormModal.vue` | `firstError` :135、`catch` :145 | 角色码必填与格式、用户类型 `type:'number'` 那个坑 |
|
||||
|
||||
⚠️ **`firstError` 必须摊平后再取**:naive 的 `Form.validate()` reject 出来的是**嵌套数组**
|
||||
(`Form.mjs:96-107` 把每一层的 `result.errors` 整个 `push` 进 `errors`),
|
||||
所以 `errors[0].message` **恒为 `undefined`**,`?? 兜底` 会把每条错误都变成「请检查表单填写」。
|
||||
实测踩到过:第一版就是 `errors[0].message`,toast 出来的是兜底文案。
|
||||
现在 7 份实现都是 `for (const entry of errors.flat(2)) { … }`。
|
||||
|
||||
**实测证据**(真浏览器,后端 8080 + `pnpm dev` 5173):
|
||||
|
||||
| 触发 | 弹出 |
|
||||
|---|---|
|
||||
| 登录页留空点登录 | `请填写账号` |
|
||||
| 新建任务留空点「创建任务」 | `请填写任务名称` |
|
||||
| 新增扩展字段填 `abc` 点确定 | `必须以 extra_ 开头,且只含小写字母、数字、下划线` |
|
||||
| 角色弹窗留空点「创建」 | `请填写角色码`(**回归**,它本来就有这写法) |
|
||||
|
||||
### 三、批次号在「修改」时锁死(**用户本轮拍板**)
|
||||
|
||||
`TaskFormModal.vue:276-291`:编辑时 `:disabled="isEdit"`,feedback 出 `batchNoHint`(`:152-158`),
|
||||
即「任务编码 **2026090241** 已按它生成,不可再改」。
|
||||
|
||||
依据与决策一起记:`task_code` = 批次号 + 4 位序号(`TaskService.nextTaskCode`,`:757-768`),
|
||||
且它**同时是文件目录名**;而服务端 `TaskService.update:365-366` 会**无条件**写入 `batch_no`,
|
||||
没有"编码已生成"的保护 —— 改了以后列表里的批次号与 `task_code` 前缀就永久对不上,且没有任何提示。
|
||||
|
||||
⚠️ **这个缝隙没有任何测试覆盖**:既有 e2e 改草稿时传的是**同一个** `batchNo`
|
||||
(`scripts/e2e-m1-task.py:293-299`),只测过「已发布不可改 `collectMode`」(`:378-384`)。
|
||||
所以这次是**由前端守住**,不是修了后端。只在界面上只读,服务端该字段仍可写。
|
||||
|
||||
新建态(不是编辑)保持可填,提示改成了「4~8 位数字。任务编码 = 批次号 + 4 位序号(如 202609 → 2026090001)」——
|
||||
它解释的是**用户填这一格时会产生的疑问**,而不是描述实现;
|
||||
`4~8 位数字` 与后端 `TaskUpsertReq.java:36` 的 `@Pattern("^\\d{4,8}$")` 逐字一致,不要改。
|
||||
|
||||
### 四、文案改动清单(改前 → 改后)
|
||||
|
||||
| 位置 | 改前 | 改后 |
|
||||
|---|---|---|
|
||||
| `TaskFormModal.vue` 填报截止 | 只选日期时按当天 23:59:59 处理 | 选到日即可,系统按当天 23:59 计算 |
|
||||
| 同上 预览标题 | `预览(以「张同学 / … / 身份证」为例)` | 标题只留「文件名预览」,样例挪到正文(`.task-form__preview-note`) |
|
||||
| 同上 弹窗标题 | 修改任务 | 修改收集任务 |
|
||||
| 同上 保存按钮 | 保存(两态同一文案) | 创建任务 / 保存修改 |
|
||||
| 同上 变量行标签 | 插入变量(12px) | 追加变量(11px;`appendVariable` 本来就是追加到末尾,不是插入到光标) |
|
||||
| 同上 命名模板提示 | 必须含 … —— 缺了任一项,文件名就无法唯一归属到人 | 必须含 …。用下面的按钮追加变量 |
|
||||
| `TaskFormSchemaDrawer.vue` 分区标题 | 内置字段(官方 21 项,不可删除) | 内置字段 |
|
||||
| 同上 内置字段提示 | 这些是认定机构要的官方字段。**不想要某一项时不要删**(也删不掉),把它的「条件显示」设为「不收集」即可。… | 不想收的字段不用删(也删不掉):把它的「条件显示」设为「不收集」。… |
|
||||
| 同上 扩展字段工具行 | `扩展字段的值落在 extras_json,key 必须带 extra_ 前缀 —— 前缀是防重名的护栏…` | **整条删除**(理由留在代码注释里) |
|
||||
| 同上 字段标识 label / 提示 | 字段标识(key)/ 必须以 extra_ 开头,只含小写字母、数字、下划线 | 字段标识 / 只能用小写字母、数字、下划线;改动后导出列名会跟着变 |
|
||||
| 同上 删除确认 | 删除后学生端不再显示这一项。确认删除? | 删除后学生端不再显示这一项。注意:要等「保存为新版本」后才真正生效。确认删除? |
|
||||
| `TaskMaterialDrawer.vue` 体积上限提示 | 可配范围 307~5120KB(认定机构的接收能力) | 可配 307~5120 KB(0.3M~5M) |
|
||||
| 同上 模板按钮提示 | 按模板初始化是幂等的,已有的材料不会重复添加 | 按模板初始化只补缺失的材料,已有的不会重复添加 |
|
||||
| 同上 空表提示 | 必传材料一项都没有时,任务发布不了(服务端会返回 40018) | 必传材料一项都没有时,任务发布不了 |
|
||||
| 同上 别名提示 | …用于从历史文件名里认出这是哪一项材料 —— 别名不全,该认出来的就认不出来 | …用于从历史文件名里认出这是哪一项材料 |
|
||||
| 同上 来源引导语 | 提示 = placeholder 全文 | **删掉该 `#feedback`**(同一句话出现两遍,且多一个 feedback 只会让格子更高) |
|
||||
|
||||
⚠️ 两条容易改错的地方:
|
||||
|
||||
- 「字段标识」**不能**写「按字段名称自动生成」:中文没法可靠转拼音,
|
||||
`suggestExtraKey()`(`frontend-pc/src/apis/xt/form-schema.ts:187-198`)对中文标签**一律落到 `extra_field`**
|
||||
(代码注释解释了为什么不猜拼音)。实测「家长联系电话」→ `extra_field`。
|
||||
- 「批次号」的提示在锁定后**变短**了:`task_code` 已经显示了
|
||||
「任务编码 2026090241 已按它生成,不可再改」,再写一遍"同时作为任务编码前缀"是重复。
|
||||
|
||||
### 五、刻意**不**动的地方(比改了什么更容易被后来者推翻)
|
||||
|
||||
| 不动 | 理由 |
|
||||
|---|---|
|
||||
| `TaskOrgDrawer.vue` 全文 | 四句提示都对应**真实误解点**(只能勾到班级一层 / 初审人留空 = 该班负责人初审 / 应填人数留空 = 不比对 / 只读原因),改短会直接改变行为理解 |
|
||||
| `TaskRosterDrawer.vue` 全文 | 说明都是「必须解释的异常」(表头认到第 6 行、列识别结果、体检阻断 vs 警告、报考范围是聚合不是上传计划);改短会让人去改自己那份**本来正确**的 Excel |
|
||||
| 四个抽屉的 `EDITABLE = [0, 1, 4]` | 是**功能**,不是文案;且与服务端放行口径一致 |
|
||||
| `index.vue:128-134` 的 `BLOCKING_HINTS` | 发布失败时「去哪里补」的指引,与文案优化无冲突(本轮回归验证过:仍输出「请点本行的『名单管理』…」) |
|
||||
| `tenant/index.vue` 的 `catch { return }` | 该表单**没有** `#feedback`,错误本来就显示在字段下,不必多加一条 toast 约定 |
|
||||
| 只读状态的操作列「只读」 | **用户本轮明确要求保留**(`index.vue:392`、`TaskFormSchemaDrawer.vue:214`/`:341` 都还在) |
|
||||
| 不新建 `src/utils/first-error.ts` | 仓库里没有单函数 utils 的先例,`frontend-pc/AGENTS.md` 要求最小改动、禁止凭空引入新抽象;7 份同形实现各自留在组件里 |
|
||||
|
||||
### 六、验证
|
||||
|
||||
| 手段 | 结果 |
|
||||
|---|---|
|
||||
| `pnpm typecheck` | 0 错(唯一一次红是 `NAlert` 变成未使用 import,已删) |
|
||||
| `pnpm lint` | 0 错 |
|
||||
| `check-wiring.py` | ⑧「`<n-*>` 组件未 import 的 0 处」等 10 项全绿,未接线声明 0 条 |
|
||||
| `verify-docs.py` | 22/22 |
|
||||
| 真浏览器走查 | 「新建收集任务」弹窗里搜不到 `本期`/`收集模式`/`认定类型`;批次号提示与锁死、footer 两态文案、`追加变量` 标签、`文件名预览` 分栏全部符合预期 |
|
||||
| 四个抽屉逐屏读文案 | 搜不到 `extras_json` / `extra_` 前缀规则 / `幂等` / `接收能力` / `纠错的时间` / `识别结果只用于提示` / `官方 21 项` |
|
||||
| 发布阻断回归 | 空草稿点「发布」仍被拦下,横幅 +「请点本行的『名单管理』…」两处都在 |
|
||||
|
||||
**没有跑 e2e**:37 个 `scripts/e2e-*.py` 全部打 HTTP 接口,无一渲染 PC 页面;本轮**没改后端**。
|
||||
契约 119 / 错误码 54 / 原型屏数 29 全不变。前端 `/assets/**` 是 immutable 缓存 → 走查后要**硬刷新**。
|
||||
|
||||
### 七、显式不做(已记录,不混进本轮)
|
||||
|
||||
1. **克隆任务**:`POST /api/v1/tasks/{id}/clone` 后端完整可跑(`TaskController.java:169-190` + `TaskCloneService`),
|
||||
`docs/08-完成度盘点与问题清单.md` 标 P1-11「已完成」,`e2e-m5-notify-wiring.py` 还在测它
|
||||
(班主任 → `40301`、管理员 → 成功),但**前端 `apis/xt/task.ts` 里连函数都没有**,
|
||||
"宣布完成"与"界面上能用"之间的落差**没有任何检查会发现**(`check-wiring.py` 10 项里没有这一类)。
|
||||
**用户本轮明确决定先不做。**
|
||||
2. `POST /api/v1/tasks/{id}/save-as-template`(保存为模板)同样前端零调用。
|
||||
3. `TaskPublishResult.qrcodeUrl` 发布后被丢弃(`frontend-pc/src/apis/xt/task.ts:108` 定义、全仓零使用)。
|
||||
4. `formTemplateId` / `materialTemplateId` 建任务时初始化未实现(`docs/03-接口设计.md` §7.2 列了;
|
||||
材料模板现在只能建完任务后从抽屉「按模板初始化」)。
|
||||
5. **把校验错误内联渲染到字段下方**:见本文第二节 —— 插槽零参数,唯一的正路是包装组件 + `feedback` 渲染函数,
|
||||
且需先验证「`h()` 出来的元素能否收到变更后的 `validationStatus`」,未验证前不开工。
|
||||
6. `index.vue:486-487` 操作列 340px 内排 6~7 个文字动作(草稿态:编辑/配置班级/名单管理/材料规则/表单设计/发布/删除),
|
||||
收成「主操作 + 下拉」会改动行内交互范式,需单独确认。
|
||||
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user