diff --git a/Plugin.php b/Plugin.php new file mode 100644 index 0000000..9158908 --- /dev/null +++ b/Plugin.php @@ -0,0 +1,1335 @@ + '偶遇', 'min_comments' => 0, 'class' => 'vip1', 'color' => '#c0c0c0'), + array('name' => '同程', 'min_comments' => 1, 'class' => 'vip2', 'color' => '#a0a0a0'), + array('name' => '涉溪', 'min_comments' => 20, 'class' => 'vip3', 'color' => '#9e7a5d'), + array('name' => '穿林', 'min_comments' => 60, 'class' => 'vip4', 'color' => '#b87333'), + array('name' => '览峰', 'min_comments' => 150, 'class' => 'vip5', 'color' => '#cb6d1e'), + array('name' => '渡川', 'min_comments' => 300, 'class' => 'vip6', 'color' => '#cc8400'), + array('name' => '聆泉', 'min_comments' => 600, 'class' => 'vip7', 'color' => '#d4af37'), + array('name' => '沐霞', 'min_comments' => 1200, 'class' => 'vip8', 'color' => '#ffb800'), + array('name' => '共云', 'min_comments' => 2400, 'class' => 'vip9', 'color' => '#ffa500'), + array('name' => '印雪', 'min_comments' => 4800, 'class' => 'vip10', 'color' => '#ff8c00'), + array('name' => '望星', 'min_comments' => 9600, 'class' => 'vip11', 'color' => '#da70d6'), + array('name' => '归真', 'min_comments' => 19200, 'class' => 'vip12', 'color' => '#a42be2'), + ); + + /** + * 楼层名称默认配置 + */ + private static $defaultFloorNames = array('沙发', '板凳', '地板'); + + /** + * 子楼层名称默认配置 + */ + private static $defaultSubFloorNames = array('B1', 'B2', 'B3'); + + /** + * 激活插件 + */ + public static function activate() + { + // 前端样式 + Typecho_Plugin::factory('Widget_Archive')->header = array('RecentlyActive_Plugin', 'outputHeader'); + + // 在评论列表添加用户等级 + Typecho_Plugin::factory('Widget_Comments_Archive')->contentEx = array('RecentlyActive_Plugin', 'addUserLevelToComment'); + + // 在评论列表添加楼层显示 + Typecho_Plugin::factory('Widget_Comments_Archive')->contentEx = array('RecentlyActive_Plugin', 'addFloorNumberToComment'); + + return _t('插件已激活,使用 RecentlyActive_Plugin::show() 显示用户活跃时间'); + } + + /** + * 禁用插件 + */ + public static function deactivate() + { + return _t('插件已禁用'); + } + + /** + * 插件配置面板 + */ + public static function config(Typecho_Widget_Helper_Form $form) + { + // 显示模式 + $displayMode = new Typecho_Widget_Helper_Form_Element_Radio('display_mode', + array( + 'relative' => '相对时间(3小时前)', + 'absolute' => '绝对时间(2023-01-01 12:00)', + 'smart' => '智能模式(1天内用相对时间,更早用绝对时间)' + ), + 'smart', + _t('时间显示模式')); + $form->addInput($displayMode); + + // 时间格式 + $dateFormat = new Typecho_Widget_Helper_Form_Element_Text('date_format', + NULL, + 'Y-m-d H:i', + _t('时间格式'), + _t('绝对时间格式,如:Y-m-d H:i')); + $form->addInput($dateFormat); + + // 在线状态阈值(分钟) + $onlineThreshold = new Typecho_Widget_Helper_Form_Element_Text('online_threshold', + NULL, + '10', + _t('在线状态阈值(分钟)'), + _t('多少分钟内显示为"在线"状态')); + $form->addInput($onlineThreshold->addRule('isInteger', _t('必须是整数'))); + + // 是否显示状态点 + $showDot = new Typecho_Widget_Helper_Form_Element_Radio('show_dot', + array( + '1' => '显示状态点 ●', + '0' => '不显示状态点' + ), + '1', + _t('状态点显示')); + $form->addInput($showDot); + + // 默认文字 + $defaultText = new Typecho_Widget_Helper_Form_Element_Text('default_text', + NULL, + '从未活跃', + _t('默认显示文字'), + _t('当用户从未活跃时显示的文字')); + $form->addInput($defaultText); + + // 楼层显示配置区域 + echo '

楼层显示配置

'; + + // 启用楼层显示 + $enableFloor = new Typecho_Widget_Helper_Form_Element_Radio('enable_floor', + array( + '1' => '启用', + '0' => '禁用' + ), + '1', + _t('启用楼层显示')); + $form->addInput($enableFloor); + + // 前三楼名称 + $floorNames = new Typecho_Widget_Helper_Form_Element_Text('floor_names', + NULL, + '沙发,板凳,地板', + _t('父评论前三楼名称'), + _t('父评论前三楼显示的名称,用英文逗号分隔,例如:沙发,板凳,地板')); + $form->addInput($floorNames); + + // 父评论显示格式 + $parentFloorFormat = new Typecho_Widget_Helper_Form_Element_Text('parent_floor_format', + NULL, + '#楼层', + _t('父评论楼层显示格式'), + _t('父评论楼层显示格式,#楼层 会被替换为实际楼层,例如:#楼层楼 会显示为 1楼')); + $form->addInput($parentFloorFormat); + + // 新增:子评论前三楼名称 + $subFloorNames = new Typecho_Widget_Helper_Form_Element_Text('sub_floor_names', + NULL, + 'B1,B2,B3', + _t('子评论前三楼名称'), + _t('子评论前三楼显示的名称,用英文逗号分隔,例如:B1,B2,B3')); + $form->addInput($subFloorNames); + + // 新增:子评论显示格式 + $subFloorFormat = new Typecho_Widget_Helper_Form_Element_Text('sub_floor_format', + NULL, + 'B#楼层', + _t('子评论楼层显示格式'), + _t('子评论楼层显示格式,#楼层 会被替换为实际楼层,例如:B#楼层 会显示为 B1')); + $form->addInput($subFloorFormat); + + // 用户等级配置区域 + echo '

用户等级配置

'; + + // 启用用户等级显示 + $enableUserLevel = new Typecho_Widget_Helper_Form_Element_Radio('enable_user_level', + array( + '1' => '启用', + '0' => '禁用' + ), + '1', + _t('启用用户等级显示')); + $form->addInput($enableUserLevel); + + // 管理员标识 + $adminLabel = new Typecho_Widget_Helper_Form_Element_Text('admin_label', + NULL, + '博主', + _t('管理员标识'), + _t('管理员在评论中显示的文字')); + $form->addInput($adminLabel); + + // 等级配置说明 + echo '

等级配置格式:等级名称|所需评论数|CSS类名|颜色值
例如:偶遇|0|vip1|#c0c0c0
每行一个等级,按评论数升序排列

'; + + // 等级配置 + $levelConfig = new Typecho_Widget_Helper_Form_Element_Textarea('level_config', + NULL, + self::getDefaultLevelConfig(), + _t('等级配置'), + _t('每行一个等级:等级名称|所需评论数|CSS类名|颜色值')); + $form->addInput($levelConfig); + + // 图标字体CSS + $iconFontCss = new Typecho_Widget_Helper_Form_Element_Textarea('iconfont_css', + NULL, + self::getDefaultIconFontCss(), + _t('图标字体CSS'), + _t('用户等级图标的CSS样式')); + $form->addInput($iconFontCss); + } + + /** + * 获取默认等级配置文本 + */ + private static function getDefaultLevelConfig() + { + $lines = array(); + foreach (self::$defaultLevels as $level) { + $lines[] = "{$level['name']}|{$level['min_comments']}|{$level['class']}|{$level['color']}"; + } + return implode("\n", $lines); + } + + /** + * 获取默认图标字体CSS - 更新为提供的CSS + */ + private static function getDefaultIconFontCss() + { + $css = '.vipicon { + font-family: "FontAwesome", "iconfont"; + font-style: normal; + font-weight: normal; + speak: none; + display: inline-block; + text-decoration: inherit; + text-align: center; + font-variant: normal; + text-transform: none; + line-height: 1em; +} +.vipicon:before { + content: "\e66a"; + font-size: 14px; +} +.com-level { display: inline-block; margin-left: 3px; } +.com-level sub { + font-size: 11px; + vertical-align: baseline; + position: relative; + top: -0.5em; + font-weight: bold; +} +/* 等级颜色定义 - 同时应用到图标和数字 */ +.com-level.vip1 .vipicon, +.com-level.vip1 sub { color: #999; } +.com-level.vip2 .vipicon, +.com-level.vip2 sub { color: #8c8c8c; } +.com-level.vip3 .vipicon, +.com-level.vip3 sub { color: #666; } +.com-level.vip4 .vipicon, +.com-level.vip4 sub { color: #52c41a; } +.com-level.vip5 .vipicon, +.com-level.vip5 sub { color: #1890ff; } +.com-level.vip6 .vipicon, +.com-level.vip6 sub { color: #722ed1; } +.com-level.vip7 .vipicon, +.com-level.vip7 sub { color: #faad14; } +.com-level.vip8 .vipicon, +.com-level.vip8 sub { color: #f5222d; } +.com-level.vip9 .vipicon, +.com-level.vip9 sub { color: #eb2f96; } +.com-level.vip10 .vipicon, +.com-level.vip10 sub { color: #fa541c; } +.com-level.vip11 .vipicon, +.com-level.vip11 sub { color: #13c2c2; } +.com-level.vip12 .vipicon, +.com-level.vip12 sub { color: #000; } +/* 动态CSS - 确保用户自定义的颜色通过内联样式生效 */ +.com-level .vipicon, +.com-level sub { + color: inherit !important; +} +/* 博主颜色 */ +.com-level.blogger .vipicon, +.com-level.blogger sub { color: #f5222d !important; }'; + + return $css; + } + + /** + * 个人用户配置(不需要) + */ + public static function personalConfig(Typecho_Widget_Helper_Form $form) {} + + /** + * 输出前端样式 + */ + public static function outputHeader() + { + try { + $options = Typecho_Widget::widget('Widget_Options')->plugin('RecentlyActive'); + $enableUserLevel = isset($options->enable_user_level) ? $options->enable_user_level : '1'; + } catch (Exception $e) { + $enableUserLevel = '1'; + } + + // 基础CSS(总是输出,用于活跃时间显示) + $css = << +.recently-active { + display: inline-block; + font-size: 15px; + color: #666; + margin-left: 5px; +} +.recently-active.online { + color: #52c41a; +} +.recently-active.online:before { + font-size: 13px; + vertical-align: middle; +} +.recently-active.online.no-dot:before { + content: ""; +} +.recently-active.offline { + color: #999; +} +.recently-active.offline:before { + font-size: 10px; +} +.recently-active.offline.no-dot:before { + content: ""; +} +.recently-active-tooltip { + cursor: help; +} + +CSS; + + // 只有启用等级显示时才输出等级CSS + if ($enableUserLevel == '1') { + try { + $iconFontCss = isset($options->iconfont_css) ? $options->iconfont_css : self::getDefaultIconFontCss(); + $css .= ""; + } catch (Exception $e) { + $css .= ""; + } + } + + echo $css; + } + + /** + * 添加楼层号到评论 - 通过钩子自动处理 + */ + public static function addFloorNumberToComment($content, $widget, $lastResult) + { + $content = empty($lastResult) ? $content : $lastResult; + + try { + // 获取插件配置 + $options = Typecho_Widget::widget('Widget_Options')->plugin('RecentlyActive'); + $enableFloor = isset($options->enable_floor) ? $options->enable_floor : '1'; + + // 如果禁用楼层显示,直接返回原内容 + if ($enableFloor == '0') { + return $content; + } + + // 判断是父评论还是子评论 + $isParentComment = ($widget->parent == 0); + + if ($isParentComment) { + // 父评论:统计全部父评论的楼层 + $floorNumber = self::getParentCommentFloorNumber($widget->coid, $widget->cid); + $floorHtml = self::generateParentFloorHtml($floorNumber, $options); + return $floorHtml . $content; + } else { + // 子评论:使用新的计数逻辑 + $subFloorNumber = self::getSubCommentFloorNumberCorrect($widget->coid, $widget->parent, $widget->cid); + $floorHtml = self::generateSubFloorHtml($subFloorNumber, $options); + return $floorHtml . $content; + } + + } catch (Exception $e) { + // 出错时返回原内容,确保评论不消失 + return $content; + } + } + + /** + * 生成父评论楼层HTML + */ + private static function generateParentFloorHtml($floorNumber, $options) + { + // 获取父评论楼层显示格式 + $parentFloorFormat = isset($options->parent_floor_format) ? $options->parent_floor_format : '#楼层'; + + // 获取父评论楼层名称配置 + $floorNames = array(); + if (isset($options->floor_names) && !empty($options->floor_names)) { + $floorNames = explode(',', $options->floor_names); + } + + // 如果配置为空,使用默认名称 + if (empty($floorNames)) { + $floorNames = self::$defaultFloorNames; + } + + // 根据楼层数获取显示文本 + if ($floorNumber <= count($floorNames) && $floorNumber > 0) { + $floorText = $floorNames[$floorNumber - 1]; + } else { + // 使用格式替换 + $floorText = str_replace('#楼层', $floorNumber, $parentFloorFormat); + } + + // 生成楼层HTML + return '' . htmlspecialchars($floorText) . ''; + } + + /** + * 生成子评论楼层HTML + */ + private static function generateSubFloorHtml($subFloorNumber, $options) + { + // 获取子评论楼层显示格式 + $subFloorFormat = isset($options->sub_floor_format) ? $options->sub_floor_format : 'B#楼层'; + + // 获取子评论楼层名称配置 + $subFloorNames = array(); + if (isset($options->sub_floor_names) && !empty($options->sub_floor_names)) { + $subFloorNames = explode(',', $options->sub_floor_names); + } + + // 如果配置为空,使用默认名称 + if (empty($subFloorNames)) { + $subFloorNames = self::$defaultSubFloorNames; + } + + // 根据楼层数获取显示文本 + if ($subFloorNumber <= count($subFloorNames) && $subFloorNumber > 0) { + $floorText = $subFloorNames[$subFloorNumber - 1]; + } else { + // 使用格式替换 + $floorText = str_replace('#楼层', $subFloorNumber, $subFloorFormat); + } + + // 生成楼层HTML + return '' . htmlspecialchars($floorText) . ''; + } + + /** + * 获取父评论的楼层号(统计全部父评论) + */ + private static function getParentCommentFloorNumber($currentCoid, $cid) + { + try { + $db = Typecho_Db::get(); + + // 查询所有父评论(parent=0)且属于当前文章,按时间升序排列 + $query = $db->select('coid') + ->from('table.comments') + ->where('cid = ?', $cid) + ->where('parent = ?', 0) + ->where('status = ?', 'approved') + ->order('coid', Typecho_Db::SORT_ASC); + + $comments = $db->fetchAll($query); + + // 查找当前评论在数组中的位置(从1开始) + foreach ($comments as $index => $comment) { + if ($comment['coid'] == $currentCoid) { + return $index + 1; + } + } + + // 如果没找到,返回1 + return 1; + + } catch (Exception $e) { + return 1; + } + } + + /** + * 获取子评论的楼层号 - 正确版本:统计同一父评论下的所有子评论 + */ + private static function getSubCommentFloorNumberCorrect($currentCoid, $parentCoid, $cid) + { + try { + $db = Typecho_Db::get(); + + // 首先,我们需要找到当前评论的根父评论 + // 对于嵌套回复,根父评论是最顶层的父评论 + $rootParentId = self::findRootParent($currentCoid, $parentCoid, $cid); + + // 如果找到的根父评论不是直接父评论,使用根父评论 + if ($rootParentId != $parentCoid) { + $parentCoid = $rootParentId; + } + + // 现在,获取该父评论下的所有子评论(按coid排序) + $allChildren = self::getAllDirectAndNestedChildren($parentCoid, $cid); + + // 按coid排序所有子评论 + usort($allChildren, function($a, $b) { + return $a['coid'] - $b['coid']; + }); + + // 查找当前评论在所有子评论中的位置 + foreach ($allChildren as $index => $comment) { + if ($comment['coid'] == $currentCoid) { + return $index + 1; // 返回楼层号 + } + } + + // 如果没找到,可能是查询有问题,尝试简单查询 + $simpleQuery = $db->select('coid') + ->from('table.comments') + ->where('cid = ?', $cid) + ->where('parent = ?', $parentCoid) + ->where('status = ?', 'approved') + ->order('coid', Typecho_Db::SORT_ASC); + + $directChildren = $db->fetchAll($simpleQuery); + + foreach ($directChildren as $index => $comment) { + if ($comment['coid'] == $currentCoid) { + return $index + 1; + } + } + + return 1; // 默认返回1 + + } catch (Exception $e) { + return 1; + } + } + + /** + * 查找评论的根父评论 + */ + private static function findRootParent($currentCoid, $parentCoid, $cid) + { + try { + $db = Typecho_Db::get(); + + // 如果当前评论的父评论是0,说明它自己就是父评论 + if ($parentCoid == 0) { + return $currentCoid; + } + + // 检查父评论的父评论 + $currentParentId = $parentCoid; + $visited = array($currentCoid); // 防止循环 + + for ($i = 0; $i < 20; $i++) { // 最多追踪20层 + if (in_array($currentParentId, $visited)) { + break; // 避免循环 + } + + $visited[] = $currentParentId; + + $parentComment = $db->fetchRow($db->select('parent') + ->from('table.comments') + ->where('coid = ?', $currentParentId) + ->where('cid = ?', $cid) + ->limit(1)); + + if (!$parentComment || $parentComment['parent'] == 0) { + // 找到了根父评论 + return $currentParentId; + } + + $currentParentId = $parentComment['parent']; + } + + // 如果循环结束还没找到,返回直接父评论 + return $parentCoid; + + } catch (Exception $e) { + return $parentCoid; + } + } + + /** + * 获取父评论下的所有子评论(包括嵌套的) + */ + private static function getAllDirectAndNestedChildren($parentCoid, $cid) + { + $db = Typecho_Db::get(); + $allChildren = array(); + + // 递归获取所有子评论 + self::collectChildrenRecursively($parentCoid, $cid, $db, $allChildren); + + return $allChildren; + } + + /** + * 递归收集子评论 + */ + private static function collectChildrenRecursively($parentCoid, $cid, $db, &$allChildren) + { + // 获取当前父评论的直接子评论 + $children = $db->fetchAll($db->select('coid', 'parent') + ->from('table.comments') + ->where('cid = ?', $cid) + ->where('parent = ?', $parentCoid) + ->where('status = ?', 'approved') + ->order('coid', Typecho_Db::SORT_ASC)); + + foreach ($children as $child) { + $allChildren[] = $child; + // 递归获取子评论的子评论 + self::collectChildrenRecursively($child['coid'], $cid, $db, $allChildren); + } + } + + /** + * 添加用户等级到评论 + */ + public static function addUserLevelToComment($content, $widget, $lastResult) + { + $content = empty($lastResult) ? $content : $lastResult; + + try { + // 获取插件配置 + $options = Typecho_Widget::widget('Widget_Options')->plugin('RecentlyActive'); + $enableUserLevel = isset($options->enable_user_level) ? $options->enable_user_level : '1'; + + // 如果禁用等级显示,直接返回原内容 + if ($enableUserLevel == '0') { + return $content; + } + + // 获取评论者邮箱 + $email = $widget->mail; + + // 如果是游客评论,不显示等级 + if (!$email) { + return $content; + } + + // 通过邮箱获取用户信息 + $userInfo = self::getUserInfoByEmail($email); + + // 获取用户组信息 + $isAdmin = false; + if ($userInfo && isset($userInfo['group'])) { + // Typecho中管理员用户组是 'administrator' + $isAdmin = ($userInfo['group'] == 'administrator'); + } + + // 如果是管理员,显示博主标识 + if ($isAdmin) { + $adminLabel = isset($options->admin_label) ? $options->admin_label : ''; + return $content . ''; + } + + // 获取用户评论数 + $commentCount = self::getUserCommentCountByEmail($email); + + // 获取用户等级 + $levelInfo = self::getUserLevel($commentCount, $options); + + // 生成等级HTML + $levelHtml = self::generateLevelHtml($levelInfo, $commentCount); + + return $content . $levelHtml; + + } catch (Exception $e) { + // 出错时返回原内容,确保评论不消失 + return $content; + } + } + + /** + * 根据邮箱获取用户信息 + */ + private static function getUserInfoByEmail($email) + { + if (!$email) return null; + + try { + $db = Typecho_Db::get(); + $user = $db->fetchRow($db->select('uid', 'name', 'mail', 'group', 'url') + ->from('table.users') + ->where('mail = ?', $email) + ->limit(1)); + + return $user; + } catch (Exception $e) { + return null; + } + } + + /** + * 根据邮箱获取用户评论数 + */ + private static function getUserCommentCountByEmail($email) + { + if (!$email) return 0; + + try { + $db = Typecho_Db::get(); + $result = $db->fetchAll($db->select(array('COUNT(cid)' => 'commentNum')) + ->from('table.comments') + ->where('mail = ?', $email)); + + return $result && isset($result[0]['commentNum']) ? intval($result[0]['commentNum']) : 0; + } catch (Exception $e) { + return 0; + } + } + + /** + * 获取用户等级信息 + */ + private static function getUserLevel($commentCount, $options) + { + // 解析等级配置 + $levels = self::parseLevelConfig($options); + + $currentLevel = null; + $nextLevel = null; + + // 查找当前等级 + for ($i = 0; $i < count($levels); $i++) { + if ($commentCount >= $levels[$i]['min_comments']) { + $currentLevel = $levels[$i]; + $currentLevel['index'] = $i + 1; + + // 获取下一等级 + if (isset($levels[$i + 1])) { + $nextLevel = $levels[$i + 1]; + } + } else { + break; + } + } + + // 如果没有找到等级,使用第一个等级 + if (!$currentLevel && count($levels) > 0) { + $currentLevel = $levels[0]; + $currentLevel['index'] = 1; + if (isset($levels[1])) { + $nextLevel = $levels[1]; + } + } + + return array( + 'current' => $currentLevel, + 'next' => $nextLevel, + 'comment_count' => $commentCount + ); + } + + /** + * 解析等级配置 + */ + private static function parseLevelConfig($options) + { + $levels = array(); + + if (isset($options->level_config) && !empty($options->level_config)) { + $lines = explode("\n", $options->level_config); + foreach ($lines as $line) { + $line = trim($line); + if (empty($line)) continue; + + $parts = explode('|', $line); + if (count($parts) >= 3) { + $level = array( + 'name' => trim($parts[0]), + 'min_comments' => intval(trim($parts[1])), + 'class' => trim($parts[2]) + ); + + // 修复:正确处理颜色值,包括空值情况 + if (count($parts) >= 4) { + $color = trim($parts[3]); + if (!empty($color)) { + $level['color'] = $color; + } else { + // 如果颜色为空,使用默认等级对应的颜色 + $level['color'] = self::getDefaultColorByClass($level['class']); + } + } else { + // 如果没有颜色配置,使用默认等级对应的颜色 + $level['color'] = self::getDefaultColorByClass($level['class']); + } + + $levels[] = $level; + } + } + } + + // 如果没有配置,使用默认配置 + if (empty($levels)) { + $levels = self::$defaultLevels; + } + + // 按评论数排序 + usort($levels, function($a, $b) { + return $a['min_comments'] - $b['min_comments']; + }); + + return $levels; + } + + /** + * 根据等级类名获取默认颜色 + */ + private static function getDefaultColorByClass($levelClass) + { + foreach (self::$defaultLevels as $level) { + if ($level['class'] == $levelClass) { + return $level['color']; + } + } + return '#666'; // 默认颜色 + } + + /** + * 生成等级HTML - 修复:使用内联样式确保颜色生效 + */ + private static function generateLevelHtml($levelInfo, $commentCount) + { + if (!$levelInfo['current']) { + return ''; + } + + $current = $levelInfo['current']; + $next = $levelInfo['next']; + + // 构建title提示文本 + $title = htmlspecialchars($current['name']); + $title .= " · {$commentCount}评"; + + if ($next) { + $needed = $next['min_comments'] - $commentCount; + if ($needed > 0) { + $title .= " · 再{$needed}评升至" . htmlspecialchars($next['name']); + } + } + + // 生成HTML - 使用行内样式确保颜色生效 + $html = sprintf( + '', + $current['class'], + $title, + $current['color'] + ); + + $html .= ''; + $html .= sprintf('%d', $current['index']); + $html .= ''; + + return $html; + } + + /** + * 获取用户活跃时间 + */ + public static function getActiveTime($userId) + { + if (!$userId) return 0; + + try { + $db = Typecho_Db::get(); + $user = $db->fetchRow($db->select('activated') + ->from('table.users') + ->where('uid = ?', $userId)); + + return $user && isset($user['activated']) ? intval($user['activated']) : 0; + } catch (Exception $e) { + return 0; + } + } + + /** + * 格式化时间显示 + */ + public static function formatTime($timestamp, $options = null) + { + // 如果未提供options,尝试获取插件配置 + if (!$options) { + try { + $options = Typecho_Widget::widget('Widget_Options')->plugin('RecentlyActive'); + } catch (Exception $e) { + $options = (object)[ + 'display_mode' => 'smart', + 'date_format' => 'Y-m-d H:i', + 'default_text' => '从未活跃' + ]; + } + } + + if (!$timestamp || $timestamp == 0) { + return isset($options->default_text) ? $options->default_text : '从未活跃'; + } + + $currentTime = time(); + $diff = $currentTime - $timestamp; + + $displayMode = isset($options->display_mode) ? $options->display_mode : 'smart'; + + // 相对时间模式 - 始终返回相对时间 + if ($displayMode == 'relative') { + return self::getRelativeTime($diff); + } + + // 智能模式 + if ($displayMode == 'smart') { + if ($diff < 86400) { // 24小时内 + return self::getRelativeTime($diff); + } else { + $format = isset($options->date_format) ? $options->date_format : 'Y-m-d H:i'; + return date($format, $timestamp); + } + } + + // 绝对时间模式 + $format = isset($options->date_format) ? $options->date_format : 'Y-m-d H:i'; + return date($format, $timestamp); + } + + /** + * 获取相对时间文本 + */ + private static function getRelativeTime($diff) + { + if ($diff < 60) { + return '刚刚'; + } elseif ($diff < 3600) { + $minutes = floor($diff / 60); + return $minutes . '分钟前'; + } elseif ($diff < 86400) { + $hours = floor($diff / 3600); + return $hours . '小时前'; + } elseif ($diff < 2592000) { // 30天 + $days = floor($diff / 86400); + return $days . '天前'; + } elseif ($diff < 31536000) { // 365天 + $months = floor($diff / 2592000); + return $months . '个月前'; + } else { + $years = floor($diff / 31536000); + return $years . '年前'; + } + } + + /** + * 判断是否在线 + */ + private static function isOnline($timestamp, $threshold) + { + if (!$timestamp) return false; + + $currentTime = time(); + $diff = $currentTime - $timestamp; + $thresholdSeconds = $threshold * 60; // 转换为秒 + + return $diff < $thresholdSeconds; + } + + /** + * 前端显示函数 - 核心方法 + */ + public static function show($userId = null, $customOptions = array()) + { + if (!$userId) { + $user = Typecho_Widget::widget('Widget_User'); + $userId = $user->uid; + } + + if (!$userId) { + return ''; + } + + $activeTime = self::getActiveTime($userId); + + // 获取插件配置 + $options = null; + try { + $optionsObj = Typecho_Widget::widget('Widget_Options'); + if ($optionsObj) { + $options = $optionsObj->plugin('RecentlyActive'); + } + } catch (Exception $e) { + // 忽略异常,使用默认配置 + } + + if (!$options) { + $options = (object)[ + 'display_mode' => 'smart', + 'date_format' => 'Y-m-d H:i', + 'default_text' => '从未活跃', + 'online_threshold' => '10', + 'show_dot' => '1' + ]; + } + + // 合并自定义选项 + if (!empty($customOptions)) { + foreach ($customOptions as $key => $value) { + $options->$key = $value; + } + } + + $formattedTime = self::formatTime($activeTime, $options); + + // 构建CSS类 + $classes = array('recently-active'); + + $onlineThreshold = isset($options->online_threshold) ? intval($options->online_threshold) : 10; + if (self::isOnline($activeTime, $onlineThreshold)) { + $classes[] = 'online'; + } else { + $classes[] = 'offline'; + } + + // 是否显示状态点 + $showDot = isset($options->show_dot) ? $options->show_dot : '1'; + if ($showDot == '0') { + $classes[] = 'no-dot'; + } + + $classStr = implode(' ', $classes); + + // 完整时间用于title提示 + $fullTime = $activeTime ? date('Y-m-d H:i:s', $activeTime) : '从未活跃'; + + return sprintf( + '%s', + $classStr, + htmlspecialchars($fullTime), + htmlspecialchars($formattedTime) + ); + } + + /** + * 获取最近活跃用户列表(侧边栏小工具) + */ + public static function getActiveUsers($limit = 5) + { + try { + $db = Typecho_Db::get(); + $users = $db->fetchAll($db->select('uid', 'name', 'mail', 'url', 'activated') + ->from('table.users') + ->where('activated > 0') + ->order('activated', Typecho_Db::SORT_DESC) + ->limit($limit)); + + $result = array(); + foreach ($users as $user) { + $result[] = array( + 'uid' => $user['uid'], + 'name' => $user['name'], + 'mail' => $user['mail'], + 'url' => $user['url'], + 'activated' => $user['activated'], + 'avatar' => self::getGravatar($user['mail']), + 'timeText' => self::formatTime($user['activated']) + ); + } + + return $result; + } catch (Exception $e) { + return array(); + } + } + + /** + * 获取Gravatar头像 + */ + private static function getGravatar($email, $size = 40) + { + $hash = md5(strtolower(trim($email))); + return "https://www.gravatar.com/avatar/{$hash}?s={$size}&d=identicon&r=g"; + } + + /** + * 获取用户的等级信息(公共方法,可在主题中使用) + */ + public static function getUserLevelInfo($userId = null) + { + if (!$userId) { + $user = Typecho_Widget::widget('Widget_User'); + $userId = $user->uid; + } + + if (!$userId) { + return null; + } + + try { + $options = Typecho_Widget::widget('Widget_Options')->plugin('RecentlyActive'); + $commentCount = self::getUserCommentCount($userId); + return self::getUserLevel($commentCount, $options); + } catch (Exception $e) { + return null; + } + } + + /** + * 显示用户等级(公共方法,可在主题中使用) + */ + public static function showUserLevel($userId = null) + { + if (!$userId) { + return ''; + } + + try { + // 获取插件配置 + $options = Typecho_Widget::widget('Widget_Options')->plugin('RecentlyActive'); + $enableUserLevel = isset($options->enable_user_level) ? $options->enable_user_level : '1'; + + // 如果禁用等级显示,直接返回空 + if ($enableUserLevel == '0') { + return ''; + } + + // 获取用户信息 + $userInfo = self::getUserInfoById($userId); + + // 判断是否是管理员 + $isAdmin = false; + if ($userInfo && isset($userInfo['group'])) { + $isAdmin = ($userInfo['group'] == 'administrator'); + } + + // 如果是管理员,显示博主标识 + if ($isAdmin) { + $adminLabel = isset($options->admin_label) ? $options->admin_label : ''; + return ''; + } + + // 获取用户评论数 + $commentCount = self::getUserCommentCount($userId); + + // 获取用户等级 + $levelInfo = self::getUserLevel($commentCount, $options); + + // 生成等级HTML + return self::generateLevelHtml($levelInfo, $commentCount); + + } catch (Exception $e) { + // 出错时返回空,不影响页面显示 + return ''; + } + } + + /** + * 根据用户ID获取用户信息 + */ + private static function getUserInfoById($userId) + { + if (!$userId) return null; + + try { + $db = Typecho_Db::get(); + $user = $db->fetchRow($db->select('uid', 'name', 'mail', 'group', 'url') + ->from('table.users') + ->where('uid = ?', $userId) + ->limit(1)); + + return $user; + } catch (Exception $e) { + return null; + } + } + + /** + * 获取用户评论数(通过用户ID) + */ + private static function getUserCommentCount($userId) + { + if (!$userId) return 0; + + try { + $db = Typecho_Db::get(); + $result = $db->fetchRow($db->select('COUNT(*) as cnt') + ->from('table.comments') + ->where('authorId = ?', $userId) + ->where('status = ?', 'approved')); + + return $result ? intval($result['cnt']) : 0; + } catch (Exception $e) { + return 0; + } + } + + /** + * 前端调用:显示楼层号(静态方法,可在主题中调用)- 修复版本 + */ + public static function showFloorNumber($coid = null, $cid = null, $customOptions = array()) + { + if (!$coid || !$cid) { + return ''; + } + + try { + // 获取插件配置 + $options = null; + try { + $optionsObj = Typecho_Widget::widget('Widget_Options'); + if ($optionsObj) { + $options = $optionsObj->plugin('RecentlyActive'); + } + } catch (Exception $e) { + // 忽略异常,使用默认配置 + } + + if (!$options) { + $options = (object)[ + 'enable_floor' => '1', + 'floor_names' => '沙发,板凳,地板', + 'parent_floor_format' => '#楼层', + 'sub_floor_names' => 'B1,B2,B3', + 'sub_floor_format' => 'B#楼层' + ]; + } + + // 合并自定义选项 + if (!empty($customOptions)) { + foreach ($customOptions as $key => $value) { + $options->$key = $value; + } + } + + $enableFloor = isset($options->enable_floor) ? $options->enable_floor : '1'; + + // 如果禁用楼层显示,直接返回空 + if ($enableFloor == '0') { + return ''; + } + + // 判断评论类型(需要查询数据库获取评论信息) + $commentInfo = self::getCommentInfo($coid); + + if (!$commentInfo) { + return ''; + } + + $isParentComment = ($commentInfo['parent'] == 0); + + if ($isParentComment) { + // 父评论楼层 + $floorNumber = self::getParentCommentFloorNumber($coid, $cid); + $floorText = self::getParentFloorText($floorNumber, $options); + $class = 'parent-floor'; + $style = 'margin-right: 5px; font-weight: bold; color: #666;'; + } else { + // 子评论楼层 - 使用新的正确计数方法 + $parentId = $commentInfo['parent']; + $subFloorNumber = self::getSubCommentFloorNumberCorrect($coid, $parentId, $cid); + $floorText = self::getSubFloorText($subFloorNumber, $options); + $class = 'sub-floor'; + $style = 'margin-right: 5px; font-weight: bold; color: #888;'; + } + + // 生成楼层HTML + return '' . htmlspecialchars($floorText) . ''; + + } catch (Exception $e) { + // 出错时返回空 + return ''; + } + } + + /** + * 获取父评论楼层文本 + */ + private static function getParentFloorText($floorNumber, $options) + { + // 获取父评论楼层名称配置 + $floorNames = array(); + if (isset($options->floor_names) && !empty($options->floor_names)) { + $floorNames = explode(',', $options->floor_names); + } + + // 如果配置为空,使用默认名称 + if (empty($floorNames)) { + $floorNames = self::$defaultFloorNames; + } + + // 根据楼层数获取显示文本 + if ($floorNumber <= count($floorNames) && $floorNumber > 0) { + return $floorNames[$floorNumber - 1]; + } else { + // 使用格式替换 + $parentFloorFormat = isset($options->parent_floor_format) ? $options->parent_floor_format : '#楼层'; + return str_replace('#楼层', $floorNumber, $parentFloorFormat); + } + } + + /** + * 获取子评论楼层文本 + */ + private static function getSubFloorText($subFloorNumber, $options) + { + // 获取子评论楼层名称配置 + $subFloorNames = array(); + if (isset($options->sub_floor_names) && !empty($options->sub_floor_names)) { + $subFloorNames = explode(',', $options->sub_floor_names); + } + + // 如果配置为空,使用默认名称 + if (empty($subFloorNames)) { + $subFloorNames = self::$defaultSubFloorNames; + } + + // 根据楼层数获取显示文本 + if ($subFloorNumber <= count($subFloorNames) && $subFloorNumber > 0) { + return $subFloorNames[$subFloorNumber - 1]; + } else { + // 使用格式替换 + $subFloorFormat = isset($options->sub_floor_format) ? $options->sub_floor_format : 'B#楼层'; + return str_replace('#楼层', $subFloorNumber, $subFloorFormat); + } + } + + /** + * 获取评论信息 + */ + private static function getCommentInfo($coid) + { + try { + $db = Typecho_Db::get(); + $comment = $db->fetchRow($db->select('coid', 'parent', 'cid') + ->from('table.comments') + ->where('coid = ?', $coid) + ->limit(1)); + + return $comment; + } catch (Exception $e) { + return null; + } + } +} \ No newline at end of file diff --git a/使用说明.md b/使用说明.md new file mode 100644 index 0000000..63c8334 --- /dev/null +++ b/使用说明.md @@ -0,0 +1,396 @@ +Typecho RecentlyActive 插件使用文档 +插件信息 +插件名称: RecentlyActive + +功能: 显示用户最近活跃时间 + +版本: 1.0.0 + +兼容性: Typecho 1.x + +作者: Your Name + +网站: https://yourwebsite.com + +功能特点 +✅ 直接读取 Typecho 自带的 activated 字段,无需额外数据库操作 +✅ 支持三种时间显示模式:相对时间、绝对时间、智能模式 +✅ 自动判断在线/离线状态 +✅ 显示状态点(可配置开关) +✅ Tooltip 提示完整时间 +✅ 侧边栏最近活跃用户列表 +✅ 后台全局配置,无需用户单独设置 +✅ 轻量级,不影响性能 + +安装方法 +方法一:手动安装 +下载插件压缩包 + +解压得到 RecentlyActive 文件夹 + +上传到 Typecho 插件目录:/usr/plugins/ + +确保路径为:/usr/plugins/RecentlyActive/Plugin.php + +登录 Typecho 后台,进入"控制台" → "插件" + +找到"用户最近活跃时间显示插件",点击"启用" + +方法二:服务器安装 +bash +# 进入插件目录 +cd /path/to/typecho/usr/plugins/ + +# 创建插件目录 +mkdir RecentlyActive + +# 上传 Plugin.php 到该目录 +后台配置 +激活插件后,在插件管理页面点击"设置"进行配置: + +基本设置 +配置项 说明 默认值 +时间显示模式 相对时间/绝对时间/智能模式 智能模式 +时间格式 绝对时间显示格式(PHP date()格式) Y-m-d H:i +在线状态阈值 多少分钟内显示为"在线"状态(分钟) 10 +状态点显示 是否显示在线/离线状态点 开启 +默认显示文字 用户从未活跃时显示的文字 从未活跃 +配置说明 +1. 时间显示模式 +相对时间: 显示"3小时前"、"2天前"等 + +绝对时间: 显示"2023-01-01 12:00" + +智能模式: 24小时内用相对时间,更早用绝对时间 + +2. 时间格式 +支持所有 PHP date() 函数格式,常用格式: + +Y-m-d H:i → 2023-01-01 12:00 + +m/d H:i → 01/01 12:00 + +H:i → 12:00 + +Y年m月d日 H:i → 2023年01月01日 12:00 + +3. 在线状态阈值 +设置用户多少分钟内活跃算"在线" + +例如:设为10,则表示10分钟内活跃的用户显示为在线状态(绿色) + +主题调用方法 +基本调用 +显示文章作者活跃时间 +php +authorId): ?> +
+ 作者: author(); ?> + authorId); ?> +
+ +显示评论者活跃时间 +php +comments()->to($comments); ?> +next()): ?> +
+
+ author(); ?> + authorId); ?> +
+
+ content(); ?> +
+
+ +安全调用(推荐) +php +authorId && class_exists('RecentlyActive_Plugin')) { + echo RecentlyActive_Plugin::show($this->authorId); +} +?> +侧边栏活跃用户列表 +php + + + +
+

