From 59b4ab6e7ebe2065e514c9d3f478ec2e762cf107 Mon Sep 17 00:00:00 2001
From: XIGE <710062962@qq.com>
Date: Mon, 23 Feb 2026 17:31:07 +0800
Subject: [PATCH] 1.0
---
Action.php | 1357 +++++++++++++++++++++++++
Plugin.php | 698 +++++++++++++
manage-panel.php | 2468 ++++++++++++++++++++++++++++++++++++++++++++++
3 files changed, 4523 insertions(+)
create mode 100644 Action.php
create mode 100644 Plugin.php
create mode 100644 manage-panel.php
diff --git a/Action.php b/Action.php
new file mode 100644
index 0000000..194a0aa
--- /dev/null
+++ b/Action.php
@@ -0,0 +1,1357 @@
+db = Typecho_Db::get();
+ $this->prefix = $this->db->getPrefix();
+ $this->request = $request ?: Typecho_Request::getInstance();
+ $this->response = $response ?: Typecho_Response::getInstance();
+ }
+
+ /**
+ * 添加备忘录 - 修复SQL错误
+ */
+ public function addMemo($data)
+ {
+ try {
+ $now = new Typecho_Date(Typecho_Date::gmtTime());
+
+ $insert = array(
+ 'content' => $data['content'],
+ 'category' => isset($data['category']) ? $data['category'] : '默认',
+ 'event_date' => !empty($data['event_date']) ? $data['event_date'] : null,
+ 'post_cids' => isset($data['post_cids']) ? $this->cleanCids($data['post_cids']) : '',
+ 'original_url' => isset($data['original_url']) ? trim($data['original_url']) : '', // 新增:原文链接
+ 'created' => $now->format('Y-m-d H:i:s'),
+ 'modified' => $now->format('Y-m-d H:i:s'),
+ 'status' => 1
+ );
+
+ $this->validateInsertData($insert);
+
+ // 修复:直接使用insert方法,不调用query()两次
+ $insertId = $this->db->query($this->db->insert($this->prefix . 'memo')->rows($insert));
+
+ // 获取最后插入的ID
+ $row = $this->db->fetchRow($this->db->select('LAST_INSERT_ID() as id'));
+ return $row ? $row['id'] : $insertId;
+
+ } catch (Exception $e) {
+ error_log("addMemo Error: " . $e->getMessage());
+ throw new Exception("发布失败: " . $e->getMessage());
+ }
+ }
+
+ /**
+ * 验证插入数据
+ */
+ private function validateInsertData(&$data)
+ {
+ if (empty($data['content'])) {
+ throw new Exception("内容不能为空");
+ }
+
+ if (!empty($data['event_date']) && !preg_match('/^\d{4}-\d{2}-\d{2}$/', $data['event_date'])) {
+ throw new Exception("日期格式错误,应为YYYY-MM-DD");
+ }
+
+ $data['content'] = trim($data['content']);
+ if (empty($data['content'])) {
+ throw new Exception("内容不能为空");
+ }
+
+ // 验证原文链接格式(如果提供)
+ if (!empty($data['original_url']) && !filter_var($data['original_url'], FILTER_VALIDATE_URL)) {
+ throw new Exception("原文链接格式不正确,请输入有效的URL");
+ }
+
+ // 确保分类不为空
+ if (empty($data['category'])) {
+ $data['category'] = '默认';
+ }
+ }
+
+ /**
+ * 更新备忘录
+ */
+ public function updateMemo($data)
+ {
+ try {
+ $now = new Typecho_Date(Typecho_Date::gmtTime());
+
+ $update = array(
+ 'content' => $data['edit_content'],
+ 'category' => isset($data['edit_category']) ? $data['edit_category'] : '默认',
+ 'event_date' => !empty($data['edit_event_date']) ? $data['edit_event_date'] : null,
+ 'post_cids' => isset($data['edit_post_cids']) ? $this->cleanCids($data['edit_post_cids']) : '',
+ 'original_url' => isset($data['edit_original_url']) ? trim($data['edit_original_url']) : '', // 新增:原文链接
+ 'modified' => $now->format('Y-m-d H:i:s')
+ );
+
+ // 验证原文链接格式(如果提供)
+ if (!empty($update['original_url']) && !filter_var($update['original_url'], FILTER_VALIDATE_URL)) {
+ throw new Exception("原文链接格式不正确,请输入有效的URL");
+ }
+
+ // 修复:直接使用update方法
+ $this->db->query($this->db->update($this->prefix . 'memo')
+ ->rows($update)
+ ->where('id = ?', intval($data['edit_id'])));
+
+ } catch (Exception $e) {
+ error_log("updateMemo Error: " . $e->getMessage());
+ throw new Exception("更新失败: " . $e->getMessage());
+ }
+ }
+
+ /**
+ * 删除备忘录
+ */
+ public function deleteMemo($id)
+ {
+ $this->db->query($this->db->delete($this->prefix . 'memo')->where('id = ?', intval($id)));
+ }
+
+ /**
+ * 批量删除
+ */
+ public function deleteMemos($ids)
+ {
+ foreach ($ids as $id) {
+ $this->deleteMemo(intval($id));
+ }
+ }
+
+ /**
+ * 获取备忘录列表(带搜索和筛选)
+ */
+ public function getMemos($page = 1, $perPage = 10, $search = '', $category = '', $orderBy = 'id', $order = 'DESC')
+ {
+ $offset = ($page - 1) * $perPage;
+
+ $query = $this->db->select()
+ ->from($this->prefix . 'memo')
+ ->where('status = ?', 1);
+
+ // 搜索条件
+ if (!empty($search)) {
+ $query->where('content LIKE ?', '%' . $search . '%');
+ }
+
+ // 分类筛选
+ if (!empty($category)) {
+ $query->where('category = ?', $category);
+ }
+
+ // 排序
+ $query->order($orderBy, $order);
+
+ // 分页
+ $query->limit($perPage)->offset($offset);
+
+ return $this->db->fetchAll($query);
+ }
+
+ /**
+ * 获取所有备忘录(用于导出)
+ */
+ public function getAllMemos()
+ {
+ $query = $this->db->select()
+ ->from($this->prefix . 'memo')
+ ->where('status = ?', 1)
+ ->order('id', Typecho_Db::SORT_DESC);
+
+ return $this->db->fetchAll($query);
+ }
+
+ /**
+ * 获取按分类分组的备忘录(用于MD导出)
+ */
+ public function getMemosGroupedByCategory()
+ {
+ $query = $this->db->select()
+ ->from($this->prefix . 'memo')
+ ->where('status = ?', 1)
+ ->order('category', Typecho_Db::SORT_ASC)
+ ->order('event_date', Typecho_Db::SORT_DESC)
+ ->order('id', Typecho_Db::SORT_DESC);
+
+ $memos = $this->db->fetchAll($query);
+ $grouped = array();
+
+ foreach ($memos as $memo) {
+ $category = $memo['category'] ?: '未分类';
+ if (!isset($grouped[$category])) {
+ $grouped[$category] = array();
+ }
+ $grouped[$category][] = $memo;
+ }
+
+ return $grouped;
+ }
+
+ /**
+ * 获取总记录数(带搜索和筛选)
+ */
+ public function getTotalCount($search = '', $category = '')
+ {
+ $query = $this->db->select('COUNT(*) as count')
+ ->from($this->prefix . 'memo')
+ ->where('status = ?', 1);
+
+ if (!empty($search)) {
+ $query->where('content LIKE ?', '%' . $search . '%');
+ }
+
+ if (!empty($category)) {
+ $query->where('category = ?', $category);
+ }
+
+ $result = $this->db->fetchRow($query);
+ return $result['count'];
+ }
+
+ /**
+ * 获取单条记录
+ */
+ public function getMemo($id)
+ {
+ $query = $this->db->select()
+ ->from($this->prefix . 'memo')
+ ->where('id = ?', $id)
+ ->limit(1);
+
+ return $this->db->fetchRow($query);
+ }
+
+ /**
+ * 获取所有分类
+ */
+ public function getAllCategories()
+ {
+ $query = $this->db->select('DISTINCT category')
+ ->from($this->prefix . 'memo')
+ ->where('status = ?', 1)
+ ->order('category', Typecho_Db::SORT_ASC);
+
+ $results = $this->db->fetchAll($query);
+ $categories = array();
+ foreach ($results as $row) {
+ if (!empty($row['category'])) {
+ $categories[] = $row['category'];
+ }
+ }
+ return $categories;
+ }
+
+ /**
+ * 获取各分类的记录数
+ */
+ public function getCategoryCounts()
+ {
+ $query = $this->db->select('category, COUNT(*) as count')
+ ->from($this->prefix . 'memo')
+ ->where('status = ?', 1)
+ ->group('category')
+ ->order('count', Typecho_Db::SORT_DESC);
+
+ $results = $this->db->fetchAll($query);
+ $counts = array();
+ foreach ($results as $row) {
+ $counts[$row['category']] = $row['count'];
+ }
+ return $counts;
+ }
+
+ /**
+ * 根据文章CID获取文章信息
+ */
+ public function getPostByCid($cid)
+ {
+ try {
+ $query = $this->db->select('cid, title, slug, created')
+ ->from($this->prefix . 'contents')
+ ->where('cid = ?', intval($cid))
+ ->where('type = ?', 'post')
+ ->where('status = ?', 'publish')
+ ->limit(1);
+
+ $post = $this->db->fetchRow($query);
+
+ if ($post) {
+ $options = Typecho_Widget::widget('Widget_Options');
+ // 根据伪静态规则生成文章URL
+ if (!empty($post['slug'])) {
+ // 假设伪静态规则为 /{slug}.html
+ $post['url'] = Typecho_Common::url($post['slug'] . '.html', $options->index);
+ } else {
+ // 如果没有slug,使用默认格式
+ $post['url'] = Typecho_Common::url('archives/' . $post['cid'], $options->index);
+ }
+ }
+
+ return $post;
+ } catch (Exception $e) {
+ return null;
+ }
+ }
+
+ /**
+ * 清理CID字符串
+ */
+ private function cleanCids($cids)
+ {
+ if (empty($cids)) {
+ return '';
+ }
+
+ $cidsArray = array_filter(array_map('trim', explode(',', $cids)));
+ $validCids = array();
+
+ foreach ($cidsArray as $cid) {
+ if (is_numeric($cid) && $cid > 0) {
+ $validCids[] = intval($cid);
+ }
+ }
+
+ return implode(',', array_unique($validCids));
+ }
+
+ /**
+ * 导出数据为文本格式
+ */
+ public function exportData()
+ {
+ $memos = $this->getAllMemos();
+ $content = '';
+
+ foreach ($memos as $memo) {
+ $line = '';
+
+ // 添加分类
+ if (!empty($memo['category'])) {
+ $line .= '[' . $memo['category'] . '] ';
+ }
+
+ // 添加日期
+ if (!empty($memo['event_date'])) {
+ $date = date('Y.m.d', strtotime($memo['event_date']));
+ $line .= $date . ' ';
+ }
+
+ // 添加内容
+ $text = str_replace(array("\r\n", "\r", "\n"), " ", $memo['content']);
+ $line .= $text;
+
+ // 添加原文链接(如果有)
+ if (!empty($memo['original_url'])) {
+ $line .= ' [' . $memo['original_url'] . ']';
+ }
+
+ $line .= "\n";
+
+ $content .= $line;
+ }
+
+ return $content;
+ }
+
+ /**
+ * 导出数据为Markdown格式
+ */
+ public function exportMdData()
+ {
+ $groupedMemos = $this->getMemosGroupedByCategory();
+ $content = "# 知识备忘录\n\n";
+ $content .= "> 导出时间:" . date('Y-m-d H:i:s') . "\n\n";
+
+ foreach ($groupedMemos as $category => $memos) {
+ $content .= "## " . $category . "\n\n";
+
+ foreach ($memos as $memo) {
+ $content .= "- ";
+
+ // 添加日期
+ if (!empty($memo['event_date'])) {
+ $date = date('Y-m-d', strtotime($memo['event_date']));
+ $content .= "**" . $date . "** ";
+ }
+
+ // 添加内容
+ $text = $memo['content'];
+ // 清理换行符,但保留URL
+ $text = preg_replace('/\r\n|\r|\n/', ' ', $text);
+ $content .= $text;
+
+ // 添加原文链接(如果有)
+ if (!empty($memo['original_url'])) {
+ $content .= " [原文链接](" . $memo['original_url'] . ")";
+ }
+
+ // 添加关联文章(如果有)
+ if (!empty($memo['post_cids'])) {
+ $cids = array_filter(array_map('trim', explode(',', $memo['post_cids'])));
+ if (count($cids) > 0) {
+ $content .= " (关联文章: " . implode(', ', $cids) . ")";
+ }
+ }
+
+ $content .= "\n";
+ }
+
+ $content .= "\n";
+ }
+
+ // 添加统计信息
+ $totalCount = $this->getTotalCount();
+ $categoryCounts = $this->getCategoryCounts();
+
+ $content .= "---\n\n";
+ $content .= "## 统计信息\n\n";
+ $content .= "- 总记录数: " . $totalCount . "\n";
+
+ foreach ($categoryCounts as $cat => $count) {
+ $content .= "- " . $cat . ": " . $count . " 条\n";
+ }
+
+ return $content;
+ }
+
+ /**
+ * 导出选中的备忘录数据
+ */
+ public function exportSelectedData($ids, $format = 'txt')
+ {
+ if (empty($ids)) {
+ throw new Exception("未选择要导出的记录");
+ }
+
+ // 获取选中的记录
+ $ids = array_map('intval', $ids);
+ $idsStr = implode(',', $ids);
+
+ $query = $this->db->select()
+ ->from($this->prefix . 'memo')
+ ->where('id IN (' . $idsStr . ')')
+ ->where('status = ?', 1)
+ ->order('id', Typecho_Db::SORT_DESC);
+
+ $memos = $this->db->fetchAll($query);
+
+ if ($format === 'md') {
+ return $this->exportSelectedAsMd($memos);
+ } else {
+ return $this->exportSelectedAsTxt($memos);
+ }
+ }
+
+ /**
+ * 导出选中记录为TXT格式
+ */
+ private function exportSelectedAsTxt($memos)
+ {
+ $content = '';
+ $content .= '=== 选中的备忘录记录 ===' . "\n";
+ $content .= '导出时间:' . date('Y-m-d H:i:s') . "\n";
+ $content .= '记录数量:' . count($memos) . " 条\n";
+ $content .= '=======================' . "\n\n";
+
+ foreach ($memos as $memo) {
+ $line = '';
+
+ // 添加ID
+ $line .= 'ID:' . $memo['id'] . ' ';
+
+ // 添加分类
+ if (!empty($memo['category'])) {
+ $line .= '[' . $memo['category'] . '] ';
+ }
+
+ // 添加日期
+ if (!empty($memo['event_date'])) {
+ $date = date('Y.m.d', strtotime($memo['event_date']));
+ $line .= $date . ' ';
+ }
+
+ // 添加内容
+ $text = str_replace(array("\r\n", "\r", "\n"), " ", $memo['content']);
+ $line .= $text;
+
+ // 添加原文链接(如果有)
+ if (!empty($memo['original_url'])) {
+ $line .= ' [' . $memo['original_url'] . ']';
+ }
+
+ // 添加创建时间
+ $line .= ' (创建:' . date('Y-m-d', strtotime($memo['created'])) . ')';
+
+ $line .= "\n";
+
+ $content .= $line;
+ }
+
+ // 添加统计信息
+ $content .= "\n" . '=== 导出完成 ===' . "\n";
+ $content .= '总记录数:' . count($memos) . " 条\n";
+ $content .= '导出时间:' . date('Y-m-d H:i:s');
+
+ return $content;
+ }
+
+ /**
+ * 导出选中记录为MD格式
+ */
+ private function exportSelectedAsMd($memos)
+ {
+ // 按分类分组
+ $grouped = array();
+ foreach ($memos as $memo) {
+ $category = $memo['category'] ?: '未分类';
+ if (!isset($grouped[$category])) {
+ $grouped[$category] = array();
+ }
+ $grouped[$category][] = $memo;
+ }
+
+ // 按分类名称排序
+ ksort($grouped);
+
+ $content = "# 选中的备忘录记录\n\n";
+ $content .= "> 导出时间:" . date('Y-m-d H:i:s') . "\n";
+ $content .= "> 记录数量:" . count($memos) . " 条\n\n";
+
+ foreach ($grouped as $category => $categoryMemos) {
+ $content .= "## " . $category . "\n\n";
+
+ foreach ($categoryMemos as $memo) {
+ $content .= "### ID:" . $memo['id'] . "\n\n";
+ $content .= "- **创建时间**:" . date('Y-m-d H:i:s', strtotime($memo['created'])) . "\n";
+
+ if (!empty($memo['event_date'])) {
+ $content .= "- **事件日期**:" . date('Y-m-d', strtotime($memo['event_date'])) . "\n";
+ }
+
+ if (!empty($memo['original_url'])) {
+ $content .= "- **原文链接**:[点击查看](" . $memo['original_url'] . ")\n";
+ }
+
+ if (!empty($memo['post_cids'])) {
+ $cids = array_filter(array_map('trim', explode(',', $memo['post_cids'])));
+ if (count($cids) > 0) {
+ $content .= "- **关联文章**:" . implode(', ', $cids) . "\n";
+ }
+ }
+
+ $content .= "\n**内容**:\n\n";
+ $content .= $memo['content'] . "\n\n";
+ $content .= "---\n\n";
+ }
+ }
+
+ // 添加统计信息
+ $content .= "## 统计信息\n\n";
+ $content .= "- **总记录数**:" . count($memos) . " 条\n";
+ $content .= "- **分类数量**:" . count($grouped) . " 个\n";
+ foreach ($grouped as $category => $categoryMemos) {
+ $content .= " - " . $category . ":" . count($categoryMemos) . " 条\n";
+ }
+ $content .= "- **导出时间**:" . date('Y-m-d H:i:s') . "\n";
+
+ return $content;
+ }
+
+ /**
+ * 导入数据(修复版)
+ */
+ public function importData($text)
+ {
+ $text = str_replace("\r\n", "\n", $text);
+ $text = str_replace("\r", "\n", $text);
+
+ $lines = explode("\n", $text);
+ $imported = 0;
+ $failed = 0;
+ $failReasons = array();
+
+ // 检查是否是Markdown格式
+ $isMarkdown = false;
+ foreach ($lines as $line) {
+ if (strpos($line, '## ') === 0 || strpos($line, '# ') === 0) {
+ $isMarkdown = true;
+ break;
+ }
+ }
+
+ if ($isMarkdown) {
+ return $this->importMdData($text);
+ }
+
+ // 原始TXT格式导入
+ foreach ($lines as $lineNum => $line) {
+ $line = trim($line);
+ if (empty($line)) continue;
+
+ // 解析分类
+ $category = '默认';
+ $event_date = null;
+ $original_url = '';
+ $content = $line;
+
+ // 匹配分类 [分类]
+ if (preg_match('/^\[([^\]]+)\]\s*(.*)$/', $line, $matches)) {
+ $category = trim($matches[1]);
+ $content = trim($matches[2]);
+ }
+
+ // 匹配日期 YYYY.MM.DD 或 YYYY.M.D
+ $datePattern = '/^(\d{4})\.(\d{1,2})\.(\d{1,2})\s+(.*)$/';
+ if (preg_match($datePattern, $content, $matches)) {
+ $year = intval($matches[1]);
+ $month = intval($matches[2]);
+ $day = intval($matches[3]);
+
+ if (checkdate($month, $day, $year)) {
+ $event_date = sprintf('%04d-%02d-%02d', $year, $month, $day);
+ $content = trim($matches[4]);
+ }
+ }
+
+ // 匹配原文链接 [URL]
+ if (preg_match('/^(.*)\s+\[(https?:\/\/[^\]]+)\]$/', $content, $matches)) {
+ $content = trim($matches[1]);
+ $original_url = trim($matches[2]);
+ }
+
+ if (empty($content)) {
+ $failed++;
+ $failReasons[] = "第 " . ($lineNum + 1) . " 行:内容为空";
+ continue;
+ }
+
+ try {
+ $now = new Typecho_Date(Typecho_Date::gmtTime());
+ $insert = array(
+ 'content' => $content,
+ 'category' => $category,
+ 'event_date' => $event_date,
+ 'post_cids' => '',
+ 'original_url' => $original_url,
+ 'created' => $now->format('Y-m-d H:i:s'),
+ 'modified' => $now->format('Y-m-d H:i:s'),
+ 'status' => 1
+ );
+
+ $this->db->query($this->db->insert($this->prefix . 'memo')->rows($insert));
+ $imported++;
+
+ } catch (Exception $e) {
+ $failed++;
+ $failReasons[] = "第 " . ($lineNum + 1) . " 行:导入失败 - " . $e->getMessage();
+ }
+ }
+
+ return array(
+ 'imported' => $imported,
+ 'failed' => $failed,
+ 'fail_reasons' => $failReasons
+ );
+ }
+
+ /**
+ * 导入Markdown格式数据
+ */
+ private function importMdData($text)
+ {
+ $text = str_replace("\r\n", "\n", $text);
+ $text = str_replace("\r", "\n", $text);
+
+ $lines = explode("\n", $text);
+ $imported = 0;
+ $failed = 0;
+ $failReasons = array();
+
+ $currentCategory = '默认';
+ $inCategory = false;
+
+ foreach ($lines as $lineNum => $line) {
+ $line = trim($line);
+ if (empty($line)) continue;
+
+ // 检测分类标题 (## 分类名)
+ if (strpos($line, '## ') === 0) {
+ $currentCategory = trim(substr($line, 3));
+ $inCategory = true;
+ continue;
+ }
+
+ // 跳过其他标题和元信息
+ if (strpos($line, '# ') === 0 || strpos($line, '> ') === 0 || strpos($line, '---') === 0) {
+ continue;
+ }
+
+ // 检测列表项 (- 内容)
+ if (strpos($line, '- ') === 0) {
+ $content = trim(substr($line, 2));
+
+ // 解析日期 (**日期**)
+ $event_date = null;
+ if (preg_match('/^\*\*(\d{4}-\d{2}-\d{2})\*\*\s*(.*)$/', $content, $matches)) {
+ $event_date = $matches[1];
+ $content = trim($matches[2]);
+ }
+
+ // 解析原文链接 [原文链接](URL)
+ $original_url = '';
+ if (preg_match('/^(.*)\s+\[原文链接\]\((https?:\/\/[^)]+)\)/', $content, $matches)) {
+ $content = trim($matches[1]);
+ $original_url = trim($matches[2]);
+ }
+
+ // 解析关联文章 (关联文章: CID列表)
+ $post_cids = '';
+ if (preg_match('/^(.*)\s+\(关联文章:\s*([^)]+)\)/', $content, $matches)) {
+ $content = trim($matches[1]);
+ $post_cids = preg_replace('/[^0-9,]/', '', $matches[2]);
+ }
+
+ if (empty($content)) {
+ $failed++;
+ $failReasons[] = "第 " . ($lineNum + 1) . " 行:内容为空";
+ continue;
+ }
+
+ try {
+ $now = new Typecho_Date(Typecho_Date::gmtTime());
+ $insert = array(
+ 'content' => $content,
+ 'category' => $currentCategory,
+ 'event_date' => $event_date,
+ 'post_cids' => $post_cids,
+ 'original_url' => $original_url,
+ 'created' => $now->format('Y-m-d H:i:s'),
+ 'modified' => $now->format('Y-m-d H:i:s'),
+ 'status' => 1
+ );
+
+ $this->db->query($this->db->insert($this->prefix . 'memo')->rows($insert));
+ $imported++;
+
+ } catch (Exception $e) {
+ $failed++;
+ $failReasons[] = "第 " . ($lineNum + 1) . " 行:导入失败 - " . $e->getMessage();
+ }
+ }
+ }
+
+ return array(
+ 'imported' => $imported,
+ 'failed' => $failed,
+ 'fail_reasons' => $failReasons
+ );
+ }
+
+ /**
+ * 导入SQL文件数据(从Typecho文章SQL导入)- 简化修复版
+ */
+ public function importSqlData($sqlContent, $category = '文章导入')
+ {
+ try {
+ $imported = 0;
+ $failed = 0;
+ $failReasons = array();
+
+ // 方法:使用最简单直接的字符串处理
+ $lines = explode("\n", $sqlContent);
+ $inInsert = false;
+ $insertBuffer = '';
+ $insertCount = 0;
+
+ error_log("开始导入SQL,总行数: " . count($lines));
+
+ foreach ($lines as $lineNum => $line) {
+ $line = trim($line);
+
+ // 跳过空行和注释
+ if (empty($line) || strpos($line, '--') === 0) {
+ continue;
+ }
+
+ // 检查是否是INSERT语句开始
+ if (!$inInsert && preg_match('/INSERT\s+INTO\s+`?(\w+contents)`?\s+\(/i', $line)) {
+ $inInsert = true;
+ $insertBuffer = $line;
+ error_log("第 {$lineNum} 行: 开始INSERT语句");
+ continue;
+ }
+
+ // 如果在INSERT语句中
+ if ($inInsert) {
+ $insertBuffer .= ' ' . $line;
+
+ // 检查是否结束(以分号结尾)
+ if (substr($line, -1) === ';') {
+ $insertCount++;
+ error_log("第 {$lineNum} 行: 结束INSERT语句 #{$insertCount}");
+
+ // 处理这个INSERT语句
+ $result = $this->parseAndImportInsert($insertBuffer, $category, $imported + 1);
+ $imported += $result['imported'];
+ $failed += $result['failed'];
+ $failReasons = array_merge($failReasons, $result['fail_reasons']);
+
+ $inInsert = false;
+ $insertBuffer = '';
+
+ // 每处理10个INSERT输出一次进度
+ if ($insertCount % 10 == 0) {
+ error_log("已处理 {$insertCount} 个INSERT语句,导入 {$imported} 条记录");
+ }
+ }
+ }
+ }
+
+ // 处理最后一个可能的未完成INSERT
+ if ($inInsert && !empty($insertBuffer)) {
+ $insertCount++;
+ error_log("处理最后一个未完成的INSERT语句 #{$insertCount}");
+ $result = $this->parseAndImportInsert($insertBuffer . ';', $category, $imported + 1);
+ $imported += $result['imported'];
+ $failed += $result['failed'];
+ $failReasons = array_merge($failReasons, $result['fail_reasons']);
+ }
+
+ error_log("导入完成: 找到 {$insertCount} 个INSERT语句,成功 {$imported} 条,失败 {$failed} 条");
+
+ return array(
+ 'imported' => $imported,
+ 'failed' => $failed,
+ 'fail_reasons' => $failReasons
+ );
+
+ } catch (Exception $e) {
+ error_log("importSqlData 异常: " . $e->getMessage());
+ return array(
+ 'imported' => 0,
+ 'failed' => 1,
+ 'fail_reasons' => array('导入失败: ' . $e->getMessage())
+ );
+ }
+ }
+
+ /**
+ * 解析并导入单个INSERT语句
+ */
+ private function parseAndImportInsert($insertSql, $category, $startIndex)
+ {
+ $imported = 0;
+ $failed = 0;
+ $failReasons = array();
+
+ try {
+ // 移除末尾的分号
+ $insertSql = rtrim($insertSql, ';');
+
+ // 尝试多种方式解析INSERT语句
+ $records = $this->parseInsertStatement($insertSql);
+
+ foreach ($records as $recordIndex => $record) {
+ $currentIndex = $startIndex + $imported + $failed;
+
+ try {
+ // 检查是否为文章
+ if (!isset($record['type']) || $record['type'] != 'post') {
+ continue; // 跳过非文章类型
+ }
+
+ // 获取标题和内容
+ $title = isset($record['title']) ? trim($record['title']) : '';
+ $text = isset($record['text']) ? trim($record['text']) : '';
+ $created = isset($record['created']) ? intval($record['created']) : time();
+
+ // 跳过空内容
+ if (empty($text) && empty($title)) {
+ $failed++;
+ continue;
+ }
+
+ // 处理内容
+ $text = $this->cleanContent($text);
+
+ // 组合内容
+ $content = '';
+ if (!empty($title)) {
+ $content = $title;
+ if (!empty($text)) {
+ $content .= "\n\n" . $text;
+ }
+ } else {
+ $content = $text;
+ }
+
+ // 截断过长的内容
+ if (mb_strlen($content, 'UTF-8') > 800) {
+ $content = mb_substr($content, 0, 800, 'UTF-8') . '...';
+ }
+
+ if (empty($content)) {
+ $failed++;
+ continue;
+ }
+
+ // 准备插入数据
+ $now = new Typecho_Date(Typecho_Date::gmtTime());
+ $createdDate = new Typecho_Date($created);
+
+ $insert = array(
+ 'content' => $content,
+ 'category' => $category,
+ 'event_date' => $createdDate->format('Y-m-d'),
+ 'post_cids' => '', // SQL导入不关联CID
+ 'original_url' => '',
+ 'created' => $now->format('Y-m-d H:i:s'),
+ 'modified' => $now->format('Y-m-d H:i:s'),
+ 'status' => 1
+ );
+
+ // 执行插入
+ $this->db->query($this->db->insert($this->prefix . 'memo')->rows($insert));
+ $imported++;
+
+ } catch (Exception $e) {
+ $failed++;
+ error_log("导入记录 {$currentIndex} 失败: " . $e->getMessage());
+ }
+ }
+
+ } catch (Exception $e) {
+ $failed++;
+ $failReasons[] = "解析INSERT语句失败: " . $e->getMessage();
+ error_log("解析INSERT语句失败: " . $e->getMessage());
+ }
+
+ return array(
+ 'imported' => $imported,
+ 'failed' => $failed,
+ 'fail_reasons' => $failReasons
+ );
+ }
+
+ /**
+ * 解析INSERT语句 - 多种方法尝试
+ */
+ private function parseInsertStatement($insertSql)
+ {
+ $records = array();
+
+ // 方法1: 使用正则提取VALUES部分
+ if (preg_match('/VALUES\s*(.+)$/is', $insertSql, $match)) {
+ $valuesPart = trim($match[1]);
+
+ // 提取所有括号内的值集
+ $valueSets = $this->extractValueSetsFromString($valuesPart);
+
+ foreach ($valueSets as $valueSet) {
+ $record = $this->parseValueSet($valueSet);
+ if (!empty($record)) {
+ $records[] = $record;
+ }
+ }
+ }
+
+ // 如果方法1失败,尝试方法2
+ if (empty($records)) {
+ $records = $this->fallbackParseInsert($insertSql);
+ }
+
+ return $records;
+ }
+
+ /**
+ * 从字符串提取值集
+ */
+ private function extractValueSetsFromString($str)
+ {
+ $sets = array();
+ $current = '';
+ $depth = 0;
+ $inString = false;
+ $quoteChar = '';
+
+ for ($i = 0; $i < strlen($str); $i++) {
+ $char = $str[$i];
+
+ // 处理字符串内的字符
+ if ($inString) {
+ if ($char === '\\') {
+ $current .= $char;
+ if ($i + 1 < strlen($str)) {
+ $current .= $str[$i + 1];
+ $i++;
+ }
+ continue;
+ }
+
+ if ($char === $quoteChar) {
+ $inString = false;
+ }
+ $current .= $char;
+ continue;
+ }
+
+ // 处理引号
+ if ($char === "'" || $char === '"') {
+ $inString = true;
+ $quoteChar = $char;
+ $current .= $char;
+ continue;
+ }
+
+ // 处理括号
+ if ($char === '(') {
+ if ($depth === 0) {
+ $current = '(';
+ } else {
+ $current .= $char;
+ }
+ $depth++;
+ continue;
+ }
+
+ if ($char === ')') {
+ $depth--;
+ if ($depth === 0) {
+ $current .= ')';
+ $sets[] = $current;
+ $current = '';
+ continue;
+ }
+ $current .= $char;
+ continue;
+ }
+
+ // 其他字符
+ if ($depth > 0) {
+ $current .= $char;
+ }
+ }
+
+ return $sets;
+ }
+
+ /**
+ * 解析值集
+ */
+ private function parseValueSet($valueSet)
+ {
+ $record = array();
+
+ // 移除括号
+ $valueSet = trim($valueSet, '()');
+
+ // 分割值
+ $values = $this->splitValues($valueSet);
+
+ // 映射到常见的Typecho字段(根据您的SQL结构调整)
+ $fieldMap = array(
+ 0 => 'cid',
+ 1 => 'title',
+ 2 => 'slug',
+ 3 => 'created',
+ 4 => 'modified',
+ 5 => 'text',
+ 6 => 'order',
+ 7 => 'authorId',
+ 8 => 'template',
+ 9 => 'type',
+ 10 => 'status',
+ 11 => 'password',
+ 12 => 'commentsNum',
+ 13 => 'allowComment',
+ 14 => 'allowPing',
+ 15 => 'allowFeed',
+ 16 => 'parent',
+ 17 => 'views'
+ );
+
+ foreach ($values as $index => $value) {
+ if (isset($fieldMap[$index])) {
+ $field = $fieldMap[$index];
+ $record[$field] = $this->cleanValue($value);
+ }
+ }
+
+ return $record;
+ }
+
+ /**
+ * 分割值
+ */
+ private function splitValues($str)
+ {
+ $values = array();
+ $current = '';
+ $depth = 0;
+ $inString = false;
+ $quoteChar = '';
+
+ for ($i = 0; $i < strlen($str); $i++) {
+ $char = $str[$i];
+
+ // 处理转义字符
+ if ($char === '\\' && $inString) {
+ $current .= $char;
+ if ($i + 1 < strlen($str)) {
+ $current .= $str[$i + 1];
+ $i++;
+ }
+ continue;
+ }
+
+ // 处理引号
+ if (($char === "'" || $char === '"') && ($i === 0 || $str[$i-1] !== '\\')) {
+ if (!$inString) {
+ $inString = true;
+ $quoteChar = $char;
+ } elseif ($char === $quoteChar) {
+ $inString = false;
+ }
+ $current .= $char;
+ continue;
+ }
+
+ // 处理逗号分隔
+ if ($char === ',' && !$inString && $depth === 0) {
+ $values[] = $current;
+ $current = '';
+ continue;
+ }
+
+ $current .= $char;
+ }
+
+ // 添加最后一个值
+ if (!empty($current)) {
+ $values[] = $current;
+ }
+
+ return $values;
+ }
+
+ /**
+ * 清理值
+ */
+ private function cleanValue($value)
+ {
+ $value = trim($value);
+
+ // 移除引号
+ if ((strpos($value, "'") === 0 && substr($value, -1) === "'") ||
+ (strpos($value, '"') === 0 && substr($value, -1) === '"')) {
+ $value = substr($value, 1, -1);
+ }
+
+ // 处理转义字符
+ $value = str_replace("\\'", "'", $value);
+ $value = str_replace('\\"', '"', $value);
+ $value = str_replace('\\\\', '\\', $value);
+
+ return $value;
+ }
+
+ /**
+ * 备用解析方法
+ */
+ private function fallbackParseInsert($insertSql)
+ {
+ $records = array();
+
+ // 尝试更简单的正则匹配
+ if (preg_match_all('/\(([^)]+)\)/', $insertSql, $matches)) {
+ foreach ($matches[1] as $match) {
+ // 假设这是值部分
+ $values = explode(',', $match);
+ if (count($values) >= 10) { // 确保有足够字段
+ $record = array(
+ 'cid' => $this->cleanValue($values[0] ?? ''),
+ 'title' => $this->cleanValue($values[1] ?? ''),
+ 'slug' => $this->cleanValue($values[2] ?? ''),
+ 'created' => $this->cleanValue($values[3] ?? ''),
+ 'modified' => $this->cleanValue($values[4] ?? ''),
+ 'text' => $this->cleanValue($values[5] ?? ''),
+ 'type' => $this->cleanValue($values[9] ?? ''),
+ 'status' => $this->cleanValue($values[10] ?? '')
+ );
+ $records[] = $record;
+ }
+ }
+ }
+
+ return $records;
+ }
+
+ /**
+ * 清理内容
+ */
+ private function cleanContent($text)
+ {
+ if (empty($text)) return '';
+
+ // 移除HTML标签
+ $text = strip_tags($text);
+
+ // 解码HTML实体
+ $text = html_entity_decode($text, ENT_QUOTES, 'UTF-8');
+
+ // 移除多余空白
+ $text = preg_replace('/\s+/', ' ', $text);
+
+ return trim($text);
+ }
+
+ /**
+ * 将URL转换为链接(静态方法)
+ * 修复:只匹配准确的URL,不包括前后文字
+ */
+ public static function makeLinksClickable($text)
+ {
+ if (empty($text)) {
+ return $text;
+ }
+
+ // 改进的URL正则表达式 - 只匹配完整的URL
+ // 匹配格式:http://example.com 或 https://example.com/path
+ // 使用单词边界确保只匹配完整的URL
+ $urlPattern = '/(?' . htmlspecialchars($url) . '';
+
+ return $link . $suffix;
+ }, $text);
+
+ return $result ? $result : $text;
+ }
+
+ /**
+ * 简单的URL检测函数 - 更保守的版本
+ */
+ public static function simpleMakeLinksClickable($text)
+ {
+ if (empty($text)) {
+ return $text;
+ }
+
+ // 更简单的正则,只匹配明显的URL
+ $urlPattern = '/(https?:\/\/[^\s<>"\'\)\(]+)/';
+
+ $result = preg_replace_callback($urlPattern, function($matches) {
+ $url = $matches[1];
+ // 移除URL末尾可能错误包含的标点
+ $url = rtrim($url, '.,;:!?)');
+
+ return '' . htmlspecialchars($url) . '';
+ }, $text);
+
+ return $result ? $result : $text;
+ }
+
+/**
+ * 获取统计信息 - 修复:使用event_date字段
+ */
+public function getStatistics()
+{
+ $stats = array();
+
+ try {
+ // 总记录数
+ $totalResult = $this->db->fetchRow($this->db->select('COUNT(*) as count')
+ ->from($this->prefix . 'memo')
+ ->where('status = ?', 1));
+ $stats['total_count'] = $totalResult ? $totalResult['count'] : 0;
+
+ // 使用原生SQL查询避免复杂的where条件
+ $sql = "SELECT
+ MIN(event_date) as oldest_date,
+ MAX(event_date) as newest_date,
+ COUNT(CASE WHEN event_date IS NOT NULL AND event_date != '' AND event_date != '0000-00-00' THEN 1 END) as dated_count
+ FROM {$this->prefix}memo
+ WHERE status = 1";
+
+ $dateResult = $this->db->fetchRow($sql);
+
+ if ($dateResult) {
+ $stats['oldest_date'] = !empty($dateResult['oldest_date']) && $dateResult['oldest_date'] != '0000-00-00' ? $dateResult['oldest_date'] : null;
+ $stats['newest_date'] = !empty($dateResult['newest_date']) && $dateResult['newest_date'] != '0000-00-00' ? $dateResult['newest_date'] : null;
+ $stats['dated_count'] = $dateResult['dated_count'] ? intval($dateResult['dated_count']) : 0;
+ } else {
+ $stats['oldest_date'] = null;
+ $stats['newest_date'] = null;
+ $stats['dated_count'] = 0;
+ }
+
+ $stats['undated_count'] = $stats['total_count'] - $stats['dated_count'];
+
+ // 分类统计
+ $categoryQuery = $this->db->select('category, COUNT(*) as count')
+ ->from($this->prefix . 'memo')
+ ->where('status = ?', 1)
+ ->group('category')
+ ->order('count', Typecho_Db::SORT_DESC);
+ $categoryResults = $this->db->fetchAll($categoryQuery);
+
+ $stats['categories'] = array();
+ foreach ($categoryResults as $row) {
+ $stats['categories'][$row['category']] = $row['count'];
+ }
+
+ return $stats;
+
+ } catch (Exception $e) {
+ error_log("getStatistics Error: " . $e->getMessage());
+ // 返回默认值避免页面崩溃
+ return array(
+ 'total_count' => 0,
+ 'oldest_date' => null,
+ 'newest_date' => null,
+ 'dated_count' => 0,
+ 'undated_count' => 0,
+ 'categories' => array()
+ );
+ }
+}
+}
\ No newline at end of file
diff --git a/Plugin.php b/Plugin.php
new file mode 100644
index 0000000..7e6e5c5
--- /dev/null
+++ b/Plugin.php
@@ -0,0 +1,698 @@
+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 '
暂无备忘录记录
';
+ 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();
+ }
+ }
+ ?>
+
+
+
+
+
备忘录
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 搜索"",分类"",共找到 条记录
+
+ 搜索"",共找到 条记录
+
+ 分类"",共 条记录
+
+
+
+
+
+
+
+
+
+
+
+
+ ' . htmlspecialchars($url) . '' . $suffix;
+ },
+ $content
+ );
+
+ if (!empty($searchKeyword)) {
+ $searchEncoded = htmlspecialchars($searchKeyword);
+ $content = preg_replace(
+ '/(' . preg_quote($searchEncoded, '/') . ')/i',
+ '$1',
+ $content
+ );
+ }
+ echo nl2br($content);
+ ?>
+
+
+
+
+
+
+ showRelatedPosts) && $config->showRelatedPosts == 1 && !empty($memo['posts'])): ?>
+
+
relatedPostsTitle) ? $config->relatedPostsTitle : '相关文章'; ?>
+
+
+
+
+
+
+
+
+ 1): ?>
+
+
+
+
+
+
+ 没有找到符合条件的备忘录
+
+ 暂无备忘录记录
+
+
+
+
+ plugin('Memo');
+
+// 引入Action类
+require_once __DIR__ . '/Action.php';
+$action = new Memo_Action();
+
+// 获取搜索和筛选参数
+$search = isset($_GET['search']) ? trim($_GET['search']) : '';
+$category = isset($_GET['category']) ? trim($_GET['category']) : '';
+$page = isset($_GET['page']) ? intval($_GET['page']) : 1;
+$perPage = isset($config->perPage) ? intval($config->perPage) : 10;
+
+// 获取所有分类
+$allCategories = $action->getAllCategories();
+$categoryCounts = $action->getCategoryCounts();
+
+// 获取统计信息
+$statistics = $action->getStatistics();
+
+// 获取默认分类设置
+$defaultCategories = isset($config->defaultCategories) ? explode("\n", $config->defaultCategories) : array('默认');
+$defaultCategories = array_map('trim', $defaultCategories);
+$defaultCategories = array_filter($defaultCategories);
+
+// 处理表单提交
+if ($_SERVER['REQUEST_METHOD'] === 'POST') {
+ // 处理新增
+ if (!empty($_POST['content'])) {
+ try {
+ $action->addMemo(array(
+ 'content' => $_POST['content'],
+ 'category' => isset($_POST['category']) ? $_POST['category'] : '默认',
+ 'event_date' => isset($_POST['event_date']) && !empty($_POST['event_date']) ? $_POST['event_date'] : null,
+ 'post_cids' => isset($_POST['post_cids']) ? $_POST['post_cids'] : '',
+ 'original_url' => isset($_POST['original_url']) ? $_POST['original_url'] : '' // 新增:原文链接
+ ));
+
+ $successMsg = '知识添加成功!';
+ // 重定向到当前页面(保持筛选状态)
+ $redirectUrl = $_SERVER['PHP_SELF'] . '?panel=Memo/manage-panel.php&page=' . $page;
+ if ($search) $redirectUrl .= '&search=' . urlencode($search);
+ if ($category) $redirectUrl .= '&category=' . urlencode($category);
+ $redirectUrl .= '#memo-list-anchor';
+ header('Location: ' . $redirectUrl);
+ exit;
+ } catch (Exception $e) {
+ $errorMsg = '添加失败: ' . htmlspecialchars($e->getMessage());
+ }
+ }
+
+ // 处理批量删除 - 修复:独立的批量删除处理
+ if (isset($_POST['bulk_delete']) && !empty($_POST['delete_ids']) && is_array($_POST['delete_ids'])) {
+ $action->deleteMemos($_POST['delete_ids']);
+ $successMsg = '删除成功!';
+ // 重定向到当前页面(保持筛选状态)
+ $redirectUrl = $_SERVER['PHP_SELF'] . '?panel=Memo/manage-panel.php&page=' . $page;
+ if ($search) $redirectUrl .= '&search=' . urlencode($search);
+ if ($category) $redirectUrl .= '&category=' . urlencode($category);
+ $redirectUrl .= '#memo-list-anchor';
+ header('Location: ' . $redirectUrl);
+ exit;
+ }
+
+ // 处理批量导出TXT - 修复:正确处理导出数据
+ if (isset($_POST['export_selected_txt']) && !empty($_POST['export_ids'])) {
+ try {
+ // 将逗号分隔的字符串转换为数组
+ $exportIds = explode(',', $_POST['export_ids']);
+ $exportIds = array_map('intval', $exportIds);
+ $exportIds = array_filter($exportIds);
+
+ if (empty($exportIds)) {
+ throw new Exception('没有选择有效的记录');
+ }
+
+ $exportContent = $action->exportSelectedData($exportIds, 'txt');
+ $filename = 'memo_selected_' . date('Ymd_His') . '.txt';
+
+ header('Content-Type: text/plain');
+ header('Content-Disposition: attachment; filename="' . $filename . '"');
+ header('Content-Length: ' . strlen($exportContent));
+ echo $exportContent;
+ exit;
+ } catch (Exception $e) {
+ $errorMsg = '导出失败: ' . htmlspecialchars($e->getMessage());
+ }
+ }
+
+ // 处理批量导出MD - 修复:正确处理导出数据
+ if (isset($_POST['export_selected_md']) && !empty($_POST['export_ids'])) {
+ try {
+ // 将逗号分隔的字符串转换为数组
+ $exportIds = explode(',', $_POST['export_ids']);
+ $exportIds = array_map('intval', $exportIds);
+ $exportIds = array_filter($exportIds);
+
+ if (empty($exportIds)) {
+ throw new Exception('没有选择有效的记录');
+ }
+
+ $exportContent = $action->exportSelectedData($exportIds, 'md');
+ $filename = 'memo_selected_' . date('Ymd_His') . '.md';
+
+ header('Content-Type: text/markdown');
+ header('Content-Disposition: attachment; filename="' . $filename . '"');
+ header('Content-Length: ' . strlen($exportContent));
+ echo $exportContent;
+ exit;
+ } catch (Exception $e) {
+ $errorMsg = '导出失败: ' . htmlspecialchars($e->getMessage());
+ }
+ }
+
+ // 处理单个导出
+ if (isset($_POST['export_single_txt']) && !empty($_POST['single_id'])) {
+ try {
+ $exportContent = $action->exportSelectedData(array(intval($_POST['single_id'])), 'txt');
+ $filename = 'memo_' . $_POST['single_id'] . '_' . date('Ymd_His') . '.txt';
+
+ header('Content-Type: text/plain');
+ header('Content-Disposition: attachment; filename="' . $filename . '"');
+ header('Content-Length: ' . strlen($exportContent));
+ echo $exportContent;
+ exit;
+ } catch (Exception $e) {
+ $errorMsg = '导出失败: ' . htmlspecialchars($e->getMessage());
+ }
+ }
+
+ if (isset($_POST['export_single_md']) && !empty($_POST['single_id'])) {
+ try {
+ $exportContent = $action->exportSelectedData(array(intval($_POST['single_id'])), 'md');
+ $filename = 'memo_' . $_POST['single_id'] . '_' . date('Ymd_His') . '.md';
+
+ header('Content-Type: text/markdown');
+ header('Content-Disposition: attachment; filename="' . $filename . '"');
+ header('Content-Length: ' . strlen($exportContent));
+ echo $exportContent;
+ exit;
+ } catch (Exception $e) {
+ $errorMsg = '导出失败: ' . htmlspecialchars($e->getMessage());
+ }
+ }
+
+ // 处理编辑
+ if (!empty($_POST['edit_id'])) {
+ $action->updateMemo(array(
+ 'edit_id' => $_POST['edit_id'],
+ 'edit_content' => $_POST['edit_content'],
+ 'edit_category' => isset($_POST['edit_category']) ? $_POST['edit_category'] : '默认',
+ 'edit_event_date' => isset($_POST['edit_event_date']) && !empty($_POST['edit_event_date']) ? $_POST['edit_event_date'] : null,
+ 'edit_post_cids' => isset($_POST['edit_post_cids']) ? $_POST['edit_post_cids'] : '',
+ 'edit_original_url' => isset($_POST['edit_original_url']) ? $_POST['edit_original_url'] : ''
+ ));
+ $successMsg = '知识更新成功!';
+ // 重定向到当前页面(保持筛选状态)
+ $redirectUrl = $_SERVER['PHP_SELF'] . '?panel=Memo/manage-panel.php&page=' . $page;
+ if ($search) $redirectUrl .= '&search=' . urlencode($search);
+ if ($category) $redirectUrl .= '&category=' . urlencode($category);
+ $redirectUrl .= '#memo-list-anchor';
+ header('Location: ' . $redirectUrl);
+ exit;
+ }
+
+ // 处理全量导出
+ if (isset($_POST['export'])) {
+ $exportContent = $action->exportData();
+ $filename = 'memo_' . date('Ymd_His') . '.txt';
+
+ header('Content-Type: text/plain');
+ header('Content-Disposition: attachment; filename="' . $filename . '"');
+ header('Content-Length: ' . strlen($exportContent));
+ echo $exportContent;
+ exit;
+ }
+
+ // 处理全量导出为MD
+ if (isset($_POST['export_md'])) {
+ $exportContent = $action->exportMdData();
+ $filename = 'memo_' . date('Ymd_His') . '.md';
+
+ header('Content-Type: text/markdown');
+ header('Content-Disposition: attachment; filename="' . $filename . '"');
+ header('Content-Length: ' . strlen($exportContent));
+ echo $exportContent;
+ exit;
+ }
+
+ // 处理导入
+ if (isset($_POST['import']) && !empty($_FILES['import_file']['tmp_name'])) {
+ $fileContent = file_get_contents($_FILES['import_file']['tmp_name']);
+ $fileExtension = strtolower(pathinfo($_FILES['import_file']['name'], PATHINFO_EXTENSION));
+
+ if ($fileExtension == 'sql') {
+ // 导入SQL文件
+ $importCategory = isset($_POST['import_category']) ? $_POST['import_category'] : '文章导入';
+ $result = $action->importSqlData($fileContent, $importCategory);
+ } else {
+ // 导入文本文件
+ $result = $action->importData($fileContent);
+ }
+
+ if ($result['imported'] > 0) {
+ $successMsg = '导入成功!成功导入 ' . $result['imported'] . ' 条记录';
+ if ($result['failed'] > 0) {
+ $successMsg .= ',失败 ' . $result['failed'] . ' 条记录';
+
+ if (!empty($result['fail_reasons'])) {
+ $errorDetails = '失败原因:
';
+ $displayCount = min(10, count($result['fail_reasons']));
+ for ($i = 0; $i < $displayCount; $i++) {
+ $errorDetails .= htmlspecialchars($result['fail_reasons'][$i]) . '
';
+ }
+ if (count($result['fail_reasons']) > 10) {
+ $errorDetails .= '...还有 ' . (count($result['fail_reasons']) - 10) . ' 条失败记录';
+ }
+ $errorMsg = $errorDetails;
+ }
+ }
+ } else {
+ $errorMsg = '导入失败!未找到可导入的记录或格式不正确';
+ if (!empty($result['fail_reasons'])) {
+ $errorMsg .= '
失败原因:
';
+ $displayCount = min(10, count($result['fail_reasons']));
+ for ($i = 0; $i < $displayCount; $i++) {
+ $errorMsg .= htmlspecialchars($result['fail_reasons'][$i]) . '
';
+ }
+ }
+ }
+ }
+}
+
+// 获取知识数据
+$memos = $action->getMemos($page, $perPage, $search, $category);
+$total = $action->getTotalCount($search, $category);
+$totalPages = ceil($total / $perPage);
+
+// 为每条记录获取关联的文章信息
+foreach ($memos as &$memo) {
+ $memo['post_info'] = array();
+ if (!empty($memo['post_cids'])) {
+ $cids = array_filter(array_map('trim', explode(',', $memo['post_cids'])));
+ foreach ($cids as $cid) {
+ if (is_numeric($cid)) {
+ $post = $action->getPostByCid($cid);
+ if ($post) {
+ $memo['post_info'][$cid] = $post;
+ }
+ }
+ }
+ }
+}
+unset($memo);
+
+// 辅助函数:将URL转换为链接(精确匹配)
+function convertUrlsToLinks($text) {
+ if (empty($text)) {
+ return $text;
+ }
+
+ // 先进行HTML实体编码
+ $encodedText = htmlspecialchars($text, ENT_QUOTES, 'UTF-8');
+
+ // 精确匹配URL的正则表达式
+ // 只匹配以http://或https://开头的完整URL
+ $urlPattern = '/(https?:\/\/[a-zA-Z0-9][-a-zA-Z0-9@:%._\+~#=]{1,256}\.[a-zA-Z0-9()]{1,6}\b[-a-zA-Z0-9()@:%_\+.~#?&\/=]*)/';
+
+ // 使用preg_replace_callback进行精确处理
+ return preg_replace_callback($urlPattern, 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 = '';
+ }
+
+ // 确保URL格式正确
+ if (!preg_match('/^https?:\/\//', $url)) {
+ return $matches[0]; // 如果不是合法URL,返回原文本
+ }
+
+ // 创建链接
+ return '' . htmlspecialchars($url) . '' . $suffix;
+ }, $encodedText);
+}
+
+?>
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
跨度时间
+
+
+ 最早:
+
+
+ 最新:
+
+
+
+
+
+
+ 最早:
+
+
+
+
+
+
+ 最新:
+
+
+
+ 有日期: 条,
+ 无日期: 条
+
+
+
+
+ 暂无事件日期数据
+
+
+
+
+
+
+
+
分类分布
+
+
+
+
暂无分类数据
+
+
+
+
+
+
+
+
+
+
+
导出知识
+
导出所有知识为文件格式
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
知识列表
+
+
+
+
+
+
+
📝
+
暂无知识记录
+
还没有添加知识,或者当前筛选条件没有匹配的记录
+
+
查看全部
+
+
+
+
+
+
+
+
+
+
+
+ 已选择 0 条记录
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ |
+
+ |
+ ID |
+ 知识内容 |
+ 知识分类 |
+ 发掘时间 |
+ 发布时间 |
+ 文章关联 |
+ 操作 |
+
+
+
+
+
+ |
+
+ |
+
+ #
+ |
+
+
+
+
+
+
+ $1',
+ $content
+ );
+ }
+
+ // 获取纯文本长度(不含HTML标签)用于判断
+ $plainText = strip_tags($content);
+
+ // 显示内容,如果过长则截取
+ $maxLength = 200; // 截取长度阈值
+ $showFullContent = false;
+
+ if (mb_strlen($plainText, 'UTF-8') > $maxLength) {
+ // 截取内容
+ $shortenedContent = mb_substr($plainText, 0, $maxLength, 'UTF-8') . '...';
+ // 重新应用HTML处理(只处理URL)
+ $shortenedContent = convertUrlsToLinks($shortenedContent);
+
+ // 如果搜索关键词被截断了,确保高亮显示
+ if ($search) {
+ $shortenedContent = preg_replace(
+ '/(' . preg_quote(htmlspecialchars($search), '/') . ')/i',
+ '$1',
+ $shortenedContent
+ );
+ }
+
+ echo nl2br($shortenedContent);
+ $showFullContent = true;
+ } else {
+ // 内容不长,显示完整内容
+ echo nl2br($content);
+ $showFullContent = false;
+ }
+ ?>
+
+
+
+
+
+
+ |
+
+
+
+
+
+ |
+
+
+
+
+
+
+ -
+
+ |
+
+
+
+
+ |
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ -
+
+ |
+
+
+
+
+
+
+
+ |
+
+
+
+
+
+
+
+
+
+ 1): ?>
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file