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
+37 -49
View File
@@ -1,63 +1,51 @@
<?php
/**
* 纪念日模块的「设置」下拉(v24 用户 2026-10-03 第 9 条)
* 纪念日模块的「设置」下拉(v24 用户 2026-10-03 第 9 条;🔴 v62 改成菜单)。
*
* 用户原话:「在新建纪念日和右侧,也增加设置按钮,可以设置纪念日的分类。」
* v24 原话:「在新建纪念日和右侧,也增加设置按钮,可以设置纪念日的分类。」
* —— 当时做成了"下拉里直接铺一张分类增删表单"。
*
* 分类清单是**家庭级**的(settings.anniv_kinds)—— 全家共用一套叫法,
* 不然「结婚纪念日」在两个人屏幕上是两套名字,看的时候对不上。
* 所以这里跟「模块主题色」一样:**能改的是所有人**,但改的是同一份。
* 🔴 v62(用户 2026-10-18 第 2 条):「设置按钮,点击后弹菜单,**第一个菜单是分类设置**,
* 进去之后可以设置分类,每一类纪念日都有不同的图标……」
* ⇒ 与日程/观影/任务三个模块**统一手感**(☰ → 一串列表 → 点进独立页):
* 下拉里**不放表单**,只放一条条目的链接;分类的增删改与图标选择都在
* `days/settings` 那一页上(见 templates/days/settings.php)。
*
* 为什么必须这么改(与日程那条同一个理由,别丢):分类是**家庭级**的,
* 一个家能有十几二十个分类,每个分类还要选图标 —— 全铺在下拉里,
* 下拉会被撑到屏幕外,而"设置"按钮点开本来就该是"一个目录"。
*
* 用法(require 前 set 好):
* $kinds 当前分类清单
* $kindCounts 每个分类用了几条(显示"这个分类有没有在用")
* $setCurrent 当前页 key('kinds')用来高亮;'' 也行(从别处挂过来时没有当前项)
*/
$daySetKinds = (array)($kinds ?? \App\Anniversary::PRESET_KINDS);
$daySetCounts = (array)($kindCounts ?? []);
$daySetCanWrite = \App\Auth::canWrite();
$setCurrent = (string)($setCurrent ?? '');
$setOn = static function (string $key) use ($setCurrent): string {
return $setCurrent === $key ? ' class="on"' : '';
};
$setIcon = static function (string $name): string {
$paths = [
// 分类:三个小方块("一格一类")
'kinds' => '<rect x="2" y="2.2" width="5" height="5" rx="1.2"/>'
. '<rect x="9" y="2.2" width="5" height="5" rx="1.2"/>'
. '<rect x="2" y="9" width="5" height="5" rx="1.2"/>'
. '<rect x="9" y="9" width="5" height="5" rx="1.2"/>',
];
$d = $paths[$name] ?? '';
if ($d === '') {
return '';
}
return '<svg class="mi" viewBox="0 0 16 16" width="16" height="16" aria-hidden="true"'
. ' fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"'
. ' stroke-linejoin="round">' . $d . '</svg>';
};
?>
<details class="movie-set menu ann-set">
<summary class="btn btn-icon" title="纪念日设置" aria-label="纪念日设置"><svg viewBox="0 0 16 16" width="18" height="18" aria-hidden="true" fill="none" stroke="currentColor" stroke-width="1.9" stroke-linecap="round"><path d="M2.5 4.5h11M2.5 8h11M2.5 11.5h11"/></svg></summary>
<div class="menu-list ann-set-list">
<div class="cal-set-h">纪念日分类(全家共用一套)</div>
<?php if (!$daySetKinds): ?>
<p class="small muted" style="margin:0 0 8px">一个分类都没有。下面加一个。</p>
<?php else: ?>
<ul class="ann-kind-list">
<?php foreach ($daySetKinds as $k): ?>
<li>
<span class="ann-kind-name"><?= e((string)$k) ?></span>
<span class="small muted"><?= (int)($daySetCounts[(string)$k] ?? 0) ?> 条在用</span>
<?php if ($daySetCanWrite): ?>
<form method="post" action="<?= e(url('days/kinds')) ?>" class="res-inline">
<?= csrf_field() ?>
<input type="hidden" name="del" value="<?= e((string)$k) ?>">
<button class="btn btn-ghost btn-sm" type="submit"
title="删掉这个分类(已用它的纪念日不变)">删</button>
</form>
<?php endif; ?>
</li>
<?php endforeach; ?>
</ul>
<?php endif; ?>
<?php if ($daySetCanWrite): ?>
<form method="post" action="<?= e(url('days/kinds')) ?>" class="led-set-form-in" style="margin-top:8px">
<?= csrf_field() ?>
<input class="input" name="add" maxlength="20" placeholder="新分类,如:相识纪念日">
<button class="btn btn-sm" type="submit">加分类</button>
</form>
<?php endif; ?>
<p class="small muted" style="margin:8px 0 0">
删掉一个分类**不会**改已用过它的纪念日 —— 只是新建时不再出现在下拉里。
</p>
<?php /* 第一项(也是目前唯一一项):分类设置 —— 名称 + 图标 + 删除 + 排序 */ ?>
<a href="<?= e(url('days/settings')) ?>"<?= $setOn('kinds') ?>><?= $setIcon('kinds') ?><span>分类设置</span></a>
</div>
</details>
<?php
// ⚠️ 只 unset **本片段自己定义**的那三个前缀变量。
// 绝不能再写 `unset($kinds, $kindCounts, $canWrite)` —— days/index.php 里
// 同样有 $canWrite(还有 $kinds),那样会把宿主模板的变量一起干掉,
// 后面每一处用到的地方都变成 Undefined variable(账本那个片段刚这么炸过一次)。
unset($daySetKinds, $daySetCounts, $daySetCanWrite);
// ⚠️ 只 unset **本片段自己定义**的变量,绝不碰宿主的 $setCurrent / $kinds / $canWrite。
unset($setCurrent, $setOn, $setIcon);
+14 -5
View File
@@ -30,11 +30,20 @@ $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);
/* 用户 2026-10-03 第 9 条:分类下拉的选项来自**可配置**的分类清单。
🔴 v62:清单改成"名称 + 图标"(`kindsFull`)。下拉里的 `<option>` 放不进 SVG,
所以这里用同一图标的 emoji 形态打头(与日历里一致)—— 用户一眼能对上
"下拉里选的那个"和"列表/日历上显示的那个"是同一个。 */
$kindsFull = (array)($kindsFull ?? \App\Anniversary::kindsFull((int)\App\Auth::familyId()));
$kinds = array_column($kindsFull, 'name');
$curKind = (string)($e0['kind'] ?? '');
if ($curKind !== '' && !in_array($curKind, $kinds, true)) {
array_unshift($kinds, $curKind);
// 老数据里的分类名可能已经从清单里删掉了 —— 仍然列出来,否则一保存就被清空
array_unshift($kindsFull, [
'name' => $curKind,
'icon' => \App\Anniversary::defaultIcon($curKind),
]);
$kinds = array_column($kindsFull, 'name');
}
/* 🔴 v41 弹框开关(与观影/旅程同一套命名习惯):
@@ -108,8 +117,8 @@ $backUrl = url('days');
<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 foreach ($kindsFull as $kf): ?>
<option value="<?= e((string)$kf['name']) ?>" <?= $curKind === (string)$kf['name'] ? 'selected' : '' ?>><?= e(\App\AnnivIcon::emo((string)$kf['icon']) . ' ' . (string)$kf['name']) ?></option>
<?php endforeach; ?>
</select>
</div>
+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 嵌套"检查抓到的就是这个)。 */ ?>
+160
View File
@@ -0,0 +1,160 @@
<?php
/**
* 纪念日「分类设置」页(v62 用户 2026-10-18 第 2 条)。
*
* 用户原话:「设置按钮,点击后弹菜单,第一个菜单是分类设置,进去之后可以设置分类,
* 每一类纪念日都有不同的图标,现在的分类,你自己把图标补全。如果是用的
* fontawesome,那么如果用户添加了自己的分类,就让用户自己选。
* 在列表中、日历中,都要根据分类,显示不同的图标。」
*
* ⇒ 这一页做三件事(都在**一张表单**里,改完点一次保存):
* ① 改名(已用它的纪念日**不会**跟着改名 —— 页面底下有一句提示,别让用户以为丢了);
* ② 换图标(下拉里是 `AnnivIcon` 那一套,选完左边立刻能预览);
* ③ 调顺序 / 增 / 删。
*
* ⚠️ 三个动作(保存 / 删除 / 上移下移 / 新增)**共用同一个表单**,靠 submit 按钮的
* `name` 区分(`del` / `move` / `add`)。为什么不用"每行一个小表单":
* 一行一个小表单的话,用户改了 A 行的图标、又点了 B 行的「删」,
* A 行那次修改会被**静默丢弃**(浏览器只提交被点的那张表单)。
* 一张表单 = 一次提交 = 屏幕上看到的改动全部生效,没有"改了一半"的中间态。
*
* ⚠️ 分类是**家庭级**的(settings.anniv_kinds):全家共用一套叫法。
* 所以这一页对**所有能写的成员**开放(不是只有管理员),但改的是同一份。
*
* @param array $kindsFull array<int,array{name:string,icon:string}>
* @param array $kindCounts 分类名 => 用了几条
*/
$kindList = (array)($kindsFull ?? \App\Anniversary::kindsFull((int)\App\Auth::familyId()));
$kindCount = (array)($kindCounts ?? []);
$canEdit = \App\Auth::canWrite();
$n = count($kindList);
?>
<div class="page-head">
<h1>纪念日分类</h1>
<span class="sub">每个分类一个图标 · 全家共用一套</span>
<div class="actions">
<a class="btn btn-ghost" href="<?= e(url('days')) ?>">返回纪念日</a>
</div>
</div>
<div class="card card-pad">
<h2 class="cinema-h2">
分类与图标
<span class="hint">列表、日历、卡片上都按这里的图标显示</span>
</h2>
<?php if (!$canEdit): ?>
<p class="empty-hint">你没有编辑权限,这里只显示现有分类。</p>
<ul class="kind-list">
<?php foreach ($kindList as $k): ?>
<li class="kind-row is-ro">
<span class="kind-prev"><?= \App\AnnivIcon::svg((string)$k['icon'], 16) ?></span>
<span class="kind-name-ro"><?= e((string)$k['name']) ?></span>
<span class="kind-use"><?= (int)($kindCount[(string)$k['name']] ?? 0) ?> 条</span>
</li>
<?php endforeach; ?>
</ul>
<?php else: ?>
<form method="post" action="<?= e(url('days/settings')) ?>" id="kindForm">
<?= csrf_field() ?>
<ol class="kind-list" id="kindList">
<?php foreach ($kindList as $i => $k): ?>
<li class="kind-row">
<span class="kind-move">
<button class="btn btn-ghost btn-sm" type="submit" name="move" value="<?= (int)$i ?>:up"
title="上移" <?= $i === 0 ? 'disabled' : '' ?>>▲</button>
<button class="btn btn-ghost btn-sm" type="submit" name="move" value="<?= (int)$i ?>:down"
title="下移" <?= $i === $n - 1 ? 'disabled' : '' ?>>▼</button>
</span>
<?php /* 左边这个 svg 是"当前图标"的预览;右边下拉一动,JS 立刻换成新的
(图标库见页面底部的 .kind-icon-bank,不在 JS 里手抄一遍 path)。 */ ?>
<span class="kind-prev" data-kind-prev="<?= (int)$i ?>"><?= \App\AnnivIcon::svg((string)$k['icon'], 16) ?></span>
<select class="select kind-sel" name="k_icon[<?= (int)$i ?>]"
aria-label="<?= e((string)$k['name']) ?>的图标" data-kind-sel="<?= (int)$i ?>">
<?php foreach (\App\AnnivIcon::choices() as $icKey => $icLabel): ?>
<option value="<?= e($icKey) ?>" <?= (string)$k['icon'] === $icKey ? 'selected' : '' ?>><?= e($icLabel) ?></option>
<?php endforeach; ?>
</select>
<input class="input kind-name-in" name="k_name[<?= (int)$i ?>]" maxlength="20"
value="<?= e((string)$k['name']) ?>" aria-label="分类名">
<span class="kind-use"><?= (int)($kindCount[(string)$k['name']] ?? 0) ?> 条</span>
<?php /* ⚠️ confirm 里**不要**拼分类名:e() 把单引号转成 `&#039;`,
而浏览器会先解实体再交给 JS ⇒ 名字里带引号时这段脚本直接语法错,
「删除」按钮点了没反应(本项目踩过同型的坑)。用固定文案最稳。 */ ?>
<button class="btn btn-warn btn-sm" type="submit" name="del" value="<?= (int)$i ?>"
onclick="return confirm('删掉这个分类?已用它的纪念日不变,只是新建时不再出现。')">删除</button>
</li>
<?php endforeach; ?>
</ol>
<?php if (!$kindList): ?>
<p class="empty-hint" style="margin:8px 0 14px">还没有分类。下面加一个。</p>
<?php endif; ?>
<div class="kind-add">
<span class="kind-add-lb">加一个分类</span>
<select class="select" name="new_icon" aria-label="新分类的图标">
<?php foreach (\App\AnnivIcon::choices() as $icKey => $icLabel): ?>
<option value="<?= e($icKey) ?>" <?= $icKey === 'star' ? 'selected' : '' ?>><?= e($icLabel) ?></option>
<?php endforeach; ?>
</select>
<input class="input" name="new_name" maxlength="20" placeholder="如:相识纪念日">
<button class="btn btn-sm" type="submit" name="add" value="1">加分类</button>
</div>
<div class="btn-row" style="margin-top:16px">
<button class="btn" type="submit" name="save" value="1">保存分类</button>
<?php /* 动过图标/名字但还没保存时 JS 会把这句显出来(见页面底部脚本):
这一页**不自动保存**,不提示的话用户挑完图标直接返回,改动就静默丢了。 */ ?>
<span class="small kind-dirty" id="kindDirty" hidden>改过了,记得点左边的「保存分类」</span>
</div>
</form>
<p class="small muted" style="margin:14px 0 0">
改名<b>不会</b>改掉已经用过旧名字的那些纪念日(它们仍记着旧分类);
删掉一个分类也只是"新建时不再出现",历史记录照旧。
<br>分类加得越多,新建时的下拉越长 —— 建议按"这件事是什么性质"来分,不要按人来分(人有关联人那一列)。
</p>
<?php /* 图标库:全部图标在这里渲染一次,JS 只做"挑一个塞进预览",
避免在 JS 里再抄一份 SVG path(两份迟早不一致)。 */ ?>
<div class="kind-icon-bank" hidden aria-hidden="true">
<?php foreach (array_keys(\App\AnnivIcon::LIST) as $icKey): ?>
<span data-ic="<?= e($icKey) ?>"><?= \App\AnnivIcon::svg($icKey, 16) ?></span>
<?php endforeach; ?>
</div>
<script>
(function () {
var bank = document.querySelector('.kind-icon-bank');
if (!bank) { return; }
/* 改完图标/名字之后**一定要点保存**(这一页不自动存)。
不提示的话,用户挑完图标顺手点「返回纪念日」,改动就静默丢了 ——
所以只要动过,就把保存按钮标脏,并挂一句小字。 */
var saveBtn = document.querySelector('button[name=save]');
var dirty = document.getElementById('kindDirty');
function markDirty() {
if (saveBtn) { saveBtn.classList.add('is-dirty'); }
if (dirty) { dirty.hidden = false; }
}
Array.prototype.forEach.call(document.querySelectorAll('[data-kind-sel]'), function (sel) {
sel.addEventListener('change', function () {
var i = sel.getAttribute('data-kind-sel');
var prev = document.querySelector('[data-kind-prev="' + i + '"]');
var src = bank.querySelector('[data-ic="' + sel.value + '"]');
if (prev && src) { prev.innerHTML = src.innerHTML; }
markDirty();
});
});
Array.prototype.forEach.call(document.querySelectorAll('#kindList input.kind-name-in'),
function (inp) { inp.addEventListener('input', markDirty); });
/* 任何一次提交(保存 / 加分类 / 删除 / 上移下移)都算"处理过了",
标志就地清掉 —— 免得提交完回来还挂着红点。 */
var f = document.getElementById('kindForm');
if (f) { f.addEventListener('submit', function () {
if (saveBtn) { saveBtn.classList.remove('is-dirty'); }
if (dirty) { dirty.hidden = true; }
}); }
})();
</script>
<?php endif; ?>
</div>