Files
FamilyBook/assets/js/entry_popup.js
T
laoyang 1bf56b114b v49:条目浮层的「详情/编辑/删除」按**来源**分形态(三颗靠右 + 就地展开/编辑 + AJAX 删除)
用户 2026-10-14 第 2 条。用户原话:
  「下面三个按钮,都放在下面的右侧…分别是详情、编辑和删除。详细用蓝色,编辑用黄色,删除用红色。
    点击详细的时候,**如果这是一个日程**(正常日程里的事件、不是来自观影/旅程等模块)
    → 就**在当前页面让模态框变大**、显示更多信息。
    点击编辑,也是**在当前的界面**弹出编辑日程的框,就地编辑;编辑完之后下面两个按钮:取消和保存。
    点击删除,就直接把这个日程给删除。
    如果当前这个日程**来自其他模块**(观影 / 旅程 / 其它)→ 点击详情就**进入那些模块**展示它。
    **这种情况下,下面就没有编辑、也没有删除,就只有一个详情。**」
拍板:① 应用范围 = 首页日程 + 日程页条目(首页近期任务保持 v43 现状);
      ② 重复日程删除 → 弹三档范围(这一次 / 这一次及以后 / 全部)。

实现(7 个文件):
 · app/helpers.php  fb_entry_json() 扩契约:source('event'/'derived') / ref / del / info /
   repeat / occ。判据 `kind==='custom' && ref_id>0`(**不是**"有没有 edit")。
 · app/Calendar.php  month() 给自定义条目带 _can_write(供 del 判定)。
 · templates/_entry_popup.php  底栏重构成「右侧按钮组 + 7 颗」:详情态三颗、
   编辑态两颗(取消/保存)、删除范围态两颗(取消/确认删除);另加一颗
   **遮罩层里的 CSRF**(正文会被整块换掉,放正文会丢)。
 · assets/js/entry_popup.js  四态状态机(detail/info/edit/delscope),按 source 决定显哪几颗。
 · assets/css/app.css  .fb-entry-acts(margin-left:auto 靠右)+ fb-a-detail/edit/del 三色
   + .is-wide(620→860px)。⚠️ 全是**作用域类**,不动全局 .btn-edit/.btn-danger 的语义。
 · index.php  新增 calendar/event?embed=detail(只吐只读详情块);
   calendar/event_del **认 X-Requested-With 回 JSON**($done() 收口,失败也回原因)。
 · templates/calendar/event.php  支持 $evDetail(只吐 kv 只读块,与可写与否无关)。

本轮修掉两个真 bug(都是"藏不掉"):
 ① `.btn{display:inline-flex}` 是作者样式,**盖过 [hidden] 的浏览器默认**
    ⇒ 底栏四态切换时前面的按钮收不起来、全挤在一条上(派生条目本该只有「详情」,
      实测三颗都露着)。⇒ 显式写 `.fb-entry-acts .btn[hidden]{display:none!important}`。
      本项目文件前部已有同款注释(.pick-row[hidden] 等),属第 N 次同类。
 ② `footShow('detail')` 把三颗无条件点亮,**盖掉** paintDetail 按来源设的 hidden。
    ⇒ detail 组只列「详情」,且 paintDetail 里**先 footShow 再设 edit/del**(顺序写进注释)。

验证(新增 2 个套件):
 · _fb_v49_entry.py(33 条真机断言:三颗靠右/配色/派生只有详情/详情就地变宽/编辑就地/
   删除落库/日程页入口/重复弹范围)
 · _b49_falsify.py(**10 注入 / 21 断言全抓**)
 · 更新 _fb_b43_ui.py 旧断言(「详细信息」→「详情」;自定义日程的详情语义从"跳页"改"就地展开")
 · familybook_deploy.sh 加 v49 静态断言段
 · 全量 e2e / vCard / 几何 / 各批反向验证全绿;部署脚本 0 FAIL(含"第49批形态");
   线上 7 个文件 md5 与本地逐字节一致;线上门禁诊断 13 项全 YES,临时页已删并复验 404

