newechoes/src/pages/articles/index.astro

492 lines
24 KiB
Plaintext
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.

---
import { getCollection } from 'astro:content';
import type { CollectionEntry } from 'astro:content';
import { contentStructure, getRelativePath, getBasename, getDirPath, getSpecialPath } from '../../content.config';
import Layout from '@/components/Layout.astro';
import Breadcrumb from '@/components/Breadcrumb.astro';
import ArticleTimeline from '@/components/ArticleTimeline.astro';
// 启用静态预渲染
export const prerender = true;
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;
}
// 生成所有可能的静态路径
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('/') : [];
// 获取所有文章,并按日期排序
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;
// 获取标签参数
const tagFilter = tag;
// 获取视图模式参数
const viewMode = view;
// 根据路径获取当前目录
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;
}
// 处理特殊ID的函数
function getArticleUrl(articleId: string) {
return `/articles/${getSpecialPath(articleId)}`;
}
---
<Layout>
<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'}`}>
<!-- 导航栏 -->
<div class="bg-white dark:bg-gray-800 rounded-xl mb-4 shadow-lg border border-gray-200 dark:border-gray-700">
<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">
<a href={`/articles/${path}${tag ? `/tag/${tag}` : ''}/grid`}
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>
<a href={`/articles/${path}${tag ? `/tag/${tag}` : ''}/timeline`}
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 && (
<a href={`/articles/${pathSegments.length > 1 ? pathSegments.slice(0, -1).join('/') : ''}`}
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">
<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 (
<a href={`/articles/${dirLink}`}
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">
<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 => (
<a href={getArticleUrl(article.id)}
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">
<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>
{article.body && (
<p class="text-xs text-gray-600 mt-1 line-clamp-2">
{extractSummary(article.body)}
</p>
)}
<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 => {
// 获取文章ID
const articleId = getRelativePath(articlePath);
// 尝试不同的方式匹配文章
const article = articles.find(a => {
// 1. 直接匹配完整路径
if (a.id === articleId) {
return true;
}
// 2. 匹配文件名(不含路径和扩展名)
const baseName = getBasename(articleId);
if (a.id === baseName) {
return true;
}
// 3. 尝试匹配相对路径的一部分
const articleParts = articleId.split('/');
const fileName = articleParts[articleParts.length - 1];
if (a.id.endsWith(fileName)) {
return true;
}
// 4. 移除.md扩展名后匹配
const idWithoutExt = articleId.replace(/\.md$/, '');
if (a.id === idWithoutExt) {
return true;
}
// 5. 处理多级目录结构
// 如果文章ID包含目录路径尝试匹配最后的文件名部分
const articlePathParts = articlePath.split('/');
const articleFileName = articlePathParts[articlePathParts.length - 1];
const articleIdParts = a.id.split('/');
const articleIdFileName = articleIdParts[articleIdParts.length - 1];
if (articleFileName === articleIdFileName) {
return true;
}
// 6. 移除扩展名后比较文件名
const fileNameWithoutExt = articleFileName.replace(/\.md$/, '');
if (articleIdFileName === fileNameWithoutExt) {
return true;
}
return false;
});
if (!article) {
return (
<div class="flex flex-col h-full p-5 border border-red-200 rounded-xl bg-red-50 shadow-lg">
<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>
<p class="text-xs text-red-600 mt-1">ID: {articleId}</p>
<div class="text-xs text-red-500 mt-2 line-clamp-1">
可用文章: {articles.map(a => a.id).join(', ')}
</div>
</div>
</div>
</div>
);
}
return (
<a href={getArticleUrl(article.id)}
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">
<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>
{article.body && (
<p class="text-xs text-gray-600 mt-1 line-clamp-2">
{extractSummary(article.body)}
</p>
)}
<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)) && (
<div class="text-center py-16 bg-white rounded-xl shadow-lg border border-gray-200 dark:border-gray-700 mb-12">
<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>
)}
<!-- 标签过滤器 -->
<div class="bg-white dark:bg-gray-800 p-8 rounded-xl shadow-lg border border-gray-200 dark:border-gray-700">
<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 (
<a href={`/articles/tag/${tag}`}
class={`py-2 px-4 rounded-full text-sm font-medium transition-all duration-300 ${
isActive
? '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'
}`}>
{tag}
</a>
);
})}
</div>
</div>
</>
) : (
<ArticleTimeline title="" itemsPerPage={10} articles={sortedArticles} />
)}
</main>
</div>
</Layout>