2026-10-26 13:25:51 +08:00
|
|
|
|
<?php
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 归档式文章列表([Archives] 短代码)
|
|
|
|
|
|
*
|
|
|
|
|
|
* ── 用法 ──────────────────────────────────────────────────────
|
|
|
|
|
|
* 在文章或页面正文里任意位置写一行:
|
|
|
|
|
|
*
|
|
|
|
|
|
* [Archives]
|
|
|
|
|
|
*
|
|
|
|
|
|
* 这个位置就会渲染出全站文章的**年 / 月归档列表**:
|
|
|
|
|
|
*
|
|
|
|
|
|
* · 默认显示**今年**,按月份归类(只列有文章的月份)
|
|
|
|
|
|
* · 每篇后面带 **浏览数 / 评论数 / 点赞数**
|
|
|
|
|
|
* · 列表上左/上右、下左/下右四角各有「上一年 / 下一年」按钮
|
|
|
|
|
|
* · 切年**纯 JS 重建 DOM,不刷新页面**
|
|
|
|
|
|
*
|
|
|
|
|
|
* 可调参数(都可省略):
|
|
|
|
|
|
*
|
|
|
|
|
|
* [Archives year="2025" month="1" nav="1"]
|
|
|
|
|
|
*
|
|
|
|
|
|
* · year 首次显示哪一年,默认今年;也可用 ?arc_year=2025 覆盖
|
|
|
|
|
|
* · month 1 = 显示月份分组标题,0 = 只平铺列表,默认 1
|
|
|
|
|
|
* · nav 1 = 显示四角年份导航,0 = 不显示,默认 1
|
|
|
|
|
|
*
|
|
|
|
|
|
* ── 为什么不用 wp_get_archives() ───────────────────────────────
|
|
|
|
|
|
* 三个原因,每个都足以否掉它:
|
|
|
|
|
|
* ① 它拿不到计数。要显示浏览数/评论数/点赞数,只能每篇再查一次 ——
|
|
|
|
|
|
* 71 篇就是 71 次查询(外加 2×71 次 meta 查询)。
|
|
|
|
|
|
* ② 它的输出格式固定(`<li><a>…</a></li>` 或下拉框),塞不进三项计数。
|
|
|
|
|
|
* ③ 它按「按月/按年」二选一输出**整条时间线**,没有"给我某一年"的
|
|
|
|
|
|
* 接口,JS 切年时无法复用它。
|
|
|
|
|
|
*
|
|
|
|
|
|
* 所以这里自己一次 SQL 取全量 + 两次批量 SQL 取计数,服务端组织成
|
|
|
|
|
|
* `年 → 月 → 条目` 的三层数组,**首屏直出今年**,其余年份以 JSON
|
|
|
|
|
|
* 随页面下发。JS 切年只是从这份 JSON 重建 DOM —— 不发起任何请求。
|
|
|
|
|
|
*
|
|
|
|
|
|
* ── 为什么计数要"批量"而不是逐篇 ───────────────────────────────
|
|
|
|
|
|
* 浏览数 `views` 与点赞数 `love` 都存在 postmeta 里(见 theme-article.php)。
|
|
|
|
|
|
* 逐篇 `get_post_meta()` 会先被 WP 的对象缓存接住一部分,但**没命中时
|
|
|
|
|
|
* 每次一条 SQL**。71 篇最坏 142 条。改成一条
|
|
|
|
|
|
* `WHERE meta_key IN ('views','love')` 就够,代价是自己在 PHP 里
|
|
|
|
|
|
* 按 post_id 归位 —— 这点代码远比 142 次往返便宜。
|
|
|
|
|
|
*
|
|
|
|
|
|
* ── 为什么评论数也走 SQL 而不是 get_comments_number() ──────────
|
|
|
|
|
|
* `get_comments_number()` 在没预热评论缓存的页面上同样是一条一条查
|
|
|
|
|
|
* (它内部走 `get_comments()` 的 count 分支,不带 `post_id` 时按文章查)。
|
|
|
|
|
|
* 归档页不是文章页,评论缓存根本没预热,所以这里必须自己 GROUP BY 一次。
|
|
|
|
|
|
*
|
|
|
|
|
|
* ── 排序 ──────────────────────────────────────────────────────
|
|
|
|
|
|
* 年份**倒序**(新的在前,符合翻归档的直觉),月份**升序**(1 月在
|
|
|
|
|
|
* 前,符合"这一年从头看"的直觉),月内日期**倒序**(新的在前)。
|
|
|
|
|
|
* 这三条方向不一致是**刻意**的:年份是"选哪一年",月份是"这一年
|
|
|
|
|
|
* 的进度条",月内是"最近写了什么"。
|
|
|
|
|
|
*
|
|
|
|
|
|
* @author Yongquan Yang <yangyq.net>
|
|
|
|
|
|
* @license MIT License
|
|
|
|
|
|
*/
|
|
|
|
|
|
|
|
|
|
|
|
if (!defined('ABSPATH')) {
|
|
|
|
|
|
exit;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 一次取全站归档数据
|
|
|
|
|
|
*
|
|
|
|
|
|
* 结果结构:
|
|
|
|
|
|
*
|
|
|
|
|
|
* array(
|
|
|
|
|
|
* 'years' => array(
|
|
|
|
|
|
* 2026 => array( // 年份,倒序
|
|
|
|
|
|
* 'count' => 12, // 该年文章总数
|
|
|
|
|
|
* 'months' => array(
|
|
|
|
|
|
* 10 => array( // 月份,升序
|
|
|
|
|
|
* 'count' => 3,
|
|
|
|
|
|
* 'items' => array( // 月内,日期倒序
|
|
|
|
|
|
* array('id'=>1,'title'=>'…','url'=>'…',
|
|
|
|
|
|
* 'day'=>'15','date'=>'2026-10-15',
|
|
|
|
|
|
* 'views'=>12,'comments'=>3,'love'=>5),
|
|
|
|
|
|
* ),
|
|
|
|
|
|
* ),
|
|
|
|
|
|
* ),
|
|
|
|
|
|
* ),
|
|
|
|
|
|
* ),
|
|
|
|
|
|
* 'order' => array(2026, 2025, 2024), // 有文章的年份,倒序
|
|
|
|
|
|
* 'latest' => 2026, // 最新一篇所在年
|
|
|
|
|
|
* 'total' => 71,
|
|
|
|
|
|
* )
|
|
|
|
|
|
*
|
|
|
|
|
|
* ⚠️ 全部文章**一次性**取回。71 篇的量级完全撑得住;如果哪天到了
|
|
|
|
|
|
* 几万篇,需要改成分年懒加载 —— 但那时前端也得改成 AJAX,
|
|
|
|
|
|
* 属于重新设计,不是在这里加个 limit 就能解决的(加了会算错每月
|
|
|
|
|
|
* 与每年的条目数)。
|
|
|
|
|
|
*
|
|
|
|
|
|
* @return array
|
|
|
|
|
|
*/
|
|
|
|
|
|
function yang_archives_data()
|
|
|
|
|
|
{
|
|
|
|
|
|
/* 同一次请求内只算一次:一个页面可能放了两个 [Archives]。
|
|
|
|
|
|
* 不用 transient —— 归档数据要随发文/评论即时变化,
|
|
|
|
|
|
* 加缓存就得处理 save_post / wp_insert_comment 失效,
|
|
|
|
|
|
* 那套失效逻辑一旦漏一条,用户看到的就是"我刚发的文章不在归档里"。
|
|
|
|
|
|
* 静态变量只活一次请求,不存在这个问题。 */
|
|
|
|
|
|
static $cache = null;
|
|
|
|
|
|
if ($cache !== null) {
|
|
|
|
|
|
return $cache;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
$empty = array('years' => array(), 'order' => array(), 'latest' => 0, 'total' => 0);
|
|
|
|
|
|
|
|
|
|
|
|
global $wpdb;
|
|
|
|
|
|
|
|
|
|
|
|
/* ① 文章本体。
|
|
|
|
|
|
*
|
|
|
|
|
|
* 只取 post 类型、已发布、非密码保护 —— 密码保护的正文别人看不到,
|
|
|
|
|
|
* 列进归档等于泄露标题。`post_password = ''` 就是排掉它们。
|
|
|
|
|
|
*
|
|
|
|
|
|
* 🔴 不要在这里 `ORDER BY post_date DESC` 之后就当排好了:
|
|
|
|
|
|
* 下面会重组成「年 → 月」,组内顺序必须由重组逻辑自己保证,
|
|
|
|
|
|
* 否则将来有人改这里的排序,页面显示顺序会莫名其妙跟着变。
|
|
|
|
|
|
* 这里的 ORDER BY 只是让同一月内的记录天然有序,省一次 usort。 */
|
|
|
|
|
|
$rows = $wpdb->get_results(
|
|
|
|
|
|
"SELECT ID, post_title, post_date
|
|
|
|
|
|
FROM {$wpdb->posts}
|
|
|
|
|
|
WHERE post_type = 'post'
|
|
|
|
|
|
AND post_status = 'publish'
|
|
|
|
|
|
AND post_password = ''
|
|
|
|
|
|
ORDER BY post_date DESC",
|
|
|
|
|
|
ARRAY_A
|
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
|
|
if (empty($rows)) {
|
|
|
|
|
|
$cache = $empty;
|
|
|
|
|
|
return $cache;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/* ② 浏览数 / 点赞数:一次批量取。
|
|
|
|
|
|
*
|
|
|
|
|
|
* `meta_value` 是 longtext,这里不强转 —— 转成 int 放到 PHP 侧做,
|
|
|
|
|
|
* 保持 SQL 可走 meta_key 索引。 */
|
|
|
|
|
|
$ids = array();
|
|
|
|
|
|
foreach ($rows as $r) {
|
|
|
|
|
|
$ids[] = (int) $r['ID'];
|
|
|
|
|
|
}
|
|
|
|
|
|
$ids_sql = implode(',', $ids);
|
|
|
|
|
|
|
|
|
|
|
|
$meta = array();
|
|
|
|
|
|
if ($ids_sql !== '') {
|
|
|
|
|
|
$mrows = $wpdb->get_results(
|
|
|
|
|
|
"SELECT post_id, meta_key, meta_value
|
|
|
|
|
|
FROM {$wpdb->postmeta}
|
|
|
|
|
|
WHERE post_id IN ({$ids_sql})
|
|
|
|
|
|
AND meta_key IN ('views', 'love')",
|
|
|
|
|
|
ARRAY_A
|
|
|
|
|
|
);
|
|
|
|
|
|
foreach ($mrows as $m) {
|
|
|
|
|
|
$pid = (int) $m['post_id'];
|
|
|
|
|
|
$key = $m['meta_key'];
|
|
|
|
|
|
if (!isset($meta[$pid])) {
|
|
|
|
|
|
$meta[$pid] = array('views' => 0, 'love' => 0);
|
|
|
|
|
|
}
|
|
|
|
|
|
$meta[$pid][$key] = (int) $m['meta_value'];
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/* ③ 评论数:一次 GROUP BY。
|
|
|
|
|
|
*
|
|
|
|
|
|
* 只数已批准的(`comment_approved = '1'`)—— 待审核的评论访客看不到,
|
|
|
|
|
|
* 数进去会出现"显示 5 条、点进去只有 3 条"。 */
|
|
|
|
|
|
$ccount = array();
|
|
|
|
|
|
if ($ids_sql !== '') {
|
|
|
|
|
|
$crows = $wpdb->get_results(
|
|
|
|
|
|
"SELECT comment_post_ID AS pid, COUNT(*) AS n
|
|
|
|
|
|
FROM {$wpdb->comments}
|
|
|
|
|
|
WHERE comment_approved = '1'
|
|
|
|
|
|
AND comment_post_ID IN ({$ids_sql})
|
|
|
|
|
|
GROUP BY comment_post_ID",
|
|
|
|
|
|
ARRAY_A
|
|
|
|
|
|
);
|
|
|
|
|
|
foreach ($crows as $c) {
|
|
|
|
|
|
$ccount[(int) $c['pid']] = (int) $c['n'];
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/* ④ 组织成 年 → 月 → 条目 */
|
|
|
|
|
|
$years = array();
|
|
|
|
|
|
$total = 0;
|
|
|
|
|
|
|
|
|
|
|
|
foreach ($rows as $r) {
|
|
|
|
|
|
$pid = (int) $r['ID'];
|
|
|
|
|
|
$date = $r['post_date']; // 'YYYY-MM-DD HH:MM:SS',WP 保证格式
|
|
|
|
|
|
$y = (int) substr($date, 0, 4);
|
|
|
|
|
|
$m = (int) substr($date, 5, 2);
|
|
|
|
|
|
$d = substr($date, 8, 2);
|
|
|
|
|
|
|
|
|
|
|
|
/* 年份/月份为 0 说明 post_date 是脏数据('0000-00-00')。
|
|
|
|
|
|
* 这类记录直接跳过而不是塞进 0 年 0 月 —— 后者会在导航里
|
|
|
|
|
|
* 制造一个永远点不到内容的"第 0 年"。 */
|
|
|
|
|
|
if ($y < 1 || $m < 1 || $m > 12) {
|
|
|
|
|
|
continue;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
$title = (string) $r['post_title'];
|
|
|
|
|
|
if ($title === '') {
|
|
|
|
|
|
$title = __('(无标题)', 'kratos');
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
$views = isset($meta[$pid]['views']) ? $meta[$pid]['views'] : 0;
|
|
|
|
|
|
$love = isset($meta[$pid]['love']) ? $meta[$pid]['love'] : 0;
|
|
|
|
|
|
$cmts = isset($ccount[$pid]) ? $ccount[$pid] : 0;
|
|
|
|
|
|
|
|
|
|
|
|
if (!isset($years[$y])) {
|
|
|
|
|
|
$years[$y] = array('count' => 0, 'months' => array());
|
|
|
|
|
|
}
|
|
|
|
|
|
if (!isset($years[$y]['months'][$m])) {
|
|
|
|
|
|
$years[$y]['months'][$m] = array('count' => 0, 'items' => array());
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
$years[$y]['months'][$m]['items'][] = array(
|
|
|
|
|
|
'id' => $pid,
|
|
|
|
|
|
'title' => $title,
|
|
|
|
|
|
'url' => get_permalink($pid),
|
|
|
|
|
|
'day' => $d,
|
|
|
|
|
|
'date' => substr($date, 0, 10),
|
|
|
|
|
|
'views' => $views,
|
|
|
|
|
|
'comments' => $cmts,
|
|
|
|
|
|
'love' => $love,
|
|
|
|
|
|
);
|
|
|
|
|
|
$years[$y]['months'][$m]['count']++;
|
|
|
|
|
|
$years[$y]['count']++;
|
|
|
|
|
|
$total++;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (empty($years)) {
|
|
|
|
|
|
$cache = $empty;
|
|
|
|
|
|
return $cache;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-10-26 18:42:40 +08:00
|
|
|
|
/* 年份、月份**都是倒序** —— 最近的在最上面。
|
|
|
|
|
|
*
|
|
|
|
|
|
* 3.2.48 改:月份从 ksort(1 月在前)改成 krsort(12 月在前)。
|
|
|
|
|
|
* 用户的原话是「月份和日期也倒序排列吧,就是说,最近的在最上面,
|
|
|
|
|
|
* 年份是最近的最靠前,月份和日期也应该保持一致」。
|
|
|
|
|
|
*
|
|
|
|
|
|
* 三者必须同向,否则页面会自相矛盾:线上真实数据出现过
|
|
|
|
|
|
* 「1 月 → 2 月 → 5 月 → 10 月」的月份顺序,而月内文章是
|
|
|
|
|
|
* 「01-21 → 01-20 → 01-19」,年份是「2026 → 2025」。
|
|
|
|
|
|
* 于是读者看到的整体序列是「2026 年的 1 月、2 月、5 月、10 月,
|
|
|
|
|
|
* 到 2025 年又从 1 月开始」—— 年份在往下走,月份却每年重置方向。
|
|
|
|
|
|
*
|
|
|
|
|
|
* 🔴 改这里必须同步改 assets/js/archives.js 的 buildYear:
|
|
|
|
|
|
* 切年时那段 JS 会用同一套类名重建 DOM,它的月份排序写死在
|
|
|
|
|
|
* `Object.keys(...).sort((a,b) => a-b)`。只改 PHP 会出现
|
|
|
|
|
|
* 「首屏 12→1,点一下下一年变回 1→12」这种只在交互后才暴露的错。
|
|
|
|
|
|
* 判据在 _archives_e2e.js:首屏与切年后的月份序列必须同向。 */
|
2026-10-26 13:25:51 +08:00
|
|
|
|
krsort($years);
|
|
|
|
|
|
foreach ($years as $y => $yv) {
|
2026-10-26 18:42:40 +08:00
|
|
|
|
krsort($years[$y]['months']);
|
2026-10-26 13:25:51 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/* 月内按日期倒序 —— 由产品自己排,不依赖 SQL 的 ORDER BY。
|
|
|
|
|
|
*
|
|
|
|
|
|
* 🔴 为什么上面已经有 `ORDER BY post_date DESC` 还要排一次:
|
|
|
|
|
|
* 那个 ORDER BY 保证的是"三张表里这一条 SQL 的返回顺序",
|
|
|
|
|
|
* 它是真实存在的契约,正常情况下够用。但归档的"日期倒序"
|
|
|
|
|
|
* 是**这个功能的对外承诺**,不该只由上游偶然保证 ——
|
|
|
|
|
|
* 一旦有人调 SQL(换成 post_date_gmt、加上 UNION、被
|
|
|
|
|
|
* object-cache 插件换成别的查询),页面顺序会静默变乱,
|
|
|
|
|
|
* 而且乱得很有迷惑性:同一天的多篇顺序不对,很难注意到。
|
|
|
|
|
|
*
|
|
|
|
|
|
* 实测证据:本地单测把夹具**乱序**喂进来(不模拟 SQL 排序),
|
|
|
|
|
|
* 产品就把 `20, 05, 01, 02` 原样输出了 —— 说明它此刻确实
|
|
|
|
|
|
* 完全依赖上游顺序。一次 usort 的代价可以忽略(一年几百条),
|
|
|
|
|
|
* 换来的是这个承诺由产品自己兜住。
|
|
|
|
|
|
*
|
|
|
|
|
|
* 用 `<=>` 比较 `YYYY-MM-DD HH:MM:SS` 字符串:定长格式下字典序
|
|
|
|
|
|
* 与时间序一致,比 strtotime 更快也不受时区影响。 */
|
|
|
|
|
|
foreach ($years as $y => $yv) {
|
|
|
|
|
|
foreach ($years[$y]['months'] as $m => $mv) {
|
|
|
|
|
|
usort($years[$y]['months'][$m]['items'], function ($a, $b) {
|
|
|
|
|
|
if ($a['date'] === $b['date']) {
|
|
|
|
|
|
// 同一时刻(极少,但导入数据里会有)→ 按 id 倒序,保证稳定
|
|
|
|
|
|
return $b['id'] <=> $a['id'];
|
|
|
|
|
|
}
|
|
|
|
|
|
return strcmp($b['date'], $a['date']);
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
$order = array_keys($years);
|
|
|
|
|
|
|
|
|
|
|
|
$cache = array(
|
|
|
|
|
|
'years' => $years,
|
|
|
|
|
|
'order' => $order,
|
|
|
|
|
|
'latest' => (int) $order[0],
|
|
|
|
|
|
'total' => $total,
|
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
|
|
return $cache;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 短代码参数
|
|
|
|
|
|
*
|
2026-10-26 14:04:12 +08:00
|
|
|
|
* @param array $atts
|
|
|
|
|
|
* @param string $tag 实际命中的标签名(`archives` 或 `Archives`)
|
2026-10-26 13:25:51 +08:00
|
|
|
|
* @return array
|
|
|
|
|
|
*/
|
2026-10-26 14:04:12 +08:00
|
|
|
|
function yang_archives_atts($atts, $tag = 'archives')
|
2026-10-26 13:25:51 +08:00
|
|
|
|
{
|
2026-10-26 14:04:12 +08:00
|
|
|
|
/* 🔴 第 3 个参数必须是**实际命中的那个 tag**,不能写死 'archives'。
|
|
|
|
|
|
* 它决定 `shortcode_atts_{$tag}` 过滤器往外传的名字。
|
|
|
|
|
|
* 我们同时注册了 `archives` 与 `Archives` 两种写法,
|
|
|
|
|
|
* 写死小写的话,用 `[Archives]` 的站点上 `shortcode_atts_Archives`
|
|
|
|
|
|
* 这个钩子永远收不到 —— 属于"功能能用但扩展点悄悄失效"的隐性 bug。 */
|
2026-10-26 13:25:51 +08:00
|
|
|
|
$a = shortcode_atts(array(
|
|
|
|
|
|
'year' => '',
|
|
|
|
|
|
'month' => '1',
|
|
|
|
|
|
'nav' => '1',
|
2026-10-26 14:04:12 +08:00
|
|
|
|
), $atts, $tag);
|
2026-10-26 13:25:51 +08:00
|
|
|
|
|
|
|
|
|
|
return array(
|
|
|
|
|
|
'year' => $a['year'] === '' ? '' : (int) $a['year'],
|
|
|
|
|
|
'month' => (int) $a['month'] === 1 ? 1 : 0,
|
|
|
|
|
|
'nav' => (int) $a['nav'] === 1 ? 1 : 0,
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 决定首屏显示哪一年
|
|
|
|
|
|
*
|
|
|
|
|
|
* 优先级:URL 参数 `?arc_year=YYYY` > 短代码 `year="YYYY"` > 今年
|
|
|
|
|
|
*
|
|
|
|
|
|
* 为什么 URL 参数优先:切年时 JS 会 `replaceState` 把年份写进地址,
|
|
|
|
|
|
* 用户复制链接给别人,对方打开必须看到**同一年的归档**。
|
|
|
|
|
|
* 如果短代码的 year 优先,这个链接就永远只能打开作者设定的那一年,
|
|
|
|
|
|
* 分享出去等于没用。
|
|
|
|
|
|
*
|
|
|
|
|
|
* ⚠️ 年份必须是数据里**真实存在**的年份,否则落到最新一年。
|
|
|
|
|
|
* 不这么做的话,`?arc_year=1900` 会渲染出一个空列表,
|
|
|
|
|
|
* 而导航按钮的禁用态又是按"有没有数据"算的 —— 页面会卡在
|
|
|
|
|
|
* 一个既不能前进也不能后退的死年。
|
|
|
|
|
|
*
|
|
|
|
|
|
* @param array $data yang_archives_data() 的结果
|
|
|
|
|
|
* @param int $attr_year 短代码上写的年份,0 表示没写
|
|
|
|
|
|
* @return int
|
|
|
|
|
|
*/
|
|
|
|
|
|
function yang_archives_pick_year($data, $attr_year)
|
|
|
|
|
|
{
|
|
|
|
|
|
$order = $data['order'];
|
|
|
|
|
|
if (empty($order)) {
|
|
|
|
|
|
return 0;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
$candidates = array();
|
|
|
|
|
|
|
|
|
|
|
|
// phpcs:ignore WordPress.Security.NonceVerification.Recommended
|
|
|
|
|
|
if (isset($_GET['arc_year'])) {
|
|
|
|
|
|
$candidates[] = (int) $_GET['arc_year'];
|
|
|
|
|
|
}
|
|
|
|
|
|
if ($attr_year > 0) {
|
|
|
|
|
|
$candidates[] = (int) $attr_year;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
foreach ($candidates as $y) {
|
|
|
|
|
|
if (isset($data['years'][$y])) {
|
|
|
|
|
|
return $y;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 都没命中 → 今年若在数据里就用今年,否则用最新一年
|
|
|
|
|
|
$this_year = (int) current_time('Y');
|
|
|
|
|
|
if (isset($data['years'][$this_year])) {
|
|
|
|
|
|
return $this_year;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return (int) $order[0];
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 渲染某一年的列表(服务端首屏用)
|
|
|
|
|
|
*
|
|
|
|
|
|
* ⚠️ JS 切年时会用**同一套类名**重建 DOM(见 assets/js/archives.js)。
|
|
|
|
|
|
* 改这里的结构必须同步改那边,否则首屏与切年后的样式会不一致。
|
|
|
|
|
|
* 判据在 `_archives_e2e.js` 里:两边建出来的节点类名必须逐个相同。
|
|
|
|
|
|
*
|
|
|
|
|
|
* @param array $ydata 某一年('count' + 'months')
|
|
|
|
|
|
* @param int $year 年份
|
|
|
|
|
|
* @param int $show_month 是否输出月份分组标题
|
|
|
|
|
|
* @return string
|
|
|
|
|
|
*/
|
|
|
|
|
|
function yang_archives_render_year($ydata, $year, $show_month)
|
|
|
|
|
|
{
|
|
|
|
|
|
if (empty($ydata['months'])) {
|
|
|
|
|
|
return '';
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
$out = '';
|
|
|
|
|
|
|
|
|
|
|
|
foreach ($ydata['months'] as $m => $mdata) {
|
|
|
|
|
|
if ($show_month) {
|
|
|
|
|
|
$out .= '<div class="ar-month">';
|
|
|
|
|
|
$out .= '<h4 class="ar-month-title">'
|
|
|
|
|
|
. sprintf(
|
|
|
|
|
|
/* translators: 1: 月份数字 2: 该月文章数 */
|
|
|
|
|
|
esc_html__('%1$s 月', 'kratos'),
|
|
|
|
|
|
/* 🔴 月份同样不过 number_format_i18n —— 它会把 10 月
|
|
|
|
|
|
* 变成「10」没问题,但一旦有人把月份格式改成两位数
|
|
|
|
|
|
* 或站点语言用非逗号分隔,就会与 JS 侧(String(m))
|
|
|
|
|
|
* 不一致。月份是编号。 */
|
|
|
|
|
|
esc_html((string) $m)
|
|
|
|
|
|
)
|
|
|
|
|
|
. '<span class="ar-month-count">'
|
2026-10-26 18:42:40 +08:00
|
|
|
|
/* 3.2.48:原来是**裸数字**,用户反馈「也不知道什么意思」。
|
|
|
|
|
|
* 改成「共 N 篇」,与年度那一行的文案一致。
|
|
|
|
|
|
*
|
|
|
|
|
|
* 🔴 容器类名 ar-month-count 保留不动 —— CSS 的胶囊底、
|
|
|
|
|
|
* 等宽数字都挂在它上面;JS 侧 buildYear 也用同一个类名。
|
|
|
|
|
|
* 改文案不改结构,两侧判据都不用迁。 */
|
|
|
|
|
|
. sprintf(
|
|
|
|
|
|
/* translators: %s: 该月文章数 */
|
|
|
|
|
|
esc_html__('共 %s 篇', 'kratos'),
|
|
|
|
|
|
esc_html(number_format_i18n($mdata['count']))
|
|
|
|
|
|
)
|
2026-10-26 13:25:51 +08:00
|
|
|
|
. '</span>'
|
|
|
|
|
|
. '</h4>';
|
|
|
|
|
|
} else {
|
|
|
|
|
|
$out .= '<div class="ar-month">';
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
$out .= '<ul class="ar-list">';
|
|
|
|
|
|
|
|
|
|
|
|
foreach ($mdata['items'] as $it) {
|
|
|
|
|
|
$out .= yang_archives_render_item($it, $m);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
$out .= '</ul></div>';
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return $out;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 渲染单条
|
|
|
|
|
|
*
|
|
|
|
|
|
* @param array $it 条目
|
|
|
|
|
|
* @param int $m 月份(用于拼「月-日」)
|
|
|
|
|
|
* @return string
|
|
|
|
|
|
*/
|
|
|
|
|
|
function yang_archives_render_item($it, $m)
|
|
|
|
|
|
{
|
|
|
|
|
|
$md = sprintf('%02d-%s', (int) $m, $it['day']);
|
|
|
|
|
|
|
|
|
|
|
|
$out = '<li class="ar-item">';
|
|
|
|
|
|
|
|
|
|
|
|
$out .= '<time class="ar-date" datetime="' . esc_attr($it['date']) . '">'
|
|
|
|
|
|
. esc_html($md) . '</time>';
|
|
|
|
|
|
|
|
|
|
|
|
$out .= '<a class="ar-title" href="' . esc_url($it['url']) . '">'
|
|
|
|
|
|
. esc_html($it['title']) . '</a>';
|
|
|
|
|
|
|
|
|
|
|
|
/* 三项计数。用的 i-hot / i-comments / i-good 是主题自带图标集里
|
|
|
|
|
|
* 已经在用的三个(page-content.php 同款),不引入新图标,
|
|
|
|
|
|
* 免得字体包里缺字形变成豆腐块。
|
|
|
|
|
|
*
|
|
|
|
|
|
* `data-zero="1"` 给零值打标记,CSS 据此弱化 —— 站内大部分文章的
|
|
|
|
|
|
* 评论数和点赞数就是 0,三项同等强调会让计数列看起来全是「0 0 0」,
|
|
|
|
|
|
* 反而看不出哪篇有热度。 */
|
|
|
|
|
|
$out .= '<span class="ar-stats">';
|
|
|
|
|
|
|
|
|
|
|
|
$out .= yang_archives_render_stat('views', 'i-hot', $it['views'],
|
|
|
|
|
|
__('浏览数', 'kratos'));
|
|
|
|
|
|
|
|
|
|
|
|
$out .= yang_archives_render_stat('comments', 'i-comments', $it['comments'],
|
|
|
|
|
|
__('评论数', 'kratos'));
|
|
|
|
|
|
|
|
|
|
|
|
$out .= yang_archives_render_stat('love', 'i-good', $it['love'],
|
|
|
|
|
|
__('点赞数', 'kratos'));
|
|
|
|
|
|
|
|
|
|
|
|
$out .= '</span>';
|
|
|
|
|
|
|
|
|
|
|
|
$out .= '</li>';
|
|
|
|
|
|
|
|
|
|
|
|
return $out;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 单个计数(图标 + 数字 + 无障碍文字)
|
|
|
|
|
|
*
|
|
|
|
|
|
* @param string $kind views|comments|love,只用于类名
|
|
|
|
|
|
* @param string $icon 图标类名
|
|
|
|
|
|
* @param int $num 数值
|
|
|
|
|
|
* @param string $label 无障碍标签
|
|
|
|
|
|
* @return string
|
|
|
|
|
|
*/
|
|
|
|
|
|
function yang_archives_render_stat($kind, $icon, $num, $label)
|
|
|
|
|
|
{
|
|
|
|
|
|
$num = (int) $num;
|
|
|
|
|
|
|
|
|
|
|
|
return '<span class="ar-stat ar-stat-' . esc_attr($kind) . '"'
|
|
|
|
|
|
. ' title="' . esc_attr($label) . '"'
|
|
|
|
|
|
. ' data-zero="' . ($num === 0 ? '1' : '0') . '">'
|
|
|
|
|
|
. '<i class="kicon ' . esc_attr($icon) . '" aria-hidden="true"></i>'
|
|
|
|
|
|
. '<span class="ar-num">' . esc_html(number_format_i18n($num)) . '</span>'
|
|
|
|
|
|
. '<span class="sr-only">' . esc_html($label) . '</span>'
|
|
|
|
|
|
. '</span>';
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 四角导航
|
|
|
|
|
|
*
|
|
|
|
|
|
* 语义按用户要求:**左侧「上一年」= 回到更近的年份**,
|
|
|
|
|
|
* **右侧「下一年」= 去更早的年份**。
|
|
|
|
|
|
*
|
|
|
|
|
|
* 🔴 这个方向初看是反的("下一年"却往过去走),但用户明确写了两遍
|
|
|
|
|
|
* 示例(2026 → 2025 → 2024),所以行为按示例来。
|
|
|
|
|
|
* 为了不让人误点,按钮上除了文案还直接显示**目标年份**,
|
|
|
|
|
|
* 这样"点下去会去哪"一眼可见,不必靠文案猜。
|
|
|
|
|
|
*
|
|
|
|
|
|
* 用的是 `<button>` 而不是 `<a href>`:
|
|
|
|
|
|
* · 切年不刷新页面,`<a>` 的默认行为要 `preventDefault` 才有意义,
|
|
|
|
|
|
* 而无 JS 时 `<a>` 带 `?arc_year=` 还能用 —— 这点是有价值的。
|
|
|
|
|
|
* · 所以这里**两者结合**:`<a href="?arc_year=YYYY">` 作为真实
|
|
|
|
|
|
* 回退(新标签/无 JS 都能用),JS 接管后 `preventDefault` 改走
|
|
|
|
|
|
* DOM 重建。这样"不刷新"是增强,不是唯一路径。
|
|
|
|
|
|
*
|
|
|
|
|
|
* 🔴 `$variant` 必须由调用方给(top / bottom):
|
|
|
|
|
|
* 第一版把 `ar-nav ar-nav-top` 写死在这里,而下排也是调这个函数生成的
|
|
|
|
|
|
* ⇒ 下排同时带上了 `ar-nav-top` 类名。虽然 `.ar-foot .ar-nav`
|
|
|
|
|
|
* (0,3,0)特异性高于 `.ar-nav-top`(0,2,0)把它压住了、视觉上没坏,
|
|
|
|
|
|
* 但类名本身就是错的("下排"不是"top"),CSS/JS 以后想按位置区分
|
|
|
|
|
|
* 就无从下手,测试也没法分辨上下排。**位置由调用方决定,不由渲染函数猜。**
|
|
|
|
|
|
*
|
|
|
|
|
|
* @param int $year 当前年份
|
|
|
|
|
|
* @param array $order 有文章的年份(倒序)
|
|
|
|
|
|
* @param string $variant top|bottom,只用于类名
|
|
|
|
|
|
* @return string
|
|
|
|
|
|
*/
|
|
|
|
|
|
function yang_archives_render_nav($year, $order, $variant = 'top')
|
|
|
|
|
|
{
|
|
|
|
|
|
$pos = array_search($year, $order, true);
|
|
|
|
|
|
if ($pos === false) {
|
|
|
|
|
|
return '';
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/* order 是倒序:[2026, 2025, 2024]
|
|
|
|
|
|
* · 更近的年份(上一年)在 pos-1
|
|
|
|
|
|
* · 更早的年份(下一年)在 pos+1 */
|
|
|
|
|
|
$prev = isset($order[$pos - 1]) ? (int) $order[$pos - 1] : 0;
|
|
|
|
|
|
$next = isset($order[$pos + 1]) ? (int) $order[$pos + 1] : 0;
|
|
|
|
|
|
|
|
|
|
|
|
$variant = ($variant === 'bottom') ? 'bottom' : 'top';
|
|
|
|
|
|
|
|
|
|
|
|
$out = '<div class="ar-nav ar-nav-' . esc_attr($variant) . '">';
|
|
|
|
|
|
$out .= yang_archives_render_nav_btn($prev, 'prev', $year);
|
|
|
|
|
|
$out .= yang_archives_render_nav_btn($next, 'next', $year);
|
|
|
|
|
|
$out .= '</div>';
|
|
|
|
|
|
|
|
|
|
|
|
return $out;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 单个导航按钮
|
|
|
|
|
|
*
|
|
|
|
|
|
* @param int $target 目标年份,0 表示没有
|
|
|
|
|
|
* @param string $dir prev|next
|
|
|
|
|
|
* @param int $year 当前年份(拼 URL 用)
|
|
|
|
|
|
* @return string
|
|
|
|
|
|
*/
|
|
|
|
|
|
function yang_archives_render_nav_btn($target, $dir, $year)
|
|
|
|
|
|
{
|
|
|
|
|
|
$is_prev = ($dir === 'prev');
|
|
|
|
|
|
$label = $is_prev ? __('上一年', 'kratos') : __('下一年', 'kratos');
|
|
|
|
|
|
$arrow = $is_prev ? '↑' : '↓';
|
|
|
|
|
|
|
|
|
|
|
|
if ($target === 0) {
|
|
|
|
|
|
// 没有目标年 → 禁用态。用 <span> 而不是 disabled 的 <a>:
|
|
|
|
|
|
// <a> 没有 disabled 语义,加了也不生效,还会被键盘 Tab 到。
|
|
|
|
|
|
return '<span class="ar-nav-btn is-disabled" aria-disabled="true">'
|
|
|
|
|
|
. '<span class="ar-nav-arrow" aria-hidden="true">' . $arrow . '</span>'
|
|
|
|
|
|
. '<span class="ar-nav-label">' . esc_html($label) . '</span>'
|
|
|
|
|
|
. '</span>';
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return '<a class="ar-nav-btn ar-nav-' . esc_attr($dir) . '"'
|
|
|
|
|
|
. ' href="' . esc_url(add_query_arg('arc_year', $target)) . '"'
|
|
|
|
|
|
. ' data-year="' . esc_attr($target) . '"'
|
|
|
|
|
|
. ' rel="nofollow">'
|
|
|
|
|
|
. '<span class="ar-nav-arrow" aria-hidden="true">' . $arrow . '</span>'
|
|
|
|
|
|
. '<span class="ar-nav-label">' . esc_html($label) . '</span>'
|
|
|
|
|
|
/* 🔴 年份**绝不能**过 number_format_i18n:它会输出「2,025」。
|
|
|
|
|
|
* 实测证据(e2e):首屏 PHP 出的是「2,025」,
|
|
|
|
|
|
* 而 JS 切年后写的是 String(year) = 「2025」——
|
|
|
|
|
|
* 同一个按钮点一下就变样,像 bug 一样。
|
|
|
|
|
|
* 年份是编号不是数量,任何时候都不该有千分位。 */
|
|
|
|
|
|
. '<span class="ar-nav-year">' . esc_html((string) $target) . '</span>'
|
|
|
|
|
|
. '</a>';
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 短代码主体
|
|
|
|
|
|
*
|
2026-10-26 14:04:12 +08:00
|
|
|
|
* 🔴 签名必须是 `($atts, $content, $tag)` 三个参数 —— 这是 WordPress
|
|
|
|
|
|
* `do_shortcode_tag()` 的调用约定:
|
|
|
|
|
|
* call_user_func( $shortcode_tags[$tag], $attr, $content, $tag )
|
|
|
|
|
|
* 中间的 `$content` 是**闭合式短代码**(`[x]...[/x]`)的中间内容,
|
|
|
|
|
|
* 自闭合短代码传空串。本短代码用不到它,但**位置不能省**:
|
|
|
|
|
|
* 省掉之后 `$tag` 会落到第 2 个参数上,实际收到的 tag 变成空串,
|
|
|
|
|
|
* `shortcode_atts_{$tag}` 就只剩 `shortcode_atts_` 这个无主钩子。
|
|
|
|
|
|
* (3.2.47 开发中实测踩到:直接调函数传 ('Archives') 是对的,
|
|
|
|
|
|
* 过 do_shortcode 才变成空 —— 差别就在中间这个 $content。)
|
|
|
|
|
|
*
|
|
|
|
|
|
* @param array $atts
|
|
|
|
|
|
* @param string $content 闭合式短代码的中间内容(本短代码不用)
|
|
|
|
|
|
* @param string $tag 实际命中的标签名(archives / Archives / ARCHIVES)
|
2026-10-26 13:25:51 +08:00
|
|
|
|
* @return string
|
|
|
|
|
|
*/
|
2026-10-26 14:04:12 +08:00
|
|
|
|
function yang_archives_shortcode($atts, $content = '', $tag = 'archives')
|
2026-10-26 13:25:51 +08:00
|
|
|
|
{
|
2026-10-26 14:04:12 +08:00
|
|
|
|
$opt = yang_archives_atts($atts, $tag);
|
2026-10-26 13:25:51 +08:00
|
|
|
|
$data = yang_archives_data();
|
|
|
|
|
|
|
|
|
|
|
|
if (empty($data['order'])) {
|
|
|
|
|
|
return '<div class="yang-archives"><p class="ar-empty">'
|
|
|
|
|
|
. esc_html__('还没有任何文章。', 'kratos') . '</p></div>';
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
$year = yang_archives_pick_year($data, $opt['year']);
|
|
|
|
|
|
if ($year === 0) {
|
|
|
|
|
|
return '<div class="yang-archives"><p class="ar-empty">'
|
|
|
|
|
|
. esc_html__('还没有任何文章。', 'kratos') . '</p></div>';
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/* 资源按需加载:只在真的用了短代码的页面加载。
|
|
|
|
|
|
* 与 theme-toc.php 同一套做法(那边也是 wp_enqueue_scripts 里
|
|
|
|
|
|
* 判 has_marker 再 enqueue)。这里更直接 —— 短代码已经开始渲染了,
|
|
|
|
|
|
* 说明页面确实要用,直接 enqueue 即可。
|
|
|
|
|
|
*
|
|
|
|
|
|
* 🔴 为什么必须走 wp_enqueue_* 而不是手写 <link>:
|
|
|
|
|
|
* 主题的 ASSET_PATH 会随「CDN 加速」选项改变(inc/theme-cdn.php),
|
|
|
|
|
|
* 手写相对路径会绕过 CDN 改写,也拿不到 THEME_VERSION 做缓存失效。 */
|
|
|
|
|
|
wp_enqueue_style(
|
|
|
|
|
|
'yang-archives',
|
|
|
|
|
|
ASSET_PATH . '/assets/css/archives.css',
|
|
|
|
|
|
array('kratos'),
|
|
|
|
|
|
THEME_VERSION
|
|
|
|
|
|
);
|
|
|
|
|
|
wp_enqueue_script(
|
|
|
|
|
|
'yang-archives',
|
|
|
|
|
|
ASSET_PATH . '/assets/js/archives.js',
|
|
|
|
|
|
array(),
|
|
|
|
|
|
THEME_VERSION,
|
|
|
|
|
|
true
|
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
|
|
/* 把全部年份的数据交给 JS。
|
|
|
|
|
|
*
|
|
|
|
|
|
* 🔴 用 `<script type="application/json">` 而不是内联 `var data = {...}`:
|
|
|
|
|
|
* ① 不污染全局作用域;
|
|
|
|
|
|
* ② 内容不需要转义 `</script>`(下面还有一道保险);
|
|
|
|
|
|
* ③ 浏览器不执行它,CSP 里 `unsafe-inline` 也管不到。
|
|
|
|
|
|
*
|
|
|
|
|
|
* 🔴 JSON_HEX_TAG 等四个 flag 是**必须**的:文章标题里出现
|
|
|
|
|
|
* `</script>` 时,若不把这四个字符转成 \\u003C 之类,
|
|
|
|
|
|
* 浏览器的 HTML 解析器会在标题中间就把脚本块截断 ——
|
|
|
|
|
|
* 页面后面全部错乱,而且看起来像"某个标题把页面搞坏了"。 */
|
|
|
|
|
|
$json = wp_json_encode(
|
|
|
|
|
|
array(
|
|
|
|
|
|
'years' => $data['years'],
|
|
|
|
|
|
'order' => $data['order'],
|
|
|
|
|
|
'latest' => $data['latest'],
|
|
|
|
|
|
'total' => $data['total'],
|
|
|
|
|
|
'current' => $year,
|
|
|
|
|
|
'showMonth' => $opt['month'],
|
|
|
|
|
|
'showNav' => $opt['nav'],
|
|
|
|
|
|
'baseUrl' => get_permalink(),
|
|
|
|
|
|
'i18n' => array(
|
|
|
|
|
|
'month' => __('%s 月', 'kratos'),
|
|
|
|
|
|
'prev' => __('上一年', 'kratos'),
|
|
|
|
|
|
'next' => __('下一年', 'kratos'),
|
|
|
|
|
|
'views' => __('浏览数', 'kratos'),
|
|
|
|
|
|
'comments' => __('评论数', 'kratos'),
|
|
|
|
|
|
'love' => __('点赞数', 'kratos'),
|
|
|
|
|
|
'untitled' => __('(无标题)', 'kratos'),
|
|
|
|
|
|
'empty' => __('这一年还没有文章。', 'kratos'),
|
|
|
|
|
|
'totalFmt' => __('共 %s 篇', 'kratos'),
|
2026-10-26 18:42:40 +08:00
|
|
|
|
/* 3.2.48:月份计数文案。与 totalFmt 同款,但**单独一个键**——
|
|
|
|
|
|
* 将来若想给月份换个说法(比如「本月 N 篇」),不必牵动年度那行。 */
|
|
|
|
|
|
'monthCount' => __('共 %s 篇', 'kratos'),
|
2026-10-26 13:25:51 +08:00
|
|
|
|
),
|
|
|
|
|
|
),
|
|
|
|
|
|
JSON_HEX_TAG | JSON_HEX_AMP | JSON_HEX_APOS | JSON_HEX_QUOT
|
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
|
|
$uid = 'yang-archives-' . wp_unique_id();
|
|
|
|
|
|
|
|
|
|
|
|
$out = '<div class="yang-archives" id="' . esc_attr($uid) . '"'
|
|
|
|
|
|
. ' data-year="' . esc_attr($year) . '">';
|
|
|
|
|
|
|
|
|
|
|
|
// 顶部:年份标题 + 四角导航上排
|
|
|
|
|
|
$out .= '<div class="ar-head">';
|
|
|
|
|
|
/* 🔴 年份标题同样**不能**过 number_format_i18n(会变「2,025」)。
|
|
|
|
|
|
* 而且要输出纯数字,与 JS 切年后的 `String(year)` 完全一致 ——
|
|
|
|
|
|
* 两边不一致就是「首屏 2,025、点一下 2025」。 */
|
|
|
|
|
|
$out .= '<h3 class="ar-year" data-role="year-title">'
|
|
|
|
|
|
. esc_html((string) $year) . '</h3>';
|
|
|
|
|
|
|
|
|
|
|
|
$out .= '<span class="ar-total" data-role="year-total">'
|
|
|
|
|
|
. sprintf(
|
|
|
|
|
|
/* translators: %s: 文章数 */
|
|
|
|
|
|
esc_html__('共 %s 篇', 'kratos'),
|
|
|
|
|
|
esc_html(number_format_i18n($data['years'][$year]['count']))
|
|
|
|
|
|
)
|
|
|
|
|
|
. '</span>';
|
|
|
|
|
|
|
|
|
|
|
|
if ($opt['nav']) {
|
|
|
|
|
|
$out .= yang_archives_render_nav($year, $data['order'], 'top');
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
$out .= '</div>';
|
|
|
|
|
|
|
|
|
|
|
|
// 列表主体
|
|
|
|
|
|
$out .= '<div class="ar-body" data-role="body">';
|
|
|
|
|
|
$out .= yang_archives_render_year($data['years'][$year], $year, $opt['month']);
|
|
|
|
|
|
$out .= '</div>';
|
|
|
|
|
|
|
|
|
|
|
|
// 底部导航(四角的下排)
|
|
|
|
|
|
if ($opt['nav']) {
|
|
|
|
|
|
$out .= '<div class="ar-foot">';
|
|
|
|
|
|
$out .= yang_archives_render_nav($year, $data['order'], 'bottom');
|
|
|
|
|
|
$out .= '</div>';
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
$out .= '<script type="application/json" data-role="data">'
|
|
|
|
|
|
. $json . '</script>';
|
|
|
|
|
|
|
|
|
|
|
|
$out .= '</div>';
|
|
|
|
|
|
|
|
|
|
|
|
return $out;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-10-26 14:04:12 +08:00
|
|
|
|
/* 🔴🔴 WP 的 shortcode 标签**区分大小写** —— 这一点 3.2.46 写错过,实测过。
|
|
|
|
|
|
*
|
|
|
|
|
|
* 内核证据(wp-includes/shortcodes.php):
|
|
|
|
|
|
* ① `get_shortcode_regex()` 直接 `implode('|', array_map('preg_quote', $tagnames))`,
|
|
|
|
|
|
* 拿注册名拼正则,**没有做任何大小写归一化**;
|
|
|
|
|
|
* ② `do_shortcode()` 里 `preg_match_all('/' . get_shortcode_regex() . '/', ...)`
|
|
|
|
|
|
* —— 修饰符只有默认,**没有 `i`**;
|
|
|
|
|
|
* ③ `do_shortcode_tag()` 拿到 `$tag = $m[2]` 后直接 `$shortcode_tags[$tag]` 精确查表。
|
|
|
|
|
|
*
|
|
|
|
|
|
* ⇒ `add_shortcode('archives', ...)` **只匹配全小写 `[archives]`**。
|
|
|
|
|
|
* 用户按文档写 `[Archives]`,正则压根不认,WordPress 就把原文原样吐出来
|
|
|
|
|
|
* —— 页面显示 `<p>[Archives]</p>`,看起来极像"短代码没装/被转义",
|
|
|
|
|
|
* 实际是**标签名大小写对不上**。
|
|
|
|
|
|
*
|
|
|
|
|
|
* ⇒ 只能把要支持的写法**逐个注册**。这里注册三种常见形态:
|
|
|
|
|
|
* 全小写(WP 惯例)、首字母大写(文档与用户习惯)、全大写。
|
|
|
|
|
|
* 注册同一个回调是安全的:`$shortcode_tags` 里就是几条 key 指向同一个函数,
|
|
|
|
|
|
* 任何时刻只会命中其中一条(正则里几个分支互斥),不会渲染两次。
|
|
|
|
|
|
*
|
|
|
|
|
|
* ⚠️ 不追求覆盖任意混合大小写(`[aRcHiVeS]`):那需要每隔位置都注册一条,
|
|
|
|
|
|
* 正则会膨胀到不可维护。三种形态已经覆盖真实使用中的全部习惯写法。 */
|
|
|
|
|
|
add_shortcode('archives', 'yang_archives_shortcode');
|
|
|
|
|
|
add_shortcode('Archives', 'yang_archives_shortcode');
|
|
|
|
|
|
add_shortcode('ARCHIVES', 'yang_archives_shortcode');
|