commit d56e3d3b3ae6fcdf3ad1e986299df061afc5516b
Author: XIGE <710062962@qq.com>
Date: Mon Feb 23 17:19:04 2026 +0800
1.0
diff --git a/Action.php b/Action.php
new file mode 100644
index 0000000..1a140df
--- /dev/null
+++ b/Action.php
@@ -0,0 +1,505 @@
+db = Collection_Plugin::getDbConnection();
+ }
+
+ /**
+ * 动作入口
+ */
+ public function action()
+ {
+ // 检查管理员权限
+ $user = Typecho_Widget::widget('Widget_User');
+ if (!$user->hasLogin() || !$user->pass('administrator', true)) {
+ $this->response->throwJson(array(
+ 'success' => false,
+ 'message' => '无权限访问'
+ ));
+ return;
+ }
+
+ // 确保返回JSON格式
+ $this->response->setContentType('application/json');
+
+ $do = $this->request->get('do');
+ if (!$do) {
+ $do = $this->request->isPost() ? $this->request->get('do') : null;
+ }
+
+ switch ($do) {
+ case 'add':
+ $this->add();
+ break;
+ case 'update':
+ $this->update();
+ break;
+ case 'delete':
+ $this->delete();
+ break;
+ case 'get':
+ $this->get();
+ break;
+ case 'getAll':
+ $this->getAll();
+ break;
+ case 'getArticleInfo':
+ $this->getArticleInfo();
+ break;
+ case 'getCommentInfo':
+ $this->getCommentInfo();
+ break;
+ case 'getUserInfo':
+ $this->getUserInfo();
+ break;
+ case 'getRelatedArticlesInfo':
+ $this->getRelatedArticlesInfo();
+ break;
+ case 'getRelatedCommentsInfo':
+ $this->getRelatedCommentsInfo();
+ break;
+ case 'getRelatedUsersInfo':
+ $this->getRelatedUsersInfo();
+ break;
+ case 'batchDelete':
+ $this->batchDelete();
+ break;
+ case 'test':
+ $this->test();
+ break;
+ default:
+ $this->response->throwJson(array(
+ 'success' => false,
+ 'message' => '无效的操作'
+ ));
+ }
+ }
+
+ /**
+ * 测试方法
+ */
+ public function test()
+ {
+ $this->response->throwJson(array(
+ 'success' => true,
+ 'message' => 'Collection插件Action工作正常',
+ 'timestamp' => time()
+ ));
+ }
+
+ /**
+ * 添加合集
+ */
+ public function add()
+ {
+ try {
+ $name = $this->request->get('name');
+ $description = $this->request->get('description');
+ $related_items = $this->request->get('related_items');
+ $sort_order = $this->request->get('sort_order', 'created_desc');
+ $collection_type = $this->request->get('collection_type', 'article');
+
+ if (empty($name)) {
+ throw new Exception('合集名称不能为空');
+ }
+
+ // 检查名称是否已存在
+ $stmt = $this->db->prepare("SELECT COUNT(*) FROM plugin_collection WHERE name = ? AND collection_type = ? AND is_active = 1");
+ $stmt->execute(array($name, $collection_type));
+ $count = $stmt->fetchColumn();
+
+ if ($count > 0) {
+ throw new Exception('合集名称已存在');
+ }
+
+ $stmt = $this->db->prepare("INSERT INTO plugin_collection (name, description, related_items, sort_order, collection_type) VALUES (?, ?, ?, ?, ?)");
+ $stmt->execute(array($name, $description, $related_items, $sort_order, $collection_type));
+
+ $id = $this->db->lastInsertId();
+
+ $this->response->throwJson(array(
+ 'success' => true,
+ 'message' => '合集添加成功',
+ 'data' => array('id' => $id)
+ ));
+ } catch (Exception $e) {
+ $this->response->throwJson(array(
+ 'success' => false,
+ 'message' => '添加失败: ' . $e->getMessage()
+ ));
+ }
+ }
+
+ /**
+ * 更新合集
+ */
+ public function update()
+ {
+ try {
+ $id = $this->request->get('id');
+ $name = $this->request->get('name');
+ $description = $this->request->get('description');
+ $related_items = $this->request->get('related_items');
+ $sort_order = $this->request->get('sort_order', 'created_desc');
+ $collection_type = $this->request->get('collection_type', 'article');
+
+ if (empty($id) || empty($name)) {
+ throw new Exception('ID和合集名称不能为空');
+ }
+
+ // 检查名称是否已存在(排除自身)
+ $stmt = $this->db->prepare("SELECT COUNT(*) FROM plugin_collection WHERE name = ? AND collection_type = ? AND id != ? AND is_active = 1");
+ $stmt->execute(array($name, $collection_type, $id));
+ $count = $stmt->fetchColumn();
+
+ if ($count > 0) {
+ throw new Exception('合集名称已存在');
+ }
+
+ $stmt = $this->db->prepare("UPDATE plugin_collection SET name = ?, description = ?, related_items = ?, sort_order = ?, collection_type = ? WHERE id = ?");
+ $result = $stmt->execute(array($name, $description, $related_items, $sort_order, $collection_type, $id));
+
+ if ($result) {
+ $this->response->throwJson(array(
+ 'success' => true,
+ 'message' => '合集更新成功'
+ ));
+ } else {
+ throw new Exception('合集不存在或更新失败');
+ }
+ } catch (Exception $e) {
+ $this->response->throwJson(array(
+ 'success' => false,
+ 'message' => '更新失败: ' . $e->getMessage()
+ ));
+ }
+ }
+
+ /**
+ * 删除合集
+ */
+ public function delete()
+ {
+ try {
+ $id = $this->request->get('id');
+
+ if (empty($id)) {
+ throw new Exception('ID不能为空');
+ }
+
+ $stmt = $this->db->prepare("UPDATE plugin_collection SET is_active = 0 WHERE id = ?");
+ $result = $stmt->execute(array($id));
+
+ if ($result) {
+ $this->response->throwJson(array(
+ 'success' => true,
+ 'message' => '合集删除成功'
+ ));
+ } else {
+ throw new Exception('合集不存在或删除失败');
+ }
+ } catch (Exception $e) {
+ $this->response->throwJson(array(
+ 'success' => false,
+ 'message' => '删除失败: ' . $e->getMessage()
+ ));
+ }
+ }
+
+ /**
+ * 批量删除合集
+ */
+ public function batchDelete()
+ {
+ try {
+ $ids = $this->request->filter('int')->getArray('collection');
+
+ if (empty($ids)) {
+ $rawIds = $this->request->get('collection');
+ if (!empty($rawIds)) {
+ $ids = is_array($rawIds) ? $rawIds : array($rawIds);
+ $ids = array_map('intval', $ids);
+ }
+ }
+
+ if (empty($ids) || !is_array($ids)) {
+ throw new Exception('请选择要删除的合集');
+ }
+
+ $ids = array_filter($ids, function($id) {
+ return is_numeric($id) && $id > 0;
+ });
+
+ if (empty($ids)) {
+ throw new Exception('无效的合集ID');
+ }
+
+ $placeholders = implode(',', array_fill(0, count($ids), '?'));
+ $stmt = $this->db->prepare("UPDATE plugin_collection SET is_active = 0 WHERE id IN ($placeholders)");
+ $result = $stmt->execute($ids);
+
+ if ($result) {
+ $this->response->throwJson(array(
+ 'success' => true,
+ 'message' => '合集批量删除成功'
+ ));
+ } else {
+ throw new Exception('删除失败');
+ }
+ } catch (Exception $e) {
+ $this->response->throwJson(array(
+ 'success' => false,
+ 'message' => '批量删除失败: ' . $e->getMessage()
+ ));
+ }
+ }
+
+ /**
+ * 获取单个合集
+ */
+ public function get()
+ {
+ try {
+ $id = $this->request->get('id');
+
+ if (empty($id)) {
+ throw new Exception('ID不能为空');
+ }
+
+ $stmt = $this->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'])) {
+ if ($collection['collection_type'] == 'comment') {
+ $collection['related_items_info'] = Collection_Plugin::getRelatedCommentsInfo($collection['related_items']);
+ } elseif ($collection['collection_type'] == 'user') {
+ $collection['related_items_info'] = Collection_Plugin::getRelatedUsersInfo($collection['related_items']);
+ } else {
+ $collection['related_items_info'] = Collection_Plugin::getRelatedArticlesInfo($collection['related_items']);
+ }
+ }
+
+ $this->response->throwJson(array(
+ 'success' => true,
+ 'data' => $collection
+ ));
+ } else {
+ throw new Exception('合集不存在');
+ }
+ } catch (Exception $e) {
+ $this->response->throwJson(array(
+ 'success' => false,
+ 'message' => '查询失败: ' . $e->getMessage()
+ ));
+ }
+ }
+
+ /**
+ * 获取所有合集
+ */
+ public function getAll()
+ {
+ try {
+ $stmt = $this->db->query("SELECT * FROM plugin_collection WHERE is_active = 1 ORDER BY collection_type, created_at DESC");
+ $collections = $stmt->fetchAll(PDO::FETCH_ASSOC);
+
+ $this->response->throwJson(array(
+ 'success' => true,
+ 'data' => $collections
+ ));
+ } catch (Exception $e) {
+ $this->response->throwJson(array(
+ 'success' => false,
+ 'message' => '查询失败: ' . $e->getMessage()
+ ));
+ }
+ }
+
+ /**
+ * 获取文章信息
+ */
+ public function getArticleInfo()
+ {
+ try {
+ $article_cid = $this->request->get('article_cid');
+
+ if (empty($article_cid)) {
+ throw new Exception('文章CID不能为空');
+ }
+
+ $articleInfo = Collection_Plugin::getArticleInfo($article_cid);
+
+ if (!empty($articleInfo['title'])) {
+ $this->response->throwJson(array(
+ 'success' => true,
+ 'data' => $articleInfo
+ ));
+ } else {
+ throw new Exception('文章不存在');
+ }
+ } catch (Exception $e) {
+ $this->response->throwJson(array(
+ 'success' => false,
+ 'message' => '获取文章信息失败: ' . $e->getMessage()
+ ));
+ }
+ }
+
+ /**
+ * 获取评论信息
+ */
+ public function getCommentInfo()
+ {
+ try {
+ $comment_coid = $this->request->get('comment_coid');
+
+ if (empty($comment_coid)) {
+ throw new Exception('评论COID不能为空');
+ }
+
+ $commentInfo = Collection_Plugin::getCommentInfo($comment_coid);
+
+ if (!empty($commentInfo['author'])) {
+ $this->response->throwJson(array(
+ 'success' => true,
+ 'data' => $commentInfo
+ ));
+ } else {
+ throw new Exception('评论不存在');
+ }
+ } catch (Exception $e) {
+ $this->response->throwJson(array(
+ 'success' => false,
+ 'message' => '获取评论信息失败: ' . $e->getMessage()
+ ));
+ }
+ }
+
+ /**
+ * 获取用户信息
+ */
+ public function getUserInfo()
+ {
+ try {
+ $user_uid = $this->request->get('user_uid');
+
+ if (empty($user_uid)) {
+ throw new Exception('用户UID不能为空');
+ }
+
+ $userInfo = Collection_Plugin::getUserInfo($user_uid);
+
+ if (!empty($userInfo['name'])) {
+ $this->response->throwJson(array(
+ 'success' => true,
+ 'data' => $userInfo
+ ));
+ } else {
+ throw new Exception('用户不存在');
+ }
+ } catch (Exception $e) {
+ $this->response->throwJson(array(
+ 'success' => false,
+ 'message' => '获取用户信息失败: ' . $e->getMessage()
+ ));
+ }
+ }
+
+ /**
+ * 获取关联文章信息
+ */
+ public function getRelatedArticlesInfo()
+ {
+ try {
+ $related_articles = $this->request->get('related_articles');
+
+ if (empty($related_articles)) {
+ throw new Exception('关联文章不能为空');
+ }
+
+ $result = Collection_Plugin::getRelatedArticlesInfo($related_articles);
+
+ $this->response->throwJson(array(
+ 'success' => true,
+ 'data' => $result
+ ));
+ } catch (Exception $e) {
+ $this->response->throwJson(array(
+ 'success' => false,
+ 'message' => '获取关联文章信息失败: ' . $e->getMessage()
+ ));
+ }
+ }
+
+ /**
+ * 获取关联评论信息
+ */
+ public function getRelatedCommentsInfo()
+ {
+ try {
+ $related_comments = $this->request->get('related_comments');
+
+ if (empty($related_comments)) {
+ throw new Exception('关联评论不能为空');
+ }
+
+ $result = Collection_Plugin::getRelatedCommentsInfo($related_comments);
+
+ $this->response->throwJson(array(
+ 'success' => true,
+ 'data' => $result
+ ));
+ } catch (Exception $e) {
+ $this->response->throwJson(array(
+ 'success' => false,
+ 'message' => '获取关联评论信息失败: ' . $e->getMessage()
+ ));
+ }
+ }
+
+ /**
+ * 获取关联用户信息
+ */
+ public function getRelatedUsersInfo()
+ {
+ try {
+ $related_users = $this->request->get('related_users');
+
+ if (empty($related_users)) {
+ throw new Exception('关联用户不能为空');
+ }
+
+ $result = Collection_Plugin::getRelatedUsersInfo($related_users);
+
+ $this->response->throwJson(array(
+ 'success' => true,
+ 'data' => $result
+ ));
+ } catch (Exception $e) {
+ $this->response->throwJson(array(
+ 'success' => false,
+ 'message' => '获取关联用户信息失败: ' . $e->getMessage()
+ ));
+ }
+ }
+}
+?>
\ No newline at end of file
diff --git a/Manage.php b/Manage.php
new file mode 100644
index 0000000..3d3e00d
--- /dev/null
+++ b/Manage.php
@@ -0,0 +1,2171 @@
+plugin('Collection');
+
+// 检查是否有搜索参数
+$searchKeyword = isset($_GET['search']) ? trim($_GET['search']) : '';
+$searchResults = array();
+$searchPerformed = false;
+$currentPage = isset($_GET['page']) ? intval($_GET['page']) : 1;
+
+// 尝试获取合集数据
+try {
+ $collections = Collection_Plugin::getAllCollections();
+} catch (Exception $e) {
+ $collections = array();
+}
+?>
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 1): ?>
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/Plugin.php b/Plugin.php
new file mode 100644
index 0000000..8b01564
--- /dev/null
+++ b/Plugin.php
@@ -0,0 +1,2029 @@
+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('/
]+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 = '' . $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],
+ '合集ID ' . $collectionId . ' 不存在
',
+ $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 = '
+
+ ';
+
+ if ($itemsCount === 1) {
+ $item = reset($items);
+
+ if ($collectionType === 'comment') {
+ $authorLink = !empty($item['url']) ?
+ '
' . htmlspecialchars($item['author']) . '' :
+ htmlspecialchars($item['author']);
+
+ $html .= '
+
';
+ } elseif ($collectionType === 'user') {
+ $userNameLink = !empty($item['url']) ?
+ '
' . htmlspecialchars($item['name']) . '' :
+ htmlspecialchars($item['name']);
+
+ $html .= '
+
+
+
用户:' . $userNameLink . '
';
+
+ if ($showDate && isset($item['created']) && $item['created']) {
+ $date = date('Y-m-d', $item['created']);
+ $html .= '
注册于 ' . $date . '
';
+ }
+
+ if (isset($item['recentActivity']) && $item['recentActivity']) {
+ $html .= '
最近活跃:' . $item['recentActivity'] . '
';
+ }
+
+ $html .= '
+
+
';
+ } else {
+ $html .= '
+
+
+
';
+
+ if ($showExcerpt && isset($item['excerpt']) && $item['excerpt']) {
+ $html .= '
' . htmlspecialchars($item['excerpt']) . '
';
+ }
+
+ if ($showDate && isset($item['created']) && $item['created']) {
+ $date = date('Y-m-d', $item['created']);
+ $html .= '
发布于 ' . $date . '
';
+ }
+
+ $html .= '
+
+
';
+ }
+ } else if ($itemsCount > 1) {
+ $html .= '
+
';
+
+ if ($displayMode === 'collapsible') {
+ // 预览内容(最多3个)
+ $previewCount = min(3, $itemsCount);
+ $previewItems = array_slice($items, 0, $previewCount);
+ $remainingItems = array_slice($items, $previewCount);
+
+ if ($previewCount > 0) {
+ $html .= '
+
';
+
+ foreach ($previewItems as $item) {
+ $html .= '
+
';
+
+ if ($collectionType === 'comment') {
+ $authorLink = !empty($item['url']) ?
+ '
' . htmlspecialchars($item['author']) . '' :
+ htmlspecialchars($item['author']);
+
+ $html .= '
+
+
+
+ ';
+ } elseif ($collectionType === 'user') {
+ $userNameLink = !empty($item['url']) ?
+ '
' . htmlspecialchars($item['name']) . '' :
+ htmlspecialchars($item['name']);
+
+ $html .= '
+
+ ' . $userNameLink . '
+
';
+
+ if ($showDate && isset($item['created']) && $item['created']) {
+ $date = date('Y-m-d', $item['created']);
+ $html .= '
注册于 ' . $date . '
';
+ }
+
+ if ($collectionType === 'user' && isset($item['recentActivity']) && $item['recentActivity']) {
+ $html .= '
最近活跃:' . $item['recentActivity'] . '
';
+ }
+ } else {
+ $html .= '
+
';
+
+ if ($showDate && isset($item['created']) && $item['created']) {
+ $date = date('Y-m-d', $item['created']);
+ $html .= '
发布于 ' . $date . '
';
+ }
+ }
+
+ $html .= '
';
+ }
+
+ $html .= '
+
';
+ }
+
+ // 如果有剩余内容,显示展开按钮和剩余内容列表
+ if (count($remainingItems) > 0) {
+ $html .= '
+
+
+
';
+
+ $html .= '
+
+
';
+
+ // 只显示剩余的内容,不重复显示预览中的内容
+ foreach ($remainingItems as $item) {
+ $html .= '
+
';
+
+ if ($collectionType === 'comment') {
+ $authorLink = !empty($item['url']) ?
+ '
' . htmlspecialchars($item['author']) . '' :
+ htmlspecialchars($item['author']);
+
+ $html .= '
+
+
+
+
+
+ ' . $authorLink . '
+
+
' . date('Y-m-d', $item['created']) . '
+
';
+ } elseif ($collectionType === 'user') {
+ $userNameLink = !empty($item['url']) ?
+ '
' . htmlspecialchars($item['name']) . '' :
+ htmlspecialchars($item['name']);
+
+ $html .= '
+
+
+ ' . $userNameLink . '
+
+
' . date('Y-m-d', $item['created']) . '
+
';
+
+ if (isset($item['recentActivity']) && $item['recentActivity']) {
+ $html .= '
最近活跃:' . $item['recentActivity'] . '
';
+ }
+ } else {
+ $html .= '
+
+
+
' . date('Y-m-d', $item['created']) . '
+
';
+
+ if ($showExcerpt && isset($item['excerpt']) && $item['excerpt']) {
+ $html .= '
' . htmlspecialchars($item['excerpt']) . '
';
+ }
+ }
+
+ $html .= '
';
+ }
+
+ $html .= '
+
+
';
+ }
+ } else {
+ // 始终展开模式
+ $html .= '
+
';
+
+ foreach ($items as $item) {
+ $html .= '
+
';
+
+ if ($collectionType === 'comment') {
+ $authorLink = !empty($item['url']) ?
+ '
' . htmlspecialchars($item['author']) . '' :
+ htmlspecialchars($item['author']);
+
+ $html .= '
+
+
+
+
+
+ ' . $authorLink . '
+
+
' . date('Y-m-d', $item['created']) . '
+
';
+ } elseif ($collectionType === 'user') {
+ $userNameLink = !empty($item['url']) ?
+ '
' . htmlspecialchars($item['name']) . '' :
+ htmlspecialchars($item['name']);
+
+ $html .= '
+
+
+ ' . $userNameLink . '
+
+
' . date('Y-m-d', $item['created']) . '
+
';
+
+ if (isset($item['recentActivity']) && $item['recentActivity']) {
+ $html .= '
最近活跃:' . $item['recentActivity'] . '
';
+ }
+ } else {
+ $html .= '
+
+
+
' . date('Y-m-d', $item['created']) . '
+
';
+
+ if ($showExcerpt && isset($item['excerpt']) && $item['excerpt']) {
+ $html .= '
' . htmlspecialchars($item['excerpt']) . '
';
+ }
+ }
+
+ $html .= '
';
+ }
+
+ $html .= '
+
';
+ }
+
+ $html .= '
+
';
+ } else {
+ $html .= '
+
+
该合集暂无关联' . ($collectionType === 'comment' ? '评论' : ($collectionType === 'user' ? '用户' : '文章')) . '
+
';
+ }
+
+ $html .= '
+
';
+
+ $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 '
+ ';
+ }
+
+ /**
+ * 同步关联内容
+ */
+ 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;
+ }
+}
+?>
\ No newline at end of file
diff --git a/db/collection_10c1eeb670.db b/db/collection_10c1eeb670.db
new file mode 100644
index 0000000..35d2727
Binary files /dev/null and b/db/collection_10c1eeb670.db differ
diff --git a/db/collection_a38a503f6f.db b/db/collection_a38a503f6f.db
new file mode 100644
index 0000000..40cf52d
Binary files /dev/null and b/db/collection_a38a503f6f.db differ
diff --git a/db/init.sql b/db/init.sql
new file mode 100644
index 0000000..0c26706
--- /dev/null
+++ b/db/init.sql
@@ -0,0 +1,21 @@
+-- ArticleCollection插件数据库初始化脚本
+
+-- 创建合集表
+CREATE TABLE IF NOT EXISTS plugin_collection (
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
+ name TEXT NOT NULL,
+ description TEXT,
+ cover TEXT,
+ related_articles TEXT,
+ sort_order TEXT DEFAULT 'created_desc',
+ is_active INTEGER DEFAULT 1,
+ created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
+ updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
+);
+
+-- 创建更新时间触发器
+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;
\ No newline at end of file