2029 lines
71 KiB
PHP
2029 lines
71 KiB
PHP
|
|
<?php
|
|||
|
|
if (!defined('__TYPECHO_ROOT_DIR__')) exit;
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 合集插件 - 创建专题合集,支持短代码展示
|
|||
|
|
*
|
|||
|
|
* @package Collection
|
|||
|
|
* @author 石头厝
|
|||
|
|
* @version 1.2.0
|
|||
|
|
* @link https://www.shitoucuo.com/
|
|||
|
|
*/
|
|||
|
|
class Collection_Plugin implements Typecho_Plugin_Interface
|
|||
|
|
{
|
|||
|
|
/**
|
|||
|
|
* 数据库文件路径
|
|||
|
|
*/
|
|||
|
|
private static $dbPath;
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 是否已添加CSS样式
|
|||
|
|
*/
|
|||
|
|
private static $cssAdded = false;
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 是否已添加JS脚本
|
|||
|
|
*/
|
|||
|
|
private static $jsAdded = false;
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 合集配置数组
|
|||
|
|
*/
|
|||
|
|
private static $collectionConfigs = array();
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 激活插件方法
|
|||
|
|
*/
|
|||
|
|
public static function activate()
|
|||
|
|
{
|
|||
|
|
// 初始化数据库
|
|||
|
|
self::initDbPath();
|
|||
|
|
self::initDatabase();
|
|||
|
|
self::migrateDatabase();
|
|||
|
|
|
|||
|
|
// 添加后台管理菜单
|
|||
|
|
Helper::addPanel(3, 'Collection/Manage.php', '合集管理', '合集管理', 'administrator');
|
|||
|
|
|
|||
|
|
// 添加动作处理
|
|||
|
|
Helper::addAction('collection', 'Collection_Action');
|
|||
|
|
|
|||
|
|
// 注册路由
|
|||
|
|
Helper::addRoute('collection_action', '/action/collection', 'Collection_Action', 'action');
|
|||
|
|
|
|||
|
|
// 注册短代码解析
|
|||
|
|
Typecho_Plugin::factory('Widget_Abstract_Contents')->contentEx = array(__CLASS__, 'parseCollectionShortcode');
|
|||
|
|
Typecho_Plugin::factory('Widget_Abstract_Contents')->excerptEx = array(__CLASS__, 'parseCollectionShortcode');
|
|||
|
|
|
|||
|
|
// 注册文章保存时的处理
|
|||
|
|
Typecho_Plugin::factory('Widget_Abstract_Contents')->save = array(__CLASS__, 'onPostSave');
|
|||
|
|
Typecho_Plugin::factory('Widget_Abstract_Contents')->write = array(__CLASS__, 'onPostWrite');
|
|||
|
|
|
|||
|
|
// 只保留footer钩子来输出JS,移除header钩子
|
|||
|
|
Typecho_Plugin::factory('Widget_Archive')->footer = array(__CLASS__, 'footer');
|
|||
|
|
|
|||
|
|
return _t('合集插件已激活');
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 禁用插件方法
|
|||
|
|
*/
|
|||
|
|
public static function deactivate()
|
|||
|
|
{
|
|||
|
|
// 移除管理菜单
|
|||
|
|
Helper::removePanel(3, 'Collection/Manage.php');
|
|||
|
|
|
|||
|
|
// 清理数据库记录
|
|||
|
|
try {
|
|||
|
|
$db = Typecho_Db::get();
|
|||
|
|
$db->query($db->delete('table.options')
|
|||
|
|
->where('name = ?', 'panelTable:Collection/Manage.php'));
|
|||
|
|
} catch (Exception $e) {
|
|||
|
|
// 忽略错误
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
Helper::removeAction('collection');
|
|||
|
|
Helper::removeRoute('collection_action');
|
|||
|
|
|
|||
|
|
return _t('合集插件已禁用');
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 获取插件配置面板
|
|||
|
|
*/
|
|||
|
|
public static function config(Typecho_Widget_Helper_Form $form)
|
|||
|
|
{
|
|||
|
|
// 显示模式
|
|||
|
|
$displayMode = new Typecho_Widget_Helper_Form_Element_Radio('displayMode', array(
|
|||
|
|
'collapsible' => '可折叠模式(默认)',
|
|||
|
|
'expanded' => '始终展开',
|
|||
|
|
'grid' => '网格布局'
|
|||
|
|
), 'collapsible', _t('合集展示模式'));
|
|||
|
|
$form->addInput($displayMode);
|
|||
|
|
|
|||
|
|
// 是否显示发布时间
|
|||
|
|
$showDate = new Typecho_Widget_Helper_Form_Element_Radio('showDate', array(
|
|||
|
|
1 => '是',
|
|||
|
|
0 => '否'
|
|||
|
|
), 1, _t('显示内容发布时间'));
|
|||
|
|
$form->addInput($showDate);
|
|||
|
|
|
|||
|
|
// 是否显示内容摘要
|
|||
|
|
$showExcerpt = new Typecho_Widget_Helper_Form_Element_Radio('showExcerpt', array(
|
|||
|
|
1 => '是',
|
|||
|
|
0 => '否'
|
|||
|
|
), 1, _t('显示内容摘要'));
|
|||
|
|
$form->addInput($showExcerpt);
|
|||
|
|
|
|||
|
|
// 是否显示评论数
|
|||
|
|
$showCommentCount = new Typecho_Widget_Helper_Form_Element_Radio('showCommentCount', array(
|
|||
|
|
1 => '是',
|
|||
|
|
0 => '否'
|
|||
|
|
), 1, _t('显示评论数'));
|
|||
|
|
$form->addInput($showCommentCount);
|
|||
|
|
|
|||
|
|
// 默认排序方式
|
|||
|
|
$defaultSort = new Typecho_Widget_Helper_Form_Element_Select('defaultSort', array(
|
|||
|
|
'created_desc' => '发布时间倒序',
|
|||
|
|
'created_asc' => '发布时间正序',
|
|||
|
|
'title_asc' => '标题正序',
|
|||
|
|
'title_desc' => '标题倒序',
|
|||
|
|
'custom' => '自定义顺序'
|
|||
|
|
), 'created_desc', _t('内容默认排序'));
|
|||
|
|
$form->addInput($defaultSort);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 个人用户的配置面板
|
|||
|
|
*/
|
|||
|
|
public static function personalConfig(Typecho_Widget_Helper_Form $form) {}
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 初始化数据库路径
|
|||
|
|
*/
|
|||
|
|
private static function initDbPath()
|
|||
|
|
{
|
|||
|
|
$dbDir = __DIR__ . '/db';
|
|||
|
|
|
|||
|
|
// 确保目录存在
|
|||
|
|
if (!is_dir($dbDir)) {
|
|||
|
|
@mkdir($dbDir, 0755, true);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
$dbFiles = glob($dbDir . '/collection_*.db');
|
|||
|
|
if (!empty($dbFiles)) {
|
|||
|
|
self::$dbPath = $dbFiles[0];
|
|||
|
|
} else {
|
|||
|
|
$randomStr = substr(md5(uniqid(rand(), true)), 0, 10);
|
|||
|
|
self::$dbPath = $dbDir . '/collection_' . $randomStr . '.db';
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 初始化数据库
|
|||
|
|
*/
|
|||
|
|
private static function initDatabase()
|
|||
|
|
{
|
|||
|
|
if (empty(self::$dbPath)) {
|
|||
|
|
self::initDbPath();
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
try {
|
|||
|
|
$db = new PDO('sqlite:' . self::$dbPath);
|
|||
|
|
$db->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
|
|||
|
|
|
|||
|
|
// 检查表是否存在
|
|||
|
|
$tableCheck = $db->query("SELECT name FROM sqlite_master WHERE type='table' AND name='plugin_collection'");
|
|||
|
|
if (!$tableCheck->fetch()) {
|
|||
|
|
// 创建表
|
|||
|
|
$db->exec("CREATE TABLE plugin_collection (
|
|||
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|||
|
|
name TEXT NOT NULL,
|
|||
|
|
description TEXT,
|
|||
|
|
related_items TEXT,
|
|||
|
|
sort_order TEXT DEFAULT 'created_desc',
|
|||
|
|
collection_type TEXT DEFAULT 'article',
|
|||
|
|
is_active INTEGER DEFAULT 1,
|
|||
|
|
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
|||
|
|
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
|||
|
|
)");
|
|||
|
|
|
|||
|
|
// 创建更新时间触发器
|
|||
|
|
$db->exec("CREATE TRIGGER IF NOT EXISTS update_collection_time
|
|||
|
|
AFTER UPDATE ON plugin_collection
|
|||
|
|
BEGIN
|
|||
|
|
UPDATE plugin_collection SET updated_at = CURRENT_TIMESTAMP WHERE id = NEW.id;
|
|||
|
|
END");
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
$db = null;
|
|||
|
|
} catch (PDOException $e) {
|
|||
|
|
error_log('Collection: 数据库初始化失败: ' . $e->getMessage());
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 数据库迁移
|
|||
|
|
*/
|
|||
|
|
private static function migrateDatabase()
|
|||
|
|
{
|
|||
|
|
try {
|
|||
|
|
$db = new PDO('sqlite:' . self::$dbPath);
|
|||
|
|
$db->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
|
|||
|
|
|
|||
|
|
// 检查表结构
|
|||
|
|
$stmt = $db->prepare("PRAGMA table_info(plugin_collection)");
|
|||
|
|
$stmt->execute();
|
|||
|
|
$columns = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
|||
|
|
|
|||
|
|
// 检查字段是否存在
|
|||
|
|
$hasSortOrder = false;
|
|||
|
|
$hasIsActive = false;
|
|||
|
|
$hasCollectionType = false;
|
|||
|
|
|
|||
|
|
foreach ($columns as $column) {
|
|||
|
|
if ($column['name'] === 'sort_order') {
|
|||
|
|
$hasSortOrder = true;
|
|||
|
|
}
|
|||
|
|
if ($column['name'] === 'is_active') {
|
|||
|
|
$hasIsActive = true;
|
|||
|
|
}
|
|||
|
|
if ($column['name'] === 'collection_type') {
|
|||
|
|
$hasCollectionType = true;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 添加缺失的字段
|
|||
|
|
if (!$hasSortOrder) {
|
|||
|
|
$db->exec("ALTER TABLE plugin_collection ADD COLUMN sort_order TEXT DEFAULT 'created_desc'");
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
if (!$hasIsActive) {
|
|||
|
|
$db->exec("ALTER TABLE plugin_collection ADD COLUMN is_active INTEGER DEFAULT 1");
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
if (!$hasCollectionType) {
|
|||
|
|
$db->exec("ALTER TABLE plugin_collection ADD COLUMN collection_type TEXT DEFAULT 'article'");
|
|||
|
|
// 重命名相关字段
|
|||
|
|
try {
|
|||
|
|
$db->exec("ALTER TABLE plugin_collection RENAME COLUMN related_articles TO related_items");
|
|||
|
|
} catch (Exception $e) {
|
|||
|
|
// 如果重命名失败,可能是已经存在或者已经重命名过了
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
$db = null;
|
|||
|
|
} catch (PDOException $e) {
|
|||
|
|
error_log('Collection数据库迁移失败: ' . $e->getMessage());
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 获取数据库连接
|
|||
|
|
*/
|
|||
|
|
public static function getDbConnection()
|
|||
|
|
{
|
|||
|
|
if (empty(self::$dbPath)) {
|
|||
|
|
self::initDbPath();
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
if (!file_exists(self::$dbPath)) {
|
|||
|
|
self::initDatabase();
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
try {
|
|||
|
|
$db = new PDO('sqlite:' . self::$dbPath);
|
|||
|
|
$db->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
|
|||
|
|
return $db;
|
|||
|
|
} catch (PDOException $e) {
|
|||
|
|
throw new Exception('数据库连接失败: ' . $e->getMessage());
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 通过合集ID获取合集信息
|
|||
|
|
*/
|
|||
|
|
public static function getCollectionById($id)
|
|||
|
|
{
|
|||
|
|
if (empty($id)) {
|
|||
|
|
return null;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
try {
|
|||
|
|
$db = self::getDbConnection();
|
|||
|
|
$stmt = $db->prepare("SELECT * FROM plugin_collection WHERE id = ? AND is_active = 1");
|
|||
|
|
$stmt->execute(array($id));
|
|||
|
|
$collection = $stmt->fetch(PDO::FETCH_ASSOC);
|
|||
|
|
|
|||
|
|
if ($collection) {
|
|||
|
|
// 处理关联内容
|
|||
|
|
if (!empty($collection['related_items'])) {
|
|||
|
|
$itemIds = explode(',', $collection['related_items']);
|
|||
|
|
$itemIds = array_map('trim', $itemIds);
|
|||
|
|
$itemIds = array_filter($itemIds);
|
|||
|
|
|
|||
|
|
if ($collection['collection_type'] == 'comment') {
|
|||
|
|
// 获取关联评论信息
|
|||
|
|
$commentsInfo = array();
|
|||
|
|
foreach ($itemIds as $coid) {
|
|||
|
|
if (is_numeric($coid)) {
|
|||
|
|
$commentInfo = self::getCommentInfo($coid);
|
|||
|
|
if (!empty($commentInfo['author'])) {
|
|||
|
|
$commentsInfo[] = array(
|
|||
|
|
'author' => $commentInfo['author'],
|
|||
|
|
'content' => $commentInfo['text'],
|
|||
|
|
'created' => $commentInfo['created'],
|
|||
|
|
'link' => $commentInfo['link'],
|
|||
|
|
'excerpt' => self::getExcerpt($commentInfo['text']),
|
|||
|
|
'coid' => $coid,
|
|||
|
|
'url' => $commentInfo['url'] // 用户网站链接
|
|||
|
|
);
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
$collection['related_items_info'] = $commentsInfo;
|
|||
|
|
} elseif ($collection['collection_type'] == 'user') {
|
|||
|
|
// 获取关联用户信息
|
|||
|
|
$usersInfo = array();
|
|||
|
|
foreach ($itemIds as $uid) {
|
|||
|
|
if (is_numeric($uid)) {
|
|||
|
|
$userInfo = self::getUserInfo($uid);
|
|||
|
|
if (!empty($userInfo['name'])) {
|
|||
|
|
$usersInfo[] = array(
|
|||
|
|
'name' => $userInfo['name'],
|
|||
|
|
'url' => $userInfo['url'],
|
|||
|
|
'created' => $userInfo['created'],
|
|||
|
|
'commentCount' => $userInfo['commentCount'],
|
|||
|
|
'recentActivity' => $userInfo['recentActivity'],
|
|||
|
|
'uid' => $uid
|
|||
|
|
);
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
$collection['related_items_info'] = $usersInfo;
|
|||
|
|
} else {
|
|||
|
|
// 获取关联文章信息
|
|||
|
|
$articlesInfo = array();
|
|||
|
|
foreach ($itemIds as $cid) {
|
|||
|
|
if (is_numeric($cid)) {
|
|||
|
|
$articleInfo = self::getArticleInfo($cid);
|
|||
|
|
if (!empty($articleInfo['title'])) {
|
|||
|
|
$articlesInfo[] = array(
|
|||
|
|
'title' => $articleInfo['title'],
|
|||
|
|
'link' => $articleInfo['link'],
|
|||
|
|
'created' => $articleInfo['created'],
|
|||
|
|
'excerpt' => self::getExcerpt($articleInfo['text']),
|
|||
|
|
'cover' => !empty($articleInfo['images']) ? $articleInfo['images'][0] : '',
|
|||
|
|
'commentCount' => self::getArticleCommentCount($cid),
|
|||
|
|
'cid' => $cid
|
|||
|
|
);
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
$collection['related_items_info'] = $articlesInfo;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 根据排序方式排序
|
|||
|
|
$collection['related_items_info'] = self::sortItems($collection['related_items_info'], $collection['sort_order'], $collection['collection_type']);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
return $collection;
|
|||
|
|
}
|
|||
|
|
} catch (Exception $e) {
|
|||
|
|
error_log('Collection: 获取合集信息失败: ' . $e->getMessage());
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
return null;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 获取所有合集
|
|||
|
|
*/
|
|||
|
|
public static function getAllCollections()
|
|||
|
|
{
|
|||
|
|
try {
|
|||
|
|
$db = self::getDbConnection();
|
|||
|
|
$stmt = $db->query("SELECT * FROM plugin_collection WHERE is_active = 1 ORDER BY collection_type, created_at DESC");
|
|||
|
|
$collections = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
|||
|
|
|
|||
|
|
return $collections;
|
|||
|
|
} catch (Exception $e) {
|
|||
|
|
error_log('Collection: 获取所有合集失败: ' . $e->getMessage());
|
|||
|
|
return array();
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 通过文章CID获取文章信息和图片
|
|||
|
|
*/
|
|||
|
|
public static function getArticleInfo($cid)
|
|||
|
|
{
|
|||
|
|
$result = array(
|
|||
|
|
'title' => '',
|
|||
|
|
'link' => '',
|
|||
|
|
'text' => '',
|
|||
|
|
'images' => array(),
|
|||
|
|
'tags' => array(),
|
|||
|
|
'created' => ''
|
|||
|
|
);
|
|||
|
|
|
|||
|
|
if (empty($cid)) {
|
|||
|
|
return $result;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
try {
|
|||
|
|
$db = Typecho_Db::get();
|
|||
|
|
$prefix = $db->getPrefix();
|
|||
|
|
|
|||
|
|
// 使用Typecho_Db的正确查询方法
|
|||
|
|
$article = $db->fetchRow($db->select()
|
|||
|
|
->from('table.contents')
|
|||
|
|
->where('cid = ?', $cid)
|
|||
|
|
->where('type = ?', 'post')
|
|||
|
|
->where('status = ?', 'publish')
|
|||
|
|
->limit(1));
|
|||
|
|
|
|||
|
|
if ($article) {
|
|||
|
|
$result['title'] = $article['title'];
|
|||
|
|
$result['link'] = self::getPostUrlByCid($cid);
|
|||
|
|
$result['text'] = $article['text'];
|
|||
|
|
$result['created'] = $article['created'];
|
|||
|
|
$result['images'] = self::getPostImagesByCid($cid);
|
|||
|
|
$result['tags'] = self::getPostTagsByCid($cid);
|
|||
|
|
}
|
|||
|
|
} catch (Exception $e) {
|
|||
|
|
error_log('Collection: 获取文章信息失败: ' . $e->getMessage());
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
return $result;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 通过评论COID获取评论信息
|
|||
|
|
*/
|
|||
|
|
public static function getCommentInfo($coid)
|
|||
|
|
{
|
|||
|
|
$result = array(
|
|||
|
|
'author' => '',
|
|||
|
|
'text' => '',
|
|||
|
|
'link' => '',
|
|||
|
|
'created' => '',
|
|||
|
|
'parent' => '',
|
|||
|
|
'mail' => '',
|
|||
|
|
'url' => ''
|
|||
|
|
);
|
|||
|
|
|
|||
|
|
if (empty($coid)) {
|
|||
|
|
return $result;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
try {
|
|||
|
|
$db = Typecho_Db::get();
|
|||
|
|
|
|||
|
|
// 获取评论信息
|
|||
|
|
$comment = $db->fetchRow($db->select()
|
|||
|
|
->from('table.comments')
|
|||
|
|
->where('coid = ?', $coid)
|
|||
|
|
->where('status = ?', 'approved')
|
|||
|
|
->limit(1));
|
|||
|
|
|
|||
|
|
if ($comment) {
|
|||
|
|
$result['author'] = $comment['author'];
|
|||
|
|
$result['text'] = $comment['text'];
|
|||
|
|
$result['created'] = $comment['created'];
|
|||
|
|
$result['mail'] = $comment['mail'];
|
|||
|
|
$result['url'] = $comment['url'];
|
|||
|
|
$result['parent'] = $comment['parent'];
|
|||
|
|
|
|||
|
|
// 获取评论所属文章信息以生成链接
|
|||
|
|
$post = $db->fetchRow($db->select('slug', 'type', 'created')
|
|||
|
|
->from('table.contents')
|
|||
|
|
->where('cid = ?', $comment['cid']));
|
|||
|
|
|
|||
|
|
if ($post) {
|
|||
|
|
$result['link'] = self::getPostUrlByCid($comment['cid']) . '#comment-' . $coid;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
} catch (Exception $e) {
|
|||
|
|
error_log('Collection: 获取评论信息失败: ' . $e->getMessage());
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
return $result;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 通过用户UID获取用户信息
|
|||
|
|
*/
|
|||
|
|
public static function getUserInfo($uid)
|
|||
|
|
{
|
|||
|
|
$result = array(
|
|||
|
|
'name' => '',
|
|||
|
|
'url' => '',
|
|||
|
|
'mail' => '',
|
|||
|
|
'created' => '',
|
|||
|
|
'recentActivity' => '',
|
|||
|
|
'commentCount' => 0
|
|||
|
|
);
|
|||
|
|
|
|||
|
|
if (empty($uid)) {
|
|||
|
|
return $result;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
try {
|
|||
|
|
$db = Typecho_Db::get();
|
|||
|
|
|
|||
|
|
// 获取用户信息
|
|||
|
|
$user = $db->fetchRow($db->select()
|
|||
|
|
->from('table.users')
|
|||
|
|
->where('uid = ?', $uid)
|
|||
|
|
->limit(1));
|
|||
|
|
|
|||
|
|
if ($user) {
|
|||
|
|
$result['name'] = $user['name'];
|
|||
|
|
$result['url'] = $user['url'];
|
|||
|
|
$result['mail'] = $user['mail'];
|
|||
|
|
$result['created'] = $user['created'];
|
|||
|
|
|
|||
|
|
// 获取用户最近活跃时间(如果有的话)
|
|||
|
|
if (isset($user['logged']) && $user['logged']) {
|
|||
|
|
$result['recentActivity'] = date('Y-m-d', $user['logged']);
|
|||
|
|
} elseif (isset($user['activated']) && $user['activated']) {
|
|||
|
|
$result['recentActivity'] = date('Y-m-d', $user['activated']);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 获取用户评论总数 - 修复语法错误
|
|||
|
|
$commentCountQuery = $db->select('COUNT(*)')
|
|||
|
|
->from('table.comments')
|
|||
|
|
->where('authorId = ?', $uid)
|
|||
|
|
->where('status = ?', 'approved');
|
|||
|
|
|
|||
|
|
$commentCount = $db->fetchRow($commentCountQuery);
|
|||
|
|
|
|||
|
|
if ($commentCount) {
|
|||
|
|
$result['commentCount'] = intval($commentCount['COUNT(*)']);
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
} catch (Exception $e) {
|
|||
|
|
error_log('Collection: 获取用户信息失败: ' . $e->getMessage());
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
return $result;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 获取文章评论数
|
|||
|
|
*/
|
|||
|
|
private static function getArticleCommentCount($cid)
|
|||
|
|
{
|
|||
|
|
try {
|
|||
|
|
$db = Typecho_Db::get();
|
|||
|
|
|
|||
|
|
$countQuery = $db->select('COUNT(*)')
|
|||
|
|
->from('table.comments')
|
|||
|
|
->where('cid = ?', $cid)
|
|||
|
|
->where('status = ?', 'approved');
|
|||
|
|
|
|||
|
|
$count = $db->fetchRow($countQuery);
|
|||
|
|
|
|||
|
|
return $count ? intval($count['COUNT(*)']) : 0;
|
|||
|
|
} catch (Exception $e) {
|
|||
|
|
error_log('Collection: 获取文章评论数失败: ' . $e->getMessage());
|
|||
|
|
return 0;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 通过文章CID获取文章URL
|
|||
|
|
*/
|
|||
|
|
public static function getPostUrlByCid($cid)
|
|||
|
|
{
|
|||
|
|
try {
|
|||
|
|
$db = Typecho_Db::get();
|
|||
|
|
|
|||
|
|
$row = $db->fetchRow($db->select('slug', 'type', 'created')
|
|||
|
|
->from('table.contents')
|
|||
|
|
->where('cid = ?', $cid)
|
|||
|
|
->where('status = ?', 'publish'));
|
|||
|
|
|
|||
|
|
if (!$row) {
|
|||
|
|
return '';
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 获取文章分类
|
|||
|
|
$category = '';
|
|||
|
|
$categories = $db->fetchAll($db->select('slug')
|
|||
|
|
->from('table.metas')
|
|||
|
|
->join('table.relationships', 'table.metas.mid = table.relationships.mid')
|
|||
|
|
->where('table.relationships.cid = ?', $cid)
|
|||
|
|
->where('table.metas.type = ?', 'category')
|
|||
|
|
->order('table.metas.order', Typecho_Db::SORT_ASC));
|
|||
|
|
|
|||
|
|
if (!empty($categories)) {
|
|||
|
|
$category = $categories[0]['slug'];
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 准备URL参数
|
|||
|
|
$date = getdate($row['created']);
|
|||
|
|
$params = array(
|
|||
|
|
'cid' => $cid,
|
|||
|
|
'slug' => $row['slug'],
|
|||
|
|
'category' => $category,
|
|||
|
|
'directory' => $category,
|
|||
|
|
'year' => $date['year'],
|
|||
|
|
'month' => str_pad($date['mon'], 2, '0', STR_PAD_LEFT),
|
|||
|
|
'day' => str_pad($date['mday'], 2, '0', STR_PAD_LEFT)
|
|||
|
|
);
|
|||
|
|
|
|||
|
|
$options = Typecho_Widget::widget('Widget_Options');
|
|||
|
|
$permalinkStructure = '';
|
|||
|
|
|
|||
|
|
if (isset($options->permalink)) {
|
|||
|
|
$permalinkStructure = $options->permalink;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
if (empty($permalinkStructure)) {
|
|||
|
|
return Typecho_Router::url($row['type'], $row, $options->index);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
$url = $permalinkStructure;
|
|||
|
|
$url = preg_replace('/\[year:digital:4\]/', $params['year'], $url);
|
|||
|
|
$url = preg_replace('/\[month:digital:2\]/', $params['month'], $url);
|
|||
|
|
$url = preg_replace('/\[day:digital:2\]/', $params['day'], $url);
|
|||
|
|
$url = preg_replace('/\[slug\]/', $params['slug'], $url);
|
|||
|
|
$url = preg_replace('/\[cid\]/', $params['cid'], $url);
|
|||
|
|
$url = preg_replace('/\[category\]/', $params['category'], $url);
|
|||
|
|
$url = preg_replace('/\[directory\]/', $params['directory'], $url);
|
|||
|
|
|
|||
|
|
if (strpos($url, '/') !== 0) {
|
|||
|
|
$url = '/' . $url;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
return rtrim($options->siteUrl, '/') . $url;
|
|||
|
|
|
|||
|
|
} catch (Exception $e) {
|
|||
|
|
error_log("Collection: 获取文章 URL 失败: " . $e->getMessage());
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
return '';
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 通过文章CID获取文章中的图片
|
|||
|
|
*/
|
|||
|
|
public static function getPostImagesByCid($cid)
|
|||
|
|
{
|
|||
|
|
try {
|
|||
|
|
$db = Typecho_Db::get();
|
|||
|
|
|
|||
|
|
$content = $db->fetchRow($db->select('text')
|
|||
|
|
->from('table.contents')
|
|||
|
|
->where('cid = ?', $cid)
|
|||
|
|
->where('type = ?', 'post')
|
|||
|
|
->where('status = ?', 'publish'));
|
|||
|
|
|
|||
|
|
if (!$content) {
|
|||
|
|
return array();
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
$images = array();
|
|||
|
|
$text = $content['text'];
|
|||
|
|
|
|||
|
|
// 标准img标签
|
|||
|
|
preg_match_all('/<img[^>]+src=[\'"]([^\'"]+)[\'"][^>]*>/i', $text, $matches1);
|
|||
|
|
if (isset($matches1[1]) && !empty($matches1[1])) {
|
|||
|
|
foreach($matches1[1] as $imageUrl) {
|
|||
|
|
$images[] = self::processImageUrl($imageUrl);
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// markdown图片语法
|
|||
|
|
preg_match_all('/!\[[^\]]*\]\(([^)]+)\)/i', $text, $matches2);
|
|||
|
|
if (isset($matches2[1]) && !empty($matches2[1])) {
|
|||
|
|
foreach($matches2[1] as $imageUrl) {
|
|||
|
|
$images[] = self::processImageUrl($imageUrl);
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 去重并过滤空值
|
|||
|
|
$images = array_filter(array_unique($images));
|
|||
|
|
$images = array_slice($images, 0, 4);
|
|||
|
|
|
|||
|
|
return $images;
|
|||
|
|
} catch (Exception $e) {
|
|||
|
|
error_log('Collection: 获取文章图片失败: ' . $e->getMessage());
|
|||
|
|
return array();
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 处理图片URL
|
|||
|
|
*/
|
|||
|
|
private static function processImageUrl($imageUrl)
|
|||
|
|
{
|
|||
|
|
if (strpos($imageUrl, 'http') !== 0) {
|
|||
|
|
$options = Typecho_Widget::widget('Widget_Options');
|
|||
|
|
$imageUrl = Typecho_Common::url($imageUrl, $options->siteUrl);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
return $imageUrl;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 通过文章CID获取文章标签
|
|||
|
|
*/
|
|||
|
|
public static function getPostTagsByCid($cid)
|
|||
|
|
{
|
|||
|
|
try {
|
|||
|
|
$db = Typecho_Db::get();
|
|||
|
|
|
|||
|
|
$tags = $db->fetchAll($db->select('table.metas.name', 'table.metas.slug')
|
|||
|
|
->from('table.metas')
|
|||
|
|
->join('table.relationships', 'table.metas.mid = table.relationships.mid')
|
|||
|
|
->where('table.relationships.cid = ?', $cid)
|
|||
|
|
->where('table.metas.type = ?', 'tag')
|
|||
|
|
->order('table.metas.order', Typecho_Db::SORT_ASC));
|
|||
|
|
|
|||
|
|
$tagNames = array();
|
|||
|
|
if (!empty($tags)) {
|
|||
|
|
foreach ($tags as $tag) {
|
|||
|
|
$tagNames[] = $tag['name'];
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
return $tagNames;
|
|||
|
|
} catch (Exception $e) {
|
|||
|
|
error_log('Collection: 获取文章标签失败: ' . $e->getMessage());
|
|||
|
|
return array();
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 获取内容摘要
|
|||
|
|
*/
|
|||
|
|
private static function getExcerpt($content, $length = 100)
|
|||
|
|
{
|
|||
|
|
$text = strip_tags($content);
|
|||
|
|
$text = preg_replace('/\s+/', ' ', $text);
|
|||
|
|
|
|||
|
|
if (mb_strlen($text, 'UTF-8') > $length) {
|
|||
|
|
$text = mb_substr($text, 0, $length, 'UTF-8') . '...';
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
return $text;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 排序内容
|
|||
|
|
*/
|
|||
|
|
private static function sortItems($items, $sortOrder, $collectionType)
|
|||
|
|
{
|
|||
|
|
if (empty($items) || count($items) <= 1) {
|
|||
|
|
return $items;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
usort($items, function($a, $b) use ($sortOrder, $collectionType) {
|
|||
|
|
switch($sortOrder) {
|
|||
|
|
case 'created_desc':
|
|||
|
|
return ($b['created'] ?? 0) - ($a['created'] ?? 0);
|
|||
|
|
case 'created_asc':
|
|||
|
|
return ($a['created'] ?? 0) - ($b['created'] ?? 0);
|
|||
|
|
case 'title_asc':
|
|||
|
|
if ($collectionType === 'user') {
|
|||
|
|
return strcmp($a['name'] ?? '', $b['name'] ?? '');
|
|||
|
|
} else {
|
|||
|
|
return strcmp($a['title'] ?? '', $b['title'] ?? '');
|
|||
|
|
}
|
|||
|
|
case 'title_desc':
|
|||
|
|
if ($collectionType === 'user') {
|
|||
|
|
return strcmp($b['name'] ?? '', $a['name'] ?? '');
|
|||
|
|
} else {
|
|||
|
|
return strcmp($b['title'] ?? '', $a['title'] ?? '');
|
|||
|
|
}
|
|||
|
|
case 'name_asc':
|
|||
|
|
return strcmp($a['name'] ?? '', $b['name'] ?? '');
|
|||
|
|
case 'name_desc':
|
|||
|
|
return strcmp($b['name'] ?? '', $a['name'] ?? '');
|
|||
|
|
case 'comments_desc':
|
|||
|
|
return ($b['commentCount'] ?? 0) - ($a['commentCount'] ?? 0);
|
|||
|
|
case 'comments_asc':
|
|||
|
|
return ($a['commentCount'] ?? 0) - ($b['commentCount'] ?? 0);
|
|||
|
|
default:
|
|||
|
|
// 自定义顺序保持原样
|
|||
|
|
return 0;
|
|||
|
|
}
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
return $items;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 解析文章内容中的合集短代码
|
|||
|
|
*/
|
|||
|
|
public static function parseCollectionShortcode($content, $widget, $lastResult)
|
|||
|
|
{
|
|||
|
|
$content = empty($lastResult) ? $content : $lastResult;
|
|||
|
|
|
|||
|
|
$pattern = '/\{collection-(\d+)\}/i';
|
|||
|
|
|
|||
|
|
if (preg_match_all($pattern, $content, $matches)) {
|
|||
|
|
$currentCid = isset($widget->cid) ? $widget->cid : 0;
|
|||
|
|
|
|||
|
|
// 收集所有需要渲染的合集ID
|
|||
|
|
$collectionIds = $matches[1];
|
|||
|
|
$collectionIndex = 0;
|
|||
|
|
|
|||
|
|
foreach ($collectionIds as $index => $collectionId) {
|
|||
|
|
// 渲染单个合集卡片,同时获取CSS样式
|
|||
|
|
list($collectionHtml, $cssStyle, $config) = self::renderSingleCollectionCard($collectionId, $collectionIndex);
|
|||
|
|
|
|||
|
|
if ($collectionHtml) {
|
|||
|
|
// 将CSS内联到HTML中(避免与足迹地图冲突)
|
|||
|
|
$collectionHtml = '<style>' . $cssStyle . '</style>' . $collectionHtml;
|
|||
|
|
$content = str_replace($matches[0][$index], $collectionHtml, $content);
|
|||
|
|
|
|||
|
|
if ($config) {
|
|||
|
|
self::$collectionConfigs[] = $config;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
if ($currentCid > 0) {
|
|||
|
|
self::syncRelatedItems($collectionId, $currentCid, 'article');
|
|||
|
|
}
|
|||
|
|
} else {
|
|||
|
|
$content = str_replace($matches[0][$index],
|
|||
|
|
'<div class="collection-error">合集ID ' . $collectionId . ' 不存在</div>',
|
|||
|
|
$content);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
$collectionIndex++;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
return $content;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 渲染单个合集卡片HTML
|
|||
|
|
*/
|
|||
|
|
private static function renderSingleCollectionCard($collectionId, $index)
|
|||
|
|
{
|
|||
|
|
$collection = self::getCollectionById($collectionId);
|
|||
|
|
if (!$collection) {
|
|||
|
|
return array(null, null, null);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
$options = Typecho_Widget::widget('Widget_Options')->plugin('Collection');
|
|||
|
|
|
|||
|
|
$displayMode = isset($options->displayMode) ? $options->displayMode : 'collapsible';
|
|||
|
|
$showDate = isset($options->showDate) ? (bool)$options->showDate : true;
|
|||
|
|
$showExcerpt = isset($options->showExcerpt) ? (bool)$options->showExcerpt : true;
|
|||
|
|
$showCommentCount = isset($options->showCommentCount) ? (bool)$options->showCommentCount : true;
|
|||
|
|
|
|||
|
|
$items = isset($collection['related_items_info']) ? $collection['related_items_info'] : array();
|
|||
|
|
$itemsCount = count($items);
|
|||
|
|
$collectionType = $collection['collection_type'];
|
|||
|
|
|
|||
|
|
$uniqueId = 'collection_' . $collectionId . '_' . $index;
|
|||
|
|
$collectionCardId = 'collection-card-' . $uniqueId;
|
|||
|
|
|
|||
|
|
// 构建统计文本 - 根据合集类型使用正确的量词
|
|||
|
|
$countText = '';
|
|||
|
|
if ($collectionType === 'comment') {
|
|||
|
|
$countText = '已收录' . $itemsCount . '条评论';
|
|||
|
|
} elseif ($collectionType === 'user') {
|
|||
|
|
$countText = '已收录' . $itemsCount . '位用户';
|
|||
|
|
} else {
|
|||
|
|
$countText = '已收录' . $itemsCount . '篇文章';
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
$html = '
|
|||
|
|
<div class="collection-card" id="' . $collectionCardId . '" data-collection-id="' . $collectionId . '" data-display-mode="' . $displayMode . '" data-collection-type="' . $collectionType . '">
|
|||
|
|
<div class="collection-header">
|
|||
|
|
<div class="collection-title-row">
|
|||
|
|
<div class="collection-title">' . htmlspecialchars($collection['name']) . '</div>
|
|||
|
|
<div class="collection-count">' . $countText . '</div>
|
|||
|
|
</div>';
|
|||
|
|
|
|||
|
|
if ($collection['description']) {
|
|||
|
|
$html .= '<div class="collection-description">' . nl2br(htmlspecialchars($collection['description'])) . '</div>';
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
$html .= '</div>';
|
|||
|
|
|
|||
|
|
if ($itemsCount === 1) {
|
|||
|
|
$item = reset($items);
|
|||
|
|
|
|||
|
|
if ($collectionType === 'comment') {
|
|||
|
|
$authorLink = !empty($item['url']) ?
|
|||
|
|
'<a href="' . htmlspecialchars($item['url']) . '" target="_blank" class="author-link">' . htmlspecialchars($item['author']) . '</a>' :
|
|||
|
|
htmlspecialchars($item['author']);
|
|||
|
|
|
|||
|
|
$html .= '
|
|||
|
|
<div class="collection-single-item">
|
|||
|
|
<div class="item-info">
|
|||
|
|
<div class="item-content-link">
|
|||
|
|
<a href="' . htmlspecialchars($item['link']) . '" target="_blank" class="comment-link">' . htmlspecialchars($item['excerpt']) . '</a>
|
|||
|
|
</div>
|
|||
|
|
<div class="comment-meta">
|
|||
|
|
<div class="comment-author">' . $authorLink . '</div>';
|
|||
|
|
|
|||
|
|
if ($showDate && isset($item['created']) && $item['created']) {
|
|||
|
|
$date = date('Y-m-d', $item['created']);
|
|||
|
|
$html .= '<div class="comment-date">评论于 ' . $date . '</div>';
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
$html .= '
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
</div>';
|
|||
|
|
} elseif ($collectionType === 'user') {
|
|||
|
|
$userNameLink = !empty($item['url']) ?
|
|||
|
|
'<a href="' . htmlspecialchars($item['url']) . '" target="_blank" class="user-link">' . htmlspecialchars($item['name']) . '</a>' :
|
|||
|
|
htmlspecialchars($item['name']);
|
|||
|
|
|
|||
|
|
$html .= '
|
|||
|
|
<div class="collection-single-item">
|
|||
|
|
<div class="item-info">
|
|||
|
|
<div class="item-name">用户:' . $userNameLink . '</div>';
|
|||
|
|
|
|||
|
|
if ($showDate && isset($item['created']) && $item['created']) {
|
|||
|
|
$date = date('Y-m-d', $item['created']);
|
|||
|
|
$html .= '<div class="item-date">注册于 ' . $date . '</div>';
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
if (isset($item['recentActivity']) && $item['recentActivity']) {
|
|||
|
|
$html .= '<div class="item-activity">最近活跃:' . $item['recentActivity'] . '</div>';
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
$html .= '
|
|||
|
|
</div>
|
|||
|
|
</div>';
|
|||
|
|
} else {
|
|||
|
|
$html .= '
|
|||
|
|
<div class="collection-single-item">
|
|||
|
|
<div class="item-info">
|
|||
|
|
<div class="item-title"><a href="' . htmlspecialchars($item['link']) . '" target="_blank">' . htmlspecialchars($item['title']) . '</a></div>';
|
|||
|
|
|
|||
|
|
if ($showExcerpt && isset($item['excerpt']) && $item['excerpt']) {
|
|||
|
|
$html .= '<p class="item-excerpt">' . htmlspecialchars($item['excerpt']) . '</p>';
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
if ($showDate && isset($item['created']) && $item['created']) {
|
|||
|
|
$date = date('Y-m-d', $item['created']);
|
|||
|
|
$html .= '<div class="item-date">发布于 ' . $date . '</div>';
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
$html .= '
|
|||
|
|
</div>
|
|||
|
|
</div>';
|
|||
|
|
}
|
|||
|
|
} else if ($itemsCount > 1) {
|
|||
|
|
$html .= '
|
|||
|
|
<div class="collection-items-container">';
|
|||
|
|
|
|||
|
|
if ($displayMode === 'collapsible') {
|
|||
|
|
// 预览内容(最多3个)
|
|||
|
|
$previewCount = min(3, $itemsCount);
|
|||
|
|
$previewItems = array_slice($items, 0, $previewCount);
|
|||
|
|
$remainingItems = array_slice($items, $previewCount);
|
|||
|
|
|
|||
|
|
if ($previewCount > 0) {
|
|||
|
|
$html .= '
|
|||
|
|
<div class="items-preview">';
|
|||
|
|
|
|||
|
|
foreach ($previewItems as $item) {
|
|||
|
|
$html .= '
|
|||
|
|
<div class="item-preview-item">';
|
|||
|
|
|
|||
|
|
if ($collectionType === 'comment') {
|
|||
|
|
$authorLink = !empty($item['url']) ?
|
|||
|
|
'<a href="' . htmlspecialchars($item['url']) . '" target="_blank" class="author-link">' . htmlspecialchars($item['author']) . '</a>' :
|
|||
|
|
htmlspecialchars($item['author']);
|
|||
|
|
|
|||
|
|
$html .= '
|
|||
|
|
<div class="item-preview-content">
|
|||
|
|
<a href="' . htmlspecialchars($item['link']) . '" target="_blank" class="comment-link">' . htmlspecialchars(self::getExcerpt($item['content'], 60)) . '</a>
|
|||
|
|
</div>
|
|||
|
|
<div class="comment-preview-meta">
|
|||
|
|
<div class="comment-preview-author">
|
|||
|
|
<strong>' . $authorLink . '</strong>
|
|||
|
|
</div>';
|
|||
|
|
|
|||
|
|
if ($showDate && isset($item['created']) && $item['created']) {
|
|||
|
|
$date = date('Y-m-d', $item['created']);
|
|||
|
|
$html .= '<div class="comment-preview-date">' . $date . '</div>';
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
$html .= '
|
|||
|
|
</div>';
|
|||
|
|
} elseif ($collectionType === 'user') {
|
|||
|
|
$userNameLink = !empty($item['url']) ?
|
|||
|
|
'<a href="' . htmlspecialchars($item['url']) . '" target="_blank" class="user-link">' . htmlspecialchars($item['name']) . '</a>' :
|
|||
|
|
htmlspecialchars($item['name']);
|
|||
|
|
|
|||
|
|
$html .= '
|
|||
|
|
<div class="item-preview-name">
|
|||
|
|
<strong>' . $userNameLink . '</strong>
|
|||
|
|
</div>';
|
|||
|
|
|
|||
|
|
if ($showDate && isset($item['created']) && $item['created']) {
|
|||
|
|
$date = date('Y-m-d', $item['created']);
|
|||
|
|
$html .= '<div class="item-preview-date">注册于 ' . $date . '</div>';
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
if ($collectionType === 'user' && isset($item['recentActivity']) && $item['recentActivity']) {
|
|||
|
|
$html .= '<div class="item-preview-activity">最近活跃:' . $item['recentActivity'] . '</div>';
|
|||
|
|
}
|
|||
|
|
} else {
|
|||
|
|
$html .= '
|
|||
|
|
<div class="item-preview-title">
|
|||
|
|
<a href="' . htmlspecialchars($item['link']) . '" target="_blank">' . htmlspecialchars($item['title']) . '</a>
|
|||
|
|
</div>';
|
|||
|
|
|
|||
|
|
if ($showDate && isset($item['created']) && $item['created']) {
|
|||
|
|
$date = date('Y-m-d', $item['created']);
|
|||
|
|
$html .= '<div class="item-preview-date">发布于 ' . $date . '</div>';
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
$html .= '</div>';
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
$html .= '
|
|||
|
|
</div>';
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 如果有剩余内容,显示展开按钮和剩余内容列表
|
|||
|
|
if (count($remainingItems) > 0) {
|
|||
|
|
$html .= '
|
|||
|
|
<div class="collection-toggle-container">
|
|||
|
|
<button type="button" class="collection-toggle-btn" data-target="' . $collectionCardId . '">
|
|||
|
|
<span class="toggle-text">展开查看更多' . ($collectionType === 'comment' ? '评论' : ($collectionType === 'user' ? '用户' : '文章')) . '(' . count($remainingItems) . '个)</span>
|
|||
|
|
<span class="toggle-icon">▼</span>
|
|||
|
|
</button>
|
|||
|
|
</div>';
|
|||
|
|
|
|||
|
|
$html .= '
|
|||
|
|
<div class="collection-items-full" style="display: none;">
|
|||
|
|
<div class="items-list">';
|
|||
|
|
|
|||
|
|
// 只显示剩余的内容,不重复显示预览中的内容
|
|||
|
|
foreach ($remainingItems as $item) {
|
|||
|
|
$html .= '
|
|||
|
|
<div class="item-list-item">';
|
|||
|
|
|
|||
|
|
if ($collectionType === 'comment') {
|
|||
|
|
$authorLink = !empty($item['url']) ?
|
|||
|
|
'<a href="' . htmlspecialchars($item['url']) . '" target="_blank" class="author-link">' . htmlspecialchars($item['author']) . '</a>' :
|
|||
|
|
htmlspecialchars($item['author']);
|
|||
|
|
|
|||
|
|
$html .= '
|
|||
|
|
<div class="item-list-content">
|
|||
|
|
<a href="' . htmlspecialchars($item['link']) . '" target="_blank" class="comment-link">' . htmlspecialchars(self::getExcerpt($item['content'], 100)) . '</a>
|
|||
|
|
</div>
|
|||
|
|
<div class="item-list-info">
|
|||
|
|
<div class="item-list-author">
|
|||
|
|
<strong>' . $authorLink . '</strong>
|
|||
|
|
</div>
|
|||
|
|
<div class="item-list-date">' . date('Y-m-d', $item['created']) . '</div>
|
|||
|
|
</div>';
|
|||
|
|
} elseif ($collectionType === 'user') {
|
|||
|
|
$userNameLink = !empty($item['url']) ?
|
|||
|
|
'<a href="' . htmlspecialchars($item['url']) . '" target="_blank" class="user-link">' . htmlspecialchars($item['name']) . '</a>' :
|
|||
|
|
htmlspecialchars($item['name']);
|
|||
|
|
|
|||
|
|
$html .= '
|
|||
|
|
<div class="item-list-info">
|
|||
|
|
<div class="item-list-name">
|
|||
|
|
<strong>' . $userNameLink . '</strong>
|
|||
|
|
</div>
|
|||
|
|
<div class="item-list-date">' . date('Y-m-d', $item['created']) . '</div>
|
|||
|
|
</div>';
|
|||
|
|
|
|||
|
|
if (isset($item['recentActivity']) && $item['recentActivity']) {
|
|||
|
|
$html .= '<div class="item-list-activity">最近活跃:' . $item['recentActivity'] . '</div>';
|
|||
|
|
}
|
|||
|
|
} else {
|
|||
|
|
$html .= '
|
|||
|
|
<div class="item-list-info">
|
|||
|
|
<div class="item-list-title">
|
|||
|
|
<a href="' . htmlspecialchars($item['link']) . '" target="_blank">' . htmlspecialchars($item['title']) . '</a>
|
|||
|
|
</div>
|
|||
|
|
<div class="item-list-date">' . date('Y-m-d', $item['created']) . '</div>
|
|||
|
|
</div>';
|
|||
|
|
|
|||
|
|
if ($showExcerpt && isset($item['excerpt']) && $item['excerpt']) {
|
|||
|
|
$html .= '<div class="item-list-excerpt">' . htmlspecialchars($item['excerpt']) . '</div>';
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
$html .= '</div>';
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
$html .= '
|
|||
|
|
</div>
|
|||
|
|
</div>';
|
|||
|
|
}
|
|||
|
|
} else {
|
|||
|
|
// 始终展开模式
|
|||
|
|
$html .= '
|
|||
|
|
<div class="items-list always-expanded">';
|
|||
|
|
|
|||
|
|
foreach ($items as $item) {
|
|||
|
|
$html .= '
|
|||
|
|
<div class="item-list-item">';
|
|||
|
|
|
|||
|
|
if ($collectionType === 'comment') {
|
|||
|
|
$authorLink = !empty($item['url']) ?
|
|||
|
|
'<a href="' . htmlspecialchars($item['url']) . '" target="_blank" class="author-link">' . htmlspecialchars($item['author']) . '</a>' :
|
|||
|
|
htmlspecialchars($item['author']);
|
|||
|
|
|
|||
|
|
$html .= '
|
|||
|
|
<div class="item-list-content">
|
|||
|
|
<a href="' . htmlspecialchars($item['link']) . '" target="_blank" class="comment-link">' . htmlspecialchars(self::getExcerpt($item['content'], 100)) . '</a>
|
|||
|
|
</div>
|
|||
|
|
<div class="item-list-info">
|
|||
|
|
<div class="item-list-author">
|
|||
|
|
<strong>' . $authorLink . '</strong>
|
|||
|
|
</div>
|
|||
|
|
<div class="item-list-date">' . date('Y-m-d', $item['created']) . '</div>
|
|||
|
|
</div>';
|
|||
|
|
} elseif ($collectionType === 'user') {
|
|||
|
|
$userNameLink = !empty($item['url']) ?
|
|||
|
|
'<a href="' . htmlspecialchars($item['url']) . '" target="_blank" class="user-link">' . htmlspecialchars($item['name']) . '</a>' :
|
|||
|
|
htmlspecialchars($item['name']);
|
|||
|
|
|
|||
|
|
$html .= '
|
|||
|
|
<div class="item-list-info">
|
|||
|
|
<div class="item-list-name">
|
|||
|
|
<strong>' . $userNameLink . '</strong>
|
|||
|
|
</div>
|
|||
|
|
<div class="item-list-date">' . date('Y-m-d', $item['created']) . '</div>
|
|||
|
|
</div>';
|
|||
|
|
|
|||
|
|
if (isset($item['recentActivity']) && $item['recentActivity']) {
|
|||
|
|
$html .= '<div class="item-list-activity">最近活跃:' . $item['recentActivity'] . '</div>';
|
|||
|
|
}
|
|||
|
|
} else {
|
|||
|
|
$html .= '
|
|||
|
|
<div class="item-list-info">
|
|||
|
|
<div class="item-list-title">
|
|||
|
|
<a href="' . htmlspecialchars($item['link']) . '" target="_blank">' . htmlspecialchars($item['title']) . '</a>
|
|||
|
|
</div>
|
|||
|
|
<div class="item-list-date">' . date('Y-m-d', $item['created']) . '</div>
|
|||
|
|
</div>';
|
|||
|
|
|
|||
|
|
if ($showExcerpt && isset($item['excerpt']) && $item['excerpt']) {
|
|||
|
|
$html .= '<div class="item-list-excerpt">' . htmlspecialchars($item['excerpt']) . '</div>';
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
$html .= '</div>';
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
$html .= '
|
|||
|
|
</div>';
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
$html .= '
|
|||
|
|
</div>';
|
|||
|
|
} else {
|
|||
|
|
$html .= '
|
|||
|
|
<div class="collection-no-items">
|
|||
|
|
<p>该合集暂无关联' . ($collectionType === 'comment' ? '评论' : ($collectionType === 'user' ? '用户' : '文章')) . '</p>
|
|||
|
|
</div>';
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
$html .= '
|
|||
|
|
</div>';
|
|||
|
|
|
|||
|
|
$config = array(
|
|||
|
|
'containerId' => $collectionCardId,
|
|||
|
|
'collectionId' => $collectionId,
|
|||
|
|
'name' => $collection['name'],
|
|||
|
|
'type' => $collectionType
|
|||
|
|
);
|
|||
|
|
|
|||
|
|
return array($html, self::getCollectionStyles(), $config);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 移除header方法,改为在解析短代码时内联CSS
|
|||
|
|
*/
|
|||
|
|
public static function header()
|
|||
|
|
{
|
|||
|
|
// 不再使用header方法输出CSS
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 输出JS到页面底部
|
|||
|
|
*/
|
|||
|
|
public static function footer()
|
|||
|
|
{
|
|||
|
|
if (!empty(self::$collectionConfigs) && !self::$jsAdded) {
|
|||
|
|
echo self::getCollectionScripts(self::$collectionConfigs);
|
|||
|
|
self::$jsAdded = true;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 获取合集CSS样式
|
|||
|
|
*/
|
|||
|
|
private static function getCollectionStyles()
|
|||
|
|
{
|
|||
|
|
return '
|
|||
|
|
.collection-card {
|
|||
|
|
border: 1px solid #e8e8e8;
|
|||
|
|
border-radius: 8px;
|
|||
|
|
overflow: hidden;
|
|||
|
|
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
|
|||
|
|
background: white;
|
|||
|
|
margin: 20px 0;
|
|||
|
|
padding: 20px;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.collection-header {
|
|||
|
|
margin-bottom: 20px;
|
|||
|
|
border-bottom: 1px solid #e8e8e8;
|
|||
|
|
padding-bottom: 15px;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.collection-title-row {
|
|||
|
|
display: flex;
|
|||
|
|
justify-content: space-between;
|
|||
|
|
align-items: center;
|
|||
|
|
margin-bottom: 10px;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.collection-title {
|
|||
|
|
margin: 0;
|
|||
|
|
font-size: 20px;
|
|||
|
|
font-weight: 600;
|
|||
|
|
color: #333;
|
|||
|
|
flex: 1;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.collection-count {
|
|||
|
|
color: #666;
|
|||
|
|
font-size: 14px;
|
|||
|
|
padding: 4px 10px;
|
|||
|
|
background: #f5f5f5;
|
|||
|
|
border-radius: 4px;
|
|||
|
|
white-space: nowrap;
|
|||
|
|
margin-left: 15px;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.collection-description {
|
|||
|
|
color: #666;
|
|||
|
|
font-size: 14px;
|
|||
|
|
line-height: 1.6;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.collection-single-item {
|
|||
|
|
display: flex;
|
|||
|
|
gap: 20px;
|
|||
|
|
align-items: flex-start;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.item-info {
|
|||
|
|
flex: 1;
|
|||
|
|
min-width: 0;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.item-title {
|
|||
|
|
margin: 0 0 10px 0;
|
|||
|
|
font-size: 18px;
|
|||
|
|
font-weight: 500;
|
|||
|
|
color: #333;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.item-title a {
|
|||
|
|
color: #333;
|
|||
|
|
text-decoration: none;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.item-title a:hover {
|
|||
|
|
color: #3b82f6;
|
|||
|
|
text-decoration: underline;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.item-name {
|
|||
|
|
margin: 0 0 10px 0;
|
|||
|
|
font-size: 18px;
|
|||
|
|
font-weight: 500;
|
|||
|
|
color: #333;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.item-name a.user-link {
|
|||
|
|
color: #333;
|
|||
|
|
text-decoration: none;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.item-name a.user-link:hover {
|
|||
|
|
color: #3b82f6;
|
|||
|
|
text-decoration: underline;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.comment-meta {
|
|||
|
|
display: flex;
|
|||
|
|
justify-content: space-between;
|
|||
|
|
align-items: center;
|
|||
|
|
margin-top: 8px;
|
|||
|
|
color: #999;
|
|||
|
|
font-size: 13px;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.comment-author, .comment-date {
|
|||
|
|
color: #999;
|
|||
|
|
font-size: 13px;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.comment-author a.author-link {
|
|||
|
|
color: #999;
|
|||
|
|
text-decoration: none;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.comment-author a.author-link:hover {
|
|||
|
|
color: #3b82f6;
|
|||
|
|
text-decoration: underline;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.item-excerpt {
|
|||
|
|
margin: 10px 0;
|
|||
|
|
color: #666;
|
|||
|
|
font-size: 14px;
|
|||
|
|
line-height: 1.6;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.item-date, .item-activity {
|
|||
|
|
color: #999;
|
|||
|
|
font-size: 13px;
|
|||
|
|
margin-top: 5px;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.item-content-link a.comment-link {
|
|||
|
|
color: #666;
|
|||
|
|
text-decoration: none;
|
|||
|
|
display: block;
|
|||
|
|
padding: 8px 0;
|
|||
|
|
border-left: none;
|
|||
|
|
padding-left: 0;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.item-content-link a.comment-link:hover {
|
|||
|
|
color: #3b82f6;
|
|||
|
|
text-decoration: underline;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.collection-items-container {
|
|||
|
|
margin-top: 15px;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.items-preview {
|
|||
|
|
margin-bottom: 15px;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.item-preview-item {
|
|||
|
|
padding: 10px 0;
|
|||
|
|
border-bottom: 1px dashed #e8e8e8;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.item-preview-item:last-child {
|
|||
|
|
border-bottom: none;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.item-preview-title a {
|
|||
|
|
color: #333;
|
|||
|
|
text-decoration: none;
|
|||
|
|
font-size: 15px;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.item-preview-title a:hover {
|
|||
|
|
color: #3b82f6;
|
|||
|
|
text-decoration: underline;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.item-preview-name, .item-preview-author {
|
|||
|
|
font-weight: 500;
|
|||
|
|
margin-bottom: 5px;
|
|||
|
|
color: #333;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.item-preview-name a.user-link,
|
|||
|
|
.item-preview-author a.author-link {
|
|||
|
|
color: #333;
|
|||
|
|
text-decoration: none;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.item-preview-name a.user-link:hover,
|
|||
|
|
.item-preview-author a.author-link:hover {
|
|||
|
|
color: #3b82f6;
|
|||
|
|
text-decoration: underline;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.item-preview-content {
|
|||
|
|
color: #666;
|
|||
|
|
font-size: 13px;
|
|||
|
|
line-height: 1.5;
|
|||
|
|
margin-bottom: 5px;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.item-preview-content a.comment-link {
|
|||
|
|
color: #666;
|
|||
|
|
text-decoration: none;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.item-preview-content a.comment-link:hover {
|
|||
|
|
color: #3b82f6;
|
|||
|
|
text-decoration: underline;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.comment-preview-meta {
|
|||
|
|
display: flex;
|
|||
|
|
justify-content: space-between;
|
|||
|
|
align-items: center;
|
|||
|
|
margin-top: 5px;
|
|||
|
|
color: #999;
|
|||
|
|
font-size: 12px;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.comment-preview-author, .comment-preview-date {
|
|||
|
|
color: #999;
|
|||
|
|
font-size: 12px;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.comment-preview-author a.author-link {
|
|||
|
|
color: #999;
|
|||
|
|
text-decoration: none;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.comment-preview-author a.author-link:hover {
|
|||
|
|
color: #3b82f6;
|
|||
|
|
text-decoration: underline;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.item-preview-date, .item-preview-activity {
|
|||
|
|
color: #999;
|
|||
|
|
font-size: 12px;
|
|||
|
|
margin-top: 3px;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.collection-toggle-container {
|
|||
|
|
text-align: center;
|
|||
|
|
margin: 15px 0;
|
|||
|
|
padding-top: 15px;
|
|||
|
|
border-top: 1px solid #e8e8e8;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.collection-toggle-btn {
|
|||
|
|
background: none;
|
|||
|
|
border: 1px solid #ddd;
|
|||
|
|
border-radius: 4px;
|
|||
|
|
padding: 8px 20px;
|
|||
|
|
color: #666;
|
|||
|
|
cursor: pointer;
|
|||
|
|
font-size: 14px;
|
|||
|
|
display: inline-flex;
|
|||
|
|
align-items: center;
|
|||
|
|
gap: 8px;
|
|||
|
|
transition: all 0.3s;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.collection-toggle-btn:hover {
|
|||
|
|
background: #f5f5f5;
|
|||
|
|
border-color: #ccc;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.collection-toggle-btn.expanded .toggle-icon {
|
|||
|
|
transform: rotate(180deg);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.toggle-icon {
|
|||
|
|
transition: transform 0.3s;
|
|||
|
|
font-size: 12px;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.collection-items-full {
|
|||
|
|
margin-top: 20px;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.items-list {
|
|||
|
|
display: flex;
|
|||
|
|
flex-direction: column;
|
|||
|
|
gap: 15px;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.item-list-item {
|
|||
|
|
padding: 15px;
|
|||
|
|
border: 1px solid #e8e8e8;
|
|||
|
|
border-radius: 6px;
|
|||
|
|
background: #f9f9f9;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.item-list-info {
|
|||
|
|
display: flex;
|
|||
|
|
justify-content: space-between;
|
|||
|
|
align-items: center;
|
|||
|
|
margin-bottom: 8px;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.item-list-title a {
|
|||
|
|
color: #333;
|
|||
|
|
text-decoration: none;
|
|||
|
|
font-size: 16px;
|
|||
|
|
font-weight: 500;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.item-list-title a:hover {
|
|||
|
|
color: #3b82f6;
|
|||
|
|
text-decoration: underline;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.item-list-name, .item-list-author {
|
|||
|
|
color: #333;
|
|||
|
|
font-size: 16px;
|
|||
|
|
font-weight: 500;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.item-list-name a.user-link,
|
|||
|
|
.item-list-author a.author-link {
|
|||
|
|
color: #333;
|
|||
|
|
text-decoration: none;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.item-list-name a.user-link:hover,
|
|||
|
|
.item-list-author a.author-link:hover {
|
|||
|
|
color: #3b82f6;
|
|||
|
|
text-decoration: underline;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.item-list-date {
|
|||
|
|
color: #999;
|
|||
|
|
font-size: 13px;
|
|||
|
|
flex-shrink: 0;
|
|||
|
|
margin-left: 15px;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.item-list-excerpt {
|
|||
|
|
color: #666;
|
|||
|
|
font-size: 14px;
|
|||
|
|
line-height: 1.5;
|
|||
|
|
margin-top: 8px;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.item-list-content {
|
|||
|
|
color: #666;
|
|||
|
|
font-size: 14px;
|
|||
|
|
line-height: 1.5;
|
|||
|
|
margin-bottom: 8px;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.item-list-content a.comment-link {
|
|||
|
|
color: #666;
|
|||
|
|
text-decoration: none;
|
|||
|
|
display: block;
|
|||
|
|
padding: 8px 0;
|
|||
|
|
border-left: none;
|
|||
|
|
padding-left: 0;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.item-list-content a.comment-link:hover {
|
|||
|
|
color: #3b82f6;
|
|||
|
|
text-decoration: underline;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.item-list-activity {
|
|||
|
|
margin-top: 5px;
|
|||
|
|
color: #666;
|
|||
|
|
font-size: 13px;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.collection-no-items {
|
|||
|
|
text-align: center;
|
|||
|
|
padding: 30px;
|
|||
|
|
color: #999;
|
|||
|
|
font-size: 16px;
|
|||
|
|
background: #f9f9f9;
|
|||
|
|
border-radius: 6px;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.collection-error {
|
|||
|
|
padding: 20px;
|
|||
|
|
background: #f8d7da;
|
|||
|
|
color: #721c24;
|
|||
|
|
border: 1px solid #f5c6cb;
|
|||
|
|
border-radius: 4px;
|
|||
|
|
text-align: center;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/* 网格布局 */
|
|||
|
|
.collection-card[data-display-mode="grid"] .items-list {
|
|||
|
|
display: grid;
|
|||
|
|
grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
|
|||
|
|
gap: 15px;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.collection-card[data-display-mode="grid"] .item-list-item {
|
|||
|
|
margin: 0;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/* 深色模式适配 */
|
|||
|
|
.dark .collection-card {
|
|||
|
|
background: #1a1a1a;
|
|||
|
|
border: 1px solid rgba(255, 255, 255, 0.1);
|
|||
|
|
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.3);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.dark .collection-header {
|
|||
|
|
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.dark .collection-title {
|
|||
|
|
color: #fff;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.dark .collection-count {
|
|||
|
|
background: #2a2a2a;
|
|||
|
|
color: #ccc;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.dark .collection-description {
|
|||
|
|
color: #ccc;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.dark .item-title a,
|
|||
|
|
.dark .item-preview-title a,
|
|||
|
|
.dark .item-list-title a {
|
|||
|
|
color: #999;
|
|||
|
|
text-decoration: none;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.dark .item-title a:hover,
|
|||
|
|
.dark .item-preview-title a:hover,
|
|||
|
|
.dark .item-list-title a:hover {
|
|||
|
|
color: #3b82f6;
|
|||
|
|
text-decoration: underline;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.dark .item-name,
|
|||
|
|
.dark .item-author,
|
|||
|
|
.dark .item-list-name,
|
|||
|
|
.dark .item-list-author,
|
|||
|
|
.dark .item-preview-name,
|
|||
|
|
.dark .item-preview-author {
|
|||
|
|
color: #ddd;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.dark .item-name a.user-link,
|
|||
|
|
.dark .item-author a.author-link,
|
|||
|
|
.dark .item-list-name a.user-link,
|
|||
|
|
.dark .item-list-author a.author-link,
|
|||
|
|
.dark .item-preview-name a.user-link,
|
|||
|
|
.dark .item-preview-author a.author-link {
|
|||
|
|
color: #ddd;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.dark .item-name a.user-link:hover,
|
|||
|
|
.dark .item-author a.author-link:hover,
|
|||
|
|
.dark .item-list-name a.user-link:hover,
|
|||
|
|
.dark .item-list-author a.author-link:hover,
|
|||
|
|
.dark .item-preview-name a.user-link:hover,
|
|||
|
|
.dark .item-preview-author a.author-link:hover {
|
|||
|
|
color: #3b82f6;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.dark .comment-meta,
|
|||
|
|
.dark .comment-author,
|
|||
|
|
.dark .comment-date,
|
|||
|
|
.dark .comment-preview-meta,
|
|||
|
|
.dark .comment-preview-author,
|
|||
|
|
.dark .comment-preview-date {
|
|||
|
|
color: #999;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.dark .comment-author a.author-link,
|
|||
|
|
.dark .comment-preview-author a.author-link {
|
|||
|
|
color: #999;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.dark .comment-author a.author-link:hover,
|
|||
|
|
.dark .comment-preview-author a.author-link:hover {
|
|||
|
|
color: #3b82f6;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.dark .item-excerpt,
|
|||
|
|
.dark .item-list-excerpt,
|
|||
|
|
.dark .item-list-content,
|
|||
|
|
.dark .item-preview-content {
|
|||
|
|
color: #ccc;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.dark .item-content-link a.comment-link,
|
|||
|
|
.dark .item-list-content a.comment-link,
|
|||
|
|
.dark .item-preview-content a.comment-link {
|
|||
|
|
color: #ccc;
|
|||
|
|
border-left: none;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.dark .item-content-link a.comment-link:hover,
|
|||
|
|
.dark .item-list-content a.comment-link:hover,
|
|||
|
|
.dark .item-preview-content a.comment-link:hover {
|
|||
|
|
color: #3b82f6;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.dark .collection-toggle-container,
|
|||
|
|
.dark .item-preview-item {
|
|||
|
|
border-top-color: rgba(255, 255, 255, 0.1);
|
|||
|
|
border-bottom-color: rgba(255, 255, 255, 0.1);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.dark .item-list-item {
|
|||
|
|
background: #2a2a2a;
|
|||
|
|
border-color: rgba(255, 255, 255, 0.1);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.dark .item-list-date,
|
|||
|
|
.dark .item-preview-date,
|
|||
|
|
.dark .item-date,
|
|||
|
|
.dark .item-activity {
|
|||
|
|
color: #999;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.dark .collection-toggle-btn {
|
|||
|
|
border-color: rgba(255, 255, 255, 0.2);
|
|||
|
|
color: #ccc;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.dark .collection-toggle-btn:hover {
|
|||
|
|
background: rgba(255, 255, 255, 0.1);
|
|||
|
|
border-color: rgba(255, 255, 255, 0.3);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.dark .collection-no-items {
|
|||
|
|
background: #2a2a2a;
|
|||
|
|
color: #ccc;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/* 响应式设计 */
|
|||
|
|
@media (max-width: 768px) {
|
|||
|
|
.collection-single-item {
|
|||
|
|
flex-direction: column;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.collection-title-row {
|
|||
|
|
flex-direction: column;
|
|||
|
|
align-items: flex-start;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.collection-count {
|
|||
|
|
margin-left: 0;
|
|||
|
|
margin-top: 5px;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.collection-card[data-display-mode="grid"] .items-list {
|
|||
|
|
grid-template-columns: 1fr;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.item-list-info {
|
|||
|
|
flex-direction: column;
|
|||
|
|
align-items: flex-start;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.item-list-date {
|
|||
|
|
margin-left: 0;
|
|||
|
|
margin-top: 5px;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.comment-meta,
|
|||
|
|
.comment-preview-meta {
|
|||
|
|
flex-direction: column;
|
|||
|
|
align-items: flex-start;
|
|||
|
|
gap: 3px;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
@media (max-width: 480px) {
|
|||
|
|
.collection-card {
|
|||
|
|
padding: 15px;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
';
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 获取合集JS脚本
|
|||
|
|
*/
|
|||
|
|
private static function getCollectionScripts($collectionConfigs)
|
|||
|
|
{
|
|||
|
|
$configsJson = json_encode($collectionConfigs);
|
|||
|
|
|
|||
|
|
return '
|
|||
|
|
<script>
|
|||
|
|
(function() {
|
|||
|
|
function initCollection() {
|
|||
|
|
// 处理可折叠合集的展开/收起功能
|
|||
|
|
document.querySelectorAll(".collection-toggle-btn").forEach(function(button) {
|
|||
|
|
button.addEventListener("click", function() {
|
|||
|
|
var targetId = this.getAttribute("data-target");
|
|||
|
|
var targetCard = document.getElementById(targetId);
|
|||
|
|
var fullContent = targetCard.querySelector(".collection-items-full");
|
|||
|
|
var toggleText = this.querySelector(".toggle-text");
|
|||
|
|
var toggleIcon = this.querySelector(".toggle-icon");
|
|||
|
|
|
|||
|
|
if (fullContent.style.display === "none") {
|
|||
|
|
fullContent.style.display = "block";
|
|||
|
|
toggleText.textContent = "收起剩余内容";
|
|||
|
|
this.classList.add("expanded");
|
|||
|
|
toggleIcon.style.transform = "rotate(180deg)";
|
|||
|
|
} else {
|
|||
|
|
fullContent.style.display = "none";
|
|||
|
|
// 更新按钮文本显示剩余内容数量
|
|||
|
|
var remainingCount = fullContent.querySelectorAll(".item-list-item").length;
|
|||
|
|
var collectionType = targetCard.getAttribute("data-collection-type");
|
|||
|
|
var typeText = collectionType === "comment" ? "评论" :
|
|||
|
|
collectionType === "user" ? "用户" : "文章";
|
|||
|
|
toggleText.textContent = "展开查看更多" + typeText + "(" + remainingCount + "个)";
|
|||
|
|
button.classList.remove("expanded");
|
|||
|
|
toggleIcon.style.transform = "";
|
|||
|
|
}
|
|||
|
|
});
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
// 网格布局的特殊处理
|
|||
|
|
document.querySelectorAll(".collection-card[data-display-mode=\'grid\']").forEach(function(card) {
|
|||
|
|
var items = card.querySelectorAll(".item-list-item");
|
|||
|
|
if (items.length > 0) {
|
|||
|
|
items.forEach(function(item) {
|
|||
|
|
item.addEventListener("mouseenter", function() {
|
|||
|
|
this.style.transform = "translateY(-2px)";
|
|||
|
|
this.style.boxShadow = "0 4px 12px rgba(0,0,0,0.15)";
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
item.addEventListener("mouseleave", function() {
|
|||
|
|
this.style.transform = "";
|
|||
|
|
this.style.boxShadow = "";
|
|||
|
|
});
|
|||
|
|
});
|
|||
|
|
}
|
|||
|
|
});
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
if (document.readyState === "loading") {
|
|||
|
|
document.addEventListener("DOMContentLoaded", initCollection);
|
|||
|
|
} else {
|
|||
|
|
setTimeout(initCollection, 100);
|
|||
|
|
}
|
|||
|
|
})();
|
|||
|
|
</script>';
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 同步关联内容
|
|||
|
|
*/
|
|||
|
|
private static function syncRelatedItems($collectionId, $itemId, $itemType = 'article')
|
|||
|
|
{
|
|||
|
|
try {
|
|||
|
|
$db = self::getDbConnection();
|
|||
|
|
|
|||
|
|
$stmt = $db->prepare("SELECT related_items, collection_type FROM plugin_collection WHERE id = ?");
|
|||
|
|
$stmt->execute(array($collectionId));
|
|||
|
|
$result = $stmt->fetch(PDO::FETCH_ASSOC);
|
|||
|
|
|
|||
|
|
if ($result) {
|
|||
|
|
// 检查合集类型是否匹配
|
|||
|
|
if ($result['collection_type'] != $itemType) {
|
|||
|
|
return false;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
$currentRelatedItems = $result['related_items'];
|
|||
|
|
$relatedItemsArray = array();
|
|||
|
|
|
|||
|
|
if (!empty($currentRelatedItems)) {
|
|||
|
|
$relatedItemsArray = explode(',', $currentRelatedItems);
|
|||
|
|
$relatedItemsArray = array_map('trim', $relatedItemsArray);
|
|||
|
|
$relatedItemsArray = array_filter($relatedItemsArray);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
if (!in_array($itemId, $relatedItemsArray)) {
|
|||
|
|
$relatedItemsArray[] = $itemId;
|
|||
|
|
$newRelatedItems = implode(',', $relatedItemsArray);
|
|||
|
|
|
|||
|
|
$updateStmt = $db->prepare("UPDATE plugin_collection SET related_items = ? WHERE id = ?");
|
|||
|
|
$updateStmt->execute(array($newRelatedItems, $collectionId));
|
|||
|
|
|
|||
|
|
return true;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
} catch (Exception $e) {
|
|||
|
|
error_log('Collection: 同步关联内容失败: ' . $e->getMessage());
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
return false;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 文章保存时的处理
|
|||
|
|
*/
|
|||
|
|
public static function onPostSave($content)
|
|||
|
|
{
|
|||
|
|
$cid = isset($content['cid']) ? $content['cid'] : 0;
|
|||
|
|
|
|||
|
|
if ($cid && isset($content['text'])) {
|
|||
|
|
$pattern = '/\{collection-(\d+)\}/i';
|
|||
|
|
if (preg_match_all($pattern, $content['text'], $matches)) {
|
|||
|
|
foreach ($matches[1] as $collectionId) {
|
|||
|
|
$collection = self::getCollectionById($collectionId);
|
|||
|
|
if ($collection) {
|
|||
|
|
self::syncRelatedItems($collectionId, $cid, 'article');
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
return $content;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 文章写入时的处理
|
|||
|
|
*/
|
|||
|
|
public static function onPostWrite($content)
|
|||
|
|
{
|
|||
|
|
return self::onPostSave($content);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 获取关联文章信息
|
|||
|
|
*/
|
|||
|
|
public static function getRelatedArticlesInfo($relatedArticles)
|
|||
|
|
{
|
|||
|
|
$result = array();
|
|||
|
|
|
|||
|
|
if (empty($relatedArticles)) {
|
|||
|
|
return $result;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
$articleIds = explode(',', $relatedArticles);
|
|||
|
|
$articleIds = array_map('trim', $articleIds);
|
|||
|
|
$articleIds = array_filter($articleIds);
|
|||
|
|
|
|||
|
|
foreach ($articleIds as $cid) {
|
|||
|
|
if (is_numeric($cid)) {
|
|||
|
|
$articleInfo = self::getArticleInfo($cid);
|
|||
|
|
if ($articleInfo['title']) {
|
|||
|
|
$result[$cid] = array(
|
|||
|
|
'title' => $articleInfo['title'],
|
|||
|
|
'link' => $articleInfo['link'],
|
|||
|
|
'cid' => $cid
|
|||
|
|
);
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
return $result;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 获取关联评论信息
|
|||
|
|
*/
|
|||
|
|
public static function getRelatedCommentsInfo($relatedComments)
|
|||
|
|
{
|
|||
|
|
$result = array();
|
|||
|
|
|
|||
|
|
if (empty($relatedComments)) {
|
|||
|
|
return $result;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
$commentIds = explode(',', $relatedComments);
|
|||
|
|
$commentIds = array_map('trim', $commentIds);
|
|||
|
|
$commentIds = array_filter($commentIds);
|
|||
|
|
|
|||
|
|
foreach ($commentIds as $coid) {
|
|||
|
|
if (is_numeric($coid)) {
|
|||
|
|
$commentInfo = self::getCommentInfo($coid);
|
|||
|
|
if ($commentInfo['author']) {
|
|||
|
|
$result[$coid] = array(
|
|||
|
|
'author' => $commentInfo['author'],
|
|||
|
|
'content' => $commentInfo['text'],
|
|||
|
|
'coid' => $coid,
|
|||
|
|
'link' => $commentInfo['link'],
|
|||
|
|
'url' => $commentInfo['url']
|
|||
|
|
);
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
return $result;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 获取关联用户信息
|
|||
|
|
*/
|
|||
|
|
public static function getRelatedUsersInfo($relatedUsers)
|
|||
|
|
{
|
|||
|
|
$result = array();
|
|||
|
|
|
|||
|
|
if (empty($relatedUsers)) {
|
|||
|
|
return $result;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
$userIds = explode(',', $relatedUsers);
|
|||
|
|
$userIds = array_map('trim', $userIds);
|
|||
|
|
$userIds = array_filter($userIds);
|
|||
|
|
|
|||
|
|
foreach ($userIds as $uid) {
|
|||
|
|
if (is_numeric($uid)) {
|
|||
|
|
$userInfo = self::getUserInfo($uid);
|
|||
|
|
if ($userInfo['name']) {
|
|||
|
|
$result[$uid] = array(
|
|||
|
|
'name' => $userInfo['name'],
|
|||
|
|
'url' => $userInfo['url'],
|
|||
|
|
'uid' => $uid,
|
|||
|
|
'commentCount' => $userInfo['commentCount'],
|
|||
|
|
'recentActivity' => $userInfo['recentActivity']
|
|||
|
|
);
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
return $result;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
?>
|