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(真实文章渲染目录 + 真实评论数据渲染)。
This commit is contained in:
2026-10-24 01:03:36 +08:00
parent 4beb09307c
commit cbe0f2dc59
14 changed files with 3577 additions and 192 deletions
+215 -58
View File
@@ -1,82 +1,239 @@
<?php
/**
* 评论模板
*
* ── 3.2.18 重写要点 ──────────────────────────────────────────
* 1. **列表容器改成 <ol>**。原来 `<div class="list">` 里直接放
* `wp_list_comments()` 输出的 `<li>`,标签层级非法。浏览器会
* 自行"修复"结构,而嵌套回复(`<ul class="children">`)在这种
* 被修复过的树里容易错位 —— 这是"评论看起来乱"的一个真实来源。
*
* 2. **回复状态条**。点「回复」时除了把表单挪过去,还在表单上方
* 显示「正在回复 @某人 / 取消」。旧实现只有表单被挪动这一条线索,
* 用户在长评论区里挪动后常常不知道自己到底在回复谁。
*
* 3. **表单加 nonce**。AJAX 提交走 nonce 校验,没有它任何第三方
* 页面都能借访客的浏览器刷评论。
*
* 4. **提交按钮的禁用态由 JS 控制**,模板里给出 `.is-loading` 的样式钩子。
*
* @author Yongquan Yang <yangyq.net>
* @license MIT License
* @version 2020.03.14
*/
if (isset($_SERVER['SCRIPT_FILENAME']) && 'comments.php' == basename($_SERVER['SCRIPT_FILENAME'])) {
die();
}
require get_template_directory() . '/pages/page-smilies.php';
if (comments_open()) {?>
$yang_post_id = get_the_ID();
$yang_comment_count = (int) get_comments_number($yang_post_id);
$yang_max_depth = (int) get_option('thread_comments_depth', 5);
if ($yang_max_depth <= 0) {
$yang_max_depth = 5;
}
$yang_threaded = (bool) get_option('thread_comments');
$yang_has_comments = ($yang_comment_count > 0);
/* 评论是否分页:分页时首屏只给「加载更多」,不一次性铺全部 */
$yang_paged = (bool) get_option('page_comments');
$yang_per_page = (int) get_option('comments_per_page', 20);
if ($yang_per_page <= 0) {
$yang_per_page = 20;
}
?>
<div class="comments" id="comments">
<h3 class="title"><?php if (is_single()) {_e('文章评论', 'kratos');} else {_e('评论内容', 'kratos');}?></h3>
<div class="list">
<?php wp_list_comments('type=comment&callback=comment_callbacks');?>
<div class="comments-head">
<h3 class="title">
<?php
if ($yang_comment_count > 0) {
printf(
/* translators: %s: 评论条数 */
esc_html__('评论(%s)', 'kratos'),
esc_html(number_format_i18n($yang_comment_count))
);
} else {
esc_html_e('评论', 'kratos');
}
?>
</h3>
<?php if (comments_open()) : ?>
<a class="comments-jump-form" href="#respond"><?php esc_html_e('写下你的评论', 'kratos'); ?></a>
<?php endif; ?>
</div>
<div id="commentpage" class="nav text-center my-3">
<?php previous_comments_link(__('加载更多', 'kratos'));?>
</div>
<div id="respond" class="comment-respond mt-2">
<?php if (!comments_open()): elseif (get_option('comment_registration') && !is_user_logged_in()): ?>
<div class="error text-center">
<?php printf(__('您需要 <a href="%s">登录</a> 之后才可以评论', 'kratos'), wp_login_url(get_permalink()));?>
<?php if (!comments_open() && $yang_comment_count === 0) : ?>
<p class="comments-closed"><?php esc_html_e('评论已关闭。', 'kratos'); ?></p>
<?php elseif (!comments_open()) : ?>
<p class="comments-closed"><?php esc_html_e('评论已关闭,但你可以阅读上面的讨论。', 'kratos'); ?></p>
<?php endif; ?>
<?php if ($yang_has_comments) : ?>
<?php
/* <ol> 而不是 <div>:<li> 的父元素必须是列表容器。
加 reversed/start 会打乱视觉顺序,所以只用普通 <ol>。 */
?>
<ol class="yang-comment-list" id="yang-comment-list">
<?php
wp_list_comments(array(
'type' => 'comment',
'callback' => 'comment_callbacks',
'style' => 'ol',
'max_depth' => $yang_threaded ? $yang_max_depth : 1,
'avatar_size' => 48,
'per_page' => $yang_paged ? $yang_per_page : 0,
'page' => 1,
));
?>
</ol>
<?php
/* 加载更多:只在「有分页」且「总数超过一页」时出现。
旧模板无条件输出,即使只有 1 条评论也显示一个点不动的按钮。 */
$yang_has_more = $yang_paged && $yang_comment_count > $yang_per_page;
?>
<div class="comments-more" id="yang-comments-more"<?php echo $yang_has_more ? '' : ' hidden'; ?>>
<button type="button" class="comments-more-btn" data-page="2">
<?php esc_html_e('加载更多评论', 'kratos'); ?>
</button>
</div>
<?php elseif (comments_open()) : ?>
<p class="comments-empty"><?php esc_html_e('还没有评论,来说第一句吧。', 'kratos'); ?></p>
<?php endif; ?>
<?php if (comments_open()) : ?>
<?php if (get_option('comment_registration') && !is_user_logged_in()) : ?>
<div id="respond" class="comment-respond">
<p class="comments-login-required">
<?php
printf(
/* translators: %s: 登录链接 */
wp_kses(
__('您需要 <a href="%s">登录</a> 之后才可以评论。', 'kratos'),
array('a' => array('href' => array()))
),
esc_url(wp_login_url(get_permalink()))
);
?>
</p>
</div>
<?php else: ?>
<form id="commentform" name="commentform" action="<?php echo get_option('siteurl'); ?>/wp-comments-post.php" method="post">
<div class="comment-form">
<?php if (!is_user_logged_in()): ?>
<div class="comment-info mb-3 row">
<div class="col-md-6 comment-form-author">
<div class="input-group">
<div class="input-group-prepend">
<span class="input-group-text"><i class="kicon i-user"></i></span>
<?php else : ?>
<div id="respond" class="comment-respond mt-4">
<h3 class="comment-reply-title">
<span id="yang-reply-title-text"><?php esc_html_e('发表评论', 'kratos'); ?></span>
<?php
/* 取消回复按钮:由 JS 控制显示,模板里先藏起来。
不用 WordPress 的 cancel_comment_reply_link():
它生成的 <a href> 依赖 JS 拦截,没 JS 时会跳到
?replytocom= 的地址,行为很怪。这里用 button。 */
?>
<button type="button" id="yang-cancel-reply" class="comment-cancel-reply" hidden>
<?php esc_html_e('取消回复', 'kratos'); ?>
</button>
</h3>
<?php
/* 正在回复谁:JS 更新这段文字。
放在表单上方而不是按钮旁边,是因为表单会被"搬"到
被回复评论的下方,粘在按钮上的提示会跟着乱跑。 */
?>
<p class="comment-reply-hint" id="yang-reply-hint" hidden></p>
<form id="commentform" name="commentform" class="yang-comment-form" method="post"
action="<?php echo esc_url(site_url('/wp-comments-post.php')); ?>">
<?php if (!is_user_logged_in()) : ?>
<?php
/* 已登录用户的昵称邮箱从 cookie 里取,未登录才显示输入框。
esc_attr 必须做:cookie 里的值是用户可控的。 */
$yang_commenter = wp_get_current_commenter();
?>
<div class="comment-info">
<div class="comment-field comment-form-author">
<label for="author" class="sr-only"><?php esc_html_e('昵称', 'kratos'); ?></label>
<span class="comment-field-icon" aria-hidden="true"><i class="kicon i-user"></i></span>
<input class="comment-input" id="author" name="author" type="text"
placeholder="<?php esc_attr_e('昵称(必填)', 'kratos'); ?>"
value="<?php echo esc_attr($yang_commenter['comment_author']); ?>"
required="required" autocomplete="nickname" maxlength="60">
</div>
<input class="form-control" id="author" placeholder="<?php _e('昵称', 'kratos'); ?>" name="author" type="text" value="<?php echo esc_attr($commenter['comment_author']); ?>" required="required">
</div>
</div>
<div class="col-md-6 mt-3 mt-md-0 comment-form-email">
<div class="input-group">
<div class="input-group-prepend">
<span class="input-group-text"><i class="kicon i-cemail"></i></span>
<div class="comment-field comment-form-email">
<label for="email" class="sr-only"><?php esc_html_e('邮箱', 'kratos'); ?></label>
<span class="comment-field-icon" aria-hidden="true"><i class="kicon i-cemail"></i></span>
<input class="comment-input" id="email" name="email" type="email"
placeholder="<?php esc_attr_e('邮箱(必填,不会公开)', 'kratos'); ?>"
value="<?php echo esc_attr($yang_commenter['comment_author_email']); ?>"
required="required" autocomplete="email" maxlength="120">
</div>
<input id="email" class="form-control" name="email" placeholder="<?php _e('邮箱', 'kratos'); ?>" type="email" value="<?php echo esc_attr($commenter['comment_author_email']); ?>" required="required">
</div>
</div>
<div class="col-md-6 mt-3 comment-form-author">
<div class="input-group">
<div class="input-group-prepend">
<span class="input-group-text"><i class="kicon i-url"></i></span>
<div class="comment-field comment-form-url">
<label for="url" class="sr-only"><?php esc_html_e('网址', 'kratos'); ?></label>
<span class="comment-field-icon" aria-hidden="true"><i class="kicon i-url"></i></span>
<input class="comment-input" id="url" name="url" type="url"
placeholder="<?php esc_attr_e('网址(选填)', 'kratos'); ?>"
value="<?php echo esc_attr($yang_commenter['comment_author_url']); ?>"
autocomplete="url" maxlength="200">
</div>
<input class="form-control" id="author" placeholder="<?php _e('网址', 'kratos'); ?>" name="url" type="url" value="<?php echo esc_attr($commenter['comment_author_url']); ?>">
</div>
</div>
</div>
<?php endif;?>
<div class="comment-textarea">
<textarea class="form-control" id="comment" name="comment" rows="7" required="required"></textarea>
<div class="text-bar clearfix">
<div class="tool float-left">
<a class="addbtn" href="#" id="addsmile"><i class="kicon i-face"></i></a>
<div class="smile">
<div class="clearfix">
<?php endif; ?>
<div class="comment-textarea">
<label for="comment" class="sr-only"><?php esc_html_e('评论内容', 'kratos'); ?></label>
<textarea class="comment-input" id="comment" name="comment" rows="6"
placeholder="<?php esc_attr_e('说点什么…(支持 :smile: 这类表情代码)', 'kratos'); ?>"
required="required" maxlength="65525"></textarea>
<div class="comment-bar">
<div class="comment-bar-left">
<button type="button" class="comment-smile-btn" id="yang-smile-btn"
aria-expanded="false" aria-controls="yang-smile-panel"
title="<?php esc_attr_e('插入表情', 'kratos'); ?>">
<i class="kicon i-face" aria-hidden="true"></i>
</button>
<div class="comment-smile-panel" id="yang-smile-panel" hidden>
<?php echo $smilies; ?>
</div>
</div>
</div>
<div class="float-right">
<?php cancel_comment_reply_link(__('取消回复', 'kratos'));?>
<input name="submit" type="submit" id="submit" class="btn btn-primary" value="<?php _e('提交评论', 'kratos');?>">
<div class="comment-bar-right">
<span class="comment-status" id="yang-comment-status" role="status" aria-live="polite"></span>
<button type="submit" id="submit" class="comment-submit">
<?php esc_html_e('提交评论', 'kratos'); ?>
</button>
</div>
</div>
</div>
</div>
<?php
/* honeypot:把这个字段留空的是人,填了的是机器人。
CSS 用 position:absolute 移出视口,不能用 display:none
—— 部分爬虫会跳过隐藏字段,但多数不会跳过"移出视口"的。 */
?>
<p class="comment-hp" aria-hidden="true">
<label for="yang_hp"><?php esc_html_e('请勿填写此项', 'kratos'); ?></label>
<input type="text" id="yang_hp" name="yang_hp" value="" tabindex="-1" autocomplete="off">
</p>
<?php
wp_nonce_field('yang_comment_submit', 'yang_comment_nonce');
comment_id_fields();
do_action('comment_form', $yang_post_id);
?>
</form>
</div>
<?php comment_id_fields();?>
<?php do_action('comment_form', $post->ID);?>
</form>
<?php endif;?>
</div>
</div>
<?php }?>
<?php endif; ?>
<?php endif; ?>
</div>