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 = '
' . htmlspecialchars($collection['name']) . '
' . $countText . '
'; if ($collection['description']) { $html .= '
' . nl2br(htmlspecialchars($collection['description'])) . '
'; } $html .= '
'; if ($itemsCount === 1) { $item = reset($items); if ($collectionType === 'comment') { $authorLink = !empty($item['url']) ? '' . htmlspecialchars($item['author']) . '' : htmlspecialchars($item['author']); $html .= '
' . $authorLink . '
'; if ($showDate && isset($item['created']) && $item['created']) { $date = date('Y-m-d', $item['created']); $html .= '
评论于 ' . $date . '
'; } $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 .= '
' . $authorLink . '
'; if ($showDate && isset($item['created']) && $item['created']) { $date = date('Y-m-d', $item['created']); $html .= '
' . $date . '
'; } $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 .= ' '; } } 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; } } ?>