Files
wordpress-kratos-yang/inc/theme-mourning.php
T
laoyang 96bbb3d1b5 v3.2.14 哀悼提示条改到整页最上方正中,只在纪念日当天出现
用户要求:哀悼说明「放在最上方的最中间,只在纪念日、哀悼模式开启时才显示,
其他时间不显示」。

改的过程中量出三个真问题:

1. 提示条与站点标题重叠 32px。提示条从 y=0 开始、高 40px,而导航栏
   .k-nav 是 position:absolute; top:0、z-index 1030;提示条 z-index 9990
   ⇒ 直接盖在 Logo 上。CSS 每个字都没写错,就是叠在一起。
2. 内容偏左 38px。为了给右侧关闭按钮让位写了 padding-right:46px、
   左侧只有 18px ⇒ 文字中心 602 vs 视口中心 640。"最中间"没做到。
3. 页脚有常驻的「纪念日哀悼说明」链接,一年 365 天都显示。平时打开
   站点的人既没看到黑白也不知在哀悼什么,这个入口本身就是困惑源。

修法:
- 提示条占文档流第一行、width:100%,内容交给内层 .ymb-inner 居中,
  左右 padding 等宽、关闭按钮 absolute 不占内部流 ⇒ 几何中心 = 视口中心。
- .k-nav 是绝对定位,body 的 padding 推不动它(这个原因单看代码想不到,
  是量几何量出来的)⇒ 单独补一条 body.has-yang-mourning-bar .k-nav{top:var(--ymb-h)}。
  偏移量由脚本读真实高度写入,窄屏换行后也准。
- 不用 fixed:那会滚动时顶部永远压一条深色带,内容从它下面穿过去。
- 移除页脚常驻入口与 wp_footer 注册,说明页入口只在提示条里。
  非哀悼日输出 0 字节。

验证:
- run_tests2 163 条(新增 9 条位置契约)
- _check_hidden.php 19 条(非哀悼日 0 字节、每个纪念日都能触发)
- mourning_bar_geometry.py 28 条(线上真实页面实测 4 种视口:
  顶部贴边、横跨整宽、居中误差 ≤1px、与标题和导航栏零重叠)
- falsify 44 条注入全被抓住(新增 9 条)

两个假绿当场被反面验证抓出并修掉:
- 判据照着 fixed 方案写,实现改成 relative 后 4 条断言集体假红
  (教训:设计前提变了,判据必须跟着改)。
- 页脚断言用 ob_start 取输出,但函数是 return 不是 echo ⇒ $fl 恒空
  ⇒ 注入"返回一个链接"仍全绿(教训:否定式断言在空输出上恒真)。
2026-10-23 20:43:25 +08:00

442 lines
20 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 日
* 又自动变黑白(那是错的)。
*
* ── 设计取舍 ──────────────────────────────────────────────────
* 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;
}
/**
* 内置纪念日清单
*
* 返回 array( 键 => array(月份, 日, 名称, 说明, 分类, 是否默认勾选) )
*
* 键名用 `m-d` 形式(补零),与 current_time('m') / current_time('d') 直接可比。
*/
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,
),
'04-04' => array(
'name' => __('清明节', 'kratos'),
'note' => __('节气日期每年浮动,这里固定为 4 月 4 日,仅作近似', 'kratos'),
'group' => 'custom',
'default' => 0,
),
);
}
/**
* 读取用户勾选的纪念日
*
* 老站点升级上来时这个选项还不存在,此时返回 presets 里 default=1 的那些。
* 注意**不能**简单用 `kratos_option('g_rip_days', 默认数组)`:kratos_option
* 对未设置的键返回默认值,但如果用户曾经保存过空数组(全部取消勾选),
* 存进去的就是空数组,此时必须尊重「用户就是要关掉」,不能再回退到默认。
* 所以这里用「option 数组里有没有这个键」来判断,而不是判断值是否为空。
*/
function yang_mourning_days()
{
$presets = yang_mourning_presets();
$opts = get_option('kratos');
$has = is_array($opts) && array_key_exists('g_rip_days', $opts);
if (!$has) {
// 从未保存过:用内置默认
$out = array();
foreach ($presets as $md => $info) {
if (!empty($info['default'])) {
$out[] = $md;
}
}
return $out;
}
$saved = $opts['g_rip_days'];
if (!is_array($saved)) {
return array();
}
// 只保留在 presets 里存在、且值为真的日期(防止脏数据)
$out = array();
foreach ($saved as $md => $v) {
if ($v && isset($presets[$md])) {
$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();
// 自定义附加日期(用户自己填的,如特定年份的哀悼日)
if (kratos_option('g_rip_extra', '')) {
$extra = kratos_option('g_rip_extra', '');
foreach (preg_split('/[\s,,、;;]+/u', (string) $extra) as $one) {
$one = trim($one);
if ($one === '') {
continue;
}
// 支持 MM-DD 与 YYYY-MM-DD 两种写法
if (preg_match('/^\d{2}-\d{2}$/', $one)) {
$days[] = $one;
} elseif (preg_match('/^(\d{4})-(\d{2})-(\d{2})$/', $one, $m)) {
// 带年份的只在当年生效,不是周年循环
if ((int) $m[1] === (int) current_time('Y')) {
$days[] = $m[2] . '-' . $m[3];
}
}
}
}
return in_array($md, $days, true);
}
/**
* 输出黑白样式
*
* 几个细节:
* 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();
if (empty($days)) {
return __('自动哀悼已开启,但一个日期都没勾选,因此不会生效。', 'kratos');
}
sort($days);
$today = current_time('m') . '-' . current_time('d');
$is = in_array($today, $days, true);
return sprintf(
/* translators: 1: 今天的月日, 2: 是否处于哀悼, 3: 勾选的日期列表 */
__('今天是 %1$s,%2$s。已启用:%3$s', 'kratos'),
$today,
$is ? __('正在哀悼,站点已黑白', 'kratos') : __('不在哀悼日', 'kratos'),
implode('、', $days)
);
}
/**
* 哀悼期间在页面**最顶部正中**显示一条提示,链接到说明页
*
* ── 为什么必须放在最顶部 ──────────────────────────────────────
* 用户的原话是「别人都不知道咋回事,为啥变黑白」。只把入口藏在页脚,
* 绝大多数人根本不会看到 —— 他们看到一片灰就关掉了。
* 所以提示条必须出现在**首屏第一眼就看到**的位置:整页最上方、水平居中。
*
* ── 只在哀悼日出现 ────────────────────────────────────────────
* 平时打开站点的人不应该看到任何"哀悼说明"的入口 —— 他既没看到黑白,
* 也不知道在对什么致哀,那个入口本身就是困惑源。所以页脚的常驻链接
* 已经移除,说明页的入口就只在这条提示里。
*
* ── 为什么不做成弹窗 ──────────────────────────────────────────
* 弹窗会挡住内容,纪念日当天每个访客每次访问都被弹一次,是骚扰。
* 一条可以关闭的细横条才是合适的强度,且关闭状态记在 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)}
@media (max-width:560px){
.yang-mourning-bar{font-size:12.5px;padding:7px 40px 7px 12px;letter-spacing:0}
.yang-mourning-bar .ymb-inner{gap:6px}
}
</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());
}
/**
* 今天哀悼的纪念日名称(用于提示条文案)
*
* 取第一个命中的,取不到返回空串。
*/
function yang_mourning_today_name()
{
$md = current_time('m') . '-' . current_time('d');
$presets = yang_mourning_presets();
if (isset($presets[$md]) && in_array($md, yang_mourning_days(), true)) {
return $presets[$md]['name'];
}
return '';
}