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 .= '
  • + +
    + ' . $username . ' +
    +
    +
  • '; + } + + $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 = '
    +
    +
    +

    ' . htmlspecialchars($title) . '' . $count . '人

    + +
    +
    +
    +
    +
      ' . $usersHtml . '
    +
    +
    +
    '; + + 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