Files
RuiPing/Plugin.php
2026-02-23 19:55:05 +08:00

2267 lines
72 KiB
PHP
Raw Permalink 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
/**
* 有意思、有启发、有温度评论集锦
*
* @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;
}
}