如果你没有 PHP 和 JS/JQuery 的基础,建议不要尝试

前言

最近在写 Miracle! 主题(Miracles 主题 2.0 版本,付费),觉得 typecho 的评论嵌套一层一层套下去很难受,而且评论框还没办法直接放到评论列表前面(我之前是用 flex 布局,从 css 层面调换了两个 div 的位置)。于是,参考了几个大佬的代码,从主题层面对 Typecho 的评论进行了一些改造,让所有的子评论都嵌套在一层。这样做了之后对评论 ajax 提交又带来了一些问题,摸索了好久终于实现了想要的效果。

我花在评论功能上的时间还比较多,所以就想来写篇文章记录一下倒腾的过程,当作一篇教程吧。

改造嵌套方式

首先在主题目录下新建一个 PHP 文件,比如Comments.php,在 functions.php 中引入。

直接把内容写在 functions.php 也是可行的,但代码很长,会让 functions.php 文件变得很乱,所以我不建议这么做。

require_once('libs/Comments.php');

在新建的 Comments.php 创建一个类(Class),继承 Typecho 的 Widget_Abstract_Comments 类。

/**
 * 评论归档组件
 *
 * @category typecho
 * @package Widget
 * @copyright Copyright (c) 2008 Typecho team (http://www.typecho.org)
 * @license GNU General Public License 2.0
 */
/* 因为代码大部分来自 Typecho,所以要开源或商用的话,最好留下 Typecho 的版权信息 */

class Miracle_Comments_Archive extends Widget_Abstract_Comments
{
    //接下来的代码写在这里
}

