Files
RuiPing/Plugin.php

2267 lines
72 KiB
PHP
Raw Normal View History

2026-02-23 19:55:05 +08:00
<?php
/**
* 有意思、有启发、有温度评论集锦
*
* @package RuiPing
* @author 石头厝
* @version 2.5.1
* @link https://www.shitoucuo.com
*/
class RuiPing_Plugin implements Typecho_Plugin_Interface
{
/**
* 激活插件
*/
public static function activate()
{
Typecho_Plugin::factory('Widget_Archive')->header = array('RuiPing_Plugin', 'header');
Typecho_Plugin::factory('Widget_Archive')->footer = array('RuiPing_Plugin', 'footer');
// 在后台页面添加代码
Typecho_Plugin::factory('admin/header.php')->header = array('RuiPing_Plugin', 'addAdminHeader');
Typecho_Plugin::factory('admin/footer.php')->end = array('RuiPing_Plugin', 'addAdminFooter');
// 使用Typecho标准的方式处理AJAX
Typecho_Plugin::factory('Widget_Archive')->handleInit = array('RuiPing_Plugin', 'handleInit');
// 添加前端评论标识钩子
Typecho_Plugin::factory('Widget_Archive')->beforeRender = array('RuiPing_Plugin', 'beforeRender');
return _t('锐评插件已激活');
}
/**
* 禁用插件
*/
public static function deactivate()
{
return _t('锐评插件已禁用');
}
/**
* 插件配置面板
*/
public static function config(Typecho_Widget_Helper_Form $form)
{
// 锐评ID设置
$commentIds = new Typecho_Widget_Helper_Form_Element_Textarea('commentIds',
NULL,
'',
_t('锐评ID'),
_t('每行一个评论ID不限制数量。可以在评论管理页面找到评论ID'));
$form->addInput($commentIds);
// 每页显示数量
$limit = new Typecho_Widget_Helper_Form_Element_Text('limit',
NULL,
'10',
_t('每页数量'),
_t('每页显示的评论数量,超过此数量将分页显示'));
$form->addInput($limit);
// 显示样式设置
$showAuthor = new Typecho_Widget_Helper_Form_Element_Radio('showAuthor',
array(
'1' => _t('显示'),
'0' => _t('不显示')
),
'1',
_t('显示评论人'));
$form->addInput($showAuthor);
$showDate = new Typecho_Widget_Helper_Form_Element_Radio('showDate',
array(
'1' => _t('显示'),
'0' => _t('不显示')
),
'1',
_t('显示评论时间'));
$form->addInput($showDate);
$showContent = new Typecho_Widget_Helper_Form_Element_Radio('showContent',
array(
'1' => _t('显示'),
'0' => _t('不显示')
),
'1',
_t('显示评论内容'));
$form->addInput($showContent);
$showSource = new Typecho_Widget_Helper_Form_Element_Radio('showSource',
array(
'1' => _t('显示'),
'0' => _t('不显示')
),
'1',
_t('显示来源文章'));
$form->addInput($showSource);
// 新增:显示原文设置
$showOriginal = new Typecho_Widget_Helper_Form_Element_Radio('showOriginal',
array(
'1' => _t('显示'),
'0' => _t('不显示')
),
'1',
_t('显示原文链接'));
$form->addInput($showOriginal);
// 链接设置
$authorLink = new Typecho_Widget_Helper_Form_Element_Radio('authorLink',
array(
'1' => _t('新窗口打开'),
'0' => _t('当前窗口打开')
),
'1',
_t('作者链接打开方式'));
$form->addInput($authorLink);
$commentLink = new Typecho_Widget_Helper_Form_Element_Radio('commentLink',
array(
'1' => _t('新窗口打开'),
'0' => _t('当前窗口打开')
),
'1',
_t('评论链接打开方式'));
$form->addInput($commentLink);
// 分页设置
$showPagination = new Typecho_Widget_Helper_Form_Element_Radio('showPagination',
array(
'1' => _t('显示'),
'0' => _t('不显示')
),
'1',
_t('显示分页'));
$form->addInput($showPagination);
// 调试模式
$debugMode = new Typecho_Widget_Helper_Form_Element_Radio('debugMode',
array(
'1' => _t('开启'),
'0' => _t('关闭')
),
'0',
_t('调试模式'),
_t('开启后显示调试信息,用于排查问题'));
$form->addInput($debugMode);
// 新增:前端锐评标识设置
$showFrontendBadge = new Typecho_Widget_Helper_Form_Element_Radio('showFrontendBadge',
array(
'1' => _t('显示'),
'0' => _t('不显示')
),
'1',
_t('前端锐评标识'),
_t('在文章评论区中为锐评评论显示标识'));
$form->addInput($showFrontendBadge);
$frontendBadgePosition = new Typecho_Widget_Helper_Form_Element_Select('frontendBadgePosition',
array(
'content-top-right' => _t('内容右上角'),
'content-top-left' => _t('内容左上角'),
'comment-top-right' => _t('评论整体右上角'),
'after-author' => _t('作者名后')
),
'content-top-right',
_t('标识位置'));
$form->addInput($frontendBadgePosition);
$frontendBadgeText = new Typecho_Widget_Helper_Form_Element_Text('frontendBadgeText',
NULL,
'❤ 锐评',
_t('标识文字'));
$form->addInput($frontendBadgeText);
$frontendBadgeColor = new Typecho_Widget_Helper_Form_Element_Text('frontendBadgeColor',
NULL,
'#d32f2f',
_t('标识颜色'));
$form->addInput($frontendBadgeColor);
// 备份和恢复设置
echo '<div class="typecho-page-title">
<h2>锐评插件 - 设置备份与恢复</h2>
</div>';
// 备份设置
$backupBtn = new Typecho_Widget_Helper_Form_Element_Submit('backupBtn', NULL, _t('备份当前设置'),
_t('将当前插件设置备份到服务器'));
$backupBtn->input->setAttribute('class', 'btn primary');
$backupBtn->input->setAttribute('onclick', 'heartfeltBackupSettings()');
$form->addItem($backupBtn);
// 恢复设置
$restoreBtn = new Typecho_Widget_Helper_Form_Element_Submit('restoreBtn', NULL, _t('从备份恢复'),
_t('从最近的备份恢复插件设置'));
$restoreBtn->input->setAttribute('class', 'btn');
$restoreBtn->input->setAttribute('onclick', 'heartfeltRestoreSettings()');
$form->addItem($restoreBtn);
// 下载备份
$downloadBtn = new Typecho_Widget_Helper_Form_Element_Submit('downloadBtn', NULL, _t('下载备份文件'),
_t('下载当前设置的备份文件'));
$downloadBtn->input->setAttribute('class', 'btn');
$downloadBtn->input->setAttribute('onclick', 'heartfeltDownloadBackup()');
$form->addItem($downloadBtn);
// 上传备份
$uploadArea = new Typecho_Widget_Helper_Form_Element_Textarea('uploadBackup', NULL, '',
_t('上传备份文件'),
_t('粘贴备份文件内容或上传备份文件进行恢复'));
$form->addInput($uploadArea);
// 添加JavaScript
self::addBackupScript();
}
/**
* 个人用户配置面板
*/
public static function personalConfig(Typecho_Widget_Helper_Form $form) {}
/**
* 处理初始化
*/
public static function handleInit()
{
// 检查是否是我们的AJAX请求
if (isset($_GET['action']) && $_GET['action'] == 'heartfelt-comments') {
self::handleAjax();
exit;
}
// 处理获取锐评ID列表的请求前端使用
if (isset($_GET['action']) && $_GET['action'] == 'ruiping-get-ids') {
self::getHeartfeltIds();
exit;
}
// 处理备份相关的AJAX请求
if (isset($_POST['heartfelt_backup_action'])) {
self::handleBackupAjax();
exit;
}
// 处理下载备份请求
if (isset($_GET['heartfelt_backup_action']) && $_GET['heartfelt_backup_action'] == 'download') {
self::downloadBackup();
exit;
}
// 处理批量操作请求
if (isset($_POST['heartfelt_batch_action'])) {
self::handleBatchAction();
exit;
}
// 处理锐评筛选页面
if (isset($_GET['heartfelt_filter'])) {
self::handleHeartfeltFilter();
exit;
}
}
/**
* 获取锐评ID列表前端使用
*/
private static function getHeartfeltIds()
{
header('Content-Type: application/json; charset=utf-8');
try {
$options = Typecho_Widget::widget('Widget_Options');
$config = $options->plugin('RuiPing');
if (!$config) {
echo json_encode(array('success' => true, 'ids' => array()));
exit;
}
$ids = self::getCommentIds($config->commentIds);
echo json_encode(array(
'success' => true,
'ids' => $ids,
'showFrontendBadge' => isset($config->showFrontendBadge) ? $config->showFrontendBadge : '1',
'badgeText' => isset($config->frontendBadgeText) ? $config->frontendBadgeText : '❤ 锐评',
'badgePosition' => isset($config->frontendBadgePosition) ? $config->frontendBadgePosition : 'content-top-right',
'badgeColor' => isset($config->frontendBadgeColor) ? $config->frontendBadgeColor : '#d32f2f',
// 新增:跳转链接和新窗口配置
'badgeLink' => 'https://www.shitoucuo.com/rp.html', // 固定跳转链接
'openInNewWindow' => true // 固定新窗口打开
));
} catch (Exception $e) {
echo json_encode(array(
'success' => false,
'message' => $e->getMessage(),
'ids' => array()
));
}
exit;
}
/**
* 页面渲染前执行(添加前端标识)
*/
public static function beforeRender()
{
$options = Typecho_Widget::widget('Widget_Options');
$config = $options->plugin('RuiPing');
// 检查是否启用前端标识
if (!$config || (isset($config->showFrontendBadge) && $config->showFrontendBadge == '0')) {
return;
}
// 只在前台文章页面添加
if (Typecho_Widget::widget('Widget_Archive')->is('single')) {
// 添加JavaScript到页面
$ajaxUrl = Typecho_Common::url('index.php?action=ruiping-get-ids', $options->siteUrl);
$badgeText = isset($config->frontendBadgeText) ? htmlspecialchars($config->frontendBadgeText) : '❤ 锐评';
$badgePosition = isset($config->frontendBadgePosition) ? $config->frontendBadgePosition : 'content-top-right';
$badgeColor = isset($config->frontendBadgeColor) ? $config->frontendBadgeColor : '#d32f2f';
$badgeLink = 'https://www.shitoucuo.com/rp.html'; // 固定跳转链接
$openInNewWindow = true; // 固定新窗口打开
$js = <<<JS
<script>
(function() {
// 等待页面加载完成
document.addEventListener('DOMContentLoaded', function() {
// 获取锐评ID列表
fetch('{$ajaxUrl}')
.then(response => response.json())
.then(data => {
if (data.success && data.ids && data.ids.length > 0) {
// 标记锐评评论
markHeartfeltComments(data.ids, {
badgeText: data.badgeText || '{$badgeText}',
badgePosition: data.badgePosition || '{$badgePosition}',
badgeColor: data.badgeColor || '{$badgeColor}',
badgeLink: data.badgeLink || '{$badgeLink}',
openInNewWindow: data.openInNewWindow || {$openInNewWindow}
});
}
})
.catch(error => {
console.log('锐评插件获取ID失败', error);
});
});
/**
* 标记锐评评论
* @param {Array} heartfeltIds 锐评ID数组
* @param {Object} options 配置选项
*/
function markHeartfeltComments(heartfeltIds, options) {
if (!heartfeltIds || heartfeltIds.length === 0) return;
heartfeltIds = heartfeltIds.map(id => parseInt(id));
// 查找所有评论项
const commentItems = document.querySelectorAll('li[id^="comment-"], li.comment-body, .comment-body');
commentItems.forEach(item => {
// 从ID或数据属性中提取评论ID
let commentId = null;
// 尝试从id属性获取
const idAttr = item.getAttribute('id');
if (idAttr) {
const match = idAttr.match(/comment-(\d+)/);
if (match && match[1]) {
commentId = parseInt(match[1]);
}
}
// 如果找到了评论ID且是锐评添加标识
if (commentId && heartfeltIds.includes(commentId)) {
addHeartfeltBadge(item, options, commentId);
}
});
}
/**
* 添加锐评标识
* @param {HTMLElement} commentElement 评论元素
* @param {Object} options 配置选项
* @param {Number} commentId 评论ID
*/
function addHeartfeltBadge(commentElement, options, commentId) {
// 检查是否已经添加过标识
if (document.querySelector('.heartfelt-frontend-badge[data-comment-id="' + commentId + '"]')) {
return;
}
// 根据位置选择目标元素
const badgePosition = options.badgePosition || 'content-top-right';
let targetElement = null;
let isContentTarget = false;
if (badgePosition.includes('content')) {
// 找评论内容容器 - 针对你的评论模板
const contentElements = commentElement.querySelectorAll('.comment-content, .cm');
if (contentElements.length > 0) {
targetElement = contentElements[0];
isContentTarget = true;
} else {
// 如果没有找到特定的内容容器,使用整个评论元素
targetElement = commentElement;
}
} else {
targetElement = commentElement;
}
// 创建标识容器
const badge = document.createElement('span');
badge.className = 'heartfelt-frontend-badge';
badge.setAttribute('data-comment-id', commentId);
// 判断是否有链接如果有则创建a标签否则直接显示文本
if (options.badgeLink) {
// 创建可点击的链接
const link = document.createElement('a');
link.href = options.badgeLink;
if (options.openInNewWindow) {
link.target = '_blank';
link.rel = 'noopener noreferrer';
}
link.textContent = options.badgeText || '❤ 锐评';
link.style.color = '#fff';
link.style.textDecoration = 'none'; // 移除下划线装饰
link.style.display = 'block';
link.title = '点击可以跳转锐评汇总页面喔'; // 添加title提示
badge.appendChild(link);
badge.style.cursor = 'pointer'; // 显示手型光标
} else {
// 没有链接,直接显示文本
badge.textContent = options.badgeText || '❤ 锐评';
badge.style.cursor = 'default';
}
badge.style.backgroundColor = options.badgeColor || '#d32f2f';
// 设置标识位置
switch (badgePosition) {
case 'content-top-right':
badge.style.position = 'absolute';
badge.style.top = '8px';
badge.style.right = '8px';
badge.style.zIndex = '100';
// 确保目标元素有相对定位
if (window.getComputedStyle(targetElement).position === 'static') {
targetElement.style.position = 'relative';
}
// 添加一些内边距避免标识覆盖内容
const currentPadding = window.getComputedStyle(targetElement).paddingRight;
if (currentPadding && currentPadding !== '0px') {
const paddingValue = parseFloat(currentPadding);
targetElement.style.paddingRight = (paddingValue + 50) + 'px';
} else {
targetElement.style.paddingRight = '50px';
}
break;
case 'content-top-left':
badge.style.position = 'absolute';
badge.style.top = '8px';
badge.style.left = '8px';
badge.style.zIndex = '100';
if (window.getComputedStyle(targetElement).position === 'static') {
targetElement.style.position = 'relative';
}
const currentPaddingLeft = window.getComputedStyle(targetElement).paddingLeft;
if (currentPaddingLeft && currentPaddingLeft !== '0px') {
const paddingValue = parseFloat(currentPaddingLeft);
targetElement.style.paddingLeft = (paddingValue + 50) + 'px';
} else {
targetElement.style.paddingLeft = '50px';
}
break;
case 'comment-top-right':
badge.style.position = 'absolute';
badge.style.top = '10px';
badge.style.right = '10px';
badge.style.zIndex = '100';
if (window.getComputedStyle(commentElement).position === 'static') {
commentElement.style.position = 'relative';
}
break;
case 'after-author':
// 找到作者名元素,添加到后面
const authorElements = commentElement.querySelectorAll('.author, .author-name, .comment-author, .auhui .author-name');
if (authorElements.length > 0) {
targetElement = authorElements[0];
badge.style.marginLeft = '8px';
badge.style.display = 'inline-block';
badge.style.verticalAlign = 'middle';
} else {
badge.style.position = 'relative';
badge.style.display = 'inline-block';
badge.style.marginBottom = '5px';
}
break;
}
// 通用样式
badge.style.color = '#fff';
badge.style.padding = '3px 8px';
badge.style.borderRadius = '4px';
badge.style.fontSize = '12px';
badge.style.fontWeight = '600';
badge.style.lineHeight = '1.2';
badge.style.boxShadow = '0 2px 5px rgba(0,0,0,0.2)';
badge.style.transition = 'all 0.3s ease';
badge.style.textShadow = '1px 1px 1px rgba(0,0,0,0.2)';
badge.style.whiteSpace = 'nowrap';
// 添加悬停效果
badge.addEventListener('mouseenter', function() {
this.style.transform = 'scale(1.05)';
this.style.boxShadow = '0 4px 10px rgba(0,0,0,0.3)';
});
badge.addEventListener('mouseleave', function() {
this.style.transform = 'scale(1)';
this.style.boxShadow = '0 2px 5px rgba(0,0,0,0.2)';
});
// 添加到元素
targetElement.appendChild(badge);
// 添加淡入动画
badge.style.opacity = '0';
badge.style.transform = 'translateY(-5px)';
setTimeout(() => {
badge.style.transition = 'opacity 0.3s ease, transform 0.3s ease';
badge.style.opacity = '1';
badge.style.transform = 'translateY(0)';
}, 10);
}
})();
</script>
JS;
// 输出JavaScript
echo $js;
}
}
/**
* 处理锐评筛选
*/
public static function handleHeartfeltFilter()
{
$filter = isset($_GET['heartfelt_filter']) ? $_GET['heartfelt_filter'] : 'all';
if ($filter == 'heartfelt') {
// 重定向到锐评专用页面
$options = Typecho_Widget::widget('Widget_Options');
$config = $options->plugin('RuiPing');
if ($config) {
$commentIds = self::getCommentIds($config->commentIds);
if (!empty($commentIds)) {
// 构建评论ID参数
$coidParams = '';
foreach ($commentIds as $id) {
$coidParams .= '&coid[]=' . $id;
}
$redirectUrl = Typecho_Common::url('manage-comments.php', $options->adminUrl) . '?' . $coidParams;
header('Location: ' . $redirectUrl);
exit;
} else {
// 如果没有锐评,显示提示信息
$options = Typecho_Widget::widget('Widget_Options');
$redirectUrl = Typecho_Common::url('manage-comments.php', $options->adminUrl);
header('Location: ' . $redirectUrl);
exit;
}
}
}
// 默认返回评论管理页面
$options = Typecho_Widget::widget('Widget_Options');
$redirectUrl = Typecho_Common::url('manage-comments.php', $options->adminUrl);
header('Location: ' . $redirectUrl);
exit;
}
/**
* 处理批量操作
*/
public static function handleBatchAction()
{
// 验证用户权限
$user = Typecho_Widget::widget('Widget_User');
if (!$user->hasLogin() || !$user->pass('administrator', true)) {
self::jsonResponse(false, '权限不足');
}
$action = $_POST['heartfelt_batch_action'];
$commentIds = isset($_POST['comment_ids']) ? $_POST['comment_ids'] : array();
if (empty($commentIds)) {
self::jsonResponse(false, '请选择要操作的评论');
}
try {
$db = Typecho_Db::get();
// 获取当前插件配置
$options = Typecho_Widget::widget('Widget_Options');
$config = $options->plugin('RuiPing');
if (!$config) {
$config = new stdClass();
$config->commentIds = '';
}
$currentIds = self::getCommentIds($config->commentIds);
switch ($action) {
case 'add':
foreach ($commentIds as $commentId) {
$commentId = intval($commentId);
if ($commentId > 0 && !in_array($commentId, $currentIds)) {
$currentIds[] = $commentId;
}
}
$message = '已批量设置为锐评';
break;
case 'remove':
$currentIds = array_filter($currentIds, function($id) use ($commentIds) {
return !in_array($id, $commentIds);
});
$message = '已批量取消锐评';
break;
default:
self::jsonResponse(false, '未知操作');
return;
}
// 更新配置
$newValue = implode("\n", $currentIds);
$existingConfig = $db->fetchRow($db->select()->from('table.options')->where('name = ?', 'plugin:RuiPing'));
if ($existingConfig) {
$existingConfigArray = unserialize($existingConfig['value']);
$existingConfigArray['commentIds'] = $newValue;
$updateResult = $db->query($db->update('table.options')
->rows(array('value' => serialize($existingConfigArray)))
->where('name = ?', 'plugin:RuiPing'));
}
self::jsonResponse(true, $message, array(
'action' => $action,
'count' => count($commentIds)
));
} catch (Exception $e) {
self::jsonResponse(false, '操作失败: ' . $e->getMessage());
}
}
/**
* 处理备份相关的AJAX请求
*/
public static function handleBackupAjax()
{
// 验证用户权限
$user = Typecho_Widget::widget('Widget_User');
if (!$user->hasLogin() || !$user->pass('administrator', true)) {
self::jsonResponse(false, '权限不足');
}
$action = $_POST['heartfelt_backup_action'];
switch ($action) {
case 'backup':
self::backupSettings();
break;
case 'restore':
self::restoreSettings();
break;
case 'upload':
self::uploadBackup();
break;
default:
self::jsonResponse(false, '未知操作');
}
}
/**
* 备份设置
*/
private static function backupSettings()
{
try {
$db = Typecho_Db::get();
// 获取当前插件设置
$config = $db->fetchRow($db->select()->from('table.options')
->where('name = ?', 'plugin:RuiPing'));
if (!$config) {
self::jsonResponse(false, '没有找到插件设置');
return;
}
// 创建备份
$backupData = array(
'plugin_config' => $config['value'],
'backup_time' => time(),
'backup_version' => '2.5.1',
'site_url' => Helper::options()->siteUrl
);
$backupContent = serialize($backupData);
// 使用更短的名字避免字段长度限制
$backupName = 'heartfelt_bak_' . date('Ymd_His');
// 保存备份到数据库
$existingBackup = $db->fetchRow($db->select()->from('table.options')
->where('name = ?', $backupName));
if ($existingBackup) {
$db->query($db->update('table.options')
->rows(array('value' => $backupContent))
->where('name = ?', $backupName));
} else {
$db->query($db->insert('table.options')
->rows(array(
'name' => $backupName,
'user' => '0',
'value' => $backupContent
)));
}
// 清理旧的备份只保留最近的5个
self::cleanOldBackups();
self::jsonResponse(true, '设置备份成功', array('backup_name' => $backupName));
} catch (Exception $e) {
self::jsonResponse(false, '备份失败: ' . $e->getMessage());
}
}
/**
* 恢复设置
*/
private static function restoreSettings()
{
try {
$db = Typecho_Db::get();
// 获取最新的备份
$backups = $db->fetchAll($db->select()->from('table.options')
->where('name LIKE ?', 'heartfelt_bak_%')
->order('name', Typecho_Db::SORT_DESC)
->limit(1));
if (empty($backups)) {
self::jsonResponse(false, '没有找到备份文件');
return;
}
$backup = $backups[0];
$backupData = unserialize($backup['value']);
if (!$backupData || !isset($backupData['plugin_config'])) {
self::jsonResponse(false, '备份文件格式错误');
return;
}
// 恢复插件设置
$existingConfig = $db->fetchRow($db->select()->from('table.options')
->where('name = ?', 'plugin:RuiPing'));
if ($existingConfig) {
$db->query($db->update('table.options')
->rows(array('value' => $backupData['plugin_config']))
->where('name = ?', 'plugin:RuiPing'));
} else {
$db->query($db->insert('table.options')
->rows(array(
'name' => 'plugin:RuiPing',
'user' => '0',
'value' => $backupData['plugin_config']
)));
}
self::jsonResponse(true, '设置恢复成功', array(
'backup_time' => date('Y-m-d H:i:s', $backupData['backup_time'])
));
} catch (Exception $e) {
self::jsonResponse(false, '恢复失败: ' . $e->getMessage());
}
}
/**
* 下载备份
*/
private static function downloadBackup()
{
try {
// 验证用户权限
$user = Typecho_Widget::widget('Widget_User');
if (!$user->hasLogin() || !$user->pass('administrator', true)) {
die('权限不足');
}
$db = Typecho_Db::get();
// 获取最新的备份
$backups = $db->fetchAll($db->select()->from('table.options')
->where('name LIKE ?', 'heartfelt_bak_%')
->order('name', Typecho_Db::SORT_DESC)
->limit(1));
if (empty($backups)) {
die('没有找到备份文件');
}
$backup = $backups[0];
$backupData = unserialize($backup['value']);
if (!$backupData || !isset($backupData['backup_time'])) {
die('备份文件格式错误');
}
$filename = "heartfelt_backup_" . date('Y-m-d_H-i-s', $backupData['backup_time']) . ".dat";
header('Content-Type: application/octet-stream');
header('Content-Disposition: attachment; filename="' . $filename . '"');
header('Content-Length: ' . strlen($backup['value']));
header('Cache-Control: no-cache, must-revalidate');
header('Pragma: no-cache');
header('Expires: 0');
echo $backup['value'];
exit;
} catch (Exception $e) {
die('下载失败: ' . $e->getMessage());
}
}
/**
* 上传备份
*/
private static function uploadBackup()
{
try {
$backupContent = isset($_POST['backup_content']) ? $_POST['backup_content'] : '';
if (empty($backupContent)) {
self::jsonResponse(false, '备份内容为空');
return;
}
// 验证备份内容
$backupData = @unserialize($backupContent);
if (!$backupData || !isset($backupData['plugin_config'])) {
self::jsonResponse(false, '备份文件格式错误');
return;
}
$db = Typecho_Db::get();
// 保存备份
$backupName = 'heartfelt_upload_' . date('Ymd_His');
$db->query($db->insert('table.options')
->rows(array(
'name' => $backupName,
'user' => '0',
'value' => $backupContent
)));
// 恢复设置
$existingConfig = $db->fetchRow($db->select()->from('table.options')
->where('name = ?', 'plugin:RuiPing'));
if ($existingConfig) {
$db->query($db->update('table.options')
->rows(array('value' => $backupData['plugin_config']))
->where('name = ?', 'plugin:RuiPing'));
} else {
$db->query($db->insert('table.options')
->rows(array(
'name' => 'plugin:RuiPing',
'user' => '0',
'value' => $backupData['plugin_config']
)));
}
self::jsonResponse(true, '备份上传并恢复成功');
} catch (Exception $e) {
self::jsonResponse(false, '上传失败: ' . $e->getMessage());
}
}
/**
* 清理旧的备份
*/
private static function cleanOldBackups()
{
try {
$db = Typecho_Db::get();
$backups = $db->fetchAll($db->select()->from('table.options')
->where('name LIKE ?', 'heartfelt_bak_%')
->order('name', Typecho_Db::SORT_DESC));
if (count($backups) > 5) {
for ($i = 5; $i < count($backups); $i++) {
$db->query($db->delete('table.options')
->where('name = ?', $backups[$i]['name']));
}
}
} catch (Exception $e) {
// 忽略清理错误
}
}
/**
* 添加备份功能相关的JavaScript
*/
private static function addBackupScript()
{
$ajaxUrl = Helper::options()->index . '/action/ruiping-backup';
?>
<script>
function heartfeltBackupSettings() {
if (!confirm('确定要备份当前设置吗?')) {
return false;
}
var $btn = jQuery('#backupBtn-btn');
var originalText = $btn.val();
$btn.val('备份中...').prop('disabled', true);
jQuery.ajax({
url: '<?php echo $ajaxUrl; ?>',
type: 'POST',
dataType: 'json',
data: {
heartfelt_backup_action: 'backup'
},
success: function(response) {
if (response.success) {
alert('设置备份成功!');
} else {
alert('备份失败: ' + response.message);
}
$btn.val(originalText).prop('disabled', false);
},
error: function() {
alert('网络错误,请重试');
$btn.val(originalText).prop('disabled', false);
}
});
return false;
}
function heartfeltRestoreSettings() {
if (!confirm('确定要从备份恢复设置吗?当前设置将被覆盖。')) {
return false;
}
var $btn = jQuery('#restoreBtn-btn');
var originalText = $btn.val();
$btn.val('恢复中...').prop('disabled', true);
jQuery.ajax({
url: '<?php echo $ajaxUrl; ?>',
type: 'POST',
dataType: 'json',
data: {
heartfelt_backup_action: 'restore'
},
success: function(response) {
if (response.success) {
alert('设置恢复成功!');
setTimeout(function() {
window.location.reload();
}, 1000);
} else {
alert('恢复失败: ' + response.message);
$btn.val(originalText).prop('disabled', false);
}
},
error: function() {
alert('网络错误,请重试');
$btn.val(originalText).prop('disabled', false);
}
});
return false;
}
function heartfeltDownloadBackup() {
// 直接打开下载链接
var downloadUrl = '<?php echo Helper::options()->index; ?>?heartfelt_backup_action=download';
window.open(downloadUrl, '_blank');
return false;
}
// 添加上传备份功能
jQuery(document).ready(function($) {
$('textarea[name="uploadBackup"]').after(
'<button type="button" class="btn" onclick="heartfeltUploadBackup()" style="margin-top: 5px;">上传并恢复备份</button>'
);
});
function heartfeltUploadBackup() {
var backupContent = jQuery('textarea[name="uploadBackup"]').val();
if (!backupContent) {
alert('请先粘贴备份文件内容');
return false;
}
if (!confirm('确定要上传并恢复此备份吗?当前设置将被覆盖。')) {
return false;
}
var $btn = jQuery('button[onclick="heartfeltUploadBackup()"]');
var originalText = $btn.text();
$btn.text('上传中...').prop('disabled', true);
jQuery.ajax({
url: '<?php echo $ajaxUrl; ?>',
type: 'POST',
dataType: 'json',
data: {
heartfelt_backup_action: 'upload',
backup_content: backupContent
},
success: function(response) {
if (response.success) {
alert('备份上传并恢复成功!');
setTimeout(function() {
window.location.reload();
}, 1000);
} else {
alert('上传失败: ' + response.message);
$btn.text(originalText).prop('disabled', false);
}
},
error: function() {
alert('网络错误,请重试');
$btn.text(originalText).prop('disabled', false);
}
});
return false;
}
</script>
<?php
}
/**
* 处理AJAX请求
*/
public static function handleAjax()
{
// 验证用户权限
$user = Typecho_Widget::widget('Widget_User');
if (!$user->hasLogin() || !$user->pass('administrator', true)) {
self::jsonResponse(false, '权限不足');
}
$db = Typecho_Db::get();
$commentId = isset($_POST['commentId']) ? intval($_POST['commentId']) : 0;
$action = isset($_POST['action']) ? $_POST['action'] : '';
if ($commentId <= 0 || !in_array($action, array('set', 'unset'))) {
self::jsonResponse(false, '参数错误');
}
// 验证评论是否存在
$comment = $db->fetchRow($db->select()->from('table.comments')->where('coid = ?', $commentId));
if (!$comment) {
self::jsonResponse(false, '评论不存在');
}
// 获取当前插件配置
$options = Typecho_Widget::widget('Widget_Options');
$config = $options->plugin('RuiPing');
if (!$config) {
$config = new stdClass();
$config->commentIds = '';
$config->limit = '10';
$config->showAuthor = '1';
$config->showDate = '1';
$config->showContent = '1';
$config->showSource = '1';
$config->showOriginal = '1';
$config->authorLink = '1';
$config->commentLink = '1';
$config->showPagination = '1';
$config->debugMode = '0';
}
$currentIds = self::getCommentIds($config->commentIds);
if ($action == 'set') {
if (!in_array($commentId, $currentIds)) {
$currentIds[] = $commentId;
}
} else {
$currentIds = array_filter($currentIds, function($id) use ($commentId) {
return $id != $commentId;
});
}
$newValue = implode("\n", $currentIds);
$existingConfig = $db->fetchRow($db->select()->from('table.options')->where('name = ?', 'plugin:RuiPing'));
if ($existingConfig) {
$existingConfigArray = unserialize($existingConfig['value']);
$existingConfigArray['commentIds'] = $newValue;
$updateResult = $db->query($db->update('table.options')
->rows(array('value' => serialize($existingConfigArray)))
->where('name = ?', 'plugin:RuiPing'));
} else {
$newConfig = array(
'commentIds' => $newValue,
'limit' => $config->limit ?: '10',
'showAuthor' => $config->showAuthor ?: '1',
'showDate' => $config->showDate ?: '1',
'showContent' => $config->showContent ?: '1',
'showSource' => $config->showSource ?: '1',
'showOriginal' => $config->showOriginal ?: '1',
'authorLink' => $config->authorLink ?: '1',
'commentLink' => $config->commentLink ?: '1',
'showPagination' => $config->showPagination ?: '1',
'debugMode' => $config->debugMode ?: '0',
'showFrontendBadge' => isset($config->showFrontendBadge) ? $config->showFrontendBadge : '1',
'frontendBadgePosition' => isset($config->frontendBadgePosition) ? $config->frontendBadgePosition : 'content-top-right',
'frontendBadgeText' => isset($config->frontendBadgeText) ? $config->frontendBadgeText : '❤ 锐评',
'frontendBadgeColor' => isset($config->frontendBadgeColor) ? $config->frontendBadgeColor : '#d32f2f'
);
$updateResult = $db->query($db->insert('table.options')
->rows(array(
'name' => 'plugin:RuiPing',
'user' => '0',
'value' => serialize($newConfig)
)));
}
if ($updateResult) {
self::jsonResponse(true, $action == 'set' ? '已设置为锐评' : '已取消锐评', array(
'commentId' => $commentId,
'action' => $action,
'isHeartfelt' => $action == 'set'
));
} else {
self::jsonResponse(false, '更新配置失败');
}
}
/**
* 添加管理员头部
*/
public static function addAdminHeader()
{
if (self::isCommentsManagementPage()) {
echo '<script>window.heartfeltPluginActive = true;</script>';
}
}
/**
* 添加管理员底部
*/
public static function addAdminFooter()
{
if (!self::isCommentsManagementPage()) {
return;
}
$options = Typecho_Widget::widget('Widget_Options');
$config = $options->plugin('RuiPing');
if (!$config) {
$currentIds = array();
} else {
$currentIds = self::getCommentIds($config->commentIds);
}
$currentIdsStr = implode(',', $currentIds);
$ajaxUrl = Typecho_Common::url('index.php', $options->siteUrl) . '?action=heartfelt-comments';
$batchAjaxUrl = Typecho_Common::url('index.php', $options->siteUrl);
$heartfeltUrl = Typecho_Common::url('index.php', $options->siteUrl) . '?heartfelt_filter=heartfelt';
echo <<<HTML
<script>
// 锐评插件 - 后台功能
var heartfeltMenuAdded = false;
function initRuiPing() {
var buttonCount = 0;
var badgeCount = 0;
// 添加锐评菜单到现有筛选菜单(只添加一次)
if (!heartfeltMenuAdded) {
addRuiPingMenu();
heartfeltMenuAdded = true;
}
// 添加锐评操作到默认批量操作菜单
addRuiPingBatchActions();
jQuery('tr[id^="comment-"]').each(function() {
var \$tr = jQuery(this);
var trId = \$tr.attr('id');
var commentId = trId.replace('comment-', '');
if (commentId && !isNaN(commentId)) {
var isHeartfelt = [{$currentIdsStr}].indexOf(parseInt(commentId)) > -1;
var operationDiv = \$tr.find('.operate');
if (operationDiv.length === 0) {
operationDiv = \$tr.find('.comment-action');
}
if (operationDiv.length === 0) {
operationDiv = \$tr.find('td:last-child');
}
if (operationDiv.length > 0) {
if (!operationDiv.find('.heartfelt-btn').length) {
var heartfeltBtn = isHeartfelt ?
'<a href="javascript:;" class="heartfelt-btn" data-comment="' + commentId + '" data-action="unset" style="color: #d32f2f; margin-left: 8px;">❤ 取消锐评</a>' :
'<a href="javascript:;" class="heartfelt-btn" data-comment="' + commentId + '" data-action="set" style="margin-left: 8px;">🤍 设为锐评</a>';
operationDiv.append(heartfeltBtn);
buttonCount++;
bindSingleButton(operationDiv.find('.heartfelt-btn').last());
}
}
if (isHeartfelt) {
var firstTd = \$tr.find('td:first-child');
if (firstTd.length > 0 && !firstTd.find('.heartfelt-badge').length) {
firstTd.css('position', 'relative');
firstTd.append('<span class="heartfelt-badge">❤ 锐评</span>');
badgeCount++;
}
}
}
});
}
// 添加锐评菜单到现有筛选菜单区域
function addRuiPingMenu() {
// 查找现有的筛选菜单 - 左边的菜单
var \$typechoTabs = jQuery('.typecho-option-tabs').first();
if (\$typechoTabs.length === 0) {
console.log('未找到筛选菜单容器');
return;
}
// 检查是否已经添加过锐评菜单
if (\$typechoTabs.find('.ruiping-menu-trigger').length > 0) {
return;
}
// 创建锐评菜单项 - 添加到垃圾菜单后面
var ruiPingMenuItem = jQuery('<li><a href="{$heartfeltUrl}" class="ruiping-menu-trigger">锐评</a></li>');
// 找到垃圾菜单并插入到它后面
var \$spamMenu = \$typechoTabs.find('a[href*="spam"]').parent();
if (\$spamMenu.length > 0) {
\$spamMenu.after(ruiPingMenuItem);
} else {
// 如果找不到垃圾菜单,添加到末尾
\$typechoTabs.append(ruiPingMenuItem);
}
// 检查当前是否在锐评页面
var currentUrl = window.location.href;
if (currentUrl.indexOf('heartfelt_filter=heartfelt') > -1 || currentUrl.indexOf('coid[]') > -1) {
ruiPingMenuItem.addClass('current');
}
}
// 添加锐评操作到默认批量操作菜单
function addRuiPingBatchActions() {
var \$operateDiv = jQuery('.operate');
if (\$operateDiv.length === 0) {
console.log('未找到操作容器');
return;
}
var \$dropdownMenu = \$operateDiv.find('.dropdown-menu');
if (\$dropdownMenu.length === 0) {
console.log('未找到下拉菜单');
return;
}
// 检查是否已经添加过锐评操作
if (\$dropdownMenu.find('.heartfelt-batch-action').length > 0) {
return;
}
// 在删除操作前添加锐评操作
var heartfeltActions = '<li class="divider heartfelt-batch-action"></li>';
heartfeltActions += '<li class="heartfelt-batch-action"><a href="javascript:;" onclick="heartfeltBatchAction(\'add\')">设为锐评</a></li>';
heartfeltActions += '<li class="heartfelt-batch-action"><a href="javascript:;" onclick="heartfeltBatchAction(\'remove\')">取消锐评</a></li>';
\$dropdownMenu.find('a[href*="delete"]').parent().before(heartfeltActions);
}
// 批量操作
function heartfeltBatchAction(action) {
var selectedComments = [];
jQuery('input[name="coid[]"]:checked').each(function() {
var commentId = jQuery(this).val();
if (commentId && !isNaN(commentId)) {
selectedComments.push(parseInt(commentId));
}
});
if (selectedComments.length === 0) {
alert('请先选择要操作的评论');
return;
}
var confirmMessage = '';
switch (action) {
case 'add':
confirmMessage = '确定要将选中的 ' + selectedComments.length + ' 条评论设为锐评吗?';
break;
case 'remove':
confirmMessage = '确定要取消选中的 ' + selectedComments.length + ' 条评论的锐评状态吗?';
break;
}
if (!confirm(confirmMessage)) {
return;
}
jQuery.ajax({
url: '{$batchAjaxUrl}',
type: 'POST',
dataType: 'json',
data: {
heartfelt_batch_action: action,
comment_ids: selectedComments
},
success: function(response) {
if (response && response.success) {
alert(response.message || '操作成功');
window.location.reload();
} else {
var message = response && response.message ? response.message : '操作失败';
alert('操作失败: ' + message);
}
},
error: function(xhr, status, error) {
var errorMsg = '网络错误,请重试。状态: ' + status;
if (xhr.status === 404) {
errorMsg = '请求的接口不存在 (404),请检查插件路由配置';
} else if (xhr.status === 403) {
errorMsg = '权限不足 (403),请重新登录';
} else if (xhr.status === 500) {
errorMsg = '服务器内部错误 (500)';
}
alert(errorMsg);
}
});
}
function bindSingleButton(btn) {
btn.off('click').on('click', function(e) {
e.preventDefault();
e.stopPropagation();
var \$btn = jQuery(this);
var commentId = \$btn.data('comment');
var action = \$btn.data('action');
if (!commentId) {
alert('错误: 未找到评论ID');
return;
}
var originalHtml = \$btn.html();
\$btn.html('处理中...').prop('disabled', true).css('opacity', '0.6');
jQuery.ajax({
url: '{$ajaxUrl}',
type: 'POST',
dataType: 'json',
data: {
commentId: commentId,
action: action
},
success: function(response) {
if (response && response.success) {
alert(response.message || '操作成功');
window.location.reload();
} else {
var message = response && response.message ? response.message : '操作失败';
alert('操作失败: ' + message);
\$btn.prop('disabled', false).css('opacity', '1').html(originalHtml);
}
},
error: function(xhr, status, error) {
var errorMsg = '网络错误,请重试。状态: ' + status;
if (xhr.status === 404) {
errorMsg = '请求的接口不存在 (404),请检查插件路由配置';
} else if (xhr.status === 403) {
errorMsg = '权限不足 (403),请重新登录';
} else if (xhr.status === 500) {
errorMsg = '服务器内部错误 (500)';
}
alert(errorMsg);
\$btn.prop('disabled', false).css('opacity', '1').html(originalHtml);
},
timeout: 10000
});
});
}
function heartfeltMainInit() {
if (!window.jQuery) {
var checkJQuery = setInterval(function() {
if (window.jQuery) {
clearInterval(checkJQuery);
heartfeltMainInit();
}
}, 100);
return;
}
jQuery(document).ready(function($) {
initRuiPing();
// 只初始化一次,避免重复
setTimeout(function() {
if (!heartfeltMenuAdded) {
initRuiPing();
}
}, 500);
});
}
heartfeltMainInit();
</script>
<style>
table {
font-size: 14px;
line-height: 1.5;
}
.heartfelt-badge {
position: absolute;
top: 50%;
left: 10px;
transform: translateY(-50%);
background: #d32f2f;
color: white;
padding: 2px 6px;
border-radius: 3px;
font-size: 11px;
font-weight: normal;
z-index: 1;
line-height: 1.2;
box-shadow: 0 1px 3px rgba(0,0,0,0.2);
}
.heartfelt-btn {
margin-left: 8px;
text-decoration: none;
font-size: 12px;
cursor: pointer;
display: inline-block;
padding: 2px 6px;
border-radius: 2px;
border: 1px solid #ddd;
background: #f9f9f9;
transition: all 0.15s ease;
}
.heartfelt-btn:hover {
color: #d32f2f !important;
text-decoration: none;
background: #fff;
border-color: #d32f2f;
}
.heartfelt-btn:disabled {
opacity: 0.6;
cursor: not-allowed;
}
.typecho-table tr td:first-child {
position: relative !important;
padding-left: 60px !important;
}
.ruiping-menu-trigger {
color: #d32f2f !important;
font-weight: bold;
}
</style>
HTML;
}
/**
* 检查是否为评论管理页面
*/
private static function isCommentsManagementPage()
{
$pathInfo = isset($_SERVER['PATH_INFO']) ? $_SERVER['PATH_INFO'] : '';
$requestUri = isset($_SERVER['REQUEST_URI']) ? $_SERVER['REQUEST_URI'] : '';
if (strpos($pathInfo, 'comments') !== false ||
strpos($requestUri, 'comments') !== false ||
strpos($requestUri, 'comment') !== false ||
(isset($_GET['__typecho_page']) && $_GET['__typecho_page'] === 'comments')) {
return true;
}
return false;
}
/**
* 插件实现方法
*/
public static function render()
{
$options = Typecho_Widget::widget('Widget_Options');
$config = $options->plugin('RuiPing');
if (!$config) {
return '<p>插件配置未找到,请重新保存插件设置</p>';
}
$currentPage = isset($_GET['heartfelt_page']) ? max(1, intval($_GET['heartfelt_page'])) : 1;
if ($config->debugMode && empty($config->commentIds)) {
return '<p class="heartfelt-debug">调试插件设置中未配置评论ID</p>';
}
$commentIds = self::getCommentIds($config->commentIds);
if (empty($commentIds)) {
if ($config->debugMode) {
return '<p class="heartfelt-debug">调试解析后的评论ID为空请检查ID格式每行一个数字ID</p>';
}
return '<p>暂无锐评</p>';
}
$limit = intval($config->limit) ?: 10;
return self::getRuiPing($commentIds, $limit, $currentPage, $config);
}
/**
* JSON响应
*/
private static function jsonResponse($success, $message = '', $data = array())
{
header('Content-Type: application/json; charset=utf-8');
echo json_encode(array(
'success' => $success,
'message' => $message,
'data' => $data
));
exit;
}
/**
* 获取评论ID数组
*/
private static function getCommentIds($commentIdsText)
{
if (empty($commentIdsText)) {
return array();
}
$ids = explode("\n", $commentIdsText);
$ids = array_map('trim', $ids);
$ids = array_filter($ids, function($id) {
return is_numeric($id) && $id > 0;
});
return $ids;
}
/**
* 获取锐评数据(带分页)- 修复排序问题
*/
private static function getRuiPing($commentIds, $limit, $currentPage, $config)
{
$db = Typecho_Db::get();
$offset = ($currentPage - 1) * $limit;
$total = count($commentIds);
$totalPages = ceil($total / $limit);
// 修复:先获取所有评论数据,按时间排序,然后再分页
$select = $db->select()
->from('table.comments')
->where('coid IN ?', $commentIds)
->order('created', Typecho_Db::SORT_DESC); // 先全局按时间排序
$allComments = $db->fetchAll($select);
// 如果获取的评论数量与ID数量不一致重新匹配
$allCommentsMap = array();
foreach ($allComments as $comment) {
$allCommentsMap[$comment['coid']] = $comment;
}
// 确保所有ID都有对应的评论数据
$validComments = array();
foreach ($commentIds as $commentId) {
if (isset($allCommentsMap[$commentId])) {
$validComments[$commentId] = $allCommentsMap[$commentId];
}
}
// 按时间排序(最新的在前面)
uasort($validComments, function($a, $b) {
return $b['created'] - $a['created'];
});
// 分页:取当前页的数据
$currentPageData = array_slice($validComments, $offset, $limit, true);
$debugInfo = '';
if ($config->debugMode) {
$debugInfo = '<div class="heartfelt-debug">';
$debugInfo .= '<p><strong>调试信息:</strong></p>';
$debugInfo .= '<p>总评论数: ' . $total . '</p>';
$debugInfo .= '<p>每页数量: ' . $limit . '</p>';
$debugInfo .= '<p>总页数: ' . $totalPages . '</p>';
$debugInfo .= '<p>当前页: ' . $currentPage . '</p>';
$debugInfo .= '<p>当前页评论数量: ' . count($currentPageData) . '</p>';
$debugInfo .= '<p>排序方式: 按时间倒序(最新的在最前面)</p>';
$debugInfo .= '</div>';
}
$html = $debugInfo;
$html .= self::generateHtml($currentPageData, $config);
if ($config->showPagination && $totalPages > 1) {
$html .= self::generatePagination($currentPage, $totalPages);
}
return $html;
}
/**
* 生成分页HTML
*/
private static function generatePagination($currentPage, $totalPages)
{
$html = '<div class="heartfelt-pagination">';
if ($currentPage > 1) {
$prevPage = $currentPage - 1;
$html .= '<a href="?' . self::buildPageQuery($prevPage) . '" class="heartfelt-page-link heartfelt-page-prev">上一页</a>';
}
$startPage = max(1, $currentPage - 2);
$endPage = min($totalPages, $currentPage + 2);
for ($i = $startPage; $i <= $endPage; $i++) {
if ($i == $currentPage) {
$html .= '<span class="heartfelt-page-current">' . $i . '</span>';
} else {
$html .= '<a href="?' . self::buildPageQuery($i) . '" class="heartfelt-page-link">' . $i . '</a>';
}
}
if ($currentPage < $totalPages) {
$nextPage = $currentPage + 1;
$html .= '<a href="?' . self::buildPageQuery($nextPage) . '" class="heartfelt-page-link heartfelt-page-next">下一页</a>';
}
$html .= '</div>';
return $html;
}
/**
* 构建分页查询参数
*/
private static function buildPageQuery($page)
{
$params = $_GET;
$params['heartfelt_page'] = $page;
return http_build_query($params);
}
/**
* 生成HTML输出
*/
private static function generateHtml($comments, $config)
{
if (empty($comments)) {
return '<p>暂无锐评</p>';
}
$html = '<div class="heartfelt-comments">';
$html .= '<div class="heartfelt-comments-list">';
foreach ($comments as $comment) {
$html .= self::generateCommentItem($comment, $config);
}
$html .= '</div>';
$html .= '</div>';
return $html;
}
/**
* 获取文章永久链接
*/
private static function getPostPermalink($cid)
{
try {
$db = Typecho_Db::get();
$post = $db->fetchRow($db->select('slug', 'type')
->from('table.contents')
->where('cid = ?', $cid)
->where('type = ?', 'post')
->limit(1));
if ($post) {
$options = Typecho_Widget::widget('Widget_Options');
$permalink = Typecho_Router::url('post', $post, $options->index);
return $permalink;
}
} catch (Exception $e) {
return '';
}
return '';
}
/**
* 获取评论链接
*/
private static function getCommentLink($comment, $config)
{
$postUrl = self::getPostPermalink($comment['cid']);
if (!$postUrl) {
return '';
}
$commentUrl = $postUrl . '#comment-' . $comment['coid'];
return $commentUrl;
}
/**
* 获取作者链接
*/
private static function getAuthorLink($comment, $config)
{
if (!empty($comment['url'])) {
$authorUrl = $comment['url'];
if (!preg_match('/^https?:\/\//', $authorUrl)) {
$authorUrl = 'http://' . $authorUrl;
}
return array('url' => $authorUrl, 'target' => $config->authorLink ? '_blank' : '');
}
return array('url' => '', 'target' => '');
}
/**
* 生成单个评论项HTML
*/
private static function generateCommentItem($comment, $config)
{
$html = '<div class="heartfelt-comment-item">';
$commentUrl = self::getCommentLink($comment, $config);
if ($config->showContent) {
$content = strip_tags($comment['text']);
if (mb_strlen($content, 'UTF-8') > 200) {
$content = mb_substr($content, 0, 200, 'UTF-8') . '...';
}
if ($commentUrl) {
$target = $config->commentLink ? ' target="_blank"' : '';
$html .= '<div class="heartfelt-comment-content">';
$html .= '<a href="' . $commentUrl . '"' . $target . ' class="heartfelt-comment-link">';
$html .= htmlspecialchars($content);
$html .= '</a>';
$html .= '</div>';
} else {
$html .= '<div class="heartfelt-comment-content">' . htmlspecialchars($content) . '</div>';
}
}
$html .= '<div class="heartfelt-comment-meta">';
if ($config->showAuthor) {
$author = $comment['author'] ?: '匿名';
$authorLinkInfo = self::getAuthorLink($comment, $config);
if (!empty($authorLinkInfo['url'])) {
$html .= '<span class="heartfelt-comment-author">';
$html .= '<a href="' . $authorLinkInfo['url'] . '" target="' . $authorLinkInfo['target'] . '" class="heartfelt-author-link">';
$html .= htmlspecialchars($author);
$html .= '</a>';
$html .= '</span>';
} else {
$html .= '<span class="heartfelt-comment-author">' . htmlspecialchars($author) . '</span>';
}
}
if ($config->showDate) {
$date = date('Y-m-d H:i', $comment['created']);
$html .= '<span class="heartfelt-comment-date">';
$html .= $date;
// 新增:显示原文链接(根据配置决定是否显示)
if ($config->showOriginal && $commentUrl) {
$target = $config->commentLink ? ' target="_blank"' : '';
$html .= '<a href="' . $commentUrl . '"' . $target . ' class="heartfelt-origin-link">原文</a>';
}
$html .= '</span>';
}
if ($config->showSource && $comment['cid']) {
try {
$postUrl = self::getPostPermalink($comment['cid']);
if ($postUrl) {
$db = Typecho_Db::get();
$post = $db->fetchRow($db->select('title')
->from('table.contents')
->where('cid = ?', $comment['cid'])
->limit(1));
if ($post && $post['title']) {
$target = $config->commentLink ? ' target="_blank"' : '';
$html .= '<span class="heartfelt-comment-source">';
$html .= '<a href="' . $postUrl . '"' . $target . ' class="heartfelt-source-link">';
$html .= htmlspecialchars($post['title']);
$html .= '</a>';
$html .= '</span>';
}
}
} catch (Exception $e) {
// 忽略文章获取错误
}
}
$html .= '</div>';
$html .= '</div>';
return $html;
}
/**
* 头部输出
*/
public static function header()
{
$css = self::getCss();
$options = Typecho_Widget::widget('Widget_Options');
$config = $options->plugin('RuiPing');
// 添加前端标识的CSS
if ($config && (!isset($config->showFrontendBadge) || $config->showFrontendBadge == '1')) {
$badgeColor = isset($config->frontendBadgeColor) ? $config->frontendBadgeColor : '#d32f2f';
$frontendCss = <<<CSS
<style>
/* 竖排锐评标识样式 */
.heartfelt-frontend-badge {
background-color: #d32f2f;
color: #fff;
padding: 3px 8px;
border-radius: 20px;
font-size: 12px;
font-weight: 600;
line-height: 1.2;
z-index: 100;
box-shadow: 0 2px 5px rgba(0,0,0,0.2);
transition: all 0.3s ease;
cursor: default;
text-shadow: 1px 1px 1px rgba(0,0,0,0.2);
white-space: nowrap;
pointer-events: auto;
/* 竖排文字 */
writing-mode: vertical-rl;
text-orientation: mixed;
height: auto;
padding: 8px 3px !important; /* 调整padding竖排时宽高互换 */
letter-spacing: 1px;
margin-top:15px;
}
.heartfelt-frontend-badge:hover {
box-shadow: 0 4px 10px rgba(0,0,0,0.3);
}
/* 针对你的评论模板的特殊处理 */
.comment-content .heartfelt-frontend-badge,
.cm .heartfelt-frontend-badge {
position: absolute !important;
top: 25px !important;
right: 15px !important;
}
/* 确保内容区域有相对定位 */
.comment-content,
.cm {
position: relative !important;
}
/* 为内容区域添加padding避免标识覆盖文字 - 调整为竖排时的宽度 */
.comment-content[style*="padding-right"],
.cm[style*="padding-right"] {
padding-right: 30px !important; /* 竖排标识宽度更小 */
}
.comment-content[style*="padding-left"],
.cm[style*="padding-left"] {
padding-left: 30px !important; /* 竖排标识宽度更小 */
}
/* 暗色模式适配 */
@media (prefers-color-scheme: dark) {
.heartfelt-frontend-badge {
box-shadow: 0 2px 5px rgba(0,0,0,0.4);
}
}
/* 修复嵌套评论的标识位置 */
.comment-child .heartfelt-frontend-badge {
font-size: 11px;
padding: 6px 2px !important; /* 竖排调整 */
top: 5px !important;
right: 5px !important;
}
/* 响应式调整 */
@media (max-width: 768px) {
.heartfelt-frontend-badge {
font-size: 11px;
padding: 6px 2px !important; /* 竖排调整 */
}
/* 移动端调整位置,避免太靠边 */
.comment-content .heartfelt-frontend-badge,
.cm .heartfelt-frontend-badge {
top: 5px !important;
right: 5px !important;
}
/* 调整内容区域的padding */
.comment-content[style*="padding-right"],
.cm[style*="padding-right"] {
padding-right: 25px !important; /* 移动端竖排标识更窄 */
}
.comment-content[style*="padding-left"],
.cm[style*="padding-left"] {
padding-left: 25px !important; /* 移动端竖排标识更窄 */
}
}
/* 动画效果 */
@keyframes heartfelt-badge-appear {
0% {
opacity: 0;
transform: translateY(-10px) scale(0.8);
}
100% {
opacity: 1;
transform: translateY(0) scale(1);
}
}
.heartfelt-frontend-badge {
animation: heartfelt-badge-appear 0.5s ease-out;
}
/* 针对其他位置的竖排调整 */
.heartfelt-frontend-badge[style*="content-top-left"] {
writing-mode: vertical-rl;
text-orientation: mixed;
padding: 8px 3px !important;
}
.heartfelt-frontend-badge[style*="comment-top-right"],
.heartfelt-frontend-badge[style*="comment-top-left"] {
writing-mode: vertical-rl;
text-orientation: mixed;
padding: 8px 3px !important;
}
/* 作者名后的标识保持横排 */
.heartfelt-frontend-badge[style*="display: inline-block"] {
writing-mode: horizontal-tb !important;
padding: 3px 8px !important;
vertical-align: middle;
}
</style>
CSS;
$css .= $frontendCss;
}
echo $css;
}
/**
* 底部输出
*/
public static function footer()
{
// 可以在这里添加其他页脚内容
}
/**
* 获取CSS样式
*/
private static function getCss()
{
return <<<CSS
<style>
/* 锐评插件核心样式 */
.heartfelt-comments {
background: #f8f9fa;
border-radius: 6px;
margin: 20px 0;
}
.heartfelt-title {
color: #d32f2f;
margin-bottom: 12px;
font-size: 1.1em;
text-align:center;
padding: 30px;
border-radius:10px;
}
.heartfelt-comments-list {
display: flex;
flex-direction: column;
gap: 20px;
}
.heartfelt-comment-item {
background:#eee;
padding: 16px;
border-radius: 10px;
position: relative;
border: 1px solid rgba(0, 0, 0, 0);
transition: all 0.3s ease;
}
.heartfelt-comment-item:hover {
border:1px solid #f15a22;
}
.heartfelt-comment-content {
color: #333;
line-height: 1.6;
margin-bottom: 12px;
font-size: 15px;
}
.heartfelt-comment-link {
color: #333;
text-decoration: none;
display: block;
padding: 8px;
margin: -8px;
border-radius: 4px;
transition: background-color 0.15s ease;
}
.heartfelt-comment-link:hover {
background-color: #f8f9fa;
color: #d32f2f;
}
.heartfelt-comment-meta {
display: flex;
flex-wrap: wrap;
gap: 12px;
font-size: 0.85em;
color: #666;
padding-top: 12px;
border-top: 1px solid #faf8f1;
}
.dark .heartfelt-comment-meta{border-top: 1px solid hsl(0 0% 15%);}
.heartfelt-comment-author,
.heartfelt-comment-date,
.heartfelt-comment-source {
display: flex;
align-items: center;
gap: 4px;
}
.heartfelt-author-link,
.heartfelt-source-link,
.heartfelt-origin-link {
color: #666;
text-decoration: none;
transition: color 0.15s ease;
}
.heartfelt-author-link:hover,
.heartfelt-source-link:hover,
.heartfelt-origin-link:hover {
color: #d32f2f;
text-decoration: none;
}
.heartfelt-origin-link {
padding: 2px 6px;
border-radius: 3px;
background: #f0f0f0;
margin-left: 8px;
font-size: 0.8em;
border: 1px solid #ddd;
}
.heartfelt-origin-link:hover {
background: #f8f0f5;
border-color: #d32f2f;
}
.heartfelt-pagination {
display: flex;
justify-content: center;
align-items: center;
gap: 8px;
margin-top: 24px;
}
.heartfelt-page-link,
.heartfelt-page-current {
padding: 6px 12px;
border-radius: 50px;
text-decoration: none;
transition: all 0.15s ease;
font-size: 0.9em;
min-width: 36px;
text-align: center;
}
.dark .heartfelt-page-link{
background-color:rgb(10 12 25 / 1)!important;
}
.heartfelt-page-link {
color: #495057;
background: white;
border: 1px solid #f15a22;
}
.heartfelt-page-link:hover {
color: #d32f2f;
background: #f8f0f5;
border-color: #d32f2f;
text-decoration: none;
}
.heartfelt-page-current {
color: white;
background: #d32f2f;
border: 1px solid #d32f2f;
font-weight: 600;
}
.heartfelt-debug {
background: #fff3cd;
border: 1px solid #ffeaa7;
padding: 12px;
border-radius: 4px;
color: #856404;
font-size: 0.9em;
margin-bottom: 12px;
}
.heartfelt-debug p {
margin: 4px 0;
}
.heartfelt-debug strong {
color: #d32f2f;
}
.heartfelt-comment-link::after {
content: "";
opacity: 0;
transition: opacity 0.15s ease;
font-size: 0.85em;
margin-left: 4px;
}
.heartfelt-comment-link:hover::after {
opacity: 1;
}
@media (max-width: 768px) {
.heartfelt-comments {
padding: 12px;
margin: 12px 0;
}
.heartfelt-comment-meta {
flex-direction: column;
gap: 6px;
}
.heartfelt-comment-content {
font-size: 0.9em;
}
.heartfelt-comment-link {
padding: 6px;
margin: -6px;
}
.heartfelt-pagination {
flex-wrap: wrap;
gap: 4px;
}
.heartfelt-page-link,
.heartfelt-page-current {
padding: 4px 8px;
font-size: 0.8em;
min-width: 30px;
}
}
</style>
CSS;
}
}