v62:纪念日列表修正 / 分类图标与设置菜单 / 首页卡片体系

- 纪念日列表:display:flex 从 td 移到内层 div(写在 td 上会破坏表格列算法,
  表现为关联人列与其它列高度不一致);行内列序按表头对齐;
  改完再点「新建」不再带出上次修改的数据;文案改为「与我相关」「来自名册」;
  别人记的、把我挂成关联人的条目一并显示(split() 返回 hiddenIds,
  路由层据此过滤「接下来的日子」,与两张表同一套可见性)
- 分类图标:新增 app/AnnivIcon.php(17 个行内 SVG 图标 + 同 key 的 emoji
  供日历纯文本处使用,内网无 CDN 不用 FontAwesome);去掉「看日历」按钮;
  设置按钮改为菜单,第一项进 days/settings 独立页;分类可自选图标,
  列表徽标与日历条目都按分类显示
- 首页卡片:新增 app/HomeCards.php 与 templates/home_cards.php;
  8 格宽 × 高 2/4/6/8;首页拆成 templates/home/_card_*.php 共 8 个片段;
  home/cards 可改尺寸、调顺序、开关
This commit is contained in:
2026-10-19 12:51:17 +08:00
parent ad7f43af68
commit 9a66068c33
21 changed files with 2172 additions and 685 deletions
+110 -31
View File
@@ -13,20 +13,39 @@
*
* ⇒ 三块(顺序按用户第 9 条要求):
* ① 接下来的日子(未来 60 天)
* ② **手动添加的**纪念日(在上)—— 可改可删;右侧按钮:改=蓝(.btn)、删=黄(.btn-warn)
* ③ 来自名册生日的(在下,实时派生、不能改删)
* ② **与我相关**的纪念日(在上)—— 可改可删;右侧按钮:改=蓝(.btn)、删=黄(.btn-warn)
* ③ 来自名册的生日(在下,实时派生、不能改删)
*
* @param array $custom 用户自建的(行上带 person_ids)
* 🔴 v62(用户 2026-10-18 第 1、2 条)改了四处:
* ① 两块标题:「我自己添加的」→「与我相关」、「来自名册的」→「来自名册」,
* 并**删掉**那句"这一块是实时派生的…"的说明;
* ② 「关于我相关」这一块要收进"别人添加、但把我也挂成关联人"的条目
* (判据见 `Anniversary::split()`);
* ③ 「看日历」按钮去掉(日历在顶栏就有,页面里再放一个只是多一个入口);
* ④ 分类徽标带图标(`AnnivIcon`),日历里用同一图标的 emoji 形态。
*
* @param array $custom 与我相关的(行上带 person_ids)
* @param array $derived 从名册生日派生的(行上也有 person_ids)
* @param int $hidden 别人记的、与我无关的条数(只用于一句小字说明)
* @param array $upcoming 接下来 60 天的
* @param array $kinds 分类清单(设置里维护)
* @param array $people 名册(把 person id 换成头像 + 称呼)
*/
$canWrite = \App\Auth::canWrite();
$kindBadge = static function (array $r): string {
$famId = (int)\App\Auth::familyId();
/* 分类徽标:图标 + 名字(v62 第 2 条「列表中…要根据分类,显示不同的图标」)。
⚠️ 图标走 `Anniversary::iconOf()`(单点来源)—— 不要在模板里再写一遍
"生日用蛋糕、忌日用蜡烛"的映射,否则设置页改了图标这里不跟着变。 */
$kindBadge = static function (array $r) use ($famId): string {
$k = (string)($r['kind'] ?? '');
return $k === '' ? '' : '<span class="node-badge">' . e($k) . '</span>';
if ($k === '') {
return '';
}
return '<span class="node-badge kind-badge">'
. \App\AnnivIcon::svg(\App\Anniversary::iconOf($famId, $k), 13, 'kind-ic')
. '<span class="kind-name">' . e($k) . '</span></span>';
};
$srcWord = static function (array $r): string {
$src = (string)($r['source'] ?? 'custom');
if ($src === 'birthday_solar') {
@@ -105,10 +124,24 @@ $renderTable = function (array $rows, bool $editable) use ($canWrite, $kindBadge
<?php endif; ?>
<?php endif; ?>
</td>
<td class="ann-people"><?= $peopleCell($r) ?></td>
<?php /* 🔴🔴 v62(用户 2026-10-18 第 1 条):「我自己添加的部分,现在显示有错位,
好像关联人和其他列,高度不一样。」
⚠️ 这里有**两个**独立的错位,别只修一个:
① 高度错位(已修):这两格原来把 `display:flex` 直接写在 **`<td>`** 上
(`.ann-people` / `.ann-acts`)。`display` 一改成 flex,这个元素
**就不再是 table-cell** —— 它从表格的列算法里脱了出来,
于是整行的列宽/基线全乱(同一行里有的格按表格排、有的格按 flex 排)。
⇒ `<td>` 恢复成普通单元格,flex 放到**里面一层 div** 上。
② **内容错位(本次修)**:表头写的是「关联人 | 分类 | 重复」,
但行里依次塞的是「分类 | 重复 | 关联人」——
三列的内容整体往左挪了一格,用户看到的"关联人"那一栏里其实是分类。
⇒ **表头保持原顺序不动**(「关联人」在「分类」前是用户当初指定的),
把**行里的顺序**改成跟表头一致:
日期 | 纪念日 | **关联人** | **分类** | 重复。 */ ?>
<td><div class="ann-people"><?= $peopleCell($r) ?></div></td>
<td><?= $kindBadge($r) ?></td>
<td class="small muted"><?= e($srcWord($r)) ?></td>
<td class="nowrap ann-acts">
<td class="nowrap"><div class="ann-acts">
<?php if ($editable && $canWrite): ?>
<?php /* 用户第 2 条:「右侧按钮,和其他模块的按钮样式保持一致,
修改用蓝色,删除用黄色。」
@@ -135,7 +168,7 @@ $renderTable = function (array $rows, bool $editable) use ($canWrite, $kindBadge
<?php else: ?>
<span class="small muted">来自名册</span>
<?php endif; ?>
</td>
</div></td>
</tr>
<?php endforeach; ?>
</tbody>
@@ -156,8 +189,12 @@ $renderTable = function (array $rows, bool $editable) use ($canWrite, $kindBadge
data-href="<?= e(url('days/new')) ?>"
aria-haspopup="dialog" aria-expanded="false">新建纪念日</button>
<?php endif; ?>
<a class="btn btn-ghost" href="<?= e(url('calendar')) ?>">看日历</a>
<?php /* 用户第 9 条:设置按钮就在「新建纪念日」右边(☰,排最右) */ ?>
<?php /* 🔴 v62(用户 2026-10-18 第 2 条):「纪念日,看日历这个按钮去掉。」
理由(用户没说的那一半):日历在**顶栏**上一直有,这一页再放一个
"看日历"只是同一件事的第二入口,还占掉了本该给「设置」的位置。
⚠️ 不要再"改个名字留着" —— 用户说的是去掉。 */ ?>
<?php /* 用户第 9 条:设置按钮就在「新建纪念日」右边(☰,排最右)。
v62:点开是**菜单**,第一项「分类设置」(可给每个分类配图标)。 */ ?>
<?php $setCurrent = 'kinds'; require __DIR__ . '/_settings_menu.php'; ?>
</div>
</div>
@@ -214,12 +251,34 @@ $renderTable = function (array $rows, bool $editable) use ($canWrite, $kindBadge
(container || document.body).appendChild(s);
s.parentNode.removeChild(s);
}
function loadEdit(url) {
/* 🔴🔴 v62(用户 2026-10-18 第 1 条):「如果我修改的一个纪念日,然后再点击新建纪念日,
竟然把我刚才修改的纪念日带进来了,其实不是新建,而是修改。」
真根因:**「修改」和「新建」共用同一个 `#anNewCard`**(v43 那轮为了"编辑也开弹框"
刻意做的)。而「修改」是把 `days/edit?embed=1` 吐回来的那块**替换**掉当前的卡片 ——
替换之后,卡片里已经躺着一份带 `input[name=id]` 的编辑表单。
这时点「新建」,旧代码只做了一件事:`setOpen(true)`(把同一张卡片显示出来),
于是屏幕上就是"刚才那条被编辑的纪念日",按确定也仍然是**更新**而不是新增。
⇒ 修法:开「新建」之前先看这张卡片**是不是还停在"改过/正在改"的状态**:
· 有 `input[name=id]` → 这是编辑表单,必须换回新建表单;
· 或者标题框里已经有内容(上次新建填了一半又关掉、或刚保存过一条)→ 同样换新,
否则"新建"会带出上一次输了一半的内容,是同一类错觉。
⇒ 换新的方式与「修改」完全对称:fetch `days/new`(内嵌片段)替换卡片。
⚠️ 只有"脏"的时候才发这一趟请求 —— 干净的弹框直接开,不多一次网络往返。 */
function needsFresh() {
var c = card();
if (!c || !window.fetch) { if (url) { window.location.href = url; } return; }
if (!c) { return false; }
if (c.querySelector('#anForm input[name="id"]')) { return true; }
var t = c.querySelector('#an-title');
return !!(t && String(t.value || '').trim() !== '');
}
function loadForm(url, title) {
var c = card();
if (!c || !window.fetch || !url) { if (url) { window.location.href = url; } return; }
c.hidden = false; // 先打开(显示"载入中"占位)
var lb = c.querySelector('#anNewCardTitle, .cal-modal-head b');
if (lb) { lb.textContent = '编辑纪念日'; }
if (lb) { lb.textContent = title; }
fetch(url, { credentials: 'same-origin',
headers: { 'X-Requested-With': 'XMLHttpRequest' } })
.then(function (r) { return r.text(); })
@@ -231,17 +290,29 @@ $renderTable = function (array $rows, bool $editable) use ($canWrite, $kindBadge
c.parentNode.replaceChild(fresh, c);
runInlineScripts(fresh, html);
setOpen(true);
/* 换了新片段之后,标题框里是空的 ⇒ 后面几次开合都不会再发请求 */
var b = document.getElementById('anNewBtn');
if (b) { b.setAttribute('aria-expanded', 'true'); }
})
.catch(function () { window.location.href = url.replace(/[?&]embed=1/, ''); });
}
document.addEventListener('click', function (e) {
var t = e.target;
if (!t || !t.closest) { return; }
if (t.closest('#anNewBtn')) { e.preventDefault(); setOpen(true); return; }
if (t.closest('#anNewBtn')) {
e.preventDefault();
var btn = t.closest('#anNewBtn');
if (needsFresh()) {
loadForm(btn.getAttribute('data-href') || '', '新建纪念日');
} else {
setOpen(true);
}
return;
}
var ed = t.closest('[data-an-edit]');
if (ed) {
e.preventDefault();
loadEdit(ed.getAttribute('data-href') || '');
loadForm(ed.getAttribute('data-href') || '', '编辑纪念日');
return;
}
if (t.closest('#anNewX') || t.closest('#anNewCancel')) { setOpen(false); return; }
@@ -341,15 +412,17 @@ $renderTable = function (array $rows, bool $editable) use ($canWrite, $kindBadge
<?php endif; ?>
</div>
<?php /* ==================== ② 手动添加的(在上,用户第 9 条)==================== */ ?>
<?php /* ==================== ② 与我相关的(在上,用户第 9 条)====================
🔴 v62(用户 2026-10-18 第 1 条):标题「我自己添加的(4)」+「只有这些能改能删」
→ **「与我相关」**(hint 整条删掉)。
理由:这一块现在不只有"我添加的",还有"别人添加、但把我挂成了关联人"的
(判据见 Anniversary::split())。继续叫"我自己添加的"就是错的;
而"只有这些能改能删"这句在**功能上**没必要跟用户解释(按按钮能改能删,一看就懂)。 */ ?>
<div class="card card-pad">
<h2 class="cinema-h2">
我自己添加的(<?= count($custom) ?>)
<span class="hint">只有这些能改能删</span>
</h2>
<h2 class="cinema-h2">与我相关(<?= count($custom) ?>)</h2>
<?php if (!$custom): ?>
<p class="empty-hint">
还没有自己添加的纪念日。
还没有与你相关的纪念日。
<?php if ($canWrite): ?>
结婚纪念日、忌日、相识的日子…点右上角「新建纪念日」加一条。
<?php endif; ?>
@@ -357,24 +430,30 @@ $renderTable = function (array $rows, bool $editable) use ($canWrite, $kindBadge
<?php else: ?>
<?php $renderTable($custom, true); ?>
<?php endif; ?>
<?php /* ⚠️ 别人记的、只跟他自己相关的那些不在这儿出现("与我相关"就该是相关的)。
但如果**一条都不提示**,用户可能以为数据丢了 —— 给一句小字,并说清去哪儿找。 */ ?>
<?php if ((int)($hidden ?? 0) > 0): ?>
<p class="small muted" style="margin:10px 0 0">
另有 <?= (int)$hidden ?> 条是别人记的、与你无关,这里不列;日历里照旧能看到。
</p>
<?php endif; ?>
</div>
<?php /* ==================== ③ 来自名册的生日(在下)==================== */ ?>
<?php /* ==================== ③ 来自名册的生日(在下)====================
🔴 v62(用户 2026-10-18 第 1 条):标题「来自名册的(20)」+「实时读出来的,
改生日这里跟着变」→ **「来自名册」**;下面那段"这一块是实时派生的…"整段删掉。
为什么删:那是**实现说明**,用户看的是日子,不是数据从哪来 ——
而"改生日这里跟着变"已经由表格右上那句「来自名册」暗示得够清楚了。 */ ?>
<div class="card card-pad">
<h2 class="cinema-h2">
来自名册的(<?= count($derived) ?>)
<span class="hint">实时读出来的,改生日这里跟着变</span>
</h2>
<h2 class="cinema-h2">来自名册(<?= count($derived) ?>)</h2>
<?php if (!$derived): ?>
<p class="empty-hint">
名册里还没有人填过生日。到「名册 → 编辑成员」把生日填上,这里就会自动出现。
</p>
<?php else: ?>
<?php $renderTable($derived, false); ?>
<p class="tip" style="margin-top:10px">
这一块是**实时派生**的 —— 改生日,这里跟着变,不用重复维护。
不需要在这里新建,也没法单独删(删掉生日就等于删了它)。
</p>
<?php endif; ?>
</div>
</div><?php /* /#anListHost */ ?>
</div><?php /* /#anListHost —— 这个 div 只开了一次(见上面 `<div id="anListHost">`),
收尾也只能有一次。曾经这一行被复制成两行 ⇒ 整个页面的 div 深度变成 -1,
元素跑到容器外(流水线的"div 嵌套"检查抓到的就是这个)。 */ ?>