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:
@@ -25,31 +25,39 @@
|
||||
</div>
|
||||
<?php else: ?>
|
||||
<div class="trip-list">
|
||||
<?php foreach ($rows as $t): ?>
|
||||
<a class="trip-card" href="<?= e(url('trips/show', ['id' => (int)$t['id']])) ?>">
|
||||
<?php /* 🔴🔴 v42(用户 2026-10-11 第 1 条):
|
||||
循环变量从 `$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">
|
||||
<?php if (!empty($t['cover_key'])): ?>
|
||||
<img src="<?= e(media_url((string)$t['cover_key'])) ?>" alt="">
|
||||
<?php if (!empty($row['cover_key'])): ?>
|
||||
<img src="<?= e(media_url((string)$row['cover_key'])) ?>" alt="">
|
||||
<?php else: ?>
|
||||
<span class="trip-cover-empty">🧳</span>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
<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">
|
||||
<?php /* 时间是从各段旅程算的;一段都没有时如实说"还没有旅程" */ ?>
|
||||
<span><?= e(\App\Trip::activitySpan($t['starts_at'] ?? null, $t['ends_at'] ?? null)) ?></span>
|
||||
<?php if (!empty($t['city'])): ?>
|
||||
<span>📍 <?= e((string)$t['city']) ?></span>
|
||||
<span><?= e(\App\Trip::activitySpan($row['starts_at'] ?? null, $row['ends_at'] ?? null)) ?></span>
|
||||
<?php if (!empty($row['city'])): ?>
|
||||
<span>📍 <?= e((string)$row['city']) ?></span>
|
||||
<?php endif; ?>
|
||||
<span><?= (int)($t['node_count'] ?? 0) ?> 段行程</span>
|
||||
<span><?= (int)($row['node_count'] ?? 0) ?> 段行程</span>
|
||||
</div>
|
||||
<?php if (!empty($t['summary'])): ?>
|
||||
<p class="trip-sum"><?= e(mb_substr((string)$t['summary'], 0, 80, 'UTF-8')) ?></p>
|
||||
<?php if (!empty($row['summary'])): ?>
|
||||
<p class="trip-sum"><?= e(mb_substr((string)$row['summary'], 0, 80, 'UTF-8')) ?></p>
|
||||
<?php endif; ?>
|
||||
<?php if (!empty($t['attendees'])): ?>
|
||||
<?php if (!empty($row['attendees'])): ?>
|
||||
<div class="trip-people">
|
||||
<?php foreach ($t['attendees'] as $a): ?>
|
||||
<?php foreach ($row['attendees'] as $a): ?>
|
||||
<?= person_chip($a) ?>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
@@ -57,5 +65,6 @@
|
||||
</div>
|
||||
</a>
|
||||
<?php endforeach; ?>
|
||||
<?php unset($row); ?>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
@@ -28,8 +28,20 @@
|
||||
* 否则用户提交失败后跳到这一页只会看到一张空白(提示全在弹框里)。
|
||||
*
|
||||
* @param array|null $t 编辑时的旅程;null = 新建
|
||||
* @param array|null $tIn 【列表页内嵌时用】同上语义
|
||||
* @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);
|
||||
$canWrite = \App\Auth::canWrite();
|
||||
$cvSlot = \App\ImageSlot::get('cover') ?: ['ratio' => '16:9', 'w' => 1600, 'h' => 900,
|
||||
|
||||
@@ -196,7 +196,7 @@ $nfEmbed = !empty($nfEmbed);
|
||||
⚠️ 隐藏字段仍是 `*_date/_hour/_minute`,提交契约一字未改;
|
||||
antd 拉不动时自动退回自建下拉(渐进增强)。 */ ?>
|
||||
<div class="field dt-range full">
|
||||
<label>开始 / 结束</label>
|
||||
<label>时间范围</label>
|
||||
<?php $dtName = 'starts_at'; $dtValue = $n['starts_at'] ?? '';
|
||||
$dtId = 'nf-s'; $dtRange = 'start'; $dtPlugin = 'antd';
|
||||
require __DIR__ . '/../_datetime.php'; ?>
|
||||
@@ -339,7 +339,7 @@ $nfEmbed = !empty($nfEmbed);
|
||||
那种做法一旦父容器有 transform/filter,定位元素会改锚点、按钮就飘了。)
|
||||
⚠️ 每个缩略图的 `photo_id` 不同 → 每张一个独立的删除表单(id 带 $ph['id'])。*/ ?>
|
||||
<div class="field full">
|
||||
<label for="nf-photo">这一段行程的照片</label>
|
||||
<label for="nf-photo">照片</label>
|
||||
<?php $delForms = []; ?>
|
||||
<?php if ($isEdit && !empty($n['attachs'])): ?>
|
||||
<div class="photo-grid">
|
||||
@@ -503,9 +503,24 @@ $nfEmbed = !empty($nfEmbed);
|
||||
(function () {
|
||||
'use strict';
|
||||
|
||||
function bindLoc(prefix) {
|
||||
var input = document.getElementById('loc-' + prefix + '-input');
|
||||
/* 🔴🔴 v42(用户 2026-10-11 第 3/4 条):
|
||||
这两段(高德地址联想 / 出行方式联动车辆与车次)原来是"页面加载时跑一次"的。
|
||||
但 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; }
|
||||
// ⚠️ 幂等:同一段表单被反复塞进 DOM(弹框开关)时不能重复绑事件
|
||||
if (input.getAttribute('data-loc-bound') === '1') { return; }
|
||||
input.setAttribute('data-loc-bound', '1');
|
||||
var wrap = input.closest('.loc-field');
|
||||
if (!wrap) { return; }
|
||||
var fAddr = wrap.querySelector('input[name="' + prefix + '_addr"]');
|
||||
@@ -572,14 +587,14 @@ $nfEmbed = !empty($nfEmbed);
|
||||
}
|
||||
}
|
||||
|
||||
bindLoc('from');
|
||||
bindLoc('to');
|
||||
|
||||
/* 出行方式 → 显隐车辆 / 车次(用户第 17 批 第 3 条) */
|
||||
var modeSel = document.querySelector('[data-mode-select]');
|
||||
if (modeSel) {
|
||||
var vehBox = document.querySelector('[data-mode-vehicle]');
|
||||
var tikBox = document.querySelector('[data-mode-ticket]');
|
||||
/* ---- 出行方式 → 显隐车辆 / 车次(用户第 17 批 第 3 条)----
|
||||
抽成函数:弹框里重新塞进一段表单时要能再跑一次。
|
||||
⚠️ 幂等:`change` 只绑一次(`data-nf-bound`),`sync()` 每次都可重跑。 */
|
||||
function bindMode(scope) {
|
||||
var modeSel = (scope || document).querySelector('[data-mode-select]');
|
||||
if (!modeSel) { return; }
|
||||
var vehBox = (scope || document).querySelector('[data-mode-vehicle]');
|
||||
var tikBox = (scope || document).querySelector('[data-mode-ticket]');
|
||||
var DRIVE = ['drive']; // 与 Trip::DRIVE_MODES 一致
|
||||
var TICKET = ['train', 'plane']; // 与 Trip::TICKET_MODES 一致
|
||||
var sync = function () {
|
||||
@@ -587,8 +602,24 @@ $nfEmbed = !empty($nfEmbed);
|
||||
if (vehBox) { vehBox.hidden = DRIVE.indexOf(v) === -1; }
|
||||
if (tikBox) { tikBox.hidden = TICKET.indexOf(v) === -1; }
|
||||
};
|
||||
modeSel.addEventListener('change', sync);
|
||||
if (modeSel.getAttribute('data-nf-bound') !== '1') {
|
||||
modeSel.setAttribute('data-nf-bound', '1');
|
||||
modeSel.addEventListener('change', 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>
|
||||
@@ -169,6 +169,23 @@ $groupMeta = [
|
||||
`_tooling/_fb_b19_backfill.php` 一次性搬进 summary。 */ ?>
|
||||
<?php if (!empty($t['summary'])): ?>
|
||||
<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 /* 老数据兜底(v24 第 19 批 第 4 条):只填过「备注」、没填过「简介」的旅程,
|
||||
@@ -513,6 +530,7 @@ foreach ($bysStage as $lst) { $shown += count($lst); }
|
||||
/* 动态插入的控件:日期 / antd / 选人 / 上传都要在这一刻升级 */
|
||||
if (window.fbUpgradeRange) { window.fbUpgradeRange(); }
|
||||
if (window.fbBootAntd) { window.fbBootAntd(); }
|
||||
if (window.fbNodeForm) { window.fbNodeForm(body() || document); }
|
||||
var f = document.getElementById('nf-title');
|
||||
if (f) { f.focus(); }
|
||||
} else if (body()) {
|
||||
@@ -525,6 +543,33 @@ foreach ($bysStage as $lst) { $shown += count($lst); }
|
||||
}
|
||||
|
||||
/* ---- 打开:从 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) {
|
||||
if (titleEl()) { titleEl().textContent = title || '行程'; }
|
||||
if (body()) { body().innerHTML = '<p class="empty-hint">正在载入…</p>'; }
|
||||
@@ -535,8 +580,12 @@ foreach ($bysStage as $lst) { $shown += count($lst); }
|
||||
.then(function (html) {
|
||||
if (!body()) { return; }
|
||||
body().innerHTML = html;
|
||||
/* ① 先执行片段自带的内联脚本(它会定义/调用自己的初始化) */
|
||||
runInlineScripts(body(), html);
|
||||
/* ② 再补跑宿主这侧的通用升级(日期/antd/行程表单),按依赖顺序 */
|
||||
if (window.fbUpgradeRange) { window.fbUpgradeRange(); }
|
||||
if (window.fbBootAntd) { window.fbBootAntd(); }
|
||||
if (window.fbNodeForm) { window.fbNodeForm(body()); }
|
||||
var f = document.getElementById('nf-title');
|
||||
if (f) { f.focus(); }
|
||||
})
|
||||
@@ -593,8 +642,10 @@ foreach ($bysStage as $lst) { $shown += count($lst); }
|
||||
return r.json().then(function (j) {
|
||||
if (body() && j && j.modal) {
|
||||
body().innerHTML = j.modal;
|
||||
runInlineScripts(body(), j.modal); // 片段里的内联脚本要执行(见 loadInto 的长注释)
|
||||
if (window.fbUpgradeRange) { window.fbUpgradeRange(); }
|
||||
if (window.fbBootAntd) { window.fbBootAntd(); }
|
||||
if (window.fbNodeForm) { window.fbNodeForm(body()); }
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user