2025-03-03 21:16:16 +08:00
|
|
|
---
|
|
|
|
interface Props {
|
|
|
|
title?: string;
|
|
|
|
itemsPerPage?: number;
|
|
|
|
}
|
|
|
|
|
|
|
|
const {
|
|
|
|
title = "文章时间线",
|
|
|
|
itemsPerPage = 10
|
|
|
|
} = Astro.props;
|
|
|
|
---
|
|
|
|
|
|
|
|
<div class="container mx-auto px-4 py-8">
|
|
|
|
{title && <h1 class="text-3xl font-bold mb-6 text-primary-900 dark:text-primary-100">{title}</h1>}
|
|
|
|
|
2025-03-08 18:16:42 +08:00
|
|
|
<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">
|
2025-03-03 21:16:16 +08:00
|
|
|
<!-- 内容将通过JS动态加载 -->
|
|
|
|
</div>
|
|
|
|
|
|
|
|
<div id="loading" class="text-center py-8">
|
|
|
|
<div class="inline-block h-8 w-8 animate-spin rounded-full border-4 border-solid border-primary-500 dark:border-primary-400 border-r-transparent align-[-0.125em] motion-reduce:animate-[spin_1.5s_linear_infinite]"></div>
|
|
|
|
<p class="mt-2 text-secondary-600 dark:text-secondary-400">加载更多...</p>
|
|
|
|
</div>
|
|
|
|
|
|
|
|
<div id="end-message" class="text-center py-8 hidden">
|
|
|
|
<p class="text-secondary-600 dark:text-secondary-400">已加载全部内容</p>
|
|
|
|
</div>
|
|
|
|
</div>
|
|
|
|
|
|
|
|
<script is:inline define:vars={{ itemsPerPage }}>
|
|
|
|
let currentPage = 1;
|
|
|
|
let isLoading = false;
|
|
|
|
let hasMoreContent = true;
|
|
|
|
|
|
|
|
async function fetchArticles(page = 1, append = false) {
|
|
|
|
if (isLoading || (!append && !hasMoreContent)) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
isLoading = true;
|
|
|
|
showLoading(true);
|
|
|
|
|
|
|
|
try {
|
|
|
|
const response = await fetch(`/api/articles?page=${page}&limit=${itemsPerPage}`);
|
|
|
|
if (!response.ok) {
|
|
|
|
throw new Error('获取文章数据失败');
|
|
|
|
}
|
|
|
|
|
|
|
|
const data = await response.json();
|
|
|
|
renderArticles(data.articles, append);
|
|
|
|
|
|
|
|
// 更新分页状态
|
|
|
|
currentPage = data.pagination.current;
|
|
|
|
hasMoreContent = data.pagination.hasNext;
|
|
|
|
|
|
|
|
if (!hasMoreContent) {
|
|
|
|
showEndMessage(true);
|
|
|
|
}
|
|
|
|
} catch (error) {
|
|
|
|
const articleTimeline = document.getElementById('article-timeline');
|
|
|
|
if (articleTimeline && !append) {
|
|
|
|
articleTimeline.innerHTML = '<div class="text-center text-red-500 py-4">获取数据失败,请稍后再试</div>';
|
|
|
|
}
|
|
|
|
} finally {
|
|
|
|
isLoading = false;
|
|
|
|
showLoading(false);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
function renderArticles(articles, append = false) {
|
|
|
|
const articleTimeline = document.getElementById('article-timeline');
|
|
|
|
if (!articleTimeline) return;
|
|
|
|
|
|
|
|
if (!articles || articles.length === 0) {
|
|
|
|
if (!append) {
|
|
|
|
articleTimeline.innerHTML = '<div class="text-center py-4 text-secondary-600 dark:text-secondary-400">暂无文章数据</div>';
|
|
|
|
}
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2025-03-08 18:16:42 +08:00
|
|
|
const articlesHTML = articles.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}"
|
|
|
|
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.date}"
|
|
|
|
class="absolute top-4 right-4 text-xs font-medium text-secondary-500 dark:text-secondary-400">
|
|
|
|
${new Date(article.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.title}
|
|
|
|
</h3>
|
|
|
|
|
|
|
|
<!-- 文章摘要 -->
|
|
|
|
${article.summary ? `
|
|
|
|
<p class="text-secondary-600 dark:text-secondary-300 line-clamp-2">
|
|
|
|
${article.summary}
|
|
|
|
</p>
|
|
|
|
` : ''}
|
|
|
|
|
|
|
|
<!-- 文章元信息 -->
|
|
|
|
<div class="flex flex-wrap items-center gap-4 text-sm">
|
2025-03-03 21:16:16 +08:00
|
|
|
${article.section ? `
|
2025-03-08 18:16:42 +08:00
|
|
|
<span class="flex items-center text-secondary-500 dark:text-secondary-400">
|
2025-03-03 21:16:16 +08:00
|
|
|
<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.section}
|
|
|
|
</span>
|
|
|
|
` : ''}
|
2025-03-08 18:16:42 +08:00
|
|
|
|
|
|
|
${article.tags && article.tags.length > 0 ? `
|
|
|
|
<div class="flex flex-wrap gap-2">
|
|
|
|
${article.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>
|
|
|
|
`).join('')}
|
|
|
|
</div>
|
|
|
|
` : ''}
|
2025-03-03 21:16:16 +08:00
|
|
|
</div>
|
2025-03-08 18:16:42 +08:00
|
|
|
|
|
|
|
<!-- 阅读更多指示器 -->
|
|
|
|
<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>
|
2025-03-03 21:16:16 +08:00
|
|
|
</div>
|
2025-03-08 18:16:42 +08:00
|
|
|
</article>
|
|
|
|
</a>
|
|
|
|
</div>
|
|
|
|
`;
|
|
|
|
}).join('');
|
2025-03-03 21:16:16 +08:00
|
|
|
|
|
|
|
if (append) {
|
|
|
|
articleTimeline.innerHTML += articlesHTML;
|
|
|
|
} else {
|
|
|
|
articleTimeline.innerHTML = articlesHTML;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
function showLoading(show) {
|
|
|
|
const loading = document.getElementById('loading');
|
|
|
|
if (loading) {
|
2025-03-08 18:16:42 +08:00
|
|
|
if (show) {
|
|
|
|
loading.classList.remove('hidden');
|
|
|
|
} else {
|
|
|
|
loading.classList.add('hidden');
|
|
|
|
}
|
2025-03-03 21:16:16 +08:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
function showEndMessage(show) {
|
|
|
|
const endMessage = document.getElementById('end-message');
|
|
|
|
if (endMessage) {
|
|
|
|
endMessage.classList.toggle('hidden', !show);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
function setupInfiniteScroll() {
|
2025-03-08 18:16:42 +08:00
|
|
|
// 直接使用滚动事件
|
2025-03-03 21:16:16 +08:00
|
|
|
window.addEventListener('scroll', handleScroll);
|
|
|
|
|
2025-03-08 18:16:42 +08:00
|
|
|
// 初始检查一次,以防内容不足一屏
|
|
|
|
setTimeout(handleScroll, 500);
|
2025-03-03 21:16:16 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
function handleScroll() {
|
|
|
|
if (isLoading || !hasMoreContent) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
const scrollY = window.scrollY;
|
|
|
|
const windowHeight = window.innerHeight;
|
|
|
|
const documentHeight = document.documentElement.scrollHeight;
|
|
|
|
|
2025-03-08 18:16:42 +08:00
|
|
|
// 当滚动到距离底部300px时加载更多
|
2025-03-03 21:16:16 +08:00
|
|
|
if (scrollY + windowHeight >= documentHeight - 300) {
|
|
|
|
fetchArticles(currentPage + 1, true);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
document.addEventListener('DOMContentLoaded', () => {
|
|
|
|
fetchArticles(1, false).then(() => {
|
|
|
|
setupInfiniteScroll();
|
|
|
|
});
|
|
|
|
});
|
|
|
|
</script>
|