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 '

锐评插件 - 设置备份与恢复

'; // 备份设置 $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 = << (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); } })(); 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'; ?> 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 ''; } } /** * 添加管理员底部 */ 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 << // 锐评插件 - 后台功能 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 ? '❤ 取消锐评' : '🤍 设为锐评'; 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('❤ 锐评'); 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('
  • 锐评
  • '); // 找到垃圾菜单并插入到它后面 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 = '
  • '; heartfeltActions += '
  • 设为锐评
  • '; heartfeltActions += '
  • 取消锐评
  • '; \$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(); 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 '

    插件配置未找到,请重新保存插件设置

    '; } $currentPage = isset($_GET['heartfelt_page']) ? max(1, intval($_GET['heartfelt_page'])) : 1; if ($config->debugMode && empty($config->commentIds)) { return '

    调试:插件设置中未配置评论ID

    '; } $commentIds = self::getCommentIds($config->commentIds); if (empty($commentIds)) { if ($config->debugMode) { return '

    调试:解析后的评论ID为空,请检查ID格式(每行一个数字ID)

    '; } return '

    暂无锐评

    '; } $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 = '
    '; $debugInfo .= '

    调试信息:

    '; $debugInfo .= '

    总评论数: ' . $total . '

    '; $debugInfo .= '

    每页数量: ' . $limit . '

    '; $debugInfo .= '

    总页数: ' . $totalPages . '

    '; $debugInfo .= '

    当前页: ' . $currentPage . '

    '; $debugInfo .= '

    当前页评论数量: ' . count($currentPageData) . '

    '; $debugInfo .= '

    排序方式: 按时间倒序(最新的在最前面)

    '; $debugInfo .= '
    '; } $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 = '
    '; if ($currentPage > 1) { $prevPage = $currentPage - 1; $html .= '上一页'; } $startPage = max(1, $currentPage - 2); $endPage = min($totalPages, $currentPage + 2); for ($i = $startPage; $i <= $endPage; $i++) { if ($i == $currentPage) { $html .= '' . $i . ''; } else { $html .= '' . $i . ''; } } if ($currentPage < $totalPages) { $nextPage = $currentPage + 1; $html .= '下一页'; } $html .= '
    '; 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 '

    暂无锐评

    '; } $html = '
    '; $html .= '
    '; foreach ($comments as $comment) { $html .= self::generateCommentItem($comment, $config); } $html .= '
    '; $html .= '
    '; 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 = '
    '; $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 .= ''; } else { $html .= '
    ' . htmlspecialchars($content) . '
    '; } } $html .= '
    '; if ($config->showAuthor) { $author = $comment['author'] ?: '匿名'; $authorLinkInfo = self::getAuthorLink($comment, $config); if (!empty($authorLinkInfo['url'])) { $html .= ''; $html .= ''; $html .= htmlspecialchars($author); $html .= ''; $html .= ''; } else { $html .= '' . htmlspecialchars($author) . ''; } } if ($config->showDate) { $date = date('Y-m-d H:i', $comment['created']); $html .= ''; $html .= $date; // 新增:显示原文链接(根据配置决定是否显示) if ($config->showOriginal && $commentUrl) { $target = $config->commentLink ? ' target="_blank"' : ''; $html .= '原文'; } $html .= ''; } 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 .= ''; $html .= ''; $html .= htmlspecialchars($post['title']); $html .= ''; $html .= ''; } } } catch (Exception $e) { // 忽略文章获取错误 } } $html .= '
    '; $html .= '
    '; 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 = << /* 竖排锐评标识样式 */ .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; } CSS; $css .= $frontendCss; } echo $css; } /** * 底部输出 */ public static function footer() { // 可以在这里添加其他页脚内容 } /** * 获取CSS样式 */ private static function getCss() { return << /* 锐评插件核心样式 */ .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; } } CSS; } }