2025-03-03 21:16:16 +08:00
|
|
|
|
---
|
|
|
|
|
import { getCollection } from 'astro:content';
|
|
|
|
|
import type { CollectionEntry } from 'astro:content';
|
2025-03-10 13:56:56 +08:00
|
|
|
|
import { contentStructure } from '../../content.config';
|
2025-03-03 21:16:16 +08:00
|
|
|
|
import Layout from '@/components/Layout.astro';
|
|
|
|
|
import Breadcrumb from '@/components/Breadcrumb.astro';
|
2025-03-09 14:37:44 +08:00
|
|
|
|
|
|
|
|
|
// 启用静态预渲染
|
|
|
|
|
export const prerender = true;
|
|
|
|
|
|
2025-03-08 18:16:42 +08:00
|
|
|
|
export function extractSummary(content: string, length = 150) {
|
|
|
|
|
// 移除 Markdown 标记
|
|
|
|
|
const plainText = content
|
|
|
|
|
.replace(/---[\s\S]*?---/, '') // 移除 frontmatter
|
|
|
|
|
.replace(/\[([^\]]+)\]\([^)]+\)/g, '$1') // 将链接转换为纯文本
|
|
|
|
|
.replace(/[#*`~>]/g, '') // 移除特殊字符
|
|
|
|
|
.replace(/\n+/g, ' ') // 将换行转换为空格
|
|
|
|
|
.trim();
|
|
|
|
|
|
|
|
|
|
// 提取指定长度的文本
|
|
|
|
|
return plainText.length > length
|
|
|
|
|
? plainText.slice(0, length).trim() + '...'
|
|
|
|
|
: plainText;
|
|
|
|
|
}
|
2025-03-03 21:16:16 +08:00
|
|
|
|
|
2025-03-09 14:37:44 +08:00
|
|
|
|
// 生成所有可能的静态路径
|
|
|
|
|
export async function getStaticPaths() {
|
|
|
|
|
const articles = await getCollection('articles');
|
|
|
|
|
const { sections } = contentStructure;
|
|
|
|
|
const allTags = articles.flatMap(article => article.data.tags || []);
|
|
|
|
|
const tags = [...new Set(allTags)].sort();
|
|
|
|
|
const views = ['grid', 'timeline'];
|
|
|
|
|
|
|
|
|
|
// 生成所有可能的路径组合
|
|
|
|
|
const paths = [];
|
|
|
|
|
|
|
|
|
|
// 1. 默认路径(无参数)
|
|
|
|
|
paths.push({ params: { path: undefined }, props: { path: '', tag: '', view: 'grid' } });
|
|
|
|
|
|
|
|
|
|
// 2. 标签路径
|
|
|
|
|
for (const tag of tags) {
|
|
|
|
|
// 标签主页
|
|
|
|
|
paths.push({
|
|
|
|
|
params: { tag },
|
|
|
|
|
props: { path: '', tag, view: 'grid' }
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// 标签视图页
|
|
|
|
|
for (const view of views) {
|
|
|
|
|
paths.push({
|
|
|
|
|
params: { tag, view },
|
|
|
|
|
props: { path: '', tag, view }
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 3. 目录路径
|
|
|
|
|
function addSectionPaths(section: any, currentPath = '') {
|
|
|
|
|
const sectionPath = currentPath ? `${currentPath}/${section.name}` : section.name;
|
|
|
|
|
|
|
|
|
|
// 添加当前目录的路径(不带 view 参数)
|
|
|
|
|
paths.push({
|
|
|
|
|
params: { path: sectionPath },
|
|
|
|
|
props: { path: sectionPath, tag: '', view: 'grid' }
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// 添加当前目录的视图路径
|
|
|
|
|
for (const view of views) {
|
|
|
|
|
paths.push({
|
|
|
|
|
params: { path: sectionPath, view },
|
|
|
|
|
props: { path: sectionPath, tag: '', view }
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 递归添加子目录的路径
|
|
|
|
|
for (const subSection of section.sections) {
|
|
|
|
|
addSectionPaths(subSection, sectionPath);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
for (const section of sections) {
|
|
|
|
|
addSectionPaths(section);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 4. 添加所有可能的目录路径(不带 view 参数)
|
|
|
|
|
function addAllPossiblePaths(section: any, currentPath = '') {
|
|
|
|
|
const sectionPath = currentPath ? `${currentPath}/${section.name}` : section.name;
|
|
|
|
|
|
|
|
|
|
// 添加当前目录的路径
|
|
|
|
|
paths.push({
|
|
|
|
|
params: { path: sectionPath },
|
|
|
|
|
props: { path: sectionPath, tag: '', view: 'grid' }
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// 递归添加子目录的路径
|
|
|
|
|
for (const subSection of section.sections) {
|
|
|
|
|
addAllPossiblePaths(subSection, sectionPath);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
for (const section of sections) {
|
|
|
|
|
addAllPossiblePaths(section);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return paths;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const { path = '', tag = '', view = 'grid' } = Astro.props;
|
|
|
|
|
const pathSegments = path ? path.split('/') : [];
|
2025-03-03 21:16:16 +08:00
|
|
|
|
|
|
|
|
|
// 获取所有文章,并按日期排序
|
|
|
|
|
const articles: CollectionEntry<'articles'>[] = await getCollection('articles');
|
|
|
|
|
const sortedArticles = articles.sort(
|
|
|
|
|
(a, b) => b.data.date.getTime() - a.data.date.getTime()
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
// 获取所有标签
|
|
|
|
|
const allTags = articles.flatMap(article => article.data.tags || []);
|
|
|
|
|
const tags = [...new Set(allTags)].sort();
|
|
|
|
|
|
|
|
|
|
// 获取内容结构
|
|
|
|
|
const { sections } = contentStructure;
|
|
|
|
|
|
|
|
|
|
// 获取标签参数
|
2025-03-09 14:37:44 +08:00
|
|
|
|
const tagFilter = tag;
|
2025-03-03 21:16:16 +08:00
|
|
|
|
|
|
|
|
|
// 获取视图模式参数
|
2025-03-09 14:37:44 +08:00
|
|
|
|
const viewMode = view;
|
2025-03-03 21:16:16 +08:00
|
|
|
|
|
|
|
|
|
// 根据路径获取当前目录
|
|
|
|
|
function getCurrentSection(pathSegments: string[]) {
|
|
|
|
|
// 过滤掉空字符串
|
|
|
|
|
const filteredSegments = pathSegments.filter(segment => segment.trim() !== '');
|
|
|
|
|
|
|
|
|
|
if (filteredSegments.length === 0) {
|
|
|
|
|
return { sections, articles: contentStructure.articles, currentPath: '' };
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
let currentSections = sections;
|
|
|
|
|
let currentPath = '';
|
|
|
|
|
let currentArticles: string[] = [];
|
|
|
|
|
|
|
|
|
|
// 遍历路径段,逐级查找
|
|
|
|
|
for (let i = 0; i < filteredSegments.length; i++) {
|
|
|
|
|
const segment = filteredSegments[i];
|
|
|
|
|
|
|
|
|
|
// 查找当前段对应的目录
|
|
|
|
|
const foundSection = currentSections.find(s => s.name === segment);
|
|
|
|
|
|
|
|
|
|
if (!foundSection) {
|
|
|
|
|
return { sections: [], articles: [], currentPath: '' };
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 更新当前路径
|
|
|
|
|
currentPath = currentPath ? `${currentPath}/${segment}` : segment;
|
|
|
|
|
|
|
|
|
|
// 如果是最后一个段,返回该目录的内容
|
|
|
|
|
if (i === filteredSegments.length - 1) {
|
|
|
|
|
return {
|
|
|
|
|
sections: foundSection.sections,
|
|
|
|
|
articles: foundSection.articles,
|
|
|
|
|
currentPath
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 否则继续向下查找
|
|
|
|
|
currentSections = foundSection.sections;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 默认返回空
|
|
|
|
|
return { sections: [], articles: [], currentPath: '' };
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 获取当前目录内容
|
|
|
|
|
const { sections: currentSections, articles: currentArticles, currentPath } = getCurrentSection(pathSegments);
|
|
|
|
|
|
|
|
|
|
// 如果有标签过滤,则过滤文章
|
|
|
|
|
let filteredArticles = sortedArticles;
|
|
|
|
|
let pageTitle = currentPath ? currentPath : '文章列表';
|
|
|
|
|
|
|
|
|
|
if (tagFilter) {
|
|
|
|
|
filteredArticles = sortedArticles.filter(article =>
|
|
|
|
|
article.data.tags && article.data.tags.includes(tagFilter)
|
|
|
|
|
);
|
|
|
|
|
pageTitle = `标签: ${tagFilter}`;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 获取面包屑导航
|
|
|
|
|
interface Breadcrumb {
|
|
|
|
|
name: string;
|
|
|
|
|
path: string;
|
|
|
|
|
}
|
|
|
|
|
|
2025-03-09 01:11:43 +08:00
|
|
|
|
// 处理特殊ID的函数
|
|
|
|
|
function getArticleUrl(articleId: string) {
|
2025-03-10 14:05:40 +08:00
|
|
|
|
return `/articles/${articleId}${view ? `/${view}` : ''}`;
|
2025-03-09 01:11:43 +08:00
|
|
|
|
}
|
2025-03-03 21:16:16 +08:00
|
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
|
|
|
|
<Layout>
|
2025-03-08 18:16:42 +08:00
|
|
|
|
<div class="bg-gray-50 dark:bg-dark-bg min-h-screen">
|
|
|
|
|
<main class={`mx-auto px-4 sm:px-6 lg:px-8 py-6 ${viewMode === 'grid' ? 'max-w-7xl' : 'max-w-5xl'}`}>
|
2025-03-03 21:16:16 +08:00
|
|
|
|
<!-- 导航栏 -->
|
2025-03-08 18:16:42 +08:00
|
|
|
|
<div class="bg-white dark:bg-gray-800 rounded-xl mb-4 shadow-lg border border-gray-200 dark:border-gray-700">
|
2025-03-03 21:16:16 +08:00
|
|
|
|
<div class="px-4 py-3">
|
|
|
|
|
<div class="flex items-center justify-between !h-10">
|
|
|
|
|
<Breadcrumb
|
|
|
|
|
pageType="articles"
|
|
|
|
|
pathSegments={pathSegments}
|
|
|
|
|
tagFilter={tagFilter}
|
|
|
|
|
/>
|
|
|
|
|
|
|
|
|
|
<!-- 视图切换按钮 -->
|
|
|
|
|
<div class="flex items-center gap-px">
|
2025-03-09 14:37:44 +08:00
|
|
|
|
<a href={`/articles/${path}${tag ? `/tag/${tag}` : ''}/grid`}
|
2025-03-03 21:16:16 +08:00
|
|
|
|
class={`px-3 py-1.5 transition-colors flex items-center gap-1 ${
|
|
|
|
|
viewMode === 'grid'
|
|
|
|
|
? 'text-primary-600'
|
|
|
|
|
: 'text-gray-400 hover:text-gray-500'
|
|
|
|
|
}`}>
|
|
|
|
|
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
|
|
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 6a2 2 0 012-2h2a2 2 0 012 2v2a2 2 0 01-2 2H6a2 2 0 01-2-2V6zM14 6a2 2 0 012-2h2a2 2 0 012 2v2a2 2 0 01-2 2h-2a2 2 0 01-2-2V6zM4 16a2 2 0 012-2h2a2 2 0 012 2v2a2 2 0 01-2 2H6a2 2 0 01-2-2v-2zM14 16a2 2 0 012-2h2a2 2 0 012 2v2a2 2 0 01-2 2h-2a2 2 0 01-2-2v-2z" />
|
|
|
|
|
</svg>
|
|
|
|
|
</a>
|
2025-03-09 14:37:44 +08:00
|
|
|
|
<a href={`/articles/${path}${tag ? `/tag/${tag}` : ''}/timeline`}
|
2025-03-03 21:16:16 +08:00
|
|
|
|
class={`px-3 py-1.5 transition-colors flex items-center gap-1 ${
|
|
|
|
|
viewMode === 'timeline'
|
|
|
|
|
? 'text-primary-600'
|
|
|
|
|
: 'text-gray-400 hover:text-gray-500'
|
|
|
|
|
}`}>
|
|
|
|
|
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
|
|
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 20H5a2 2 0 01-2-2V6a2 2 0 012-2h10a2 2 0 012 2v1m2 13a2 2 0 01-2-2V7m2 13a2 2 0 002-2V9a2 2 0 00-2-2h-2m-4-3H9M7 16h6M7 8h6v4H7V8z" />
|
|
|
|
|
</svg>
|
|
|
|
|
</a>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
{viewMode === 'grid' ? (
|
|
|
|
|
<>
|
|
|
|
|
<!-- 内容卡片网格 -->
|
|
|
|
|
<div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-6 mb-12">
|
|
|
|
|
{/* 上一级目录卡片 - 仅在浏览目录时显示 */}
|
|
|
|
|
{!tagFilter && pathSegments.length > 0 && (
|
2025-03-10 14:05:40 +08:00
|
|
|
|
<a href={`/articles/${pathSegments.length > 1 ? pathSegments.slice(0, -1).join('/') : ''}/${view}`}
|
2025-03-08 18:16:42 +08:00
|
|
|
|
class="group flex flex-col h-full p-5 border border-gray-200 dark:border-gray-700 rounded-xl bg-white dark:bg-gray-800 hover:shadow-xl hover:-translate-y-1 transition-all duration-300 shadow-lg">
|
2025-03-03 21:16:16 +08:00
|
|
|
|
<div class="flex items-center">
|
|
|
|
|
<div class="w-10 h-10 flex items-center justify-center rounded-lg bg-primary-100 text-primary-600 group-hover:bg-primary-200 transition-colors">
|
|
|
|
|
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
|
|
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M11 17l-5-5m0 0l5-5m-5 5h12" />
|
|
|
|
|
</svg>
|
|
|
|
|
</div>
|
|
|
|
|
<div class="ml-3 flex-1">
|
|
|
|
|
<div class="font-bold text-base text-gray-800 dark:text-gray-100 group-hover:text-primary-700 dark:group-hover:text-primary-300 transition-colors">返回上级目录</div>
|
|
|
|
|
<div class="text-xs text-gray-500">返回上一级</div>
|
|
|
|
|
</div>
|
|
|
|
|
<div class="text-primary-500 opacity-0 group-hover:opacity-100 transition-opacity">
|
|
|
|
|
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" viewBox="0 0 20 20" fill="currentColor">
|
|
|
|
|
<path fill-rule="evenodd" d="M12.707 5.293a1 1 0 010 1.414L9.414 10l3.293 3.293a1 1 0 01-1.414 1.414l-4-4a1 1 0 010-1.414l4-4a1 1 0 011.414 0z" clip-rule="evenodd" />
|
|
|
|
|
</svg>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</a>
|
|
|
|
|
)}
|
|
|
|
|
|
|
|
|
|
{/* 目录卡片 - 仅在浏览目录时显示 */}
|
|
|
|
|
{!tagFilter && currentSections.map(section => {
|
|
|
|
|
// 确保目录链接正确生成
|
|
|
|
|
const dirLink = currentPath ? `${currentPath}/${section.name}` : section.name;
|
|
|
|
|
|
|
|
|
|
return (
|
2025-03-09 14:37:44 +08:00
|
|
|
|
<a href={`/articles/${dirLink}`}
|
2025-03-08 18:16:42 +08:00
|
|
|
|
class="group flex flex-col h-full p-5 border border-gray-200 dark:border-gray-700 rounded-xl bg-white dark:bg-gray-800 hover:shadow-xl hover:-translate-y-1 transition-all duration-300 shadow-lg">
|
2025-03-03 21:16:16 +08:00
|
|
|
|
<div class="flex items-center">
|
|
|
|
|
<div class="w-10 h-10 flex items-center justify-center rounded-lg bg-primary-100 text-primary-600 group-hover:bg-primary-200 transition-colors">
|
|
|
|
|
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
|
|
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 19a2 2 0 01-2-2V7a2 2 0 012-2h4l2 2h4a2 2 0 012 2v1M5 19h14a2 2 0 002-2v-5a2 2 0 00-2-2H9a2 2 0 00-2 2v5a2 2 0 01-2 2z" />
|
|
|
|
|
</svg>
|
|
|
|
|
</div>
|
|
|
|
|
<div class="ml-3 flex-1">
|
|
|
|
|
<div class="font-bold text-base text-gray-800 dark:text-gray-100 group-hover:text-primary-700 dark:group-hover:text-primary-300 transition-colors line-clamp-1">{section.name}</div>
|
|
|
|
|
<div class="text-xs text-gray-500 flex items-center mt-1">
|
|
|
|
|
{section.sections.length > 0 && (
|
|
|
|
|
<span class="flex items-center mr-3">
|
|
|
|
|
<svg xmlns="http://www.w3.org/2000/svg" class="h-3 w-3 mr-1" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
|
|
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 7v10a2 2 0 002 2h14a2 2 0 002-2V9a2 2 0 00-2-2h-6l-2-2H5a2 2 0 00-2 2z" />
|
|
|
|
|
</svg>
|
|
|
|
|
{section.sections.length} 个子目录
|
|
|
|
|
</span>
|
|
|
|
|
)}
|
|
|
|
|
{section.articles.length > 0 && (
|
|
|
|
|
<span class="flex items-center">
|
|
|
|
|
<svg xmlns="http://www.w3.org/2000/svg" class="h-3 w-3 mr-1" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
|
|
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" />
|
|
|
|
|
</svg>
|
|
|
|
|
{section.articles.length} 篇文章
|
|
|
|
|
</span>
|
|
|
|
|
)}
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
<div class="text-primary-500 opacity-0 group-hover:opacity-100 transition-opacity">
|
|
|
|
|
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" viewBox="0 0 20 20" fill="currentColor">
|
|
|
|
|
<path fill-rule="evenodd" d="M7.293 14.707a1 1 0 010-1.414L10.586 10 7.293 6.707a1 1 0 011.414-1.414l4 4a1 1 0 010 1.414l-4 4a1 1 0 01-1.414 0z" clip-rule="evenodd" />
|
|
|
|
|
</svg>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</a>
|
|
|
|
|
);
|
|
|
|
|
})}
|
|
|
|
|
|
|
|
|
|
{/* 文章卡片 - 根据是否有标签过滤显示不同内容 */}
|
|
|
|
|
{tagFilter ? (
|
|
|
|
|
// 显示标签过滤后的文章
|
|
|
|
|
filteredArticles.map(article => (
|
2025-03-09 01:11:43 +08:00
|
|
|
|
<a href={getArticleUrl(article.id)}
|
2025-03-08 18:16:42 +08:00
|
|
|
|
class="group flex flex-col h-full p-5 border border-gray-200 dark:border-gray-700 rounded-xl bg-white dark:bg-gray-800 hover:shadow-xl hover:-translate-y-1 transition-all duration-300 shadow-lg">
|
2025-03-03 21:16:16 +08:00
|
|
|
|
<div class="flex items-start">
|
|
|
|
|
<div class="w-10 h-10 flex-shrink-0 flex items-center justify-center rounded-lg bg-primary-100 text-primary-600 group-hover:bg-primary-200 transition-colors">
|
|
|
|
|
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
|
|
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 20H5a2 2 0 01-2-2V6a2 2 0 012-2h10a2 2 0 012 2v1m2 13a2 2 0 01-2-2V7m2 13a2 2 0 002-2V9a2 2 0 00-2-2h-2m-4-3H9M7 16h6M7 8h6v4H7V8z" />
|
|
|
|
|
</svg>
|
|
|
|
|
</div>
|
|
|
|
|
<div class="ml-3 flex-1">
|
|
|
|
|
<h3 class="font-bold text-base text-gray-800 dark:text-gray-100 group-hover:text-primary-700 dark:group-hover:text-primary-300 transition-colors line-clamp-2">{article.data.title}</h3>
|
2025-03-08 18:16:42 +08:00
|
|
|
|
{article.body && (
|
|
|
|
|
<p class="text-xs text-gray-600 mt-1 line-clamp-2">
|
|
|
|
|
{extractSummary(article.body)}
|
|
|
|
|
</p>
|
2025-03-03 21:16:16 +08:00
|
|
|
|
)}
|
|
|
|
|
<div class="text-xs text-gray-500 mt-2 flex items-center justify-between">
|
|
|
|
|
<time datetime={article.data.date.toISOString()}>
|
|
|
|
|
{article.data.date.toLocaleDateString('zh-CN', {year: 'numeric', month: 'long', day: 'numeric'})}
|
|
|
|
|
</time>
|
|
|
|
|
<span class="text-primary-600 font-medium">阅读全文</span>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</a>
|
|
|
|
|
))
|
|
|
|
|
) : (
|
|
|
|
|
// 显示当前目录的文章
|
|
|
|
|
currentArticles.map(articlePath => {
|
2025-03-10 13:56:56 +08:00
|
|
|
|
// 获取文章ID - 不需要移除src/content前缀,因为contentStructure中已经是相对路径
|
|
|
|
|
const articleId = articlePath;
|
2025-03-03 21:16:16 +08:00
|
|
|
|
|
2025-03-10 13:56:56 +08:00
|
|
|
|
// 尝试匹配文章
|
|
|
|
|
const article = articles.find(a => a.id === articleId);
|
2025-03-03 21:16:16 +08:00
|
|
|
|
|
|
|
|
|
if (!article) {
|
|
|
|
|
return (
|
2025-03-08 18:16:42 +08:00
|
|
|
|
<div class="flex flex-col h-full p-5 border border-red-200 rounded-xl bg-red-50 shadow-lg">
|
2025-03-03 21:16:16 +08:00
|
|
|
|
<div class="flex items-start">
|
|
|
|
|
<div class="w-10 h-10 flex-shrink-0 flex items-center justify-center rounded-lg bg-red-100 text-red-600">
|
|
|
|
|
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
|
|
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z" />
|
|
|
|
|
</svg>
|
|
|
|
|
</div>
|
|
|
|
|
<div class="ml-3 flex-1">
|
|
|
|
|
<h3 class="font-bold text-base text-red-800">文章不存在</h3>
|
2025-03-10 13:56:56 +08:00
|
|
|
|
<p class="text-xs text-red-600 mt-1">
|
|
|
|
|
<div>原始路径: {articlePath}</div>
|
|
|
|
|
<div>文章ID: {articleId}</div>
|
|
|
|
|
<div>当前目录: {currentPath}</div>
|
|
|
|
|
</p>
|
|
|
|
|
<div class="text-xs text-red-500 mt-2">
|
|
|
|
|
<div>可用的文章ID:</div>
|
|
|
|
|
<div class="line-clamp-3">{articles.map(a => a.id).join(', ')}</div>
|
2025-03-03 21:16:16 +08:00
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return (
|
2025-03-10 13:56:56 +08:00
|
|
|
|
<a href={`/articles/${article.id}`}
|
2025-03-08 18:16:42 +08:00
|
|
|
|
class="group flex flex-col h-full p-5 border border-gray-200 dark:border-gray-700 rounded-xl bg-white dark:bg-gray-800 hover:shadow-xl hover:-translate-y-1 transition-all duration-300 shadow-lg">
|
2025-03-03 21:16:16 +08:00
|
|
|
|
<div class="flex items-start">
|
|
|
|
|
<div class="w-10 h-10 flex-shrink-0 flex items-center justify-center rounded-lg bg-primary-100 text-primary-600 group-hover:bg-primary-200 transition-colors">
|
|
|
|
|
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
|
|
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 20H5a2 2 0 01-2-2V6a2 2 0 012-2h10a2 2 0 012 2v1m2 13a2 2 0 01-2-2V7m2 13a2 2 0 002-2V9a2 2 0 00-2-2h-2m-4-3H9M7 16h6M7 8h6v4H7V8z" />
|
|
|
|
|
</svg>
|
|
|
|
|
</div>
|
|
|
|
|
<div class="ml-3 flex-1">
|
|
|
|
|
<h3 class="font-bold text-base text-gray-800 dark:text-gray-100 group-hover:text-primary-700 dark:group-hover:text-primary-300 transition-colors line-clamp-2">{article.data.title}</h3>
|
2025-03-08 18:16:42 +08:00
|
|
|
|
{article.body && (
|
|
|
|
|
<p class="text-xs text-gray-600 mt-1 line-clamp-2">
|
|
|
|
|
{extractSummary(article.body)}
|
|
|
|
|
</p>
|
2025-03-03 21:16:16 +08:00
|
|
|
|
)}
|
|
|
|
|
<div class="text-xs text-gray-500 mt-2 flex items-center justify-between">
|
|
|
|
|
<time datetime={article.data.date.toISOString()}>
|
|
|
|
|
{article.data.date.toLocaleDateString('zh-CN', {year: 'numeric', month: 'long', day: 'numeric'})}
|
|
|
|
|
</time>
|
|
|
|
|
<span class="text-primary-600 font-medium">阅读全文</span>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</a>
|
|
|
|
|
);
|
|
|
|
|
})
|
|
|
|
|
)}
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
{/* 空内容提示 */}
|
|
|
|
|
{((tagFilter && filteredArticles.length === 0) || (!tagFilter && currentSections.length === 0 && currentArticles.length === 0)) && (
|
2025-03-08 18:16:42 +08:00
|
|
|
|
<div class="text-center py-16 bg-white rounded-xl shadow-lg border border-gray-200 dark:border-gray-700 mb-12">
|
2025-03-03 21:16:16 +08:00
|
|
|
|
<svg xmlns="http://www.w3.org/2000/svg" class="h-16 w-16 mx-auto text-primary-200 mb-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
|
|
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M20 13V6a2 2 0 00-2-2H6a2 2 0 00-2 2v7m16 0v5a2 2 0 01-2 2H6a2 2 0 01-2-2v-5m16 0h-2.586a1 1 0 00-.707.293l-2.414 2.414a1 1 0 01-.707.293h-3.172a1 1 0 01-.707-.293l-2.414-2.414A1 1 0 006.586 13H4" />
|
|
|
|
|
</svg>
|
|
|
|
|
<h3 class="text-2xl font-bold text-gray-700 mb-2">
|
|
|
|
|
{tagFilter ? `没有找到标签为 "${tagFilter}" 的文章` : '此目录为空'}
|
|
|
|
|
</h3>
|
|
|
|
|
<p class="text-gray-500 max-w-md mx-auto">
|
|
|
|
|
{tagFilter ? '请尝试其他标签或返回文章列表' : '此目录下暂无内容,请浏览其他目录或返回上一级'}
|
|
|
|
|
</p>
|
|
|
|
|
</div>
|
|
|
|
|
)}
|
|
|
|
|
|
|
|
|
|
<!-- 标签过滤器 -->
|
2025-03-08 18:16:42 +08:00
|
|
|
|
<div class="bg-white dark:bg-gray-800 p-8 rounded-xl shadow-lg border border-gray-200 dark:border-gray-700">
|
2025-03-03 21:16:16 +08:00
|
|
|
|
<h2 class="text-2xl font-bold mb-6 text-primary-900 dark:text-primary-100 flex items-center">
|
|
|
|
|
<svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6 mr-2" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
|
|
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M7 7h.01M7 3h5c.512 0 1.024.195 1.414.586l7 7a2 2 0 010 2.828l-7 7a2 2 0 01-2.828 0l-7-7A1.994 1.994 0 013 12V7a4 4 0 014-4z" />
|
|
|
|
|
</svg>
|
|
|
|
|
文章标签
|
|
|
|
|
</h2>
|
|
|
|
|
|
|
|
|
|
<div class="flex flex-wrap gap-2">
|
|
|
|
|
{tags.map(tag => {
|
|
|
|
|
const isActive = tag === tagFilter;
|
|
|
|
|
return (
|
2025-03-09 14:37:44 +08:00
|
|
|
|
<a href={`/articles/tag/${tag}`}
|
2025-03-08 18:16:42 +08:00
|
|
|
|
class={`py-2 px-4 rounded-full text-sm font-medium transition-all duration-300 ${
|
2025-03-03 21:16:16 +08:00
|
|
|
|
isActive
|
2025-03-08 18:16:42 +08:00
|
|
|
|
? 'bg-primary-600 text-white dark:bg-primary-500 dark:text-gray-100 hover:bg-primary-700 dark:hover:bg-primary-600 shadow-md hover:shadow-lg'
|
|
|
|
|
: 'bg-primary-50 dark:bg-gray-700/50 text-primary-600 dark:text-gray-300 hover:bg-primary-100 dark:hover:bg-gray-700 hover:text-primary-700 dark:hover:text-primary-400'
|
2025-03-03 21:16:16 +08:00
|
|
|
|
}`}>
|
|
|
|
|
{tag}
|
|
|
|
|
</a>
|
|
|
|
|
);
|
|
|
|
|
})}
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</>
|
|
|
|
|
) : (
|
2025-03-10 14:05:40 +08:00
|
|
|
|
<div class="container mx-auto px-4 py-8">
|
|
|
|
|
{/* 时间线视图 */}
|
|
|
|
|
<div id="article-timeline" class="relative space-y-8 before:absolute before:inset-0 before:ml-5 before:h-full before:w-0.5 before:-translate-x-px before:bg-gradient-to-b before:from-transparent before:via-primary-300 before:to-transparent md:before:mx-auto md:before:translate-x-0">
|
|
|
|
|
{sortedArticles.length > 0 ? (
|
|
|
|
|
sortedArticles.map((article, index) => {
|
|
|
|
|
const isEven = index % 2 === 0;
|
|
|
|
|
return (
|
|
|
|
|
<div class="relative group">
|
|
|
|
|
{/* 时间线节点 */}
|
|
|
|
|
<div class="absolute left-5 -translate-x-1/2 md:left-1/2 top-6 flex h-3 w-3 items-center justify-center">
|
|
|
|
|
<div class="h-2 w-2 rounded-full bg-primary-500 dark:bg-primary-400 ring-2 ring-white dark:ring-gray-900 ring-offset-2 ring-offset-white dark:ring-offset-gray-900"></div>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
{/* 文章卡片 */}
|
|
|
|
|
<a href={`/articles/${article.id}${viewMode ? `/${viewMode}` : ''}`}
|
|
|
|
|
class={`group/card ml-10 md:ml-0 ${isEven ? 'md:mr-[50%] md:pr-8' : 'md:ml-[50%] md:pl-8'} block`}>
|
|
|
|
|
<article class="relative flex flex-col gap-4 rounded-xl bg-white dark:bg-gray-800 p-6 shadow-lg hover:shadow-xl hover:-translate-y-1 transition-all duration-300 border border-gray-200 dark:border-gray-700">
|
|
|
|
|
{/* 日期标签 */}
|
|
|
|
|
<time datetime={article.data.date.toISOString()}
|
|
|
|
|
class="absolute top-4 right-4 text-xs font-medium text-secondary-500 dark:text-secondary-400">
|
|
|
|
|
{article.data.date.toLocaleDateString('zh-CN', {year: 'numeric', month: 'long', day: 'numeric'})}
|
|
|
|
|
</time>
|
|
|
|
|
|
|
|
|
|
{/* 文章标题 */}
|
|
|
|
|
<h3 class="pr-16 text-xl font-bold text-gray-900 dark:text-gray-100 group-hover/card:text-primary-600 dark:group-hover/card:text-primary-400 transition-colors">
|
|
|
|
|
{article.data.title}
|
|
|
|
|
</h3>
|
|
|
|
|
|
|
|
|
|
{/* 文章摘要 */}
|
|
|
|
|
{article.body && (
|
|
|
|
|
<p class="text-secondary-600 dark:text-secondary-300 line-clamp-2">
|
|
|
|
|
{extractSummary(article.body)}
|
|
|
|
|
</p>
|
|
|
|
|
)}
|
|
|
|
|
|
|
|
|
|
{/* 文章元信息 */}
|
|
|
|
|
<div class="flex flex-wrap items-center gap-4 text-sm">
|
|
|
|
|
{article.data.section && (
|
|
|
|
|
<span class="flex items-center text-secondary-500 dark:text-secondary-400">
|
|
|
|
|
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4 mr-1" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
|
|
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 7v10a2 2 0 002 2h14a2 2 0 002-2V9a2 2 0 00-2-2h-6l-2-2H5a2 2 0 00-2 2z" />
|
|
|
|
|
</svg>
|
|
|
|
|
{article.data.section}
|
|
|
|
|
</span>
|
|
|
|
|
)}
|
|
|
|
|
|
|
|
|
|
{article.data.tags && article.data.tags.length > 0 && (
|
|
|
|
|
<div class="flex flex-wrap gap-2">
|
|
|
|
|
{article.data.tags.map(tag => (
|
|
|
|
|
<span class="text-xs bg-primary-50 dark:bg-primary-900/30 text-primary-600 dark:text-primary-400 py-1 px-2 rounded-full">
|
|
|
|
|
#{tag}
|
|
|
|
|
</span>
|
|
|
|
|
))}
|
|
|
|
|
</div>
|
|
|
|
|
)}
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
{/* 阅读更多指示器 */}
|
|
|
|
|
<div class="flex items-center text-sm text-primary-600 dark:text-primary-400 group-hover/card:translate-x-1 transition-transform">
|
|
|
|
|
<span class="font-medium">阅读全文</span>
|
|
|
|
|
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4 ml-1" viewBox="0 0 20 20" fill="currentColor">
|
|
|
|
|
<path fill-rule="evenodd" d="M7.293 14.707a1 1 0 010-1.414L10.586 10 7.293 6.707a1 1 0 011.414-1.414l4 4a1 1 0 010 1.414l-4 4a1 1 0 01-1.414 0z" clip-rule="evenodd" />
|
|
|
|
|
</svg>
|
|
|
|
|
</div>
|
|
|
|
|
</article>
|
|
|
|
|
</a>
|
|
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
})
|
|
|
|
|
) : (
|
|
|
|
|
<div class="text-center py-4 text-secondary-600 dark:text-secondary-400">暂无文章数据</div>
|
|
|
|
|
)}
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
2025-03-03 21:16:16 +08:00
|
|
|
|
)}
|
|
|
|
|
</main>
|
|
|
|
|
</div>
|
|
|
|
|
</Layout>
|