Files
laoyang 9a66068c33 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 可改尺寸、调顺序、开关
2026-10-19 12:51:17 +08:00

166 lines
8.0 KiB
PHP
Raw Permalink 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
/**
* 纪念日分类的图标(v62 用户 2026-10-18 第 2 条)。
*
* 用户原话:「设置按钮,点击后弹菜单,第一个菜单是分类设置,进去之后可以设置分类,
* 每一类纪念日都有不同的图标……如果是用的 fontawesome,那么如果用户
* 添加了自己的分类,就让用户自己选。在列表中、日历中,都要根据分类,
* 显示不同的图标。」
*
* ⇒ 为什么**不自带 FontAwesome**(两条硬理由,别改回去):
* ① 体积:整套 webfont(woff2 + css)≈ 100~300 KB,而这个项目是"点开就是一个
* 家庭站点"的自建站,首页已经背了 antd(1.8 MB)。为了十几个图标再背一整套
* 字体不划算,而且中文字形根本用不上。
* ② 离线/内网:本项目所有静态资源都是本地 vendor 目录,不能挂 CDN(记忆里
* "静态资源地址必须带版本号"那一条的同一个前提)——引 CDN 会在没外网的
* 家庭内网里直接变成空白方块。
* ⇒ 用**内置的行内 SVG 图标集**:一个图标一条 `<path>`(stroke 描边、currentColor
* 取文字颜色),列表 / 卡片里直接内联;日历那种纯文本的地方用同一图标的
* emoji 兜底(`emo()`)—— 日历的事件标题是纯文本,塞不进 SVG。
*
* ⚠️ **图标 key 是**契约**:它存在 settings(anniv_kinds)里,用户自己选。
* 所以:① 已有 key 永远不要改名(老数据会变成"认不出的图标");
* ② 新增图标只能往后加;③ 认不出的 key 一律回落 `dot`,不抛异常
* (用户可能手改过库、或跨版本导入过配置)。
*
* @see \App\Anniversary::kindsFull() 分类清单(名称 + 图标)的读写
*/
declare(strict_types=1);
namespace App;
final class AnnivIcon
{
/** 认不出/没配时用哪个 */
public const FALLBACK = 'dot';
/**
* key => [中文名, emoji, SVG 内容(24×24 画布,stroke 描边)]
*
* ⚠️ SVG 内容里**不要**写 `fill` / `stroke` —— 由 svg() 统一给
* `fill="none" stroke="currentColor"`,这样图标颜色永远跟着所在位置的文字色,
* 浅底深底都不会变成"看不见的黑块"。
*/
public const LIST = [
'cake' => ['蛋糕(生日)', '🎂',
'<path d="M4 20h16"/>'
. '<path d="M5 20v-4.6a2 2 0 0 1 2-2h10a2 2 0 0 1 2 2V20"/>'
. '<path d="M9 13.4V10M15 13.4V10"/>'
. '<path d="M9 7.4c.9-.9.2-2-.2-2.4-.5.6-1.1 1.5-.2 2.4Z"/>'
. '<path d="M15 7.4c.9-.9.2-2-.2-2.4-.5.6-1.1 1.5-.2 2.4Z"/>'],
'ring' => ['戒指(结婚)', '💍',
'<circle cx="12" cy="15" r="5.2"/>'
. '<path d="M8.8 9.4 12 5.2l3.2 4.2"/>'
. '<path d="M8.8 9.4h6.4"/>'],
'heart' => ['爱心(相识/恋爱)', '❤️',
'<path d="M12 20s-7-4.4-7-9.2A3.9 3.9 0 0 1 12 8.2a3.9 3.9 0 0 1 7 2.6C19 15.6 12 20 12 20Z"/>'],
'candle' => ['蜡烛(忌日/追思)', '🕯️',
'<path d="M9 21h6"/>'
. '<rect x="9" y="10.5" width="6" height="10.5" rx="1"/>'
. '<path d="M12 10.5V8.4"/>'
. '<path d="M12 8.4c1-1 1-2.2 0-3-1 .8-1 2 0 3Z"/>'],
'star' => ['星星(节日)', '⭐',
'<path d="M12 3.6l2.6 5.3 5.8.8-4.2 4.1 1 5.8L12 16.9l-5.2 2.7 1-5.8-4.2-4.1 5.8-.8Z"/>'],
'gift' => ['礼物', '🎁',
'<rect x="4" y="11" width="16" height="9" rx="1"/>'
. '<path d="M3 7.4h18v3.6H3zM12 7.4V20"/>'
. '<path d="M12 7.4S10.6 3 8 3a2.2 2.2 0 0 0 0 4.4Z"/>'
. '<path d="M12 7.4S13.4 3 16 3a2.2 2.2 0 0 1 0 4.4Z"/>'],
'baby' => ['孩子(满月/出生)', '👶',
'<circle cx="12" cy="7.6" r="3.4"/>'
. '<path d="M6.4 20c0-3.6 2.5-6 5.6-6s5.6 2.4 5.6 6"/>'
. '<path d="M10.5 7.2h.01M13.5 7.2h.01"/>'],
'home' => ['家(搬家/乔迁)', '🏠',
'<path d="M4 10.6 12 4l8 6.6V20H4z"/>'
. '<path d="M9.6 20v-5.2h4.8V20"/>'],
'book' => ['书(毕业/升学)', '📖',
'<path d="M12 7.2c-1.6-1.4-3.6-2-6-2v11c2.4 0 4.4.6 6 2 1.6-1.4 3.6-2 6-2v-11c-2.4 0-4.4.6-6 2Z"/>'
. '<path d="M12 7.2v11"/>'],
'plane' => ['飞机(远行/旅行)', '✈️',
'<path d="M21 4 3 11.2l6.4 2.2L11.6 20 21 4Z"/>'],
'camera' => ['相机(合影/留念)', '📷',
'<rect x="3.4" y="7.4" width="17.2" height="12" rx="2"/>'
. '<circle cx="12" cy="13.4" r="3.4"/>'
. '<path d="M8.6 7.4 10 4.8h4l1.4 2.6"/>'],
'music' => ['音符(音乐/演出)', '🎵',
'<circle cx="8" cy="17.4" r="2.6"/>'
. '<circle cx="17" cy="15.4" r="2.6"/>'
. '<path d="M10.6 17.4V7l9-2v10.4"/>'],
'trophy' => ['奖杯(成就/晋升)', '🏆',
'<path d="M8 4.6h8v4.8a4 4 0 0 1-8 0Z"/>'
. '<path d="M8 6.2H5.4v1.4a3.2 3.2 0 0 0 3.2 3.2"/>'
. '<path d="M16 6.2h2.6v1.4a3.2 3.2 0 0 1-3.2 3.2"/>'
. '<path d="M12 13.4V17M8.6 20h6.8"/>'],
'clock' => ['时钟(周年/入职)', '⏰',
'<circle cx="12" cy="12" r="7.6"/>'
. '<path d="M12 7.6V12l3 1.8"/>'],
'flower' => ['花(表白/祝福)', '🌸',
'<circle cx="12" cy="12" r="1.6"/>'
. '<circle cx="12" cy="6.6" r="2.3"/>'
. '<circle cx="16.9" cy="10.2" r="2.3"/>'
. '<circle cx="15" cy="16" r="2.3"/>'
. '<circle cx="9" cy="16" r="2.3"/>'
. '<circle cx="7.1" cy="10.2" r="2.3"/>'],
'leaf' => ['叶(自然而然)', '🌿',
'<path d="M20 4C10 4 4.4 9 4.4 15.4c0 1.8.6 3.4 1.6 4.6C9 20 20 16 20 4Z"/>'
. '<path d="M6 20C8.6 14 12 10.6 17 8"/>'],
'bell' => ['铃铛(提醒/纪念)', '🔔',
'<path d="M9 18a3 3 0 0 0 6 0"/>'
. '<path d="M6.6 18h10.8c-1.4-1.4-1.8-2.6-1.8-4.6V11a4.6 4.6 0 0 0-9.2 0v2.4c0 2-.4 3.2-1.8 4.6Z"/>'
. '<path d="M12 6.4V4.8"/>'],
'dot' => ['圆点(其它)', '⚪',
'<circle cx="12" cy="12" r="4"/>'],
];
public static function has(string $key): bool
{
return $key !== '' && isset(self::LIST[$key]);
}
/** 认不出的 key 一律回落,绝不抛异常(配置可能来自旧版本/手改) */
public static function normalize(string $key): string
{
$key = strtolower(trim($key));
return self::has($key) ? $key : self::FALLBACK;
}
/** 中文名(给设置页的下拉用) */
public static function label(string $key): string
{
return self::LIST[self::normalize($key)][0];
}
public static function emo(string $key): string
{
return self::LIST[self::normalize($key)][1];
}
/**
* 行内 SVG。
*
* ⚠️ 返回值是**可信 HTML**(不含任何用户输入:key 先 normalize,size 转 int),
* 模板里直接 `<?= ... ?>` 输出,不要 e() 一遍(那会把标签转义成文字)。
* aria-hidden:图标永远是"文字旁边的装饰",读屏器念分类名就够了。
*/
public static function svg(string $key, int $size = 15, string $cls = ''): string
{
$k = self::normalize($key);
$n = max(8, min(96, $size));
return '<svg class="ai ai-' . e($k) . ($cls !== '' ? ' ' . e($cls) : '')
. '" viewBox="0 0 24 24" width="' . $n . '" height="' . $n . '"'
. ' aria-hidden="true" focusable="false" fill="none" stroke="currentColor"'
. ' stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round">'
. self::LIST[$k][2] . '</svg>';
}
/** 设置页下拉里能选的全部图标:key => "emoji 中文名" */
public static function choices(): array
{
$out = [];
foreach (self::LIST as $k => $v) {
$out[$k] = $v[1] . ' ' . $v[0];
}
return $out;
}
}