/* ============================================================================ 🔴 v43(用户 2026-10-11 第 6 条)—— 条目详情浮层 ---------------------------------------------------------------------------- 用户原话: 「首页上的日程、首页上的近期任务,以及日程里面的日程,点击它们的时候, 都会弹出一个框显示更详细的信息。上面有不同的功能按钮,比如说详细信息, 在日程里面,如果是观影啊、旅程啊,点击详细信息就会进入到观影或者旅程的 那个相关的页面。如果是日程的话,点击详细信息就进到日程的详细页面。 当然还可以直接点击编辑,点击编辑不同的来源就进入不同的编辑页面, 就在当前的页面弹窗,把之前的窗给去掉,然后弹出编辑的那个框来就可以了。」 ⇒ 一个浮层 + 事件委托,服务全站三种入口: · 首页「今天的日程」(`.ag-ev` / `.ag-allday-item`) · 首页「近期任务」(`.tk-row` 的标题) · 日程页月/周/日视图的条目(`.cal-ev` / `.tg-block`) 设计取舍(每条都对应一个踩过的坑): ① **事件委托挂 document**:列表会被 AJAX 换掉(新建日程后就地插入、 首页任务勾选后重画),一次性绑定永久有效(v40 的教训)。 ② **点条目 = 弹浮层,不跳页**:拦掉 `` 的默认行为。 ⚠️ 必须按 `[data-fb-entry]` 精确匹配 —— 不能笼统拦 `.cal-ev`: 页面里还有"点某天新建日程"那套逻辑(它同样排除 `.cal-ev`)。 ③ **数据全在 `data-fb-entry` 上**:浮层不发请求就能画出来,秒开; 服务端不参与"点开一条"这个动作。 ④ **「编辑」就地换内容**:取该来源的编辑表单片段(`?embed=1`)。 用户原话「把之前的窗给去掉,然后弹出编辑的那个框来」—— 所以是"正文整块换掉",不是叠第二层浮层。 ⑤ **保存走原生提交**:四个来源的 update 路由本来就收普通 POST 并跳转, 不依赖任何页面自带脚本 ⇒ 浮层在任何页面都能保存成功。 (自己写 AJAX 保存要为四个来源各写一套"成功后回哪"的规则,得不偿失。 原生提交会整页刷新 —— 对"改了数据"这件事来说,刷新一次反而更清楚。) ⑥ **Esc 只关最上面那层**:antd 面板开着时先让它收(v38/v39 的坑)。 ⚠️ 本脚本**全站加载**(layout.php):三处入口分散在不同页面,按需加载的判据 会在这些页面之间来回漏;它只有几 KB,不值得为省这点流量赌漏加载。 ============================================================================ */ (function () { 'use strict'; var modal = null; var detailCache = null; /* 详情态的数据(从编辑态「返回」时用它重画) */ function M() { if (!modal) { modal = document.getElementById('fbEntryModal'); } return modal; } function bodyEl() { return document.getElementById('fbEntryBody'); } function titleEl(){ return document.getElementById('fbEntryTitle'); } function tipEl() { return document.getElementById('fbEntryTip'); } function byId(id) { return document.getElementById(id); } function esc(s) { return String(s == null ? '' : s) .replace(/&/g, '&').replace(//g, '>') .replace(/"/g, '"').replace(/'/g, '''); } function setTip(msg, err) { var t = tipEl(); if (!t) { return; } t.textContent = msg || ''; t.className = 'cal-modal-tip' + (err ? ' err' : ''); } /* ---- 底栏两种形态 --------------------------------------------------------- */ function footDetail() { ['fbEntryDetail', 'fbEntryEdit'].forEach(function (id) { var e = byId(id); if (e) { e.hidden = false; } }); ['fbEntryBack', 'fbEntrySave'].forEach(function (id) { var e = byId(id); if (e) { e.hidden = true; } }); } function footEdit() { ['fbEntryDetail', 'fbEntryEdit'].forEach(function (id) { var e = byId(id); if (e) { e.hidden = true; } }); ['fbEntryBack', 'fbEntrySave'].forEach(function (id) { var e = byId(id); if (e) { e.hidden = false; } }); } function footNone() { /* 载入中 / 出错:全都先藏起来 */ ['fbEntryDetail', 'fbEntryEdit', 'fbEntryBack', 'fbEntrySave'].forEach(function (id) { var e = byId(id); if (e) { e.hidden = true; } }); } /* ---- 详情态渲染 ----------------------------------------------------------- */ function rowsHtml(list) { var html = ''; (list || []).forEach(function (r) { if (!r || r[1] === '' || r[1] == null) { return; } html += '
' + esc(r[0]) + '
' + esc(r[1]) + '
'; }); return html || '

这条没有更多信息。

'; } function paintDetail(data) { if (titleEl()) { titleEl().textContent = data.title || '详情'; } if (bodyEl()) { bodyEl().innerHTML = '
' + rowsHtml(data.rows) + '
'; } var det = byId('fbEntryDetail'); if (det) { if (data.detail) { det.href = data.detail; det.hidden = false; } else { det.hidden = true; } } var ed = byId('fbEntryEdit'); if (ed) { ed.disabled = false; ed.hidden = !data.edit; } // 「编辑」不可用时别把它留在底栏上(否则点了没反应) if (!data.edit) { var e2 = byId('fbEntryEdit'); if (e2) { e2.hidden = true; } } setTip(''); footDetail(); if (!data.edit) { var e3 = byId('fbEntryEdit'); if (e3) { e3.hidden = true; } } } function restoreDetail() { if (!detailCache) { return; } var d = detailCache; paintDetail(d); } /* ---- 打开 / 关闭 ---------------------------------------------------------- */ function setOpen(on) { var m = M(); if (!m) { return; } m.hidden = !on; if (!on) { detailCache = null; if (bodyEl()) { bodyEl().innerHTML = ''; } } } function antdOpen() { var d = document.querySelector('.ant-picker-dropdown'); return !!(d && !d.classList.contains('ant-picker-dropdown-hidden')); } /* ---- 编辑片段:取回 → 剥壳 → 塞进正文 → 补跑初始化 ------------------------ */ /* innerHTML 不执行脚本(v42 的血泪结论)⇒ 抠出来重跑。 */ function runInlineScripts(container, html) { var re = /]*>([\s\S]*?)<\/script>/gi, m, code = ''; while ((m = re.exec(html)) !== null) { if (!/\bsrc\s*=/i.test(m[0].split('>')[0])) { code += m[1] + '\n;\n'; } } if (!code) { return; } var s = document.createElement('script'); s.textContent = code; (container || document.body).appendChild(s); s.parentNode.removeChild(s); } /** * 片段可能是三种形态:整页(带 layout)/只带弹框壳/只有表单。 * 我们要的是**表单本体** —— 弹框壳必须剥掉,否则正文里又多一层遮罩。 */ function pickForm(html) { var tmp = document.createElement('div'); tmp.innerHTML = html; var form = tmp.querySelector('#anForm, #mvForm, #nodeForm, #tripForm, #calEditForm, form'); if (form) { return { form: form, html: html }; } return null; } function loadEdit(url) { if (!url) { setTip('这条没有可编辑的表单。', true); return; } setTip(''); footNone(); if (titleEl()) { titleEl().textContent = '载入中…'; } if (bodyEl()) { bodyEl().innerHTML = '

正在载入…

'; } if (!window.fetch) { window.location.href = url; return; } fetch(url, { credentials: 'same-origin', headers: { 'X-Requested-With': 'XMLHttpRequest' } }) .then(function (r) { return r.text(); }) .then(function (html) { if (!bodyEl()) { return; } var picked = pickForm(html); if (!picked) { window.location.href = url; return; } bodyEl().innerHTML = ''; bodyEl().appendChild(picked.form); // 片段里的初始化脚本 + 宿主这侧的通用升级(日期 / antd / 选人 / 上传 / 行程表单) runInlineScripts(bodyEl(), picked.html); if (window.fbUpgradeRange) { window.fbUpgradeRange(); } if (window.fbBootAntd) { window.fbBootAntd(); } if (window.fbNodeForm) { window.fbNodeForm(bodyEl()); } if (typeof window.fbLoadPicker === 'function') { window.fbLoadPicker(); } if (typeof window.fbInitPickers === 'function') { window.fbInitPickers(bodyEl()); } /* 🔴 v47(用户 2026-10-13):**地址联想**也要补一次。 「日程里输入地点要调高德」「行程的出发/到达地要调高德」—— 这两处的编辑表单都是走这条 AJAX 换片段的路径进来的, 而 layout 的按需加载只在**首屏**扫过一次 `[data-addr]` ⇒ 片段里的地点框没人绑,联想永远不出现(正是 v42 在行程上踩过的同一个坑: 内联 script 不执行 + 没人补初始化)。 */ if (typeof window.fbLoadAddr === 'function') { window.fbLoadAddr(bodyEl()); } // 标题取表单片段里的自我描述(各模板的弹框头) var h = picked.form.querySelector('.cal-modal-head b') || (html.match(/aria-label="([^"]*编辑[^"]*)"/) || [])[1]; if (titleEl()) { titleEl().textContent = h ? String(h).trim() : '编辑'; } footEdit(); var f = bodyEl().querySelector('#nf-title, #an-title, #ev-title, #title, input[name="title"]'); if (f) { f.focus(); } }) .catch(function () { window.location.href = url; }); } /** 编辑态「保存」= 原生提交那张表单(服务端 update 路由会跳转,刷新到新页)。 */ function submitEdit() { var form = bodyEl() && (bodyEl().querySelector('form#anForm, form#mvForm, form#nodeForm, form#tripForm, form#calEditForm') || bodyEl().querySelector('form')); if (!form) { setTip('找不到可保存的表单。', true); return; } form.setAttribute('data-fb-entry-submit', '1'); if (typeof form.requestSubmit === 'function') { form.requestSubmit(); } else { form.submit(); } } /* ---- 打开一条 ------------------------------------------------------------- */ function openEntry(el) { var raw = el.getAttribute('data-fb-entry') || ''; var data; try { data = JSON.parse(raw); } catch (e) { return; } /* 数据坏了就别拦点击 */ if (!data || typeof data !== 'object') { return; } detailCache = data; paintDetail(data); setOpen(true); } document.addEventListener('click', function (e) { var t = e.target; if (!t || !t.closest) { return; } if (t.closest('[data-fb-entry-close]')) { e.preventDefault(); setOpen(false); return; } if (t.closest('#fbEntryEdit')) { e.preventDefault(); if (detailCache && detailCache.edit) { loadEdit(detailCache.edit); } return; } if (t.closest('#fbEntryBack')) { e.preventDefault(); restoreDetail(); return; } if (t.closest('#fbEntrySave')) { e.preventDefault(); submitEdit(); return; } var item = t.closest('[data-fb-entry]'); if (item) { e.preventDefault(); openEntry(item); return; } var m = M(); if (m && t === m) { setOpen(false); } /* 点遮罩关;点面板内不关 */ }); document.addEventListener('keydown', function (e) { if (e.key !== 'Escape') { return; } var m = M(); if (!m || m.hidden) { return; } if (antdOpen()) { return; } /* 让 antd 先收它自己的面板 */ e.preventDefault(); setOpen(false); }); /* 编辑态里表单用回车提交时,别让浮层的 Esc 逻辑挡住(默认行为即可)—— 这里不再另挂 submit 监听:原生提交就是要它跳走。 */ window.fbEntryOpen = openEntry; window.fbEntryClose = function () { setOpen(false); }; })();