⚠️ 反向验证抓到一条**假绿**(值得记下来):注入「给派生条目也塞 del」时探针**没变红** ——
   因为按钮显隐是 `source` **且** `del` 的多条件"与",只塞 del 在界面上显不出来。
   ⇒ 补一条**数据层**断言(直接读 data-fb-entry 的 del 必须为空),两层判据都在。

⚠️ 末段唯一"失败"是沙箱的安全删除门禁(SAFE_DELETE_BULK_CONFIRM_REQUIRED,按轮次累计、
   本轮流水线删超 50 个文件):单独重跑 _fb_v46_carddav.py 是 **29/0**,与环境有关,与本次改动无关。
2026-10-14 19:05:13 +08:00

440 lines
22 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 条):首页日程 / 首页任务 / 日程页条目点开弹浮层。
🔴🔴 v49(用户 2026-10-14)——**用户原话**:
「在首页上点击某一个日程…下面三个按钮,都放在下面的右侧…三个按钮分别是详情、编辑和删除。
详细用蓝色,编辑用黄色,删除用红色。
当点击详细的时候,**如果这是一个日程**(正常的日程里的事件、不是来自观影/旅程等其他模块)
→ 就**在当前页面让模态框变大**、显示更多信息。
当点击编辑的时候,也是**在当前的界面**弹出编辑日程的框,就地编辑;
编辑完之后下面有两个按钮:**取消和保存**。
当点击删除的时候,就直接把这个日程给删除。
如果当前这个日程**来自其他模块**(观影 / 旅程 / 其它)→ 点击详情就**进入那些模块**展示它
(观影→那一天的观影详情页;旅程→那个行程所在的旅程页)。
**这种情况下,下面就没有编辑、也没有删除,就只有一个详情。**」
⇒ 底栏四态(都在同一排、**一律靠右**):
① detail(默认):`详情`(蓝) + `编辑`(黄) + `删除`(红)
· 只有 source='event'(自定义日程)才给「编辑」「删除」;
其余来源(观影/旅程/任务/纪念日/农历/节假日)**只给「详情」**。
· 「删除」还要 `del` 非空(= 我在这本日历上可写)。
② info(点「详情」,仅 source='event'):面板加宽、正文换成服务端只读详情。
底栏回到 detail(还能继续编辑/删除)。
③ edit(点「编辑」):正文换成编辑表单;底栏 = `取消` + `保存`。
④ delscope(点「删除」且是**重复日程**):正文问"删哪些";底栏 = `取消` + `确认删除`。
不重复的日程直接删除(不需范围)。
设计取舍(每条都对应一个踩过的坑):
① **事件委托挂 document**:列表会被 AJAX 换掉(新建日程后就地插入、首页任务勾选后重画),
一次性绑定永久有效(v40 的教训)。
② **点条目 = 弹浮层,不跳页**:拦掉 `<a href>` 默认行为。
⚠️ 必须按 `[data-fb-entry]` 精确匹配 —— 不能笼统拦 `.cal-ev`
(页面里还有"点某天新建日程"那套逻辑,它同样排除 `.cal-ev`)。
③ **数据全在 `data-fb-entry` 上**:浮层不发请求就能画出来,秒开。
④ **详情/编辑都就地换正文**(用户:「就在当前的页面」)——不叠第二层浮层。
⑤ **保存走原生提交**:四个来源的 update 路由本来就收普通 POST 并跳转
(自己写 AJAX 保存要为四个来源各写一套"成功后回哪",得不偿失)。
⑥ **删除走 AJAX**(v49 新增,服务端 `calendar/event_del` 认 X-Requested-With 回 JSON):
成功后就地摘掉那一条,**不刷页** —— 与编辑不同,删除不需要"回到哪一页"的语义。
⚠️ 两种例外退回整页重载:`removed_forward`(这一次及以后砍掉了整个系列尾段,
就地摘一条会漏;且这条系列在视图里可能横跨多天),以及
拿不到 JSON(老服务端 / 出错)。
⑦ **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 panelEl(){ return document.getElementById('fbEntryPanel'); }
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' : '');
}
/* ---- 底栏:按状态显示哪几颗 ----------------------------------------------
四组 id 各对应一种状态。全部**先隐藏再显示**,避免残留上一态可见的按钮
(比如从"编辑态"直接点 Esc 关掉、再开另一条 —— 若不收,"保存"会留在屏幕上)。
⚠️🔴 `detail` 这一态**只列「详情」**:另外两颗(编辑/删除)要不要显,
由 `paintDetail()` **按来源**决定(派生条目只有详情)。
如果把三颗都列在这里,`footShow('detail')` 会把它们无条件点亮,
**盖掉** paintDetail 刚设好的 hidden —— 本轮实测就是这么翻车的
(观影条目本该只有「详情」,三颗却都露着)。
⇒ 调用顺序也必须配合:**先 footShow('detail')、再设编辑/删除**。 */
var GROUPS = {
detail: ['fbEntryDetail'],
edit: ['fbEntryCancel', 'fbEntrySave'],
delscope: ['fbEntryDelCancel', 'fbEntryDelGo']
};
var ALL_BTNS = ['fbEntryDetail', 'fbEntryEdit', 'fbEntryDelete',
'fbEntryCancel', 'fbEntrySave', 'fbEntryDelCancel', 'fbEntryDelGo'];
function footShow(state) {
ALL_BTNS.forEach(function (id) { var e = byId(id); if (e) { e.hidden = true; } });
(GROUPS[state] || []).forEach(function (id) {
var e = byId(id); if (e) { e.hidden = false; }
});
}
function footNone() {
ALL_BTNS.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>';
}
/**
* 画详情态。`data` 来自 `data-fb-entry`(服务端 fb_entry_json 产出)。
*
* 底栏规则(用户第 2 条的核心):
* · 「详情」:**恒显**。source='event' 时它就是"就地展开更多信息"(下面用 info 片段);
* 其余来源它就是那个跳去模块页的链接(href = detail)。
* · 「编辑」「删除」:**只有 source='event'** 才显(且各要有地址:edit / del 非空)。
* 这就是用户说的"来自其他模块时,下面就没有编辑、也没有删除,就只有一个详情"。
*/
function paintDetail(data) {
var isEvent = (data.source === 'event');
if (titleEl()) { titleEl().textContent = data.title || '详情'; }
if (bodyEl()) { bodyEl().innerHTML = '<dl class="kv">' + rowsHtml(data.rows) + '</dl>'; }
if (panelEl()) { panelEl().classList.remove('is-wide'); }
setTip('');
/* ⚠️ 顺序要紧:**先** footShow('detail')(它会把所有按钮收掉、只点亮「详情」),
**再**按来源点亮/收起「编辑」「删除」。反过来会被 footShow 盖掉。 */
footShow('detail');
var det = byId('fbEntryDetail');
if (det) {
/* ⚠️ 两种「详情」共用同一颗按钮:
· source='event' → 就地展开(不设 href,交给 JS,用 data-fb-info 标记)
· 其余来源 → 真链接(href 指向该模块的详情/那一天) */
if (isEvent && data.info) {
det.setAttribute('href', '#');
det.setAttribute('data-fb-info', '1');
} else {
det.removeAttribute('data-fb-info');
if (data.detail) { det.href = data.detail; } else { det.removeAttribute('href'); }
}
}
/* 🔴 用户:「来自其他模块时,下面就没有编辑、也没有删除,就只有一个详情」——
所以这两颗**只对 source='event'** 显(且各要有地址)。 */
var ed = byId('fbEntryEdit');
if (ed) { ed.hidden = !(isEvent && data.edit); }
var de = byId('fbEntryDelete');
if (de) { de.hidden = !(isEvent && data.del); }
}
function restoreDetail() {
if (!detailCache) { return; }
paintDetail(detailCache);
}
/* ---- 打开 / 关闭 ---------------------------------------------------------- */
function setOpen(on) {
var m = M();
if (!m) { return; }
m.hidden = !on;
if (!on) {
detailCache = null;
if (bodyEl()) { bodyEl().innerHTML = ''; }
if (panelEl()) { panelEl().classList.remove('is-wide'); }
var m2 = M();
if (m2) { m2.classList.remove('is-delscope'); }
}
}
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);
if (panelEl()) { panelEl().classList.remove('is-wide'); }
// 片段里的初始化脚本 + 宿主这侧的通用升级(日期 / 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() : '编辑'; }
footShow('edit');
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(); }
}
/* ---- ③ 详情:就地展开更多信息(仅 source='event')--------------------------
用户:「让模态框变大,然后显示出来更多的信息」——
⇒ 面板加 `is-wide`(CSS 放宽 max-width),正文换成服务端 `?embed=detail` 的只读块。
失败(拿不到片段)→ 退回跳详情页,别把用户卡在一个空面板里。 */
function loadInfo(url) {
if (!url) { return; }
setTip('');
if (panelEl()) { panelEl().classList.add('is-wide'); }
if (bodyEl()) { bodyEl().innerHTML = '<p class="empty-hint">正在载入…</p>'; }
footNone();
fetch(url, { credentials: 'same-origin', headers: { 'X-Requested-With': 'XMLHttpRequest' } })
.then(function (r) { return r.text(); })
.then(function (html) {
var tmp = document.createElement('div');
tmp.innerHTML = html;
var block = tmp.querySelector('.ev-detail-block') || tmp.querySelector('.card-pad')
|| tmp.querySelector('.kv') || tmp;
if (!block) { window.location.href = url; return; }
if (bodyEl()) { bodyEl().innerHTML = ''; bodyEl().appendChild(block); }
footShow('detail'); // 还能继续编辑/删除
})
.catch(function () { window.location.href = url; });
}
/* ---- ④ 删除:AJAX,成功就地摘条 -------------------------------------------- */
/* 从当前 DOM 里摘掉这条:两处入口的选择器不同,都试一遍。
⚠️ 用 `data-fb-entry` 里那个 ref 找(就是日程 id),不是靠标题文本
—— 同名日程很常见,按文本会误摘。 */
function removeFromDom(evId, occDay) {
if (!evId) { return false; }
var hit = false;
var els = document.querySelectorAll('[data-fb-entry]');
Array.prototype.forEach.call(els, function (el) {
var d;
try { d = JSON.parse(el.getAttribute('data-fb-entry') || '{}'); } catch (e) { return; }
/* 只处理**同一条**自定义日程(`ref` 就是日程 id,由服务端给出)。
⚠️ 不要拿标题文本比 —— 同名日程很常见,会误摘别人的。 */
if (String(d.ref || '') !== String(evId)) { return; }
if (occDay && (d.occ || '') !== '' && (d.occ || '') !== occDay) { return; } // 只摘那一天
var box = el.closest('.ag-ev, .ag-allday-item, .cal-ev, .tg-block') || el;
if (box && box.parentNode) { box.parentNode.removeChild(box); hit = true; }
});
return hit;
}
var delCtx = null; /* { url, id, occ, repeat, ym } */
function askDelete(data, evId, occ) {
delCtx = { url: data.del, id: evId, occ: occ || '', repeat: !!data.repeat };
if (!data.repeat) {
/* 不重复:不用问范围,但**要确认一次**(删除不可逆)。
⚠️ 用原生 confirm 而不是自绘 —— 它同步返回、不引入新的 DOM 状态,
而且浮层里已经有一层遮罩了,再叠一层自绘框 Esc/焦点都难处理。 */
if (window.confirm('删除这条日程?')) { doDelete('all'); }
return;
}
/* 重复日程:正文变成"删哪些"(三档,与详情页/编辑那三档同措辞),
底栏 = 取消 + 确认删除。 */
if (titleEl()) { titleEl().textContent = '删除重复日程'; }
if (bodyEl()) {
bodyEl().innerHTML =
'<p class="ev-del-head">这是一条<b>重复日程</b>,删哪些?</p>' +
'<div class="ev-del-picks">' +
'<label class="chk-inline"><input type="radio" name="fbscope" value="once">' +
'<span>只删<b>这一次</b>(' + esc(occ) + ')</span></label>' +
'<label class="chk-inline"><input type="radio" name="fbscope" value="future">' +
'<span>删<b>这一次及以后</b>(' + esc(occ) + ' 起,之前的留着)</span></label>' +
'<label class="chk-inline"><input type="radio" name="fbscope" value="all" checked>' +
'<span>删<b>全部</b>(整条系列)</span></label>' +
'</div>';
}
if (panelEl()) { panelEl().classList.remove('is-wide'); }
setTip('');
footShow('delscope');
}
function csrfToken() {
var el = document.querySelector('#fbEntryModal .fb-entry-csrf input[name="_csrf"]');
return el ? el.value : '';
}
function doDelete(scope) {
if (!delCtx || !delCtx.url) { return; }
var fd = new FormData();
fd.append('_csrf', csrfToken());
fd.append('id', String(delCtx.id));
fd.append('scope', scope || 'all');
if (delCtx.occ) { fd.append('occ', delCtx.occ); }
setTip('正在删除…');
footNone();
if (!window.fetch) { window.location.reload(); return; }
fetch(delCtx.url, { method: 'POST', body: fd, credentials: 'same-origin',
headers: { 'X-Requested-With': 'XMLHttpRequest' } })
.then(function (r) { return r.json(); })
.then(function (res) {
if (!res || !res.ok) {
setTip((res && res.msg) || '删除失败。', true);
footShow('detail'); // 回到详情态,用户还能重试
return;
}
/* ⚠️ "这一次及以后"砍的是**系列尾段**,就地摘一条会漏掉后面那些
⇒ 整页重载(视图由服务端重画,最稳)。 */
if (res.removed_forward) { window.location.reload(); return; }
var n = removeFromDom(delCtx.id, (scope === 'once') ? delCtx.occ : '');
setOpen(false);
/* 摘不掉(比如首页那条是"全天"块、结构不同)→ 重载一次,别假装成功。 */
if (!n) { window.location.reload(); }
})
.catch(function () { window.location.reload(); });
}
/* ---- 打开一条 ------------------------------------------------------------- */
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('#fbEntryDelGo')) {
e.preventDefault();
var sel = document.querySelector('#fbEntryBody input[name="fbscope"]:checked');
doDelete(sel ? sel.value : 'all');
return;
}
if (t.closest('#fbEntryDelCancel')) { e.preventDefault(); restoreDetail(); return; }
/* ① 删除 */
if (t.closest('#fbEntryDelete')) {
e.preventDefault();
if (detailCache && detailCache.del) {
askDelete(detailCache, detailCache.ref, detailCache.occ || '');
}
return;
}
/* ② 编辑 */
if (t.closest('#fbEntryEdit')) {
e.preventDefault();
if (detailCache && detailCache.edit) { loadEdit(detailCache.edit); }
return;
}
/* 编辑态:取消(回详情)/ 保存 */
if (t.closest('#fbEntryCancel')) { e.preventDefault(); restoreDetail(); return; }
if (t.closest('#fbEntrySave')) { e.preventDefault(); submitEdit(); return; }
/* ③ 详情:source='event' 就地展开;其余来源是普通链接(浏览器自己跳) */
var detBtn = t.closest('#fbEntryDetail');
if (detBtn) {
if (detBtn.getAttribute('data-fb-info') === '1') {
e.preventDefault();
if (detailCache && detailCache.info) { loadInfo(detailCache.info); }
}
return; /* 派生来源:不 preventDefault,让它走 href 跳去那个模块 */
}
var item = t.closest('[data-fb-entry]');
if (item) { e.preventDefault(); openEntry(item); return; }
var m2 = M();
if (m2 && t === m2) { 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); };
})();