Files
WhoReadThis/Plugin.php
2026-02-23 21:09:17 +08:00

895 lines
31 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<?php
/**
* 谁读了本文
*
* @package WhoReadThis
* @author 石头厝
* @version 1.0.8
* @link https://www.shitoucuo.com
*/
if (!defined('__TYPECHO_ROOT_DIR__')) exit;
class WhoReadThis_Plugin implements Typecho_Plugin_Interface
{
// 数据库表名
private static $_table = 'whoreadthis';
// 是否已输出资源
private static $_assetsOutput = false;
/**
* 激活插件
*/
public static function activate()
{
// 创建数据表记录阅读者
$db = Typecho_Db::get();
$prefix = $db->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 '<!-- WhoReadThis Plugin Assets -->';
echo '<style>';
echo self::getPluginStyles();
echo '</style>';
echo '<script>';
echo self::getPluginScripts();
echo '</script>';
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 .= '<li class="whoreadthis-user">
<a href="' . htmlspecialchars($link) . '" target="_blank" class="whoreadthis-avatar-wrapper"
title="' . $username . '" rel="noopener noreferrer">
<div class="whoreadthis-avatar">
<img src="' . htmlspecialchars($avatar) . '" alt="' . $username . '" loading="lazy">
</div>
</a>
</li>';
}
$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 = '<div class="whoreadthis-container" id="' . $containerId . '" data-avatar-size="' . $avatarSize . '">
<div class="whoreadthis-header-section">
<div class="whoreadthis-header">
<h3>' . htmlspecialchars($title) . '<span class="whoreadthis-count-badge">' . $count . '人</span></h3>
<button class="whoreadthis-toggle-btn' . ($isOpen ? ' expanded' : ' collapsed') . '">
<span class="toggle-arrow">↓</span>
</button>
</div>
</div>
<div class="whoreadthis-content-section">
<div class="whoreadthis-items-wrapper' . $isOpen . '">
<ul class="whoreadthis-users">' . $usersHtml . '</ul>
</div>
</div>
</div>';
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();
}