网页端表单里,必填项现在是在标题后面加一颗红色星号(如「书名 」「片名」)。
问题:星号是追加在文字末尾的,中文标题长短不一, 星号位置就参差不齐;再加上不同表单的标签写法不统一(有的 <label> 带空格、 有的 <i>*</i> 紧贴、有的 .mv-lb 用伪元素补冒号), 视觉上经常对不齐,排版不好看。
<label>
<i>*</i>
.mv-lb
所有必填项统一改成「标题用红色 + 加粗」,去掉红色星号。
用户习惯之后,一看标题字体是红色加粗(比如「日程名称」四个字是红的、粗的), 就知道这是必填项。这样比加星号更整齐,也不再有对齐问题。
已核到的必填标记位置:
books/form.php
calendar/calendars.php
calendar/_event_fields.php
$star
days/form.php
ledger/index.php
movies/form.php
movies/cinemas.php
nav/index.php
people/form.php
people/import.php
resources/index.php
tasks/_add_modal.php
tasks/_edit_form.php
trips/form.php
trips/node_form.php
_datetime.php
$dtLabelStar
*
::after
所有必填项统一改成标签本身红色 + 加粗,星号全部去掉。
CSS(assets/css/app.css)
assets/css/app.css
:root
--req: #B03A2E
--red
--req-w: 700
<span class="mv-lb">
<span class="dt-inline-lab">
<span class="dt-range-label">
.is-req
模板:30 处星号逐条精确替换成 class="is-req"(_tooling/_v94_req_label.py,每条断言"命中且仅命中一次");两处动态生成标签的闭包(_event_fields.php 的 $evLab、_datetime.php 的 $dtLabelReq)改成拼 is-req。
class="is-req"
_tooling/_v94_req_label.py
_event_fields.php
$evLab
$dtLabelReq
is-req
坑 1:本站标签的基线字重是 600,不是 400。 .form-grid > .field > label、.card .field > label、.mv-row > .mv-lb、.dt-range-label 四处全都是 font-weight: 600。 第一版 --req-w 写的是 650 —— 在只提供 400/500/600/700 字重的字体上会被取整回 600,等于完全没加粗,用户看到的效果只剩"变红"。已改成 700。 ⇒ 守卫判据因此写成「必填字重 严格大于 从 CSS 里解析出来的基线」,而不是写死 700,将来谁调基线它自己会跟着重算。
.form-grid > .field > label
.card .field > label
.mv-row > .mv-lb
.dt-range-label
font-weight: 600
--req-w
坑 2:!important 不是偷懒,是必需的。 .is-req(0,1,0)要覆盖的 .card .field > label:not(.job-lb):not(.check-item):not(.chk-inline) 特异性是(0,4,1)。实测去掉 !important 后:
!important
.card .field > label:not(.job-lb):not(.check-item):not(.chk-inline)
坑 3:冒号是伪元素,必须一起改。 .mv-lb::after / .dt-inline-lab::after 把「:」画成灰色 400 字重。只改标签本体会出现"红字 + 灰冒号"的半截效果,已一并覆盖。
.mv-lb::after
.dt-inline-lab::after
_fb_v94_static.py
_fb_v94_req.py
探针在真实 Chromium 里量 getComputedStyle,覆盖:观影「记一场电影」+ 新建影院弹框、旅程、新增成员、导航页、任务新建弹框、首页「新建日程」弹框(用户点名的场景)。判据包括:
getComputedStyle
--req
required
checkValidity()
如实记录,因为它们说明"断言能红"比"断言全绿"重要得多:
--ink
<label class="mv-lb is-req">
label.is-req
全量流水线正在跑(含线上部署)。跑完会补一条收尾评论说明部署结果,然后关闭本工单。
这一条不是需求本身的改动,但它是这批改动必须修掉的配套问题,记在这里备查。
流水线跑到最后一步「部署脚本静态断言本地空跑」时失败,日志里有两行:
_fb_static_dry2.sh: line 4813: perl: command not found [FAIL] 任务标题标签不是「任务名称」
第二行是正常的:必填标记从 <i>*</i> 改成了 .is-req,部署脚本里那条断言的字面量 必须跟着改(已改,并且保留 is-req —— 否则会变成「标签在、必填标记丢了也能过」的假绿)。
第一行才是真问题。
部署脚本里剥 CSS 注释用的是:
_css_code=$(perl -0777 -pe 's:/\*.*?\*/::gs' "$SITE/assets/css/app.css")
本机没有 perl(which perl 为空,PortableGit 不带它)。命令失败 ⇒ _css_code 是空串。 而往后每一条这种形状的断言:
which perl
_css_code
grep -qF '.ag-now-dot' <<<"$_css_code" && { echo "[FAIL] 死规则还在"; exit 1; }
在空串上恒不命中、恒为真 ⇒ 「app.css 里还留着死规则」这类"删干净了吗"的检查 全部失效,而日志一片绿。
这比"检查报错"恶劣得多:闸门坏了不报警,只会放行。而它正是部署前最后一道防线。
_pick_php
app.css
新增 _v94_dry_falsify.py:每次从部署脚本现抽快照,然后注入三处。 注意 _fb_static_dry2.sh 是从 familybook_deploy.sh 自动生成的快照, 改它不等于改源头(下次运行就被覆盖,且线上真跑的断言毫无变化)—— 一律改部署脚本。
_v94_dry_falsify.py
_fb_static_dry2.sh
familybook_deploy.sh
3/3 全部被抓住,且已登记进流水线(部署 标签,每次全量必跑)。
部署
_fb_static_all.sh
templates/tasks/index.php
templates/tasks/_add_modal.php
这两个都不影响产品代码,但都让流水线白跑 2 小时,且失败信息指向错误的方向。
v44 反向验证 报:
v44 反向验证
证伪通过 16 项,失败 1 项 x 注入后「旧的日期控件真的不渲染」变红 [FAIL] ... <<< 未变红 → 这条断言可能是**恒真**的摆设
按字面读,结论是「那条断言写错了」——完全错误。翻原文才看到:
playwright._impl._errors.Error: Page.goto: net::ERR_UNSAFE_PORT at http://127.0.0.1:10080/index.php?r=auth/login
探针端口是 bind(('127.0.0.1', 0)) 让系统随机给的,这次抽到 10080, 在 Chromium 的受限端口名单里。浏览器连不上 ⇒ 探针崩 ⇒ 上层只看到 「期望的红没出现」⇒ 归因到断言。
bind(('127.0.0.1', 0))
为什么特别坑:① 失败信息指向错误方向,让你去审一条完全正确的断言; ② 概率性,上次是绿的,容易被当 flaky 放过;③ 错误在子进程里, falsify 只贴末尾几行,ERR_UNSAFE_PORT 在更早的位置。
ERR_UNSAFE_PORT
修法:抽成公共模块 _tooling/_fb_port.py 的 pick_port(),抽到黑名单就重抽。 把流水线里所有裸抽端口的探针批量换过去 —— 本轮一次改了 61 个文件 (39 个单行式 + 20 个四行式 + 2 个手工),改完 61/61 语法通过。
_tooling/_fb_port.py
pick_port()
⚠️ 这里有个教训:_fb_v94_req.py 里我自己几小时前刚踩过 6666 并写了黑名单, 但那份逻辑只活在那一个文件里,其余 60 多个探针仍然裸抽 ⇒ 换个套件又栽一次。 同一个坑出现第二次,就该抽成公共设施。
修完端口后,v44 仍然红,但换了个形态:
[FAIL] 基线全绿(探针本身没问题) <<< 第一行是「任务名称」… 实际读到「要做什么」 [FAIL] 补丁已命中(templates/tasks/_add_modal.php)<<< 没找到注入点,跳过
真根因:_b44_falsify.py 有 5 处注入打的是 templates/tasks/_add_modal.php (v78 起任务表单搬到了这个片段),但它的 RESTORE_RELS 清单里没有这个文件。
_b44_falsify.py
RESTORE_RELS
后果分两层:
patch()
而 _b44_falsify.py 自己写着这条规矩("凡是 falsify 会 patch 的文件, 都必须在这个清单里")—— 规矩写在注释里不会自己生效,所以又被违反了。
修法:补上清单 + 做成静态核对工具 _fb_restore_rels_check.py, 登记进流水线(每轮几秒钟)。它的判据是「脚本 patch 的目标文件是否都在拷贝清单里」。
_fb_restore_rels_check.py
⚠️ 这个检查器自己也有过一次假绿:第一版只认 patch('x','y','z') 调用形态, 而这批 falsify 用的是 (描述, (路径, 原文, 替换), (断言)) 嵌套元组 ⇒ 每个脚本抽出 0 个目标 ⇒ 差集恒空 ⇒ 打印「✅ 全部通过」。 这是最坏的一种假绿:检查器本身没生效,却给出最让人放心的结论。 已加自检:抽出 0 条 = 检查未生效 = 失败(不是通过)。
patch('x','y','z')
(描述, (路径, 原文, 替换), (断言))
证伪通过 17 项,失败 0 项
基线全绿、5 处注入全部命中、ERR_UNSAFE_PORT 出现 0 次。
#B03A2E
.mv-lb.is-req
.dt-inline-lab.is-req
.dt-range-label.is-req
.field.is-req > label
.field.is-req label
#F08A78
模板(16 个文件 / 30 处)
$req
$evLabelStar
:has()
_fb_v94_live.py
线上实测(不是看 CSS 源码,是 getComputedStyle 的真实值):
rgb(176,58,46)
rgb(44,35,29)
冒号也同色(::after 实测 rgb(176,58,46)),页面上已无星号。
反向验证的 5 类注入(每类都必须让预期的那几条断言变红):
daf5ca9 v94 必填项改成「标签红色加粗」,去掉红色星号 (#3)
daf5ca9
17 个文件,+176 / -44。
No dependencies set.
The note is not visible to the blocked user.
现象
网页端表单里,必填项现在是在标题后面加一颗红色星号(如「书名 」「片名」)。
问题:星号是追加在文字末尾的,中文标题长短不一,
星号位置就参差不齐;再加上不同表单的标签写法不统一(有的
<label>带空格、有的
<i>*</i>紧贴、有的.mv-lb用伪元素补冒号),视觉上经常对不齐,排版不好看。
期望
所有必填项统一改成「标题用红色 + 加粗」,去掉红色星号。
用户习惯之后,一看标题字体是红色加粗(比如「日程名称」四个字是红的、粗的),
就知道这是必填项。这样比加星号更整齐,也不再有对齐问题。
涉及范围(全站网页端表单)
已核到的必填标记位置:
books/form.php书名calendar/calendars.php名字 / 导进哪一本 / .ics 文件calendar/_event_fields.php标签闭包($star参数)+ 日期范围days/form.php叫什么 / 按什么重复 / 日期ledger/index.php类别 / 名称 / 金额movies/form.php片名 / 影院名称movies/cinemas.php影院名称nav/index.php网址 / 所属板块 / 板块名 / 名称 / 搜索地址 / 关键词参数people/form.php姓名people/import.php联系人文件resources/index.php归属于谁 / 标题tasks/_add_modal.php任务名称tasks/_edit_form.php任务名称trips/form.php旅程名trips/node_form.php类型 / 标题_datetime.php日期控件标签星号($dtLabelStar)验收标准
*。—— 不能因为改字体导致标签换行、错位或高度变化。
.mv-lb这类用::after补冒号的标签,改成红加粗后冒号也要跟着变红(不能出现"红字 + 灰冒号"的半截效果)。
进展:改动已落码,正在跑全量验证
一、实现方式
所有必填项统一改成标签本身红色 + 加粗,星号全部去掉。
CSS(
assets/css/app.css):root新增两个语义变量:--req: #B03A2E—— 必填红。没有复用--red:--red(#C0533F) 是动作红(删除按钮底色),当文字色用在暖纸底(#F7F3EA)上对比度只有 ≈3.4:1,13-14px 标签偏浅;取深一档的 #B03A2E(≈4.6:1)。--req-w: 700—— 必填字重。<label>/<span class="mv-lb">/<span class="dt-inline-lab">/<span class="dt-range-label">,并含.is-req加在包裹层时的兜底、深色主题备用色。模板:30 处星号逐条精确替换成
class="is-req"(_tooling/_v94_req_label.py,每条断言"命中且仅命中一次");两处动态生成标签的闭包(_event_fields.php的$evLab、_datetime.php的$dtLabelReq)改成拼is-req。二、过程中发现并修掉的三个坑(都会导致"看着改完了、其实没生效")
坑 1:本站标签的基线字重是 600,不是 400。
.form-grid > .field > label、.card .field > label、.mv-row > .mv-lb、.dt-range-label四处全都是font-weight: 600。第一版
--req-w写的是 650 —— 在只提供 400/500/600/700 字重的字体上会被取整回 600,等于完全没加粗,用户看到的效果只剩"变红"。已改成 700。⇒ 守卫判据因此写成「必填字重 严格大于 从 CSS 里解析出来的基线」,而不是写死 700,将来谁调基线它自己会跟着重算。
坑 2:
!important不是偷懒,是必需的。.is-req(0,1,0)要覆盖的.card .field > label:not(.job-lb):not(.check-item):not(.chk-inline)特异性是(0,4,1)。实测去掉!important后:CSS 文本一个字都没错,但页面上就是不红了 —— 只有拿真浏览器量计算色才能发现。
坑 3:冒号是伪元素,必须一起改。
.mv-lb::after/.dt-inline-lab::after把「:」画成灰色 400 字重。只改标签本体会出现"红字 + 灰冒号"的半截效果,已一并覆盖。三、验证(三层,全部带反向验证)
_fb_v94_static.py!important、颜色不走变量、星号回来、漏标一个必填 …)_fb_v94_req.py(真浏览器计算样式)探针在真实 Chromium 里量
getComputedStyle,覆盖:观影「记一场电影」+ 新建影院弹框、旅程、新增成员、导航页、任务新建弹框、首页「新建日程」弹框(用户点名的场景)。判据包括:--req,且不是墨色/灰required与checkValidity()没受影响(改样式没弄丢校验)四、反向验证过程中,探针自己被抓出的两个假绿
如实记录,因为它们说明"断言能红"比"断言全绿"重要得多:
--req改成墨色时判据②假绿:原判据写成「必填色 ≠ 墨色 且--req≠--ink」,一旦注入把--req也改成墨色,后半段前提不成立 ⇒ 整条短路成 False。已改成前置断言 ⓪(三种颜色两两不同)+ ② 只判"不是墨色/灰"。!important的注入原本打在观影页,而观影页的必填标签是<label class="mv-lb is-req">,label.is-req(0,2,0) 与.mv-row > .mv-lb(0,2,0) 同特异性、靠源码顺序取胜 ⇒ 去掉!important照样红。改到成员页/导航页(被 0,4,1 压着)才真能变红。五、尚未完成
全量流水线正在跑(含线上部署)。跑完会补一条收尾评论说明部署结果,然后关闭本工单。
补充:验证过程中发现并修掉了一个「部署闸门失效」的隐患
这一条不是需求本身的改动,但它是这批改动必须修掉的配套问题,记在这里备查。
现象
流水线跑到最后一步「部署脚本静态断言本地空跑」时失败,日志里有两行:
第二行是正常的:必填标记从
<i>*</i>改成了.is-req,部署脚本里那条断言的字面量必须跟着改(已改,并且保留
is-req—— 否则会变成「标签在、必填标记丢了也能过」的假绿)。第一行才是真问题。
真问题:闸门里有一批断言在「空输出」上恒真
部署脚本里剥 CSS 注释用的是:
本机没有 perl(
which perl为空,PortableGit 不带它)。命令失败 ⇒_css_code是空串。而往后每一条这种形状的断言:
在空串上恒不命中、恒为真 ⇒ 「app.css 里还留着死规则」这类"删干净了吗"的检查
全部失效,而日志一片绿。
这比"检查报错"恶劣得多:闸门坏了不报警,只会放行。而它正是部署前最后一道防线。
修法(三条一起)
_pick_php已有,本机/线上都必有),不再依赖 perl;app.css386,599 字节 → 剥注释后 209,480 字节)。只判"非空"挡不住"读错文件 / 被截断",体量断言才挡得住。
反向验证(证明这三条护栏真有牙齿)
新增
_v94_dry_falsify.py:每次从部署脚本现抽快照,然后注入三处。注意
_fb_static_dry2.sh是从familybook_deploy.sh自动生成的快照,改它不等于改源头(下次运行就被覆盖,且线上真跑的断言毫无变化)—— 一律改部署脚本。
<i>*</i>星号形态3/3 全部被抓住,且已登记进流水线(
部署标签,每次全量必跑)。顺带清理
_fb_static_all.sh是 2026-10-21 的手工快照,没有任何脚本引用,且它查的是templates/tasks/index.php里的「任务名称」—— 那块表单 v78 起已搬到templates/tasks/_add_modal.php,所以那条断言在本文件里本来就恒假。已在文件头标注「已废弃,不要运行」,保留只为存档对比。
补充二:全量流水线跑出来的两个「测试基建」问题
这两个都不影响产品代码,但都让流水线白跑 2 小时,且失败信息指向错误的方向。
问题一:随机端口抽到 Chromium 受限端口,反向验证把它报成「断言没牙齿」
v44 反向验证报:按字面读,结论是「那条断言写错了」——完全错误。翻原文才看到:
探针端口是
bind(('127.0.0.1', 0))让系统随机给的,这次抽到 10080,在 Chromium 的受限端口名单里。浏览器连不上 ⇒ 探针崩 ⇒ 上层只看到
「期望的红没出现」⇒ 归因到断言。
为什么特别坑:① 失败信息指向错误方向,让你去审一条完全正确的断言;
② 概率性,上次是绿的,容易被当 flaky 放过;③ 错误在子进程里,
falsify 只贴末尾几行,
ERR_UNSAFE_PORT在更早的位置。修法:抽成公共模块
_tooling/_fb_port.py的pick_port(),抽到黑名单就重抽。把流水线里所有裸抽端口的探针批量换过去 —— 本轮一次改了 61 个文件
(39 个单行式 + 20 个四行式 + 2 个手工),改完 61/61 语法通过。
⚠️ 这里有个教训:
_fb_v94_req.py里我自己几小时前刚踩过 6666 并写了黑名单,但那份逻辑只活在那一个文件里,其余 60 多个探针仍然裸抽 ⇒ 换个套件又栽一次。
同一个坑出现第二次,就该抽成公共设施。
问题二:falsify 的「拷贝清单」漏了它要 patch 的文件
修完端口后,v44 仍然红,但换了个形态:
真根因:
_b44_falsify.py有 5 处注入打的是templates/tasks/_add_modal.php(v78 起任务表单搬到了这个片段),但它的
RESTORE_RELS清单里没有这个文件。后果分两层:
patch()找不到原文 ⇒ 5 处注入全部"不算通过";⇒ 看起来像源码写错了,其实是副本不干净。
本轮实测就是这样:基线读到的标签是「截止时间(有才会出现在日历里)」,
那句正是注入串里的旧行为文案。
而
_b44_falsify.py自己写着这条规矩("凡是 falsify 会 patch 的文件,都必须在这个清单里")—— 规矩写在注释里不会自己生效,所以又被违反了。
修法:补上清单 + 做成静态核对工具
_fb_restore_rels_check.py,登记进流水线(每轮几秒钟)。它的判据是「脚本 patch 的目标文件是否都在拷贝清单里」。
⚠️ 这个检查器自己也有过一次假绿:第一版只认
patch('x','y','z')调用形态,而这批 falsify 用的是
(描述, (路径, 原文, 替换), (断言))嵌套元组⇒ 每个脚本抽出 0 个目标 ⇒ 差集恒空 ⇒ 打印「✅ 全部通过」。
这是最坏的一种假绿:检查器本身没生效,却给出最让人放心的结论。
已加自检:抽出 0 条 = 检查未生效 = 失败(不是通过)。
这一轮修完后 v44 的结果
基线全绿、5 处注入全部命中、
ERR_UNSAFE_PORT出现 0 次。收尾:已实现、已验证、已部署、已提交
改了什么
CSS(
assets/css/app.css):root新增--req: #B03A2E、--req-w: 700--red(#C0533F):那是动作红(删除按钮底色),当文字色放在暖纸底#F7F3EA 上对比度只有 ≈3.4:1,13-14px 的标签字号偏浅。
#B03A2E约 4.6:1。--req-w必须是 700,不能 650/600:本站标签的基线字重本来就是 600(
.form-grid > .field > label、.card .field > label、.mv-row > .mv-lb、.dt-range-label四处都是 600)。写 650 会被取整回 600 = 完全没加粗,用户看到的只剩「变红」,而他要求的是「红色加粗」。
label.is-req/.mv-lb.is-req/.dt-inline-lab.is-req/.dt-range-label.is-req,用
!important打穿各宿主选择器的特异性。.mv-lb::after/.dt-inline-lab::after原本是灰色 400 字重,只改标签本体会出现「红字 + 灰冒号」的半截效果。
.field.is-req > label等),只作用直接子级 ——没写成
.field.is-req label,那会把「提醒」那排 checkbox 的小标签一起染红。#F08A78。模板(16 个文件 / 30 处)
_event_fields.php、_datetime.php:标签闭包第三参数$star→$req,$dtLabelStar→$dtLabelReq,删掉死引用$evLabelStar。<i>*</i>换成给标签加is-req类。.is-req而不靠required属性反推,原因:CSS 没有父选择器,:has()在旧 WebView 不可用,而且「影院名称」「所属板块」本来就靠 JS 校验、DOM 上没有
required。验证结果
_fb_v94_static.py_fb_v94_req.py_fb_v94_live.py线上实测(不是看 CSS 源码,是
getComputedStyle的真实值):rgb(176,58,46)rgb(44,35,29)/ 600rgb(176,58,46)rgb(44,35,29)/ 600rgb(176,58,46)rgb(44,35,29)/ 600rgb(176,58,46)rgb(44,35,29)/ 600冒号也同色(
::after实测rgb(176,58,46)),页面上已无星号。反向验证的 5 类注入(每类都必须让预期的那几条断言变红):
--req-w调回 600 → 加粗类断言红--req调成墨色 → 「必须红」类断言红!important→ 裸<label>那几条红(证明!important确实必需)<label>不覆盖.mv-lb→.mv-lb形态那几条红提交
daf5ca9v94 必填项改成「标签红色加粗」,去掉红色星号 (#3)17 个文件,+176 / -44。