From bdc98b18441d37440e30db2ef0f9f9873c5b1964 Mon Sep 17 00:00:00 2001
From: XIGE <710062962@qq.com>
Date: Mon, 23 Feb 2026 21:09:17 +0800
Subject: [PATCH] 1.0
---
Plugin.php | 895 +++++++++++++++++++++++++++++++++++++++
assets/who-read-this.css | 282 ++++++++++++
assets/who-read-this.js | 150 +++++++
前端调用.txt | 1 +
4 files changed, 1328 insertions(+)
create mode 100644 Plugin.php
create mode 100644 assets/who-read-this.css
create mode 100644 assets/who-read-this.js
create mode 100644 前端调用.txt
diff --git a/Plugin.php b/Plugin.php
new file mode 100644
index 0000000..c063b76
--- /dev/null
+++ b/Plugin.php
@@ -0,0 +1,895 @@
+getPrefix();
+
+ $table = $prefix . self::$_table;
+ $sql = "CREATE TABLE IF NOT EXISTS `{$table}` (
+ `id` int(11) NOT NULL AUTO_INCREMENT,
+ `cid` int(11) NOT NULL COMMENT '文章ID',
+ `uid` int(11) DEFAULT 0 COMMENT '用户ID',
+ `username` varchar(200) DEFAULT NULL COMMENT '用户名',
+ `email` varchar(200) DEFAULT NULL COMMENT '用户邮箱',
+ `website` varchar(500) DEFAULT NULL COMMENT '用户网站',
+ `avatar` varchar(500) DEFAULT NULL COMMENT '头像URL',
+ `ip` varchar(45) DEFAULT NULL COMMENT 'IP地址',
+ `created` int(10) NOT NULL COMMENT '阅读时间',
+ PRIMARY KEY (`id`),
+ KEY `cid` (`cid`),
+ KEY `uid` (`uid`),
+ KEY `ip` (`ip`)
+ ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;";
+
+ try {
+ $db->query($sql);
+ } catch (Exception $e) {
+ // 表可能已经存在
+ }
+
+ // 挂载文章页
+ Typecho_Plugin::factory('Widget_Archive')->footer = array(__CLASS__, 'trackVisitor');
+
+ return _t('插件已激活,请在后台设置中开启功能');
+ }
+
+ /**
+ * 禁用插件
+ */
+ public static function deactivate()
+ {
+ return _t('插件已禁用');
+ }
+
+ /**
+ * 插件配置方法
+ */
+ public static function config(Typecho_Widget_Helper_Form $form)
+ {
+ // 是否开启功能
+ $enable = new Typecho_Widget_Helper_Form_Element_Radio(
+ 'enable',
+ array('1' => _t('开启'), '0' => _t('关闭')),
+ '1',
+ _t('是否开启功能'),
+ _t('关闭后前端将不会显示谁读了本文')
+ );
+ $form->addInput($enable);
+
+ // 是否屏蔽管理员记录和显示
+ $hideAdmin = new Typecho_Widget_Helper_Form_Element_Radio(
+ 'hide_admin',
+ array(
+ '1' => _t('是(不记录也不显示)'),
+ '0' => _t('否(正常记录和显示)')
+ ),
+ '0',
+ _t('是否屏蔽管理员'),
+ _t('开启后管理员阅读本文不会被记录,也不会在列表中显示')
+ );
+ $form->addInput($hideAdmin);
+
+ // 显示模式
+ $mode = new Typecho_Widget_Helper_Form_Element_Radio(
+ 'mode',
+ array(
+ 'all' => _t('显示所有阅读者'),
+ 'login' => _t('仅显示登录用户'),
+ 'recent' => _t('仅显示近期阅读者(24小时内)')
+ ),
+ 'all',
+ _t('显示模式'),
+ _t('选择要显示的用户类型')
+ );
+ $form->addInput($mode);
+
+ // 显示数量限制
+ $limit = new Typecho_Widget_Helper_Form_Element_Text(
+ 'limit',
+ NULL,
+ '20',
+ _t('最大显示数量'),
+ _t('最多显示多少个阅读者(0表示不限制)')
+ );
+ $limit->input->setAttribute('class', 'mini');
+ $limit->addRule('isInteger', _t('请输入数字'));
+ $form->addInput($limit);
+
+ // IP去重时间(小时)
+ $ip_interval = new Typecho_Widget_Helper_Form_Element_Text(
+ 'ip_interval',
+ NULL,
+ '24',
+ _t('IP去重时间(小时)'),
+ _t('同一IP多少小时内不重复记录')
+ );
+ $ip_interval->input->setAttribute('class', 'mini');
+ $ip_interval->addRule('isInteger', _t('请输入数字'));
+ $form->addInput($ip_interval);
+
+ // 自定义标题
+ $title = new Typecho_Widget_Helper_Form_Element_Text(
+ 'title',
+ NULL,
+ '谁读了本文',
+ _t('显示标题'),
+ _t('前端显示的标题文字')
+ );
+ $form->addInput($title);
+
+ // 展开默认状态
+ $default_open = new Typecho_Widget_Helper_Form_Element_Radio(
+ 'default_open',
+ array('0' => _t('收起'), '1' => _t('展开')),
+ '0',
+ _t('默认状态'),
+ _t('初始显示时是展开还是收起状态')
+ );
+ $form->addInput($default_open);
+
+ // 头像大小
+ $avatar_size = new Typecho_Widget_Helper_Form_Element_Text(
+ 'avatar_size',
+ NULL,
+ '40',
+ _t('头像尺寸(像素)'),
+ _t('头像显示的大小,单位:像素')
+ );
+ $avatar_size->input->setAttribute('class', 'mini');
+ $avatar_size->addRule('isInteger', _t('请输入数字'));
+ $form->addInput($avatar_size);
+ }
+
+ /**
+ * 配置保存处理
+ */
+ public static function configHandle($settings, $isInit)
+ {
+ // 确保所有设置都有值
+ $defaults = array(
+ 'enable' => '1',
+ 'hide_admin' => '0',
+ 'mode' => 'all',
+ 'limit' => '20',
+ 'ip_interval' => '24',
+ 'title' => '谁读了本文',
+ 'default_open' => '0',
+ 'avatar_size' => '40'
+ );
+
+ foreach ($defaults as $key => $value) {
+ if (!isset($settings[$key])) {
+ $settings[$key] = $value;
+ }
+ }
+
+ Helper::configPlugin('WhoReadThis', $settings);
+ }
+
+ /**
+ * 个人用户配置面板
+ */
+ public static function personalConfig(Typecho_Widget_Helper_Form $form) {}
+
+ /**
+ * 追踪访问者并输出资源
+ */
+ public static function trackVisitor()
+ {
+ $archive = Typecho_Widget::widget('Widget_Archive');
+ if (!$archive->is('single')) {
+ return;
+ }
+
+ $options = Helper::options()->plugin('WhoReadThis');
+ if (!$options || $options->enable == '0') {
+ return;
+ }
+
+ $cid = $archive->cid;
+ $ip = self::getClientIP();
+
+ // 检查是否屏蔽管理员
+ $hideAdmin = isset($options->hide_admin) ? $options->hide_admin : '0';
+ $user = Typecho_Widget::widget('Widget_User');
+ $isAdmin = $user->hasLogin() && $user->pass('administrator', true);
+
+ if ($hideAdmin == '1' && $isAdmin) {
+ return; // 如果是管理员且开启屏蔽,则不记录
+ }
+
+ // 检查IP是否在指定时间内已记录
+ $db = Typecho_Db::get();
+ $table = $db->getPrefix() . self::$_table;
+ $ipInterval = isset($options->ip_interval) ? intval($options->ip_interval) : 24;
+
+ $checkSql = $db->select('id')
+ ->from($table)
+ ->where('cid = ?', $cid)
+ ->where('ip = ?', $ip)
+ ->where('created > ?', time() - ($ipInterval * 3600))
+ ->limit(1);
+
+ $check = $db->fetchRow($checkSql);
+
+ if (!$check) {
+ // 获取当前用户信息
+ $uid = $user->hasLogin() ? $user->uid : 0;
+ $username = $user->hasLogin() ? $user->screenName : '访客';
+ $email = $user->hasLogin() ? $user->mail : '';
+ $website = $user->hasLogin() ? $user->url : '';
+
+ // 获取头像
+ $avatar = '';
+ if ($email) {
+ $avatar = 'https://cravatar.cn/avatar/' . md5(strtolower(trim($email))) . '?s=200&d=identicon';
+ } else {
+ // 为访客生成随机头像
+ $avatar = 'https://cravatar.cn/avatar/' . md5($ip) . '?s=200&d=identicon';
+ }
+
+ // 记录阅读者
+ $insert = $db->insert($table)->rows(array(
+ 'cid' => $cid,
+ 'uid' => $uid,
+ 'username' => $username,
+ 'email' => $email,
+ 'website' => $website,
+ 'avatar' => $avatar,
+ 'ip' => $ip,
+ 'created' => time()
+ ));
+
+ $db->query($insert);
+ }
+
+ // 输出资源文件(只输出一次)
+ if (!self::$_assetsOutput) {
+ echo '';
+ echo '';
+
+ echo '';
+
+ self::$_assetsOutput = true;
+ }
+ }
+
+ /**
+ * 前端调用函数
+ */
+ public static function show()
+ {
+ $archive = Typecho_Widget::widget('Widget_Archive');
+ if (!$archive->is('single')) {
+ return '';
+ }
+
+ $options = Helper::options()->plugin('WhoReadThis');
+ if (!$options || $options->enable == '0') {
+ return '';
+ }
+
+ $cid = $archive->cid;
+ $db = Typecho_Db::get();
+ $table = $db->getPrefix() . self::$_table;
+
+ // 构建查询
+ $select = $db->select()->from($table)->where('cid = ?', $cid);
+
+ // 如果开启了屏蔽管理员,排除管理员记录
+ $hideAdmin = isset($options->hide_admin) ? $options->hide_admin : '0';
+ if ($hideAdmin == '1') {
+ // 获取所有管理员ID
+ $adminUsers = $db->fetchAll($db->select('uid')->from('table.users')->where('group = ?', 'administrator'));
+ if ($adminUsers) {
+ $adminIds = array_column($adminUsers, 'uid');
+ if (!empty($adminIds)) {
+ $select->where('uid NOT IN ?', $adminIds);
+ }
+ }
+ }
+
+ $mode = isset($options->mode) ? $options->mode : 'all';
+ switch ($mode) {
+ case 'login':
+ $select->where('uid > 0');
+ break;
+ case 'recent':
+ $select->where('created > ?', time() - 86400); // 24小时内
+ break;
+ }
+
+ $select->order('id', Typecho_Db::SORT_DESC);
+
+ $limit = isset($options->limit) ? intval($options->limit) : 20;
+ if ($limit > 0) {
+ $select->limit($limit);
+ }
+
+ try {
+ $readers = $db->fetchAll($select);
+ } catch (Exception $e) {
+ return '';
+ }
+
+ // 如果没有阅读者数据,返回空字符串(不显示任何内容)
+ if (empty($readers)) {
+ return '';
+ }
+
+ $usersHtml = '';
+ $uniqueReaders = array();
+
+ foreach ($readers as $reader) {
+ // 去重,同个用户只显示一次
+ $key = $reader['uid'] ?: $reader['ip'];
+ if (isset($uniqueReaders[$key])) {
+ continue;
+ }
+ $uniqueReaders[$key] = true;
+
+ $avatar = $reader['avatar'] ?: 'https://cravatar.cn/avatar/0?s=200&d=identicon';
+ $username = htmlspecialchars($reader['username']);
+ $link = !empty($reader['website']) ? $reader['website'] : '#';
+
+ $usersHtml .= '
+
+
+
 . ')
+
+
+ ';
+ }
+
+ $count = count($uniqueReaders);
+ $defaultOpen = isset($options->default_open) ? $options->default_open : '0';
+ $isOpen = $defaultOpen == '1' ? ' expanded' : ' collapsed';
+ $title = isset($options->title) ? $options->title : '谁读了本文';
+ $avatarSize = isset($options->avatar_size) ? intval($options->avatar_size) : 40;
+
+ // 生成唯一的容器ID
+ $containerId = 'whoreadthis-' . $cid;
+
+ $html = '';
+
+ return $html;
+ }
+
+ /**
+ * 获取插件CSS样式(包含深色模式)
+ */
+ private static function getPluginStyles()
+ {
+ return '
+ /* WhoReadThis 插件专用样式 - 包含深色模式 */
+ .whoreadthis-container {
+ border-radius: 10px;
+ overflow: hidden;
+ position: relative;
+ z-index: 10;
+ margin: 0 0 30px 0;
+ clear: both;
+ box-sizing: border-box;
+ }
+
+ /* 标题区域 */
+ .whoreadthis-container .whoreadthis-header-section {
+ padding: 18px 24px;
+ background: linear-gradient(135deg, #6a11cb 0%, #2575fc 100%);
+ position: relative;
+ z-index: 20;
+ box-sizing: border-box;
+ }
+
+ /* 深色模式标题区域 */
+ body.dark .whoreadthis-container .whoreadthis-header-section,
+ body.dark-mode .whoreadthis-container .whoreadthis-header-section,
+ body.theme-dark .whoreadthis-container .whoreadthis-header-section,
+ .dark .whoreadthis-container .whoreadthis-header-section,
+ .dark-mode .whoreadthis-container .whoreadthis-header-section {
+ background: rgb(10 12 25 / 1);
+ color:rgb(156 163 175 / var(--tw-text-opacity))!important;
+ }
+
+ /* 标题文字 */
+ .whoreadthis-container .whoreadthis-header {
+ margin: 0;
+ font-size: 18px;
+ color: #fff;
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ cursor: pointer;
+ user-select: none;
+ font-weight: 600;
+ position: relative;
+ z-index: 25;
+ box-sizing: border-box;
+ }
+
+ .dark .whoreadthis-container .whoreadthis-toggle-btn {color: rgb(156 163 175 / var(--tw-text-opacity))!important;}
+ .dark .whoreadthis-count-badge {color: rgb(156 163 175 / var(--tw-text-opacity))!important;}
+ /* 深色模式标题文字 */
+ body.dark .whoreadthis-container .whoreadthis-header h3,
+ body.dark-mode .whoreadthis-container .whoreadthis-header h3,
+ body.theme-dark .whoreadthis-container .whoreadthis-header h3,
+ .dark .whoreadthis-container .whoreadthis-header h3,
+ .dark-mode .whoreadthis-container .whoreadthis-header h3 {
+ color: rgb(156 163 175 / var(--tw-text-opacity))!important;
+ }
+
+ /* 计数徽章 */
+ .whoreadthis-container .whoreadthis-count-badge {
+ background-color: rgba(255, 255, 255, 0.2);
+ color: white;
+ padding: 4px 8px;
+ border-radius: 12px;
+ font-size: 13px;
+ margin-left: 8px;
+ display: inline-block;
+ vertical-align: middle;
+ }
+
+ /* 深色模式计数徽章 */
+ body.dark .whoreadthis-container .whoreadthis-count-badge,
+ body.dark-mode .whoreadthis-container .whoreadthis-count-badge,
+ body.theme-dark .whoreadthis-container .whoreadthis-count-badge,
+ .dark .whoreadthis-container .whoreadthis-count-badge,
+ .dark-mode .whoreadthis-container .whoreadthis-count-badge {
+ background-color: rgba(255, 255, 255, 0.1);
+ color: #e2e8f0;
+ }
+
+ /* 切换按钮 */
+ .whoreadthis-container .whoreadthis-toggle-btn {
+ font-size: 0.8em;
+ color: #fff;
+ display: flex;
+ align-items: center;
+ gap: 5px;
+ padding: 4px 10px;
+ border-radius: 4px;
+ transition: all 0.3s ease;
+ font-weight: normal;
+ border: none;
+ cursor: pointer;
+ position: relative;
+ z-index: 30;
+ background: transparent;
+ box-sizing: border-box;
+ }
+
+ .whoreadthis-container .whoreadthis-toggle-btn:hover {
+ background: rgba(255, 255, 255, 0.2);
+ }
+
+ .whoreadthis-container .whoreadthis-toggle-btn .toggle-arrow {
+ font-size: 14px;
+ line-height: 1;
+ transition: transform 0.3s ease;
+ display: inline-block;
+ }
+
+ .whoreadthis-container .whoreadthis-toggle-btn.expanded .toggle-arrow {
+ transform: rotate(180deg);
+ }
+
+ /* 内容区域 */
+ .whoreadthis-container .whoreadthis-content-section {
+ padding: 0 24px;
+ background: #f9fafb;
+ border-top: none;
+ border-radius: 0 0 10px 10px;
+ position: relative;
+ z-index: 15;
+ box-sizing: border-box;
+ }
+
+ /* 深色模式内容区域 */
+ body.dark .whoreadthis-container .whoreadthis-content-section,
+ body.dark-mode .whoreadthis-container .whoreadthis-content-section,
+ body.theme-dark .whoreadthis-container .whoreadthis-content-section,
+ .dark .whoreadthis-container .whoreadthis-content-section,
+ .dark-mode .whoreadthis-container .whoreadthis-content-section {
+ background: rgb(15 23 42 / 1);
+ }
+
+ /* 项目包装器 */
+ .whoreadthis-container .whoreadthis-items-wrapper {
+ transition: all 0.3s ease;
+ overflow: hidden;
+ position: relative;
+ z-index: 15;
+ box-sizing: border-box;
+ }
+
+ .whoreadthis-container .whoreadthis-items-wrapper.collapsed {
+ max-height: 0;
+ opacity: 0;
+ visibility: hidden;
+ padding: 0;
+ }
+
+ .whoreadthis-container .whoreadthis-items-wrapper.expanded {
+ max-height: 5000px;
+ opacity: 1;
+ visibility: visible;
+ padding: 20px 0;
+ }
+
+ /* 阅读者列表 */
+ .whoreadthis-container .whoreadthis-users {
+ display: flex;
+ flex-wrap: wrap;
+ gap: 12px;
+ margin: 0;
+ padding: 0;
+ list-style: none;
+ position: relative;
+ z-index: 40;
+ box-sizing: border-box;
+ }
+
+ .whoreadthis-container .whoreadthis-user {
+ position: relative;
+ z-index: 45;
+ box-sizing: border-box;
+ }
+
+ /* 头像包装器 */
+ .whoreadthis-container .whoreadthis-avatar-wrapper {
+ position: relative;
+ display: block;
+ z-index: 50;
+ text-decoration: none;
+ box-sizing: border-box;
+ }
+
+ /* 头像样式 */
+ .whoreadthis-container .whoreadthis-avatar {
+ width: 40px;
+ height: 40px;
+ border-radius: 50%;
+ overflow: hidden;
+ cursor: pointer;
+ display: block;
+ position: relative;
+ z-index: 55;
+ transition: transform 0.3s ease;
+ box-sizing: border-box;
+ }
+
+ .whoreadthis-container .whoreadthis-avatar:hover {
+ transform: scale(1.05);
+ }
+
+ .whoreadthis-container .whoreadthis-avatar img {
+ width: 100%;
+ height: 100%;
+ object-fit: cover;
+ border-radius: 50%;
+ padding: 2px;
+ border: 2px solid #f15a22;
+ display: block;
+ transition: transform 0.4s ease-out;
+ box-sizing: border-box;
+ }
+
+ .whoreadthis-container .whoreadthis-avatar img:hover {
+ transform: rotateZ(360deg);
+ }
+
+ /* 空状态 */
+ .whoreadthis-container .whoreadthis-empty {
+ text-align: center;
+ color: #95a5a6;
+ font-style: italic;
+ padding: 20px;
+ }
+
+ /* 响应式设计 */
+ @media (max-width: 768px) {
+ .whoreadthis-container .whoreadthis-header-section,
+ .whoreadthis-container .whoreadthis-content-section {
+ padding: 14px 18px;
+ }
+
+ .whoreadthis-container .whoreadthis-header {
+ font-size: 16px;
+ }
+
+ .whoreadthis-container .whoreadthis-users {
+ gap: 8px;
+ }
+ }
+
+ @media (max-width: 480px) {
+ .whoreadthis-container .whoreadthis-header-section,
+ .whoreadthis-container .whoreadthis-content-section {
+ padding: 12px 15px;
+ }
+
+ .whoreadthis-container .whoreadthis-header {
+ font-size: 14px;
+ }
+
+ .whoreadthis-container .whoreadthis-count-badge {
+ font-size: 11px;
+ padding: 3px 6px;
+ }
+
+ .whoreadthis-container .whoreadthis-users {
+ gap: 6px;
+ justify-content: center;
+ }
+ }
+
+ /* 动画效果 */
+ @keyframes whoreadthisFadeIn {
+ from {
+ opacity: 0;
+ transform: translateY(10px);
+ }
+ to {
+ opacity: 1;
+ transform: translateY(0);
+ }
+ }
+
+ .whoreadthis-container .whoreadthis-items-wrapper.expanded .whoreadthis-user {
+ animation: whoreadthisFadeIn 0.3s ease forwards;
+ }
+
+ /* 为每个头像添加延迟动画 */
+ .whoreadthis-container .whoreadthis-items-wrapper.expanded .whoreadthis-user:nth-child(1) { animation-delay: 0.05s; }
+ .whoreadthis-container .whoreadthis-items-wrapper.expanded .whoreadthis-user:nth-child(2) { animation-delay: 0.1s; }
+ .whoreadthis-container .whoreadthis-items-wrapper.expanded .whoreadthis-user:nth-child(3) { animation-delay: 0.15s; }
+ .whoreadthis-container .whoreadthis-items-wrapper.expanded .whoreadthis-user:nth-child(4) { animation-delay: 0.2s; }
+ .whoreadthis-container .whoreadthis-items-wrapper.expanded .whoreadthis-user:nth-child(5) { animation-delay: 0.25s; }
+ .whoreadthis-container .whoreadthis-items-wrapper.expanded .whoreadthis-user:nth-child(6) { animation-delay: 0.3s; }
+ .whoreadthis-container .whoreadthis-items-wrapper.expanded .whoreadthis-user:nth-child(7) { animation-delay: 0.35s; }
+ .whoreadthis-container .whoreadthis-items-wrapper.expanded .whoreadthis-user:nth-child(8) { animation-delay: 0.4s; }
+ .whoreadthis-container .whoreadthis-items-wrapper.expanded .whoreadthis-user:nth-child(9) { animation-delay: 0.45s; }
+ .whoreadthis-container .whoreadthis-items-wrapper.expanded .whoreadthis-user:nth-child(10) { animation-delay: 0.5s; }
+
+ /* 头像大小动态设置 */
+ .whoreadthis-container[data-avatar-size] .whoreadthis-avatar {
+ width: var(--whoreadthis-avatar-size, 40px);
+ height: var(--whoreadthis-avatar-size, 40px);
+ }
+
+ .whoreadthis-container[data-avatar-size] .whoreadthis-avatar img {
+ width: var(--whoreadthis-avatar-size, 40px);
+ height: var(--whoreadthis-avatar-size, 40px);
+ }
+ ';
+ }
+
+ /**
+ * 获取插件JavaScript
+ */
+ private static function getPluginScripts()
+ {
+ return '
+ (function() {
+ function initWhoReadThis() {
+ const containers = document.querySelectorAll(".whoreadthis-container");
+
+ containers.forEach(container => {
+ if (container.dataset.whoReadThisInitialized === "true") {
+ return;
+ }
+
+ container.dataset.whoReadThisInitialized = "true";
+
+ // 设置头像大小
+ const avatarSize = container.getAttribute("data-avatar-size");
+ if (avatarSize && avatarSize !== "40") {
+ container.style.setProperty("--whoreadthis-avatar-size", avatarSize + "px");
+ }
+
+ const header = container.querySelector(".whoreadthis-header");
+ const itemsWrapper = container.querySelector(".whoreadthis-items-wrapper");
+ const toggleBtn = container.querySelector(".whoreadthis-toggle-btn");
+
+ if (!header || !itemsWrapper || !toggleBtn) return;
+
+ // 绑定标题点击事件
+ header.addEventListener("click", function(e) {
+ if (e.target.closest(".whoreadthis-toggle-btn")) {
+ return;
+ }
+ toggleWhoReadThis(container);
+ });
+
+ // 绑定按钮点击事件
+ toggleBtn.addEventListener("click", function(e) {
+ e.stopPropagation();
+ toggleWhoReadThis(container);
+ });
+
+ // 处理头像链接点击
+ const avatarWrappers = container.querySelectorAll(".whoreadthis-avatar-wrapper");
+ avatarWrappers.forEach(wrapper => {
+ const link = wrapper.getAttribute("href");
+ if (link && link !== "#") {
+ wrapper.addEventListener("click", function(e) {
+ e.stopPropagation();
+ window.open(link, "_blank");
+ });
+ }
+ });
+
+ // 锚点跳转支持
+ const containerId = container.id;
+ if (window.location.hash === "#whoreadthis" || window.location.hash === "#" + containerId) {
+ setTimeout(function() {
+ container.scrollIntoView({ behavior: "smooth" });
+ expandWhoReadThis(container);
+ }, 300);
+ }
+ });
+ }
+
+ function toggleWhoReadThis(container) {
+ const itemsWrapper = container.querySelector(".whoreadthis-items-wrapper");
+ const toggleBtn = container.querySelector(".whoreadthis-toggle-btn");
+ const arrow = toggleBtn.querySelector(".toggle-arrow");
+
+ if (!itemsWrapper || !toggleBtn || !arrow) return;
+
+ if (itemsWrapper.classList.contains("collapsed")) {
+ expandWhoReadThis(container);
+ } else {
+ collapseWhoReadThis(container);
+ }
+ }
+
+ function expandWhoReadThis(container) {
+ const itemsWrapper = container.querySelector(".whoreadthis-items-wrapper");
+ const toggleBtn = container.querySelector(".whoreadthis-toggle-btn");
+ const arrow = toggleBtn.querySelector(".toggle-arrow");
+
+ if (itemsWrapper && toggleBtn && arrow) {
+ itemsWrapper.className = "whoreadthis-items-wrapper expanded";
+ toggleBtn.className = "whoreadthis-toggle-btn expanded";
+ arrow.textContent = "↑";
+ }
+ }
+
+ function collapseWhoReadThis(container) {
+ const itemsWrapper = container.querySelector(".whoreadthis-items-wrapper");
+ const toggleBtn = container.querySelector(".whoreadthis-toggle-btn");
+ const arrow = toggleBtn.querySelector(".toggle-arrow");
+
+ if (itemsWrapper && toggleBtn && arrow) {
+ itemsWrapper.className = "whoreadthis-items-wrapper collapsed";
+ toggleBtn.className = "whoreadthis-toggle-btn collapsed";
+ arrow.textContent = "↓";
+ }
+ }
+
+ // 初始化
+ if (document.readyState === "loading") {
+ document.addEventListener("DOMContentLoaded", function() {
+ setTimeout(initWhoReadThis, 100);
+ });
+ } else {
+ setTimeout(initWhoReadThis, 100);
+ }
+
+ // 暴露公共API
+ window.WhoReadThis = {
+ init: initWhoReadThis,
+ expand: function(containerId) {
+ const container = document.getElementById(containerId);
+ if (container) {
+ expandWhoReadThis(container);
+ }
+ },
+ collapse: function(containerId) {
+ const container = document.getElementById(containerId);
+ if (container) {
+ collapseWhoReadThis(container);
+ }
+ },
+ toggle: function(containerId) {
+ const container = document.getElementById(containerId);
+ if (container) {
+ toggleWhoReadThis(container);
+ }
+ }
+ };
+
+ })();
+ ';
+ }
+
+ /**
+ * 获取客户端IP
+ */
+ private static function getClientIP()
+ {
+ $ip = '';
+
+ // 检查HTTP头
+ $headers = array(
+ 'HTTP_CLIENT_IP',
+ 'HTTP_X_FORWARDED_FOR',
+ 'HTTP_X_FORWARDED',
+ 'HTTP_FORWARDED_FOR',
+ 'HTTP_FORWARDED',
+ 'REMOTE_ADDR'
+ );
+
+ foreach ($headers as $header) {
+ if (isset($_SERVER[$header]) && !empty($_SERVER[$header]) && strcasecmp($_SERVER[$header], 'unknown')) {
+ $ip = $_SERVER[$header];
+ break;
+ }
+ }
+
+ // 处理多个IP的情况
+ if (strpos($ip, ',') !== false) {
+ $ips = explode(',', $ip);
+ $ip = trim($ips[0]);
+ }
+
+ // 验证IP格式
+ if (!filter_var($ip, FILTER_VALIDATE_IP)) {
+ $ip = $_SERVER['REMOTE_ADDR'] ?? '0.0.0.0';
+ }
+
+ return $ip;
+ }
+}
+
+/**
+ * 前端调用助手函数
+ */
+function showWhoReadThis()
+{
+ echo WhoReadThis_Plugin::show();
+}
\ No newline at end of file
diff --git a/assets/who-read-this.css b/assets/who-read-this.css
new file mode 100644
index 0000000..dbbe2a0
--- /dev/null
+++ b/assets/who-read-this.css
@@ -0,0 +1,282 @@
+/* WhoReadThis 插件专用样式 - 独立文件避免冲突 */
+/* 版本: 1.0.6 */
+.whoreadthis-container {
+ border-radius: 10px;
+ overflow: hidden;
+ position: relative;
+ z-index: 10;
+ margin: 0 0 30px 0;
+ clear: both;
+ box-sizing: border-box;
+}
+
+/* 标题区域 */
+.whoreadthis-container .whoreadthis-header-section {
+ padding: 18px 24px;
+ background: linear-gradient(135deg, #6a11cb 0%, #2575fc 100%);
+ border: 1px solid #e1e8ed;
+ position: relative;
+ z-index: 20;
+ box-sizing: border-box;
+}
+
+/* 深色模式支持 */
+body.dark-mode .whoreadthis-container .whoreadthis-header-section,
+body.theme-dark .whoreadthis-container .whoreadthis-header-section,
+body.dark .whoreadthis-container .whoreadthis-header-section {
+ background: rgb(10 12 25 / 1);
+ border: 1px solid #2d3748;
+}
+
+/* 标题文字 */
+.whoreadthis-container .whoreadthis-header {
+ margin: 0;
+ font-size: 18px;
+ color: #fff;
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ cursor: pointer;
+ user-select: none;
+ font-weight: 600;
+ position: relative;
+ z-index: 25;
+ box-sizing: border-box;
+}
+
+/* 深色模式标题 */
+body.dark-mode .whoreadthis-container .whoreadthis-header h3,
+body.theme-dark .whoreadthis-container .whoreadthis-header h3,
+body.dark .whoreadthis-container .whoreadthis-header h3 {
+ color: #e2e8f0 !important;
+}
+
+/* 计数徽章 */
+.whoreadthis-container .whoreadthis-count-badge {
+ background-color: rgba(255, 255, 255, 0.2);
+ color: white;
+ padding: 4px 8px;
+ border-radius: 12px;
+ font-size: 13px;
+ margin-left: 8px;
+ display: inline-block;
+ vertical-align: middle;
+}
+
+/* 深色模式徽章 */
+body.dark-mode .whoreadthis-container .whoreadthis-count-badge,
+body.theme-dark .whoreadthis-container .whoreadthis-count-badge,
+body.dark .whoreadthis-container .whoreadthis-count-badge {
+ background-color: rgba(255, 255, 255, 0.1);
+ color: #e2e8f0;
+}
+
+/* 切换按钮 */
+.whoreadthis-container .whoreadthis-toggle-btn {
+ font-size: 0.8em;
+ color: #fff;
+ display: flex;
+ align-items: center;
+ gap: 5px;
+ padding: 4px 10px;
+ border-radius: 4px;
+ transition: all 0.3s ease;
+ font-weight: normal;
+ border: none;
+ cursor: pointer;
+ position: relative;
+ z-index: 30;
+ background: transparent;
+ box-sizing: border-box;
+}
+
+.whoreadthis-container .whoreadthis-toggle-btn:hover {
+ background: rgba(255, 255, 255, 0.2);
+}
+
+.whoreadthis-container .whoreadthis-toggle-btn .toggle-arrow {
+ font-size: 14px;
+ line-height: 1;
+ transition: transform 0.3s ease;
+ display: inline-block;
+}
+
+.whoreadthis-container .whoreadthis-toggle-btn.expanded .toggle-arrow {
+ transform: rotate(180deg);
+}
+
+/* 内容区域 */
+.whoreadthis-container .whoreadthis-content-section {
+ padding: 0 24px;
+ background: #f9fafb;
+ border-top: none;
+ border-radius: 0 0 10px 10px;
+ position: relative;
+ z-index: 15;
+ box-sizing: border-box;
+}
+
+/* 深色模式内容区域 */
+body.dark-mode .whoreadthis-container .whoreadthis-content-section,
+body.theme-dark .whoreadthis-container .whoreadthis-content-section,
+body.dark .whoreadthis-container .whoreadthis-content-section {
+ background: rgb(15 23 42 / 1);
+}
+
+/* 项目包装器 */
+.whoreadthis-container .whoreadthis-items-wrapper {
+ transition: all 0.3s ease;
+ overflow: hidden;
+ position: relative;
+ z-index: 15;
+ box-sizing: border-box;
+}
+
+.whoreadthis-container .whoreadthis-items-wrapper.collapsed {
+ max-height: 0;
+ opacity: 0;
+ visibility: hidden;
+ padding: 0;
+}
+
+.whoreadthis-container .whoreadthis-items-wrapper.expanded {
+ max-height: 5000px;
+ opacity: 1;
+ visibility: visible;
+ padding: 20px 0;
+}
+
+/* 阅读者列表 */
+.whoreadthis-container .whoreadthis-users {
+ display: flex;
+ flex-wrap: wrap;
+ gap: 12px;
+ margin: 0;
+ padding: 0;
+ list-style: none;
+ position: relative;
+ z-index: 40;
+ box-sizing: border-box;
+}
+
+.whoreadthis-container .whoreadthis-user {
+ position: relative;
+ z-index: 45;
+ box-sizing: border-box;
+}
+
+/* 头像包装器 */
+.whoreadthis-container .whoreadthis-avatar-wrapper {
+ position: relative;
+ display: block;
+ z-index: 50;
+ text-decoration: none;
+ box-sizing: border-box;
+}
+
+/* 头像样式 */
+.whoreadthis-container .whoreadthis-avatar {
+ width: 40px;
+ height: 40px;
+ border-radius: 50%;
+ overflow: hidden;
+ cursor: pointer;
+ display: block;
+ position: relative;
+ z-index: 55;
+ transition: transform 0.3s ease;
+ box-sizing: border-box;
+}
+
+.whoreadthis-container .whoreadthis-avatar:hover {
+ transform: scale(1.05);
+}
+
+.whoreadthis-container .whoreadthis-avatar img {
+ width: 100%;
+ height: 100%;
+ object-fit: cover;
+ border-radius: 50%;
+ padding: 2px;
+ border: 2px solid #f15a22;
+ display: block;
+ transition: transform 0.4s ease-out;
+ box-sizing: border-box;
+}
+
+.whoreadthis-container .whoreadthis-avatar img:hover {
+ transform: rotateZ(360deg);
+}
+
+/* 空状态 */
+.whoreadthis-container .whoreadthis-empty {
+ text-align: center;
+ color: #95a5a6;
+ font-style: italic;
+ padding: 20px;
+}
+
+/* 响应式设计 */
+@media (max-width: 768px) {
+ .whoreadthis-container .whoreadthis-header-section,
+ .whoreadthis-container .whoreadthis-content-section {
+ padding: 14px 18px;
+ }
+
+ .whoreadthis-container .whoreadthis-header {
+ font-size: 16px;
+ }
+
+ .whoreadthis-container .whoreadthis-users {
+ gap: 8px;
+ }
+}
+
+@media (max-width: 480px) {
+ .whoreadthis-container .whoreadthis-header-section,
+ .whoreadthis-container .whoreadthis-content-section {
+ padding: 12px 15px;
+ }
+
+ .whoreadthis-container .whoreadthis-header {
+ font-size: 14px;
+ }
+
+ .whoreadthis-container .whoreadthis-count-badge {
+ font-size: 11px;
+ padding: 3px 6px;
+ }
+
+ .whoreadthis-container .whoreadthis-users {
+ gap: 6px;
+ justify-content: center;
+ }
+}
+
+/* 动画效果 */
+@keyframes whoreadthisFadeIn {
+ from {
+ opacity: 0;
+ transform: translateY(10px);
+ }
+ to {
+ opacity: 1;
+ transform: translateY(0);
+ }
+}
+
+.whoreadthis-container .whoreadthis-items-wrapper.expanded .whoreadthis-user {
+ animation: whoreadthisFadeIn 0.3s ease forwards;
+}
+
+/* 为每个头像添加延迟动画 */
+.whoreadthis-container .whoreadthis-items-wrapper.expanded .whoreadthis-user:nth-child(1) { animation-delay: 0.05s; }
+.whoreadthis-container .whoreadthis-items-wrapper.expanded .whoreadthis-user:nth-child(2) { animation-delay: 0.1s; }
+.whoreadthis-container .whoreadthis-items-wrapper.expanded .whoreadthis-user:nth-child(3) { animation-delay: 0.15s; }
+.whoreadthis-container .whoreadthis-items-wrapper.expanded .whoreadthis-user:nth-child(4) { animation-delay: 0.2s; }
+.whoreadthis-container .whoreadthis-items-wrapper.expanded .whoreadthis-user:nth-child(5) { animation-delay: 0.25s; }
+.whoreadthis-container .whoreadthis-items-wrapper.expanded .whoreadthis-user:nth-child(6) { animation-delay: 0.3s; }
+.whoreadthis-container .whoreadthis-items-wrapper.expanded .whoreadthis-user:nth-child(7) { animation-delay: 0.35s; }
+.whoreadthis-container .whoreadthis-items-wrapper.expanded .whoreadthis-user:nth-child(8) { animation-delay: 0.4s; }
+.whoreadthis-container .whoreadthis-items-wrapper.expanded .whoreadthis-user:nth-child(9) { animation-delay: 0.45s; }
+.whoreadthis-container .whoreadthis-items-wrapper.expanded .whoreadthis-user:nth-child(10) { animation-delay: 0.5s; }
\ No newline at end of file
diff --git a/assets/who-read-this.js b/assets/who-read-this.js
new file mode 100644
index 0000000..ebc9c69
--- /dev/null
+++ b/assets/who-read-this.js
@@ -0,0 +1,150 @@
+/**
+ * WhoReadThis 插件专用脚本 - 独立文件避免冲突
+ * 版本: 1.0.6
+ */
+
+(function() {
+ 'use strict';
+
+ // 等待DOM加载完成
+ function initWhoReadThis() {
+ const containers = document.querySelectorAll('.whoreadthis-container');
+
+ containers.forEach(container => {
+ if (container.dataset.whoReadThisInitialized === 'true') {
+ return;
+ }
+
+ container.dataset.whoReadThisInitialized = 'true';
+
+ const header = container.querySelector('.whoreadthis-header');
+ const itemsWrapper = container.querySelector('.whoreadthis-items-wrapper');
+ const toggleBtn = container.querySelector('.whoreadthis-toggle-btn');
+
+ // 设置头像大小
+ const avatarSize = container.getAttribute('data-avatar-size');
+ if (avatarSize && avatarSize !== '40') {
+ const avatars = container.querySelectorAll('.whoreadthis-avatar');
+ const avatarImgs = container.querySelectorAll('.whoreadthis-avatar img');
+
+ avatars.forEach(avatar => {
+ avatar.style.width = avatarSize + 'px';
+ avatar.style.height = avatarSize + 'px';
+ });
+
+ avatarImgs.forEach(img => {
+ img.style.width = avatarSize + 'px';
+ img.style.height = avatarSize + 'px';
+ });
+ }
+
+ if (!header || !itemsWrapper || !toggleBtn) {
+ return;
+ }
+
+ // 绑定标题点击事件
+ header.addEventListener('click', function(e) {
+ if (e.target.closest('.whoreadthis-toggle-btn')) {
+ return;
+ }
+ toggleWhoReadThis(container);
+ });
+
+ // 绑定按钮点击事件
+ toggleBtn.addEventListener('click', function(e) {
+ e.stopPropagation();
+ toggleWhoReadThis(container);
+ });
+
+ // 处理头像链接点击
+ const avatarWrappers = container.querySelectorAll('.whoreadthis-avatar-wrapper');
+ avatarWrappers.forEach(wrapper => {
+ const link = wrapper.getAttribute('href');
+ if (link && link !== '#') {
+ wrapper.addEventListener('click', function(e) {
+ e.stopPropagation();
+ window.open(link, '_blank');
+ });
+ }
+ });
+
+ // 锚点跳转支持
+ if (window.location.hash === '#whoreadthis' || window.location.hash === '#whoreadthis-' + container.id.split('-')[1]) {
+ setTimeout(function() {
+ container.scrollIntoView({ behavior: 'smooth' });
+ expandWhoReadThis(container);
+ }, 300);
+ }
+ });
+ }
+
+ function toggleWhoReadThis(container) {
+ const itemsWrapper = container.querySelector('.whoreadthis-items-wrapper');
+ const toggleBtn = container.querySelector('.whoreadthis-toggle-btn');
+
+ if (!itemsWrapper || !toggleBtn) {
+ return;
+ }
+
+ if (itemsWrapper.classList.contains('collapsed')) {
+ expandWhoReadThis(container);
+ } else {
+ collapseWhoReadThis(container);
+ }
+ }
+
+ function expandWhoReadThis(container) {
+ const itemsWrapper = container.querySelector('.whoreadthis-items-wrapper');
+ const toggleBtn = container.querySelector('.whoreadthis-toggle-btn');
+
+ if (itemsWrapper && toggleBtn) {
+ itemsWrapper.className = 'whoreadthis-items-wrapper expanded';
+ toggleBtn.className = 'whoreadthis-toggle-btn expanded';
+ toggleBtn.querySelector('.toggle-arrow').textContent = '↑';
+ }
+ }
+
+ function collapseWhoReadThis(container) {
+ const itemsWrapper = container.querySelector('.whoreadthis-items-wrapper');
+ const toggleBtn = container.querySelector('.whoreadthis-toggle-btn');
+
+ if (itemsWrapper && toggleBtn) {
+ itemsWrapper.className = 'whoreadthis-items-wrapper collapsed';
+ toggleBtn.className = 'whoreadthis-toggle-btn collapsed';
+ toggleBtn.querySelector('.toggle-arrow').textContent = '↓';
+ }
+ }
+
+ // 页面加载完成后初始化
+ if (document.readyState === 'loading') {
+ document.addEventListener('DOMContentLoaded', function() {
+ setTimeout(initWhoReadThis, 100);
+ });
+ } else {
+ setTimeout(initWhoReadThis, 100);
+ }
+
+ // 暴露公共API
+ window.WhoReadThis = {
+ init: initWhoReadThis,
+ expand: function(containerId) {
+ const container = document.getElementById(containerId);
+ if (container) {
+ expandWhoReadThis(container);
+ }
+ },
+ collapse: function(containerId) {
+ const container = document.getElementById(containerId);
+ if (container) {
+ collapseWhoReadThis(container);
+ }
+ },
+ toggle: function(containerId) {
+ const container = document.getElementById(containerId);
+ if (container) {
+ toggleWhoReadThis(container);
+ }
+ }
+ };
+
+})();
\ No newline at end of file
diff --git a/前端调用.txt b/前端调用.txt
new file mode 100644
index 0000000..96e8d98
--- /dev/null
+++ b/前端调用.txt
@@ -0,0 +1 @@
+
\ No newline at end of file