接下来的内容几乎都是直接复制 Typecho 评论区的源代码,做了一些小修改,包括评论列表的结构也在这里定义了,请按照实际情况进行修改(通常只需要修改评论列表结构那一部分就好了),这些代码在我修改前来自 @AlanDecode@bakaomg (ohmyga)

 /**
  * 当前页
  *
  * @access private
  * @var integer
  */
 private $_currentPage;

 /**
  * 所有文章个数
  *
  * @access private
  * @var integer
  */
 private $_total = false;

 /**/
  * 子父级评论关系
  *
  * @access private
  * @var array
  */
 private $_threadedComments = array();

 /**
  * 多级评论回调函数
  * 
  * @access private
  * @var mixed
  */
 private $_customThreadedCommentsCallback = false;

 /**
  * _singleCommentOptions  
  * 
  * @var mixed
  * @access private
  */
 private $_singleCommentOptions = NULL;

 /**
  * 安全组件
  */
 private $_security = NULL;

 private $_commentAuthors = array();

 /**
  * 构造函数,初始化组件
  *
  * @access public
  * @param mixed $request request对象
  * @param mixed $response response对象
  * @param mixed $params 参数列表
  * @return void
  */
 public function __construct($request, $response, $params = NULL) {
  parent::__construct($request, $response, $params);
  $this->parameter->setDefault('parentId=0&commentPage=0&commentsNum=0&allowComment=1');

  Typecho_Widget::widget('Widget_Security')->to($this->_security);

  /** 初始化回调函数 */
  if (function_exists('threadedComments')) {
   $this->_customThreadedCommentsCallback = true;
  }
 }

 /**
  * 评论回调函数
  * 
  * @access private
  * @return void
  */
 private function threadedCommentsCallback() {
  $singleCommentOptions = $this->_singleCommentOptions;
  if (function_exists('threadedComments')) {
   return threadedComments($this, $singleCommentOptions);
  }

  $commentClass = '';
  $commentOwner = '';
  if ($this->authorId) {
   if ($this->authorId == $this->ownerId) {
    $commentClass .= ' comment-by-author';
    $commentOwner .= '<div class="comment-by-author"></div>';
   } else {
    $commentClass .= ' comment-by-user';
   }
  }else{
   $commentClass .= ' comment-by-guest';
  }

  if ($this->levels > 0) {
       $commentClass .= ' comment-child';
   } else {
       $commentClass .= ' comment-parent';
   }

  //评论结构
?>

    <div id="<?php $this->theId(); ?>" class="comments-box<?php echo $commentClass; ?>">
     <main class="comments-content-wrap">
      <div class="comments-avatar-box">
       <img class="comments-avatar" src="<?php echo Utils::gravatar($this->mail); ?>" /><?php echo $commentOwner;//这个 Utils::gravatar 方法并没有在 Typecho 中被定义,使用时请替换成其他的方法 ?>
      </div>
      <div class="comments-content">
       <div class="comments-author">
        <?php if(!empty($this->url)) {
               echo '<a href="'.$this->url.'" target="_blank">'.$this->author.'</a>';
              }else{
               echo '<span>'.$this->author.'</span>';
              } ?>
         <small class="comments-reply-info"><?php if($this->getParent()){echo $GLOBALS['lang_comment']['reply_to']; echo $this->getParent(); //这里前面的全局变量是 Miracle 主题里的语言包,请按照实际情况替换成纯文本,后面的也一样 ?> · <?php echo Utils::parseCommentDate($this->created);}//这里的 Utils::parseCommentDate 是我在主题中定义的,请替换成其他的方法(用于输出评论的日期) ?></small>
       </div>
       <div class="comments-info">
              <div class="comments-meta">
              <?php echo Utils::parseCommentDate($this->created); ?>
              <?php if ('waiting' == $this->status) { ?> <span class="comments-status"><?php if(!$this->user->hasLogin()) { echo $GLOBALS['lang_comment']['status_info']['logout']; }else{ echo $GLOBALS['lang_comment']['status_info']['login']; } ?></span><?php } ?>
              </div>
              <div class="comments-reply">
              <?php $this->reply('<button class="comments-reply-btn hint--bottom" aria-label="'.$GLOBALS['lang_comment']['reply_to'].' @'.$this->author.'">'.$GLOBALS['lang_comment']['reply_btn'].'</button>'); //这里的 aria-label 是我主题内置的一个显示 tooltip 的属性,请按照实际情况删除或替换 ?>
              </div>
              <div class="comments-text textretty">
              <?php echo Contents::parseEmo($this->content); //这个 Contents::parseEmo 方法是用于解析 owo 表情的,并非 typecho 自带,实际使用请删除或替换 ?>
              </div>
       </div>
      </div>
     </main>

<?php if ($this->children) { ?>     <div class="comment-children" itemprop="discusses">
      <?php $this->threadedComments(); ?>
     </div><?php } ?>
    </div>

<?php
 }

 /**
  * 获取被回复者
  */
 private function getParent() {
  $db = Typecho_Db::get();
  $parentID = $db->fetchRow($db->select()->from('table.comments')->where('coid = ?', $this->coid));
  $parentID = $parentID['parent'];
  if($parentID == '0') {
   return '';
  } else {
   $author=$db->fetchRow($db->select()->from('table.comments')->where('coid = ?', $parentID));
   $link = 'href="#comment-'.$author['coid'].'"';

   //如果是删除的评论
   if (!array_key_exists('author', $author) || empty($author['author'])) {
    $author['author'] = $GLOBALS['lang_comment']['deleted'];
    $link = '';
   } elseif (!empty($author['author']) && @$author['status'] == 'waiting'){
    //如果是带审核的评论
    if(!$this->user->hasLogin()) {
     $author['author'] = $GLOBALS['lang_comment']['waiting'];
     $link = '';
    }else{
     $author['author'] = $author['author'];
    }
   }

   if (!$link) {
    return '<a class="comment-reply-at" no-go>@'.$author['author'].'</a> ';
   }else{
    return '<a '.$link.' class="comment-reply-at" no-go>@'.$author['author'].'</a> ';
   }
    
  }
 }

 /**
  * 获取当前评论链接
  *
  * @access protected
  * @return string
  */
 protected function ___permalink() {
  if ($this->options->commentsPageBreak) {
   $pageRow = array('permalink' => $this->parentContent['pathinfo'], 'commentPage' => $this->_currentPage);
   return Typecho_Router::url('comment_page',
          $pageRow, $this->options->index) . '#' . $this->theId;
  }

  return $this->parentContent['permalink'] . '#' . $this->theId;
 }

 /**
  * 子评论
  *
  * @access protected
  * @return array
  */
 protected function ___children() {
  return $this->options->commentsThreaded && !$this->isTopLevel && isset($this->_threadedComments[$this->coid])
         ? $this->_threadedComments[$this->coid] : array();
 }

 /**
  * 是否到达顶层
  *
  * @access protected
  * @return boolean
  */
 protected function ___isTopLevel() {
  return $this->levels > 0;
 }

 /**
  * 重载内容获取
  *
  * @access protected
  * @return void
  */
 protected function ___parentContent() {
  return $this->parameter->parentContent;
 }

 /**
  * 输出文章评论数
  *
  * @access public
  * @param string $string 评论数格式化数据
  * @return void
  */
 public function num() {
  $args = func_get_args();
  if (!$args) {
   $args[] = '%d';
  }

  $num = intval($this->_total);
  echo sprintf(isset($args[$num]) ? $args[$num] : array_pop($args), $num);
 }

 /**
  * 执行函数
  *
  * @access public
  * @return void
  */
 public function execute() {
  if (!$this->parameter->parentId) { return; }

  $commentsAuthor = Typecho_Cookie::get('__typecho_remember_author');
  $commentsMail = Typecho_Cookie::get('__typecho_remember_mail');

  // 对已登录用户显示待审核评论,方便前台管理
  if ($this->user->hasLogin()) {
   $select = $this->select()->where('table.comments.cid = ?', $this->parameter->parentId)
             ->where('table.comments.status = ? OR table.comments.status = ?', 'approved', 'waiting');
  } else {
   $select = $this->select()->where('table.comments.cid = ?', $this->parameter->parentId)
            ->where('table.comments.status = ? OR (table.comments.author = ? AND table.comments.mail = ? AND table.comments.status = ?)', 'approved', $commentsAuthor, $commentsMail, 'waiting');
  }
  $threadedSelect = NULL;

  if ($this->options->commentsShowCommentOnly) {
   $select->where('table.comments.type = ?', 'comment');
  }

  $select->order('table.comments.coid', 'ASC');
  $this->db->fetchAll($select, array($this, 'push'));

  /** 需要输出的评论列表 */
  $outputComments = array();

  /** 如果开启评论回复 */
  if ($this->options->commentsThreaded) {
   
   foreach ($this->stack as $coid => &$comment) {
    
    /** 取出父节点 */
    $parent = $comment['parent'];

    /** 如果存在父节点 */
    if (0 != $parent && isset($this->stack[$parent])) {
     
     /** 如果当前节点深度大于最大深度, 则将其挂接在父节点上 */
     if ($comment['levels'] >= 2) {
      $comment['levels'] = $this->stack[$parent]['levels'];
      $parent = $this->stack[$parent]['parent'];     // 上上层节点
      $comment['parent'] = $parent;
     }

     /** 计算子节点顺序 */
     $comment['order'] = isset($this->_threadedComments[$parent])
                         ? count($this->_threadedComments[$parent]) + 1 : 1;
     
     /** 如果是子节点 */
     $this->_threadedComments[$parent][$coid] = $comment;
    } else {
     $outputComments[$coid] = $comment;
    }

   }
   
   $this->stack = $outputComments;
  }

  /** 评论排序 */
  if ('DESC' == $this->options->commentsOrder) {
   $this->stack = array_reverse($this->stack, true);
  }

  /** 评论总数 */
  $this->_total = count($this->stack);

  /** 对评论进行分页 */
  if ($this->options->commentsPageBreak) {
   
   if ('last' == $this->options->commentsPageDisplay && !$this->parameter->commentPage) {
    $this->_currentPage = ceil($this->_total / $this->options->commentsPageSize);
   } else {
    $this->_currentPage = $this->parameter->commentPage ? $this->parameter->commentPage : 1;
   }

   /** 截取评论 */
   $this->stack = array_slice($this->stack,
                  ($this->_currentPage - 1) * $this->options->commentsPageSize, $this->options->commentsPageSize);

   /** 评论置位 */
   $this->row = current($this->stack);
   $this->length = count($this->stack);
  }

  reset($this->stack);
 }

 /**
  * 将每行的值压入堆栈
  *
  * @access public
  * @param array $value 每行的值
  * @return array
  */
 public function push(array $value) {
  $value = $this->filter($value);

  /** 计算深度 */
  if (0 != $value['parent'] && isset($this->stack[$value['parent']]['levels'])) {
   $value['levels'] = $this->stack[$value['parent']]['levels'] + 1;
  } else {
   $value['levels'] = 0;
  }

  $value['realParent'] = $value['parent'];

  /** 重载push函数,使用coid作为数组键值,便于索引 */
  $this->stack[$value['coid']] = $value;
  $this->_commentAuthors[$value['coid']] = $value['author'];
  $this->length ++;
        
  return $value;
 }
 
 /**
  * 输出分页
  *
  * @access public
  * @param string $prev 上一页文字
  * @param string $next 下一页文字
  * @param int $splitPage 分割范围
  * @param string $splitWord 分割字符
  * @param string $template 展现配置信息
  * @return void
  */
 public function pageNav($prev = '&laquo;', $next = '&raquo;', $splitPage = 3, $splitWord = '...', $template = '') {
  if ($this->options->commentsPageBreak && $this->_total > $this->options->commentsPageSize) {
   $default = array(
    'wrapTag'    =>  'ol',
    'wrapClass'  =>  'page-navigator'
   );

   if (is_string($template)) {
    parse_str($template, $config);
   } else {
    $config = $template;
   }

   $template = array_merge($default, $config);

   $pageRow = $this->parameter->parentContent;
   $pageRow['permalink'] = $pageRow['pathinfo'];

   $query = Typecho_Router::url('comment_page', $pageRow, $this->options->index);

   /** 使用盒状分页 */
   $nav = new Typecho_Widget_Helper_PageNavigator_Box($this->_total,
          $this->_currentPage, $this->options->commentsPageSize, $query);

   $nav->setPageHolder('commentPage');
   $nav->setAnchor('comments');

   echo '<' . $template['wrapTag'] . (empty($template['wrapClass']) 
        ? '' : ' class="' . $template['wrapClass'] . '"') . '>';
        $nav->render($prev, $next, $splitPage, $splitWord, $template);
   echo '</' . $template['wrapTag'] . '>';
  }
 }

 /**
  * 递归输出评论
  *
  * @access protected
  * @return void
  */
 public function threadedComments() {
  $children = $this->children;

  if ($children) {
   //缓存变量便于还原
   $tmp = $this->row;
   $this->sequence ++;

   //在子评论之前输出
   echo $this->_singleCommentOptions->before;

   foreach ($children as $child) {
    $this->row = $child;
    $this->threadedCommentsCallback();
    $this->row = $tmp;
   }

   //在子评论之后输出
   echo $this->_singleCommentOptions->after;

   $this->sequence --;
  }
 }

 /**
  * 列出评论
  * 
  * @access private
  * @param mixed $singleCommentOptions 单个评论自定义选项
  * @return void
  */
 public function listComments($singleCommentOptions = NULL) {
  //初始化一些变量
  $this->_singleCommentOptions = Typecho_Config::factory($singleCommentOptions);
  $this->_singleCommentOptions->setDefault(array(
    'before'        =>  '<ol class="comment-list">',
    'after'         =>  '</ol>',
    'beforeAuthor'  =>  '',
    'afterAuthor'   =>  '',
    'beforeDate'    =>  '',
    'afterDate'     =>  '',
    'dateFormat'    =>  $this->options->commentDateFormat,
    'replyWord'     =>  _t('回复'),
    'commentStatus' =>  _t('您的评论正等待审核!'),
    'avatarSize'    =>  32,
    'defaultAvatar' =>  NULL
  ));

  $this->pluginHandle()->trigger($plugged)->listComments($this->_singleCommentOptions, $this);

  if (!$plugged) {
   if ($this->have()) { 
    echo $this->_singleCommentOptions->before;
            
    while ($this->next()) {
     $this->threadedCommentsCallback();
    }
            
    echo $this->_singleCommentOptions->after;
   }
  }
 }

 /**
  * 重载alt函数,以适应多级评论
  * 
  * @access public
  * @return void
  */
 public function alt() {
  $args = func_get_args();
  $num = func_num_args();
        
  $sequence = $this->levels <= 0 ? $this->sequence : $this->order;
        
  $split = $sequence % $num;
  echo $args[(0 == $split ? $num : $split) -1];
 }

 /**
  * 根据深度余数输出
  *
  * @access public
  * @param string $param 需要输出的值
  * @return void
  */
 public function levelsAlt() {
  $args = func_get_args();
  $num = func_num_args();
  $split = $this->levels % $num;
  echo $args[(0 == $split ? $num : $split) -1];
 }
    
 /**
  * 评论回复链接
  * 
  * @access public
  * @param string $word 回复链接文字
  * @return void
 */
 public function reply($word = '') {
  if ($this->options->commentsThreaded && $this->parameter->allowComment) {
   $word = empty($word) ? _t('回复') : $word;
   $this->pluginHandle()->trigger($plugged)->reply($word, $this);
            
   if (!$plugged) {
    echo '<a href="' . substr($this->permalink, 0, - strlen($this->theId) - 1) . '?replyTo=' . $this->coid .
         '#' . $this->parameter->respondId . '" rel="nofollow" onclick="return TypechoComment.reply(\'' .
         $this->theId . '\', ' . $this->coid . ');" no-pjax>' . $word . '</a>';
   }
  }
 }
    
 /**
  * 取消评论回复链接
  * 
  * @access public
  * @param string $word 取消回复链接文字
  * @return void
  */
 public function cancelReply($word = '') {
  if ($this->options->commentsThreaded) {
   $word = empty($word) ? _t('取消回复') : $word;
   $this->pluginHandle()->trigger($plugged)->cancelReply($word, $this);
            
   if (!$plugged) {
    $replyId = $this->request->filter('int')->replyTo;
    echo '<a id="cancel-comment-reply-link" href="' . $this->parameter->parentContent['permalink'] . '#' . $this->parameter->respondId .
         '" rel="nofollow"' . ($replyId ? '' : ' style="display:none"') . ' onclick="return TypechoComment.cancelReply();">' . $word . '</a>';
   }
  }
 }
}

