Files
wordpress-kratos-yang/inc/theme-comments.php
T
laoyang 460abcdb97 3.2.19: 修复关闭 wpDiscuz 后评论列表整段不显示
现象:#5264 等文章标题「评论(4)」与输入框都在,但 <ol> 内 0 字节。
只有单页评论的文章必现,多页文章反而正常。

根因:3.2.18 给 wp_list_comments() 传了 page=1 / per_page=10,
进入核心的"独立查询"分支。当这两个值与核心自己推导的
current_cpage / comments_per_page 恰好相等时,内层 if 不成立,
$_comments 一次都没被赋值(源码注释说会回退到 $wp_query->comments,
但那句注释下面没有回退代码),结果 paged_walk(null) 返回空串。

修复:
- 新增 yang_comment_page():按顶层评论分页 + get_children() 展平子孙。
  按顶层分页可避免父子被切到两页产生孤儿回复。
- comments.php 把结果当第二参数显式传给 wp_list_comments,
  走 null !== $comments 分支,绕开有问题的推导。

连带修掉的三处:
- Walker_Comment::end_el() 总会补 </li>,callback 再写一个导致
  </li></li>,子容器被挤成父评论的兄弟节点、CSS 栅格失效。
  yang_comment_html() 新增 $close_li,列表路径交给 walker 闭合。
- 核心 start_lvl() 固定输出 class="children",而样式表按
  .yang-children 写,类名不符导致缩进/栅格全失效。
  新增 Yang_Walker_Comment 覆盖 start_lvl()。
- AJAX「加载更多」原先只取顶层、只渲染顶层,吞掉全部子回复
  (实测某篇 10 条顶层名下 12 条回复丢失)。改为走同一 walker,
  has_more 也改成与首屏同口径的顶层计数。
2026-10-24 08:23:00 +08:00

