Files
FamilyBook/assets/js/entry_popup.js
T
laoyang 8c651b43eb v43 第 43 批:六条反馈(界面 5 + 条目详情浮层)
用户 2026-10-11 反馈六条,逐条定位真根因后修复:

① 纪念日「日期」多出一个控件 —— 只保留最新的那个
   两层根因:(a)原生 `input[type=date]` 当初没包在 `.dt-native` 里,
   v42 那条"AntD 挂上后收起原生框"的选择器根本不命中;(b)更要命的是
   `.dt-native` 基类写着 `display: contents` —— 该元素**没有盒子**,
   给它 position/width 完全无效 ⇒ 必须把 display 显式改回 block 才收得起来。

② 编辑纪念日把底层列表页弄没 —— 「修改」原来是整页跳 `days/edit`
   (套 layout = 一张只有弹框的空白页)。改为 `?embed=1` 只吐弹框那一块,
   列表页 JS 取回来替换 `#anNewCard` 并打开 —— 与「新建」同一观感。
   内嵌形态标 `$anModalOnly` 以免打 standalone 标记(关闭时不再跳页)。

③ 拖图裁剪的层被弹框盖住 —— `.crop-overlay` 是 z-index:100 而 `.cal-modal`
   是 200 ⇒ 裁剪层被整块盖住。提到 260(与 v41 的 .cn-modal 同一处理)。

④ 行程弹框里"参与的人"弹不出选人框 —— person_picker.js 只绑定一次,
   而弹框表单是 AJAX 片段 ⇒ 里面的 `.pp` 从未被 init。三件事一起:
   暴露 `window.fbInitPickers()`(幂等)+ layout 暴露 `window.fbLoadPicker()`
   (脚本是按需加载的)+ 塞完片段后补调初始化。

⑤ 日程新建排版/控件/文案 —— 改走观影式 `.mv-grid`(标签对齐 + 冒号);
   起止补上 `$dtPlugin='antd'`(试点正式推广到日程);标签「时间范围」→「开始 / 结束」;
   删三句提示(结束留空 / 多选提醒 / 全天)。

⑥ 条目详情浮层(首页日程 / 首页近期任务 / 日程页条目)——
   点条目不再直接跳页,弹浮层显示详情;「详细信息」跳该来源详情页;
   「编辑」就地换成该来源的编辑表单(四处 `?embed=1` 片段)。
   新增 `templates/_entry_popup.php` + `assets/js/entry_popup.js` +
   `fb_entry_json()`(三个入口共用一个编码函数)。

验证:php -l 157 文件全过;div 深度 33 组合全配平;片段体检 20 片段无越界;
本地 e2e 1510/0;v43 真机 33/0;v43 反向验证 8/0;v38 36/0;v42 33/0;
b31/b32 反向验证 8/0 与 5/0;部署断言本地空跑 RC=0;完整流水线 PUSH_RC=0。
2026-10-12 00:24:20 +08:00

245 lines
11 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/* ============================================================================
🔴 v43(用户 2026-10-11 第 6 条)—— 条目详情浮层
----------------------------------------------------------------------------
用户原话:
「首页上的日程、首页上的近期任务,以及日程里面的日程,点击它们的时候,
都会弹出一个框显示更详细的信息。上面有不同的功能按钮,比如说详细信息,
在日程里面,如果是观影啊、旅程啊,点击详细信息就会进入到观影或者旅程的
那个相关的页面。如果是日程的话,点击详细信息就进到日程的详细页面。
当然还可以直接点击编辑,点击编辑不同的来源就进入不同的编辑页面,
就在当前的页面弹窗,把之前的窗给去掉,然后弹出编辑的那个框来就可以了。」
⇒ 一个浮层 + 事件委托,服务全站三种入口:
· 首页「今天的日程」(`.ag-ev` / `.ag-allday-item`)
· 首页「近期任务」(`.tk-row` 的标题)
· 日程页月/周/日视图的条目(`.cal-ev` / `.tg-block`)
设计取舍(每条都对应一个踩过的坑):
① **事件委托挂 document**:列表会被 AJAX 换掉(新建日程后就地插入、
首页任务勾选后重画),一次性绑定永久有效(v40 的教训)。
② **点条目 = 弹浮层,不跳页**:拦掉 `<a href>` 的默认行为。
⚠️ 必须按 `[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, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;')
.replace(/"/g, '&quot;').replace(/'/g, '&#39;');
}
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 += '<div class="kv-row"><dt>' + esc(r[0]) + '</dt><dd>' + esc(r[1]) + '</dd></div>';
});
return html || '<p class="empty-hint">这条没有更多信息。</p>';
}
function paintDetail(data) {
if (titleEl()) { titleEl().textContent = data.title || '详情'; }
if (bodyEl()) { bodyEl().innerHTML = '<dl class="kv">' + rowsHtml(data.rows) + '</dl>'; }
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 = /<script\b[^>]*>([\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 = '<p class="empty-hint">正在载入…</p>'; }
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()); }
// 标题取表单片段里的自我描述(各模板的弹框头)
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); };
})();