日程新建/修改后刷新页面,看不到改动结果,需改为 JS 就地更新 #37

Closed
opened 2026-11-01 00:35:11 +00:00 by laoyang · 2 comments
Owner

现象

用户 2026-11-01:

日程又有了新bug,修改一个日程时,页面会刷新,这样如果我新建或者修改的日程不是今天的,
那么当刷新后,就看不到我编辑的日程的变化,不知道修改成功了没有。
修改和新建,都不应该刷新页面,而是使用js动态调整。

两个具体场景:

① 修改(最严重)

点条目 → 浮层「编辑」→ 改内容 → 「保存」。

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 的月份>。后果:

  • 在首页点进来的(首页只有今天的日程卡片):改的是 12 月的日程 → 刷新后跳到 12 月日历页,
    首页上原本那条本来就不在,「改成功了没有」完全无从判断;
  • 在周/日视图里改:跳回月历页(服务端 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 只管开合,
没有任何提交拦截 ⇒ 首页新建 = 原生提交 = 整页刷新回首页。

根因

  1. calendar/event_update 无 AJAX 分支,前端没有任何「就地更新」的落点;
  2. 「不刷新」的实现只写在日程模块页的内联脚本里,首页那张同源表单拿不到;
  3. 现有 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 的根子是两处渲染必须同步。
改成「服务端渲染片段 → 前端整块替换」后只有一处渲染,问题从根上消失。

验收标准

  1. 首页点日程 → 编辑 → 保存:不刷新、浮层关闭、有明确的「已保存」提示
  2. 日程页月 / 周 / 日视图里编辑 → 保存:不刷新,改动立刻在原位置可见
  3. 首页「新建日程」→ 提交:不刷新、浮层关闭、有提示;若新日程落在今天,卡片上立刻出现
  4. 日程页「新建日程」行为不变(回归)
  5. 改到别的月份 / 全天状态改变 / 重复日程三档 scope:页面不刷新,且提示说清结果
  6. 无 JS 时原生表单提交仍能保存并跳转(渐进增强兜底)
  7. 局部替换后:滚动位置保持、「当前时间红线」计时器恢复、点格子新建等交互仍有效

为什么单独建单

纯网页端改动(index.php 加 AJAX 分支 + assets/js 新增 cal_sync.js + entry_popup.js 保存改 fetch)。#34 是鸿蒙端时间选择器参数写反、#33 是鸿蒙端地址联想,两者都在 FamilyBook_harmonyos 仓库的 .ets 文件里,与本单文件零重叠、验证方式(真机 vs 浏览器)也不同。

## 现象 用户 2026-11-01: > 日程又有了新bug,修改一个日程时,页面会刷新,这样如果我新建或者修改的日程不是今天的, > 那么当刷新后,就看不到我编辑的日程的变化,不知道修改成功了没有。 > 修改和新建,都不应该刷新页面,而是使用js动态调整。 两个具体场景: ### ① 修改(最严重) 点条目 → 浮层「编辑」→ 改内容 → 「保存」。 `assets/js/entry_popup.js` 的 `submitEdit()` **就是原生表单提交**: ```js 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 的月份>`。后果: - 在**首页**点进来的(首页只有今天的日程卡片):改的是 12 月的日程 → 刷新后跳到 12 月日历页, 首页上原本那条本来就不在,「改成功了没有」完全无从判断; - 在**周/日视图**里改:跳回月历页(服务端 `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` 只管开合, **没有任何提交拦截** ⇒ 首页新建 = 原生提交 = 整页刷新回首页。 ## 根因 1. `calendar/event_update` 无 AJAX 分支,前端没有任何「就地更新」的落点; 2. 「不刷新」的实现只写在日程模块页的内联脚本里,首页那张同源表单拿不到; 3. 现有 `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 的根子是**两处渲染必须同步**。 改成「服务端渲染片段 → 前端整块替换」后只有一处渲染,问题从根上消失。 ## 验收标准 1. 首页点日程 → 编辑 → 保存:**不刷新**、浮层关闭、有明确的「已保存」提示 2. 日程页月 / 周 / 日视图里编辑 → 保存:不刷新,改动立刻在原位置可见 3. 首页「新建日程」→ 提交:不刷新、浮层关闭、有提示;若新日程落在今天,卡片上立刻出现 4. 日程页「新建日程」行为不变(回归) 5. 改到别的月份 / 全天状态改变 / 重复日程三档 scope:页面不刷新,且提示说清结果 6. 无 JS 时原生表单提交仍能保存并跳转(渐进增强兜底) 7. 局部替换后:滚动位置保持、「当前时间红线」计时器恢复、点格子新建等交互仍有效 --- ### 为什么单独建单 纯网页端改动(index.php 加 AJAX 分支 + assets/js 新增 cal_sync.js + entry_popup.js 保存改 fetch)。#34 是鸿蒙端时间选择器参数写反、#33 是鸿蒙端地址联想,两者都在 FamilyBook_harmonyos 仓库的 .ets 文件里,与本单文件零重叠、验证方式(真机 vs 浏览器)也不同。
Author
Owner

已实现并上线,等你点验

commit 7f3df7a(已推送 01d5bc2..7f3df7a),已部署 family.yangyq.net。
线上 6 个文件逐字节校验与仓库一致,权限正常,PHP 语法检查全过,站点无 500。

效果

场景 改前 改后
首页点日程 → 编辑 → 保存 整页跳走,落到那条日程所在月份的日历页(首页那条本来就不在,看不出改没改) 不刷新,浮层关闭,顶部一句「日程已保存」,改动原地生效
日程页月视图编辑 整页刷新,ym 跳到 starts_at 的月份 不刷新,仍停在你看的那个月,改动立刻在那一格可见
日程页周视图编辑 整页刷新,被踢回月视图 不刷新,仍是周视图
改到别的月份 跳到那个月 不刷新,视图不动,那条从当前月消失
首页「新建日程」 整页刷新回首页 不刷新,落在今天的立刻出现在卡片上
日程页「新建日程」 已是不刷新(手写插 DOM) 仍不刷新,但改走同一个机制(见下)

关键改法:不再手写复刻渲染

原来「新建不刷新」是手写复刻服务端渲染做的(putMonth / putTimed /
relayoutColumn,约 270 行)。那段代码自己的注释里就记着一次真实事故:

新加一条 07:00 的日程被 append 到末尾 ⇒ 落进「+N 条」看不见,可刷新一次又跑到第一位

根子是「两处渲染必须同步」。修改要处理的情况更多(跨天变了 / 全天状态变了 /
换了日历本改颜色 / 改了重复规则 / 挪到别的月),复刻必然出错。

⇒ 现在改成 服务端渲染片段 → 前端整块替换:只有一处渲染,
「插进去的样子」与「刷新一次的样子」必然相同。
那 270 行手写插入整段删掉,新建/修改/首页新建三条路走同一个 refresh()。

改动清单

服务端

  • calendar/event_update 补 AJAX 分支(原来一条都没有);失败也回 JSON
  • 抽出 calendar_view_data():整页与片段共用同一份取数/建网格
  • 新增片段路由 calendar/view(月/周/日)+ home/agenda(首页卡片)
  • 抽出 templates/calendar/_view.php —— 视图区从页面模板搬出来,
    原文一字未改(用脚本逐行比对证明 270 行零差异)

前端

  • 新增 assets/js/cal_sync.js:取片段替换;只换内容不换容器;
    保首页卡片滚动位置;重绘后重建红线定时器;失败退回整页刷新
  • entry_popup.js 的「保存」改 fetch → 关浮层 + 提示 + 重绘
    (名册/观影/行程/任务四个来源仍是原生提交,不动它们以免扩大回归面)
  • 「点格子新建」的逐元素绑定改成 document 委托(重绘后新节点才有监听)

🔴 本轮实测抓到并修掉的一个真 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 计数判"有没有刷新")

  • ① 月视图编辑:不导航 / 浮层关 / 有提示 / 改动原地可见 / 视图仍 11 月 /
    数据岛同步 / 重新加载后服务端确实是新标题(不只信前端提示)
  • ③ 周视图编辑:不导航 / 仍是周视图 / 改动可见
  • ④ 改到别的月份:不导航 / 视图仍停 11 月 / 那条从 11 月消失
  • ② 首页新建:不导航 / 有提示 / 今天卡片上立刻出现
  • ⑧ 日历页新建(原手写 insert 已删):不导航 / 出现在 3 月 10 日那格 / 浮层关 / 有提示
  • ⑤ 重绘后点格子仍能开新建浮层,且预填 12-25
  • ⑥ 重绘后红线还在,且是"现在"
  • ⑦ 全程零 JS 异常