之后就是要对 comments.php 文件进行改造了,除了表单部分不变,把其他的都删掉,在文件开头这样写。

//这个 Miracle_Comments_Archive 是你刚才新建的那个类名
$this->widget('Miracle_Comments_Archive', array(
   'parentId'      => $this->hidden ? 0 : $this->cid,
   'parentContent' => $this->row,
   'respondId'     => $this->respondId,
   'commentPage'   => $this->request->filter('int')->commentPage,
   'allowComment'  => $this->allow('comment')
))->to($comments);

之后就是常规的,判断是否允许评论、评论表单、评论分页之类的东西,在这里就不讨论了,不同的是,在用 $comments->listComments() 输出评论列表的时候,可以把它放在评论表单后面了。

Ajax 提交评论

搭配 ajax 能够在不刷新页面的情况下就提交评论,有助于提升用户体验。有许多大佬都写过类似的教程,只不过修改了评论嵌套方式之后情况稍微特殊了一点,虽然也只是在插入新评论的时候要稍微变通一下,但我还是打算把整个过程都写一遍。

下面的代码需要搭配 jQuery 实现,并作为 js 文件在前端引用

绑定「回复」「取消回复」事件

例子:

function bindButton() {
    $(".comments-reply a").click(function () {
            replyTo = $(this).parent().parent().parent().parent().parent().attr("id");
            console.log(replyTo);
        });
    $(".cancel-comment-reply a").click(function () { replyTo = ''; });
}
bindButton();

