newechoes/src/pages/articles/index.astro
lsy ad02d7b38b 增加mdx支持
修复文章格式问题
2025-03-10 15:15:30 +08:00

544 lines
28 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 } from '../../content.config';
import Layout from '@/components/Layout.astro';
import Breadcrumb from '@/components/Breadcrumb.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/${articleId}${view ? `/${view}` : ''}`;
}
---
<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('/') : ''}/${view}`}
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 min-w-0">
<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 dark:text-gray-400 mt-1 line-clamp-2 break-words">
{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 truncate ml-2">阅读全文</span>
</div>
{article.data.tags && article.data.tags.length > 0 && (
<div class="flex flex-wrap gap-2 mt-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>
</a>
))
) : (
// 显示当前目录的文章
currentArticles.map(articlePath => {
// 获取文章ID - 不需要移除src/content前缀因为contentStructure中已经是相对路径
const articleId = articlePath;
// 尝试匹配文章
const article = articles.find(a => a.id === articleId);
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">
<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>
</div>
</div>
</div>
</div>
);
}
return (
<a href={`/articles/${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 min-w-0">
<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 dark:text-gray-400 mt-1 line-clamp-2 break-words">
{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 truncate ml-2">阅读全文</span>
</div>
{article.data.tags && article.data.tags.length > 0 && (
<div class="flex flex-wrap gap-2 mt-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>
</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>
</>
) : (
<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>
)}
</main>
</div>
</Layout>