日程新建/修改不再刷新页面:服务端片段 + 就地重绘 (#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:
@@ -0,0 +1,338 @@
|
||||
/* ============================================================================
|
||||
日程「就地更新」—— 新建 / 修改**不刷新页面**(工单 #37)
|
||||
----------------------------------------------------------------------------
|
||||
用户原话(2026-11-01):
|
||||
「日程又有了新bug,修改一个日程时,页面会刷新,这样如果我新建或者修改的日程
|
||||
不是今天的,那么当刷新后,就看不到我编辑的日程的变化,不知道修改成功了没有。
|
||||
修改和新建,都不应该刷新页面,而是使用js动态调整。」
|
||||
|
||||
---------------------------------------------------------------- 为什么这么写
|
||||
历史上「新建不刷新」是靠**手写复刻服务端渲染**做的(`templates/calendar/index.php`
|
||||
底部那段 `putMonth` / `putTimed` / `relayoutColumn`)。那段代码自己的注释里就写着:
|
||||
· "插入用的定位方式必须与服务端渲染的完全一致,否则刷新一次小条就会跳位置"
|
||||
· 一次真实事故:新加一条 07:00 的日程被 append 到末尾 ⇒ 落进「+N 条」看不见,
|
||||
可刷新一次又跑到第一位
|
||||
修改要处理的情况比新建多得多(跨天变了 / 全天状态变了 / 换了日历本改了颜色 /
|
||||
改了重复规则 / 从这天挪到了别月),复刻必然出错。
|
||||
|
||||
⇒ 本文件改用**服务端渲染片段 → 前端整块替换**:
|
||||
改完 → 取一份服务端画好的视图 → 换掉容器里的内容。
|
||||
只有一处渲染,"插进去的样子"与"刷新一次的样子"**不可能**不一致。
|
||||
|
||||
---------------------------------------------------------------- 三个落点
|
||||
① `[data-cal-view]` 日历页的月/周/日视图(`templates/calendar/_view.php`)
|
||||
② `#homeCard-agenda` 首页「今天的日程」卡片(`templates/home/_card_agenda.php`)
|
||||
③ 「+N 条」清单的 `#calDayData` 数据岛(月视图那一份 JSON,换掉后清单才跟着变对)
|
||||
|
||||
---------------------------------------------------------------- 硬约束(都是踩过的)
|
||||
· **只换内容、不换容器**:容器上挂着 `data-cal-view-kind` 之类的上下文,
|
||||
换掉会让 CSS 的 `.cal-main > *` 重算(视觉会跳一下),而且监听会丢。
|
||||
· **替换前记滚动位置**:`.hc-agenda [data-ag-scroll]` 进场会滚到"现在",
|
||||
不恢复的话用户正看着的那条就被弹走了。
|
||||
· **替换后要补跑三件事**:红线定时器(`data-now-line` 是 app.js 首屏扫的)、
|
||||
「+N 条」数据岛、以及本文件自己的重绑。否则换进来的新节点是"死的"。
|
||||
· **幂等**:脚本全站加载,被重复引入(AJAX 换正文、模板重复)不能绑两遍 ——
|
||||
`window.__fbCalSyncBound` 闸门。
|
||||
· **拿不到片段就退回整页刷新**:宁可刷一次,不要"没刷新却也没变"。
|
||||
用户最怕的就是"不知道修改成功了没有",那种状态比刷新更糟。
|
||||
============================================================================ */
|
||||
(function () {
|
||||
'use strict';
|
||||
if (window.__fbCalSyncBound) { return; }
|
||||
window.__fbCalSyncBound = true;
|
||||
|
||||
/* ---------------------------------------------------------------- 工具 */
|
||||
|
||||
function byId(id) { return document.getElementById(id); }
|
||||
|
||||
/** 当前页面是哪个视图:从容器上读(服务端渲染时就写好了)。 */
|
||||
function viewKind() {
|
||||
var el = document.querySelector('[data-cal-view]');
|
||||
return el ? (el.getAttribute('data-cal-view-kind') || '') : '';
|
||||
}
|
||||
|
||||
/**
|
||||
* 拼局部重绘要请求的地址。
|
||||
*
|
||||
* ⚠️ 参数**照抄当前页面的 URL**:用户在月视图看 11 月,就还取 11 月;
|
||||
* 在周视图看某周,就还取那一周。绝不"按改动的那条日程重新算" ——
|
||||
* 改一条别的月份的日程时,那会把用户正在看的那一屏换掉。
|
||||
* 这条是用户报的现象的核心(改完刷新,跳到了别的月份/别的视图)。
|
||||
*/
|
||||
function viewUrl() {
|
||||
var kind = viewKind();
|
||||
if (!kind) { return ''; }
|
||||
var q = new URLSearchParams();
|
||||
q.set('r', 'calendar/view');
|
||||
q.set('view', kind);
|
||||
/* 上下文全部从**容器自己身上**读(服务端渲染时写的,准)。
|
||||
不去猜隐藏域、也不从页头翻页链接里抠字符串 —— 那些在"翻过页"之后会对不上
|
||||
(踩过:`#calJumpD` 带的是 anchor,翻到 12 月它还是 11 月那天的日期)。 */
|
||||
var el = document.querySelector('[data-cal-view]');
|
||||
var ym = el ? (el.getAttribute('data-cal-ym') || '') : '';
|
||||
var anc = el ? (el.getAttribute('data-cal-anchor') || '') : '';
|
||||
if (kind === 'week' || kind === 'day') {
|
||||
if (anc) { q.set('d', anc); }
|
||||
} else if (ym) {
|
||||
q.set('ym', ym);
|
||||
}
|
||||
return 'index.php?' + q.toString();
|
||||
}
|
||||
|
||||
function fetchHtml(url) {
|
||||
return fetch(url, {
|
||||
credentials: 'same-origin',
|
||||
headers: { 'X-Requested-With': 'XMLHttpRequest' }
|
||||
}).then(function (r) {
|
||||
if (!r.ok) { throw new Error('http ' + r.status); }
|
||||
return r.text();
|
||||
});
|
||||
}
|
||||
|
||||
/* ---------------------------------------------------------------- 重绘 */
|
||||
|
||||
/** 记录 / 恢复首页日程卡片的滚动位置(它进场会滚到"现在")。 */
|
||||
function grabScroll() {
|
||||
var box = document.querySelector('#homeCard-agenda [data-ag-scroll]');
|
||||
return box ? box.scrollTop : null;
|
||||
}
|
||||
function putScroll(top) {
|
||||
if (top === null || top === undefined) { return; }
|
||||
var box = document.querySelector('#homeCard-agenda [data-ag-scroll]');
|
||||
if (box) { box.scrollTop = top; }
|
||||
}
|
||||
|
||||
/**
|
||||
* 把新片段塞进容器:**只换子节点,不动容器本身**。
|
||||
*
|
||||
* 片段顶层是一个 `<div class="cal-view">`(或卡片的 `.hc-body`),
|
||||
* 拿它的 innerHTML 填进现有容器即可 —— 这样容器上的属性/监听都还在。
|
||||
*/
|
||||
function swap(host, html) {
|
||||
var tmp = document.createElement('div');
|
||||
tmp.innerHTML = html;
|
||||
/* 片段顶层可能是 .cal-view(日历)或 .hc-body(卡片),两种都认 */
|
||||
var src = tmp.querySelector('[data-cal-view], .hc-body') || tmp.firstElementChild;
|
||||
if (!src) { return false; }
|
||||
host.innerHTML = src.innerHTML;
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* 重画「+N 条」用的数据岛(月视图)。
|
||||
*
|
||||
* ⚠️ 为什么必须换它:清单内容来自 `#calDayData` 那份 JSON。
|
||||
* 改了一条之后如果不更新,点「+N 条」看到的还是改之前的样子 ——
|
||||
* 这正是本项目 v70 记过的那类"清单与格子不一致"。
|
||||
* 数据岛由服务端随视图片段一起产出(`_view.php` 里就在网格前面),
|
||||
* 所以这里只要把新的那份整段搬过来。
|
||||
*/
|
||||
function syncDayData(html) {
|
||||
var host = byId('calDayData');
|
||||
if (!host) { return; }
|
||||
var tmp = document.createElement('div');
|
||||
tmp.innerHTML = html;
|
||||
var fresh = tmp.querySelector('#calDayData');
|
||||
if (!fresh) { return; }
|
||||
try { host.textContent = fresh.textContent || '{}'; } catch (e) { /* 失败就别动 */ }
|
||||
}
|
||||
|
||||
/** 重绘后补跑那些"首屏扫过一次"的初始化。 */
|
||||
function reinit() {
|
||||
/* 红线:app.js 里那个 IIFE 只在首屏扫 `[data-now-line]` 并起定时器。
|
||||
换进来的新节点没人管它 —— 不补的话线会停在服务端给的位置不动
|
||||
(一小时后再看就明显错了)。这里就地算一次当前位置,并起一个新的定时器
|
||||
(旧的会因为没有节点而空转,量极小;用标记避免重复起)。 */
|
||||
var lines = document.querySelectorAll('[data-now-line]');
|
||||
if (lines.length) {
|
||||
var paint = function () {
|
||||
var d = new Date();
|
||||
var pct = ((d.getHours() * 60 + d.getMinutes()) / 1440) * 100;
|
||||
Array.prototype.forEach.call(lines, function (el) {
|
||||
el.style.top = pct.toFixed(4) + '%';
|
||||
el.setAttribute('title',
|
||||
'现在 ' + ('0' + d.getHours()).slice(-2) + ':' + ('0' + d.getMinutes()).slice(-2));
|
||||
});
|
||||
};
|
||||
paint();
|
||||
if (!window.__fbCalNowTimer) {
|
||||
window.__fbCalNowTimer = setInterval(paint, 60000);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 局部重绘入口。
|
||||
*
|
||||
* @param {Object} [opts]
|
||||
* @param {string} [opts.msg] 成功提示(落在页面上的轻提示里)
|
||||
* @param {boolean} [opts.hard] true = 强制走整页刷新(例如整条系列都变了)
|
||||
* @return {Promise<boolean>} 重绘是否成功
|
||||
*/
|
||||
function refresh(opts) {
|
||||
opts = opts || {};
|
||||
if (opts.hard || !window.fetch) {
|
||||
window.location.reload();
|
||||
return Promise.resolve(false);
|
||||
}
|
||||
|
||||
var hasCal = !!document.querySelector('[data-cal-view]');
|
||||
var hasCard = !!(byId('homeCard-agenda') || document.querySelector('#homeCard-agenda'));
|
||||
|
||||
/* 两个落点都不在(比如在任务页、资源页):这条日程的改动没有可见的地方,
|
||||
安静地成功即可 —— 不要为了"看不见的东西"刷一次页。 */
|
||||
if (!hasCal && !hasCard) { return Promise.resolve(true); }
|
||||
|
||||
var jobs = [];
|
||||
if (hasCal) {
|
||||
var u = viewUrl();
|
||||
if (!u) { window.location.reload(); return Promise.resolve(false); }
|
||||
jobs.push(fetchHtml(u).then(function (html) {
|
||||
var host = document.querySelector('[data-cal-view]');
|
||||
if (!host || !swap(host, html)) { throw new Error('swap'); }
|
||||
syncDayData(html);
|
||||
}));
|
||||
}
|
||||
if (hasCard) {
|
||||
jobs.push(fetchHtml('index.php?r=home/agenda').then(function (html) {
|
||||
var host = document.querySelector('#homeCard-agenda');
|
||||
/* 卡片的外层是 `<section id="homeCard-agenda">`,要换的是里面的 `.hc-body`。
|
||||
当天的日程块本身就是 `.hc-body.hc-agenda` —— 拿它当替换目标。 */
|
||||
var body = host ? host.querySelector('.hc-body') : null;
|
||||
if (!body) { throw new Error('no body'); }
|
||||
var top = grabScroll();
|
||||
if (!swap(body, html)) { throw new Error('swap'); }
|
||||
putScroll(top);
|
||||
}));
|
||||
}
|
||||
|
||||
return Promise.all(jobs).then(function () {
|
||||
reinit();
|
||||
if (opts.msg) { toast(opts.msg); }
|
||||
return true;
|
||||
}).catch(function () {
|
||||
/* 任何一步失败 → 退回整页刷新。宁可刷一次,也不要"没刷新却也没变"。 */
|
||||
window.location.reload();
|
||||
return false;
|
||||
});
|
||||
}
|
||||
|
||||
/* ---------------------------------------------------------------- 轻提示 */
|
||||
|
||||
/**
|
||||
* 页面顶部一句轻提示。
|
||||
*
|
||||
* ⚠️ 为什么不用 `flash`(服务端那条):flash 要等下一次整页渲染才出现,
|
||||
* 而本工单的整个目的就是"不刷新"。没有提示的话,用户在原地看不到任何
|
||||
* 变化时仍然会怀疑"到底存没存上"(他原话:"不知道修改成功了没有")。
|
||||
*/
|
||||
var toastTimer = null;
|
||||
function toast(msg, isErr) {
|
||||
/* ⚠️ 复用站内**已有的**那套浮动小条(`.tk-flash`,v55 为"任务状态改完给一句反馈"
|
||||
加的那一个),不另发明一套 —— 两套视觉迟早不一致,深色模式还要各补一遍。
|
||||
⚠️ 开关只认 `hidden` 属性(与 `templates/tasks/show.php` 同一个用法):
|
||||
`[hidden]` 的 `display:none` 是浏览器默认样式、优先级最低,
|
||||
CSS 里必须有一条 `.tk-flash[hidden]{display:none}` 兜着,
|
||||
否则作者样式里的 `display` 会把它盖掉 ⇒ 提示永远收不起来。 */
|
||||
var box = byId('fbCalToast');
|
||||
if (!box) {
|
||||
box = document.createElement('div');
|
||||
box.id = 'fbCalToast';
|
||||
box.className = 'tk-flash';
|
||||
box.setAttribute('role', 'status');
|
||||
box.setAttribute('aria-live', 'polite');
|
||||
box.hidden = true;
|
||||
document.body.appendChild(box);
|
||||
}
|
||||
box.textContent = msg;
|
||||
box.className = 'tk-flash' + (isErr ? ' is-err' : '');
|
||||
box.hidden = false;
|
||||
if (toastTimer) { clearTimeout(toastTimer); }
|
||||
toastTimer = setTimeout(function () {
|
||||
box.hidden = true;
|
||||
}, 3200);
|
||||
}
|
||||
|
||||
/* ---------------------------------------------------------------- 首页新建 */
|
||||
|
||||
/**
|
||||
* 首页那张「新建日程」表单**也要不刷新**。
|
||||
*
|
||||
* ⚠️ 只接管"首页那条路":判据是页面上**没有** `.cal-wrap`(那是日历模块页)。
|
||||
* 日历页自己有完整的无刷新逻辑(模板底部那段),两边都绑会导致
|
||||
* 提交两次(一次 fetch 一次原生)—— 本项目 `add_modal.js` 的注释里
|
||||
* 记着同一类"绑两遍"事故。
|
||||
*/
|
||||
function bindHomeAddForm() {
|
||||
if (document.querySelector('.cal-wrap')) { return; } /* 日历模块页:不接管 */
|
||||
var form = byId('calAddForm');
|
||||
if (!form || form.getAttribute('data-fb-cal-bound') === '1') { return; }
|
||||
if (!window.fetch) { return; }
|
||||
form.setAttribute('data-fb-cal-bound', '1');
|
||||
|
||||
var tip = byId('calAddTip');
|
||||
|
||||
function sayErr(msg) {
|
||||
if (tip) { tip.textContent = msg; tip.className = 'cal-modal-tip err'; }
|
||||
else { toast(msg, true); }
|
||||
}
|
||||
|
||||
form.addEventListener('submit', function (e) {
|
||||
if (form.getAttribute('data-native-submit') === '1') { return; }
|
||||
e.preventDefault();
|
||||
var sub = byId('calAddSubmit');
|
||||
if (sub) { sub.disabled = true; }
|
||||
if (tip) { tip.textContent = ''; tip.className = 'cal-modal-tip'; }
|
||||
|
||||
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 (sub) { sub.disabled = false; }
|
||||
if (d && d.ok) {
|
||||
/* 关浮层 → 清掉"只填一次"的字段(保留日历本选择,用户常连加几条) */
|
||||
if (window.fbAddModal && form.closest('.cal-modal')) {
|
||||
window.fbAddModal.close('#' + form.closest('.cal-modal').id);
|
||||
} else {
|
||||
var m = form.closest('.cal-modal');
|
||||
if (m) { m.hidden = true; }
|
||||
}
|
||||
['ev-title', 'ev-location', 'ev-note'].forEach(function (id) {
|
||||
var el = byId(id);
|
||||
if (el) { el.value = ''; }
|
||||
});
|
||||
/* 观影/旅程本是派生的:服务端给了 goto,就别急着说"日程已加上" */
|
||||
refresh({ msg: (d.msg || '日程已加上。') });
|
||||
return;
|
||||
}
|
||||
if (d && d.msg) { sayErr(d.msg); return; }
|
||||
/* 拿不到 JSON:会话过期 / 500 → 退回原生提交,让服务端说话 */
|
||||
form.setAttribute('data-native-submit', '1');
|
||||
if (typeof form.requestSubmit === 'function') { form.requestSubmit(); }
|
||||
else { form.submit(); }
|
||||
})
|
||||
.catch(function () {
|
||||
if (sub) { sub.disabled = false; }
|
||||
form.setAttribute('data-native-submit', '1');
|
||||
if (typeof form.requestSubmit === 'function') { form.requestSubmit(); }
|
||||
else { form.submit(); }
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/* ---------------------------------------------------------------- 导出 */
|
||||
|
||||
window.fbCalSync = {
|
||||
refresh: refresh,
|
||||
viewUrl: viewUrl,
|
||||
viewKind: viewKind,
|
||||
toast: toast
|
||||
};
|
||||
|
||||
if (document.readyState === 'loading') {
|
||||
document.addEventListener('DOMContentLoaded', bindHomeAddForm);
|
||||
} else {
|
||||
bindHomeAddForm();
|
||||
}
|
||||
})();
|
||||
@@ -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