Files
FamilyBook/assets/js/entry_popup.js
T
laoyang b45cf671d1 v71c:首页「通知消息」改成正规卡片,样式直接复用通知页
用户 2026-10-21:
「先把web端的首页修一下,通知消息这个,也是首页的卡片,可以调整顺序和大小。
 另外,这个样式也不好看,我看个人头像点击后,通知消息那个页面的样式就不错,
 直接复用就可以了。」

① 卡片化(HomeCards 注册表 + 默认布局 + 取数)
   · TYPES 新增 'notif'(w4/h4,route=notifications,more=全部通知)
   · DEFAULT_LAYOUT 钉在 brief 之后,与 days 并一行(宽 4)——
     若插在 brief 与 agenda 之间,agenda/tasks 会被挤成一上一下(实测 C13 顶边 407 vs 751)
   · data() 加 case 'notif' → 新增 notifData():最新 10 条 + 未读数,
     与原来的取数口径一字不差;关掉卡片就不查这两条 SQL
   · templates/home.php 删掉网格前那块写死区块(v30 第 5 条「必须在往年的今天前面」
     这条约束作废 —— 那时还没有卡片体系);index.php 不再单独传 notifs/notifUnread

② 样式复用 templates/notifications/index.php
   · 结构与通知页完全一致:头像在前 → 正文 → kind 图标在右(带 ntf-kind-{kind})
     (旧首页那块是"图标在前、头像第二",正是用户说不好看的那一版)
   · 新增 .hc-ntf 作用域只补"卡片比整页小"的三条差异:正文允许折两行(宽 2 时
     单行省略会把正文全吃掉)、时间挪到正文下一行、未读数做小注脚
   · 不带每条右侧「删除」按钮(4×4 的卡里塞两个按钮会把正文挤成两三个字),
     删除在通知页做,卡片右上角有「全部通知」

③ 顺带修掉 v63 A1 抓住的一处:'notif' 的 mod 给 'home' 而不是空串
   (通知不在 Module::LIST,但它是首页自己的卡片;空串会让「全部通知」成为
   一排同色模块按钮里唯一白底 ghost 的那个)
   ⇒ TYPES docstring 立规矩:route 非空就必须有 mod,认不出归哪个模块就归 home

验证(familybook_push.sh 第 6 轮全绿,PUSH_RC=0,2h11m)
   v62 首页卡片 56/0 · v62 反向验证 14/0(新增 M8–M12 五条注入)
   b30 通知 16/0 · b30 反向验证 18/0
   v63 40/0 · v63 反向验证 51/0 · e2e 1523/0 · 部署静态断言空跑 RC=0

同批带上上一轮 v70/v71 的日历修复(已单独验过,本轮流水线一并回归):
   · v70 日历导入后条目不显示:+N 条从纯文本 span 改成真按钮,点开是当天全部
     条目浮层;AJAX 新建后就地插入与服务端同形(回 entry,不让前端自己拼)
   · v71 ICS 导入顺序:主事件 → 覆盖行 → EXDATE 由 Calendar::importIcs() 统一保证
2026-10-21 13:22:37 +08:00

459 lines
23 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);
}
/**
* 直接用**数据对象**开详情层(不需要页面上存在对应的 DOM 元素)。
*
* 起源 v70(用户 2026-10-20 第 2 条):月视图的「+N 条」点开是"当天的清单",
* 清单里的每一条并没有自己的 `<a data-fb-entry>`(它们只是清单里的一行文字),
* 而 `openEntry(el)` 只吃元素 ⇒ 清单项点不进详情。
* ⇒ 抽出这个入口,`openEntry()` 也走它,两条路径共用同一套渲染与底栏规则。
*
* ⚠️ 数据结构必须与 `fb_entry_json()` 产出的一致(服务端保证)。
*/
function openEntryData(data) {
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;
/* v70:按**数据对象**开详情层(月视图「+N 条」的清单项用这个入口) */
window.fbEntryOpenData = openEntryData;
window.fbEntryClose = function () { setOpen(false); };
})();