# 表单页 / 动态行 版面铁律

触发：用户说「布局不合理」「排布不合理的排布」「手机上输入框太小」「改版面」「调布局」，或新建含动态增删行的表单页（活动录入、拜访记录、订单明细这类）。

## 一、排查顺序：先量，再改，最后才看美感

1. **先把页面填成「有内容」的状态再截图**：空表单量不出问题，动态行都不存在。
2. 用 JS 量几何，不要先看截图：
   - 同一行各子元素 `getBoundingClientRect()` 的右边缘是否一致（是否对齐到同一列线）；
   - 是否溢父元素/溢视口（`r.right - parent.right > 2`、`r.right > clientWidth + 2`）；
   - 最小可交互控件宽度：手机上 `<input>` 被压到几十像素就是坏（正常应 > 200px）；
   - 固定底栏是否真遮挡：先 `scrollTo(0, body.scrollHeight)` 再比 rect，`元素.bottom < 底栏.top` 才算不遮挡。
3. **视觉模型（vision_analyze）的结论一律当假设，不当事实**。它会：把「视口自然截断」读成「被底栏遮挡」；把存在的小字标签读成「没有标签」；把浮层过场读成「字段错行」；把 1px 的视觉差说成「缩进没对齐」。每条视觉结论先用第 2 步的量测证实或推翻——否则会去修不存在的问题，还占掉修真问题的时间。反之，视觉模型对**美感**（间距节奏、重心、饱满度）的判断是可用的。
4. 改完用**同一组指标**复量，再交给视觉模型看美感。
5. 用 DOM 取证时别忘了表格/列表是可视化事实：`document.querySelectorAll('#app h3 .n')` 之类的序号序列，一眼就能看出结构性错乱（见第五节）。

## 二、栅格：动态行的模板必须写在 CSS 类里

- 行模板（`grid-template-columns`）**写成 CSS 类**，不要写在 JS 生成的 `style` 上：内联样式优先级高于媒体查询，窄屏永远覆盖不掉，手机端输入框会被压成 30~90px。
- 按屏宽分层写：
  ```css
  @media(min-width:761px){ .drow-param{grid-template-columns:220px minmax(0,1fr) 36px} }
  @media(max-width:760px){
    .drow-param{grid-template-columns:minmax(0,1fr) 36px}
    .drow>*:not(.del){grid-column:1}   /* 字段全部竖排 */
    .drow>.del{grid-column:2;grid-row:1} /* 删除键固定右上 */
  }
  ```
  窄屏统一「字段竖排 + 删除键固定第一行」，比硬塞多列可用得多。
- 轨道用 `minmax(0,1fr)`，不要裸 `1fr`：`1fr` 的隐含最小尺寸是 min-content，长中文串会把整行撑破。
- **只写一个轴的定位会打乱整行**：给栅格子元素只写 `grid-row:1`（不给列）时，自动布局会把它放进该行的第一个空列，后面的元素跟着错位（表现为「第 1 个输入框跑到右列、删除键占满整行」）。要么两个轴都写，要么一个都不写、交给文档顺序。
- 同一张卡片里多行字段要落在**同一套列线**上：外层统一列数（如 `repeat(6,1fr)`），字段按 span 组合（3+3 或 2+2+2）。一行 `1fr 1fr`、下一行 `1fr 1fr 1fr` 会显得右缘参差。
- 分支切换会隐藏某字段时，用 JS 动态改 span **把整行吃满**（有细分=2+2+2，无细分=3+3），否则右侧空一截——这是「排布不合理」最常见的来源之一。
- 跨行的宽字段用 `grid-column:1/4` 通到行尾，右边缘才和行内其他元素齐。
- 删除键给足点击区（≥36×40）并带底/带边框；纯字符 `×` 无底无框会被评价为「不像按钮」。

## 三、固定底栏

- 底栏内层容器必须与主内容容器**同 max-width、同内边距**，否则提示文字/按钮和卡片左右边线差几十像素（被读成「没对齐」）。
- 父元素若是 flex（例如用 `style.display='flex'` 打开底栏），内层 `max-width + margin:auto` 会**退化成内容宽度**并整体偏移——必须补 `width:100%`。
- 窄屏让提示独占一行、按钮均分：`.bar .msg{flex:1 1 100%}` + `.bar .btn{flex:1 1 0}`，否则提示换行后和按钮挤成一团。
- 主内容 `padding-bottom` ≥ 底栏高 + 余量（≥130px），滚到底时最后一屏才不被压住。

## 四、版面结构（用户偏好：页面要饱满，不留大片留白）

- **不要为了放两张小卡片就开右栏**：表单很长时右栏会空出一大片，本身就是「排布不合理」。次级信息（最近提交、填写要点、说明卡）置底用 `repeat(auto-fit,minmax(300px,1fr))` 并排，表单通栏（max-width 1100~1200）。
- 空白多不靠「加内容填满」解决，先把结构性空白去掉（取消空侧栏、字段吃满列）。
- 页内**编号徽标要随分支动态**：两个分支各自 1..N 但都写在静态 HTML 里，切换后会出现重复编号（两个 ④）。由切换函数按类型改写徽标文本。

## 五、互斥分支的不变量检查

- `classList.toggle(cls, force)`：**`force=false` 是「移除该类」，不是「隐藏」**。要按条件隐藏必须传 `!cond`；写反了会让两个互斥分支同时显示（或同时隐藏），而且**不报错**，页面看上去只是「多了几张卡」。
- 改完做一行不变量验证：把**可见**元素（`offsetParent !== null`）的卡片编号/标题拼成序列，断言两分支分别是 `1..N` 且互不混入。这是唯一能一眼看出分支互斥失效的廉价检查。
- 用 `querySelectorAll` 直接取全部元素做序列检查会**同时含被隐藏分支**，看到「1→2→3→7→2→3→8」这种假重复，先把 `offsetParent` 过滤加上再下结论。

## 六、自动化填表与回归底线

- **程序化填值必须派发事件**：动态行的状态存在内存数组里、由 `input` 事件同步；直接 `el.value='...'` 不触发，校验会误报「待完善」、提交也会漏数据。测试填充一律 `el.dispatchEvent(new Event('input',{bubbles:true}))`。看到「明明填了却说缺失」，先怀疑自己没派发事件，再怀疑页面有 bug。
- 反过来，页面自身的草稿恢复若通过 `addRow(...)` 重建行（而不是直接塞 DOM），状态是同步的；改动草稿逻辑时别把它改成直接赋值。
- 版面改动也要跑一遍功能回归：填完整 → 校验通过 → 提交 → 核对本地/云端落盘。只动 CSS/类名时最容易被跳过，但行模板类名一改就可能连带影响采集。
- 交付前用真实视口量两组数据：桌面（1280/1360）与手机（390）。两端都要「溢父元素 0、超视口 0」。