DOM 等价性 4 项(这是方案的立论基础)
月/周/日视图 + 首页卡片:refresh() 之后的 DOM 与"刷新一次"的 DOM
逐字节相同(规范化掉时间相关的 title/top 与属性顺序后比较)。
关键是这条判据自己也做了反向验证:重绘时故意删一个字、故意多插一个节点,
判据都转红 —— 证明它不是"比较两份同样的东西"而恒绿。

反向验证 7 项(证明判据有牙齿)

故意做坏 判据反应
refresh 置空 改动没出现在原地 → 红
refresh 改成一味 reload 发生了整页导航 → 红
用不调 reinit 的替身 refresh 红线定时器没建立 → 红
抹掉容器上的 data-cal-ym viewUrl 不再带 ym → 红

第一版反向验证报了「抓到 1 / 漏掉 3」,查出来是判据自己的 bug:
page.evaluate('() => {...}') 传字符串只会求值出函数对象、不会调用,
破坏从未注入;另外"红线 title 是'现在 HH:MM'"本身就是服务端渲染时就写好的,
那条判据恒真。两处都改掉后才全抓到。

服务端 11 项(curl,覆盖不带 JS 的用户)

  • 非 AJAX 提交仍 302 回日历,且真的存进库(渐进增强兜底没坏)
  • AJAX 提交 200 + {"ok":true,...},带 date 字段
  • 失败路径也回 {"ok":false,"msg":"找不到这条日程。"}
  • calendar/view 回月/周片段、带数据岛;home/agenda 回卡片片段
  • 未登录访问片段路由被拒(302,不泄漏)

