166 lines
8.0 KiB
PHP
166 lines
8.0 KiB
PHP
<?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;
|
|||
|
|
}
|
|||
|
|
}
|