这里的选择器需要根据主题情况进行修改,这里的 .comments-reply a.cancel-comment-reply a 指的分别是「回复评论」的 a 标签和「取消回复」的 a 标签。

好像把 Miracle! 的评论区暴露了呢 2333

另外,获取 replyTo(回复评论的 id)时有.parent().parent().parent().parent().parent().attr("id")这样的结构,是在通过这个被点击的.comments-reply a获取到被回复评论的 id,需要根据主题实际情况修改

以 Miracle! 的评论结构为例

还要注意的是,之所以我在写这一段代码的时候要把它封装成函数,是为了兼容 pjax,在 pjax 完成页面切换之后需要在回调函数里再次调用这个函数,重新绑定按钮,避免切换页面后 ajax 评论失效。

$(document).pjax('a[href^="'+siteurl+'"]:not(...)', {container: '#pjax-container', fragment: '#pjax-container', timeout: 8000}
).on('pjax:send', function() {
    beforePjax();
}).on('pjax:complete', function() {
    //...
    bindButton();
    //...
});

before() & after()

先定义两个函数,分别是评论提交前后执行的代码,需要注意的是,这个 before 指的是 ajax 发送但还没有得到回复的时候,after 指的是完成了评论提交、插入、处理等所有的操作。这里再给 after() 传入一个叫做 ok 的参数,用来作为评论是否发送成功的标志。