#35 跨天回归 11 项(本次重构动了视图区,必须证明没破坏已验收的成果)
造「11/10 20:00 → 11/12 02:00」跨三天日程,从片段路由取周视图:
data-seg="start/mid/end" 三档都在、is-seg-* 类都在、↳ 延续标记在、
三段贴满整列(left:0; width:100%)、中间天不在全天行。11/11 全过。

静态检查

  • 37 个模板片段无"unset/覆盖宿主变量"违规(新增的 _view.php 在内)
  • 渲染后 20 块内联 JS 逐块 node --check,0 块有问题
  • 运行期日志零 Warning / Notice / Fatal

部署记录

备份:/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>,文件一改版本号就变,不会被旧缓存挡住。

需要你点验

  1. 首页点一条日程 → 编辑 → 改标题 → 保存:页面不该动,顶部出现「日程已保存」,
    浮层关掉。再刷新一次,改的内容还在。
  2. 日程页周视图里改一条:不该被踢回月视图。
  3. 改一条不是本月的日程(把日期改到别的月份):页面不该跳到那个月。
  4. 首页「新建日程」:加上之后首页卡片上该立刻能看到。
  5. 点日历格子新建(月/周/日都试):仍能正常打开浮层并预填你点的位置。

已知取舍

  • 名册 / 观影 / 行程 / 任务的编辑保存仍是整页跳转。它们各自的 update 路由
    没有 JSON 出口,硬改要把四个模块一起动;你这次报的是日程,范围就收在日程上,
    不顺手扩大回归面。要一并改的话另开单。
  • 重绘是"整块替换视图区",所以那一瞬间的 <a> 焦点会丢(键盘用户 Tab 位置会回到
    文档开头)。鼠标/触摸使用无感知。
