Files
Memo/Plugin.php
2026-02-23 17:31:07 +08:00

698 lines
25 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<?php
/**
* 知识备忘插件
*
* @package Memo
* @author 石头厝
* @version 1.1.0
* @link https://www.shitoucuo.com/
*/
class Memo_Plugin implements Typecho_Plugin_Interface
{
/**
* 激活插件
*/
public static function activate()
{
// 创建新的数据表,不与旧插件冲突
$db = Typecho_Db::get();
$prefix = $db->getPrefix();
$tableName = $prefix . 'memo';
// 检查表是否存在
$tables = $db->fetchAll($db->query("SHOW TABLES LIKE '{$tableName}'"));
if (empty($tables)) {
// 创建新表
$sql = "CREATE TABLE `{$tableName}` (
`id` INT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY,
`content` TEXT NOT NULL COMMENT '内容',
`category` VARCHAR(100) DEFAULT '默认' COMMENT '分类',
`event_date` DATE NULL COMMENT '事件日期(可选)',
`post_cids` VARCHAR(255) DEFAULT '' COMMENT '关联文章CID多个用逗号分隔',
`original_url` VARCHAR(500) DEFAULT '' COMMENT '原文链接',
`created` DATETIME NOT NULL COMMENT '创建时间',
`modified` DATETIME NOT NULL COMMENT '修改时间',
`status` TINYINT(1) DEFAULT 1 COMMENT '状态1正常'
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='备忘录记录表';";
try {
$db->query($sql);
} catch (Typecho_Db_Exception $e) {
throw new Typecho_Plugin_Exception('创建数据表失败: ' . $e->getMessage());
}
} else {
// 检查表结构如果缺少original_url字段则添加
$columns = $db->fetchAll($db->query("SHOW COLUMNS FROM `{$tableName}`"));
$hasOriginalUrl = false;
foreach ($columns as $column) {
if ($column['Field'] == 'original_url') {
$hasOriginalUrl = true;
break;
}
}
if (!$hasOriginalUrl) {
// 添加original_url字段
try {
$db->query("ALTER TABLE `{$tableName}` ADD COLUMN `original_url` VARCHAR(500) DEFAULT '' COMMENT '原文链接' AFTER `post_cids`");
} catch (Typecho_Db_Exception $e) {
error_log("添加original_url字段失败: " . $e->getMessage());
}
}
}
// 添加管理菜单
Helper::addPanel(3, 'Memo/manage-panel.php', '知识备忘', '备忘管理', 'administrator');
return _t('备忘录插件已激活,请到插件设置中进行配置');
}
/**
* 禁用插件
*/
public static function deactivate()
{
Helper::removePanel(3, 'Memo/manage-panel.php');
return _t('备忘录插件已禁用');
}
/**
* 插件配置面板
*/
public static function config(Typecho_Widget_Helper_Form $form)
{
// 每页显示数量
$perPage = new Typecho_Widget_Helper_Form_Element_Text(
'perPage',
NULL,
'30',
_t('后台每页显示数量'),
_t('后台管理页面每页显示的备忘录数量')
);
$perPage->input->setAttribute('class', 'mini');
$form->addInput($perPage->addRule('isInteger', _t('请输入整数')));
// 前端每页显示数量
$frontPerPage = new Typecho_Widget_Helper_Form_Element_Text(
'frontPerPage',
NULL,
'20',
_t('主题调用每页显示数量'),
_t('在主题中调用时每页显示的备忘录数量')
);
$frontPerPage->input->setAttribute('class', 'mini');
$form->addInput($frontPerPage->addRule('isInteger', _t('请输入整数')));
// 默认分类设置
$defaultCategories = new Typecho_Widget_Helper_Form_Element_Textarea(
'defaultCategories',
NULL,
"技术/经验\n公司/副业\n户外/亲子\n主题/插件\n兴趣/爱好\n工作/效率\n创作/媒体\n个人/成长\n程序/应用",
_t('默认分类'),
_t('每行一个分类,回车分隔。这些分类将在发布和编辑时显示在下拉框中。')
);
$form->addInput($defaultCategories);
// 是否显示事件时间
$showEventDate = new Typecho_Widget_Helper_Form_Element_Radio(
'showEventDate',
array(
'1' => _t('显示'),
'0' => _t('不显示')
),
'1',
_t('显示事件时间'),
_t('是否在备忘录中显示事件时间(如果设置了的话)')
);
$form->addInput($showEventDate);
// 是否显示关联文章
$showRelatedPosts = new Typecho_Widget_Helper_Form_Element_Radio(
'showRelatedPosts',
array(
'1' => _t('显示'),
'0' => _t('不显示')
),
'1',
_t('显示关联文章'),
_t('是否在备忘录下方显示关联的文章链接')
);
$form->addInput($showRelatedPosts);
// 关联文章标题
$relatedPostsTitle = new Typecho_Widget_Helper_Form_Element_Text(
'relatedPostsTitle',
NULL,
'相关文章',
_t('关联文章标题'),
_t('关联文章部分的标题文本')
);
$form->addInput($relatedPostsTitle);
}
/**
* 个人用户配置
*/
public static function personalConfig(Typecho_Widget_Helper_Form $form)
{
// 个人配置(如果需要)
}
/**
* 插件配置面板
*/
public static function configPanel($panel)
{
// 配置面板
}
/**
* 渲染方法(用于在主题模板中调用)
*/
public static function render()
{
// 获取配置
$options = Typecho_Widget::widget('Widget_Options');
$config = $options->plugin('Memo');
// 引入Action类
require_once dirname(__FILE__) . '/Action.php';
$action = new Memo_Action();
// 参数处理
$currentFile = basename($_SERVER['PHP_SELF']);
$selectedCategory = isset($_GET['cat']) ? trim($_GET['cat']) : '';
$searchKeyword = isset($_GET['search']) ? trim(urldecode($_GET['search'])) : '';
$currentPage = isset($_GET['page']) && $_GET['page'] > 0 ? intval($_GET['page']) : 1;
$perPage = isset($config->frontPerPage) ? intval($config->frontPerPage) : 20;
// 获取分类列表
$categories = $action->getAllCategories();
$categoryCounts = $action->getCategoryCounts();
// 获取备忘录数据
$memos = $action->getMemos($currentPage, $perPage, $searchKeyword, $selectedCategory);
$total = $action->getTotalCount($searchKeyword, $selectedCategory);
$totalPages = ceil($total / $perPage);
// 如果没有数据
if (empty($memos) && empty($searchKeyword) && empty($selectedCategory)) {
echo '<div class="memo-empty">暂无备忘录记录</div>';
return;
}
// 为每条记录获取关联的文章信息
foreach ($memos as &$memo) {
if (!empty($memo['post_cids'])) {
$cids = array_filter(array_map('trim', explode(',', $memo['post_cids'])));
$posts = array();
foreach ($cids as $cid) {
if (is_numeric($cid)) {
$post = $action->getPostByCid($cid);
if ($post) {
$posts[] = $post;
}
}
}
$memo['posts'] = $posts;
} else {
$memo['posts'] = array();
}
}
?>
<!-- 样式部分 -->
<style>
.memo-container {
background: #fff;
border-radius: 8px;
padding: 20px;
}
.memo-title {
text-align: center;
margin-bottom: 20px;
color: #333;
font-size: 20px;
padding-bottom: 10px;
border-bottom: 2px solid #007bff;
}
/* 搜索框 */
.memo-search {
margin-bottom: 20px;
text-align: center;
}
.memo-search input {
padding: 10px 15px;
border: 1px solid #ddd;
border-radius: 4px;
width: 300px;
max-width: 100%;
font-size: 14px;
}
.memo-search button {
padding: 10px 20px;
background: #007bff;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
font-size: 14px;
margin-left: 5px;
}
/* 分类筛选 */
.memo-categories {
margin-bottom: 20px;
text-align: center;
}
.memo-category-list {
display: flex;
flex-wrap: wrap;
gap: 8px;
justify-content: center;
list-style: none;
padding: 0;
}
.memo-category-item {
display: inline-block;
}
.memo-category-link {
display: inline-block;
padding: 6px 12px;
background: #f8f9fa;
color: #495057;
text-decoration: none;
border-radius: 20px;
border: 1px solid #dee2e6;
font-size: 13px;
transition: all 0.2s;
}
.memo-category-link:hover {
background: #e9ecef;
}
.memo-category-link.active {
background: #28a745;
color: white;
border-color: #28a745;
}
.memo-category-count {
font-size: 11px;
opacity: 0.8;
margin-left: 4px;
}
/* 备忘录列表 */
.memo-list {
list-style: none;
padding: 0;
margin: 0;
}
.memo-item {
padding: 15px;
border-bottom: 1px solid #eee;
transition: background 0.2s;
}
.memo-item:hover {
background: #f8f9fa;
}
.memo-item-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 8px;
}
.memo-category {
display: inline-block;
background: #e8f5e8;
color: #2e7d32;
padding: 3px 8px;
border-radius: 3px;
font-size: 12px;
font-weight: 500;
}
.memo-date {
color: #666;
font-size: 12px;
}
.memo-content {
color: #333;
line-height: 1.6;
font-size: 14px;
word-wrap: break-word;
}
/* URL链接样式 - 修复2只对URL本身加链接 */
.memo-url-link {
color: #0066cc;
text-decoration: none;
border-bottom: 1px dotted #0066cc;
word-break: break-all;
}
.memo-url-link:hover {
color: #0056b3;
border-bottom: 1px solid #0056b3;
}
/* 原文链接 */
.memo-original-link {
margin-top: 10px;
padding: 8px 12px;
background: #f8f9fa;
border-radius: 4px;
border-left: 3px solid #28a745;
}
.memo-original-title {
font-size: 12px;
font-weight: 600;
color: #495057;
margin-bottom: 5px;
}
.memo-original-url {
color: #0066cc;
text-decoration: none;
font-size: 13px;
word-break: break-all;
}
.memo-original-url:hover {
text-decoration: underline;
}
/* 关联文章 */
.memo-related-posts {
margin-top: 10px;
padding: 8px 12px;
background: #f8f9fa;
border-radius: 4px;
border-left: 3px solid #007bff;
}
.memo-related-title {
font-size: 12px;
font-weight: 600;
color: #495057;
margin-bottom: 5px;
}
.memo-related-list {
list-style: none;
padding: 0;
margin: 0;
}
.memo-related-item {
margin-bottom: 4px;
}
.memo-related-link {
color: #0066cc;
text-decoration: none;
font-size: 13px;
}
/* 分页 */
.memo-pagination {
margin-top: 30px;
text-align: center;
}
.memo-pagination a,
.memo-pagination span {
display: inline-block;
padding: 6px 12px;
margin: 0 2px;
background: #f8f9fa;
color: #495057;
text-decoration: none;
border-radius: 4px;
border: 1px solid #dee2e6;
font-size: 13px;
}
.memo-pagination a:hover {
background: #e9ecef;
}
.memo-pagination .current {
background: #007bff;
color: white;
border-color: #007bff;
}
/* 空状态 */
.memo-empty {
text-align: center;
padding: 40px 20px;
color: #999;
font-size: 14px;
}
/* 筛选信息 */
.memo-filter-info {
text-align: center;
margin: 10px 0 20px;
padding: 8px 12px;
background: #e7f3ff;
border-radius: 4px;
color: #0066cc;
font-size: 13px;
}
.memo-filter-info a {
color: #666;
margin-left: 10px;
text-decoration: none;
font-size: 12px;
}
@media (max-width: 768px) {
.memo-container {
padding: 15px;
}
.memo-search input {
width: 100%;
margin-bottom: 10px;
}
.memo-search button {
width: 100%;
margin-left: 0;
}
.memo-item-header {
flex-direction: column;
align-items: flex-start;
gap: 5px;
}
}
</style>
<div class="memo-container">
<h2 class="memo-title">备忘录</h2>
<!-- 搜索框 -->
<div class="memo-search">
<form method="get" action="<?php echo $currentFile; ?>">
<input type="text" name="search" placeholder="搜索备忘录内容..." value="<?php echo htmlspecialchars($searchKeyword); ?>">
<?php if ($selectedCategory): ?>
<input type="hidden" name="cat" value="<?php echo htmlspecialchars($selectedCategory); ?>">
<?php endif; ?>
<button type="submit">搜索</button>
<?php if ($searchKeyword || $selectedCategory): ?>
<a href="<?php echo $currentFile; ?>" style="margin-left: 10px; font-size: 13px; color: #666;">清除筛选</a>
<?php endif; ?>
</form>
</div>
<!-- 分类筛选 -->
<?php if (!empty($categories)): ?>
<div class="memo-categories">
<ul class="memo-category-list">
<li class="memo-category-item">
<a href="<?php echo $currentFile; ?>"
class="memo-category-link <?php echo empty($selectedCategory) ? 'active' : ''; ?>">
全部
</a>
</li>
<?php foreach ($categories as $category): ?>
<li class="memo-category-item">
<a href="<?php echo $currentFile . '?cat=' . urlencode($category); ?>"
class="memo-category-link <?php echo $selectedCategory == $category ? 'active' : ''; ?>">
<?php echo htmlspecialchars($category); ?>
<?php if (isset($categoryCounts[$category])): ?>
<span class="memo-category-count">(<?php echo $categoryCounts[$category]; ?>)</span>
<?php endif; ?>
</a>
</li>
<?php endforeach; ?>
</ul>
</div>
<?php endif; ?>
<!-- 筛选信息 -->
<?php if ($searchKeyword || $selectedCategory): ?>
<div class="memo-filter-info">
<?php if ($searchKeyword && $selectedCategory): ?>
搜索"<strong><?php echo htmlspecialchars($searchKeyword); ?></strong>",分类"<strong><?php echo htmlspecialchars($selectedCategory); ?></strong>",共找到 <?php echo $total; ?> 条记录
<?php elseif ($searchKeyword): ?>
搜索"<strong><?php echo htmlspecialchars($searchKeyword); ?></strong>",共找到 <?php echo $total; ?> 条记录
<?php elseif ($selectedCategory): ?>
分类"<strong><?php echo htmlspecialchars($selectedCategory); ?></strong>",共 <?php echo $total; ?> 条记录
<?php endif; ?>
</div>
<?php endif; ?>
<!-- 备忘录列表 -->
<?php if (!empty($memos)): ?>
<div class="memo-list">
<?php foreach ($memos as $memo): ?>
<div class="memo-item">
<div class="memo-item-header">
<?php if (!empty($memo['category'])): ?>
<span class="memo-category"><?php echo htmlspecialchars($memo['category']); ?></span>
<?php endif; ?>
<?php if (!empty($memo['event_date']) && isset($config->showEventDate) && $config->showEventDate == 1): ?>
<span class="memo-date"><?php echo date('Y.m.d', strtotime($memo['event_date'])); ?></span>
<?php endif; ?>
</div>
<div class="memo-content">
<?php
$content = $memo['content'];
// 修复2精确匹配URL只对URL本身加链接
$content = htmlspecialchars($content);
// 精确的URL正则匹配
$content = preg_replace_callback(
'/(https?:\/\/[a-zA-Z0-9][-a-zA-Z0-9@:%._\+~#=]{1,256}\.[a-zA-Z0-9()]{1,6}\b[-a-zA-Z0-9()@:%_\+.~#?&\/=]*)/',
function($matches) {
$url = $matches[1];
// 清理URL末尾可能错误的标点符号
$punctuation = array('.', ',', ';', ':', '!', '?', ')', ']', '}');
$lastChar = substr($url, -1);
if (in_array($lastChar, $punctuation)) {
$url = substr($url, 0, -1);
$suffix = $lastChar;
} else {
$suffix = '';
}
return '<a href="' . htmlspecialchars($url) . '" class="memo-url-link" target="_blank" rel="noopener noreferrer">' . htmlspecialchars($url) . '</a>' . $suffix;
},
$content
);
if (!empty($searchKeyword)) {
$searchEncoded = htmlspecialchars($searchKeyword);
$content = preg_replace(
'/(' . preg_quote($searchEncoded, '/') . ')/i',
'<mark style="background: yellow;">$1</mark>',
$content
);
}
echo nl2br($content);
?>
</div>
<?php if (!empty($memo['original_url'])): ?>
<div class="memo-original-link">
<div class="memo-original-title">原文链接:</div>
<a href="<?php echo htmlspecialchars($memo['original_url']); ?>"
class="memo-original-url"
target="_blank"
rel="noopener noreferrer">
<?php echo htmlspecialchars($memo['original_url']); ?>
</a>
</div>
<?php endif; ?>
<?php if (isset($config->showRelatedPosts) && $config->showRelatedPosts == 1 && !empty($memo['posts'])): ?>
<div class="memo-related-posts">
<div class="memo-related-title"><?php echo isset($config->relatedPostsTitle) ? $config->relatedPostsTitle : '相关文章'; ?></div>
<ul class="memo-related-list">
<?php foreach ($memo['posts'] as $post): ?>
<li class="memo-related-item">
<a href="<?php echo $post['url']; ?>" class="memo-related-link" target="_blank">
<?php echo htmlspecialchars($post['title']); ?>
</a>
</li>
<?php endforeach; ?>
</ul>
</div>
<?php endif; ?>
</div>
<?php endforeach; ?>
</div>
<!-- 分页 -->
<?php if ($totalPages > 1): ?>
<div class="memo-pagination">
<?php
$start = max(1, $currentPage - 2);
$end = min($totalPages, $currentPage + 2);
if ($currentPage > 1): ?>
<a href="<?php echo $currentFile; ?>?page=<?php echo $currentPage-1; ?><?php echo $selectedCategory ? '&cat=' . urlencode($selectedCategory) : ''; ?><?php echo $searchKeyword ? '&search=' . urlencode($searchKeyword) : ''; ?>">上一页</a>
<?php endif; ?>
<?php if ($start > 1): ?>
<a href="<?php echo $currentFile; ?>?page=1<?php echo $selectedCategory ? '&cat=' . urlencode($selectedCategory) : ''; ?><?php echo $searchKeyword ? '&search=' . urlencode($searchKeyword) : ''; ?>">1</a>
<?php if ($start > 2): ?>...<?php endif; ?>
<?php endif; ?>
<?php for ($i = $start; $i <= $end; $i++): ?>
<?php if ($i == $currentPage): ?>
<span class="current"><?php echo $i; ?></span>
<?php else: ?>
<a href="<?php echo $currentFile; ?>?page=<?php echo $i; ?><?php echo $selectedCategory ? '&cat=' . urlencode($selectedCategory) : ''; ?><?php echo $searchKeyword ? '&search=' . urlencode($searchKeyword) : ''; ?>"><?php echo $i; ?></a>
<?php endif; ?>
<?php endfor; ?>
<?php if ($end < $totalPages): ?>
<?php if ($end < $totalPages - 1): ?>...<?php endif; ?>
<a href="<?php echo $currentFile; ?>?page=<?php echo $totalPages; ?><?php echo $selectedCategory ? '&cat=' . urlencode($selectedCategory) : ''; ?><?php echo $searchKeyword ? '&search=' . urlencode($searchKeyword) : ''; ?>"><?php echo $totalPages; ?></a>
<?php endif; ?>
<?php if ($currentPage < $totalPages): ?>
<a href="<?php echo $currentFile; ?>?page=<?php echo $currentPage+1; ?><?php echo $selectedCategory ? '&cat=' . urlencode($selectedCategory) : ''; ?><?php echo $searchKeyword ? '&search=' . urlencode($searchKeyword) : ''; ?>">下一页</a>
<?php endif; ?>
</div>
<?php endif; ?>
<?php else: ?>
<div class="memo-empty">
<?php if ($searchKeyword || $selectedCategory): ?>
没有找到符合条件的备忘录
<?php else: ?>
暂无备忘录记录
<?php endif; ?>
</div>
<?php endif; ?>
</div>
<?php
}
}