Files
FamilyBook/templates/days/form.php
T
laoyang 81dfacaf1b 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。
2026-10-11 18:35:03 +08:00

267 lines
12 KiB
PHP
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.
<?php
/**
* 新建 / 编辑纪念日(v24 用户第 13 条;**v41 改成弹框 + 观影式排版**)
*
* 用户原话:
* · 第 13 条:「可以根据阴历和阳历来设置纪念日 …… 纪念日,需要按照阴历和阳历进行重复。」
* · v41 第 10 条:「新建纪念日,采用弹框形式,排版参考观影模块。」
*
* ⇒ 两条落实:
* ① **弹框**(`$anModal`):从列表页内嵌,初始关闭;直达 `days/new` 时以打开状态渲染;
* ② **排版**:整表单改用观影那套 `.mv-row`(左标签 62px + 右对齐 + 自动冒号,
* 控件占满剩余宽度),不再用 `.field` 堆叠。
*
* ⚠️ 重复方式这一栏是核心:阳历 / 农历 / 只此一次。选农历时下面的月/日
* 填的是**农历**的月日(不是公历换算)。
*
* @param array|null $editing 编辑时的既有记录
* @param array $people 名册
*/
/* 🔴 v41:被列表页 `require` 内嵌时必须**自己兜住变量**。
否则 `$editing` 未定义 → 每一处都吐 `Warning: Undefined variable`
且**打进 HTML 正文**(`HTTP 200`,看着"页面正常",实际满屏警告,
还把 `class="cal-modal"` 那行**切断**了 —— 实测:弹框容器因此变成
`<div class="cal-modal"<br /><b>Warning</b>…` ⇒ 前端 `getElementById` 拿不到,
"点新建没反应"。这正是全站那条"模板片段不许偷宿主变量"的同一类坑。 */
$editing = $editing ?? null;
$people = $people ?? [];
$kinds = $kinds ?? null;
$e0 = $editing ?: [];
$v = static fn(string $k, $def = '') => $e0[$k] ?? $def;
$repeat = (string)($e0['repeat_type'] ?? 'solar');
// 用户 2026-10-03 第 9 条:分类下拉的选项来自**可配置**的分类清单
$kinds = (array)($kinds ?? \App\Anniversary::PRESET_KINDS);
$curKind = (string)($e0['kind'] ?? '');
if ($curKind !== '' && !in_array($curKind, $kinds, true)) {
array_unshift($kinds, $curKind);
}
/* 🔴 v41 弹框开关(与观影/旅程同一套命名习惯):
$anModal —— 渲染成弹框壳
$anModalOnly —— 被列表页内嵌(初始关闭)
$anOpen —— 初始打开(直达 / AJAX 校验失败重画)
$anEmbed —— 只吐片段(详情页/列表页弹框 fetch 用) */
$anModal = !isset($anModal) ? true : (bool)$anModal;
$anModalOnly = !empty($anModalOnly);
$anOpen = !empty($anOpen);
$anEmbed = !empty($anEmbed);
$anShell = !isset($anShell) ? !$anEmbed : (bool)$anShell;
$backUrl = url('days');
?>
<?php if (!$anEmbed && !$anModal): ?>
<div class="page-head">
<h1><?= $editing ? '编辑纪念日' : '新建纪念日' ?></h1>
<div class="actions">
<a class="btn btn-ghost" href="<?= e($backUrl) ?>">返回</a>
</div>
</div>
<?php endif; ?>
<?php if ($anShell): ?>
<div class="<?= $anModal ? 'cal-modal' : 'card card-pad' ?>"<?php
if ($anModal) {
echo ' id="anNewCard" role="dialog" aria-modal="true" aria-label="'
. ($editing ? '编辑纪念日' : '新建纪念日') . '"' . ($anOpen ? '' : ' hidden');
/* 🔴 v42 第 9 条:标记"这是**整页直达**的弹框"(不是列表页内嵌的),
以及它该退回到哪里 —— 关闭时若无宿主容器,就跳回列表页。
`$anModalOnly` = "被列表页内嵌",那种情形由列表页的容器接管,不用跳页。 */
if (!$anModalOnly) {
echo ' data-an-standalone="1" data-an-back="' . e($backUrl) . '"';
}
} ?>>
<?php if ($anModal): ?>
<div class="cal-modal-panel mv-panel">
<div class="cal-modal-head">
<b><?= $editing ? '编辑纪念日' : '新建纪念日' ?></b>
<button type="button" class="cal-modal-x" id="anNewX" aria-label="关闭">&#10005;</button>
</div>
<?php endif; ?>
<?php endif; ?>
<form method="post" action="<?= e(url('days/save')) ?>" id="anForm">
<?= csrf_field() ?>
<?php if ($editing): ?><input type="hidden" name="id" value="<?= (int)$e0['id'] ?>"><?php endif; ?>
<?php if ($anModal && !$editing): ?>
<input type="hidden" name="back" value="days">
<?php endif; ?>
<div class="mv-form-inner">
<div class="mv-grid">
<div class="mv-row">
<span class="mv-lb">叫什么<i>*</i></span>
<input class="input" id="an-title" name="title" required
value="<?= e((string)$v('title')) ?>" placeholder="如:结婚纪念日">
</div>
<div class="mv-row">
<span class="mv-lb">按什么重复<i>*</i></span>
<select class="select" id="an-repeat" name="repeat_type">
<?php foreach (\App\Anniversary::REPEAT as $rk => $rv): ?>
<option value="<?= e($rk) ?>" <?= $repeat === $rk ? 'selected' : '' ?>><?= e($rv) ?></option>
<?php endforeach; ?>
</select>
</div>
<div class="mv-row">
<span class="mv-lb">分类</span>
<select class="select" id="an-kind" name="kind">
<option value="">不分类</option>
<?php foreach ($kinds as $k): ?>
<option value="<?= e((string)$k) ?>" <?= $curKind === (string)$k ? 'selected' : '' ?>><?= e((string)$k) ?></option>
<?php endforeach; ?>
</select>
</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">
<span class="mv-lb">日期<i>*</i></span>
<div class="dt-input dt-antd-single" data-antd-date="1" id="an-date-wrap">
<input type="date" class="dt-date" id="an-date" name="an_date"
value="<?= e(sprintf('%04d-%02d-%02d', (int)$v('year', (int)date('Y')), (int)$v('month', 1), (int)$v('day', 1))) ?>"
aria-label="日期">
<input type="hidden" id="an-month" name="month" value="<?= (int)$v('month', 1) ?>">
<input type="hidden" id="an-day" name="day" value="<?= (int)$v('day', 1) ?>">
</div>
</div>
<div class="mv-row">
<span class="mv-lb">与谁相关</span>
<div class="mv-fill">
<?php
$ppName = 'person_ids[]';
$ppPeople = $people;
$ppSelected = (array)($e0['person_ids'] ?? []);
if (!$ppSelected && (int)($e0['person_id'] ?? 0) > 0) {
$ppSelected = [(int)$e0['person_id']];
}
$ppMode = 'multi';
$ppOpenBtn = '+ 关联人';
$ppTitle = '这个纪念日与谁相关';
require __DIR__ . '/../_person_picker.php';
?>
</div>
</div>
<div class="mv-row">
<span class="mv-lb">农历闰月</span>
<label class="chk-inline">
<input type="checkbox" id="an-leap" name="is_leap" value="1" <?= !empty($e0['is_leap']) ? 'checked' : '' ?>>
<span>这个农历月是闰月</span>
</label>
</div>
<div class="mv-row">
<span class="mv-lb">备注</span>
<textarea class="input" id="an-note" name="note" rows="2"><?= e((string)$v('note')) ?></textarea>
</div>
</div>
</div>
<?php if ($anShell && $anModal): ?>
<div class="cal-modal-foot">
<span class="cal-modal-tip" id="anNewTip"></span>
<button class="btn btn-sm" type="submit" form="anForm" id="anNewSubmit">确定</button>
<button class="btn btn-ghost btn-sm" type="button" id="anNewCancel">取消</button>
</div>
</div><?php /* /.cal-modal-panel */ ?>
<?php elseif ($anEmbed): ?>
<div class="cal-modal-foot">
<span class="cal-modal-tip" id="anNewTip"></span>
<button class="btn btn-sm" type="submit" form="anForm" id="anNewSubmit">确定</button>
<button class="btn btn-ghost btn-sm" type="button" data-nf-close>取消</button>
</div>
<?php else: ?>
<div class="btn-row">
<button class="btn" type="submit" id="anNewSubmit"><?= $editing ? '确定' : '添加' ?></button>
<a class="btn btn-ghost" href="<?= e($backUrl) ?>">取消</a>
</div>
<?php endif; ?>
</form>
<?php if ($anShell): ?>
</div><?php /* 关掉开头那个外层(.cal-modal 或 .card) */ ?>
<?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; ?>