## 已实现并上线,等你点验 commit `7f3df7a`(已推送 `01d5bc2..7f3df7a`),已部署 family.yangyq.net。 线上 6 个文件逐字节校验与仓库一致,权限正常,PHP 语法检查全过,站点无 500。 ### 效果 | 场景 | 改前 | 改后 | |---|---|---| | 首页点日程 → 编辑 → 保存 | 整页跳走,落到那条日程所在月份的日历页(首页那条本来就不在,看不出改没改) | **不刷新**,浮层关闭,顶部一句「日程已保存」,改动原地生效 | | 日程页月视图编辑 | 整页刷新,`ym` 跳到 `starts_at` 的月份 | **不刷新**,仍停在你看的那个月,改动立刻在那一格可见 | | 日程页周视图编辑 | 整页刷新,**被踢回月视图** | **不刷新**,仍是周视图 | | 改到别的月份 | 跳到那个月 | **不刷新**,视图不动,那条从当前月消失 | | 首页「新建日程」 | 整页刷新回首页 | **不刷新**,落在今天的立刻出现在卡片上 | | 日程页「新建日程」 | 已是不刷新(手写插 DOM) | 仍不刷新,但改走同一个机制(见下) | ### 关键改法:不再手写复刻渲染 原来「新建不刷新」是**手写复刻服务端渲染**做的(`putMonth` / `putTimed` / `relayoutColumn`,约 270 行)。那段代码自己的注释里就记着一次真实事故: > 新加一条 07:00 的日程被 append 到末尾 ⇒ 落进「+N 条」看不见,可刷新一次又跑到第一位 根子是「两处渲染必须同步」。修改要处理的情况更多(跨天变了 / 全天状态变了 / 换了日历本改颜色 / 改了重复规则 / 挪到别的月),复刻必然出错。 ⇒ 现在改成 **服务端渲染片段 → 前端整块替换**:只有一处渲染, 「插进去的样子」与「刷新一次的样子」**必然**相同。 那 270 行手写插入**整段删掉**,新建/修改/首页新建三条路走同一个 `refresh()`。 ### 改动清单 **服务端** - `calendar/event_update` 补 AJAX 分支(原来**一条都没有**);失败也回 JSON - 抽出 `calendar_view_data()`:整页与片段共用同一份取数/建网格 - 新增片段路由 `calendar/view`(月/周/日)+ `home/agenda`(首页卡片) - 抽出 `templates/calendar/_view.php` —— 视图区从页面模板搬出来, **原文一字未改**(用脚本逐行比对证明 270 行零差异) **前端** - 新增 `assets/js/cal_sync.js`:取片段替换;只换内容不换容器; 保首页卡片滚动位置;重绘后重建红线定时器;失败退回整页刷新 - `entry_popup.js` 的「保存」改 fetch → 关浮层 + 提示 + 重绘 (名册/观影/行程/任务四个来源仍是原生提交,不动它们以免扩大回归面) - 「点格子新建」的逐元素绑定改成 document 委托(重绘后新节点才有监听) ### 🔴 本轮实测抓到并修掉的一个真 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` 计数判"有没有刷新") - ① 月视图编辑:不导航 / 浮层关 / 有提示 / 改动原地可见 / 视图仍 11 月 / 数据岛同步 / **重新加载后服务端确实是新标题**(不只信前端提示) - ③ 周视图编辑:不导航 / 仍是周视图 / 改动可见 - ④ 改到别的月份:不导航 / 视图仍停 11 月 / 那条从 11 月消失 - ② 首页新建:不导航 / 有提示 / 今天卡片上立刻出现 - ⑧ 日历页新建(原手写 insert 已删):不导航 / 出现在 3 月 10 日那格 / 浮层关 / 有提示 - ⑤ 重绘后点格子仍能开新建浮层,且预填 12-25 - ⑥ 重绘后红线还在,且是"现在" - ⑦ 全程零 JS 异常 **DOM 等价性 4 项**(这是方案的立论基础) 月/周/日视图 + 首页卡片:`refresh()` 之后的 DOM 与"刷新一次"的 DOM **逐字节相同**(规范化掉时间相关的 `title`/`top` 与属性顺序后比较)。 关键是这条判据**自己也做了反向验证**:重绘时故意删一个字、故意多插一个节点, 判据都转红 —— 证明它不是"比较两份同样的东西"而恒绿。 **反向验证 7 项**(证明判据有牙齿) | 故意做坏 | 判据反应 | |---|---| | `refresh` 置空 | 改动没出现在原地 → 红 | | `refresh` 改成一味 `reload` | 发生了整页导航 → 红 | | 用不调 reinit 的替身 refresh | 红线定时器没建立 → 红 | | 抹掉容器上的 `data-cal-ym` | `viewUrl` 不再带 ym → 红 | > 第一版反向验证报了「抓到 1 / 漏掉 3」,查出来是**判据自己的 bug**: > `page.evaluate('() => {...}')` 传字符串只会求值出函数对象、**不会调用**, > 破坏从未注入;另外"红线 title 是'现在 HH:MM'"本身就是服务端渲染时就写好的, > 那条判据恒真。两处都改掉后才全抓到。 **服务端 11 项**(curl,覆盖不带 JS 的用户) - 非 AJAX 提交仍 **302** 回日历,且**真的存进库**(渐进增强兜底没坏) - AJAX 提交 200 + `{"ok":true,...}`,带 `date` 字段 - 失败路径也回 `{"ok":false,"msg":"找不到这条日程。"}` - `calendar/view` 回月/周片段、带数据岛;`home/agenda` 回卡片片段 - 未登录访问片段路由被拒(302,不泄漏) **#35 跨天回归 11 项**(本次重构动了视图区,必须证明没破坏已验收的成果) 造「11/10 20:00 → 11/12 02:00」跨三天日程,从**片段路由**取周视图: `data-seg="start/mid/end"` 三档都在、`is-seg-*` 类都在、`↳` 延续标记在、 三段贴满整列(`left:0; width:100%`)、**中间天不在全天行**。11/11 全过。 **静态检查** - 37 个模板片段无"unset/覆盖宿主变量"违规(新增的 `_view.php` 在内) - 渲染后 20 块内联 JS 逐块 `node --check`,0 块有问题 - 运行期日志零 Warning / Notice / Fatal ### 部署记录 ``` 备份:/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>`,文件一改版本号就变,**不会被旧缓存挡住**。 ### 需要你点验 1. **首页**点一条日程 → 编辑 → 改标题 → 保存:页面**不该动**,顶部出现「日程已保存」, 浮层关掉。再刷新一次,改的内容还在。 2. **日程页周视图**里改一条:不该被踢回月视图。 3. 改一条**不是本月**的日程(把日期改到别的月份):页面不该跳到那个月。 4. 首页「新建日程」:加上之后首页卡片上该立刻能看到。 5. 点日历格子新建(月/周/日都试):仍能正常打开浮层并预填你点的位置。 ### 已知取舍 - 名册 / 观影 / 行程 / 任务的**编辑**保存仍是整页跳转。它们各自的 update 路由 没有 JSON 出口,硬改要把四个模块一起动;你这次报的是日程,范围就收在日程上, 不顺手扩大回归面。要一并改的话另开单。 - 重绘是"整块替换视图区",所以那一瞬间的 `<a>` 焦点会丢(键盘用户 Tab 位置会回到 文档开头)。鼠标/触摸使用无感知。
Author
Owner

