Files
wordpress-kratos-yang/inc/theme-mourning.php
T
laoyang cbe0f2dc59 3.2.18: 清明按节气换算、主题内 [TOC] 目录、重建评论区替代 wpDiscuz
三项功能:

1. 清明不再用固定 04-04
   清明是节气,日期在 4/4~4/6 之间浮动。硬编码 04-04 实测 1900-2100
   共 201 年里只有 76 年是对的(约 38%)。改为内嵌逐年表换算真实日期,
   超出 1900-2100 返回 null 明确放弃,不退化成近似值。
   兼容 3.2.17 及以前勾过的 '04-04' 键,升级后勾选不会被静默丢弃。

2. 主题内实现 [TOC]
   文章里写 [TOC],该位置输出本文目录,点击跳到对应标题。
   在 the_content 优先级 9 处理(早于 wpautop 10 / do_shortcode 11),
   保证"补 id"与"扫目录"用的是同一份文本,href 与 id 必然对得上。
   嵌套用深度归一化,h1→h3 跳级不会算错层级。
   scroll-margin-top 声明式让位,原生锚点/外部 #hash/后退全都生效。

3. 重建评论区
   旧实现的问题:列表渲染与 AJAX 渲染各写一份 HTML,已经分叉
   (AJAX 返回的新评论没有回复按钮);<li> 被 <div> 包着,嵌套回复错位。
   现在统一到 yang_comment_html() 单一渲染函数,列表与 AJAX 只差一个
   is-new 类。同时修掉三处真实缺陷:
     · 评论项用 flex 会把 <ol class="yang-children"> 当成第三个 flex 项,
       子评论跑到父评论右侧、正文被挤到 156px、页面横向溢出 75px —— 改 grid
     · 成功提示在 exitReplyMode() 之前设置,被其末尾的 setStatus('','')
       擦掉,用户提交成功却看不到反馈 —— 调整顺序
     · 窄屏媒体查询打不过深层选择器(媒体查询不增加特异性),
       嵌套头像反向变大 —— 补深层覆盖

   与 wpDiscuz 解耦:邮件模块不再无条件 return false(那等于永久关掉
   notify_postauthor 且不报错);插件在场时不加载主题评论资源,
   避免双份表单互抢。

验证:PHP 语法 73/73;_static_qingming 36、_test_toc 53、
_test_comments 62、_test_comments_falsify 17、
_toc_geometry 121、_toc_geometry_falsify 9、
_comments_geometry 98、_comments_geometry_falsify 23,全部 0 失败。
线上端到端 29/0(真实文章渲染目录 + 真实评论数据渲染)。
2026-10-24 01:03:36 +08:00