最近活跃用户

+ +
+ + +自定义参数调用 +php + 'relative', // 强制使用相对时间 + 'online_threshold' => 30, // 30分钟内算在线 + 'show_dot' => 0, // 不显示状态点 + 'date_format' => 'H:i', // 时间格式:只显示时分 + 'default_text' => '暂无记录' // 自定义默认文字 +)); +?> +参数说明 +参数 类型 说明 默认值 +display_mode string 显示模式:relative/absolute/smart 插件设置 +online_threshold int 在线状态阈值(分钟) 插件设置 +show_dot int 是否显示状态点:1显示/0不显示 插件设置 +date_format string 绝对时间格式 插件设置 +default_text string 默认显示文字 插件设置 +CSS 样式定制 +插件自带基本样式,如需自定义可覆盖以下 CSS 类: + +css +/* 活跃时间基础样式 */ +.recently-active { + font-size: 12px; + color: #666; + margin-left: 5px; +} + +/* 在线状态 */ +.recently-active.online { + color: #52c41a; /* 绿色 */ +} + +/* 离线状态 */ +.recently-active.offline { + color: #999; /* 灰色 */ +} + +/* 状态点 */ +.recently-active.online:before { + content: "● "; + font-size: 10px; +} + +.recently-active.offline:before { + content: "○ "; + font-size: 10px; +} + +/* 不显示状态点 */ +.recently-active.no-dot:before { + content: "" !important; +} + +/* 工具提示 */ +.recently-active-tooltip { + cursor: help; + border-bottom: 1px dotted #ccc; +} + +/* 侧边栏活跃用户列表 */ +.active-users { + list-style: none; + padding: 0; + margin: 0; +} + +.active-users li { + padding: 8px 0; + border-bottom: 1px solid #eee; + display: flex; + align-items: center; +} + +.active-users li:last-child { + border-bottom: none; +} + +.active-users img { + border-radius: 50%; + margin-right: 10px; +} + +.active-users .user-name { + flex: 1; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.active-users .active-time { + font-size: 11px; + color: #999; +} +显示效果示例 +1. 在线状态 +text +作者:张三 ● 3分钟前 +(绿色文字,带实心圆点) + +2. 离线状态 +text +作者:李四 ○ 3小时前 +(灰色文字,带空心圆点) + +3. 智能模式 +24小时内:2小时前 + +24小时外:2023-01-01 12:00 + +4. Tooltip 提示 +鼠标悬停在活跃时间上,显示完整时间: + +text +最后活跃: 2023-12-10 14:30:25 +常见问题 +Q1: 插件激活后没有任何显示? +A: 请检查: + +插件是否已激活 + +主题中是否正确调用 RecentlyActive_Plugin::show() + +用户是否有 activated 字段值 + +查看网页源代码,确认是否有输出 + +Q2: 显示"从未活跃"是什么情况? +A: 表示该用户的 activated 字段为 0 或空,可能是: + +用户从未登录过 + +Typecho 未更新该字段 + +用户数据异常 + +Q3: 如何修改在线状态的颜色? +A: 在主题CSS中添加: + +css +.recently-active.online { + color: #ff0000; /* 改为红色 */ +} +Q4: 侧边栏头像不显示? +A: Gravatar 可能需要科学上网,可以替换为本地头像: + +php +// 修改 Plugin.php 中的 getGravatar 方法 +private static function getGravatar($email, $size = 40) +{ + // 使用本地默认头像 + return Helper::options()->themeUrl . '/images/default-avatar.png'; +} +Q5: 时间显示不正确? +A: 检查服务器时区设置: + +Typecho 后台 → 设置 → 时区 + +服务器 PHP 时区设置 + +确保时间戳正确 + +高级用法 +在 functions.php 中添加辅助函数 +php +// 在主题的 functions.php 中添加 +if (!function_exists('showActiveTime')) { + /** + * 显示用户活跃时间(简化调用) + * @param int $userId 用户ID + * @param array $options 自定义选项 + * @return string + */ + function showActiveTime($userId = null, $options = array()) + { + if (class_exists('RecentlyActive_Plugin')) { + return RecentlyActive_Plugin::show($userId, $options); + } + return ''; + } +} + +// 使用 +echo showActiveTime($this->authorId); +获取原始活跃时间戳 +php + +判断用户是否在线 +php +在线'; + } else { + echo '离线'; + } +} +?> +文件结构 +text +RecentlyActive/ +├── Plugin.php # 插件主文件 +├── README.md # 说明文档(本文件) +└── screenshot.png # 插件截图(可选) +更新日志 +v1.0.0 (2023-12-10) +✅ 首次发布 + +✅ 基本活跃时间显示功能 + +✅ 三种时间显示模式 + +✅ 在线状态判断 + +✅ 侧边栏活跃用户列表 + +✅ 后台配置界面 + +技术支持 +如有问题,可通过以下方式联系: + +在插件发布页面留言 + +访问作者网站 + +GitHub Issues(如有) + +注意事项 +本插件依赖 Typecho 的 activated 字段,请确保该字段正常工作 + +部分主题可能需要调整 CSS 样式以适应显示 + +建议在正式使用前进行测试 + +定期备份数据库 + +许可证 +MIT License + +祝您使用愉快! 🎉 + +文档版本:v1.0.0 +最后更新:2023-12-10 +作者:Your Name \ No newline at end of file diff --git a/插件设置.txt b/插件设置.txt new file mode 100644 index 0000000..7ed2b13 --- /dev/null +++ b/插件设置.txt @@ -0,0 +1,73 @@ +等级配置 +偶遇|0|vip1|#c0c0c0 +同程|1|vip2|#a0a0a0 +涉溪|20|vip3|#9e7a5d +穿林|60|vip4|#b87333 +览峰|150|vip5|#cb6d1e +渡川|300|vip6|#cc8400 +聆泉|600|vip7|#d4af37 +沐霞|1200|vip8|#ffb800 +共云|2400|vip9|#ffa500 +印雪|4800|vip10|#ff8c00 +望星|9600|vip11|#da70d6 +归真|19200|vip12|#a42be2 + +图标字体CSS + +.vipicon { + font-family: "FontAwesome", "iconfont"; + font-style: normal; + font-weight: normal; + speak: none; + display: inline-block; + text-decoration: inherit; + text-align: center; + font-variant: normal; + text-transform: none; + line-height: 1em; +} +.vipicon:before { + content: "\e66a"; + font-size: 14px; +} +.com-level { display: inline-block; margin-left: 3px; } +.com-level sub { + font-size: 11px; + vertical-align: baseline; + position: relative; + top: -0.5em; + font-weight: bold; +} +/* 等级颜色定义 - 同时应用到图标和数字 */ +.com-level.vip1 .vipicon, +.com-level.vip1 sub { color: #999; } +.com-level.vip2 .vipicon, +.com-level.vip2 sub { color: #8c8c8c; } +.com-level.vip3 .vipicon, +.com-level.vip3 sub { color: #666; } +.com-level.vip4 .vipicon, +.com-level.vip4 sub { color: #52c41a; } +.com-level.vip5 .vipicon, +.com-level.vip5 sub { color: #1890ff; } +.com-level.vip6 .vipicon, +.com-level.vip6 sub { color: #722ed1; } +.com-level.vip7 .vipicon, +.com-level.vip7 sub { color: #faad14; } +.com-level.vip8 .vipicon, +.com-level.vip8 sub { color: #f5222d; } +.com-level.vip9 .vipicon, +.com-level.vip9 sub { color: #eb2f96; } +.com-level.vip10 .vipicon, +.com-level.vip10 sub { color: #fa541c; } +.com-level.vip11 .vipicon, +.com-level.vip11 sub { color: #13c2c2; } +.com-level.vip12 .vipicon, +.com-level.vip12 sub { color: #000; } +/* 动态CSS - 确保用户自定义的颜色通过内联样式生效 */ +.com-level .vipicon, +.com-level sub { + color: inherit !important; +} +/* 博主颜色 */ +.com-level.blogger .vipicon, +.com-level.blogger sub { color: #f5222d !important; } \ No newline at end of file