日程新建/修改不再刷新页面:服务端片段 + 就地重绘 (#37)
用户原话(2026-11-01):
「修改和新建,都不应该刷新页面,而是使用js动态调整。」
## 改法:不再手写复刻渲染,改成"服务端渲染片段 → 前端整块替换"
历史上「新建不刷新」是靠手写复刻服务端渲染做的(`putMonth` / `putTimed` /
`relayoutColumn`),它自己的注释里就记着一次真实事故:新加一条 07:00 的日程被
append 到末尾 ⇒ 落进「+N 条」看不见,刷新后又跑到第一位。修改要处理的情况更多
(跨天 / 全天状态 / 换日历本改颜色 / 改重复规则 / 挪到别的月),复刻必然出错。
⇒ 现在只有**一处渲染**:前端取服务端画好的视图片段整块替换。
"插进去的样子"与"刷新一次的样子"必然相同。
## 服务端
- `calendar/event_update` 认 `X-Requested-With`(与 event_add / event_del 同一套约定)
回 JSON;失败也回 JSON(可预期的拒绝不该把用户甩到一个刷新过的页面上)。
三处失败出口 + 四处成功出口收口成两个闭包,避免"某条路径仍刷页"。
- 抽出 `calendar_view_data()`:整页渲染与局部重绘共用同一份取数/建网格逻辑。
- 新增 `calendar/view` 片段路由(月/周/日视图片段)。
- 抽出 `home_agenda_events()` + 新增 `home/agenda` 片段路由(首页卡片)。
- 抽出 `templates/calendar/_view.php`:视图区从页面模板里搬出来(原文一字未改,
已用脚本逐行比对证明 270 行零差异)。
- `templates/layout.php` 加载 `cal_sync.js`。
## 前端
- 新增 `assets/js/cal_sync.js`:`refresh()` 按当前视图取片段替换;
只换内容不换容器;保首页卡片滚动位置;重绘后补跑红线定时器;
失败退回整页刷新(宁可刷一次,也不要"没刷新却也没变")。
- `entry_popup.js` 的「保存」改 fetch 提交 → 关浮层 + 轻提示 + 局部重绘。
名册/观影/行程/任务四个来源保持原生提交,范围不收不必要的回归面。
- 删掉日历页那 ~270 行手写插入,新建统一走同一个 `refresh()`。
- 「点格子新建」的 per-element 绑定改成 document 委托(重绘后新节点才有监听)。
## 本轮实测抓到并修掉的真 bug
`form.id` 被同名 input 遮蔽:日程编辑表里有 `<input name="id">`,
HTMLFormElement 的具名访问优先级高于原型上的 `id` ⇒ `form.id === 'calEditForm'`
恒为 false ⇒ 日程也走原生提交、整页刷新。
改用 `form.getAttribute('id')`。
## 验证(全部做过反向验证)
- 浏览器端到端 28 项:编辑/新建都不导航、改动原地可见、视图不跳、数据岛同步、
重绘后点格子仍有效、红线定时器重建、无 JS 异常
- 反向验证 7 项:把 refresh 置空 / 改成 reload / 不调 reinit / 抹掉上下文属性,
判据逐条转红(证明判据有牙齿)
- DOM 等价性 4 项:月/周/日视图与首页卡片,重绘后与刷新后的 DOM **逐字节相同**
(这条判据本身也做了反向验证:删一个字、多一个节点都能抓到)
- 服务端 11 项:非 AJAX 仍 302 且真存库、AJAX 回 JSON、失败也回 JSON、
片段路由可用、未登录被拒
- #35 跨天回归 11 项:三段的 seg/cont/is-seg-* 标记全在、贴满整列、不进全天行
- 静态检查:37 个模板片段无作用域违规;20 块内联 JS 语法全过;运行期零 Warning
This commit is contained in:
@@ -46,8 +46,12 @@
|
||||
(页面里还有"点某天新建日程"那套逻辑,它同样排除 `.cal-ev`)。
|
||||
③ **数据全在 `data-fb-entry` 上**:浮层不发请求就能画出来,秒开。
|
||||
④ **详情/编辑都就地换正文**(用户:「就在当前的页面」)——不叠第二层浮层。
|
||||
⑤ **保存走原生提交**:四个来源的 update 路由本来就收普通 POST 并跳转
|
||||
(自己写 AJAX 保存要为四个来源各写一套"成功后回哪",得不偿失)。
|
||||
⑤ **保存:日程走 AJAX,其余来源走原生提交**(工单 #37 起)。
|
||||
· 日程(`#calEditForm`)→ fetch → 成功后原地重绘,**不刷新**。
|
||||
服务端 `calendar/event_update` 认 X-Requested-With 回 JSON(新加的分支)。
|
||||
· 名册 / 观影 / 行程 / 任务 → 仍是原生提交 + 302,行为与以前完全一致。
|
||||
它们各自的 update 路由没有 JSON 出口,硬改要把四个模块一起动;
|
||||
用户报的是日程,范围就收在日程上,不顺手扩大回归面。
|
||||
⑥ **删除走 AJAX**(v49 新增,服务端 `calendar/event_del` 认 X-Requested-With 回 JSON):
|
||||
成功后就地摘掉那一条,**不刷页** —— 与编辑不同,删除不需要"回到哪一页"的语义。
|
||||
⚠️ 两种例外退回整页重载:`removed_forward`(这一次及以后砍掉了整个系列尾段,
|
||||
@@ -308,14 +312,93 @@
|
||||
.catch(function () { window.location.href = url; });
|
||||
}
|
||||
|
||||
/** 编辑态「保存」= 原生提交那张表单(服务端 update 路由会跳转,刷新到新页)。 */
|
||||
/** 找到编辑态那张表单(四个来源的 id 不同,兜底取第一个 form)。 */
|
||||
function editForm() {
|
||||
var b = bodyEl();
|
||||
if (!b) { return null; }
|
||||
return b.querySelector('form#anForm, form#mvForm, form#nodeForm, form#tripForm, form#calEditForm')
|
||||
|| b.querySelector('form');
|
||||
}
|
||||
|
||||
/**
|
||||
* 编辑态「保存」。
|
||||
*
|
||||
* 🔵 工单 #37(用户 2026-11-01)——**用户原话**:
|
||||
* 「修改一个日程时,页面会刷新,这样如果我新建或者修改的日程不是今天的,
|
||||
* 那么当刷新后,就看不到我编辑的日程的变化,不知道修改成功了没有。
|
||||
* 修改和新建,都不应该刷新页面,而是使用js动态调整。」
|
||||
*
|
||||
* ⇒ **日程**(`#calEditForm`)改走 AJAX + 原地重绘;
|
||||
* 另外三个来源(名册 / 观影 / 行程 / 任务…)保持原生提交 ——
|
||||
* 它们各自的 update 路由没有 JSON 出口,硬改就得把四个模块一起动,
|
||||
* 而用户这次报的是日程。范围收在日程上,其它来源行为一字不变(避免回归)。
|
||||
*/
|
||||
function submitEdit() {
|
||||
var form = bodyEl() && (bodyEl().querySelector('form#anForm, form#mvForm, form#nodeForm, form#tripForm, form#calEditForm')
|
||||
|| bodyEl().querySelector('form'));
|
||||
var form = editForm();
|
||||
if (!form) { setTip('找不到可保存的表单。', true); return; }
|
||||
form.setAttribute('data-fb-entry-submit', '1');
|
||||
if (typeof form.requestSubmit === 'function') { form.requestSubmit(); }
|
||||
else { form.submit(); }
|
||||
|
||||
/* 只有日程那张表有 AJAX 出口(`calendar/event_update` 认 X-Requested-With)。
|
||||
其余来源照旧原生提交,与改动前完全一致。 */
|
||||
/* ⚠️⚠️ 必须用 `getAttribute('id')`,**不能**写 `form.id`:
|
||||
日程编辑表里有 `<input type="hidden" name="id">`,而 HTMLFormElement 的具名访问
|
||||
优先级高于原型上的 `id` 属性 ⇒ `form.id` 拿到的是那个 input 元素。
|
||||
原来那句 `form.id === 'calEditForm'` 因此**恒为 false**,日程也走了原生提交、
|
||||
整页刷新(本轮实测抓到:请求上没有 X-Requested-With,响应 302)。 */
|
||||
var isCal = form.getAttribute('id') === 'calEditForm'
|
||||
&& typeof window.fetch === 'function';
|
||||
if (!isCal) {
|
||||
form.setAttribute('data-fb-entry-submit', '1');
|
||||
if (typeof form.requestSubmit === 'function') { form.requestSubmit(); }
|
||||
else { form.submit(); }
|
||||
return;
|
||||
}
|
||||
submitEditAsync(form);
|
||||
}
|
||||
|
||||
/** 日程:fetch 提交 → 成功则关浮层 + 局部重绘;失败留在浮层里让人改。 */
|
||||
function submitEditAsync(form) {
|
||||
var saveBtn = byId('fbEntrySave');
|
||||
if (saveBtn) { saveBtn.disabled = true; }
|
||||
setTip('正在保存…');
|
||||
footNone(); /* 保存中把按钮收起来,防连点(重复提交在重复日程上后果很重) */
|
||||
|
||||
fetch(form.getAttribute('action'), {
|
||||
method: 'POST', body: new FormData(form), credentials: 'same-origin',
|
||||
headers: { 'X-Requested-With': 'XMLHttpRequest', 'Accept': 'application/json' }
|
||||
})
|
||||
.then(function (r) { return r.json().catch(function () { return null; }); })
|
||||
.then(function (d) {
|
||||
if (saveBtn) { saveBtn.disabled = false; }
|
||||
if (d && d.ok) {
|
||||
setOpen(false);
|
||||
/* `moved_series`(改「这一次」/「从现在到以后」)= 整条系列都变了,
|
||||
影响面超出当前这一屏 ⇒ 交给 refresh 自己判断要不要硬刷。 */
|
||||
var sync = window.fbCalSync;
|
||||
if (sync && typeof sync.refresh === 'function') {
|
||||
sync.refresh({ msg: (d.msg || '已保存。'), hard: !!d.moved_series });
|
||||
} else {
|
||||
/* 没加载到脚本(老缓存):退回刷新,别当成功 */
|
||||
window.location.reload();
|
||||
}
|
||||
return;
|
||||
}
|
||||
/* 业务拒绝(没权限 / 时间不对 / 标题为空):留在浮层里,让用户改完再存 */
|
||||
if (d && d.msg) {
|
||||
setTip(d.msg, true);
|
||||
footShow('edit');
|
||||
return;
|
||||
}
|
||||
/* 拿不到 JSON:会话过期 / 500 → 退回原生提交,让服务端说话(与改动前一致) */
|
||||
form.setAttribute('data-fb-entry-submit', '1');
|
||||
if (typeof form.requestSubmit === 'function') { form.requestSubmit(); }
|
||||
else { form.submit(); }
|
||||
})
|
||||
.catch(function () {
|
||||
if (saveBtn) { saveBtn.disabled = false; }
|
||||
form.setAttribute('data-fb-entry-submit', '1');
|
||||
if (typeof form.requestSubmit === 'function') { form.requestSubmit(); }
|
||||
else { form.submit(); }
|
||||
});
|
||||
}
|
||||
|
||||
/* ---- ③ 详情:就地展开更多信息 ----------------------------------------------
|
||||
|
||||
Reference in New Issue
Block a user