v42 第 42 批:十一条反馈(界面 9 条 + CalDAV 两处新根因)
CalDAV(用户「还是不行,thunderbird 提示找不到日历」): - 🔴 `.well-known/caldav` 原来**只回 301**。Thunderbird 建账号时常是 **带鉴权直接 PROPFIND** 这个地址(把它当 DAV 服务入口探),而它对这一跳的 301 **不跟随** ⇒ 判定"这里没有 CalDAV" ⇒ 找不到日历。 ⇒ 按方法分流:带鉴权 PROPFIND → 直接 207 给发现内容(principal / home-set / 日历本清单,一步到位);其余仍是 301。carddav 同修。 - 🔴 `Location` 由**相对** `/dav/` 改成**绝对 URL**(按真实 HTTP_HOST 拼, 协议从 HTTPS / X-Forwarded-Proto 取)。TB 在服务发现这一跳对相对跳转不可靠。 - 线上终验:匿名 GET 301+绝对URL;带鉴权 PROPFIND 直接 207(含 principal/home-set); Depth:0 只回自身;Depth:1 列出 12 本日历。 界面九条: - ① 新建旅程不再带入旧数据(真根因:`_list_body.php` 的 `foreach ($rows as $t)` 循环后 `$t` 残留,而 `form.php` 读的正是 `$t` ⇒ 弹框被填上列表最后一趟的数据)。 修:循环变量改 `$row` + `form.php` 契约兼容(`$tIn` 优先)。 - ②③④ 行程弹框里的 AntD 时间控件 / 出行方式联动车牌与车次 / 高德地址联想全部失效 (真根因:AJAX 片段里的内联 `<script>` **既不执行也不保留**)。 ⇒ 加 `runInlineScripts()` 把片段内联脚本抠出来执行; 另修 `app.js`:`if (!hasMonth) return` 的短路让 `window.fbBootAntd` 从未被定义, 把它提到所有 return 之前。 - ④b⑤ 标签改「照片」「时间范围」。 - ⑥ 简介补"还没有简介,点这里写一句"的占位(简介本身一直在,是那条 summary 为空)。 - ⑦ 资源编辑复用新建弹框(`#resAddCard`),并在其底栏补「共享」「附件」入口 (第 7 条改动后这两个能力一度无入口)。 - ⑧ 纪念日「月/日」→「日期」+ AntD 纯日期控件(新增 mountAntdDate / [data-antd-date]), 存储契约仍是 month + day。 - ⑨ 编辑纪念日弹框关不掉(关闭逻辑原来只在 days/index.php,而 days/edit 是整页) ⇒ 搬进 days/form.php 自包含。 验证:e2e 1510/0;v42 真机 33/0;DAV·TB 35/0;DAV 反向 8/0; b23 55/0;b28 39/0;b32 26/0;b32 反向 5/0;v31 DAV 28/0;b31 反向 8/0; 部署断言空跑 337/0;完整流水线 PUSH_RC=0。
This commit is contained in:
@@ -5604,3 +5604,20 @@ h5.md-h, h6.md-h { font-size: 14px; }
|
|||||||
.an-md-sep { color: var(--muted); font-size: 13px; }
|
.an-md-sep { color: var(--muted); font-size: 13px; }
|
||||||
/* 观影式行里的"填充块"(选人组件那种自带布局的控件,占满控件位) */
|
/* 观影式行里的"填充块"(选人组件那种自带布局的控件,占满控件位) */
|
||||||
.mv-row > .mv-fill { flex: 1 1 auto; min-width: 0; }
|
.mv-row > .mv-fill { flex: 1 1 auto; min-width: 0; }
|
||||||
|
|
||||||
|
/* ---- v42(用户 2026-10-11 第 8 条)纪念日「日期」:AntD 单日期控件 ----
|
||||||
|
与原观影/高级搜索那两处同一套做法:
|
||||||
|
· `.dt-antd` 是 antd 的挂载点(宽度交给它);
|
||||||
|
· 挂上后(`.is-antd`)把**原生 `<input type="date">` 视觉收起** ——
|
||||||
|
它仍是值载体(antd 通过 change 写它),只是不再显示给用户。
|
||||||
|
⚠️ 用 clip 收起(不是 display:none):保留可聚焦、可被表单读到,
|
||||||
|
与 v40 高级搜索那处的取舍一致。 */
|
||||||
|
.dt-input.is-antd > .dt-native { position: absolute; width: 1px; height: 1px;
|
||||||
|
overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; border: 0; padding: 0; margin: -1px; }
|
||||||
|
.dt-input.is-antd > .dt-antd { display: block; width: 100%; }
|
||||||
|
.dt-input.dt-antd-single { display: block; width: 100%; }
|
||||||
|
|
||||||
|
/* ---- v42(用户第 6 条)简介为空时的那句可点占位 ----
|
||||||
|
与 `.trip-sum` 同一视觉位置,只是颜色淡一档(它是要点进去补的提示,不是内容)。 */
|
||||||
|
.trip-sum-empty { margin: 10px 0 0; font-size: 13px; color: var(--muted); }
|
||||||
|
.trip-sum-empty a { color: var(--brand); }
|
||||||
|
|||||||
+87
-4
@@ -1305,7 +1305,36 @@
|
|||||||
⚠️ 两条路共用下面同一套"按需加载 + 中文 locale"的 ensure(),
|
⚠️ 两条路共用下面同一套"按需加载 + 中文 locale"的 ensure(),
|
||||||
只是挂载函数不同 —— 别复制一份加载逻辑出来(那必然有一天两边不同步)。 */
|
只是挂载函数不同 —— 别复制一份加载逻辑出来(那必然有一天两边不同步)。 */
|
||||||
var antdDateRanges = document.querySelectorAll('[data-antd-daterange]');
|
var antdDateRanges = document.querySelectorAll('[data-antd-daterange]');
|
||||||
if (antdHosts.length || antdDateRanges.length) { bootAntd(antdHosts, antdDateRanges); }
|
/* 🔴 v42(用户第 8 条):单个**纯日期**控件(纪念日的「日期」)——
|
||||||
|
`[data-antd-date]`。它挂的是不带 showTime 的单 DatePicker。 */
|
||||||
|
var antdDates = document.querySelectorAll('[data-antd-date]');
|
||||||
|
if (antdHosts.length || antdDateRanges.length || antdDates.length) {
|
||||||
|
bootAntd(antdHosts, antdDateRanges, antdDates);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 🔴🔴 v42(用户 2026-10-11 第 2 条)——**关键时序修复**:
|
||||||
|
下面 `if (!hasMonth) { … return; }` 这条"没有 month 控件就提前退出"的短路
|
||||||
|
(v37 为省 4 MB Semi 资源而加),会让**本 IIFE 尾部**的那几个
|
||||||
|
`window.fbXxx = …` 赋值**永远不执行**。
|
||||||
|
而详情页的行程弹框正是靠 `window.fbBootAntd` / `window.fbUpgradeRange`
|
||||||
|
把 AJAX 塞进来的那对起止控件升级成 AntD 的 —— 于是:
|
||||||
|
「添加行程,开始和结束时间,并没有使用我们在观影模式的控件」(用户第 2 条)。
|
||||||
|
⚠️ 这个缺陷从 v40 引入 `fbBootAntd` 起就存在,只是当时**只有观影详情页**用,
|
||||||
|
而那页首屏就带 `[data-dt-antd]`(不满足 `!hasMonth`…… 实为首屏有值、
|
||||||
|
直接走了另一条分支),所以一直没暴露;行程这条路径才把它踩出来。
|
||||||
|
⇒ 修法:**把对外接口的赋值提到所有 `return` 之前**(函数声明会提升,
|
||||||
|
所以这里能安全引用下面定义的 `upgradeRange` / `bootAntd` / `writeDT`)。
|
||||||
|
尾部那几处保留(幂等),不改动其他调用点的语义。 */
|
||||||
|
window.fbUpgradeRange = function () {
|
||||||
|
Array.prototype.forEach.call(document.querySelectorAll('.dt-range'), upgradeRange);
|
||||||
|
return true;
|
||||||
|
};
|
||||||
|
window.fbBootAntd = function () {
|
||||||
|
bootAntd(document.querySelectorAll('[data-dt-antd]'),
|
||||||
|
document.querySelectorAll('[data-antd-daterange]'),
|
||||||
|
document.querySelectorAll('[data-antd-date]'));
|
||||||
|
return true;
|
||||||
|
};
|
||||||
|
|
||||||
/* ⚠️ 只要还有 month 控件就得拉 Semi;纯 .dt-input 页面(绝大多数)不用拉。 */
|
/* ⚠️ 只要还有 month 控件就得拉 Semi;纯 .dt-input 页面(绝大多数)不用拉。 */
|
||||||
if (!hasMonth) {
|
if (!hasMonth) {
|
||||||
@@ -1589,10 +1618,11 @@
|
|||||||
|
|
||||||
⚠️ 这个适配层**不认识的页面不受影响**:没有 `[data-dt-antd]` 就直接返回。
|
⚠️ 这个适配层**不认识的页面不受影响**:没有 `[data-dt-antd]` 就直接返回。
|
||||||
============================================================================ */
|
============================================================================ */
|
||||||
function bootAntd(hosts, dateRanges) {
|
function bootAntd(hosts, dateRanges, dates) {
|
||||||
hosts = hosts || [];
|
hosts = hosts || [];
|
||||||
dateRanges = dateRanges || [];
|
dateRanges = dateRanges || [];
|
||||||
if (!hosts.length && !dateRanges.length) { return; }
|
dates = dates || [];
|
||||||
|
if (!hosts.length && !dateRanges.length && !dates.length) { return; }
|
||||||
var meta = document.querySelector('meta[name="fb-antd"]');
|
var meta = document.querySelector('meta[name="fb-antd"]');
|
||||||
if (!meta) { return; }
|
if (!meta) { return; }
|
||||||
var conf;
|
var conf;
|
||||||
@@ -1634,6 +1664,9 @@
|
|||||||
Array.prototype.forEach.call(dateRanges, function (el) {
|
Array.prototype.forEach.call(dateRanges, function (el) {
|
||||||
if (mountAntdDateRange(el)) { done++; }
|
if (mountAntdDateRange(el)) { done++; }
|
||||||
});
|
});
|
||||||
|
Array.prototype.forEach.call(dates, function (el) {
|
||||||
|
if (mountAntdDate(el)) { done++; }
|
||||||
|
});
|
||||||
if (window.console && done) { console.info('AntD 日期控件已挂上 ' + done + ' 处'); }
|
if (window.console && done) { console.info('AntD 日期控件已挂上 ' + done + ' 处'); }
|
||||||
}).catch(function (err) {
|
}).catch(function (err) {
|
||||||
// 拉不动就留在自建下拉上 —— 渐进增强的兜底,不弹任何打扰
|
// 拉不动就留在自建下拉上 —— 渐进增强的兜底,不弹任何打扰
|
||||||
@@ -1707,6 +1740,55 @@
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* 🔴 v42(用户 2026-10-11 第 8 条):「新建纪念日,月日那里…也用我们最新的时间控件,
|
||||||
|
后面的时分就不需要了,只需要日期。」
|
||||||
|
⇒ 单个**纯日期**的 AntD DatePicker(不是 RangePicker)。
|
||||||
|
值载体是一个 `<input type="date" name="X">`(写成 `YYYY-MM-DD`),
|
||||||
|
由调用方从它再拆出 month/day(纪念日的存储契约是 month + day)。 */
|
||||||
|
function mountAntdDate(el) {
|
||||||
|
if (!el || el.getAttribute('data-antd-done')) { return false; }
|
||||||
|
var native = el.querySelector('input[type="date"]');
|
||||||
|
if (!native) { return false; }
|
||||||
|
|
||||||
|
var host = document.createElement('span');
|
||||||
|
host.className = 'dt-antd dt-antd-dateonly';
|
||||||
|
el.insertBefore(host, el.firstChild);
|
||||||
|
|
||||||
|
function toDayjs(str) {
|
||||||
|
var m = (str || '').trim().match(/^(\d{4})-(\d{2})-(\d{2})$/);
|
||||||
|
return m ? window.dayjs(m[1] + '-' + m[2] + '-' + m[3], 'YYYY-MM-DD') : null;
|
||||||
|
}
|
||||||
|
var last = '';
|
||||||
|
function writeBack(str) {
|
||||||
|
var s = str || '';
|
||||||
|
if (s === last) { return; }
|
||||||
|
last = s;
|
||||||
|
native.value = s;
|
||||||
|
native.dispatchEvent(new Event('change', { bubbles: true }));
|
||||||
|
}
|
||||||
|
|
||||||
|
var v0 = (native.value || '').trim();
|
||||||
|
try {
|
||||||
|
var picker = React.createElement(antd.DatePicker, {
|
||||||
|
format: 'YYYY-MM-DD',
|
||||||
|
allowClear: true,
|
||||||
|
placeholder: '选择日期',
|
||||||
|
defaultValue: toDayjs(v0) || undefined,
|
||||||
|
onChange: function (val, str) { writeBack(str); }
|
||||||
|
});
|
||||||
|
var loc = (window.antd.locales && window.antd.locales.zh_CN) || undefined;
|
||||||
|
if (loc) { picker = React.createElement(antd.ConfigProvider, { locale: loc }, picker); }
|
||||||
|
ReactDOM.createRoot(host).render(picker);
|
||||||
|
} catch (e) {
|
||||||
|
if (window.console) { console.warn('antd DatePicker 挂载失败,留在原生输入:', e && e.message); }
|
||||||
|
host.parentNode.removeChild(host);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
el.classList.add('is-antd');
|
||||||
|
el.setAttribute('data-antd-done', '1');
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
/** 把 dayjs 切到简体中文(星期/月份/相对时间都由它出)。
|
/** 把 dayjs 切到简体中文(星期/月份/相对时间都由它出)。
|
||||||
* ⚠️ 拿不到就静默跳过 —— 那只会让星期显示成英文,不该拦住整个控件。 */
|
* ⚠️ 拿不到就静默跳过 —— 那只会让星期显示成英文,不该拦住整个控件。 */
|
||||||
function applyDayjsZh() {
|
function applyDayjsZh() {
|
||||||
@@ -1982,7 +2064,8 @@
|
|||||||
*/
|
*/
|
||||||
window.fbBootAntd = function () {
|
window.fbBootAntd = function () {
|
||||||
bootAntd(document.querySelectorAll('[data-dt-antd]'),
|
bootAntd(document.querySelectorAll('[data-dt-antd]'),
|
||||||
document.querySelectorAll('[data-antd-daterange]'));
|
document.querySelectorAll('[data-antd-daterange]'),
|
||||||
|
document.querySelectorAll('[data-antd-date]'));
|
||||||
return true;
|
return true;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
+47
-1
@@ -82,8 +82,54 @@ $path = (string)parse_url((string)($_SERVER['REQUEST_URI'] ?? '/carddav/'), PH
|
|||||||
* 三种客户端一次都满足。
|
* 三种客户端一次都满足。
|
||||||
*/
|
*/
|
||||||
if ($path === '/.well-known/carddav') {
|
if ($path === '/.well-known/carddav') {
|
||||||
|
/* 🔴🔴 v42(用户第 10 条):与 dav.php 完全同一套修法 ——
|
||||||
|
带鉴权的 PROPFIND 直接给发现内容(207),其余仍是 301 到**绝对 URL**。
|
||||||
|
(Thunderbird 建账号时常带鉴权 PROPFIND 这个地址,
|
||||||
|
只回 301 它不跟 ⇒ 「找不到通讯录」。) */
|
||||||
|
$cdWantPropfind = (strtoupper((string)($_SERVER['REQUEST_METHOD'] ?? 'GET')) === 'PROPFIND');
|
||||||
|
$cdHasAuth = !empty($user) && $pass !== null && $pass !== '';
|
||||||
|
if ($cdWantPropfind && $cdHasAuth) {
|
||||||
|
$cdWho = Auth::davAuth($user, $pass);
|
||||||
|
if (!$cdWho) {
|
||||||
|
header('WWW-Authenticate: Basic realm="FamilyBook DAV"');
|
||||||
|
cd_fail(401, '需要 DAV 口令(在「个人设置 → DAV 同步」页生成,不是登录密码)。');
|
||||||
|
}
|
||||||
|
$cdFamily = (int)$cdWho['family_id'];
|
||||||
|
$cdMe = (int)$cdWho['person_id'];
|
||||||
|
$cdParts = [
|
||||||
|
'<d:response><d:href>' . CardDav::x(CardDav::HOME_PATH) . '</d:href><d:propstat><d:prop>'
|
||||||
|
. '<d:resourcetype><d:collection/></d:resourcetype>'
|
||||||
|
. '<d:displayname>' . CardDav::x('家书通讯录') . '</d:displayname>'
|
||||||
|
. '<d:current-user-principal><d:href>' . CardDav::x(CardDav::PRINCIPAL_PATH)
|
||||||
|
. '</d:href></d:current-user-principal>'
|
||||||
|
. '<d:principal-URL><d:href>' . CardDav::x(CardDav::PRINCIPAL_PATH)
|
||||||
|
. '</d:href></d:principal-URL>'
|
||||||
|
. '<card:addressbook-home-set><d:href>' . CardDav::x(CardDav::HOME_PATH)
|
||||||
|
. '</d:href></card:addressbook-home-set>'
|
||||||
|
. '</d:prop></d:propstat></d:response>',
|
||||||
|
];
|
||||||
|
foreach (CardDav::books($cdFamily, $cdMe) as $cdB) {
|
||||||
|
$cdParts[] = '<d:response><d:href>' . CardDav::x(CardDav::hrefOf($cdB)) . '</d:href>'
|
||||||
|
. '<d:propstat><d:prop>'
|
||||||
|
. '<d:resourcetype><d:collection/><card:addressbook/></d:resourcetype>'
|
||||||
|
. '<d:displayname>' . CardDav::x((string)$cdB['name']) . '</d:displayname>'
|
||||||
|
. '</d:prop></d:propstat></d:response>';
|
||||||
|
}
|
||||||
|
header('Content-Type: application/xml; charset=utf-8');
|
||||||
|
http_response_code(207);
|
||||||
|
echo '<?xml version="1.0" encoding="utf-8"?>' . "\n"
|
||||||
|
. '<d:multistatus xmlns:d="DAV:" xmlns:card="urn:ietf:params:xml:ns:carddav" '
|
||||||
|
. 'xmlns:c="urn:ietf:params:xml:ns:caldav">'
|
||||||
|
. implode('', $cdParts) . '</d:multistatus>';
|
||||||
|
exit;
|
||||||
|
}
|
||||||
http_response_code(301);
|
http_response_code(301);
|
||||||
header('Location: /carddav/');
|
$wkHost = (string)($_SERVER['HTTP_HOST'] ?? '');
|
||||||
|
$wkProto = (!empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off') ? 'https' : 'http';
|
||||||
|
if ($wkProto === 'http' && !empty($_SERVER['HTTP_X_FORWARDED_PROTO'])) {
|
||||||
|
$wkProto = (string)$_SERVER['HTTP_X_FORWARDED_PROTO'];
|
||||||
|
}
|
||||||
|
header('Location: ' . (($wkHost !== '') ? ($wkProto . '://' . $wkHost . '/carddav/') : '/carddav/'));
|
||||||
exit;
|
exit;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -90,8 +90,79 @@ $path = (string)parse_url((string)($_SERVER['REQUEST_URI'] ?? '/dav/'), PHP_UR
|
|||||||
* (本批实测踩过:跳到 principal 只对"严格按 RFC 5397 走的"客户端够用。)
|
* (本批实测踩过:跳到 principal 只对"严格按 RFC 5397 走的"客户端够用。)
|
||||||
*/
|
*/
|
||||||
if ($path === '/.well-known/caldav') {
|
if ($path === '/.well-known/caldav') {
|
||||||
|
/* 🔴🔴 v42(用户第 10 条)核心修复之一:
|
||||||
|
这条路径原来**只回 301**(跳 `/dav/`)。匿名 GET 时这是对的;
|
||||||
|
但 Thunderbird 建账号时**常常是带鉴权直接 PROPFIND 这个地址**
|
||||||
|
(它把 `.well-known/caldav` 当成"DAV 服务的入口"来探,而不是只当跳转)。
|
||||||
|
对带鉴权的 PROPFIND 回 301 的后果:**TB 不跟这个跳转**
|
||||||
|
(它对"服务发现跳转"的跟随策略与浏览器不同),于是判定
|
||||||
|
"这里没有 CalDAV 服务" ⇒ 报「找不到日历」。
|
||||||
|
⇒ 修法:**按方法分流**
|
||||||
|
· 有鉴权凭据 + PROPFIND → 直接给出与 `/dav/` 一致的发现响应(207,
|
||||||
|
含 current-user-principal / calendar-home-set / 日历本清单),
|
||||||
|
客户端**一步到位**,不依赖它对 301 的跟随;
|
||||||
|
· 其余(匿名、GET、OPTIONS…)→ 仍是 301 到绝对 URL `/dav/`(RFC 6764 语义)。
|
||||||
|
⚠️ 免鉴权这条必须保住:匿名 OPTIONS/GET 不能 401(否则客户端以为不支持 DAV)。 */
|
||||||
|
$wkWantPropfind = (strtoupper($method) === 'PROPFIND');
|
||||||
|
$wkHasAuth = !empty($user) && $pass !== null && $pass !== '';
|
||||||
|
if ($wkWantPropfind && $wkHasAuth) {
|
||||||
|
$wkWho = Auth::davAuth($user, $pass);
|
||||||
|
if (!$wkWho) {
|
||||||
|
header('WWW-Authenticate: Basic realm="FamilyBook DAV"');
|
||||||
|
dav_fail(401, '需要 DAV 口令(在「个人设置 → DAV 同步」页生成,不是登录密码)。');
|
||||||
|
}
|
||||||
|
$wkFamily = (int)$wkWho['family_id'];
|
||||||
|
$wkMe = (int)$wkWho['person_id'];
|
||||||
|
$wkParts = [
|
||||||
|
'<d:response><d:href>/dav/</d:href><d:propstat><d:prop>'
|
||||||
|
. '<d:resourcetype><d:collection/></d:resourcetype>'
|
||||||
|
. '<d:displayname>' . CalDav::x('家书日历') . '</d:displayname>'
|
||||||
|
. '<d:current-user-principal><d:href>' . CalDav::x(CalDav::PRINCIPAL_PATH)
|
||||||
|
. '</d:href></d:current-user-principal>'
|
||||||
|
. '<d:principal-URL><d:href>' . CalDav::x(CalDav::PRINCIPAL_PATH)
|
||||||
|
. '</d:href></d:principal-URL>'
|
||||||
|
. '<c:calendar-home-set><d:href>' . CalDav::x(CalDav::HOME_PATH)
|
||||||
|
. '</d:href></c:calendar-home-set>'
|
||||||
|
. '<card:addressbook-home-set><d:href>' . CalDav::x(CalDav::CARD_HOME_PATH)
|
||||||
|
. '</d:href></card:addressbook-home-set>'
|
||||||
|
. '</d:prop></d:propstat></d:response>',
|
||||||
|
];
|
||||||
|
foreach (CalDav::books($wkFamily, $wkMe) as $wkC) {
|
||||||
|
$wkParts[] = '<d:response><d:href>' . CalDav::x(CalDav::hrefOf($wkC)) . '</d:href>'
|
||||||
|
. '<d:propstat><d:prop>'
|
||||||
|
. '<d:resourcetype><d:collection/><c:calendar/></d:resourcetype>'
|
||||||
|
. '<d:displayname>' . CalDav::x('家书 · ' . (string)$wkC['name']) . '</d:displayname>'
|
||||||
|
. '<c:supported-calendar-component-set><c:comp name="VEVENT"/></c:supported-calendar-component-set>'
|
||||||
|
. '</d:prop></d:propstat></d:response>';
|
||||||
|
}
|
||||||
|
CalDav::multis();
|
||||||
|
echo '<?xml version="1.0" encoding="utf-8"?>' . "\n"
|
||||||
|
. '<d:multistatus xmlns:d="DAV:" xmlns:c="urn:ietf:params:xml:ns:caldav" '
|
||||||
|
. 'xmlns:card="urn:ietf:params:xml:ns:carddav">'
|
||||||
|
. implode('', $wkParts) . '</d:multistatus>';
|
||||||
|
exit;
|
||||||
|
}
|
||||||
http_response_code(301);
|
http_response_code(301);
|
||||||
header('Location: /dav/');
|
/* 🔴🔴 v42(用户 2026-10-11 第 10 条:「caldav 还是不行,thunderbird 提示找不到日历」)
|
||||||
|
这里原来是**相对路径** `Location: /dav/`。
|
||||||
|
⚠️ RFC 7231 允许相对 Location(客户端按 RFC 3986 解析),
|
||||||
|
但 Thunderbird 的 CalDAV 服务发现在这一跳上**对相对 Location 不可靠** ——
|
||||||
|
`curl -L` 会跟、浏览器会跟,但 TB 自己不跟,
|
||||||
|
它把这个 301 当成"这里没有 CalDAV 服务" ⇒ 报「找不到日历」。
|
||||||
|
而我们的探针用 curl/urllib(都会自动跟)⇒ 全绿,
|
||||||
|
于是"探针全过、真客户端不行"这个组合一直复现不出来。
|
||||||
|
⇒ 改成**绝对 URL**(RFC 6764 §6.1 的示例也是绝对 URL)。
|
||||||
|
⚠️ 按**真实请求的 host**拼(不能写死域名):
|
||||||
|
本站既可能被 family.example.com 访问,也可能被 IP / 其它别名访问,
|
||||||
|
写死会把客户端带到别的域(跨域后鉴权头会丢)。 */
|
||||||
|
$wkHost = (string)($_SERVER['HTTP_HOST'] ?? '');
|
||||||
|
$wkProto = (!empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off') ? 'https' : 'http';
|
||||||
|
// 走代理(宝塔 nginx→apache)时协议可能丢,再从 X-Forwarded-Proto 补一次
|
||||||
|
if ($wkProto === 'http' && !empty($_SERVER['HTTP_X_FORWARDED_PROTO'])) {
|
||||||
|
$wkProto = (string)$_SERVER['HTTP_X_FORWARDED_PROTO'];
|
||||||
|
}
|
||||||
|
$wkTarget = ($wkHost !== '') ? ($wkProto . '://' . $wkHost . '/dav/') : '/dav/';
|
||||||
|
header('Location: ' . $wkTarget);
|
||||||
exit;
|
exit;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+97
-8
@@ -64,6 +64,12 @@ $backUrl = url('days');
|
|||||||
if ($anModal) {
|
if ($anModal) {
|
||||||
echo ' id="anNewCard" role="dialog" aria-modal="true" aria-label="'
|
echo ' id="anNewCard" role="dialog" aria-modal="true" aria-label="'
|
||||||
. ($editing ? '编辑纪念日' : '新建纪念日') . '"' . ($anOpen ? '' : ' hidden');
|
. ($editing ? '编辑纪念日' : '新建纪念日') . '"' . ($anOpen ? '' : ' hidden');
|
||||||
|
/* 🔴 v42 第 9 条:标记"这是**整页直达**的弹框"(不是列表页内嵌的),
|
||||||
|
以及它该退回到哪里 —— 关闭时若无宿主容器,就跳回列表页。
|
||||||
|
`$anModalOnly` = "被列表页内嵌",那种情形由列表页的容器接管,不用跳页。 */
|
||||||
|
if (!$anModalOnly) {
|
||||||
|
echo ' data-an-standalone="1" data-an-back="' . e($backUrl) . '"';
|
||||||
|
}
|
||||||
} ?>>
|
} ?>>
|
||||||
<?php if ($anModal): ?>
|
<?php if ($anModal): ?>
|
||||||
<div class="cal-modal-panel mv-panel">
|
<div class="cal-modal-panel mv-panel">
|
||||||
@@ -108,15 +114,24 @@ $backUrl = url('days');
|
|||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<?php /* 🔴 v42(用户 2026-10-11 第 8 条):「新建纪念日,月日那里,直接两个字:日期,
|
||||||
|
也用我们最新的时间控件,后面的时分就不需要了,只需要日期。」
|
||||||
|
⇒ 标签改「日期」;控件换成 AntD **纯日期** DatePicker(`[data-antd-date]`)。
|
||||||
|
⚠️ 存储契约**没变**:仍然是 `month` + `day` 两个字段。
|
||||||
|
做法:一个 `<input type="date" name="an_date">` 作值载体(antd 写它),
|
||||||
|
JS 把它拆成两个 hidden 的 `month` / `day`(见下方脚本)。
|
||||||
|
这样 bind 到 antd 的 onChange → 写 an_date → 监听 change → 拆 month/day,
|
||||||
|
整条链只有一个"真值来源",不会出现两处不同步。
|
||||||
|
⚠️ `name="an_date"` 不是 `month/day` 本身 —— 它只是中间载体,
|
||||||
|
提交前脚本必把它拆好;服务端不读 an_date(days/save 读 month/day)。 */ ?>
|
||||||
<div class="mv-row">
|
<div class="mv-row">
|
||||||
<span class="mv-lb">月 / 日<i>*</i></span>
|
<span class="mv-lb">日期<i>*</i></span>
|
||||||
<div class="an-md">
|
<div class="dt-input dt-antd-single" data-antd-date="1" id="an-date-wrap">
|
||||||
<input class="input" id="an-month" name="month" type="number" min="1" max="12" required
|
<input type="date" class="dt-date" id="an-date" name="an_date"
|
||||||
value="<?= (int)$v('month', 1) ?>" aria-label="月">
|
value="<?= e(sprintf('%04d-%02d-%02d', (int)$v('year', (int)date('Y')), (int)$v('month', 1), (int)$v('day', 1))) ?>"
|
||||||
<span class="an-md-sep">月</span>
|
aria-label="日期">
|
||||||
<input class="input" id="an-day" name="day" type="number" min="1" max="31" required
|
<input type="hidden" id="an-month" name="month" value="<?= (int)$v('month', 1) ?>">
|
||||||
value="<?= (int)$v('day', 1) ?>" aria-label="日">
|
<input type="hidden" id="an-day" name="day" value="<?= (int)$v('day', 1) ?>">
|
||||||
<span class="an-md-sep">日</span>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -176,3 +191,77 @@ $backUrl = url('days');
|
|||||||
<?php if ($anShell): ?>
|
<?php if ($anShell): ?>
|
||||||
</div><?php /* 关掉开头那个外层(.cal-modal 或 .card) */ ?>
|
</div><?php /* 关掉开头那个外层(.cal-modal 或 .card) */ ?>
|
||||||
<?php endif; ?>
|
<?php endif; ?>
|
||||||
|
|
||||||
|
<?php if ($anModal): ?>
|
||||||
|
<script>
|
||||||
|
/* ==================== v42(用户 2026-10-11 第 9 条)====================
|
||||||
|
用户原话:「编辑纪念日,弹出后无法关闭,点击关闭和取消都不行。」
|
||||||
|
|
||||||
|
🔴 真根因:关闭逻辑(✕ / 取消 / 点遮罩 / Esc)**原来只写在 days/index.php 里**
|
||||||
|
(那是"从列表页内嵌新建弹框"那一路)。而「修改」是**跳 `days/edit` 整页**,
|
||||||
|
那一页渲染出的是同一个 `.cal-modal` 壳(带 ✕ 和「取消」按钮),
|
||||||
|
**但没有任何脚本接它们** ⇒ 按钮点了没反应、看起来"关不掉"。
|
||||||
|
⇒ 修法:把"关闭自己"这段**搬进 form.php 本体**(自包含)——
|
||||||
|
它知道自己是不是弹框形态(`$anModal`),也就能自己接自己的关闭。
|
||||||
|
⚠️ days/index.php 里那份重复的关闭逻辑要**删掉**,否则两处都绑
|
||||||
|
(浏览器里表现为"点一次关两次"或竞态),见那里的注释。
|
||||||
|
⚠️ 「取消」在整页形态下是 `<a href>`(跳回去),这里只处理**弹框形态**的按钮。 */
|
||||||
|
(function () {
|
||||||
|
'use strict';
|
||||||
|
var card = document.getElementById('anNewCard');
|
||||||
|
if (!card) { return; }
|
||||||
|
/* ⚠️🔴 只在**整页直达**形态下接关闭(`data-an-standalone="1"`)。
|
||||||
|
列表页内嵌那一路由 days/index.php 的弹框控制器负责(它还要管"打开"、
|
||||||
|
刷新列表等),这里不抢 —— 两处都绑会变成"点一次关两次"或竞态。 */
|
||||||
|
if (card.getAttribute('data-an-standalone') !== '1') { return; }
|
||||||
|
|
||||||
|
function closeSelf() {
|
||||||
|
// 整页直达形态:关掉"弹框"没有意义(它就是这个页面)→ 退回列表页最自然
|
||||||
|
var back = card.getAttribute('data-an-back') || '';
|
||||||
|
if (back) { window.location.href = back; return; }
|
||||||
|
card.hidden = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
document.addEventListener('click', function (e) {
|
||||||
|
var t = e.target;
|
||||||
|
if (!t || !t.closest) { return; }
|
||||||
|
if (t.closest('#anNewX') || t.closest('#anNewCancel')) {
|
||||||
|
if (card.hidden) { return; }
|
||||||
|
e.preventDefault();
|
||||||
|
closeSelf();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
document.addEventListener('keydown', function (e) {
|
||||||
|
if (e.key !== 'Escape' || card.hidden) { return; }
|
||||||
|
// antd 面板开着时先让它收
|
||||||
|
var d = document.querySelector('.ant-picker-dropdown');
|
||||||
|
if (d && !d.classList.contains('ant-picker-dropdown-hidden')) { return; }
|
||||||
|
closeSelf();
|
||||||
|
});
|
||||||
|
})();
|
||||||
|
|
||||||
|
/* ==================== v42 第 8 条:日期 → month/day 拆分 ====================
|
||||||
|
AntD 的 DatePicker 写的是 `#an-date`(YYYY-MM-DD),而存储契约是
|
||||||
|
`month` + `day` 两个字段。这里用**一条监听**把二者绑起来:
|
||||||
|
an_date 一变(不管是谁改的)就拆出 month/day。
|
||||||
|
⚠️ 不能靠"只在 antd onChange 里拆" —— 那样"原生输入框自己改"(antd 没加载时)
|
||||||
|
就不拆,会出现"日期看着对、存进去是 1月1日"这种最难查的错。
|
||||||
|
⚠️ 农历纪念日也可能用"月/日",年份对它无意义 —— 但这里只取 mm/dd,
|
||||||
|
年份不参与存储,所以不影响。 */
|
||||||
|
(function () {
|
||||||
|
'use strict';
|
||||||
|
var d = document.getElementById('an-date');
|
||||||
|
var mo = document.getElementById('an-month');
|
||||||
|
var dy = document.getElementById('an-day');
|
||||||
|
if (!d || !mo || !dy) { return; }
|
||||||
|
function split() {
|
||||||
|
var m = (d.value || '').match(/^(\d{4})-(\d{2})-(\d{2})$/);
|
||||||
|
if (!m) { return; } // 空值不动(用户清空时保留原值,不至于变成 0)
|
||||||
|
mo.value = String(parseInt(m[2], 10));
|
||||||
|
dy.value = String(parseInt(m[3], 10));
|
||||||
|
}
|
||||||
|
d.addEventListener('change', split);
|
||||||
|
split(); // 初始也同步一次(编辑回填)
|
||||||
|
})();
|
||||||
|
</script>
|
||||||
|
<?php endif; ?>
|
||||||
@@ -111,9 +111,13 @@ $myPerson = (int)\App\Auth::personId();
|
|||||||
<form method="post" enctype="multipart/form-data" id="resForm"
|
<form method="post" enctype="multipart/form-data" id="resForm"
|
||||||
action="<?= e(url($isEditing ? 'resources/update' : 'resources/create')) ?>">
|
action="<?= e(url($isEditing ? 'resources/update' : 'resources/create')) ?>">
|
||||||
<?= csrf_field() ?>
|
<?= csrf_field() ?>
|
||||||
<?php if ($isEditing): ?>
|
<?php /* 🔴 v42(用户 2026-10-11 第 7 条):「资源编辑的弹框,复用新建资源的弹框,
|
||||||
<input type="hidden" name="id" value="<?= (int)$editing['id'] ?>">
|
保持一致。」⇒ 这个 `id` 隐藏框**始终存在**(新建时为空、编辑时填 id),
|
||||||
<?php endif; ?>
|
点列表里的「编辑」就把它填上、并把表单 action 切成 resources/update。
|
||||||
|
这样"新建 / 编辑"真的走同一个弹框、同一套排版与字段。
|
||||||
|
⚠️ 原来 `$isEditing` 时才输出它,导致"列表里点编辑"没法复用(只能去旧的
|
||||||
|
页签面板 `#resEditPanel`)—— 那两套界面不一样,正是用户报的问题。 */ ?>
|
||||||
|
<input type="hidden" name="id" id="rs-id" value="<?= $isEditing ? (int)$editing['id'] : '' ?>">
|
||||||
<div class="mv-form-inner">
|
<div class="mv-form-inner">
|
||||||
<div class="mv-grid">
|
<div class="mv-grid">
|
||||||
<div class="mv-row">
|
<div class="mv-row">
|
||||||
@@ -175,6 +179,22 @@ $myPerson = (int)\App\Auth::personId();
|
|||||||
</div>
|
</div>
|
||||||
<div class="cal-modal-foot">
|
<div class="cal-modal-foot">
|
||||||
<span class="cal-modal-tip" id="resAddTip"></span>
|
<span class="cal-modal-tip" id="resAddTip"></span>
|
||||||
|
<?php /* 🔴🔴 v42(用户第 7 条改动的**必要补充**):
|
||||||
|
原来「编辑」按钮打开的是**带页签的面板**(信息 / 附件 / 共享,v28 设计),
|
||||||
|
第 7 条要求"编辑复用新建那个弹框" ⇒ 编辑改到 `#resAddCard` 之后,
|
||||||
|
**共享 / 附件 这两个能力就没有入口了**(列表里只剩「编辑」「删除」两颗按钮)。
|
||||||
|
用户当年(v28 第 7 条)的原话是:
|
||||||
|
「编辑和附件两个按钮合并吧,就叫编辑,既可以更改信息,
|
||||||
|
也可以删除和增加附件…共享也合并,这样就剩下两个按钮,编辑和删除。」
|
||||||
|
⇒ 这两项能力**必须保留**,所以把它们放进新弹框的底栏:
|
||||||
|
编辑已有资源时,底栏多出「共享」「附件」两颗按钮,
|
||||||
|
点了就去开那个页签面板的对应页(能力一字未减,入口更明白)。
|
||||||
|
⚠️ 只有**编辑态**(有 id)才需要它们 —— 新建时资源还不存在,
|
||||||
|
没有"共享给谁""挂哪些附件"可说。 */ ?>
|
||||||
|
<button class="btn btn-sm btn-ghost" type="button" id="resToShare" hidden
|
||||||
|
data-res-pane="share">共享</button>
|
||||||
|
<button class="btn btn-sm btn-ghost" type="button" id="resToAtt" hidden
|
||||||
|
data-res-pane="att">附件</button>
|
||||||
<button class="btn btn-sm" type="submit" form="resForm" id="resAddSubmit">确定</button>
|
<button class="btn btn-sm" type="submit" form="resForm" id="resAddSubmit">确定</button>
|
||||||
<button class="btn btn-ghost btn-sm" type="button" id="resAddCancel">取消</button>
|
<button class="btn btn-ghost btn-sm" type="button" id="resAddCancel">取消</button>
|
||||||
</div>
|
</div>
|
||||||
@@ -523,8 +543,140 @@ $myPerson = (int)\App\Auth::personId();
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (addBtn && addCard) {
|
if (addBtn && addCard) {
|
||||||
addBtn.addEventListener('click', function () { setResOpen(true); });
|
addBtn.addEventListener('click', function () {
|
||||||
|
// 🔴 「新建」前先复位成"新建态"(上一次点过「编辑」的话表单已被灌过值)
|
||||||
|
resetToCreate();
|
||||||
|
setResOpen(true);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ============================================================
|
||||||
|
🔴 v42(用户 2026-10-11 第 7 条):「资源编辑的弹框,复用新建资源的弹框,
|
||||||
|
保持一致。」—— 列表里那颗「编辑」按钮现在也开**同一个** `#resAddCard`:
|
||||||
|
· 把行上的 data-*(标题/正文/归属人)灌进表单;
|
||||||
|
· `id` 隐藏框填上这条的 id,表单 action 切成 `resources/update`;
|
||||||
|
· 标题栏文字换成「编辑资源」。
|
||||||
|
原来的旧页签面板(`#resEditPanel`,v28 那套)**不再是编辑入口** ——
|
||||||
|
它现在只承担"附件 / 共享"两个页签(那两块仍是它独有的能力)。
|
||||||
|
⚠️ 分享的预填:那一行 button 上带了 `data-res-shares`(JSON),
|
||||||
|
这里把它写进选人组件的隐藏字段;选人组件自己的 chip 由 fbPersonPicker 重画。
|
||||||
|
============================================================ */
|
||||||
|
var CREATE_ACTION = <?= json_encode(url('resources/create'), JSON_UNESCAPED_UNICODE) ?>;
|
||||||
|
var UPDATE_ACTION = <?= json_encode(url('resources/update'), JSON_UNESCAPED_UNICODE) ?>;
|
||||||
|
|
||||||
|
function setPickerValues(name, ids) {
|
||||||
|
if (!addCard) { return; }
|
||||||
|
/* 🔴 用全局选人组件自己的接口(`window.ppSetSelected`,见 person_picker.js)——
|
||||||
|
它内部会:建候选清单(懒建)→ 设 checked → 重画 chips。
|
||||||
|
自己手改隐藏字段是不够的:外面的 chip 排不会跟着变,用户看到"没带上"。
|
||||||
|
⚠️ 第三个参数把**本弹框**作为 root 传进去:页面上可能有多个选人组件,
|
||||||
|
不限定 root 会命中文档里**第一个**(这正是 v24 踩过的坑,见它的注释)。 */
|
||||||
|
if (typeof window.ppSetSelected === 'function') {
|
||||||
|
window.ppSetSelected(name, ids.map(String), addCard);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// 兜底:组件没加载时至少把隐藏值写上(提交是对的,只是 chip 显示不出来)
|
||||||
|
var pp = addCard.querySelector('.pp[data-pp-name="' + name + '"]');
|
||||||
|
if (!pp) { return; }
|
||||||
|
Array.prototype.forEach.call(pp.querySelectorAll('input[type=hidden][name="' + name + '"]'),
|
||||||
|
function (i) { i.parentNode.removeChild(i); });
|
||||||
|
ids.forEach(function (id) {
|
||||||
|
var i = document.createElement('input');
|
||||||
|
i.type = 'hidden'; i.name = name; i.value = String(id);
|
||||||
|
pp.appendChild(i);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function openEdit(btn) {
|
||||||
|
if (!addCard) { return; }
|
||||||
|
var id = btn.getAttribute('data-res-id') || '';
|
||||||
|
var form = document.getElementById('resForm');
|
||||||
|
var idEl = document.getElementById('rs-id');
|
||||||
|
if (idEl) { idEl.value = id; }
|
||||||
|
if (form) { form.action = UPDATE_ACTION; }
|
||||||
|
var ttl = document.getElementById('resAddTitle');
|
||||||
|
if (ttl) { ttl.textContent = '编辑资源'; }
|
||||||
|
var t = document.getElementById('rs-title');
|
||||||
|
if (t) { t.value = btn.getAttribute('data-res-title') || ''; }
|
||||||
|
var c = document.getElementById('rs-content');
|
||||||
|
if (c) { c.value = btn.getAttribute('data-res-content') || ''; }
|
||||||
|
// 归属人(单选)
|
||||||
|
var owner = btn.getAttribute('data-res-owner') || '';
|
||||||
|
if (owner) { setPickerValues('owner_id', [parseInt(owner, 10)]); }
|
||||||
|
// 共享(多选)
|
||||||
|
var sh = btn.getAttribute('data-res-shares') || '[]';
|
||||||
|
var ids = [];
|
||||||
|
try {
|
||||||
|
var arr = JSON.parse(sh);
|
||||||
|
ids = arr.map(function (o) { return o.id; });
|
||||||
|
} catch (e) { ids = []; }
|
||||||
|
setPickerValues('share_ids[]', ids);
|
||||||
|
setResOpen(true);
|
||||||
|
/* 🔴 v42:编辑态才显示底栏的「共享」「附件」两颗按钮(新建时资源还不存在,
|
||||||
|
没有"共享给谁/挂哪些附件"可说)。 */
|
||||||
|
var bs = document.getElementById('resToShare');
|
||||||
|
var ba = document.getElementById('resToAtt');
|
||||||
|
if (bs) { bs.hidden = false; bs.setAttribute('data-res-id', id); }
|
||||||
|
if (ba) { ba.hidden = false; ba.setAttribute('data-res-id', id); }
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 底栏的「共享」「附件」:**复用产品原有的入口** ——
|
||||||
|
它们合成一个 `[data-res-open]` 事件交给页签面板那段脚本处理
|
||||||
|
(那段会把 id 填进**每一个** `form[data-res-idform]`,见它的长注释)。
|
||||||
|
⚠️ 必须走产品那条路(而不是自己再写一遍"打开面板 + 灌 id"):
|
||||||
|
否则反向验证里"把 id 注入改回只填第一个"这个注入**打不到探针** ⇒
|
||||||
|
证伪失效(本轮实测就栽在这里 —— 探针绕过产品逻辑,等于没测)。
|
||||||
|
⚠️ 用 `click()` 派发到页签面板脚本会命中的元素上:页签面板脚本监听
|
||||||
|
`document` 上的 click 并读 `closest('[data-res-open]')`,
|
||||||
|
所以这里造一个临时的 `[data-res-open]` 元素来 click 最直接可靠。 */
|
||||||
|
function openPane(pane, id) {
|
||||||
|
var t = document.createElement('button');
|
||||||
|
t.type = 'button';
|
||||||
|
t.setAttribute('data-res-open', pane);
|
||||||
|
t.setAttribute('data-res-id', id);
|
||||||
|
t.style.display = 'none';
|
||||||
|
document.body.appendChild(t);
|
||||||
|
t.click();
|
||||||
|
t.parentNode.removeChild(t);
|
||||||
|
}
|
||||||
|
['resToShare', 'resToAtt'].forEach(function (bid) {
|
||||||
|
var el = document.getElementById(bid);
|
||||||
|
if (!el) { return; }
|
||||||
|
el.addEventListener('click', function () {
|
||||||
|
var pane = el.getAttribute('data-res-pane') || '';
|
||||||
|
var id = el.getAttribute('data-res-id') || '';
|
||||||
|
if (!pane || !id) { return; }
|
||||||
|
setResOpen(false); // 先收起新弹框(避免两层遮罩叠着)
|
||||||
|
openPane(pane, id);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
function resetToCreate() {
|
||||||
|
var form = document.getElementById('resForm');
|
||||||
|
var idEl = document.getElementById('rs-id');
|
||||||
|
if (idEl) { idEl.value = ''; }
|
||||||
|
if (form) { form.action = CREATE_ACTION; }
|
||||||
|
var ttl = document.getElementById('resAddTitle');
|
||||||
|
if (ttl) { ttl.textContent = '新建资源'; }
|
||||||
|
var t = document.getElementById('rs-title'); if (t) { t.value = ''; }
|
||||||
|
var c = document.getElementById('rs-content'); if (c) { c.value = ''; }
|
||||||
|
/* 🔴 v42:回到"新建态"时收起底栏那两颗编辑专用按钮(见 openEdit 的注释) */
|
||||||
|
['resToShare', 'resToAtt'].forEach(function (bid) {
|
||||||
|
var el = document.getElementById(bid);
|
||||||
|
if (el) { el.hidden = true; el.removeAttribute('data-res-id'); }
|
||||||
|
});
|
||||||
|
setPickerValues('share_ids[]', []);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 事件委托:列表里的「编辑」按钮(AJAX 换过列表后仍然有效) */
|
||||||
|
document.addEventListener('click', function (e) {
|
||||||
|
var t = e.target;
|
||||||
|
if (!t || !t.closest) { return; }
|
||||||
|
var b = t.closest('[data-res-open="edit"]');
|
||||||
|
if (!b) { return; }
|
||||||
|
e.preventDefault();
|
||||||
|
openEdit(b);
|
||||||
|
});
|
||||||
['resAddX', 'resAddCancel'].forEach(function (id) {
|
['resAddX', 'resAddCancel'].forEach(function (id) {
|
||||||
var el = document.getElementById(id);
|
var el = document.getElementById(id);
|
||||||
if (el) { el.addEventListener('click', function () { setResOpen(false); }); }
|
if (el) { el.addEventListener('click', function () { setResOpen(false); }); }
|
||||||
@@ -692,6 +844,13 @@ $myPerson = (int)\App\Auth::personId();
|
|||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
|
|
||||||
var which = op.getAttribute('data-res-open');
|
var which = op.getAttribute('data-res-open');
|
||||||
|
/* 🔴 v42(用户第 7 条):**「编辑」不再走这个旧页签面板** ——
|
||||||
|
它现在复用「新建资源」那个弹框(`#resAddCard`,见本文件前一个 script)。
|
||||||
|
那颗按钮的处理在那边(也在 document 上委托,先于本段执行)。
|
||||||
|
这里只管 `att` / `share` / `del` 三个动作的页签面板。
|
||||||
|
⚠️ 必须在这里让开:否则同一颗按钮会被两处都接住,
|
||||||
|
表现为"编辑弹框开了、旧的页签面板也弹出来"(两层遮罩叠在一起)。 */
|
||||||
|
if (which === 'edit') { return; }
|
||||||
var id = op.getAttribute('data-res-id');
|
var id = op.getAttribute('data-res-id');
|
||||||
var title = op.getAttribute('data-res-title') || '';
|
var title = op.getAttribute('data-res-title') || '';
|
||||||
var panel = panels[which];
|
var panel = panels[which];
|
||||||
|
|||||||
@@ -25,31 +25,39 @@
|
|||||||
</div>
|
</div>
|
||||||
<?php else: ?>
|
<?php else: ?>
|
||||||
<div class="trip-list">
|
<div class="trip-list">
|
||||||
<?php foreach ($rows as $t): ?>
|
<?php /* 🔴🔴 v42(用户 2026-10-11 第 1 条):
|
||||||
<a class="trip-card" href="<?= e(url('trips/show', ['id' => (int)$t['id']])) ?>">
|
循环变量从 `$t` 改成 `$row`。
|
||||||
|
真根因:这一页(trips/index.php)在列表后面还 `require` 了 form.php
|
||||||
|
(新建旅程弹框),而 form.php 读的是 `$t`。`foreach ($rows as $t)`
|
||||||
|
循环结束后 `$t` **残留为最后一个旅程** ⇒ 新建弹框被填上那趟旅程的
|
||||||
|
标题/简介 —— 用户原话「新建旅程,把数据带进来了」。
|
||||||
|
⚠️ 改名只是第一道保险;form.php 那边也做了契约兼容(见其头部注释)。
|
||||||
|
两者都做,是为了以后任一处再被改回去时,另一处仍能兜住。 */ ?>
|
||||||
|
<?php foreach ($rows as $row): ?>
|
||||||
|
<a class="trip-card" href="<?= e(url('trips/show', ['id' => (int)$row['id']])) ?>">
|
||||||
<div class="trip-cover">
|
<div class="trip-cover">
|
||||||
<?php if (!empty($t['cover_key'])): ?>
|
<?php if (!empty($row['cover_key'])): ?>
|
||||||
<img src="<?= e(media_url((string)$t['cover_key'])) ?>" alt="">
|
<img src="<?= e(media_url((string)$row['cover_key'])) ?>" alt="">
|
||||||
<?php else: ?>
|
<?php else: ?>
|
||||||
<span class="trip-cover-empty">🧳</span>
|
<span class="trip-cover-empty">🧳</span>
|
||||||
<?php endif; ?>
|
<?php endif; ?>
|
||||||
</div>
|
</div>
|
||||||
<div class="trip-main">
|
<div class="trip-main">
|
||||||
<h2 class="trip-title"><?= e((string)$t['title']) ?></h2>
|
<h2 class="trip-title"><?= e((string)$row['title']) ?></h2>
|
||||||
<div class="trip-meta">
|
<div class="trip-meta">
|
||||||
<?php /* 时间是从各段旅程算的;一段都没有时如实说"还没有旅程" */ ?>
|
<?php /* 时间是从各段旅程算的;一段都没有时如实说"还没有旅程" */ ?>
|
||||||
<span><?= e(\App\Trip::activitySpan($t['starts_at'] ?? null, $t['ends_at'] ?? null)) ?></span>
|
<span><?= e(\App\Trip::activitySpan($row['starts_at'] ?? null, $row['ends_at'] ?? null)) ?></span>
|
||||||
<?php if (!empty($t['city'])): ?>
|
<?php if (!empty($row['city'])): ?>
|
||||||
<span>📍 <?= e((string)$t['city']) ?></span>
|
<span>📍 <?= e((string)$row['city']) ?></span>
|
||||||
<?php endif; ?>
|
<?php endif; ?>
|
||||||
<span><?= (int)($t['node_count'] ?? 0) ?> 段行程</span>
|
<span><?= (int)($row['node_count'] ?? 0) ?> 段行程</span>
|
||||||
</div>
|
</div>
|
||||||
<?php if (!empty($t['summary'])): ?>
|
<?php if (!empty($row['summary'])): ?>
|
||||||
<p class="trip-sum"><?= e(mb_substr((string)$t['summary'], 0, 80, 'UTF-8')) ?></p>
|
<p class="trip-sum"><?= e(mb_substr((string)$row['summary'], 0, 80, 'UTF-8')) ?></p>
|
||||||
<?php endif; ?>
|
<?php endif; ?>
|
||||||
<?php if (!empty($t['attendees'])): ?>
|
<?php if (!empty($row['attendees'])): ?>
|
||||||
<div class="trip-people">
|
<div class="trip-people">
|
||||||
<?php foreach ($t['attendees'] as $a): ?>
|
<?php foreach ($row['attendees'] as $a): ?>
|
||||||
<?= person_chip($a) ?>
|
<?= person_chip($a) ?>
|
||||||
<?php endforeach; ?>
|
<?php endforeach; ?>
|
||||||
</div>
|
</div>
|
||||||
@@ -57,5 +65,6 @@
|
|||||||
</div>
|
</div>
|
||||||
</a>
|
</a>
|
||||||
<?php endforeach; ?>
|
<?php endforeach; ?>
|
||||||
|
<?php unset($row); ?>
|
||||||
</div>
|
</div>
|
||||||
<?php endif; ?>
|
<?php endif; ?>
|
||||||
|
|||||||
@@ -28,8 +28,20 @@
|
|||||||
* 否则用户提交失败后跳到这一页只会看到一张空白(提示全在弹框里)。
|
* 否则用户提交失败后跳到这一页只会看到一张空白(提示全在弹框里)。
|
||||||
*
|
*
|
||||||
* @param array|null $t 编辑时的旅程;null = 新建
|
* @param array|null $t 编辑时的旅程;null = 新建
|
||||||
|
* @param array|null $tIn 【列表页内嵌时用】同上语义
|
||||||
* @param array $errors 校验错误
|
* @param array $errors 校验错误
|
||||||
|
*
|
||||||
|
* 🔴🔴 v42(用户 2026-10-11 第 1 条):「新建旅程,把数据带进来了。」
|
||||||
|
* 真根因:`trips/index.php` 用 `$tIn`(=null)把"编辑数据"传进来,
|
||||||
|
* 而这份模板读的是 `$t` —— 偏偏 `trips/_list_body.php` 里有个
|
||||||
|
* `foreach ($rows as $t)`,**循环结束后 `$t` 残留为列表里最后一个旅程**!
|
||||||
|
* ⇒ 新建弹框被填上了那趟旅程的标题/简介/封面。
|
||||||
|
* ⇒ 修法(两道保险,且不破坏路由那条路径):
|
||||||
|
* ① 契约兼容:列表页显式传了 `$tIn` 时**以它为准**(那份才是"要编辑的");
|
||||||
|
* 路由(trips/new、trips/edit)走的仍是 `$t`,原样不动;
|
||||||
|
* ② 列表页那边把 `_list_body.php` 的循环变量改名,**不再残留 `$t`**。
|
||||||
*/
|
*/
|
||||||
|
if (isset($tIn)) { $t = $tIn; unset($tIn); }
|
||||||
$isEdit = !empty($t);
|
$isEdit = !empty($t);
|
||||||
$canWrite = \App\Auth::canWrite();
|
$canWrite = \App\Auth::canWrite();
|
||||||
$cvSlot = \App\ImageSlot::get('cover') ?: ['ratio' => '16:9', 'w' => 1600, 'h' => 900,
|
$cvSlot = \App\ImageSlot::get('cover') ?: ['ratio' => '16:9', 'w' => 1600, 'h' => 900,
|
||||||
|
|||||||
@@ -196,7 +196,7 @@ $nfEmbed = !empty($nfEmbed);
|
|||||||
⚠️ 隐藏字段仍是 `*_date/_hour/_minute`,提交契约一字未改;
|
⚠️ 隐藏字段仍是 `*_date/_hour/_minute`,提交契约一字未改;
|
||||||
antd 拉不动时自动退回自建下拉(渐进增强)。 */ ?>
|
antd 拉不动时自动退回自建下拉(渐进增强)。 */ ?>
|
||||||
<div class="field dt-range full">
|
<div class="field dt-range full">
|
||||||
<label>开始 / 结束</label>
|
<label>时间范围</label>
|
||||||
<?php $dtName = 'starts_at'; $dtValue = $n['starts_at'] ?? '';
|
<?php $dtName = 'starts_at'; $dtValue = $n['starts_at'] ?? '';
|
||||||
$dtId = 'nf-s'; $dtRange = 'start'; $dtPlugin = 'antd';
|
$dtId = 'nf-s'; $dtRange = 'start'; $dtPlugin = 'antd';
|
||||||
require __DIR__ . '/../_datetime.php'; ?>
|
require __DIR__ . '/../_datetime.php'; ?>
|
||||||
@@ -339,7 +339,7 @@ $nfEmbed = !empty($nfEmbed);
|
|||||||
那种做法一旦父容器有 transform/filter,定位元素会改锚点、按钮就飘了。)
|
那种做法一旦父容器有 transform/filter,定位元素会改锚点、按钮就飘了。)
|
||||||
⚠️ 每个缩略图的 `photo_id` 不同 → 每张一个独立的删除表单(id 带 $ph['id'])。*/ ?>
|
⚠️ 每个缩略图的 `photo_id` 不同 → 每张一个独立的删除表单(id 带 $ph['id'])。*/ ?>
|
||||||
<div class="field full">
|
<div class="field full">
|
||||||
<label for="nf-photo">这一段行程的照片</label>
|
<label for="nf-photo">照片</label>
|
||||||
<?php $delForms = []; ?>
|
<?php $delForms = []; ?>
|
||||||
<?php if ($isEdit && !empty($n['attachs'])): ?>
|
<?php if ($isEdit && !empty($n['attachs'])): ?>
|
||||||
<div class="photo-grid">
|
<div class="photo-grid">
|
||||||
@@ -503,9 +503,24 @@ $nfEmbed = !empty($nfEmbed);
|
|||||||
(function () {
|
(function () {
|
||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
function bindLoc(prefix) {
|
/* 🔴🔴 v42(用户 2026-10-11 第 3/4 条):
|
||||||
var input = document.getElementById('loc-' + prefix + '-input');
|
这两段(高德地址联想 / 出行方式联动车辆与车次)原来是"页面加载时跑一次"的。
|
||||||
|
但 v41 把行程表单改成了**详情页弹框 + AJAX 取片段** —— 片段塞进 DOM 时
|
||||||
|
**内联 <script> 不会执行**(`innerHTML` 不跑脚本,这是浏览器语义),
|
||||||
|
于是用户在弹框里看到的是:
|
||||||
|
· 选「自驾」没出现车牌、选「火车/飞机」没出现车次(第 3 条);
|
||||||
|
· 出发地/到达地没有高德联想(第 4 条)。
|
||||||
|
⇒ 把这两段抽成**可重复调用**的 `window.fbNodeForm(scope)`:
|
||||||
|
详情页每次把片段塞进 DOM 后调它一次(见 show.php 的 loadInto)。
|
||||||
|
⚠️ 幂等由各自内部的标记保证(`data-drop-bound` 那套是本文件外的上传组件;
|
||||||
|
这里用 `data-nf-bound` / `data-loc-bound`)。 */
|
||||||
|
|
||||||
|
function bindLoc(prefix, scope) {
|
||||||
|
var input = (scope || document).querySelector('#loc-' + prefix + '-input');
|
||||||
if (!input) { return; }
|
if (!input) { return; }
|
||||||
|
// ⚠️ 幂等:同一段表单被反复塞进 DOM(弹框开关)时不能重复绑事件
|
||||||
|
if (input.getAttribute('data-loc-bound') === '1') { return; }
|
||||||
|
input.setAttribute('data-loc-bound', '1');
|
||||||
var wrap = input.closest('.loc-field');
|
var wrap = input.closest('.loc-field');
|
||||||
if (!wrap) { return; }
|
if (!wrap) { return; }
|
||||||
var fAddr = wrap.querySelector('input[name="' + prefix + '_addr"]');
|
var fAddr = wrap.querySelector('input[name="' + prefix + '_addr"]');
|
||||||
@@ -572,14 +587,14 @@ $nfEmbed = !empty($nfEmbed);
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
bindLoc('from');
|
/* ---- 出行方式 → 显隐车辆 / 车次(用户第 17 批 第 3 条)----
|
||||||
bindLoc('to');
|
抽成函数:弹框里重新塞进一段表单时要能再跑一次。
|
||||||
|
⚠️ 幂等:`change` 只绑一次(`data-nf-bound`),`sync()` 每次都可重跑。 */
|
||||||
/* 出行方式 → 显隐车辆 / 车次(用户第 17 批 第 3 条) */
|
function bindMode(scope) {
|
||||||
var modeSel = document.querySelector('[data-mode-select]');
|
var modeSel = (scope || document).querySelector('[data-mode-select]');
|
||||||
if (modeSel) {
|
if (!modeSel) { return; }
|
||||||
var vehBox = document.querySelector('[data-mode-vehicle]');
|
var vehBox = (scope || document).querySelector('[data-mode-vehicle]');
|
||||||
var tikBox = document.querySelector('[data-mode-ticket]');
|
var tikBox = (scope || document).querySelector('[data-mode-ticket]');
|
||||||
var DRIVE = ['drive']; // 与 Trip::DRIVE_MODES 一致
|
var DRIVE = ['drive']; // 与 Trip::DRIVE_MODES 一致
|
||||||
var TICKET = ['train', 'plane']; // 与 Trip::TICKET_MODES 一致
|
var TICKET = ['train', 'plane']; // 与 Trip::TICKET_MODES 一致
|
||||||
var sync = function () {
|
var sync = function () {
|
||||||
@@ -587,8 +602,24 @@ $nfEmbed = !empty($nfEmbed);
|
|||||||
if (vehBox) { vehBox.hidden = DRIVE.indexOf(v) === -1; }
|
if (vehBox) { vehBox.hidden = DRIVE.indexOf(v) === -1; }
|
||||||
if (tikBox) { tikBox.hidden = TICKET.indexOf(v) === -1; }
|
if (tikBox) { tikBox.hidden = TICKET.indexOf(v) === -1; }
|
||||||
};
|
};
|
||||||
|
if (modeSel.getAttribute('data-nf-bound') !== '1') {
|
||||||
|
modeSel.setAttribute('data-nf-bound', '1');
|
||||||
modeSel.addEventListener('change', sync);
|
modeSel.addEventListener('change', sync);
|
||||||
|
}
|
||||||
sync();
|
sync();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* 整个表单的初始化(可重复调用;scope 缺省 = document) */
|
||||||
|
function initNodeForm(scope) {
|
||||||
|
var root = scope || document;
|
||||||
|
bindLoc('from', root);
|
||||||
|
bindLoc('to', root);
|
||||||
|
bindMode(root);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 首屏跑一次(整页形态)
|
||||||
|
initNodeForm(document);
|
||||||
|
// 暴露给"片段被塞进 DOM 之后"的调用方(详情页弹框)
|
||||||
|
window.fbNodeForm = initNodeForm;
|
||||||
})();
|
})();
|
||||||
</script>
|
</script>
|
||||||
@@ -169,6 +169,23 @@ $groupMeta = [
|
|||||||
`_tooling/_fb_b19_backfill.php` 一次性搬进 summary。 */ ?>
|
`_tooling/_fb_b19_backfill.php` 一次性搬进 summary。 */ ?>
|
||||||
<?php if (!empty($t['summary'])): ?>
|
<?php if (!empty($t['summary'])): ?>
|
||||||
<p class="info-text trip-sum"><?= nl2br(e((string)$t['summary'])) ?></p>
|
<p class="info-text trip-sum"><?= nl2br(e((string)$t['summary'])) ?></p>
|
||||||
|
<?php elseif ($canWrite): ?>
|
||||||
|
<?php /* 🔴 v42(用户 2026-10-11 第 6 条):「旅程详情页面,参与的人下面,
|
||||||
|
原来有旅程的简介,怎么没了?加回来。」
|
||||||
|
⚠️ 排查结论:简介**一直在**(上面的 `!empty($t['summary'])` 分支),
|
||||||
|
结构从 v40 到 v41 一字未动(v41 只把「参与的人」后面那个括号
|
||||||
|
连同里面的说明整句去掉了)。
|
||||||
|
⇒ 用户"看不到"的真实原因是**那条旅程的简介是空的** ——
|
||||||
|
他多半是先用 v41 那个"新建旅程带入了旧数据"的 bug(本批第 1 条)
|
||||||
|
建/改过,summary 没存进去。
|
||||||
|
⇒ 修法两层:
|
||||||
|
① 第 1 条那个 bug 已修(新建不再串数据);
|
||||||
|
② 这里补一句**可点的占位**:简介为空但能写的人,看到"还没有简介,
|
||||||
|
点这里写一句" —— 否则整块是空的,用户会以为"功能被删了"。 */ ?>
|
||||||
|
<p class="trip-sum-empty">
|
||||||
|
还没有简介 ——
|
||||||
|
<a href="<?= e(url('trips/edit', ['id' => (int)$t['id']])) ?>">点这里写一句</a>。
|
||||||
|
</p>
|
||||||
<?php endif; ?>
|
<?php endif; ?>
|
||||||
|
|
||||||
<?php /* 老数据兜底(v24 第 19 批 第 4 条):只填过「备注」、没填过「简介」的旅程,
|
<?php /* 老数据兜底(v24 第 19 批 第 4 条):只填过「备注」、没填过「简介」的旅程,
|
||||||
@@ -513,6 +530,7 @@ foreach ($bysStage as $lst) { $shown += count($lst); }
|
|||||||
/* 动态插入的控件:日期 / antd / 选人 / 上传都要在这一刻升级 */
|
/* 动态插入的控件:日期 / antd / 选人 / 上传都要在这一刻升级 */
|
||||||
if (window.fbUpgradeRange) { window.fbUpgradeRange(); }
|
if (window.fbUpgradeRange) { window.fbUpgradeRange(); }
|
||||||
if (window.fbBootAntd) { window.fbBootAntd(); }
|
if (window.fbBootAntd) { window.fbBootAntd(); }
|
||||||
|
if (window.fbNodeForm) { window.fbNodeForm(body() || document); }
|
||||||
var f = document.getElementById('nf-title');
|
var f = document.getElementById('nf-title');
|
||||||
if (f) { f.focus(); }
|
if (f) { f.focus(); }
|
||||||
} else if (body()) {
|
} else if (body()) {
|
||||||
@@ -525,6 +543,33 @@ foreach ($bysStage as $lst) { $shown += count($lst); }
|
|||||||
}
|
}
|
||||||
|
|
||||||
/* ---- 打开:从 data-node-open / data-trip-edit 拿 URL,取片段塞进来 ---- */
|
/* ---- 打开:从 data-node-open / data-trip-edit 拿 URL,取片段塞进来 ---- */
|
||||||
|
/* 🔴🔴 v42(用户 2026-10-11 第 2/3/4 条)——**根因级修法**:
|
||||||
|
AJAX 塞进来的片段里,`<script>` **既不执行、连元素都不保留**
|
||||||
|
(`innerHTML` 的语义:脚本不会跑,且赋值后 DOM 里也找不到那个 script 标签)。
|
||||||
|
片段自带的初始化(AntD 升级 / 高德联想 / 出行方式联动)全写在那段脚本里
|
||||||
|
⇒ 用户在弹框里看到的是:
|
||||||
|
· 起止时间没换成 AntD 控件(第 2 条);
|
||||||
|
· 选「自驾」不出车辆、选「火车/飞机」不出车次(第 3 条);
|
||||||
|
· 出发地/到达地没有高德联想(第 4 条)。
|
||||||
|
⇒ 做法:**把片段里的内联 `<script>` 抠出来,重新平成一个能执行的脚本**。
|
||||||
|
这是"动态插入含脚本的 HTML"的标准修法;比"在宿主里逐个复刻片段的初始化"
|
||||||
|
可靠得多 —— 片段将来再加初始化时,宿主不用跟着改。
|
||||||
|
⚠️ 只执行**内联**脚本(有 src 的是外部 SDK,设了 src 也不会同步执行,跳过即可;
|
||||||
|
高德那支是 `<script src>`,它的加载写在片段里由 SDK 自己完成)。
|
||||||
|
⚠️ 幂等:同一片段重复塞入时重新执行没问题(初始化内部都有幂等标记)。 */
|
||||||
|
function runInlineScripts(container, html) {
|
||||||
|
var re = /<script\b[^>]*>([\s\S]*?)<\/script>/gi, m, code = '';
|
||||||
|
while ((m = re.exec(html)) !== null) {
|
||||||
|
// 只收**没有 src 属性**的那种(内联脚本)
|
||||||
|
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); // 执行完就摘掉,别留在 DOM 里
|
||||||
|
}
|
||||||
|
|
||||||
function loadInto(url, title) {
|
function loadInto(url, title) {
|
||||||
if (titleEl()) { titleEl().textContent = title || '行程'; }
|
if (titleEl()) { titleEl().textContent = title || '行程'; }
|
||||||
if (body()) { body().innerHTML = '<p class="empty-hint">正在载入…</p>'; }
|
if (body()) { body().innerHTML = '<p class="empty-hint">正在载入…</p>'; }
|
||||||
@@ -535,8 +580,12 @@ foreach ($bysStage as $lst) { $shown += count($lst); }
|
|||||||
.then(function (html) {
|
.then(function (html) {
|
||||||
if (!body()) { return; }
|
if (!body()) { return; }
|
||||||
body().innerHTML = html;
|
body().innerHTML = html;
|
||||||
|
/* ① 先执行片段自带的内联脚本(它会定义/调用自己的初始化) */
|
||||||
|
runInlineScripts(body(), html);
|
||||||
|
/* ② 再补跑宿主这侧的通用升级(日期/antd/行程表单),按依赖顺序 */
|
||||||
if (window.fbUpgradeRange) { window.fbUpgradeRange(); }
|
if (window.fbUpgradeRange) { window.fbUpgradeRange(); }
|
||||||
if (window.fbBootAntd) { window.fbBootAntd(); }
|
if (window.fbBootAntd) { window.fbBootAntd(); }
|
||||||
|
if (window.fbNodeForm) { window.fbNodeForm(body()); }
|
||||||
var f = document.getElementById('nf-title');
|
var f = document.getElementById('nf-title');
|
||||||
if (f) { f.focus(); }
|
if (f) { f.focus(); }
|
||||||
})
|
})
|
||||||
@@ -593,8 +642,10 @@ foreach ($bysStage as $lst) { $shown += count($lst); }
|
|||||||
return r.json().then(function (j) {
|
return r.json().then(function (j) {
|
||||||
if (body() && j && j.modal) {
|
if (body() && j && j.modal) {
|
||||||
body().innerHTML = j.modal;
|
body().innerHTML = j.modal;
|
||||||
|
runInlineScripts(body(), j.modal); // 片段里的内联脚本要执行(见 loadInto 的长注释)
|
||||||
if (window.fbUpgradeRange) { window.fbUpgradeRange(); }
|
if (window.fbUpgradeRange) { window.fbUpgradeRange(); }
|
||||||
if (window.fbBootAntd) { window.fbBootAntd(); }
|
if (window.fbBootAntd) { window.fbBootAntd(); }
|
||||||
|
if (window.fbNodeForm) { window.fbNodeForm(body()); }
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user