用户 2026-11-01:
日程又有了新bug,修改一个日程时,页面会刷新,这样如果我新建或者修改的日程不是今天的, 那么当刷新后,就看不到我编辑的日程的变化,不知道修改成功了没有。 修改和新建,都不应该刷新页面,而是使用js动态调整。
两个具体场景:
点条目 → 浮层「编辑」→ 改内容 → 「保存」。
assets/js/entry_popup.js 的 submitEdit() 就是原生表单提交:
assets/js/entry_popup.js
submitEdit()
function submitEdit() { var form = bodyEl() && (...); form.setAttribute('data-fb-entry-submit', '1'); if (typeof form.requestSubmit === 'function') { form.requestSubmit(); } else { form.submit(); } }
⇒ 整页跳走,落到 calendar?ym=<starts_at 的月份>。后果:
calendar?ym=<starts_at 的月份>
redirect('calendar', ['ym'=>...])
calendar/event_update
event_add
event_del
templates/calendar/index.php 底部那段内联 JS 已经实现了「新建不刷新」(putMonth / putTimed / relayoutColumn),但只在日程模块页生效 —— 它 document.getElementById('calAddForm') 拿的是本页那张表单。
templates/calendar/index.php
putMonth
putTimed
relayoutColumn
document.getElementById('calAddForm')
而首页卡片右上角那颗「新建日程」(HomeCards::TYPES['agenda']['add'])用的是同一份 templates/calendar/_add_modal.php,由 assets/js/add_modal.js 只管开合, 没有任何提交拦截 ⇒ 首页新建 = 原生提交 = 整页刷新回首页。
HomeCards::TYPES['agenda']['add']
templates/calendar/_add_modal.php
assets/js/add_modal.js
calendar/event_update 认 X-Requested-With(与 event_add / event_del 同一套约定), 回 JSON:{ok, msg, event:{id, starts_at, ends_at, all_day, cal, color, time, title, entry}}。 scope 为 once / future 时同样回 JSON(并标明是否动了整条系列,前端据此决定重绘范围)。 无 JS 的原生表单提交照旧 302,渐进增强的兜底不变。
X-Requested-With
{ok, msg, event:{id, starts_at, ends_at, all_day, cal, color, time, title, entry}}
scope
once
future
新增 assets/js/cal_sync.js(按需加载,通篇幂等):
assets/js/cal_sync.js
fbCalSync.refresh(opts)
#calAddForm
.cal-wrap
fbCalSync.afterSave()
entry_popup.js
entry_popup.js 的 submitEdit() 改为 fetch 提交 → 成功后关浮层 + 提示「已保存」+ 调用局部重绘。
现有 putMonth 的注释已经记着一次真实事故:新加一条 07:00 的日程被 append 到末尾 ⇒ 落进「+N 条」看不见,刷新后又跑到第一位。这类 bug 的根子是两处渲染必须同步。 改成「服务端渲染片段 → 前端整块替换」后只有一处渲染,问题从根上消失。
纯网页端改动(index.php 加 AJAX 分支 + assets/js 新增 cal_sync.js + entry_popup.js 保存改 fetch)。#34 是鸿蒙端时间选择器参数写反、#33 是鸿蒙端地址联想,两者都在 FamilyBook_harmonyos 仓库的 .ets 文件里,与本单文件零重叠、验证方式(真机 vs 浏览器)也不同。
commit 7f3df7a(已推送 01d5bc2..7f3df7a),已部署 family.yangyq.net。 线上 6 个文件逐字节校验与仓库一致,权限正常,PHP 语法检查全过,站点无 500。
7f3df7a
01d5bc2..7f3df7a
ym
starts_at
原来「新建不刷新」是手写复刻服务端渲染做的(putMonth / putTimed / relayoutColumn,约 270 行)。那段代码自己的注释里就记着一次真实事故:
新加一条 07:00 的日程被 append 到末尾 ⇒ 落进「+N 条」看不见,可刷新一次又跑到第一位
根子是「两处渲染必须同步」。修改要处理的情况更多(跨天变了 / 全天状态变了 / 换了日历本改颜色 / 改了重复规则 / 挪到别的月),复刻必然出错。
⇒ 现在改成 服务端渲染片段 → 前端整块替换:只有一处渲染, 「插进去的样子」与「刷新一次的样子」必然相同。 那 270 行手写插入整段删掉,新建/修改/首页新建三条路走同一个 refresh()。
refresh()
服务端
calendar_view_data()
calendar/view
home/agenda
templates/calendar/_view.php
前端
form.id 被同名 input 遮蔽:日程编辑表里有 <input name="id">, 而 HTMLFormElement 的具名访问优先级高于原型上的 id ⇒ form.id === 'calEditForm' 恒为 false ⇒ 日程也走原生提交、照旧整页刷新。
form.id
<input name="id">
id
form.id === 'calEditForm'
第一版就是这样:功能"看起来没生效",但没有任何报错。改用 form.getAttribute('id')。 (这条要是没抓到,整个工单等于没做。)
form.getAttribute('id')
浏览器端到端 28 项(Playwright + 真实 PHP + 真实 MySQL,framenavigated 计数判"有没有刷新")
framenavigated
DOM 等价性 4 项(这是方案的立论基础) 月/周/日视图 + 首页卡片:refresh() 之后的 DOM 与"刷新一次"的 DOM 逐字节相同(规范化掉时间相关的 title/top 与属性顺序后比较)。 关键是这条判据自己也做了反向验证:重绘时故意删一个字、故意多插一个节点, 判据都转红 —— 证明它不是"比较两份同样的东西"而恒绿。
title
top
反向验证 7 项(证明判据有牙齿)
refresh
reload
data-cal-ym
viewUrl
第一版反向验证报了「抓到 1 / 漏掉 3」,查出来是判据自己的 bug: page.evaluate('() => {...}') 传字符串只会求值出函数对象、不会调用, 破坏从未注入;另外"红线 title 是'现在 HH:MM'"本身就是服务端渲染时就写好的, 那条判据恒真。两处都改掉后才全抓到。
page.evaluate('() => {...}')
服务端 11 项(curl,覆盖不带 JS 的用户)
{"ok":true,...}
date
{"ok":false,"msg":"找不到这条日程。"}
#35 跨天回归 11 项(本次重构动了视图区,必须证明没破坏已验收的成果) 造「11/10 20:00 → 11/12 02:00」跨三天日程,从片段路由取周视图: data-seg="start/mid/end" 三档都在、is-seg-* 类都在、↳ 延续标记在、 三段贴满整列(left:0; width:100%)、中间天不在全天行。11/11 全过。
data-seg="start/mid/end"
is-seg-*
↳
left:0; width:100%
静态检查
_view.php
node --check
备份:/home/btops/deploy-backups/fb37-20261101-090509/(按目录结构保存 4 个被覆盖的文件) 回滚:cp -a /home/btops/deploy-backups/fb37-20261101-090509/. /www/wwwroot/family.yangyq.net/
线上 cal_sync.js sha256 前 32 位 e475256c80422518c43d03af6b74cb00,与仓库一致。 asset() 会自动带 ?v=<md5>,文件一改版本号就变,不会被旧缓存挡住。
cal_sync.js
e475256c80422518c43d03af6b74cb00
asset()
?v=<md5>
<a>
用户 2026-11-01 回复:「好了,37可以关闭了。」
commit 7f3df7a,已部署 family.yangyq.net(6 个文件 sha256 与仓库一致)。
核心改法:不再手写复刻服务端渲染,改成「服务端渲染片段 → 前端整块替换」。 原来那 270 行手写插入(putMonth / putTimed / relayoutColumn)整段删掉 —— 只有一处渲染,「插进去的样子」与「刷新一次的样子」必然相同。
服务端:index.php(event_update 补 AJAX 分支、抽 calendar_view_data()、 新增 calendar/view + home/agenda 片段路由、抽 home_agenda_events())、 templates/calendar/_view.php(新建,视图区原文一字未改搬出)、 templates/layout.php(加载 cal_sync.js)
index.php
event_update
home_agenda_events()
templates/layout.php
前端:assets/js/cal_sync.js(新建)、assets/js/entry_popup.js(保存改 fetch)、 templates/calendar/index.php(删手写插入、格子改事件委托)
form.id 被同名 input 遮蔽:日程编辑表里有 <input name="id">, HTMLFormElement 的具名访问优先级高于原型 id ⇒ form.id === 'calEditForm' 恒为 false ⇒ 日程照旧整页刷新,且没有任何报错。改用 form.getAttribute('id')。
备份:/home/btops/deploy-backups/fb37-20261101-090509/ 回滚:cp -a 上面这个目录/. /www/wwwroot/family.yangyq.net/
相关 17 条踩坑(含本单的 form.id 遮蔽、pipefail+grep -q 假失败、 判据盯的观测点恒真等)已写入 memory/projects/familybook/pitfalls.md。
pipefail
grep -q
memory/projects/familybook/pitfalls.md
No dependencies set.
The note is not visible to the blocked user.
现象
用户 2026-11-01:
两个具体场景:
① 修改(最严重)
点条目 → 浮层「编辑」→ 改内容 → 「保存」。
assets/js/entry_popup.js的submitEdit()就是原生表单提交:⇒ 整页跳走,落到
calendar?ym=<starts_at 的月份>。后果:首页上原本那条本来就不在,「改成功了没有」完全无从判断;
redirect('calendar', ['ym'=>...])不带 view),视图都变了,更看不出改了什么;
calendar/event_update完全没有 AJAX 分支(只有event_add/event_del有),所以这条路根本没有「就地更新」的可能。
② 新建(首页那条路)
templates/calendar/index.php底部那段内联 JS 已经实现了「新建不刷新」(putMonth/putTimed/relayoutColumn),但只在日程模块页生效 —— 它document.getElementById('calAddForm')拿的是本页那张表单。而首页卡片右上角那颗「新建日程」(
HomeCards::TYPES['agenda']['add'])用的是同一份templates/calendar/_add_modal.php,由assets/js/add_modal.js只管开合,没有任何提交拦截 ⇒ 首页新建 = 原生提交 = 整页刷新回首页。
根因
calendar/event_update无 AJAX 分支,前端没有任何「就地更新」的落点;putMonth/putTimed/relayoutColumn是手写复刻服务端渲染(注释里自己写着「必须与服务端渲染的完全一致,否则刷新一次小条就会跳位置」),每加一条渲染规则就要改两处,
是这类「插入结果与刷新结果不一致」的长期来源。
方案
服务端
calendar/event_update认X-Requested-With(与event_add/event_del同一套约定),回 JSON:
{ok, msg, event:{id, starts_at, ends_at, all_day, cal, color, time, title, entry}}。scope为once/future时同样回 JSON(并标明是否动了整条系列,前端据此决定重绘范围)。无 JS 的原生表单提交照旧 302,渐进增强的兜底不变。
前端:改成「片段局部重绘」而不是手写插 DOM
新增
assets/js/cal_sync.js(按需加载,通篇幂等):fbCalSync.refresh(opts):按当前视图 fetch 片段,替换网格 / 卡片区域 ——内容由服务端同一份模板渲染,天然与「刷新一次」完全一致,不再需要手写复刻;
#calAddForm的提交(仅当页面没有.cal-wrap时,即首页那条路);日历模块页仍由页面自己的逻辑管,避免重复绑定;
fbCalSync.afterSave()供entry_popup.js在保存成功后调用。entry_popup.js的submitEdit()改为 fetch 提交 → 成功后关浮层 + 提示「已保存」+ 调用局部重绘。为什么不用继续手写插入
现有
putMonth的注释已经记着一次真实事故:新加一条 07:00 的日程被 append 到末尾 ⇒落进「+N 条」看不见,刷新后又跑到第一位。这类 bug 的根子是两处渲染必须同步。
改成「服务端渲染片段 → 前端整块替换」后只有一处渲染,问题从根上消失。
验收标准
为什么单独建单
纯网页端改动(index.php 加 AJAX 分支 + assets/js 新增 cal_sync.js + entry_popup.js 保存改 fetch)。#34 是鸿蒙端时间选择器参数写反、#33 是鸿蒙端地址联想,两者都在 FamilyBook_harmonyos 仓库的 .ets 文件里,与本单文件零重叠、验证方式(真机 vs 浏览器)也不同。
已实现并上线,等你点验
commit
7f3df7a(已推送01d5bc2..7f3df7a),已部署 family.yangyq.net。线上 6 个文件逐字节校验与仓库一致,权限正常,PHP 语法检查全过,站点无 500。
效果
ym跳到starts_at的月份关键改法:不再手写复刻渲染
原来「新建不刷新」是手写复刻服务端渲染做的(
putMonth/putTimed/relayoutColumn,约 270 行)。那段代码自己的注释里就记着一次真实事故:根子是「两处渲染必须同步」。修改要处理的情况更多(跨天变了 / 全天状态变了 /
换了日历本改颜色 / 改了重复规则 / 挪到别的月),复刻必然出错。
⇒ 现在改成 服务端渲染片段 → 前端整块替换:只有一处渲染,
「插进去的样子」与「刷新一次的样子」必然相同。
那 270 行手写插入整段删掉,新建/修改/首页新建三条路走同一个
refresh()。改动清单
服务端
calendar/event_update补 AJAX 分支(原来一条都没有);失败也回 JSONcalendar_view_data():整页与片段共用同一份取数/建网格calendar/view(月/周/日)+home/agenda(首页卡片)templates/calendar/_view.php—— 视图区从页面模板搬出来,原文一字未改(用脚本逐行比对证明 270 行零差异)
前端
assets/js/cal_sync.js:取片段替换;只换内容不换容器;保首页卡片滚动位置;重绘后重建红线定时器;失败退回整页刷新
entry_popup.js的「保存」改 fetch → 关浮层 + 提示 + 重绘(名册/观影/行程/任务四个来源仍是原生提交,不动它们以免扩大回归面)
🔴 本轮实测抓到并修掉的一个真 bug
form.id被同名 input 遮蔽:日程编辑表里有<input name="id">,而 HTMLFormElement 的具名访问优先级高于原型上的
id⇒form.id === 'calEditForm'恒为 false ⇒ 日程也走原生提交、照旧整页刷新。第一版就是这样:功能"看起来没生效",但没有任何报错。改用
form.getAttribute('id')。(这条要是没抓到,整个工单等于没做。)
验证:28 项浏览器 + 11 项服务端 + 11 项 #35 回归,全部做过反向验证
浏览器端到端 28 项(Playwright + 真实 PHP + 真实 MySQL,
framenavigated计数判"有没有刷新")数据岛同步 / 重新加载后服务端确实是新标题(不只信前端提示)
DOM 等价性 4 项(这是方案的立论基础)
月/周/日视图 + 首页卡片:
refresh()之后的 DOM 与"刷新一次"的 DOM逐字节相同(规范化掉时间相关的
title/top与属性顺序后比较)。关键是这条判据自己也做了反向验证:重绘时故意删一个字、故意多插一个节点,
判据都转红 —— 证明它不是"比较两份同样的东西"而恒绿。
反向验证 7 项(证明判据有牙齿)
refresh置空refresh改成一味reloaddata-cal-ymviewUrl不再带 ym → 红服务端 11 项(curl,覆盖不带 JS 的用户)
{"ok":true,...},带date字段{"ok":false,"msg":"找不到这条日程。"}calendar/view回月/周片段、带数据岛;home/agenda回卡片片段#35 跨天回归 11 项(本次重构动了视图区,必须证明没破坏已验收的成果)
造「11/10 20:00 → 11/12 02:00」跨三天日程,从片段路由取周视图:
data-seg="start/mid/end"三档都在、is-seg-*类都在、↳延续标记在、三段贴满整列(
left:0; width:100%)、中间天不在全天行。11/11 全过。静态检查
_view.php在内)node --check,0 块有问题部署记录
线上
cal_sync.jssha256 前 32 位e475256c80422518c43d03af6b74cb00,与仓库一致。asset()会自动带?v=<md5>,文件一改版本号就变,不会被旧缓存挡住。需要你点验
浮层关掉。再刷新一次,改的内容还在。
已知取舍
没有 JSON 出口,硬改要把四个模块一起动;你这次报的是日程,范围就收在日程上,
不顺手扩大回归面。要一并改的话另开单。
<a>焦点会丢(键盘用户 Tab 位置会回到文档开头)。鼠标/触摸使用无感知。
用户验收通过,关单(2026-11-01)
用户 2026-11-01 回复:「好了,37可以关闭了。」
交付回顾
commit
7f3df7a,已部署 family.yangyq.net(6 个文件 sha256 与仓库一致)。核心改法:不再手写复刻服务端渲染,改成「服务端渲染片段 → 前端整块替换」。
原来那 270 行手写插入(
putMonth/putTimed/relayoutColumn)整段删掉 ——只有一处渲染,「插进去的样子」与「刷新一次的样子」必然相同。
改动文件
服务端:
index.php(event_update补 AJAX 分支、抽calendar_view_data()、新增
calendar/view+home/agenda片段路由、抽home_agenda_events())、templates/calendar/_view.php(新建,视图区原文一字未改搬出)、templates/layout.php(加载 cal_sync.js)前端:
assets/js/cal_sync.js(新建)、assets/js/entry_popup.js(保存改 fetch)、templates/calendar/index.php(删手写插入、格子改事件委托)本轮实测抓到并修掉的真 bug
form.id被同名 input 遮蔽:日程编辑表里有<input name="id">,HTMLFormElement 的具名访问优先级高于原型
id⇒form.id === 'calEditForm'恒为 false⇒ 日程照旧整页刷新,且没有任何报错。改用
form.getAttribute('id')。验证矩阵(全部做过反向验证)
framenavigated计数判「有没有刷新」)失败也回 JSON、片段路由可用、未登录被拒
部署与回滚
已知取舍
硬改要动四个模块;本次范围收在日程上)。要一并改请另开单。
<a>焦点会丢(键盘用户 Tab 位置回文档开头)。鼠标/触摸无感知。
沉淀
相关 17 条踩坑(含本单的
form.id遮蔽、pipefail+grep -q假失败、判据盯的观测点恒真等)已写入
memory/projects/familybook/pitfalls.md。