738 lines
24 KiB
PHP
738 lines
24 KiB
PHP
|
|
<?php
|
|||
|
|
/**
|
|||
|
|
* 前端评论区用户信息卡片
|
|||
|
|
*
|
|||
|
|
* @package UserCard
|
|||
|
|
* @author 石头厝
|
|||
|
|
* @version 3.2.0
|
|||
|
|
* @link https://www.shitoucuo.com
|
|||
|
|
*/
|
|||
|
|
|
|||
|
|
if (!defined('__TYPECHO_ROOT_DIR__')) exit;
|
|||
|
|
|
|||
|
|
class UserCard_Plugin implements Typecho_Plugin_Interface
|
|||
|
|
{
|
|||
|
|
/**
|
|||
|
|
* 激活插件
|
|||
|
|
*/
|
|||
|
|
public static function activate()
|
|||
|
|
{
|
|||
|
|
// 添加数据库字段
|
|||
|
|
$db = Typecho_Db::get();
|
|||
|
|
$prefix = $db->getPrefix();
|
|||
|
|
|
|||
|
|
try {
|
|||
|
|
$db->query("ALTER TABLE `{$prefix}users` ADD `user_feed` VARCHAR(255) DEFAULT NULL");
|
|||
|
|
} catch (Exception $e) {
|
|||
|
|
// 字段可能已存在
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 添加管理菜单
|
|||
|
|
Helper::addPanel(3, 'UserCard/manage-users.php', 'RSS卡片', 'RSS卡片', 'administrator');
|
|||
|
|
|
|||
|
|
// 前端资源
|
|||
|
|
Typecho_Plugin::factory('Widget_Archive')->header = array('UserCard_Plugin', 'addHeader');
|
|||
|
|
|
|||
|
|
return _t('用户卡片插件已激活');
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 禁用插件
|
|||
|
|
*/
|
|||
|
|
public static function deactivate()
|
|||
|
|
{
|
|||
|
|
// 移除管理菜单
|
|||
|
|
Helper::removePanel(3, 'UserCard/manage-users.php');
|
|||
|
|
return _t('用户卡片插件已禁用');
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 插件配置
|
|||
|
|
*/
|
|||
|
|
public static function config(Typecho_Widget_Helper_Form $form)
|
|||
|
|
{
|
|||
|
|
// 管理员评论是否显示卡片
|
|||
|
|
$show_admin_card = new Typecho_Widget_Helper_Form_Element_Radio(
|
|||
|
|
'show_admin_card',
|
|||
|
|
array(
|
|||
|
|
'1' => _t('显示'),
|
|||
|
|
'0' => _t('不显示')
|
|||
|
|
),
|
|||
|
|
'1',
|
|||
|
|
_t('管理员评论卡片'),
|
|||
|
|
_t('是否在管理员发表的评论上显示用户卡片(普通用户不受此设置影响)')
|
|||
|
|
);
|
|||
|
|
$form->addInput($show_admin_card);
|
|||
|
|
|
|||
|
|
// RSS缓存时间
|
|||
|
|
$cache_time = new Typecho_Widget_Helper_Form_Element_Text(
|
|||
|
|
'cache_time',
|
|||
|
|
NULL,
|
|||
|
|
'3600',
|
|||
|
|
_t('RSS缓存时间(秒)'),
|
|||
|
|
_t('RSS数据缓存时间,建议设置为3600秒(1小时)')
|
|||
|
|
);
|
|||
|
|
$form->addInput($cache_time->addRule('required', _t('必须填写缓存时间'))->addRule('isInteger', _t('请输入整数')));
|
|||
|
|
|
|||
|
|
// 最多显示条数
|
|||
|
|
$max_items = new Typecho_Widget_Helper_Form_Element_Text(
|
|||
|
|
'max_items',
|
|||
|
|
NULL,
|
|||
|
|
'5',
|
|||
|
|
_t('最多显示文章数'),
|
|||
|
|
_t('用户卡片中最多显示的RSS文章数量')
|
|||
|
|
);
|
|||
|
|
$form->addInput($max_items->addRule('required', _t('必须填写显示条数'))->addRule('isInteger', _t('请输入整数')));
|
|||
|
|
|
|||
|
|
// RSS超时时间
|
|||
|
|
$timeout = new Typecho_Widget_Helper_Form_Element_Text(
|
|||
|
|
'timeout',
|
|||
|
|
NULL,
|
|||
|
|
'10',
|
|||
|
|
_t('RSS请求超时时间(秒)'),
|
|||
|
|
_t('获取RSS数据时的超时时间')
|
|||
|
|
);
|
|||
|
|
$form->addInput($timeout->addRule('required', _t('必须填写超时时间'))->addRule('isInteger', _t('请输入整数')));
|
|||
|
|
|
|||
|
|
// 添加卡片显示延迟
|
|||
|
|
$hover_delay = new Typecho_Widget_Helper_Form_Element_Text(
|
|||
|
|
'hover_delay',
|
|||
|
|
NULL,
|
|||
|
|
'200',
|
|||
|
|
_t('卡片显示延迟(毫秒)'),
|
|||
|
|
_t('鼠标悬停后显示卡片的延迟时间,防止误触发')
|
|||
|
|
);
|
|||
|
|
$form->addInput($hover_delay->addRule('required', _t('必须填写延迟时间'))->addRule('isInteger', _t('请输入整数')));
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 个人配置 - 保持为空
|
|||
|
|
*/
|
|||
|
|
public static function personalConfig(Typecho_Widget_Helper_Form $form)
|
|||
|
|
{
|
|||
|
|
// 不在个人页面显示RSS地址字段
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 添加头部资源
|
|||
|
|
*/
|
|||
|
|
public static function addHeader()
|
|||
|
|
{
|
|||
|
|
// 只在文章页面加载
|
|||
|
|
if (!Typecho_Widget::widget('Widget_Archive')->is('single')) {
|
|||
|
|
return;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 获取配置
|
|||
|
|
$options = Typecho_Widget::widget('Widget_Options')->plugin('UserCard');
|
|||
|
|
$hover_delay = isset($options->hover_delay) ? intval($options->hover_delay) : 200;
|
|||
|
|
|
|||
|
|
echo '<style>
|
|||
|
|
/* 用户卡片样式 */
|
|||
|
|
.comment-author.usercard-wrapper {
|
|||
|
|
position: relative;
|
|||
|
|
display: inline-block;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.comment-author.usercard-wrapper a {
|
|||
|
|
color: #4a90e2;
|
|||
|
|
text-decoration: none;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.comment-author.usercard-wrapper a:hover {
|
|||
|
|
color: #2c6db5;
|
|||
|
|
text-decoration: underline;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.usercard-popup {
|
|||
|
|
display: none;
|
|||
|
|
position: absolute;
|
|||
|
|
z-index: 9999000;
|
|||
|
|
width: 250px;
|
|||
|
|
background: white;
|
|||
|
|
border-radius: 10px;
|
|||
|
|
box-shadow: 0 5px 20px rgba(0,0,0,0.15);
|
|||
|
|
left: 0;
|
|||
|
|
top: 100%;
|
|||
|
|
margin-top: 5px;
|
|||
|
|
opacity: 0;
|
|||
|
|
transform: translateY(-10px);
|
|||
|
|
transition: opacity 0.3s ease, transform 0.3s ease;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.usercard-popup.active {
|
|||
|
|
display: block;
|
|||
|
|
opacity: 1;
|
|||
|
|
transform: translateY(0);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.usercard-header {
|
|||
|
|
padding: 8px 15px;
|
|||
|
|
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
|||
|
|
color: white;
|
|||
|
|
border-radius: 8px 8px 0 0;
|
|||
|
|
text-align:center;
|
|||
|
|
overflow:hidden;
|
|||
|
|
position: relative;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.dark .usercard-header h3{
|
|||
|
|
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
.usercard-header h3 {
|
|||
|
|
margin: 0;
|
|||
|
|
font-size: 14px;
|
|||
|
|
font-weight: 600;
|
|||
|
|
position: relative;
|
|||
|
|
z-index: 1;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.usercard-body {
|
|||
|
|
padding: 15px;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.usercard-info p {
|
|||
|
|
margin: 5px 0;
|
|||
|
|
font-size: 13px;
|
|||
|
|
line-height: 1.5;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.usercard-info strong {
|
|||
|
|
color: #666;
|
|||
|
|
min-width: 80px;
|
|||
|
|
display: inline-block;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.usercard-info a {
|
|||
|
|
color: #4a90e2;
|
|||
|
|
text-decoration: none;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.usercard-info a:hover {
|
|||
|
|
text-decoration: underline;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.usercard-rss h4 {
|
|||
|
|
margin: 5px 0 5px 0;
|
|||
|
|
color: #333;
|
|||
|
|
font-size: 14px;
|
|||
|
|
font-weight: 600;
|
|||
|
|
padding-bottom: 8px;
|
|||
|
|
border-bottom: 2px solid #4a90e2;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.usercard-rss-list {
|
|||
|
|
list-style: none;
|
|||
|
|
padding: 0;
|
|||
|
|
margin: 0;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.usercard-rss-list li {
|
|||
|
|
padding: 5px 0;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.usercard-rss-list li:last-child {
|
|||
|
|
border-bottom: none;
|
|||
|
|
padding-bottom:none;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.usercard-rss-list a {
|
|||
|
|
color: #2c3e50;
|
|||
|
|
text-decoration: none;
|
|||
|
|
font-size: 13px;
|
|||
|
|
display: block;
|
|||
|
|
width: 230px;
|
|||
|
|
white-space: nowrap;
|
|||
|
|
overflow: hidden;
|
|||
|
|
text-overflow: ellipsis;
|
|||
|
|
transition: color 0.3s ease;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.usercard-rss-list a:hover {
|
|||
|
|
color: #4a90e2;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.usercard-rss-date {
|
|||
|
|
font-size: 12px;
|
|||
|
|
color: #999;
|
|||
|
|
display: block;
|
|||
|
|
margin-top: 3px;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/* 黑暗模式样式 */
|
|||
|
|
.dark .usercard-popup {
|
|||
|
|
background: rgb(10 12 25 / 1);
|
|||
|
|
border: 1px solid #333;
|
|||
|
|
box-shadow: 0 5px 20px rgba(0,0,0,0.3);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.dark .usercard-header {
|
|||
|
|
background: #1d1d1e;
|
|||
|
|
border-bottom: 1px solid #333;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.dark .usercard-info p {
|
|||
|
|
color: rgb(156 163 175 / var(--tw-text-opacity));
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.dark .usercard-info strong {
|
|||
|
|
color: #a0aec0;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.dark .usercard-info a {
|
|||
|
|
color: #90cdf4;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.dark .usercard-info a:hover {
|
|||
|
|
color: #63b3ed;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.dark .usercard-rss h4 {
|
|||
|
|
color: rgb(156 163 175 / var(--tw-text-opacity));
|
|||
|
|
border-bottom-color: #333;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.dark .usercard-rss-list a {
|
|||
|
|
color: #cbd5e0;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.dark .usercard-rss-list a:hover {
|
|||
|
|
color: #90cdf4;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.dark .usercard-rss-date {
|
|||
|
|
color: #a0aec0;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/* 响应式设计 */
|
|||
|
|
@media (max-width: 768px) {
|
|||
|
|
.usercard-popup {
|
|||
|
|
width: 220px;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.usercard-body {
|
|||
|
|
padding: 12px;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.usercard-rss-list a {
|
|||
|
|
width: 200px;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
@media (max-width: 480px) {
|
|||
|
|
.usercard-popup {
|
|||
|
|
width: 200px;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.usercard-body {
|
|||
|
|
padding: 10px;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.usercard-info p {
|
|||
|
|
font-size: 12px;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.usercard-rss-list a {
|
|||
|
|
width: 180px;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.usercard-rss h4 {
|
|||
|
|
font-size: 13px;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/* 卡片内部边框 */
|
|||
|
|
.usercard-rss h4 {
|
|||
|
|
position: relative;
|
|||
|
|
margin-bottom: 10px;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/* RSS文章项悬停效果 */
|
|||
|
|
.usercard-rss-list li {
|
|||
|
|
transition: transform 0.2s ease;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.usercard-rss-list li:hover {
|
|||
|
|
transform: translateX(5px);
|
|||
|
|
}
|
|||
|
|
</style>';
|
|||
|
|
|
|||
|
|
echo '<script>
|
|||
|
|
document.addEventListener("DOMContentLoaded", function() {
|
|||
|
|
var hoverDelay = ' . $hover_delay . ';
|
|||
|
|
var hideDelay = 150;
|
|||
|
|
var showTimer = null;
|
|||
|
|
var hideTimer = null;
|
|||
|
|
var activeCard = null;
|
|||
|
|
var isMouseOverCard = false;
|
|||
|
|
|
|||
|
|
// 初始化所有卡片
|
|||
|
|
var initCards = function() {
|
|||
|
|
var wrappers = document.querySelectorAll(".usercard-wrapper");
|
|||
|
|
|
|||
|
|
wrappers.forEach(function(wrapper) {
|
|||
|
|
var popup = wrapper.querySelector(".usercard-popup");
|
|||
|
|
if (!popup) return;
|
|||
|
|
|
|||
|
|
var userLink = wrapper.querySelector("a");
|
|||
|
|
|
|||
|
|
// 鼠标进入用户链接
|
|||
|
|
userLink.addEventListener("mouseenter", function(e) {
|
|||
|
|
clearTimeout(hideTimer);
|
|||
|
|
clearTimeout(showTimer);
|
|||
|
|
|
|||
|
|
// 如果已经有激活的卡片且不是当前卡片,先隐藏
|
|||
|
|
if (activeCard && activeCard !== wrapper) {
|
|||
|
|
var otherPopup = activeCard.querySelector(".usercard-popup");
|
|||
|
|
if (otherPopup) {
|
|||
|
|
otherPopup.classList.remove("active");
|
|||
|
|
isMouseOverCard = false;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
showTimer = setTimeout(function() {
|
|||
|
|
popup.classList.add("active");
|
|||
|
|
activeCard = wrapper;
|
|||
|
|
|
|||
|
|
// 检查卡片位置,防止溢出屏幕
|
|||
|
|
adjustPopupPosition(popup);
|
|||
|
|
}, hoverDelay);
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
// 鼠标离开用户链接
|
|||
|
|
userLink.addEventListener("mouseleave", function(e) {
|
|||
|
|
clearTimeout(showTimer);
|
|||
|
|
|
|||
|
|
// 设置一个小的延迟,检查鼠标是否移动到了卡片上
|
|||
|
|
setTimeout(function() {
|
|||
|
|
if (!isMouseOverCard) {
|
|||
|
|
hideTimer = setTimeout(function() {
|
|||
|
|
if (!isMouseOverCard) {
|
|||
|
|
popup.classList.remove("active");
|
|||
|
|
if (activeCard === wrapper) {
|
|||
|
|
activeCard = null;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}, hideDelay);
|
|||
|
|
}
|
|||
|
|
}, 50);
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
// 鼠标进入卡片
|
|||
|
|
popup.addEventListener("mouseenter", function(e) {
|
|||
|
|
isMouseOverCard = true;
|
|||
|
|
clearTimeout(hideTimer);
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
// 鼠标离开卡片
|
|||
|
|
popup.addEventListener("mouseleave", function(e) {
|
|||
|
|
isMouseOverCard = false;
|
|||
|
|
hideTimer = setTimeout(function() {
|
|||
|
|
popup.classList.remove("active");
|
|||
|
|
if (activeCard === wrapper) {
|
|||
|
|
activeCard = null;
|
|||
|
|
}
|
|||
|
|
}, hideDelay);
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
// 卡片内的链接点击
|
|||
|
|
popup.addEventListener("click", function(e) {
|
|||
|
|
if (e.target.tagName === "A") {
|
|||
|
|
// 让链接正常跳转
|
|||
|
|
e.stopPropagation();
|
|||
|
|
}
|
|||
|
|
});
|
|||
|
|
});
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
// 调整卡片位置,防止溢出屏幕
|
|||
|
|
function adjustPopupPosition(popup) {
|
|||
|
|
var rect = popup.getBoundingClientRect();
|
|||
|
|
var viewportWidth = window.innerWidth;
|
|||
|
|
|
|||
|
|
// 如果卡片右侧超出屏幕,向左移动
|
|||
|
|
if (rect.right > viewportWidth) {
|
|||
|
|
var overflow = rect.right - viewportWidth;
|
|||
|
|
popup.style.left = -overflow + "px";
|
|||
|
|
} else {
|
|||
|
|
popup.style.left = "0";
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 窗口调整大小时重新定位卡片
|
|||
|
|
window.addEventListener("resize", function() {
|
|||
|
|
var activePopup = document.querySelector(".usercard-popup.active");
|
|||
|
|
if (activePopup) {
|
|||
|
|
adjustPopupPosition(activePopup);
|
|||
|
|
}
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
// 初始加载
|
|||
|
|
setTimeout(initCards, 100);
|
|||
|
|
|
|||
|
|
// 监听动态加载的评论
|
|||
|
|
var observer = new MutationObserver(function(mutations) {
|
|||
|
|
for (var mutation of mutations) {
|
|||
|
|
if (mutation.addedNodes.length > 0) {
|
|||
|
|
setTimeout(initCards, 100);
|
|||
|
|
break;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
observer.observe(document.body, {
|
|||
|
|
childList: true,
|
|||
|
|
subtree: true
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
// 点击页面其他地方关闭卡片
|
|||
|
|
document.addEventListener("click", function(e) {
|
|||
|
|
if (!e.target.closest(".usercard-wrapper")) {
|
|||
|
|
var cards = document.querySelectorAll(".usercard-popup.active");
|
|||
|
|
cards.forEach(function(card) {
|
|||
|
|
card.classList.remove("active");
|
|||
|
|
});
|
|||
|
|
activeCard = null;
|
|||
|
|
isMouseOverCard = false;
|
|||
|
|
}
|
|||
|
|
});
|
|||
|
|
});
|
|||
|
|
</script>';
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 生成用户卡片(供主题调用)
|
|||
|
|
*/
|
|||
|
|
public static function render($comments)
|
|||
|
|
{
|
|||
|
|
if (!$comments || $comments->authorId == 0) {
|
|||
|
|
// 游客
|
|||
|
|
if ($comments->url) {
|
|||
|
|
return '<a href="' . $comments->url . '" rel="external nofollow" target="_blank">' . $comments->author . '</a>';
|
|||
|
|
} else {
|
|||
|
|
return $comments->author;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
try {
|
|||
|
|
$db = Typecho_Db::get();
|
|||
|
|
$user = $db->fetchRow($db->select()
|
|||
|
|
->from('table.users')
|
|||
|
|
->where('uid = ?', $comments->authorId));
|
|||
|
|
|
|||
|
|
if (!$user) {
|
|||
|
|
return $comments->author;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 获取插件配置
|
|||
|
|
$options = Typecho_Widget::widget('Widget_Options')->plugin('UserCard');
|
|||
|
|
$show_admin_card = isset($options->show_admin_card) ? intval($options->show_admin_card) : 1;
|
|||
|
|
|
|||
|
|
// 检查用户是否为管理员
|
|||
|
|
$is_admin = ($user['group'] == 'administrator');
|
|||
|
|
|
|||
|
|
// 如果是管理员且配置为不显示卡片,则返回普通链接
|
|||
|
|
if ($is_admin && !$show_admin_card) {
|
|||
|
|
if (!empty($user['url'])) {
|
|||
|
|
return '<a href="' . $user['url'] . '" target="_blank" rel="nofollow">' . $comments->author . '</a>';
|
|||
|
|
} else {
|
|||
|
|
return $comments->author;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 评论数
|
|||
|
|
$commentCount = $db->fetchObject($db->select('COUNT(*) as cnt')
|
|||
|
|
->from('table.comments')
|
|||
|
|
->where('authorId = ?', $user['uid'])
|
|||
|
|
->where('status = ?', 'approved'))->cnt;
|
|||
|
|
|
|||
|
|
// RSS文章
|
|||
|
|
$rssItems = array();
|
|||
|
|
if (!empty($user['user_feed'])) {
|
|||
|
|
$rssItems = self::getRssItems($user['user_feed']);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 基本信息
|
|||
|
|
$displayName = !empty($user['screenName']) ? $user['screenName'] : $user['name'];
|
|||
|
|
$userUrl = !empty($user['url']) ? $user['url'] : '#';
|
|||
|
|
|
|||
|
|
// 构建HTML
|
|||
|
|
$html = '<span class="comment-author usercard-wrapper">';
|
|||
|
|
$html .= '<a href="' . $userUrl . '" target="_blank" rel="nofollow">' . $displayName . '</a>';
|
|||
|
|
|
|||
|
|
// 卡片
|
|||
|
|
$html .= '<div class="usercard-popup">';
|
|||
|
|
$html .= '<div class="usercard-header">';
|
|||
|
|
$html .= '<h3>' . $displayName . '</h3>';
|
|||
|
|
$html .= '</div>';
|
|||
|
|
|
|||
|
|
$html .= '<div class="usercard-body">';
|
|||
|
|
$html .= '<div class="usercard-info">';
|
|||
|
|
|
|||
|
|
// 注册时间
|
|||
|
|
$html .= '<p><strong>' . _t('注册时间:') . '</strong>' . date('Y-m-d', $user['created']) . '</p>';
|
|||
|
|
|
|||
|
|
// 评论数
|
|||
|
|
$html .= '<p><strong>' . _t('已评论数:') . '</strong>' . $commentCount . _t('条') . '</p>';
|
|||
|
|
|
|||
|
|
// 最后登录
|
|||
|
|
if ($user['logged']) {
|
|||
|
|
$html .= '<p><strong>' . _t('最后登录:') . '</strong>' . date('m-d H:i', $user['logged']) . '</p>';
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
$html .= '</div>';
|
|||
|
|
|
|||
|
|
// RSS文章
|
|||
|
|
if (!empty($rssItems) && is_array($rssItems) && count($rssItems) > 0) {
|
|||
|
|
// 获取配置中的显示条数
|
|||
|
|
$max_display_items = isset($options->max_items) ? intval($options->max_items) : 5;
|
|||
|
|
|
|||
|
|
// 限制显示的条数
|
|||
|
|
$displayItems = array_slice($rssItems, 0, $max_display_items);
|
|||
|
|
|
|||
|
|
$html .= '<div class="usercard-rss">';
|
|||
|
|
$html .= '<h4>' . _t('') . '</h4>';
|
|||
|
|
$html .= '<ul class="usercard-rss-list">';
|
|||
|
|
|
|||
|
|
foreach ($displayItems as $item) {
|
|||
|
|
$title = htmlspecialchars($item['title']);
|
|||
|
|
if (mb_strlen($title, 'UTF-8') > 40) {
|
|||
|
|
$title = mb_substr($title, 0, 40, 'UTF-8') . '...';
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
$html .= '<li>';
|
|||
|
|
$html .= '<a href="' . $item['link'] . '" target="_blank" title="' . htmlspecialchars($item['title']) . '">' . $title . '</a>';
|
|||
|
|
$html .= '<span class="usercard-rss-date">' . date('Y-m-d', $item['date']) . '</span>';
|
|||
|
|
$html .= '</li>';
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
$html .= '</ul>';
|
|||
|
|
$html .= '</div>';
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
$html .= '</div>';
|
|||
|
|
$html .= '</div>';
|
|||
|
|
$html .= '</span>';
|
|||
|
|
|
|||
|
|
return $html;
|
|||
|
|
|
|||
|
|
} catch (Exception $e) {
|
|||
|
|
// 出错时返回简单链接
|
|||
|
|
if ($comments->url) {
|
|||
|
|
return '<a href="' . $comments->url . '" rel="external nofollow" target="_blank">' . $comments->author . '</a>';
|
|||
|
|
} else {
|
|||
|
|
return $comments->author;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 获取RSS文章
|
|||
|
|
*/
|
|||
|
|
private static function getRssItems($url)
|
|||
|
|
{
|
|||
|
|
if (empty($url)) {
|
|||
|
|
return array();
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 检查缓存
|
|||
|
|
$cacheKey = 'usercard_rss_' . md5($url);
|
|||
|
|
$cache = self::getCache($cacheKey);
|
|||
|
|
|
|||
|
|
if ($cache !== false) {
|
|||
|
|
return $cache;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 获取配置
|
|||
|
|
$options = Typecho_Widget::widget('Widget_Options')->plugin('UserCard');
|
|||
|
|
$timeout = isset($options->timeout) ? intval($options->timeout) : 10;
|
|||
|
|
|
|||
|
|
// 固定从RSS获取最多10条数据保存到缓存
|
|||
|
|
$fetch_max_items = 10;
|
|||
|
|
|
|||
|
|
// 获取RSS
|
|||
|
|
$context = stream_context_create(array(
|
|||
|
|
'http' => array('timeout' => $timeout),
|
|||
|
|
'ssl' => array('verify_peer' => false)
|
|||
|
|
));
|
|||
|
|
|
|||
|
|
$content = @file_get_contents($url, false, $context);
|
|||
|
|
|
|||
|
|
if (empty($content)) {
|
|||
|
|
return array();
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
$xml = @simplexml_load_string($content);
|
|||
|
|
if (!$xml) {
|
|||
|
|
return array();
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
$items = array();
|
|||
|
|
$counter = 0;
|
|||
|
|
|
|||
|
|
// RSS格式
|
|||
|
|
if (isset($xml->channel->item)) {
|
|||
|
|
foreach ($xml->channel->item as $item) {
|
|||
|
|
if ($counter >= $fetch_max_items) break;
|
|||
|
|
|
|||
|
|
$title = trim((string)$item->title);
|
|||
|
|
$link = trim((string)$item->link);
|
|||
|
|
|
|||
|
|
if ($title && $link) {
|
|||
|
|
$pubDate = isset($item->pubDate) ? strtotime((string)$item->pubDate) : time();
|
|||
|
|
|
|||
|
|
$items[] = array(
|
|||
|
|
'title' => $title,
|
|||
|
|
'link' => $link,
|
|||
|
|
'date' => $pubDate
|
|||
|
|
);
|
|||
|
|
$counter++;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 设置缓存
|
|||
|
|
$cacheTime = isset($options->cache_time) ? intval($options->cache_time) : 3600;
|
|||
|
|
self::setCache($cacheKey, $items, $cacheTime);
|
|||
|
|
|
|||
|
|
return $items;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 获取缓存
|
|||
|
|
*/
|
|||
|
|
private static function getCache($key)
|
|||
|
|
{
|
|||
|
|
$cacheFile = dirname(__FILE__) . '/cache/' . $key . '.json';
|
|||
|
|
|
|||
|
|
if (file_exists($cacheFile)) {
|
|||
|
|
$data = json_decode(file_get_contents($cacheFile), true);
|
|||
|
|
if ($data && isset($data['expire']) && $data['expire'] > time()) {
|
|||
|
|
return $data['data'];
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
return false;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 设置缓存
|
|||
|
|
*/
|
|||
|
|
private static function setCache($key, $data, $expire = 3600)
|
|||
|
|
{
|
|||
|
|
$cacheDir = dirname(__FILE__) . '/cache';
|
|||
|
|
|
|||
|
|
if (!is_dir($cacheDir)) {
|
|||
|
|
@mkdir($cacheDir, 0777, true);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
$cacheFile = $cacheDir . '/' . $key . '.json';
|
|||
|
|
$cacheData = array(
|
|||
|
|
'data' => $data,
|
|||
|
|
'expire' => time() + $expire
|
|||
|
|
);
|
|||
|
|
|
|||
|
|
@file_put_contents($cacheFile, json_encode($cacheData, JSON_UNESCAPED_UNICODE));
|
|||
|
|
}
|
|||
|
|
}
|