用户验收通过,关单(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')。

验证矩阵(全部做过反向验证)

  • 浏览器端到端 28 项(framenavigated 计数判「有没有刷新」)
  • DOM 等价性 4 项:月/周/日视图 + 首页卡片,重绘后与刷新后 逐字节相同
  • 反向验证 7 项:refresh 置空 / 改成 reload / 不调 reinit / 抹掉上下文属性 → 逐条转红
  • 服务端 11 项(curl):非 AJAX 仍 302 且真存库、AJAX 回 JSON、
    失败也回 JSON、片段路由可用、未登录被拒
  • #35 跨天回归 11 项:三段的 seg/cont/is-seg-* 全在、贴满整列、不进全天行
  • 静态:37 个模板片段无作用域违规;20 块内联 JS 全过;运行期零 Warning

部署与回滚

备份:/home/btops/deploy-backups/fb37-20261101-090509/
回滚:cp -a 上面这个目录/. /www/wwwroot/family.yangyq.net/

已知取舍

  • 名册 / 观影 / 行程 / 任务的编辑保存仍是整页跳转(各自 update 路由无 JSON 出口,
    硬改要动四个模块;本次范围收在日程上)。要一并改请另开单。
  • 重绘是整块替换视图区,那一瞬间 <a> 焦点会丢(键盘用户 Tab 位置回文档开头)。
    鼠标/触摸无感知。

沉淀

相关 17 条踩坑(含本单的 form.id 遮蔽、pipefail+grep -q 假失败、
判据盯的观测点恒真等)已写入
memory/projects/familybook/pitfalls.md。

## 用户验收通过,关单(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')`。 ### 验证矩阵(全部做过反向验证) - 浏览器端到端 **28 项**(`framenavigated` 计数判「有没有刷新」) - DOM 等价性 **4 项**:月/周/日视图 + 首页卡片,重绘后与刷新后 **逐字节相同** - 反向验证 **7 项**:refresh 置空 / 改成 reload / 不调 reinit / 抹掉上下文属性 → 逐条转红 - 服务端 **11 项**(curl):非 AJAX 仍 302 且真存库、AJAX 回 JSON、 失败也回 JSON、片段路由可用、未登录被拒 - #35 跨天回归 **11 项**:三段的 seg/cont/is-seg-* 全在、贴满整列、不进全天行 - 静态:37 个模板片段无作用域违规;20 块内联 JS 全过;运行期零 Warning ### 部署与回滚 ``` 备份:/home/btops/deploy-backups/fb37-20261101-090509/ 回滚:cp -a 上面这个目录/. /www/wwwroot/family.yangyq.net/ ``` ### 已知取舍 - 名册 / 观影 / 行程 / 任务的**编辑**保存仍是整页跳转(各自 update 路由无 JSON 出口, 硬改要动四个模块;本次范围收在日程上)。要一并改请另开单。 - 重绘是整块替换视图区,那一瞬间 `<a>` 焦点会丢(键盘用户 Tab 位置回文档开头)。 鼠标/触摸无感知。 ### 沉淀 相关 17 条踩坑(含本单的 `form.id` 遮蔽、`pipefail`+`grep -q` 假失败、 判据盯的观测点恒真等)已写入 `memory/projects/familybook/pitfalls.md`。
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: laoyang/FamilyBook#37