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:
+175
-20
@@ -38,6 +38,23 @@ final class Anniversary
|
||||
/** 庆典类纪念日大致的分类,给个默认值方便挑选(用户可自由填) */
|
||||
public const PRESET_KINDS = ['生日', '结婚纪念日', '相识纪念日', '忌日', '节日', '其它'];
|
||||
|
||||
/**
|
||||
* 预置分类各配哪个图标(v62)。
|
||||
*
|
||||
* ⚠️ 这张表只用于**兜底**:分类名 → 图标真正的来源是 settings 里那份
|
||||
* `anniv_kinds`(用户可以在「分类设置」里逐条改)。这里的作用是
|
||||
* 「老库升级上来、还没配过图标」时,六个预置分类不至于全变成同一个圆点。
|
||||
* ⚠️ 值必须是 AnnivIcon 里存在的 key;认不出的会被 normalize 成 dot。
|
||||
*/
|
||||
public const PRESET_ICONS = [
|
||||
'生日' => 'cake',
|
||||
'结婚纪念日' => 'ring',
|
||||
'相识纪念日' => 'heart',
|
||||
'忌日' => 'candle',
|
||||
'节日' => 'star',
|
||||
'其它' => 'dot',
|
||||
];
|
||||
|
||||
public const ATTACH = 'anniversary';
|
||||
|
||||
// ---------------- ① 用户自建的纪念日 ----------------
|
||||
@@ -309,12 +326,24 @@ final class Anniversary
|
||||
* 用户原话:「纪念日中,手动添加的纪念日和来自其他模块的,例如生日的纪念日,
|
||||
* 要分开,手动纪念日在上面。」
|
||||
*
|
||||
* 所以返回 ['custom' => [...], 'derived' => [...]] 两块,各按日期排。
|
||||
* 所以返回 ['custom' => [...], 'derived' => [...]], 'hidden' => N 三块,各按日期排。
|
||||
* ⚠️ 派生块用**这个人的出生年**来算"今年是几号"没法排序(农历要逐年换算),
|
||||
* 这里统一按 month/day 排(也就是"一年里按日历顺序"),
|
||||
* 与列表上显示的「X 月 X 日」完全一致 —— 不会出现"排出来的顺序和看到的不一样"。
|
||||
*
|
||||
* @return array{custom:array,derived:array}
|
||||
* 🔴 v62(用户 2026-10-18 第 1 条):第一块从「我自己添加的」改叫「与我相关」,
|
||||
* 原话:「这里要注意,如果是别人分享给我的,就是我是关联人,这里应该也要显示。」
|
||||
* ⇒ 判据(**满足一条就留下**):
|
||||
* ① 这条纪念日没填关联人 —— 它是"全家的事"(结婚纪念日、家庭节日…),
|
||||
* 谁添加的都该看得到;
|
||||
* ② 我是它的关联人之一 —— 别人添加的、把我挂上了,就该出现在我这儿。
|
||||
* 反过来:别人给他自己家亲戚记的忌日(关联人只有那位亲戚)**不进我的列表** ——
|
||||
* 那正是"与我无关"。这类条数放回 `hidden` 里,模板上给一句小字说明,
|
||||
* 免得用户以为"数据丢了"(它们在日历里照旧出现)。
|
||||
*
|
||||
* ⚠️ 派生块(名册生日)**不过这个筛**:生日本来就已经是"与我相关"的结果集。
|
||||
*
|
||||
* @return array{custom:array,derived:array,hidden:int,hiddenIds:array}
|
||||
*/
|
||||
public static function split(int $familyId, int $viewerId): array
|
||||
{
|
||||
@@ -322,6 +351,10 @@ final class Anniversary
|
||||
// 一次把所有人的关联人查出来(避免逐行查库)——v24 第 25 批 用户第 2 条
|
||||
$pmap = self::peopleMap(array_column($rows, 'id'));
|
||||
$custom = [];
|
||||
$hidden = 0;
|
||||
/* 被筛掉的 id:页面上方那块「接下来的日子」也要跟着筛,否则出现
|
||||
"上面列出来一条、下面两张表都找不到它"的自相矛盾(v62 实测踩到)。 */
|
||||
$hiddenIds = [];
|
||||
foreach ($rows as $r) {
|
||||
$r['source'] = 'custom';
|
||||
$r['person_name'] = '';
|
||||
@@ -334,6 +367,13 @@ final class Anniversary
|
||||
} else {
|
||||
$r['person_ids'] = [];
|
||||
}
|
||||
// 见上面 ①②:只有"明确挂了别人、且没挂我"的才排除
|
||||
if ($r['person_ids'] && $viewerId > 0
|
||||
&& !in_array((int)$viewerId, array_map('intval', $r['person_ids']), true)) {
|
||||
$hidden++;
|
||||
$hiddenIds[] = $aid;
|
||||
continue;
|
||||
}
|
||||
$custom[] = $r;
|
||||
}
|
||||
$derived = self::birthdays($familyId, $viewerId);
|
||||
@@ -350,54 +390,166 @@ final class Anniversary
|
||||
};
|
||||
usort($custom, $byMD);
|
||||
usort($derived, $byMD);
|
||||
return ['custom' => $custom, 'derived' => $derived];
|
||||
return ['custom' => $custom, 'derived' => $derived, 'hidden' => $hidden,
|
||||
'hiddenIds' => $hiddenIds];
|
||||
}
|
||||
|
||||
// ---------------- 纪念日分类(v24 用户 2026-10-03 第 9 条)----------------
|
||||
|
||||
/**
|
||||
* 用户自定义的分类清单(family 级,存 settings)。
|
||||
* 用户自定义的分类清单(family 级,存 settings)—— **带图标**(v62)。
|
||||
*
|
||||
* 用户原话:「在新建纪念日和右侧,也增加设置按钮,可以设置纪念日的分类。」
|
||||
* v62 追加:「每一类纪念日都有不同的图标……如果用户添加了自己的分类,就让用户自己选。」
|
||||
*
|
||||
* ⚠️ 与分类**落到每条纪念日上的 kind 字段**是两件事:
|
||||
* · 这里的清单 = 新建时下拉里给哪些选项(可增删);
|
||||
* · 这里的清单 = 新建时下拉里给哪些选项(可增删、可换图标);
|
||||
* · kind = 这一条实际选了哪个。
|
||||
* 删掉一个分类**不**会把已用的纪念日改掉(那些 kind 仍然是那个字),
|
||||
* 只是新建时不再出现在下拉里 —— 强改历史数据比留着一个"没人认识的分类"更糟。
|
||||
*
|
||||
* @return string[] 有序、去重、非空
|
||||
* ⚠️ 存储形状变过一次(**必须兼容老数据**):
|
||||
* v24 起:`["生日","结婚纪念日"]` —— 纯字符串数组
|
||||
* v62 起:`[{"n":"生日","i":"cake"}, ...]` —— 名称 + 图标
|
||||
* 读到字符串就按"没配图标"处理(用 PRESET_ICONS / 名字猜一个),
|
||||
* 读到对象就取 n/i。**不改老库、不做一次性迁移**:读的时候兼容,写的时候
|
||||
* 自然升级 —— 迁移脚本在"用户还没打开过设置页"时没有任何收益。
|
||||
*
|
||||
* @return array<int,array{name:string,icon:string}> 有序、去重、非空
|
||||
*/
|
||||
public static function kinds(int $familyId): array
|
||||
public static function kindsFull(int $familyId): array
|
||||
{
|
||||
$raw = (string)Setting::get($familyId, 'anniv_kinds', '');
|
||||
$dec = $raw !== '' ? json_decode($raw, true) : null;
|
||||
if (!is_array($dec)) {
|
||||
return self::PRESET_KINDS; // 没配过 → 用预置那套
|
||||
}
|
||||
$out = [];
|
||||
foreach ($dec as $k) {
|
||||
$k = trim((string)$k);
|
||||
if ($k !== '' && mb_strlen($k, 'UTF-8') <= 20 && !in_array($k, $out, true)) {
|
||||
$out[] = $k;
|
||||
// 没配过 → 用预置那套(每个都配好图标)
|
||||
$out = [];
|
||||
foreach (self::PRESET_KINDS as $k) {
|
||||
$out[] = ['name' => $k, 'icon' => self::defaultIcon($k)];
|
||||
}
|
||||
return $out;
|
||||
}
|
||||
$out = [];
|
||||
$seen = [];
|
||||
foreach ($dec as $item) {
|
||||
if (is_array($item)) {
|
||||
$n = trim((string)($item['n'] ?? $item['name'] ?? ''));
|
||||
$i = (string)($item['i'] ?? $item['icon'] ?? '');
|
||||
} else {
|
||||
$n = trim((string)$item);
|
||||
$i = '';
|
||||
}
|
||||
if ($n === '' || mb_strlen($n, 'UTF-8') > 20 || isset($seen[$n])) {
|
||||
continue;
|
||||
}
|
||||
$seen[$n] = true;
|
||||
$out[] = ['name' => $n, 'icon' => $i !== '' ? AnnivIcon::normalize($i) : self::defaultIcon($n)];
|
||||
}
|
||||
// ⚠️ 空数组是**合法**的(用户可能就是不想有分类),所以不在这里兜回预置;
|
||||
// 只有"从来没配过"($raw === '')才给预置。
|
||||
return $out;
|
||||
}
|
||||
|
||||
public static function saveKinds(int $familyId, array $kinds): void
|
||||
/** 只要名字(老的调用点、下拉、`in_array` 判断都还用它) */
|
||||
public static function kinds(int $familyId): array
|
||||
{
|
||||
return array_column(self::kindsFull($familyId), 'name');
|
||||
}
|
||||
|
||||
/** 名字 → 图标(设置页之外的一切渲染都问这里) */
|
||||
public static function kindIcons(int $familyId): array
|
||||
{
|
||||
$out = [];
|
||||
foreach (self::kindsFull($familyId) as $r) {
|
||||
$out[$r['name']] = $r['icon'];
|
||||
}
|
||||
return $out;
|
||||
}
|
||||
|
||||
/**
|
||||
* 某个分类该用哪个图标。
|
||||
*
|
||||
* ⚠️ 三条回落,顺序不能反:
|
||||
* ① 清单里配过的 → 用它(用户自己选的);
|
||||
* ② 清单里没有(分类被删了、或者是老数据里的 kind)→ 按名字猜(PRESET_ICONS);
|
||||
* ③ 还猜不出 → dot。
|
||||
* 这样"删掉一个分类"之后,那些老纪念日**仍然有图标**,不会变成一片空白。
|
||||
*/
|
||||
public static function iconOf(int $familyId, string $kind): string
|
||||
{
|
||||
$kind = trim($kind);
|
||||
if ($kind === '') {
|
||||
return AnnivIcon::FALLBACK;
|
||||
}
|
||||
static $cache = [];
|
||||
if (!isset($cache[$familyId])) {
|
||||
$cache[$familyId] = self::kindIcons($familyId);
|
||||
}
|
||||
if (isset($cache[$familyId][$kind])) {
|
||||
return $cache[$familyId][$kind];
|
||||
}
|
||||
return self::defaultIcon($kind);
|
||||
}
|
||||
|
||||
/** 按名字猜一个图标(预置表 → 关键词 → dot) */
|
||||
public static function defaultIcon(string $kind): string
|
||||
{
|
||||
$kind = trim($kind);
|
||||
if (isset(self::PRESET_ICONS[$kind])) {
|
||||
return self::PRESET_ICONS[$kind];
|
||||
}
|
||||
// 关键词兜底:用户自己起的名字里带"生日""结婚"……也该有个像样的图标
|
||||
$guess = [
|
||||
'生日' => 'cake', '寿' => 'cake',
|
||||
'结婚' => 'ring', '婚' => 'ring',
|
||||
'相识' => 'heart', '恋爱' => 'heart', '爱' => 'heart',
|
||||
'忌' => 'candle', '追思' => 'candle', '逝' => 'candle',
|
||||
'节' => 'star', '庆祝' => 'star',
|
||||
'满月' => 'baby', '出生' => 'baby', '孩子' => 'baby',
|
||||
'搬家' => 'home', '乔迁' => 'home', '家' => 'home',
|
||||
'毕业' => 'book', '升学' => 'book', '入学' => 'book',
|
||||
'旅' => 'plane', '行' => 'plane',
|
||||
'合影' => 'camera', '照片' => 'camera',
|
||||
'音乐' => 'music', '演出' => 'music',
|
||||
'奖' => 'trophy', '晋升' => 'trophy', '成就' => 'trophy',
|
||||
'入职' => 'clock', '周年' => 'clock',
|
||||
'花' => 'flower', '祝福' => 'flower',
|
||||
'礼物' => 'gift',
|
||||
];
|
||||
foreach ($guess as $needle => $icon) {
|
||||
if (mb_strpos($kind, $needle, 0, 'UTF-8') !== false) {
|
||||
return $icon;
|
||||
}
|
||||
}
|
||||
return AnnivIcon::FALLBACK;
|
||||
}
|
||||
|
||||
/**
|
||||
* 保存分类清单。
|
||||
*
|
||||
* @param array $kinds 元素可以是 string(老调用点:只要名字)或
|
||||
* array{name,icon}(新:带图标)
|
||||
*/
|
||||
public static function saveKinds(int $familyId, array $kinds): void
|
||||
{
|
||||
$out = [];
|
||||
$seen = [];
|
||||
foreach ($kinds as $k) {
|
||||
$k = trim((string)$k);
|
||||
if ($k === '' || mb_strlen($k, 'UTF-8') > 20) {
|
||||
if (is_array($k)) {
|
||||
$name = trim((string)($k['name'] ?? $k['n'] ?? ''));
|
||||
$icon = (string)($k['icon'] ?? $k['i'] ?? '');
|
||||
} else {
|
||||
$name = trim((string)$k);
|
||||
$icon = '';
|
||||
}
|
||||
if ($name === '' || mb_strlen($name, 'UTF-8') > 20 || isset($seen[$name])) {
|
||||
continue;
|
||||
}
|
||||
if (!in_array($k, $out, true)) {
|
||||
$out[] = $k;
|
||||
}
|
||||
$seen[$name] = true;
|
||||
$out[] = [
|
||||
'n' => $name,
|
||||
'i' => $icon !== '' ? AnnivIcon::normalize($icon) : self::defaultIcon($name),
|
||||
];
|
||||
if (count($out) >= 30) { // 上限:一个下拉放不下更多了
|
||||
break;
|
||||
}
|
||||
@@ -488,6 +640,9 @@ final class Anniversary
|
||||
'date' => $date,
|
||||
'title' => (string)($a['title'] ?? ''),
|
||||
'kind' => (string)($a['kind'] ?? ''),
|
||||
/* v62:分类图标(key)—— 日历拿它拼 emoji 前缀,卡片拿它画 SVG。
|
||||
在这里一次性算好,别让每个渲染点各自去查一遍配置。 */
|
||||
'icon' => self::iconOf($familyId, (string)($a['kind'] ?? '')),
|
||||
'person_id' => (int)($a['person_id'] ?? 0),
|
||||
'source' => (string)($a['source'] ?? 'custom'),
|
||||
'note' => (string)($a['note'] ?? ''),
|
||||
|
||||
Reference in New Issue
Block a user