var comment = {}

comment.before = function(){
    //先禁用评论表单
    $("#comment-form input,#comment-form textarea").attr('disabled', true).css('cursor', 'not-allowed');
    //... 这之后可以写入一些动画之类的
}

comment.after = function(ok){
    //先取消对表单的禁用
    $("#comment-form input,#comment-form textarea").attr('disabled', false).css('cursor', 'pointer');
    if(ok){
        //如果发送成功
        $("#textarea").val('');//清空评论框
        replyTo = '';//清空回复 id
    }
    //...
}

监听评论表单 submit 事件(核心)

这里是最核心的部分,大概是这样:

function commentCore() {
 $('#comment-form').submit(function() {
    //监听评论表单submit事件
    var commentData = $(this).serializeArray(); //获取表单POST的数据
    $.ajax({
        type: $(this).attr('method'),
        url: $(this).attr('action'),
        data: commentData,
        error: function(e) {
            //发送 ajax 失败的处理
        },
        success: function(data) {
            //发送成功的处理
   });
}
commentCore();

error 这一部分比较简单,不是很重要,发送一个提示框告诉用户评论失败就好,这里就不赘述了。

最主要的就是 success 的部分,这个 success 不代表评论发送成功,而是 ajax 请求发送成功,所以这里还是要考虑到 Typecho 反垃圾、信息提交不完整等情况导致的评论失败。

if (!$('#comments', data).length) {//通过传过来的 data 是否包含评论区 html 判断是否成功
    var msg = $('title').eq(0).text().trim().toLowerCase() === 'error' ? $('.container', data).eq(0).text() : '评论提交失败!';//获取评论失败的提示信息
    notyf.error(msg);//提示用户失败,notyf.error 可以改成 alert 之类其他的方法,这个方法不是原生的
    comment.after(false);//评论结束,传入 false 表示评论失败
    return false
}

接下来就是对成功发送后的处理了,我们先需要获取新评论的 id

//把传来的数据压入一个新建的 body 标签里,作为一个 dom 元素储存在变量里
var htmlData = $(document.createElement('body')).append(data);
if (htmlData.html()) {
    //如果 htmlData 存在,获取 id
    newCommentId = htmlData.html().match(/id=\"?comment-\d+/g).join().match(/\d+/g).sort(function (a, b) { return a - b }).pop();
}else{
    //如果不存在,提示错误
    notyf.error('获取评论 ID 时发生错误,请尝试刷新');
    return false;
}

然后,通过 replyTo 变量是否有内容判断这个评论是父级评论还是子级评论

因为之前修改了评论结构,所以这个时候只有直接评论的评论才被算作是父级评论,其他回复父级评论或其他子级评论的评论都是子级评论(有点绕口?

var newComment; //先定义一个全局变量,等会用来储存新评论的 html 结构
if(''===replyTo) {
    //处理父级评论
}
else {
    //处理子级评论
}

处理父级评论有三种情况:

  1. 没有评论列表结构(没有评论)
  2. 不在评论列表第一页
  3. 在第一页,且有评论列表结构

实现代码如下:

if(!$('.comments-list').length) {
    //如果没有评论列表的结构
    //简单粗暴,从返回的数据中找到评论列表的结构,这里需要根据主题的实际情况修改选择器
    //.comment-box 是评论表单的容器
    //.comment-list-body 是评论列表的容器
    $('.comment-box').after($(htmlData)[0].querySelector('.comment-list-body'));
}
else if($('.prev').length) {
    //如果不在第一页,直接模拟点击分页导航的第一个 a 标签,跳转到第一页
    //这里不用压入最新评论,因为跳转之后通过 pjax 或刷新就能得到最新的评论列表
    $('.comments-pagenav li a').eq(1).click();
}else{
    //如果在第一页且拥有评论列表的基础结构
    //获取新评论的 html
    newComment  = $("#comment-" + newCommentId, data);
    //然后将新评论压入评论列表
    $('.comments-list').first().prepend(newComment);
}

然后处理子级评论,这里就有些不同了,总共分四种情况

  1. 如果回复的对象是父级评论

    1. 如果父级评论已有评论列表结构(.comment-children)
    2. 如果父级评论没有评论列表结构
  2. 如果回复的对象是子级评论(对应的父级评论也有评论列表结构)

实际代码如下:

newComment = $("#comment-" + newCommentId, data);
if($('#' + replyTo).hasClass('comment-parent')){
    //如果回复的对象是父级评论
    if ($('#' + replyTo).find('.comment-children').length) {
        //当前父评论已经有嵌套的结构
        //向对应的评论列表插入新评论
        $('#' + replyTo + ' .comment-children .comment-list').first().prepend(newComment);
        TypechoComment.cancelReply();
    }
    else {
        //当前父评论没有嵌套的结构
        //先插入嵌套的评论列表结构(根据主题实际情况)
        $('#' + replyTo).append('<div class="comment-children"><div class="comments-list"></div></div>');
        //插入新评论
        $('#' + replyTo + ' .comment-children .comments-list').first().prepend(newComment);
        TypechoComment.cancelReply();
    }
}else{
    //如果回复的对象是子级评论
    //直接插入在对应的子级评论之后
    $('#' + replyTo).after(newComment);
}

兼容评论反垃圾

Typecho 自带有反垃圾评论的功能(后他-设置-评论-反垃圾),但这不兼容 ajax 评论提交,于是很多人会选择强制关闭反垃圾,但这又可能会造成垃圾评论满天飞的奇观。这里可以用一段 js 代码来兼容 Typecho 的评论反垃圾,中间会用到一些变量,所以建议写在 php 文件里。

找到刚才的 Comments.php 在 Class 的最后加入这一段代码。

/**
 * 评论反垃圾
 *
 * @access public
 */
public static function AntiSpam($comment) {
    echo '<!--<nocompress>--><script>(function(){var a=document.addEventListener?{add:"addEventListener",focus:"focus",load:"DOMContentLoaded"}:{add:"attachEvent",focus:"onfocus",load:"onload"};var c,d,e,f,b=document.getElementById("'.$comment->respondId.'");null!=b&&(c=b.getElementsByTagName("form"),c.length>0&&(d=c[0],e=d.getElementsByTagName("textarea")[0],f=!1,null!=e&&"text"==e.name&&e[a.add](a.focus,function(){if(!f){var a=document.createElement("input");a.type="hidden",a.name="_",d.appendChild(a),f=!0,a.value='.Typecho_Common::shuffleScriptVar($comment->security->getToken($comment->request->getRequestUrl())).'}})))})();</script><!--</nocompress>-->';
}

然后回到 comments.php,在评论表单的最后插入

<?php if($this->options->commentsAntiSpam) Miracle_Comments_Archive::AntiSpam($this); ?>
//这个 Miracle_Comments_Archive 改成你的类名

解决评论框不跟随

按下回复按钮的时候,评论框通常会直接移动到对应的评论的,如果没有,可能是少了一段神奇的 js。

function replyScript($archive) {
    if ($archive->allow('comment')) echo "<!--<nocompress>--><script type=\"text/javascript\">(function(){window.TypechoComment={dom:function(id){return document.getElementById(id)},create:function(tag,attr){var el=document.createElement(tag);for(var key in attr){el.setAttribute(key,attr[key])}return el},reply:function(cid,coid){var comment=this.dom(cid),parent=comment.parentNode,response=this.dom('$archive->respondId'),input=this.dom('comment-parent'),form='form'==response.tagName?response:response.getElementsByTagName('form')[0],textarea=response.getElementsByTagName('textarea')[0];if(null==input){input=this.create('input',{'type':'hidden','name':'parent','id':'comment-parent'});form.appendChild(input)}input.setAttribute('value',coid);if(null==this.dom('comment-form-place-holder')){var holder=this.create('div',{'id':'comment-form-place-holder'});response.parentNode.insertBefore(holder,response)}comment.appendChild(response);this.dom('cancel-comment-reply-link').style.display='';if(null!=textarea&&'text'==textarea.name){textarea.focus()}return false},cancelReply:function(){var response=this.dom('$archive->respondId'),holder=this.dom('comment-form-place-holder'),input=this.dom('comment-parent');if(null!=input){input.parentNode.removeChild(input)}if(null==holder){return true}this.dom('cancel-comment-reply-link').style.display='none';holder.parentNode.insertBefore(response,holder);return false}}})();</script><!--</nocompress>-->";
}

在评论区的最开始插入:

<?php replyScript(); ?>

最后

对 Typecho 评论的改造就到这里了,这里全都是在主题层面上进行加工,希望能对你有所帮助。

Miracle! 付费版正在内测,目前演示站已经出来了,第一个测试版也已经完工了。如果想要加入内测可以联系我(QQ: 1415757672),内测版价格 40 元(早些时候还没有成型,所以算的是 30,现在进入内测是 40),进入内测可以先拿到测试版,能看到主题开发的进展。当然如果不放心也可以等正式版,价格也不会差太多。

参考

  • VOID 主题 | AlanDecode
  • Castle 主题 | ohmyga
  • Aria 主题 | Siphils