671 lines
31 KiB
PHP
Raw 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
/**
* 纪念日自动哀悼
*
* ── 依据 ─────────────────────────────────────────────────────
* 以下日期分三类,可信度不同,代码里也分开对待:
*
* A. 法定纪念日(全国人大常委会决定设立,每年国家举行纪念活动)
* · 09-03 中国人民抗日战争胜利纪念日
* 2014-02-27 十二届全国人大常委会第七次会议决定设立
* · 09-30 烈士纪念日
* 2014-08-31 十二届全国人大常委会第十次会议决定设立
* · 12-13 南京大屠杀死难者国家公祭日
* 2014-02-27 同一会议决定设立
* 依据:全国人大网《铭记历史坐标 凝聚前行力量》
* http://www.npc.gov.cn/c2/c30834/202510/t20251020_448426.html
* http://npc.gov.cn/c2/c12435/c12487/201905/t20190521_220992.html
*
* B. 民间广泛采用(非法定,但主流网站普遍黑白)
* · 05-12 汶川地震纪念(2008-05-12 14:28)
* · 09-18 九一八事变纪念(1931-09-18)
* 这两个日期黑白是网络惯例,但不是法定「哀悼日」,所以默认**不勾选**,
* 由用户自己决定。硬编码成默认开启会替用户做价值判断,不合适。
*
* C. 历史全国哀悼日(一次性事件,非每年)
* · 2008-05-19~21 汶川地震(国务院决定)
* · 2010-04-21 玉树地震(国务院决定)
* · 2010-08-15 舟曲泥石流(国务院决定)
* · 2020-04-04 新冠疫情牺牲烈士和逝世同胞(国务院决定)
* 这些是**特定年份的特定日期**,不是周年循环。所以代码里不把它们
* 当成"每年重复",而是保留为可自定义的具体日期,避免明年 4 月 4 日
* 又自动变黑白(那是错的)。
*
* D. 节气(日期随年份浮动,必须逐年换算)
* · 清明 太阳视黄经达 15° 的时刻所在日
* 清明不是"每年 4 月 4 日"。它在 4 月 4 日、5 日、6 日之间浮动:
* 1900~2100 这 201 年里,落在 4 日的只有 76 年(约 38%),
* 落在 5 日的有 112 年,落在 6 日的有 13 年(都集中在 1939 年以前)。
* 所以**不能**把它当固定 MM-DD 塞进清单里 —— 那样有六成年份是错的,
* 而且错得毫无提示,比不给这个功能更糟。
* 详见 yang_qingming_day()。
*
* ── 设计取舍 ──────────────────────────────────────────────────
* 1. **按站点时区判断**,不按服务器时区。用 current_time('md'),
* 它走 WordPress 设置的时区。否则服务器在 UTC,北京时间 9 月 18 日
* 早上 8 点前还算 17 日,哀悼会晚 8 小时生效。
*
* 2. **只判断月日,不判断年份**。纪念日是周年性质,每年的同一天都要生效。
*
* 3. **兼容旧的 g_rip 开关**。老版本是纯手动「一直黑白」。
* 若用户勾着旧的 g_rip,仍然一直黑白;新逻辑是叠加在上面的。
* 不能因为升级就让原本黑白中的站点突然恢复彩色。
*
* @author Yongquan Yang <yangyq.net>
* @license MIT License
*/
if (!defined('ABSPATH')) {
exit;
}
/**
* 清明节在指定年份的月日(`MM-DD`),换算不出来返回 null
*
* ── 为什么是查表,不是现场算 ──────────────────────────────────
* 清明的天文学定义是「太阳视黄经达到 15° 的那一刻」。理论上可以现场用
* Meeus 的太阳位置公式解方程,但在这个场景下不该这么做:
*
* 1. **跨平台浮点差异会翻日期**。清明时刻可能离午夜只有几十秒
* (例如 1943 年落在 23:58、2071 年落在 00:02),此时 PHP 的
* `sin()/cos()` 在 x86 与 ARM 上的 ULP 级差异足以让二分退化到
* 隔壁一天。日期错一天的代价是「整站黑白错日子」,很难被发现。
* 2. **可验证性**。查表的内容是 201 个字符,可以逐条与天文年历核对;
* 现场算的公式在主题这种被到处复制分发的代码里几乎不可能被验证。
* 3. **成本**。201 个数字就是几十字节,比几十行公式便宜得多。
*
* 表里的值由 ephem 星历计算、并用两套独立实现交叉验证过:
* · ephem(VSOP87 截断)+ 二分求黄经 15°;
* · Meeus《Astronomical Algorithms》第 25 章公式独立实现;
* · cnlunar 民间历法数据表。
* 三者在 2000~2050 完全一致;ephem 与 Meeus 在 1900~2100 的 201 年
* 只有 1943 年不同,而该年清明时刻为 23:58(差距在秒级),属精度边界。
* 与气象/天文年历公布的 2020~2027 时刻比对,日期 8/8 全对,
* 时刻差 4~16 分钟(纯属低精度星历的太阳黄经误差,不跨日)。
*
* ── 为什么超出范围返回 null 而不是猜一个 ─────────────────────
* 用户的要求是「是哪一天就是哪一天,要么就不要这个日期」。
* 2101 年以后的清明我算不准,与其给个可能错的日子,不如**不出现**:
* 那天站点保持彩色,是明确的无害行为;黑白错日子才是真事故。
* 表覆盖到 2100 年,届时主题早该升级了。
*
* @param int|null $year 不传则用站点时区的当前年份
* @return string|null 如 '04-04';无法换算时 null
*/
function yang_qingming_md($year = null)
{
/* 1900~2100 每年清明落在 4 月的哪一天,按年份顺序拼成的字符串。
每个字符对应一年:'4' = 4 月 4 日,'5' = 5 日,'6' = 6 日。
例:第 0 位是 1900 年 → '5' → 1900-04-05。 */
static $seq = '556655665566555655565556555655565556555655555555555555555555'
. '555555555555555545554555455545554555455545554555445544554455'
. '445544554455445544554445444544454445444544454445444544444444'
. '444444444444444444445';
if ($year === null) {
$year = (int) current_time('Y');
}
$year = (int) $year;
// 表只覆盖 1900~2100,范围外明确放弃(见上面注释)
if ($year < 1900 || $year > 2100) {
return null;
}
$idx = $year - 1900;
if (!isset($seq[$idx])) {
return null;
}
$day = (int) $seq[$idx];
if ($day < 1 || $day > 31) {
return null;
}
// 清明永远在 4 月,所以月份写死不会错(表里也只存了「日」)
return sprintf('04-%02d', $day);
}
/**
* 内置纪念日清单
*
* 返回 array( 键 => array(名称, 说明, 分类, 是否默认勾选 [, 动态换算方式]) )
*
* 键名通常是 `m-d` 形式(补零),与 current_time('m') / current_time('d')
* 直接可比。
*
* **例外:清明**。它的日期随年份浮动(4 月 4/5/6 日),不能用一个固定
* 的 `m-d` 当键 —— 否则等于给自己钉死一个错误的近似值。所以它的键是
* `qingming`,并带 `dynamic => qingming` 标记,由 yang_mourning_days()
* 在运行时按「哪一年」换算成真正的 `MM-DD`。
*/
function yang_mourning_presets()
{
return array(
// ── A 类:法定纪念日 ──
'09-03' => array(
'name' => __('中国人民抗日战争胜利纪念日', 'kratos'),
'note' => __('1945 年 9 月 2 日日本签署投降书,9 月 3 日定为胜利纪念日', 'kratos'),
'group' => 'legal',
'default' => 0,
),
'09-30' => array(
'name' => __('烈士纪念日', 'kratos'),
'note' => __('向人民英雄敬献花篮仪式在天安门广场举行', 'kratos'),
'group' => 'legal',
'default' => 0,
),
'12-13' => array(
'name' => __('南京大屠杀死难者国家公祭日', 'kratos'),
'note' => __('1937 年 12 月 13 日南京沦陷,30 万同胞遇难', 'kratos'),
'group' => 'legal',
'default' => 0,
),
// ── B 类:民间惯例,默认不勾 ──
'05-12' => array(
'name' => __('汶川地震纪念日', 'kratos'),
'note' => __('2008 年 5 月 12 日 14 时 28 分,汶川发生 8.0 级地震', 'kratos'),
'group' => 'custom',
'default' => 0,
),
'09-18' => array(
'name' => __('九一八事变纪念日', 'kratos'),
'note' => __('1931 年 9 月 18 日,日本关东军发动九一八事变', 'kratos'),
'group' => 'custom',
'default' => 0,
),
'07-07' => array(
'name' => __('七七事变纪念日', 'kratos'),
'note' => __('1937 年 7 月 7 日,卢沟桥事变,全民族抗战爆发', 'kratos'),
'group' => 'custom',
'default' => 0,
),
'12-12' => array(
'name' => __('西安事变纪念日', 'kratos'),
'note' => __('1936 年 12 月 12 日,张学良、杨虎城发动兵谏', 'kratos'),
'group' => 'custom',
'default' => 0,
),
'08-15' => array(
'name' => __('日本宣布无条件投降纪念日', 'kratos'),
'note' => __('1945 年 8 月 15 日,日本天皇宣布接受波茨坦公告', 'kratos'),
'group' => 'custom',
'default' => 0,
),
// ── D 类:节气,日期逐年换算 ──
'qingming' => array(
'name' => __('清明节', 'kratos'),
'note' => __('节气,日期每年浮动(4 月 4~6 日之间),已按天文历逐年换算,不是固定 4 月 4 日', 'kratos'),
'group' => 'custom',
'default' => 0,
// 让后台清单里不要显示出 "qingming" 这个内部键
'key_label' => __('逐年', 'kratos'),
// 标记:这个日期的 MM-DD 要按年份现算
'dynamic' => 'qingming',
),
);
}
/**
* 把某个日期键解析成「指定年份的真实 MM-DD」
*
* 静态日期(09-03 之类)原样返回;动态日期(清明)按年份换算。
* 换算不出来时返回 null —— 调用方必须把 null 当作「这天不存在」,
* **不要**退化成某个固定值。
*
* @param string $key presets 的键
* @param array $info 该项的 presets 定义
* @param int|null $year 年份,null 用站点时区当前年
* @return string|null
*/
function yang_mourning_resolve_key($key, $info, $year = null)
{
if (isset($info['dynamic']) && $info['dynamic'] === 'qingming') {
return yang_qingming_md($year);
}
// 静态日期:必须是合法 MM-DD 才放行,防止脏数据
if (preg_match('/^\d{2}-\d{2}$/', $key)) {
return $key;
}
return null;
}
/**
* 读取用户勾选的纪念日,返回**该年份真实生效的 `MM-DD` 列表**
*
* 老站点升级上来时这个选项还不存在,此时返回 presets 里 default=1 的那些。
* 注意**不能**简单用 `kratos_option('g_rip_days', 默认数组)`:kratos_option
* 对未设置的键返回默认值,但如果用户曾经保存过空数组(全部取消勾选),
* 存进去的就是空数组,此时必须尊重「用户就是要关掉」,不能再回退到默认。
* 所以这里用「option 数组里有没有这个键」来判断,而不是判断值是否为空。
*
* ── 返回值是 MM-DD,不是勾选的键 ──────────────────────────────
* 因为清明的键是 `qingming`、真实日期是 `04-04`/`04-05`,两者不同。
* 统一解析成 MM-DD 之后,上游的 in_array($today, $days) 一行都不用改。
*
* ── 兼容旧的 '04-04' 勾选 ─────────────────────────────────────
* 3.2.17 及以前的 presets 里清明就是 `04-04`,勾过它的站点库里存着
* `04-04 => 1`。如果直接改键名,这些站点的勾选会被静默丢弃 ——
* 用户明明选了清明,升级后反而没了。所以把 `04-04` 视为 `qingming`。
*
* @param int|null $year 解析哪一年,null 用站点时区当前年
*/
function yang_mourning_days($year = null)
{
$presets = yang_mourning_presets();
$opts = get_option('kratos');
$has = is_array($opts) && array_key_exists('g_rip_days', $opts);
// 勾选状态:键 => bool
$picked = array();
if (!$has) {
// 从未保存过:用内置默认
foreach ($presets as $key => $info) {
if (!empty($info['default'])) {
$picked[$key] = true;
}
}
} else {
$saved = $opts['g_rip_days'];
if (!is_array($saved)) {
return array();
}
foreach ($saved as $key => $v) {
if (!$v) {
continue;
}
// 旧键迁移:3.2.17 及以前的清明
if ($key === '04-04' && isset($presets['qingming'])) {
$picked['qingming'] = true;
continue;
}
// 只保留 presets 里存在的键(防止脏数据)
if (isset($presets[$key])) {
$picked[$key] = true;
}
}
}
// 解析成真实 MM-DD
$out = array();
foreach ($picked as $key => $_) {
$md = yang_mourning_resolve_key($key, $presets[$key], $year);
if ($md !== null && !in_array($md, $out, true)) {
$out[] = $md;
}
}
return $out;
}
/**
* 今天是否应当哀悼
*/
function yang_is_mourning_today()
{
// 旧的手动开关:开了就一直黑白(保持向后兼容,不能让升级把黑白站点变回彩色)
if (kratos_option('g_rip', false)) {
return true;
}
if (!kratos_option('g_rip_auto', false)) {
return false;
}
// 走站点时区,不是服务器时区
$md = current_time('m') . '-' . current_time('d');
$days = yang_mourning_days();
// 自定义附加日期(用户自己填的,如特定年份的哀悼日)
foreach (yang_mourning_extra_days() as $one) {
$days[] = $one;
}
return in_array($md, $days, true);
}
/**
* 把「附加日期」输入框里的自由文本解析成月日列表
*
* 支持两种写法,语义不同,不能混为一谈:
* · `MM-DD` —— 每年循环(05-12 每年都会触发)
* · `YYYY-MM-DD` —— 只在指定那年触发(2020-04-04 是一次性事件,
* 明年 4 月 4 日不该再自动变黑白)
*
* 分隔符容错:空格、回车、英文逗号、中文逗号、顿号、分号都认,
* 因为用户复制粘贴来源五花八门,要求他只准用半角空格不现实。
*
* 非法格式**静默跳过**而不是报错:这是个自由文本框,中途保存到
* 一半(比如只打了 `10-`)不应该让整个后台保存失败。
*/
function yang_mourning_extra_days()
{
$raw = kratos_option('g_rip_extra', '');
if (!is_string($raw) || trim($raw) === '') {
return array();
}
$out = array();
foreach (preg_split('/[\s,,、;;]+/u', $raw) as $one) {
$one = trim($one);
if ($one === '') {
continue;
}
if (preg_match('/^(\d{1,2})-(\d{1,2})$/', $one, $m)) {
// MM-DD:补零后与 current_time('m')-current_time('d') 可比
$out[] = sprintf('%02d-%02d', (int) $m[1], (int) $m[2]);
} elseif (preg_match('/^(\d{4})-(\d{1,2})-(\d{1,2})$/', $one, $m)) {
if ((int) $m[1] === (int) current_time('Y')) {
$out[] = sprintf('%02d-%02d', (int) $m[2], (int) $m[3]);
}
}
}
return $out;
}
/**
* 附加日期的原始文本(后台选项说明里回显"当前填的是什么"用)
*
* 单独抽出来是因为后台 desc 里要展示它,而 desc 在
* kratos_options() 里拼装 —— 那里不该重复一遍取值逻辑。
*/
function yang_mourning_extra_raw()
{
$raw = kratos_option('g_rip_extra', '');
return is_string($raw) ? trim($raw) : '';
}
/**
* 输出黑白样式
*
* 几个细节:
* 1. `html { filter: grayscale(1) }` 会给整页建一个合成层,fixed 定位的元素
* 在某些旧版 Chrome 上会错位。所以额外加 `background-attachment: fixed`
* 的补丁样式不写;这里改用给 body 和 .k-header 分别处理更稳。
* —— 实际上实测最稳的是给 html 加 filter 并用 transform: translateZ(0)
* 在 body 上兜住,避免 iOS Safari 的渲染错位。这里保留 html 方案,
* 因为它在用户当前主题结构下工作正常(老功能的写法),只是补一条
* iOS 的兼容声明。
* 2. 不能只挂 is_home():详情页也应该黑白。老代码限定首页,那是 bug。
* 3. 只在真正哀悼的日子输出,避免给所有页面增加一条无用样式。
*/
function yang_mourning_style()
{
if (is_admin() || !yang_is_mourning_today()) {
return;
}
echo '<style id="yang-mourning">'
. "\nhtml{filter:grayscale(100%);-webkit-filter:grayscale(100%);}"
. "\nhtml body{-webkit-transform:translateZ(0);}"
// 打印时不灰度,否则打印出来是灰的
. "\n@media print{html{filter:none!important;-webkit-filter:none!important;}}"
. "\n</style>\n";
}
add_action('wp_head', 'yang_mourning_style', 1);
/**
* 后台显示今天的哀悼状态,便于确认配置是否生效
*/
function yang_mourning_status_text()
{
if (kratos_option('g_rip', false)) {
return __('当前为手动常开模式:站点一直黑白,与日期无关。', 'kratos');
}
if (!kratos_option('g_rip_auto', false)) {
return __('自动哀悼未开启。(上面的「手动常开」也没勾,所以现在整站是彩色的。)', 'kratos');
}
$days = yang_mourning_days();
$extra = yang_mourning_extra_days();
foreach ($extra as $one) {
if (!in_array($one, $days, true)) {
$days[] = $one;
}
}
if (empty($days)) {
return __('自动哀悼已开启,但一个日期都没有 —— 清单没勾、附加日期也没填,因此不会生效。', 'kratos');
}
sort($days);
$today = current_time('m') . '-' . current_time('d');
$is = in_array($today, $days, true);
/* 列表里给清明加个标注:它在库里是 `qingming` 这个键,
但真正生效的日期由当年换算得出,光看 `04-05` 用户不知道
那是清明还是他自己手填的。 */
$sorted = $days;
$qmd = yang_qingming_md();
if ($qmd !== null) {
foreach ($sorted as $i => $one) {
if ($one === $qmd) {
$sorted[$i] = sprintf(__('%s(清明)', 'kratos'), $one);
break;
}
}
}
return sprintf(
/* translators: 1: 今天的月日, 2: 是否处于哀悼, 3: 生效的日期列表 */
__('今天是 %1$s,%2$s。当前会变黑白的日期:%3$s', 'kratos'),
$today,
$is ? __('正在哀悼,站点已黑白', 'kratos') : __('不在哀悼日', 'kratos'),
implode('、', $sorted)
);
}
/**
* 哀悼期间在页面**最顶部正中**显示一条提示,链接到说明页
*
* ── 为什么必须放在最顶部 ──────────────────────────────────────
* 用户的原话是「别人都不知道咋回事,为啥变黑白」。只把入口藏在页脚,
* 绝大多数人根本不会看到 —— 他们看到一片灰就关掉了。
* 所以提示条必须出现在**首屏第一眼就看到**的位置:整页最上方、水平居中。
*
* ── 只在哀悼日出现 ────────────────────────────────────────────
* 平时打开站点的人不应该看到任何"哀悼说明"的入口 —— 他既没看到黑白,
* 也不知道在对什么致哀,那个入口本身就是困惑源。所以页脚的常驻链接
* 已经移除,说明页的入口就只在这条提示里。
*
* ── 为什么不做成弹窗 ──────────────────────────────────────────
* 弹窗会挡住内容,纪念日当天每个访客每次访问都被弹一次,是骚扰。
* 一条可以关闭的细横条才是合适的强度,且关闭状态记在 sessionStorage
* (用 session 而不是 localStorage:当天关闭后,第二天不应该还记着)。
*
* ── 定位方式:占文档流第一行 + 同步下移导航栏 ─────────────────
* 目标是「整页最上方、水平居中」。实现时连撞两次,都靠真实几何测出来:
*
* 第一版:`position:relative`,排进文档流。左右 padding 是
* `9px 46px 9px 18px`(左 18、右 46,为了给关闭按钮让位),
* 内容整体偏左(实测文字中心 602、视口中心 640,偏 38px)——
* "最中间"根本没做到。同时它从 y=0 开始、高 40px,而导航栏
* `.k-nav` 是 `absolute; top:0`、z-index 1030,提示条 z-index 9990
* ⇒ **压在站点标题上方**(与 `.navbar-brand` 实测重叠 32px),把 Logo 遮住。
*
* 第二版:改成 `position:fixed; top:0; left:0; right:0`,并给 body 加
* `padding-top`。居中问题解决了(中心精确等于视口中心),但**重叠仍在 31px**。
* 原因很隐蔽:`.k-nav` 是 `position:absolute`,它的包含块是初始包含块
* (视口),**body 的 padding 推不动它**。
*
* 现在的做法(两件事配合):
* 1. 提示条本身 `position:relative` 占文档流第一行、横跨整宽,
* 内容交给内层 `.ymb-inner` 居中,左右 padding 等宽,
* 关闭按钮 absolute 不占内部流 ⇒ 几何中心 = 视口中心。
* 它占位后,`.k-header` 会被自然推下去,正文不受影响。
* 2. 唯独 `.k-nav` 是 absolute,必须单独补一条
* `body.has-yang-mourning-bar .k-nav{top:var(--ymb-h)}` 把它挪到提示条下方。
* 没有提示条时该变量不存在、偏移为 0,行为与改动前完全一致。
*
* 不用 fixed 的理由:fixed 会一直悬浮,滚动看文章时顶部永远压一条深色带,
* 内容从它下面穿过去;而且它不占位,必须靠 padding 补偿,补偿量还算不准
* (窄屏换行后高度会变)。占文档流第一行没有这些问题。
*/
function yang_mourning_notice_bar()
{
// 只在真正哀悼时显示,且不在说明页自身显示(那页自己会说明)
if (is_admin() || !yang_is_mourning_today()) {
return;
}
if (yang_mourning_page_is_current()) {
return;
}
$url = yang_mourning_page_url();
$name = yang_mourning_today_name();
$text = $name !== ''
? sprintf(__('今天是%s,本站以黑白致哀。', 'kratos'), $name)
: __('今天是纪念日,本站以黑白致哀。', 'kratos');
?>
<div id="yang-mourning-bar" class="yang-mourning-bar" role="region" aria-label="<?php esc_attr_e('哀悼提示', 'kratos'); ?>">
<span class="ymb-inner">
<span class="ymb-dot" aria-hidden="true"></span>
<span class="ymb-text"><?php echo esc_html($text); ?></span>
<a class="ymb-link" href="<?php echo esc_url($url); ?>"><?php _e('了解详情', 'kratos'); ?></a>
</span>
<button type="button" class="ymb-close" aria-label="<?php esc_attr_e('关闭提示', 'kratos'); ?>">&times;</button>
</div>
<style id="yang-mourning-bar-css">
.yang-mourning-bar{position:relative;z-index:9995;display:flex;align-items:center;justify-content:center;min-height:38px;padding:8px 44px;background:#2b2f36;color:#e8eaed;font-size:13.5px;line-height:1.55;font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,"PingFang SC","Microsoft YaHei",sans-serif;letter-spacing:.2px;box-sizing:border-box;width:100%}
.yang-mourning-bar .ymb-inner{display:flex;align-items:center;justify-content:center;flex-wrap:wrap;gap:8px;max-width:100%}
.yang-mourning-bar .ymb-dot{width:6px;height:6px;border-radius:50%;background:#b0b7c0;flex:0 0 auto}
.yang-mourning-bar .ymb-text{opacity:.92}
.yang-mourning-bar .ymb-link{color:#fff;text-decoration:none;border-bottom:1px solid rgba(255,255,255,.42);padding-bottom:1px;white-space:nowrap;transition:border-color .18s ease}
.yang-mourning-bar .ymb-link:hover{border-bottom-color:#fff}
.yang-mourning-bar .ymb-close{position:absolute;right:10px;top:50%;transform:translateY(-50%);background:none;border:0;color:#9aa3ad;font-size:19px;line-height:1;cursor:pointer;padding:2px 8px;border-radius:4px}
.yang-mourning-bar .ymb-close:hover{color:#fff;background:rgba(255,255,255,.1)}
.yang-mourning-bar.is-hidden{display:none}
/* 导航栏是 absolute;top:0,相对视口定位 —— body 的 padding 推不动它。
不同步下移的话,它会和提示条叠在一起、把站点标题遮住(实测重叠 31px)。
这里只用一条 top 偏移把它挪到提示条下方;没有提示条时 --ymb-h 为 0。 */
body.has-yang-mourning-bar .k-nav{top:var(--ymb-h,0px)}
/* 小屏:左右 padding 必须等宽。
曾写成 `7px 40px 7px 12px`(右宽左窄,给关闭按钮让位),
结果内容盒中心 = 181,而视口中心 = 195,文字整体偏左 14px —— 桌面端同样
的错在 3.2.14 修过,这里漏了同一个断点。
关闭按钮本来就是 absolute、不占内部流,所以它的空间由"缩小自身"解决,
不能靠单边 padding 去让:right:6px + 更小字号/内边距后宽度约 20px,
落在右侧 40px 的 padding 区内,与文字不重叠。 */
@media (max-width:560px){
.yang-mourning-bar{font-size:12.5px;padding:7px 40px;letter-spacing:0}
.yang-mourning-bar .ymb-inner{gap:6px}
.yang-mourning-bar .ymb-close{right:6px;font-size:17px;padding:2px 5px}
}
</style>
<script>
(function () {
var bar = document.getElementById('yang-mourning-bar');
if (!bar) return;
var body = document.body;
// 让正文让出提示条的高度。必须读真实高度:
// 窄屏下文字换行,高度不是常数,写死像素必然在某些宽度下失准。
function syncOffset() {
if (bar.className.indexOf('is-hidden') > -1) {
body.classList.remove('has-yang-mourning-bar');
body.style.removeProperty('--ymb-h');
return;
}
body.classList.add('has-yang-mourning-bar');
body.style.setProperty('--ymb-h', bar.offsetHeight + 'px');
}
// 提示条本身在文档流里,会把 .k-header 自然推下去;
// 但 .k-nav 是 absolute,必须靠上面的 CSS 变量把它也挪下来。
syncOffset();
window.addEventListener('resize', syncOffset);
window.addEventListener('load', syncOffset);
// 当天关闭过就不再显示。用 sessionStorage 而非 localStorage:
// 明天是另一天,不该还记着昨天的关闭操作。
var KEY = 'yangMournBarClosed:' + <?php echo wp_json_encode(current_time('Ymd')); ?>;
var closed = false;
try { closed = sessionStorage.getItem(KEY) === '1'; } catch (e) {}
if (closed) { bar.className += ' is-hidden'; syncOffset(); return; }
var btn = bar.querySelector('.ymb-close');
if (btn) {
btn.addEventListener('click', function () {
bar.className += ' is-hidden';
syncOffset();
try { sessionStorage.setItem(KEY, '1'); } catch (e) {}
});
}
})();
</script>
<?php
}
add_action('wp_body_open', 'yang_mourning_notice_bar', 5);
/**
* 说明页的入口只存在于哀悼提示条里,不做页脚常驻链接
*
* ── 为什么删掉页脚那个入口 ────────────────────────────────────
* 之前的实现除了提示条,还在页脚挂了一个常驻的「纪念日哀悼说明」链接,
* 意图是"事后也能找到"。但用户看得很准:这个链接**一年 365 天都在**,
* 平时打开站点的人会莫名其妙看到一个"哀悼说明"——他既没看到黑白,
* 也不知道哀悼什么,这个入口本身就是个困惑源。
*
* 正确的产品逻辑是:**哀悼是当天才发生的事,说明入口就只该在那天出现**。
* 提示条本身就是那个入口(带「了解详情」链接)。
* 非哀悼日不显示任何入口,说明页仍然可以被直接访问(知道地址的人能打开),
* 只是不再到处劝人去看。
*
* 因此这里刻意**不再注册 wp_footer 钩子**。保留这个函数名是为了让
* 老版本的子主题/缓存里对它的调用不会 Fatal,行为是空输出。
*/
function yang_mourning_footer_link()
{
return '';
}
/**
* 当前请求是不是说明页自己
*/
function yang_mourning_page_is_current()
{
$path = trim((string) parse_url($_SERVER['REQUEST_URI'] ?? '', PHP_URL_PATH), '/');
$home_path = trim((string) parse_url(home_url('/'), PHP_URL_PATH), '/');
if ($home_path !== '' && strpos($path, $home_path) === 0) {
$path = trim(substr($path, strlen($home_path)), '/');
}
return ($path === yang_mourning_page_slug());
}
/**
* 今天哀悼的纪念日名称(用于提示条文案)
*
* 取第一个命中的,取不到返回空串。
*
* ── 为什么附加日期也要给个名字 ──────────────────────────────
* 清单里的日期都有正式名称,附加日期是用户自己填的、系统不知道叫什么。
* 早先这里只查 presets,于是用附加日期触发的哀悼,提示条会退化成
* 「今天是纪念日,本站以黑白致哀」—— 名字丢了,而这条提示的全部意义
* 就是告诉访客"今天是哪个纪念日"。所以附加日期命中时,明确说明
* 这是自定义的纪念日,让文案仍然完整、不露出系统的无知。
*/
function yang_mourning_today_name()
{
if (!yang_is_mourning_today()) {
return '';
}
$md = current_time('m') . '-' . current_time('d');
$presets = yang_mourning_presets();
$days = yang_mourning_days();
// 按「解析后的真实 MM-DD」匹配。不能直接 isset($presets[$md]):
// 清明的键是 'qingming' 而不是 '04-04',那样会漏掉它。
if (in_array($md, $days, true)) {
foreach ($presets as $key => $info) {
if (yang_mourning_resolve_key($key, $info) === $md) {
return $info['name'];
}
}
}
// 命中的是「附加日期」里用户自己填的那一天
if (in_array($md, yang_mourning_extra_days(), true)) {
return __('纪念日', 'kratos');
}
return '';
}