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],
'
' . htmlspecialchars($item['excerpt']) . '
'; } if ($showDate && isset($item['created']) && $item['created']) { $date = date('Y-m-d', $item['created']); $html .= '该合集暂无关联' . ($collectionType === 'comment' ? '评论' : ($collectionType === 'user' ? '用户' : '文章')) . '