879 lines
35 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
/**
* 评论区:统一渲染 + AJAX 提交 + 评论通知
*
* ── 为什么要重写 ──────────────────────────────────────────────
* 旧实现有两个根本问题,都不是"不好看"能概括的:
*
* 1. **同一份评论渲染了两遍,而且已经分叉**。
* `comment_callbacks()`(列表渲染)里有「回复」按钮,
* `comment_callback()`(AJAX 提交后返回的新评论)里**没有** ——
* 于是通过 AJAX 刚发出来的那条评论,页面上根本没有回复入口,
* 刷新一次才会出现(因为刷新走的是列表渲染)。
* 这类 bug 的根因不是漏写,是"两个地方各写一份 HTML"。
* 现在两边都调 `yang_comment_html()`,物理上不可能再分叉。
*
* 2. **列表容器是 <div>,里面直接放 <li>**。
* `comments.php` 原来写的是 `<div class="list">` + `wp_list_comments()`,
* 后者输出的是 `<li>`。`<li>` 的父元素必须是 <ul>/<ol>,
* 否则浏览器会自己"猜"结构,不同浏览器猜法不同,
* 嵌套回复(<ul class="children">)在某些浏览器里会错位。
*
* ── AJAX 设计的三条约束 ──────────────────────────────────────
* 1. **不用 layer 弹提示**。layer 是个通用弹窗库,用来显示"评论提交成功"
* 太重(遮罩、动画、层级都多余),而且它失败时会把整个提示吞掉。
* 这里用轻量的内联状态条,并直接显示后端返回的**人话**错误。
* 2. **必须防重复提交**。旧实现 beforeSend 只弹了个"提交中",
* 按钮仍可点,网络慢时用户会连点,产生多条重复评论。
* 3. **后端错误要可读**。WordPress 的评论错误是 WP_Error,
* 旧代码把 message 直接扔给前端,有些情况下是空串或英文提示。
* 这里统一映射成中文,并区分"表单错误"(可修正)与"系统错误"。
*
* ── 与 wpDiscuz 的关系 ───────────────────────────────────────
* 这个文件是**替代** wpDiscuz 的实现,不是与它共存。装了插件时
* 插件会接管评论表单,主题这套不生效(也不该生效,两套会打架)。
* 卸载插件后,主题这套自动接管,且评论通知不依赖插件 ——
* 见本文件末尾的通知部分。
*
* @author Yongquan Yang <yangyq.net>
* @license MIT License
*/
if (!defined('ABSPATH')) {
exit;
}
/**
* wpDiscuz 是否在接管评论
*
* 用途:卸载插件后要自动切到主题通知,不能依赖插件还活着。
* 只看类/函数是否存在,不做 is_plugin_active() —— 那需要
* 在后台上下文才有 plugin.php,前台调用会 Fatal。
*/
function yang_comments_plugin_active()
{
return class_exists('WpdiscuzCore') || function_exists('wpdiscuz');
}
/**
* 渲染单条评论的 HTML
*
* **列表渲染与 AJAX 返回共用这一个函数**。这是本次重写的核心 ——
* 只要两处都调它,就不可能再出现"AJAX 出来的评论少个按钮"。
*
* @param WP_Comment $comment 评论对象
* @param array $args wp_list_comments 传入的参数(深度等)
* @param int $depth 当前深度
* @param bool $is_ajax 是否是被 AJAX 返回的新评论
* @param bool $close_li 是否在末尾闭合 </li>
* —— 走 wp_list_comments 时必须传 false,
* 否则子评论会被挤到 </li> 外面(详见下方注释)
* @return string
*/
function yang_comment_html($comment, $args = array(), $depth = 1, $is_ajax = false, $close_li = true)
{
if (!($comment instanceof WP_Comment)) {
return '';
}
$comment_id = (int) $comment->comment_ID;
$max_depth = isset($args['max_depth']) ? (int) $args['max_depth'] : 0;
$depth = max(1, (int) $depth);
// 作者能否编辑(管理员/编辑在每条上看到「编辑」入口)
$can_edit = current_user_can('edit_comment', $comment_id);
// 评论者是不是文章作者(给个标记,便于区分)
$post = get_post($comment->comment_post_ID);
$is_post_author = $post && ((int) $post->post_author === (int) $comment->user_id) && $comment->user_id > 0;
$author_name = $comment->comment_author !== ''
? $comment->comment_author
: __('匿名访客', 'kratos');
/* 评论内容:comment_text() 会走 WordPress 的完整过滤链
(smilies、wpautop、kese、@提及高亮都在那上面挂着),
所以必须用它,而不是 $comment->comment_content。 */
ob_start();
comment_text($comment_id);
$content = ob_get_clean();
$avatar = '';
if (get_option('show_avatars') && function_exists('get_avatar')) {
$avatar = get_avatar($comment, 48, '', $author_name, array(
'class' => 'yang-comment-avatar-img',
'extra_attr' => 'loading="lazy"',
));
}
$classes = array('yang-comment');
if ($is_ajax) {
$classes[] = 'is-new';
}
if ($comment->comment_approved === '0') {
$classes[] = 'is-pending';
}
$o = array();
$o[] = '<li id="comment-' . $comment_id . '" class="' . esc_attr(implode(' ', $classes)) . '"'
. ' data-comment-id="' . $comment_id . '"'
. ' data-depth="' . $depth . '">';
// ── 头像列 ──
$o[] = '<div class="yang-comment-avatar">';
if ($avatar !== '') {
$o[] = $avatar;
} else {
// 没头像时给个占位,别让左侧塌掉导致文字贴边
$o[] = '<span class="yang-comment-avatar-ph" aria-hidden="true">'
. esc_html(mb_substr($author_name, 0, 1, 'UTF-8'))
. '</span>';
}
$o[] = '</div>';
// ── 主体 ──
$o[] = '<div class="yang-comment-main">';
$o[] = '<div class="yang-comment-head">';
$o[] = '<cite class="yang-comment-author">' . get_comment_author_link($comment) . '</cite>';
if ($is_post_author) {
$o[] = '<span class="yang-comment-badge">' . __('作者', 'kratos') . '</span>';
}
if ($comment->comment_approved === '0') {
$o[] = '<span class="yang-comment-badge is-pending">' . __('待审核', 'kratos') . '</span>';
}
$o[] = '<time class="yang-comment-time" datetime="'
. esc_attr(mysql2date('c', $comment->comment_date)) . '">'
. esc_html(mysql2date('Y年m月d日 H:i', $comment->comment_date))
. '</time>';
$o[] = '</div>'; // head
// 被回复者的提示(@某人),由 comment_add_at 过滤器加到正文里,
// 这里不再重复,避免出现两次
$o[] = '<div class="yang-comment-content">' . $content . '</div>';
// ── 操作区 ──
$o[] = '<div class="yang-comment-actions">';
/* 回复按钮:可回复的前提有三个 ——
① 评论还开着;② 没超过主题设置的嵌套层数;③ 不是待审核的。 */
$can_reply = comments_open($comment->comment_post_ID);
if ($max_depth > 0 && $depth >= $max_depth) {
$can_reply = false;
}
if ($comment->comment_approved === '0' && !$can_edit) {
$can_reply = false;
}
if ($can_reply) {
$o[] = '<button type="button" class="yang-comment-reply"'
. ' data-comment-id="' . $comment_id . '"'
. ' data-author="' . esc_attr($author_name) . '">'
. '<i class="kicon i-reply" aria-hidden="true"></i>'
. '<span>' . __('回复', 'kratos') . '</span>'
. '</button>';
}
if ($can_edit) {
$o[] = '<a class="yang-comment-edit" href="' . esc_url(get_edit_comment_link($comment_id)) . '">'
. __('编辑', 'kratos') . '</a>';
}
$o[] = '</div>'; // actions
$o[] = '</div>'; // main
/* 闭合 </li> 交给调用方决定。
wp_list_comments 的 walker 会在 start_el 之后、start_lvl 之前
插入子评论容器,所以走列表渲染时这条 </li> **必须先不闭合**,
否则子评论会被挤到 </li> 外面,变成父评论的兄弟节点 ——
HTML 会变成 </li></li><ol class="children"> 这种畸形结构,
CSS 里 "子评论占满整行" 的规则就完全不生效。
AJAX 单独拼 HTML 时没有 walker,才需要自己闭合。 */
if ($close_li) {
$o[] = '</li>';
}
return implode('', $o);
}
/**
* 评论列表的 walker
*
* 只覆盖两处,其余沿用 Walker_Comment:
* 1. 子评论容器类名用 `yang-children`(CSS 按这个类名写缩进与栅格)。
* 核心固定输出 `class="children"`,样式对不上。
* 2. 配合 `comment_callbacks()` 不再自己闭合 </li>,
* 让核心的 end_el 去闭合 —— 这样 <ol> 才落在 <li> 内部。
*/
class Yang_Walker_Comment extends Walker_Comment
{
public function start_lvl(&$output, $depth = 0, $args = array())
{
$GLOBALS['comment_depth'] = $depth + 1;
$output .= '<ol class="yang-children">' . "\n";
}
}
/**
* wp_list_comments 的回调(列表渲染)
*
* 只是把参数转交给统一渲染函数。保留这个函数名是因为
* comments.php 里引用了它,且子主题可能覆盖过。
*
* 注意末尾传 false:不在这里闭合 </li>,交给 walker 的 end_el。
* 原因见 yang_comment_html() 里的注释。
*/
function comment_callbacks($comment, $args = array(), $depth = 1)
{
echo yang_comment_html($comment, $args, $depth, false, false);
}
/**
* 把 WordPress 的评论错误翻译成人话
*
* ── 为什么需要这层 ────────────────────────────────────────────
* `wp_handle_comment_submission()` 返回的 WP_Error,message 有时是
* 英文(插件/核心的默认文案)、有时是空串、有时是带 HTML 的。
* 直接扔到前端会显示成 "ERROR: please type a comment",或者一片空白
* —— 用户看到空白只会以为"点了没反应",然后继续点。
*
* 这里做三件事:给空 message 兜底、按 code 覆盖成中文、
* 剥掉残留的 HTML 标签。
*/
function yang_comment_error_text($error)
{
if (!is_wp_error($error)) {
return __('提交失败,请稍后重试。', 'kratos');
}
$code = $error->get_error_code();
$msg = trim(wp_strip_all_tags((string) $error->get_error_message()));
// 按错误码给确定的中文文案(比核心的英文默认值友好)
$map = array(
'require_name_email' => __('昵称和邮箱是必填的。', 'kratos'),
'comment_author_email_invalid' => __('邮箱格式不对,请检查一下。', 'kratos'),
'comment_author_url_invalid' => __('网址格式不对,请检查一下。', 'kratos'),
'comment_id_not_found' => __('这篇文章不存在,无法评论。', 'kratos'),
'comment_closed' => __('这篇文章已关闭评论。', 'kratos'),
'comment_on_trash' => __('这篇文章在回收站里,无法评论。', 'kratos'),
'comment_on_draft' => __('草稿状态的文章无法评论。', 'kratos'),
'comment_on_password_protected' => __('这是密码保护的文章,需要先输入密码。', 'kratos'),
'comment_duplicate' => __('你刚才已经发过一模一样的评论了,请勿重复提交。', 'kratos'),
'comment_flood' => __('发得太快了,歇一会儿再发吧。', 'kratos'),
'not_logged_in' => __('需要登录后才能评论。', 'kratos'),
'unapproved_comment' => __('你的评论正在等待审核。', 'kratos'),
);
if (isset($map[$code])) {
return $map[$code];
}
// 没映射到的:有 message 就用(剥掉 HTML),没有就给通用兜底
if ($msg !== '') {
return $msg;
}
return __('提交失败,请稍后重试。', 'kratos');
}
/**
* AJAX 提交评论
*
* ── 返回格式 ──────────────────────────────────────────────────
* 统一 JSON:
* 成功 { ok: true, html: '<li>…</li>', comment_id, depth,
* count, count_text, parent_id, message }
* 失败 { ok: false, message, code, field }
*
* `field` 指出是哪个字段的问题(author/email/comment),
* 前端据此把焦点送回去 —— 比只弹一句"昵称必填"有用。
*
* ── 兼容旧的 action 名 ───────────────────────────────────────
* 旧版前端用的 action 是 `ajax_comment`。新前端也用同一个名字,
* 这样浏览器缓存了旧 JS 的站点在过渡期也不会 404。
*/
function yang_ajax_submit_comment()
{
// 评论必须走 nonce 校验:没有它,第三方页面可以借访客身份刷评论
$nonce = isset($_POST['yang_comment_nonce']) ? $_POST['yang_comment_nonce'] : '';
if (!wp_verify_nonce($nonce, 'yang_comment_submit')) {
wp_send_json(array(
'ok' => false,
'code' => 'bad_nonce',
'message' => __('页面已过期,请刷新后重新提交。', 'kratos'),
), 403);
}
$comment = wp_handle_comment_submission(wp_unslash($_POST));
if (is_wp_error($comment)) {
$data = $comment->get_error_data();
// 403 = 被判定为垃圾/需要登录等;422 = 表单填写问题
$status = 400;
if (isset($data['status'])) {
$status = (int) $data['status'];
}
wp_send_json(array(
'ok' => false,
'code' => $comment->get_error_code(),
'field' => yang_comment_error_field($comment->get_error_code()),
'message' => yang_comment_error_text($comment),
), $status > 0 ? $status : 400);
}
// 写入评论者 cookie,这样他下次来名字邮箱还在
$user = wp_get_current_user();
do_action('set_comment_cookies', $comment, $user);
$post_id = (int) $comment->comment_post_ID;
$parent_id = (int) $comment->comment_parent;
/* 深度:wp_list_comments 的 depth 从 1 开始。
父评论的深度 + 1 就是新评论的深度;顶层评论是 1。 */
$depth = 1;
if ($parent_id > 0) {
$parent = get_comment($parent_id);
if ($parent) {
// 用已有的 wp_list_comments 深度逻辑:数祖先层数 + 1
$ancestors = get_comment_ancestors($parent);
$depth = count($ancestors) + 2;
}
}
$max_depth = (int) get_option('thread_comments_depth', 5);
if ($max_depth <= 0) {
$max_depth = 5;
}
$args = array(
'max_depth' => $max_depth,
'style' => 'ul',
'short_ping' => true,
'avatar_size' => 48,
);
$html = yang_comment_html($comment, $args, $depth, true);
// 评论数:只数已通过的(待审核不该算进"N 条评论")
$count = (int) get_comments_number($post_id);
wp_send_json(array(
'ok' => true,
'html' => $html,
'comment_id' => (int) $comment->comment_ID,
'depth' => $depth,
'max_depth' => $max_depth,
'parent_id' => $parent_id,
'count' => $count,
'count_text' => yang_comment_count_text($count),
'approved' => ($comment->comment_approved !== '0'),
'message' => ($comment->comment_approved === '0')
? __('评论已提交,正在等待审核。', 'kratos')
: __('评论发表成功。', 'kratos'),
));
}
add_action('wp_ajax_nopriv_ajax_comment', 'yang_ajax_submit_comment');
add_action('wp_ajax_ajax_comment', 'yang_ajax_submit_comment');
/**
* 取某个评论的所有祖先 id(从近到远)
*
* WordPress 没有现成函数,但它自己有 wp_list_comments 内部用的逻辑。
* 这里自己走一遍 parent 链,顺便防环(脏数据有可能会造成死循环)。
*/
function get_comment_ancestors($comment, $limit = 20)
{
$out = array();
$seen = array((int) $comment->comment_ID => true);
$cur = (int) $comment->comment_parent;
$i = 0;
while ($cur > 0 && $i < $limit) {
if (isset($seen[$cur])) {
break; // 环,直接停
}
$seen[$cur] = true;
$out[] = $cur;
$c = get_comment($cur);
if (!$c) {
break;
}
$cur = (int) $c->comment_parent;
$i++;
}
return $out;
}
/**
* 错误码 → 出问题的字段(前端用来聚焦)
*/
function yang_comment_error_field($code)
{
if (strpos($code, 'author') !== false || $code === 'require_name_email') {
return 'author';
}
if (strpos($code, 'email') !== false) {
return 'email';
}
if (strpos($code, 'url') !== false) {
return 'url';
}
if ($code === 'comment_duplicate' || $code === 'comment_flood') {
return 'comment';
}
return '';
}
/**
* 评论数文案(与 single.php 里 comments_number() 的风格保持一致)
*
* 单独抽出来是因为 AJAX 返回后前端要就地更新"N 条评论",
* 不能为了改个数字重新渲染整页。
*/
function yang_comment_count_text($n)
{
$n = (int) $n;
if ($n === 0) {
return __('0 条评论', 'kratos');
}
if ($n === 1) {
return __('1 条评论', 'kratos');
}
return sprintf(__('%s 条评论', 'kratos'), number_format_i18n($n));
}
/**
* 取「第 N 页评论」:按**顶层评论**分页,再把每条的子孙展开成平铺数组
*
* ── 为什么不直接把 page/per_page 交给 wp_list_comments ──────
* WP 那个分支有缺陷:当 `page`/`per_page` 与它自己从查询变量推导出的
* 当前页完全相等时,内层 `if` 不成立,`$_comments` 就**一次都没被赋值**
* (源码里那句"fall back on $wp_query->comments"的注释下面并没有回退代码),
* 最终 `paged_walk(null, ...)` 返回空字符串。表现就是:
* 「评论(N)」标题在、输入框在,列表一条不显示。
* 单页评论的文章(max_num_comment_pages=1)必然命中,多页的反而侥幸正常 ——
* 这也是它为什么看起来"时好时坏"。
*
* 所以取数由这里负责,`wp_list_comments()` 的第 2 个参数直接收数组,
* 走它的 `null !== $comments` 分支,不碰那段有问题的推导。
*
* ── 为什么按顶层分页 ────────────────────────────────────────
* 分页若按"行"算,一条顶层评论和它的 10 条回复可能被切到两页,
* 第 2 页会出现一堆没有父评论的孤儿回复。按顶层 + 整体展开,
* 父子永远同页。
*
* @param int $post_id 文章 ID
* @param int $page 第几页(从 1 开始)
* @param int $per_page 每页多少条顶层评论;0 = 不分页,全取
* @return WP_Comment[] 平铺后的评论(父在子前,可直接交给 wp_list_comments)
*/
function yang_comment_page($post_id, $page = 1, $per_page = 0)
{
$post_id = (int) $post_id;
if ($post_id <= 0) {
return array();
}
$order = strtoupper((string) get_option('comment_order', 'asc')) === 'DESC' ? 'DESC' : 'ASC';
$args = array(
'post_id' => $post_id,
'status' => 'approve',
'orderby' => 'comment_date_gmt',
'order' => $order,
'parent' => 0,
'hierarchical' => 'threaded',
);
if ($per_page > 0) {
$args['number'] = (int) $per_page;
$args['offset'] = max(0, ((int) $page - 1) * (int) $per_page);
}
$top = get_comments($args);
if (!$top) {
return array();
}
/* 展开子孙。用 get_children() 而不是读 $comment->children ——
后者是 protected,而且要先经 hierarchical='threaded' 才会被填充。 */
$flat = array();
foreach ($top as $c) {
$flat[] = $c;
$kids = $c->get_children(array(
'format' => 'flat',
'status' => 'approve',
'orderby' => 'comment_date_gmt',
'order' => $order,
));
foreach ($kids as $kid) {
$flat[] = $kid;
}
}
return $flat;
}
/**
* 「加载更多评论」AJAX
*
* 旧模板里 `previous_comments_link(__('加载更多'))` 是个普通链接,
* 点了整页跳转(还跳到评论分页 URL)。这里改成原地加载。
*
* 取数与首屏共用 `yang_comment_page()`:以前这里自己写了一份只取顶层、
* 只渲染顶层的查询,导致点「加载更多」出来的评论**全部没有子回复**
* (实测某篇 10 条顶层名下有 12 条回复被吞掉)。
*/
function yang_ajax_load_comments()
{
$post_id = isset($_POST['post_id']) ? (int) $_POST['post_id'] : 0;
$page = isset($_POST['page']) ? max(2, (int) $_POST['page']) : 2;
if ($post_id <= 0 || !get_post($post_id)) {
wp_send_json(array('ok' => false, 'message' => __('参数错误。', 'kratos')), 400);
}
$per_page = (int) get_option('comments_per_page', 20);
if ($per_page <= 0) {
$per_page = 20;
}
$comments = yang_comment_page($post_id, $page, $per_page);
$max_depth = (int) get_option('thread_comments_depth', 5);
$args = array(
'max_depth' => $max_depth > 0 ? $max_depth : 5,
'style' => 'ul',
'short_ping' => true,
'avatar_size' => 48,
);
/* 渲染走 wp_list_comments + 同一个 walker,和首屏完全同一条路径。
以前这里是 foreach 手动拼 yang_comment_html(),没有 walker,
于是子回复被平铺成父评论的兄弟节点、层级信息全丢
—— 实测某篇 10 条顶层名下的 12 条回复结构全错。 */
ob_start();
wp_list_comments(array(
'type' => 'comment',
'callback' => 'comment_callbacks',
'walker' => new Yang_Walker_Comment(),
'style' => 'ol',
'max_depth' => $max_depth > 0 ? $max_depth : 5,
'avatar_size' => 48,
'per_page' => 0,
'page' => 0,
), $comments);
$html = ob_get_clean();
// 分页按顶层算,所以"还有没有更多"也要按顶层比,不能拿总评论数比
$top_total = (int) get_comments(array(
'post_id' => $post_id,
'status' => 'approve',
'parent' => 0,
'count' => true,
));
$loaded_top = $page * $per_page;
$total = (int) get_comments_number($post_id);
wp_send_json(array(
'ok' => true,
'html' => $html,
'page' => $page,
'has_more' => $loaded_top < $top_total,
'next_page' => $page + 1,
'loaded' => $loaded_top,
'total' => $total,
));
}
add_action('wp_ajax_nopriv_yang_load_comments', 'yang_ajax_load_comments');
add_action('wp_ajax_yang_load_comments', 'yang_ajax_load_comments');
/**
* 前端需要的配置,localize 给 JS
*
* ── 样式为什么也在这里加载 ────────────────────────────────
* 评论只在「单页/页面 + 评论开启」时才出现,没必要全站加载一份
* 10KB 的评论样式。放这里与 JS 同一个条件,一起加载一起不加载。
*/
function yang_comment_scripts()
{
if (is_admin()) {
return;
}
// 只在会渲染评论的页面加载(与 comments_template() 的触发条件一致)
if (!is_singular()) {
return;
}
/* wpDiscuz 在场时**不加载**主题这套脚本与样式。
*
* 原因:插件会接管评论表单(换成它自己的 #wpdcom),并绑定自己的
* 提交逻辑。若主题的 comments.js 也照常加载,它会在
* document 上挂 click 侦听去认 `.yang-comment-reply`,
* 还会找 `#commentform` 去绑 submit —— 找得到就形成"两套一起提交",
* 找不到就静默什么都不做。两种都不对。
*
* 同理 CSS:插件的评论样式自成体系,主题的 comments.css 用的是
* .yang-comment-* 前缀不会撞,但会给插件不存在的元素加规则,
* 白白增加一份体积与排查噪音。
*
* 注意:这只影响"装了插件还没卸载"的过渡期。卸载后自动接管。 */
if (yang_comments_plugin_active()) {
return;
}
wp_enqueue_style(
'yang-comments',
ASSET_PATH . '/assets/css/comments.css',
array('kratos'),
THEME_VERSION
);
wp_enqueue_script(
'yang-comments',
ASSET_PATH . '/assets/js/comments.js',
array(),
THEME_VERSION,
true
);
wp_localize_script('yang-comments', 'yangComment', array(
'ajaxUrl' => admin_url('admin-ajax.php'),
'nonce' => wp_create_nonce('yang_comment_submit'),
'postId' => get_the_ID() ? (int) get_the_ID() : 0,
'order' => get_option('comment_order', 'asc'),
'maxDepth' => (int) get_option('thread_comments_depth', 5),
'i18n' => array(
'submitting' => __('提交中…', 'kratos'),
'submit' => __('提交评论', 'kratos'),
'replyTo' => __('回复', 'kratos'),
'cancelReply' => __('取消回复', 'kratos'),
'networkErr' => __('网络异常,请检查网络后重试。', 'kratos'),
'emptyComment' => __('评论内容不能为空。', 'kratos'),
'tooLong' => __('评论内容太长了。', 'kratos'),
'loading' => __('加载中…', 'kratos'),
'noMore' => __('没有更多评论了', 'kratos'),
),
));
}
add_action('wp_enqueue_scripts', 'yang_comment_scripts', 21);
/**
* 兼容旧函数名
*
* inc/theme-article.php 里原来有一个同名函数并在 wp_enqueue_scripts
* 上注册。那个已经删掉,但子主题/缓存里的旧代码可能还在调它,
* 保留一个空实现避免 Fatal。
*
* 注意:**这里刻意不重复注册钩子**。两个都注册会导致评论样式与脚本
* 各加载两遍(浏览器去重但 wp_localize 会输出两份配置对象)。
*/
if (!function_exists('comment_scripts')) {
function comment_scripts()
{
// 由 yang_comment_scripts() 接管,这里保持空实现仅为兼容
}
}
/* ══════════════════════════════════════════════════════════════
* 评论通知(不依赖 wpDiscuz)
*
* ── 与 theme-mail.php 的分工 ─────────────────────────────────
* theme-mail.php 负责"通道"(SMTP、去重、格式),本文件负责
* "什么时候该发、发给谁"。分开是因为通道层要能独立工作 ——
* 即使评论通知全关掉,测试邮件也得能发出去。
*
* ── 卸载 wpDiscuz 后的行为 ──────────────────────────────────
* 插件在时:通知由插件发(带订阅/退订),主题这边全部让开。
* 插件不在时:主题接管,发两类邮件 ——
* · 有人回复了你的评论 → 给被回复者
* · 文章有新评论 → 给文章作者(可关)
* 这个切换靠 yang_comments_plugin_active() 判断,自动完成,
* 用户不需要改任何设置。
* ══════════════════════════════════════════════════════════════ */
/**
* 有新评论时,通知被回复的那个人
*
* 只在**回复**场景发($comment->comment_parent > 0)。
* 顶层新评论通知文章作者,走的是 WordPress 核心的 notify_postauthor,
* 不需要在这里重复一遍 —— 否则又是"一条评论两封邮件"。
*
* ── 为什么不用 comment_post 而用 wp_insert_comment ──────────
* comment_post 在评论写入后立刻触发,此时 comment_approved 已就位;
* wp_insert_comment 更底层,会连后台手动插入也触发。
* 这里要的是"访客提交"这一种,所以用 comment_post,并显式检查
* 是否是待审核(待审核的评论不该通知被回复者去看一条不存在的回复)。
*/
function yang_notify_comment_reply($comment_id, $comment_approved = 1)
{
if (!yang_mail_smtp_ready()) {
return;
}
// 插件在就让它发,别抢
if (yang_comments_plugin_active()) {
return;
}
// 主题选项里允许整体关掉
if (!kratos_option('m_comment_notify', true)) {
return;
}
$comment = get_comment($comment_id);
if (!$comment) {
return;
}
$parent_id = (int) $comment->comment_parent;
if ($parent_id <= 0) {
return; // 顶层评论:交给核心的 notify_postauthor
}
$parent = get_comment($parent_id);
if (!$parent) {
return;
}
// 被回复者没留邮箱就发不了;自己回复自己也不发
$to = trim((string) $parent->comment_author_email);
if ($to === '' || !is_email($to)) {
return;
}
$new_mail = trim((string) $comment->comment_author_email);
if ($new_mail !== '' && strtolower($new_mail) === strtolower($to)) {
return;
}
// 同一个收件人对同一条评论只发一次(AJAX 重试、插件重复触发都能挡住)
$key = 'yang_creply_' . $comment_id . '_' . md5($to);
if (get_transient($key)) {
return;
}
set_transient($key, 1, 300);
$post = get_post($comment->comment_post_ID);
$title = $post ? get_the_title($post) : __('(文章已删除)', 'kratos');
$link = get_comment_link($comment);
$author = $comment->comment_author !== '' ? $comment->comment_author : __('匿名访客', 'kratos');
$quotes = array(
array(
'label' => sprintf(__('你之前说的(%s)', 'kratos'), $parent->comment_author),
'content' => yang_mail_text($parent->comment_content, 1200),
'meta' => mysql2date('Y-m-d H:i', $parent->comment_date),
),
array(
'label' => sprintf(__('%s 回复说', 'kratos'), $author),
'content' => yang_mail_text($comment->comment_content, 1200),
'meta' => mysql2date('Y-m-d H:i', $comment->comment_date),
),
);
$body = yang_mail_render(array(
'title' => sprintf(__('%s · 新回复', 'kratos'), get_bloginfo('name')),
'preheader' => sprintf(__('%s 在《%s》下回复了你的评论。', 'kratos'), $author, $title),
'badge' => __('新回复', 'kratos'),
'heading' => __('有人回复了你的评论', 'kratos'),
'intro' => sprintf(__('你在《%s》下的评论收到了一条回复。', 'kratos'), $title),
'quotes' => $quotes,
'rows' => array(
array('label' => __('文章', 'kratos'), 'value' => $title),
array('label' => __('评论者', 'kratos'), 'value' => $author, 'strong' => true),
array('label' => __('时间', 'kratos'), 'value' => mysql2date('Y-m-d H:i', $comment->comment_date)),
),
'cta_text' => __('查看完整讨论', 'kratos'),
'cta_url' => $link,
'note' => __('你收到这封邮件,是因为有人回复了你的评论。', 'kratos'),
));
// 注意:这里**不做** yang_mail_escape_for_wpdiscuz() 的预编码。
// 那个函数是为了抵消 wpDiscuz 自己多 decode 一次;现在没有插件了,
// 预编码反而会让邮件正文里出现字面的 &amp;lt; —— 是实打实的 bug。
wp_mail(
$to,
sprintf(__('[%s] 有人回复了你的评论', 'kratos'), get_bloginfo('name')),
$body
);
}
add_action('comment_post', 'yang_notify_comment_reply', 20, 2);
/**
* 评论通过审核时通知评论者
*
* 与上面同理:插件在就让它发,不在就主题发。
* 这个通知很有用 —— 访客留了评论却因为审核没看到,
* 不告诉他,他只会以为没提交成功。
*/
function yang_notify_comment_approved($comment)
{
if (!yang_mail_smtp_ready() || yang_comments_plugin_active()) {
return;
}
if (!kratos_option('m_comment_approved_notify', true)) {
return;
}
if (!($comment instanceof WP_Comment)) {
return;
}
$to = trim((string) $comment->comment_author_email);
if ($to === '' || !is_email($to)) {
return;
}
$key = 'yang_cappr_' . (int) $comment->comment_ID;
if (get_transient($key)) {
return;
}
set_transient($key, 1, 300);
$post = get_post($comment->comment_post_ID);
$title = $post ? get_the_title($post) : __('(文章已删除)', 'kratos');
$body = yang_mail_render(array(
'title' => sprintf(__('%s · 评论已通过审核', 'kratos'), get_bloginfo('name')),
'preheader' => sprintf(__('你在《%s》下的评论已通过审核并公开显示。', 'kratos'), $title),
'badge' => __('已通过', 'kratos'),
'heading' => __('你的评论已通过审核', 'kratos'),
'intro' => sprintf(__('你在《%s》下提交的评论已经审核通过,现在其他读者可以看到它了。', 'kratos'), $title),
'quotes' => array(
array(
'label' => __('你说过的', 'kratos'),
'content' => yang_mail_text($comment->comment_content, 1200),
'meta' => mysql2date('Y-m-d H:i', $comment->comment_date),
),
),
'rows' => array(
array('label' => __('文章', 'kratos'), 'value' => $title),
),
'cta_text' => __('去看看', 'kratos'),
'cta_url' => get_comment_link($comment),
'note' => __('这是评论审核结果通知,无需回复。', 'kratos'),
));
wp_mail(
$to,
sprintf(__('[%s] 你的评论已通过审核', 'kratos'), get_bloginfo('name')),
$body
);
}
add_action('comment_unapproved_to_approved', 'yang_notify_comment_approved', 20, 1);
/**
* 评论表单的字段顺序与占位符调整
*
* WordPress 默认顺序是 comment → author → email → url,
* 而主题的视觉设计是「昵称/邮箱/网址在上,内容在下」。
* 原主题用 recover_comment_fields() 把 comment 挪到最后,
* 这里保留同样的顺序,但改成可预测的实现。
*/
function yang_comment_form_fields($fields)
{
if (!isset($fields['comment'])) {
return $fields;
}
$comment = $fields['comment'];
unset($fields['comment']);
$fields['comment'] = $comment;
return $fields;
}
add_filter('comment_form_fields', 'yang_comment_form_fields', 20);