fix(pc): 弹窗遮罩改毛玻璃并清掉 9 处提示折行
遮罩底色被 Naive 写死在 css-render 里(Modal / Drawer 主题没有 mask token, themeOverrides 改不到),只能上 CSS;加 backdrop-filter 后遮罩后面的页面糊掉、 弹层本体仍是纯白,NModal / NDrawer / useDialog 共用 .n-modal-mask 一条规则覆盖。 折行的根因是格子宽度写死(半宽 264px / span 8 192px / 定宽 160px),与窗口宽度无关: 能压短的压短;「文件名预览」那处是 .n-form-item-blank 属于 flex 行、把提示挤成 3 行, 改成包一层块级容器;材料预览失败的占位在 108px 小格里曾折 3 行且被裁掉。 约定写进 frontend-pc/AGENTS.md 第 11 条与 code-css skill;实测与刻意不改的清单见 docs/交接摘要.md 的 §一·四十二。
This commit is contained in:
+235
-4
@@ -2470,7 +2470,7 @@ CI/CD 与 test/prod 环境 → 压测 → 用户手册与客服话术 → 缩略
|
||||
|
||||
### 四、终验与提交
|
||||
|
||||
- `verify-all --with-e2e` → 见提交信息里的最终数字(37 个 E2E 脚本全绿)。
|
||||
- `verify-all --with-e2e` → 见提交信息里的最终数字(当时 37 个 E2E 脚本全绿;**现已 38 个 / 1712 条断言**,见 §一·四十一)。
|
||||
- E2E 总数 36 → **37**(新增 `e2e-m1-task-delete.py`),断言数相应增加。
|
||||
|
||||
---
|
||||
@@ -2847,7 +2847,7 @@ CI/CD 与 test/prod 环境 → 压测 → 用户手册与客服话术 → 缩略
|
||||
| 手段 | 结果 |
|
||||
|---|---|
|
||||
| `scripts/e2e-m10-role.py`(新增) | **77/77**:改授权后**同一个 token** 立刻生效 / 停用立刻失效 / 内置角色只读 / 越权与自锁 / 删除守卫 `40045` / 内置角色权限以代码为准 + 同步回填幂等 / 跨租户 |
|
||||
| `python scripts/verify-all.py --with-e2e` | **9 项全过 / 0 失败 / 0 跳过**;**37 个脚本 / 1617 条断言 / 0 失败**(2026-10 收敛删掉 2 个脚本后已重跑填数) |
|
||||
| `python scripts/verify-all.py --with-e2e` | **9 项全过 / 0 失败 / 0 跳过**;**37 个脚本 / 1617 条断言 / 0 失败**(2026-10 收敛删掉 2 个脚本后已重跑填数;**现已 38 个 / 1712 条**,见 §一·四十一) |
|
||||
| 浏览器实测(真点) | 新建自定义角色 → 账号页角色候选出现它(且平台角色已不在候选里)→ 改权限落库(`sys_role_permission` 逐字核对)→ 删除(软删 + 授权/菜单行一并软删);截图留证 |
|
||||
| 机械校验 | `check-api-parity`(契约 **119** / 已实现 118 / 缺口 1)、`verify-docs` 22/22(原型 **29** 屏)、`check-wiring` 未接线 0、`check-entity-columns` 28 实体一致、`check-manual` 0 错 |
|
||||
| 前端 | `pnpm typecheck` 0 错、`eslint` 0 错(`views/role/**`、`apis/xt/role.ts` 与改动过的账号页) |
|
||||
@@ -2892,7 +2892,7 @@ CI/CD 与 test/prod 环境 → 压测 → 用户手册与客服话术 → 缩略
|
||||
| PC 业务菜单 | 12 | **10** |
|
||||
| 小程序页面 | 10 | **9** |
|
||||
| 原型屏 | 32 | **29**(学生 10 / 班主任 9 / 管理端 10) |
|
||||
| E2E 脚本 | 39 | **37**(删 `e2e-m2-join.py`、`e2e-m7-cleanup.py`);断言 1698 → **1617 条** |
|
||||
| E2E 脚本 | 39 | **37**(删 `e2e-m2-join.py`、`e2e-m7-cleanup.py`);断言 1698 → **1617 条**。**2026-10 又新增 `e2e-m3-flow.py` → 38 个 / 1712 条**(见 §一·四十一) |
|
||||
|
||||
> ⚠️ **两个容易算错的数**(都踩过):
|
||||
> ① **契约是 119,不是 116**。116 是"删 13 个**代码端点**"(cleanup 8 + join 5),
|
||||
@@ -3465,7 +3465,7 @@ const feedbackNodes = children || feedback ? (…) : this.renderExplains.length
|
||||
| 四个抽屉逐屏读文案 | 搜不到 `extras_json` / `extra_` 前缀规则 / `幂等` / `接收能力` / `纠错的时间` / `识别结果只用于提示` / `官方 21 项` |
|
||||
| 发布阻断回归 | 空草稿点「发布」仍被拦下,横幅 +「请点本行的『名单管理』…」两处都在 |
|
||||
|
||||
**没有跑 e2e**:37 个 `scripts/e2e-*.py` 全部打 HTTP 接口,无一渲染 PC 页面;本轮**没改后端**。
|
||||
**没有跑 e2e**:38 个 `scripts/e2e-*.py` 全部打 HTTP 接口,无一渲染 PC 页面;本轮**没改后端**。
|
||||
契约 119 / 错误码 54 / 原型屏数 29 全不变。前端 `/assets/**` 是 immutable 缓存 → 走查后要**硬刷新**。
|
||||
|
||||
### 七、显式不做(已记录,不混进本轮)
|
||||
@@ -3484,5 +3484,236 @@ const feedbackNodes = children || feedback ? (…) : this.renderExplains.length
|
||||
6. `index.vue:486-487` 操作列 340px 内排 6~7 个文字动作(草稿态:编辑/配置班级/名单管理/材料规则/表单设计/发布/删除),
|
||||
收成「主操作 + 下拉」会改动行内交互范式,需单独确认。
|
||||
|
||||
---
|
||||
|
||||
## §一·四十一 填报流程端到端测试计划(2026-10)
|
||||
|
||||
> 需求原话:**"列一个测试计划,端到端测试完整的填报流程,测试用例尽量覆盖可能的错误情况。"**
|
||||
> 落地物两份:`docs/11-填报流程端到端测试计划.md`(·覆盖矩阵与判据口径)+ `scripts/e2e-m3-flow.py`(·只补缺口的可执行脚本)。
|
||||
|
||||
### 一、为什么不能只"再写一个 E2E 脚本"
|
||||
|
||||
先做了一件比写脚本更重要的事:**核对既有 5 个 `e2e-m3-*.py` + `e2e-m5-audit.py` 到底断言了什么**——
|
||||
不是读它们的 docstring(那是自述),而是 `grep 'check(\|ok('` 逐条抄出**实际断言**。
|
||||
结论:**填报链路的 100 条左右用例里,约 55 条已被覆盖**,剩下 45 条是真空。
|
||||
于是本轮**只写这 45 条**:原计划"完整重写一遍链路"会多出约 1000 行、**零新信号**。
|
||||
|
||||
### 二、交付物
|
||||
|
||||
| # | 文件 | 说明 |
|
||||
|:--:|---|---|
|
||||
| 1 | `docs/11-填报流程端到端测试计划.md`(新建) | 覆盖矩阵 **A~J 十组**(每行都有「覆盖者」列,**空行 0**)、判据速查表、执行步骤、已定案缺陷 F-125~F-129、与《06》的映射 |
|
||||
| 2 | `scripts/e2e-m3-flow.py`(新建,1122 行) | `--group chain\|errors\|all`、`--fixture-only`、`--keep`、`--h5-url`、`--full`、`--clean <taskId>`;自建夹具 + `finally` 清理(失败路径也清) |
|
||||
| 3 | `README.md` / `docs/08` / `docs/交接摘要.md` / `scripts/verify-all.py` / `AGENTS.md` | 脚本数 37 → **38**、断言 1617 → **1712** 同步;`docs/08` 登记 F-125~F-129 |
|
||||
|
||||
### 三、覆盖矩阵的结论(本脚本 45 条)
|
||||
|
||||
| 组 | 本脚本补的缺口 |
|
||||
|---|---|
|
||||
| B 鉴权 | 无 token / 签名篡改 / `exp` 过期 → `40101` |
|
||||
| C 门禁 | 明文证件号**全文扫**(响应 + 库 + 日志);**任务已截止时门禁仍成功**(有意解耦),紧随的取表单才 `40008` |
|
||||
| D 表单 | 不存在的 taskId;任务 0/4/2/3 逐状态 `40008`(**「恢复」那一步要断言"又能用了"**,否则断言退化成"永远 40008");响应全文无明文 |
|
||||
| E 草稿 | 已提交后改 → `40030`;**已驳回后改 → `40030`(XFAIL F-125)**;越权 / 不存在的单 / 缺 taskId;暂停后改 → `40008`;扩展字段白名单 + **塞错口袋被静默丢弃(F-129)** |
|
||||
| F 材料 | 空文件、不存在的材料项、**已提交/已驳回后传删 → `40030`**、越权(含 `process` 与列表两个端点)、签名链接归属、§9.5 秒传、§9.7 超 200 → `40011`、§9.8 `prefer=original` 回退时 `actualKind` 如实告知 |
|
||||
| G 校验 | 「无」是合法值;`VAL-22` 只对「可见+必填+非锁定」;`VAL-17`;`VAL-01` 正向;`VAL-08`/`VAL-20` 黄;越权提交;暂停/截止提交 `40008`;解绑门禁 `40007`;**红色不能靠 `confirmWarnings` 绕过** |
|
||||
| H 撤回 | `status=4` 时 `progress.canEdit/canSubmit` **都是 true**(F-125 的运行期证据);**撤回 → 再提交**闭环 |
|
||||
| I 驳回 | 学生侧提示「被驳回,请修改后重新提交」+ `canFillNow=true`;**整改再提交走不通(XFAIL F-125)** |
|
||||
| J 跨步骤 | 名册 `submit_status` **全轨迹** `0→1→2→4→2→3` 逐步查库;**表单升版后提交旧草稿**(不报 `40025`、按当前 schema 校验、`form_version` 是快照);并发提交(钉住"无保护") |
|
||||
|
||||
### 四、实测数字(2026-10)
|
||||
|
||||
| 跑法 | 通过 | 失败 | XFAIL |
|
||||
|---|:--:|:--:|:--:|
|
||||
| `--group chain` | 74 | 0 | 5 |
|
||||
| `--group errors` | 27 | 0 | 1 |
|
||||
| 无参数(两组) | **95** | **0** | **6** |
|
||||
| `python scripts/verify-all.py --with-e2e` | **9 项全过 / 0 失败 / 0 跳过**;**38 个脚本 / 1712 条断言 / 0 失败**(297s) | | |
|
||||
|
||||
(1712 − 1617 = **95**,正好是本脚本新增的断言数 —— 这个等式本身就是"没有重复覆盖"的证据。)
|
||||
跑完残留夹具任务 **0 个**、临时文件 **0 个**;演示任务 17 / 2097 / 2215 未被触碰。
|
||||
⚠️ 但仍要记得:跑 `e2e-m3-*` 会**清掉名册绑定**(既有行为,不是本脚本引入的)。
|
||||
|
||||
### 五、本轮定案的缺陷(**只测不改**)
|
||||
|
||||
| 编号 | 一句话 | 状态 |
|
||||
|---|---|---|
|
||||
| **F-125** | **学生被驳回后改不了任何东西** —— 改字段/传材料/删材料全 `40030`、撤回 `40001`,而 `progress.canEdit`/`canSubmit`/`canFillNow`/`statusText` 四处同时告诉他"你可以改"。契约 `§2.4` 也站学生一边(4 → 1 可整改) | **P0 · 阻断闭环**,4 条 XFAIL |
|
||||
| **F-128** | **扩展字段存得进、读不出** —— `assemble` 从不把 `extrasJson` 合进 `values`,于是填了也不回显、**必填扩展字段永远满足不了**(该任务对所有人永久阻断) | **P0 · 阻断功能**,2 条 XFAIL |
|
||||
| F-129 | 把**已声明**的扩展字段放进 `formData`:`code=0` 但值被静默丢弃(未声明的键反而会明确 `40001`) | P2,钉住现状 |
|
||||
| F-126 | `e2e-m3-draft.py:11` 自述「已提交的不能直接改」但**全文没有这条断言** —— docstring 的 `[x]` 清单在本仓库被当覆盖率唯一账本 | 本轮已补断言 |
|
||||
| F-127 | 契约 `§9.10` 写"校验失败返回 `40001`",实现是 HTTP 200 + `code:0` + `Report`;实现口径合理但**没登记进 `§17.2` 的有意偏离清单(D-1~D-12)** | 只提议补 D-13 |
|
||||
|
||||
**为什么 F-125 没被 1617 条断言抓到**:`e2e-m5-audit.py:531-538` 重交前只改了库里的 `reject_count`,
|
||||
**没改任何业务字段** —— 它证明的是"原样重交能过",而问题恰恰在"**改了**反而过不去"。
|
||||
**为什么 F-128 没暴露**:内置表单 `BuiltinFields.java` **一个扩展字段都没有**,既有脚本用的全是内置表单 →
|
||||
这条路径从来没有数据走过。
|
||||
|
||||
### 六、两条教训(已固化成规程)
|
||||
|
||||
1. **自述不算覆盖**。"读 docstring 说覆盖了什么"会得到虚高的覆盖率,而且**不会让任何东西变红**。
|
||||
改成 `grep 'check(\|ok('` 抄实际断言 —— 这条规程一上来就抓到 F-126。
|
||||
2. **XFAIL 的断言必须写「契约期望」,不能写「现状」**。`EP-I09` 第一版只断言"原样重交能成功",
|
||||
而那本来就成功 → 它报了个 `XPASS` 说"缺陷已修"。**把缺陷写成规格,比不写用例更危险。**
|
||||
同理:状态机用例不能只断言"永远 `40008`"(那会掩盖"状态机卡死"),
|
||||
序列里必须留一步断言**恢复后可填**。
|
||||
|
||||
### 七、显式不做
|
||||
|
||||
① **不改业务代码**(F-125 / F-128 / F-129 的建议写在 `docs/11` 与 `docs/08` 里,等确认);
|
||||
② 不为 M4 未实现功能写用例(`40004/40005/40006/40043/50002/50003/50004`);
|
||||
③ 不新增依赖(只用标准库 + 复用既有 `scripts/e2e-*.py` 的工具函数,**不自己再写一份** `call/upload/mint`);
|
||||
④ 不把浏览器自动化引入仓库;⑤ 不动 `docs/06-验收测试清单.md`(客户向,含 M4 与已取消二期);
|
||||
⑥ 范围只含填报链路,导出(M6) / 通知(M7) / 压测不在本轮。
|
||||
|
||||
### 八、L3-B 真实浏览器走查(加分项,4 条里走了 3 条)
|
||||
|
||||
`pnpm dev:h5`(5174)+ `--fixture-only --keep --h5-url` 拿地址,用真浏览器走了一遍:
|
||||
|
||||
| 走到的 | 结果 |
|
||||
|---|---|
|
||||
| 扫码落地页(①) | 「示范职业学校 / E2E流程 d7545c / 收集中」+ scene 回显 |
|
||||
| 门禁核验(③) | 手输姓名 + 身份证 → 「✓ 核对通过」,带出脱敏号与老师预填的班级/报考三项;**库里确认** `bind_user_id=1385`、`bind_type=1`、`submit_status=1` |
|
||||
| 表单(④) | 17 个字段全部渲染;锁定项标「由老师按报考计划确定,不可修改」;**补考三项按 `visibleWhen` 正确隐藏** |
|
||||
| 自动保存(⑤) | 改「本职业工作年限」失焦 → 「已保存 10:05:29」,库里 `work_years=3`、`changed_fields` 为 NULL(名册没有该项 → 不算"改过老师填的",逻辑正确) |
|
||||
| 提交被拦(⑦) | 「还不能提交 / 必须处理 1 / `VAL-05` 缺少必传材料:身份证」——红色清单原样渲染 |
|
||||
| **F-125 的用户侧证据** | 把 `status` 改成 4 后改字段失焦 → **「保存失败:已提交的填报单不能直接修改,请先撤回」**,而**同一屏没有任何撤回入口**;退到「我的填报」卡片写着**「被驳回,请修改后重新提交」**、按钮**「修改后重新提交」** —— 界面自己的行动号召把学生带进死胡同 |
|
||||
|
||||
未走的两条:黄警告确认框(夹具只造出红色)、传图结果卡(**本轮可用的浏览器自动化没有文件选择能力**,只能人工走查)。
|
||||
两个坑记进 `docs/11` §5.3 了:① 不能用"去掉 URL 里的 scene"复现「无 scene 有新 token」,**scene 已进本地缓存**;
|
||||
② `--fixture-only` **不触发清理**,走查完要自己删任务 + 删 stub-login 真建的 `sys_user` + 删 `_tmp_flow_*.xlsx`。
|
||||
新增工具 `scripts/dev-reveal-roster-idcard.py <taskId>`(只读;原 `_reveal-roster-idcard.py` 的 task id **写死在 :59**)。
|
||||
|
||||
⚠️ **走查/全量 E2E 之后要恢复演示态**:`e2e-m3-*` 会清 `collect_roster.bind_user_id`,
|
||||
本轮跑完全量后已按本文件「演示态的现状」把 **task 17 / 名册行 38 重新绑回 `sys_user.id=151`**(`bind_type=1`、`submit_status=1`)。
|
||||
⚠️ 顺带发现:文档里记的演示草稿 `collect_submission.id=31`(`work_unit=测试单位XYZ`)**已不在库里**,
|
||||
当前 task 17 上的草稿是 **id=60(`work_unit` 为空)**——那份"接着往下试"的演示草稿已经不是原来那份了。
|
||||
|
||||
### 九、L3-C 微信小程序端走查(2026-10 补,需求方点名要求)
|
||||
|
||||
需求方明确要求**「学生提交的过程要测微信小程序端」**——这正是本仓库唯一"代码同步维护、却从没被任何自动化跑过"的一面
|
||||
(AGENTS.md 的节奏是「先做 H5」)。用微信开发者工具自动化(`wmp_*` 工具族 / `miniprogram-automator` 0.12.1)在
|
||||
模拟器里(iPhone 12/13、SDK 3.17.3、工具 8.0.5)把 10 步走完了,完整步骤表在 `docs/11` §5.4。
|
||||
|
||||
**结论:小程序端除登录外整条链路都是好的,只有一根线断了 —— 见 F-130。**
|
||||
|
||||
| 走到的 | 结果 |
|
||||
|---|---|
|
||||
| 落地页 | 「示范职业学校 / E2E流程 1b2dc4 / **收集中**」+ scene 回显(匿名接口在小程序里同样通) |
|
||||
| 门禁核验 | 通过;库里 `bind_user_id=1390`、`bind_type=1` |
|
||||
| 填报表单 | 68 个元素全渲染,锁定项标注正确 |
|
||||
| **传材料(这是 H5 那轮没走到的)** | mock `chooseImage` → 控制台 `[材料压缩] IDCARD P2 证件翻拍:4KB → 2KB(q88)` → `collect_file` 落库,规范名 `流程甲…-3610292008XXXXXX13-身份证.jpg` |
|
||||
| 提交(红) | 页内红单 `VAL-05 缺少必传材料:身份证` + 「知道了,去修改」 |
|
||||
| **提交(黄 → 确认框)** | `VAL-08「身份证」清晰度偏低(72×72)` + 页内**「仍然提交」** → 这才是 `docs/11` §5.3 第 2 条(H5 那轮没造出黄色,本轮被小程序补上了) |
|
||||
| 落库 | `collect_submission.status=1`(待初审)、`submit_time` 写入、`collect_roster.submit_status=2` |
|
||||
|
||||
**F-130(P0)一句话**:小程序端 **A** 从来不调 `/mp/login`(token 永远空 → 门禁 40101),**B** 就算调了,
|
||||
`scene` 带 taskId 时 `/mp/login` 必然 500(`MpLoginService.java:127` 匿名查租户表被租户拦截器拒 → 40101 被包成 50000)。
|
||||
对照实测:同秒同端点,`scene=t1` → **200 + token**,`scene=t1-k2313` → **500**——而学生扫的码必然带 taskId。
|
||||
|
||||
⚠️ **本轮走查是"桥接"验的**:把 token 直接塞进 storage,只为把「登录坏了」与「其余链路好不好」分开。
|
||||
**F-130 修好后这一步必须删。**
|
||||
⚠️ **可自动化**:`/mp/login` 此前没有 E2E 覆盖,理由是「脚本拿不到 `wx.login` code」
|
||||
(`scripts/e2e-m3-gate.py:18`)——**本轮证明拿得到**:在页面上下文里调 `wx.login()` 能得到**真 code**,
|
||||
后端真换到了 openid。所以这条链路是可以固化的。
|
||||
⚠️ **收尾**:`--fixture-only` 不触发清理;登录成功那次真建了 `sys_user`(uid 1390,已绑名册行 21695),
|
||||
失败的登录会**回滚**(不建账号)。
|
||||
|
||||
---
|
||||
|
||||
## §一·四十二 弹窗遮罩毛玻璃 + 提示折行清理(2026-10)
|
||||
|
||||
> 需求原话:**"1.给系统中的弹窗背景添加毛玻璃效果 2.系统中的label或tip中还存在换行,优化一下。"**
|
||||
> 用户拍板:**只做 PC 管理端**(学生端小程序/H5 不动);毛玻璃**只加在遮罩层**,弹层本体保持纯白。
|
||||
|
||||
### 一、毛玻璃:只能用 CSS,主题变量改不到
|
||||
|
||||
`frontend-pc/src/styles/index.scss` 的 `:root` 新增两个变量(`:41-42`),并在 `:142-147` 新增:
|
||||
|
||||
```scss
|
||||
.n-modal-mask.n-modal-mask,
|
||||
.n-drawer-mask.n-drawer-mask:not(.n-drawer-mask--invisible) {
|
||||
background-color: var(--xt-mask-bg); /* rgb(16 24 40 / 26%) */
|
||||
backdrop-filter: blur(var(--xt-mask-blur)) saturate(130%); /* 8px */
|
||||
-webkit-backdrop-filter: blur(var(--xt-mask-blur)) saturate(130%);
|
||||
}
|
||||
```
|
||||
|
||||
- **为什么不能走 `App.vue` 的 `themeOverrides`**:Naive 把遮罩色**写死在 css-render 里** ——
|
||||
`naive-ui/es/modal/src/styles/index.cssr.mjs:13` 的 `modal-mask` = `rgba(0,0,0,.4)`、
|
||||
`es/drawer/src/styles/index.cssr.mjs:136` 的 `drawer-mask` = `rgba(0,0,0,.3)`;
|
||||
Modal / Drawer 的主题里**没有 mask token**(`modal/styles/light.mjs` 只有 color / textColor / boxShadow)。
|
||||
- **三种弹层一条规则覆盖**:`useDialog()` 走 `es/dialog/src/DialogEnvironment.mjs:4,137`
|
||||
(`createBlock(Modal_default, …)`)→ 与普通弹窗共用 `.n-modal-mask`(**本轮实机确认**)。
|
||||
- 双类名与 `.n-card.n-card` 同一招(css-render 运行时注入 `<head>`,先后顺序不保证);
|
||||
`.n-drawer-mask--invisible`(`show-mask="transparent"`,`es/drawer/.../index.cssr.mjs:143`)
|
||||
必须排除,否则"看不见的遮罩"会被染色变成"看得见的挡板"(仓里暂时没人用这个属性,是给以后留的)。
|
||||
- 观感:Chrome 不会让 `backdrop-filter` 跟着 `opacity` 淡入;实机截图看**没有**明显"硬弹",
|
||||
所以**没有**加 keyframes 动画。
|
||||
- 自查一行:`getComputedStyle(document.querySelector('.n-modal-mask')).backdropFilter`
|
||||
→ `blur(8px) saturate(1.3)`,弹层本体(`.n-card.n-modal` / `.n-dialog`)仍是 `rgb(255, 255, 255)`。
|
||||
|
||||
### 二、提示折行:9 处(格子宽度都是写死的,与窗口宽度无关)
|
||||
|
||||
| 位置 | 改法 | 实测 |
|
||||
|---|---|---|
|
||||
| `views/task/TaskFormModal.vue:374`(样式 `:431`) | `.n-form-item-blank` 是 **flex 行**:提示与 `<code>` 被并排压缩 → 包一层 `__preview-box`(`width:100%`),提示独占一行 | 191px / **3 行** → 603px / 1 行 |
|
||||
| `views/task/TaskFormSchemaDrawer.vue:622` | "只能用小写字母、数字、下划线;改动后导出列名会跟着变" → 去掉"改动后…会" | 276px 里 2 行 → 264px / 1 行 |
|
||||
| `views/task/TaskMaterialDrawer.vue:624` | "档位决定分辨率与清晰度,一般不用改" → "影响清晰度,一般不用改"(字段 label 已是「压缩档位」) | 192px 里 2 行 → 156px / 1 行 |
|
||||
| `views/task/TaskMaterialDrawer.vue:576` | 别名提示删掉"这是哪一项材料 —— 该认出来的"(第二行只剩"不出来"三个字) | 641px 里 2 行 → 1 行 |
|
||||
| `views/task/TaskOrgDrawer.vue:283` | "留空表示不比对,填了才与名册比" → "留空 = 不比对"(后半句是前半句的逆否) | 160px 定宽列里 2 行 → 1 行 |
|
||||
| `views/user/UserFormModal.vue:331` | "登录名不可修改:改了会让历史记录与导出包里的账号对不上" → "不可修改:会让历史记录与导出包对不上" | 264px 里 2 行 → 216px / 1 行 |
|
||||
| `views/user/UserFormModal.vue:343` | "列表只返回脱敏值,这里必须重填一次(加密存储)" → "必须重填一次(列表只给脱敏值)" | 252px 里 2 行 → 180px / 1 行 |
|
||||
| `views/role/RoleFormModal.vue:250` | 删掉"建好后在右侧授权矩阵里增删"(矩阵就在同屏) | 480px 里 2 行 → 324px / 1 行 |
|
||||
| `views/audit/FilePreview.vue:140` | "(链接过期或已删除)" → "(链接已过期)" | `.fp__box` 固定 108×108:120px 的文字折 3 行且被 `overflow:hidden` **裁掉** → 2 行(`<br>` 是刻意的) |
|
||||
|
||||
**label 侧本轮 0 命中**:可达的 10 个路由(`/dashboard`、`/collect/{task,audit,export,notify}`、
|
||||
`/setting/dict`、`/org/{manage,user,role}`、`/platform/log`)与打开的弹层/抽屉里,
|
||||
标签**没有一个折行**(表单全 `label-placement="top"`、表格列宽写死、表头折行由 CSS 截断)。
|
||||
**未实测**:`/platform/tenant`(本账号 403)、字典的新增弹窗(无按钮)、审核详情抽屉(列表为空)、
|
||||
租户详情/编辑弹窗 —— 静态估算不折行,实施时顺带看一眼。
|
||||
|
||||
### 三、刻意不改
|
||||
|
||||
`views/dict/index.vue:425-429` 的项码提示(56 字)、`TaskRosterDrawer` 的长说明、
|
||||
`views/audit/FilePreview.vue:179` 的 `.fp__missing`(`<br>` = 标题行 + 说明段)、
|
||||
`ExportDetailDrawer` 的 `.ed__muted.ed__tip` 长段落 —— 都是**说明性长文**,折行是预期;
|
||||
压短会丢真实后果("查询不报错、只显示空白"这类)。
|
||||
|
||||
### 四、约定沉淀
|
||||
|
||||
- `frontend-pc/AGENTS.md` 新增**第 11 条**:提示要在自己的格子里单行放下 + 各格子的字数经验值
|
||||
(12px:`span 12` ≈ 23 汉字 / `span 8` ≈ 16 / 两列网格 ≈ 21 / 520px 弹窗全宽 ≈ 39;
|
||||
11px 的 `__fallback` 在 160px 定宽列 ≈ 14)+ `.n-form-item-blank` 是 flex 行 + 遮罩只能用 CSS。
|
||||
- `frontend-pc/.agents/skills/code-css/SKILL.md`:变量表补 `--xt-mask-bg` / `--xt-mask-blur`,
|
||||
新增「弹窗遮罩(毛玻璃)」小节。
|
||||
- **审计手法**(贴控制台,逐页/逐弹层跑):只看**叶子元素**、文字 2~60 字、
|
||||
**只数文本行盒的 top 数**(不数元素盒顶)。
|
||||
⚠️ 两个误报类必须人工看一眼:① 行内元素跨行(长段落里的 `<code>`,如导出页的 `certutil`);
|
||||
② **元素盒顶与文本行顶差 ≥ 0.6 行高**(垂直居中/大 padding 的块,如导出表的日期单元格)——
|
||||
所以判"命中"要看 `display` 与宽度,别直接改文案。
|
||||
|
||||
### 五、验证
|
||||
|
||||
| 手段 | 结果 |
|
||||
|---|---|
|
||||
| `pnpm typecheck` | 0 错 |
|
||||
| `pnpm lint` | **1 条既有报错**:`frontend-pc/AGENTS.md:91`(第 10 条的 ```ts 片段是裸 `} catch`,ESLint 的 markdown 处理器解析不了)。已单独取 `HEAD` 版本复现同样一条 → **与本轮无关**;本轮新增的 markdown 与代码 0 新增报错 |
|
||||
| `check-wiring.py` | 10 项全绿,未接线声明 0 条 |
|
||||
| `verify-docs.py` | 23 项全部通过 |
|
||||
| 实机(1386×800,Vite dev + 后端 8080) | 三种弹层遮罩 `backdropFilter = blur(8px) saturate(1.3)`、底色 `rgba(16, 24, 40, 0.26)`,本体仍 `rgb(255, 255, 255)`;9 处提示逐个量 `height ÷ lineHeight === 1`(`.fp__fail` 那处用同款样式的离屏复现量);10 个路由 + 弹层/抽屉审计 **0 命中** |
|
||||
| 截图 | `C:\Users\93292\Downloads\pc-mask-glass-2026-10-14.jpg`(useDialog「还不能发布」+ 背后毛玻璃) |
|
||||
|
||||
**没有跑 e2e**:38 个 `scripts/e2e-*.py` 全打 HTTP 接口,无一渲染 PC 页面;本轮**没改后端、没改接口与库表**,
|
||||
契约 119 / 错误码 54 / 原型屏数 29 全不变。前端 `/assets/**` 是 immutable 缓存 → 走查后要**硬刷新**。
|
||||
|
||||
### 六、显式不做
|
||||
|
||||
1. **学生端(uni-app H5 与小程序)的遮罩与文案**:`uni.showModal` 是原生弹窗,CSS 改不到;
|
||||
小程序端还有 F-130 未解。
|
||||
2. **浮层类**(`NPopover` / `NTooltip` / `NDropdown` / 下拉面板)的毛玻璃 —— 用户只要"弹窗背景"那一层。
|
||||
3. `docs/prototype/*`:管理端原型没有弹窗,无需同步。
|
||||
4. `frontend-pc/AGENTS.md:91` 那条**既有 lint 报错**:只报告、未顺手改(要把 ```ts 换成 ```text
|
||||
或补成完整函数才行,属另一件事)。
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -30,6 +30,7 @@ description: SCSS 写法、样式作用域与全局样式的边界。编写或
|
||||
| `--xt-card-pad-x` / `--xt-card-pad-y` | `18px` / `16px` | 卡片内容区左右 / 上下内边距(**别在页面里写死**,见下) |
|
||||
| `--xt-band-bg` | `#f7f8fa` | 浅灰色带:卡片头 / 表头 / 卡片内的子格子 |
|
||||
| `--xt-text` / `--xt-text-2` / `--xt-text-3` | `#1f2229` / `#667085` / `#98a2b3` | 主 / 次 / 弱文字 |
|
||||
| `--xt-mask-bg` / `--xt-mask-blur` | `rgb(16 24 40 / 26%)` / `8px` | 弹窗遮罩的底色与模糊半径(见下节) |
|
||||
|
||||
### ⚠️ 卡片内边距:**不要让页面自己写**
|
||||
|
||||
@@ -67,6 +68,30 @@ Naive 的规则是 `cB("card-content", "padding: 0 var(--n-padding-left) var(--n
|
||||
3. **区域间隔靠"色带 + 描边",不是靠加大 margin**:卡片之间 `--xt-gap`,卡片内区块 12~20px。
|
||||
上一版的教训是把卡片写成 `:bordered="false"` + 内容区白底 → 白卡片浮在白底上,整页没有任何边界。
|
||||
|
||||
## 弹窗遮罩(毛玻璃):**只能用 CSS**,别去找主题变量
|
||||
|
||||
遮罩层(弹窗背后那层灰黑)走毛玻璃:`styles/index.scss` 里一条规则覆盖
|
||||
`NModal` / `NDrawer` / `useDialog()` 三种弹层(`useDialog` 内部就是 `NModal`)。
|
||||
|
||||
```scss
|
||||
.n-modal-mask.n-modal-mask,
|
||||
.n-drawer-mask.n-drawer-mask:not(.n-drawer-mask--invisible) {
|
||||
background-color: var(--xt-mask-bg);
|
||||
backdrop-filter: blur(var(--xt-mask-blur)) saturate(130%);
|
||||
-webkit-backdrop-filter: blur(var(--xt-mask-blur)) saturate(130%);
|
||||
}
|
||||
```
|
||||
|
||||
- ⚠️ **为什么不能用 `App.vue` 的 `themeOverrides`**:Naive 把遮罩色**写死在 css-render 里**
|
||||
(`modal/src/styles/index.cssr.mjs` 的 `modal-mask` = `rgba(0,0,0,.4)`、
|
||||
`drawer/src/styles/index.cssr.mjs` 的 `drawer-mask` = `rgba(0,0,0,.3)`),
|
||||
Modal / Drawer 的**主题里没有 mask token** —— 主题变量改不到它。
|
||||
- ⚠️ **双类名**:与 `.n-card.n-card` 同一招(css-render 运行时注入 `<head>`,先后顺序不保证,把权重提到 (0,2,0))。
|
||||
- ⚠️ **`.n-drawer-mask--invisible`**(`show-mask="transparent"`)必须排除:它是"看不见的遮罩",
|
||||
被染色就变成"看得见的挡板"。`.n-modal-mask` 没有对应的隐藏变体。
|
||||
- 自查:`getComputedStyle(document.querySelector('.n-modal-mask')).backdropFilter`
|
||||
与弹层本体的 `backgroundColor`(应为 `rgb(255, 255, 255)`)。
|
||||
|
||||
## 按钮的可见性
|
||||
|
||||
- 页面**主操作**(新建 / 导入 / 批量通过…)用 `type="primary"`,**默认尺寸**(不要 `size="small"`)。
|
||||
|
||||
@@ -100,3 +100,15 @@ pnpm lint # ESLint(@antfu/eslint-config)
|
||||
⚠️ 这个插槽**不接收任何参数**(内部用 `resolveWrappedSlot(slot, wrapper)` = `wrapper(slot())`),
|
||||
所以**没有**「`#feedback="{ status }"` 里顺手渲染错误」这种写法 —— 想内联显示错误只能给
|
||||
`NFormItem` 的 `feedback` **prop** 传渲染函数,那需要自写包装组件,别顺手试。
|
||||
11. **提示(`#feedback` / `__hint`)要在自己的格子里单行放下**:放不下就**改短**,或把这一项改成
|
||||
`:span="24"`。格子的**可用宽度是写死的**(弹窗宽度写死、两列网格、`span 8`),**窗口拉宽没有用**。
|
||||
2026-10 实测过一轮:提示折成 2~3 行时读起来根本不像一句话(最惨的一处被 flex 挤到 191px、折了 3 行)。
|
||||
12px 字的经验值:`span 12`(620px 弹窗)≈ **23 个汉字**、`span 8`(680px 弹窗)≈ **16 个**、
|
||||
`.uf__grid` 两列 ≈ **21 个**、全宽表单项(520px 弹窗)≈ **39 个**
|
||||
(11px 的 `.org-drawer__fallback` 在 160px 定宽列里 ≈ 14 个)。
|
||||
⚠️ **`.n-form-item-blank` 是 flex 行**(`form-item` 是 grid、blank 区是 flex)→ 同一个 `NFormItem`
|
||||
里放两个元素会被并排压缩;要"提示在上、内容在下"必须包一层块级容器
|
||||
(写法见 `src/views/task/TaskFormModal.vue` 的 `__preview-box`)。
|
||||
⚠️ **弹窗遮罩的毛玻璃只能用 CSS**:Naive 把遮罩色写死在 css-render 里,Modal / Drawer 主题
|
||||
**没有 mask token**,`App.vue` 的 `themeOverrides` 改不到它 —— 规则在 `src/styles/index.scss`,
|
||||
细节见 `.agents/skills/code-css/SKILL.md`。
|
||||
|
||||
@@ -36,6 +36,10 @@
|
||||
--xt-text: #1f2229;
|
||||
--xt-text-2: #667085;
|
||||
--xt-text-3: #98a2b3;
|
||||
|
||||
/* 弹窗遮罩:比 Naive 原生的 .4/.3 浅一档,模糊才透得出来(见下方「弹窗遮罩」) */
|
||||
--xt-mask-bg: rgb(16 24 40 / 26%);
|
||||
--xt-mask-blur: 8px;
|
||||
}
|
||||
|
||||
* {
|
||||
@@ -117,6 +121,31 @@ body {
|
||||
border-bottom-color: var(--xt-card-border-weak);
|
||||
}
|
||||
|
||||
/* ── 弹窗遮罩(毛玻璃) ─────────────────────────────── */
|
||||
|
||||
/**
|
||||
* 三种弹层的遮罩层都变毛玻璃:`NModal`、`NDrawer`、以及 `useDialog()` 的提示框。
|
||||
* **弹层本体保持纯白**,只处理背后那一层。
|
||||
*
|
||||
* ⚠️ 为什么只能用 CSS、改不到 `App.vue` 的 themeOverrides:Naive 把遮罩色**写死在
|
||||
* css-render 里**(`modal/src/styles/index.cssr.mjs` 的 `modal-mask` = `rgba(0,0,0,.4)`、
|
||||
* `drawer/src/styles/index.cssr.mjs` 的 `drawer-mask` = `rgba(0,0,0,.3)`),
|
||||
* Modal / Drawer 的主题里**没有 mask token** —— 主题变量改不到它。
|
||||
* ⚠️ 为什么用双类名:理由同上一条(css-render 运行时注入 <head>,先后顺序不保证),
|
||||
* 与 `.n-card.n-card` 是同一招。
|
||||
* ⚠️ `DialogEnvironment` 里 `useDialog()` 走的是 `NModal`(`dialog/src/DialogEnvironment.mjs`),
|
||||
* 所以它和普通弹窗共用 `.n-modal-mask`,一条规则全覆盖。
|
||||
* ⚠️ `.n-drawer-mask--invisible`(`show-mask="transparent"`)必须排除在外:它是
|
||||
* "看不见的遮罩",被这条规则染上颜色就变成"看得见的挡板"。仓里目前没人用这个属性,
|
||||
* 这个排除是给以后用的。`.n-modal-mask` 没有对应的隐藏变体。
|
||||
*/
|
||||
.n-modal-mask.n-modal-mask,
|
||||
.n-drawer-mask.n-drawer-mask:not(.n-drawer-mask--invisible) {
|
||||
background-color: var(--xt-mask-bg);
|
||||
backdrop-filter: blur(var(--xt-mask-blur)) saturate(130%);
|
||||
-webkit-backdrop-filter: blur(var(--xt-mask-blur)) saturate(130%);
|
||||
}
|
||||
|
||||
/* ── 列表页卡片 ─────────────────────────────────────── */
|
||||
|
||||
/**
|
||||
|
||||
@@ -137,7 +137,7 @@ onMounted(loadNormal)
|
||||
<NSpin v-if="loading" :size="16" />
|
||||
<img v-else-if="src" :src="src" class="fp__img" alt="材料预览" />
|
||||
<div v-else class="fp__fail">
|
||||
预览取不到<br>(链接过期或已删除)
|
||||
预览取不到<br>(链接已过期)
|
||||
</div>
|
||||
</div>
|
||||
<div class="fp__name" :title="fileName">{{ fileName }}</div>
|
||||
|
||||
@@ -247,7 +247,7 @@ defineExpose({ add, edit })
|
||||
<NSelect v-model:value="form.templateCode" :options="templateOptions" />
|
||||
<template #feedback>
|
||||
<span class="rf__hint">
|
||||
复制它的权限码与菜单;建好后在右侧授权矩阵里增删。新角色至少要有一项权限与一个菜单
|
||||
复制它的权限码与菜单。新角色至少要有一项权限与一个菜单
|
||||
</span>
|
||||
</template>
|
||||
</NFormItem>
|
||||
|
||||
@@ -365,8 +365,16 @@ defineExpose({ add, edit })
|
||||
</div>
|
||||
|
||||
<NFormItem label="文件名预览">
|
||||
<span class="task-form__preview-note">以「张同学 / 361029200912100015 / 身份证」为例</span>
|
||||
<code class="task-form__preview">{{ namingPreview }}</code>
|
||||
<!--
|
||||
⚠️ 必须包一层块级容器:`.n-form-item-blank` 是 **flex 行**(form-item 是 grid、
|
||||
blank 区是 flex),直接放两个子元素会被排成同一行、各自被压缩 ——
|
||||
实测这行提示被挤到 191px、折成 3 行("样例"根本读不出是一句话)。
|
||||
包成块级容器后 note 独占一行(容器 603px,需要 312px)。
|
||||
-->
|
||||
<div class="task-form__preview-box">
|
||||
<span class="task-form__preview-note">以「张同学 / 361029200912100015 / 身份证」为例</span>
|
||||
<code class="task-form__preview">{{ namingPreview }}</code>
|
||||
</div>
|
||||
</NFormItem>
|
||||
</NForm>
|
||||
|
||||
@@ -415,6 +423,15 @@ defineExpose({ add, edit })
|
||||
color: #8a8a8a;
|
||||
}
|
||||
|
||||
/**
|
||||
* 样例提示 + 文件名预览:**必须整体占满 blank 区**。
|
||||
* `.n-form-item-blank` 是 flex 行,不包这一层的话两个子元素会被并排压缩
|
||||
* (实测提示被挤到 191px、折成 3 行)。
|
||||
*/
|
||||
&__preview-box {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
&__preview-note {
|
||||
display: block;
|
||||
margin-bottom: 4px;
|
||||
|
||||
@@ -619,7 +619,7 @@ defineExpose({ open })
|
||||
<NInput v-model:value="extraForm.key" placeholder="extra_xxx" :maxlength="46" clearable />
|
||||
<template #feedback>
|
||||
<span class="fd__hint fd__hint--inline">
|
||||
只能用小写字母、数字、下划线;改动后导出列名会跟着变
|
||||
只能用小写字母、数字、下划线;导出列名跟着变
|
||||
</span>
|
||||
</template>
|
||||
</NFormItemGi>
|
||||
|
||||
@@ -573,8 +573,7 @@ defineExpose({ open })
|
||||
<NDynamicTags v-model:value="form.aliases" :max="6" />
|
||||
<template #feedback>
|
||||
<span class="mat-form__hint">
|
||||
输入后回车,如:身份证正面、身份证人像面。
|
||||
用于从历史文件名里认出这是哪一项材料 —— 别名不全,该认出来的就认不出来
|
||||
输入后回车,如:身份证正面、身份证人像面。用于从历史文件名认出材料,别名不全就认不出来
|
||||
</span>
|
||||
</template>
|
||||
</NFormItemGi>
|
||||
@@ -622,7 +621,7 @@ defineExpose({ open })
|
||||
<NFormItemGi :span="8" label="压缩档位" path="compressProfile">
|
||||
<NSelect v-model:value="form.compressProfile" :options="profileOptions" />
|
||||
<template #feedback>
|
||||
<span class="mat-form__hint">档位决定分辨率与清晰度,一般不用改</span>
|
||||
<span class="mat-form__hint">影响清晰度,一般不用改</span>
|
||||
</template>
|
||||
</NFormItemGi>
|
||||
|
||||
|
||||
@@ -280,7 +280,7 @@ defineExpose({ open })
|
||||
:disabled="readonly"
|
||||
/>
|
||||
<div class="org-drawer__fallback">
|
||||
留空表示不比对,填了才与名册比
|
||||
留空 = 不比对
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -328,7 +328,7 @@ defineExpose({ add, edit })
|
||||
<NFormItem label="登录账号" path="username">
|
||||
<NInput v-model:value="form.username" :disabled="isEdit" placeholder="如 zhangsan" clearable />
|
||||
<template v-if="isEdit" #feedback>
|
||||
<span class="uf__hint">登录名不可修改:改了会让历史记录与导出包里的账号对不上</span>
|
||||
<span class="uf__hint">不可修改:会让历史记录与导出包对不上</span>
|
||||
</template>
|
||||
</NFormItem>
|
||||
|
||||
@@ -340,7 +340,7 @@ defineExpose({ add, edit })
|
||||
<NInput v-model:value="form.phone" placeholder="11 位手机号" :maxlength="11" clearable />
|
||||
<template #feedback>
|
||||
<span class="uf__hint">
|
||||
{{ isEdit ? '列表只返回脱敏值,这里必须重填一次(加密存储)' : '加密存储,列表只显示脱敏值' }}
|
||||
{{ isEdit ? '必须重填一次(列表只给脱敏值)' : '加密存储,列表只显示脱敏值' }}
|
||||
</span>
|
||||
</template>
|
||||
</NFormItem>
|
||||
|
||||
Reference in New Issue
Block a user