优化知识库展示
列表页改进: - 显示元数据标题而非文件名 - 显示作者、发布时间、平台 - 显示正文概述前两行 - 根据平台显示图标 文章页改进: - 过滤 YAML front matter - 只显示正文内容
This commit is contained in:
+100
-34
@@ -9,6 +9,66 @@ $docDir = getUserDocDir($username);
|
|||||||
// 搜索关键词
|
// 搜索关键词
|
||||||
$search = trim($_GET['q'] ?? '');
|
$search = trim($_GET['q'] ?? '');
|
||||||
|
|
||||||
|
// 解析 Markdown 文件的 YAML front matter
|
||||||
|
function parseMarkdownWithMeta($filePath) {
|
||||||
|
$content = file_get_contents($filePath);
|
||||||
|
$meta = [
|
||||||
|
'title' => basename($filePath),
|
||||||
|
'author' => '',
|
||||||
|
'date' => '',
|
||||||
|
'platform' => '',
|
||||||
|
'url' => '',
|
||||||
|
'summary' => ''
|
||||||
|
];
|
||||||
|
|
||||||
|
// 解析 YAML front matter
|
||||||
|
if (preg_match('/^---\s*\n(.*?)\n---\s*\n(.*)$/s', $content, $matches)) {
|
||||||
|
$yaml = $matches[1];
|
||||||
|
$body = $matches[2];
|
||||||
|
|
||||||
|
// 解析 YAML 字段
|
||||||
|
if (preg_match('/^title:\s*["\']?(.+?)["\']?\s*$/m', $yaml, $m)) {
|
||||||
|
$meta['title'] = trim($m[1], '"\'');
|
||||||
|
}
|
||||||
|
if (preg_match('/^author:\s*(.+?)\s*$/m', $yaml, $m)) {
|
||||||
|
$meta['author'] = trim($m[1]);
|
||||||
|
}
|
||||||
|
if (preg_match('/^date:\s*(.+?)\s*$/m', $yaml, $m)) {
|
||||||
|
$meta['date'] = trim($m[1]);
|
||||||
|
}
|
||||||
|
if (preg_match('/^platform:\s*(.+?)\s*$/m', $yaml, $m)) {
|
||||||
|
$meta['platform'] = trim($m[1]);
|
||||||
|
}
|
||||||
|
if (preg_match('/^url:\s*(.+?)\s*$/m', $yaml, $m)) {
|
||||||
|
$meta['url'] = trim($m[1]);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 提取正文概述(前两行非空内容)
|
||||||
|
$lines = preg_split('/\r?\n/', trim($body));
|
||||||
|
$summaryLines = [];
|
||||||
|
foreach ($lines as $line) {
|
||||||
|
$line = trim($line);
|
||||||
|
if (empty($line) || strpos($line, '##') === 0) continue;
|
||||||
|
$summaryLines[] = $line;
|
||||||
|
if (count($summaryLines) >= 2) break;
|
||||||
|
}
|
||||||
|
$meta['summary'] = implode(' ', $summaryLines);
|
||||||
|
} else {
|
||||||
|
// 没有 front matter,从正文提取
|
||||||
|
$lines = preg_split('/\r?\n/', trim($content));
|
||||||
|
$summaryLines = [];
|
||||||
|
foreach ($lines as $line) {
|
||||||
|
$line = trim($line);
|
||||||
|
if (empty($line) || strpos($line, '#') === 0) continue;
|
||||||
|
$summaryLines[] = $line;
|
||||||
|
if (count($summaryLines) >= 2) break;
|
||||||
|
}
|
||||||
|
$meta['summary'] = implode(' ', $summaryLines);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $meta;
|
||||||
|
}
|
||||||
|
|
||||||
// 递归获取文档列表
|
// 递归获取文档列表
|
||||||
$documents = [];
|
$documents = [];
|
||||||
|
|
||||||
@@ -24,10 +84,8 @@ function scanDirectory($dir, $baseDir, $search) {
|
|||||||
$relativePath = str_replace($baseDir . '/', '', $fullPath);
|
$relativePath = str_replace($baseDir . '/', '', $fullPath);
|
||||||
|
|
||||||
if (is_dir($fullPath)) {
|
if (is_dir($fullPath)) {
|
||||||
// 递归扫描子目录
|
|
||||||
$docs = array_merge($docs, scanDirectory($fullPath, $baseDir, $search));
|
$docs = array_merge($docs, scanDirectory($fullPath, $baseDir, $search));
|
||||||
} elseif (is_file($fullPath)) {
|
} elseif (is_file($fullPath)) {
|
||||||
// 只处理 .md 和 .txt 文件
|
|
||||||
$ext = strtolower(pathinfo($item, PATHINFO_EXTENSION));
|
$ext = strtolower(pathinfo($item, PATHINFO_EXTENSION));
|
||||||
if (!in_array($ext, ['md', 'txt', 'markdown'])) continue;
|
if (!in_array($ext, ['md', 'txt', 'markdown'])) continue;
|
||||||
|
|
||||||
@@ -36,21 +94,26 @@ function scanDirectory($dir, $baseDir, $search) {
|
|||||||
// 搜索过滤
|
// 搜索过滤
|
||||||
if ($search) {
|
if ($search) {
|
||||||
$searchLower = mb_strtolower($search);
|
$searchLower = mb_strtolower($search);
|
||||||
$titleLower = mb_strtolower($item);
|
|
||||||
$contentLower = mb_strtolower($content);
|
$contentLower = mb_strtolower($content);
|
||||||
if (strpos($titleLower, $searchLower) === false &&
|
if (strpos($contentLower, $searchLower) === false) {
|
||||||
strpos($contentLower, $searchLower) === false) {
|
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 解析元数据
|
||||||
|
$meta = parseMarkdownWithMeta($fullPath);
|
||||||
|
|
||||||
$docs[] = [
|
$docs[] = [
|
||||||
'name' => $item,
|
'name' => $item,
|
||||||
'path' => $relativePath,
|
'path' => $relativePath,
|
||||||
'folder' => dirname($relativePath) === '.' ? '' : dirname($relativePath),
|
'folder' => dirname($relativePath) === '.' ? '' : dirname($relativePath),
|
||||||
'size' => filesize($fullPath),
|
'size' => filesize($fullPath),
|
||||||
'mtime' => filemtime($fullPath),
|
'mtime' => filemtime($fullPath),
|
||||||
'preview' => mb_substr(strip_tags($content), 0, 200)
|
'title' => $meta['title'],
|
||||||
|
'author' => $meta['author'],
|
||||||
|
'date' => $meta['date'],
|
||||||
|
'platform' => $meta['platform'],
|
||||||
|
'summary' => $meta['summary']
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -59,21 +122,32 @@ function scanDirectory($dir, $baseDir, $search) {
|
|||||||
|
|
||||||
$documents = scanDirectory($docDir, $docDir, $search);
|
$documents = scanDirectory($docDir, $docDir, $search);
|
||||||
|
|
||||||
// 按文件夹分组,然后按修改时间排序
|
// 按日期排序(新文件在前)
|
||||||
usort($documents, function($a, $b) {
|
usort($documents, function($a, $b) {
|
||||||
// 先按文件夹排序
|
// 优先按日期排序
|
||||||
if ($a['folder'] !== $b['folder']) {
|
if ($a['date'] && $b['date']) {
|
||||||
return strcmp($a['folder'], $b['folder']);
|
return strcmp($b['date'], $a['date']);
|
||||||
}
|
}
|
||||||
// 同一文件夹按修改时间排序(新文件在前)
|
// 其次按修改时间
|
||||||
return $b['mtime'] - $a['mtime'];
|
return $b['mtime'] - $a['mtime'];
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// 平台图标映射
|
||||||
|
function getPlatformIcon($platform) {
|
||||||
|
$icons = [
|
||||||
|
'bilibili' => '📺',
|
||||||
|
'youtube' => '▶️',
|
||||||
|
'zhihu' => '💡',
|
||||||
|
'wechat' => '💬',
|
||||||
|
];
|
||||||
|
return $icons[$platform] ?? '📄';
|
||||||
|
}
|
||||||
?>
|
?>
|
||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html lang="zh-CN">
|
<html lang="zh-CN">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
<meta name="viewport" content="width=device=device-width, initial-scale=1.0">
|
||||||
<title>知识库 - <?php echo htmlspecialchars($username); ?></title>
|
<title>知识库 - <?php echo htmlspecialchars($username); ?></title>
|
||||||
<link rel="stylesheet" href="style.css">
|
<link rel="stylesheet" href="style.css">
|
||||||
</head>
|
</head>
|
||||||
@@ -115,34 +189,26 @@ usort($documents, function($a, $b) {
|
|||||||
<?php endif; ?>
|
<?php endif; ?>
|
||||||
</div>
|
</div>
|
||||||
<?php else: ?>
|
<?php else: ?>
|
||||||
<?php
|
<?php foreach ($documents as $doc): ?>
|
||||||
$currentFolder = '';
|
|
||||||
foreach ($documents as $doc):
|
|
||||||
// 显示文件夹分隔
|
|
||||||
if ($doc['folder'] !== $currentFolder):
|
|
||||||
$currentFolder = $doc['folder'];
|
|
||||||
if ($currentFolder):
|
|
||||||
?>
|
|
||||||
<div class="folder-separator">
|
|
||||||
📁 <?php echo htmlspecialchars($currentFolder); ?>
|
|
||||||
</div>
|
|
||||||
<?php
|
|
||||||
endif;
|
|
||||||
endif;
|
|
||||||
?>
|
|
||||||
<a href="view.php?file=<?php echo urlencode($doc['path']); ?><?php echo $search ? '&q=' . urlencode($search) : ''; ?>"
|
<a href="view.php?file=<?php echo urlencode($doc['path']); ?><?php echo $search ? '&q=' . urlencode($search) : ''; ?>"
|
||||||
class="document-card">
|
class="document-card">
|
||||||
<div class="doc-icon">📄</div>
|
<div class="doc-icon"><?php echo getPlatformIcon($doc['platform']); ?></div>
|
||||||
<div class="doc-info">
|
<div class="doc-info">
|
||||||
<div class="doc-name"><?php echo htmlspecialchars($doc['name']); ?></div>
|
<div class="doc-title"><?php echo htmlspecialchars($doc['title']); ?></div>
|
||||||
<div class="doc-meta">
|
<div class="doc-meta">
|
||||||
<?php echo round($doc['size'] / 1024, 1); ?> KB ·
|
<?php if ($doc['author']): ?>
|
||||||
<?php echo date('Y-m-d H:i', $doc['mtime']); ?>
|
<span class="meta-author">👤 <?php echo htmlspecialchars($doc['author']); ?></span>
|
||||||
<?php if ($doc['folder']): ?>
|
<?php endif; ?>
|
||||||
· 📁 <?php echo htmlspecialchars($doc['folder']); ?>
|
<?php if ($doc['date']): ?>
|
||||||
|
<span class="meta-date">📅 <?php echo htmlspecialchars($doc['date']); ?></span>
|
||||||
|
<?php endif; ?>
|
||||||
|
<?php if ($doc['platform']): ?>
|
||||||
|
<span class="meta-platform">🏷️ <?php echo htmlspecialchars($doc['platform']); ?></span>
|
||||||
<?php endif; ?>
|
<?php endif; ?>
|
||||||
</div>
|
</div>
|
||||||
<div class="doc-preview"><?php echo htmlspecialchars($doc['preview']); ?>...</div>
|
<?php if ($doc['summary']): ?>
|
||||||
|
<div class="doc-summary"><?php echo htmlspecialchars(mb_substr($doc['summary'], 0, 150)); ?><?php echo mb_strlen($doc['summary']) > 150 ? '...' : ''; ?></div>
|
||||||
|
<?php endif; ?>
|
||||||
</div>
|
</div>
|
||||||
</a>
|
</a>
|
||||||
<?php endforeach; ?>
|
<?php endforeach; ?>
|
||||||
|
|||||||
@@ -542,33 +542,45 @@ body::after {
|
|||||||
z-index: 1;
|
z-index: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
.doc-name {
|
.doc-title {
|
||||||
font-size: 16px;
|
font-size: 17px;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
color: var(--text-primary);
|
color: var(--text-primary);
|
||||||
margin-bottom: 6px;
|
margin-bottom: 8px;
|
||||||
font-family: 'JetBrains Mono', monospace;
|
line-height: 1.4;
|
||||||
}
|
}
|
||||||
|
|
||||||
.doc-meta {
|
.doc-meta {
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
color: var(--text-muted);
|
color: var(--text-muted);
|
||||||
margin-bottom: 8px;
|
margin-bottom: 10px;
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 12px;
|
||||||
font-family: 'JetBrains Mono', monospace;
|
font-family: 'JetBrains Mono', monospace;
|
||||||
}
|
}
|
||||||
|
|
||||||
.doc-meta .size,
|
.doc-meta .meta-author,
|
||||||
.doc-meta .date {
|
.doc-meta .meta-date,
|
||||||
opacity: 0.8;
|
.doc-meta .meta-platform {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.doc-summary {
|
||||||
|
font-size: 13px;
|
||||||
|
color: var(--text-secondary);
|
||||||
|
line-height: 1.6;
|
||||||
|
opacity: 0.85;
|
||||||
|
}
|
||||||
|
|
||||||
|
.doc-name {
|
||||||
|
display: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.doc-preview {
|
.doc-preview {
|
||||||
font-size: 13px;
|
display: none;
|
||||||
color: var(--text-secondary);
|
|
||||||
overflow: hidden;
|
|
||||||
text-overflow: ellipsis;
|
|
||||||
white-space: nowrap;
|
|
||||||
line-height: 1.5;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.empty {
|
.empty {
|
||||||
|
|||||||
@@ -10,14 +10,13 @@ $docDir = getUserDocDir($username);
|
|||||||
$file = $_GET['file'] ?? '';
|
$file = $_GET['file'] ?? '';
|
||||||
$search = $_GET['q'] ?? '';
|
$search = $_GET['q'] ?? '';
|
||||||
|
|
||||||
// 安全检查:防止目录遍历攻击(允许子目录,但不能跳出用户目录)
|
// 安全检查
|
||||||
if (empty($file) || strpos($file, '..') !== false) {
|
if (empty($file) || strpos($file, '..') !== false) {
|
||||||
die('无效的文件名');
|
die('无效的文件名');
|
||||||
}
|
}
|
||||||
|
|
||||||
$filePath = $docDir . '/' . $file;
|
$filePath = $docDir . '/' . $file;
|
||||||
|
|
||||||
// 确保文件在用户目录内
|
|
||||||
$realPath = realpath($filePath);
|
$realPath = realpath($filePath);
|
||||||
$realDocDir = realpath($docDir);
|
$realDocDir = realpath($docDir);
|
||||||
if ($realPath === false || strpos($realPath, $realDocDir) !== 0) {
|
if ($realPath === false || strpos($realPath, $realDocDir) !== 0) {
|
||||||
@@ -30,9 +29,40 @@ if (!file_exists($filePath)) {
|
|||||||
|
|
||||||
$content = file_get_contents($filePath);
|
$content = file_get_contents($filePath);
|
||||||
|
|
||||||
|
// 解析元数据
|
||||||
|
$title = basename($file);
|
||||||
|
$meta = [
|
||||||
|
'title' => $title,
|
||||||
|
'author' => '',
|
||||||
|
'date' => '',
|
||||||
|
'platform' => ''
|
||||||
|
];
|
||||||
|
|
||||||
|
// 提取并移除 YAML front matter
|
||||||
|
if (preg_match('/^---\s*\n(.*?)\n---\s*\n(.*)$/s', $content, $matches)) {
|
||||||
|
$yaml = $matches[1];
|
||||||
|
$content = $matches[2];
|
||||||
|
|
||||||
|
// 解析元数据(用于页面标题)
|
||||||
|
if (preg_match('/^title:\s*["\']?(.+?)["\']?\s*$/m', $yaml, $m)) {
|
||||||
|
$title = trim($m[1], '"\'');
|
||||||
|
$meta['title'] = $title;
|
||||||
|
}
|
||||||
|
if (preg_match('/^author:\s*(.+?)\s*$/m', $yaml, $m)) {
|
||||||
|
$meta['author'] = trim($m[1]);
|
||||||
|
}
|
||||||
|
if (preg_match('/^date:\s*(.+?)\s*$/m', $yaml, $m)) {
|
||||||
|
$meta['date'] = trim($m[1]);
|
||||||
|
}
|
||||||
|
if (preg_match('/^platform:\s*(.+?)\s*$/m', $yaml, $m)) {
|
||||||
|
$meta['platform'] = trim($m[1]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// 简单的 Markdown 转换
|
// 简单的 Markdown 转换
|
||||||
function markdownToHtml($text) {
|
function markdownToHtml($text) {
|
||||||
// 标题
|
// 标题
|
||||||
|
$text = preg_replace('/^#### (.*$)/m', '<h4>$1</h4>', $text);
|
||||||
$text = preg_replace('/^### (.*$)/m', '<h3>$1</h3>', $text);
|
$text = preg_replace('/^### (.*$)/m', '<h3>$1</h3>', $text);
|
||||||
$text = preg_replace('/^## (.*$)/m', '<h2>$1</h2>', $text);
|
$text = preg_replace('/^## (.*$)/m', '<h2>$1</h2>', $text);
|
||||||
$text = preg_replace('/^# (.*$)/m', '<h1>$1</h1>', $text);
|
$text = preg_replace('/^# (.*$)/m', '<h1>$1</h1>', $text);
|
||||||
@@ -71,8 +101,6 @@ if ($search) {
|
|||||||
$htmlContent = highlightSearch($htmlContent, $search);
|
$htmlContent = highlightSearch($htmlContent, $search);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 获取文件名(不含路径)
|
|
||||||
$displayName = basename($file);
|
|
||||||
$folder = dirname($file) !== '.' ? dirname($file) : '';
|
$folder = dirname($file) !== '.' ? dirname($file) : '';
|
||||||
?>
|
?>
|
||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
@@ -80,7 +108,7 @@ $folder = dirname($file) !== '.' ? dirname($file) : '';
|
|||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
<title><?php echo htmlspecialchars($displayName); ?> - 知识库</title>
|
<title><?php echo htmlspecialchars($title); ?> - 知识库</title>
|
||||||
<link rel="stylesheet" href="style.css">
|
<link rel="stylesheet" href="style.css">
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
@@ -89,7 +117,7 @@ $folder = dirname($file) !== '.' ? dirname($file) : '';
|
|||||||
<a href="dashboard.php<?php echo $search ? '?q=' . urlencode($search) : ''; ?>" class="btn-back">
|
<a href="dashboard.php<?php echo $search ? '?q=' . urlencode($search) : ''; ?>" class="btn-back">
|
||||||
← 返回列表
|
← 返回列表
|
||||||
</a>
|
</a>
|
||||||
<h1><?php echo htmlspecialchars($displayName); ?></h1>
|
<h1><?php echo htmlspecialchars($title); ?></h1>
|
||||||
<?php if ($folder): ?>
|
<?php if ($folder): ?>
|
||||||
<div class="folder-info">📁 <?php echo htmlspecialchars($folder); ?></div>
|
<div class="folder-info">📁 <?php echo htmlspecialchars($folder); ?></div>
|
||||||
<?php endif; ?>
|
<?php endif; ?>
|
||||||
|
|||||||
Reference in New Issue
Block a user