将所有页面替换为动态构建

This commit is contained in:
lsy 2025-03-09 14:37:44 +08:00
parent 16125a580c
commit d3ea05700c
9 changed files with 1184 additions and 207 deletions

2
.gitignore vendored
View File

@ -22,3 +22,5 @@ pnpm-debug.log*
# jetbrains setting folder
.idea/
.vercel

View File

@ -4,7 +4,6 @@ import { defineConfig } from 'astro/config';
import tailwindcss from '@tailwindcss/vite';
import react from '@astrojs/react';
import node from '@astrojs/node';
import remarkEmoji from 'remark-emoji';
import rehypeExternalLinks from 'rehype-external-links';
import sitemap from '@astrojs/sitemap';
@ -12,6 +11,8 @@ import fs from 'node:fs';
import path from 'node:path';
import { SITE_URL } from './src/consts';
import vercel from '@astrojs/vercel';
function getArticleDate(articleId) {
try {
const mdPath = path.join(process.cwd(), 'src/content', articleId + '.md');
@ -30,12 +31,14 @@ function getArticleDate(articleId) {
// https://astro.build/config
export default defineConfig({
site: SITE_URL, // 替换为您的实际网站 URL
output: 'server',
site: SITE_URL,
output: 'static',
trailingSlash: 'ignore',
build: {
format: 'directory'
},
vite: {
plugins: [tailwindcss()],
build: {
@ -49,6 +52,8 @@ export default defineConfig({
'react-vendor': ['react', 'react-dom'],
// 其他大型依赖也可以单独打包
'chart-vendor': ['chart.js'],
// 将 ECharts 单独打包
'echarts-vendor': ['echarts'],
// 将其他组件打包到一起
'components': ['./src/components']
}
@ -102,12 +107,7 @@ export default defineConfig({
entryLimit: 5
})
],
// 添加 Node.js 适配器配置
adapter: node({
mode: 'standalone' // 独立模式,适合大多数部署环境
}),
// Markdown 配置
markdown: {
syntaxHighlight: 'prism',
@ -126,5 +126,7 @@ export default defineConfig({
// 启用自动换行,防止水平滚动
wrap: true,
}
}
},
adapter: vercel()
});

908
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -12,6 +12,7 @@
"@astrojs/node": "^9.1.2",
"@astrojs/react": "^4.2.1",
"@astrojs/sitemap": "^3.2.1",
"@astrojs/vercel": "^8.1.1",
"@tailwindcss/vite": "^4.0.9",
"@types/react": "^19.0.10",
"@types/react-dom": "^19.0.4",

View File

@ -1,200 +1,96 @@
---
import type { CollectionEntry } from 'astro:content';
interface Props {
title?: string;
itemsPerPage?: number;
articles: CollectionEntry<'articles'>[];
}
const {
title = "文章时间线",
itemsPerPage = 10
itemsPerPage = 10,
articles = []
} = Astro.props;
// 按日期排序文章
const sortedArticles = articles.sort(
(a, b) => b.data.date.getTime() - a.data.date.getTime()
);
---
<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>}
<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">
<!-- 内容将通过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>
{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>
<script is:inline define:vars={{ itemsPerPage }}>
let currentPage = 1;
let isLoading = false;
let hasMoreContent = true;
{/* 文章卡片 */}
<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.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>
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);
}
}
{/* 文章标题 */}
<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>
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;
}
{/* 文章摘要 */}
{article.data.summary && (
<p class="text-secondary-600 dark:text-secondary-300 line-clamp-2">
{article.data.summary}
</p>
)}
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">
${article.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.section}
</span>
` : ''}
${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}
{/* 文章元信息 */}
<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>
`).join('')}
)}
{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>
<!-- 阅读更多指示器 -->
<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>
`;
}).join('');
if (append) {
articleTimeline.innerHTML += articlesHTML;
} else {
articleTimeline.innerHTML = articlesHTML;
}
}
function showLoading(show) {
const loading = document.getElementById('loading');
if (loading) {
if (show) {
loading.classList.remove('hidden');
} else {
loading.classList.add('hidden');
}
}
}
function showEndMessage(show) {
const endMessage = document.getElementById('end-message');
if (endMessage) {
endMessage.classList.toggle('hidden', !show);
}
}
function setupInfiniteScroll() {
// 直接使用滚动事件
window.addEventListener('scroll', handleScroll);
// 初始检查一次,以防内容不足一屏
setTimeout(handleScroll, 500);
}
function handleScroll() {
if (isLoading || !hasMoreContent) {
return;
}
const scrollY = window.scrollY;
const windowHeight = window.innerHeight;
const documentHeight = document.documentElement.scrollHeight;
// 当滚动到距离底部300px时加载更多
if (scrollY + windowHeight >= documentHeight - 300) {
fetchArticles(currentPage + 1, true);
}
}
document.addEventListener('DOMContentLoaded', () => {
fetchArticles(1, false).then(() => {
setupInfiniteScroll();
});
});
</script>
{/* 阅读更多指示器 */}
<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>

View File

@ -18,7 +18,6 @@ export const VISITED_PLACES = [ '黑龙江', '吉林', '辽宁', '北京', '天
export const DOUBAN_ID = 'lsy22';
// Git 配置 - 只包含用户需要修改的内容
export const GIT_CONFIG = {
// 每页显示的项目数量

View File

@ -0,0 +1,95 @@
---
import ArticlesPage, { getStaticPaths as getOriginalPaths } from './index.astro';
// 重新导出 getStaticPaths处理所有路径模式
export async function getStaticPaths() {
const paths = await getOriginalPaths();
const allPaths = paths.map(({ props }) => {
const results = [];
// 1. 如果有标签,添加标签路径
if (props.tag) {
// 标签主页
results.push({
params: { path: `tag/${props.tag}` },
props: { ...props }
});
// 标签视图页
results.push({
params: { path: `tag/${props.tag}/grid` },
props: { ...props, view: 'grid' }
});
results.push({
params: { path: `tag/${props.tag}/timeline` },
props: { ...props, view: 'timeline' }
});
}
// 2. 如果有路径,添加目录路径
if (props.path) {
// 目录主页
results.push({
params: { path: props.path },
props: { ...props }
});
// 目录视图页
results.push({
params: { path: `${props.path}/grid` },
props: { ...props, view: 'grid' }
});
results.push({
params: { path: `${props.path}/timeline` },
props: { ...props, view: 'timeline' }
});
}
return results;
}).flat();
// 添加顶级视图路径
allPaths.push(
{
params: { path: 'grid' },
props: { path: '', tag: '', view: 'grid' }
},
{
params: { path: 'timeline' },
props: { path: '', tag: '', view: 'timeline' }
}
);
return allPaths;
}
// 使用主页面组件
const { props } = Astro;
// 解析路径参数
const pathParts = (Astro.params.path as string | undefined)?.split('/') || [];
let path = '';
let tag = '';
let view = 'grid';
if (pathParts[0] === 'tag' && pathParts.length >= 2) {
// 标签路径
tag = pathParts[1];
view = pathParts[2] || 'grid';
} else if (['grid', 'timeline'].includes(pathParts[pathParts.length - 1])) {
// 视图路径
view = pathParts.pop() || 'grid';
path = pathParts.join('/');
} else {
// 普通目录路径
path = Astro.params.path || '';
}
// 合并属性
const mergedProps = {
...props,
path,
tag,
view
};
---
<ArticlesPage {...mergedProps} />

View File

@ -5,6 +5,10 @@ import { contentStructure, getRelativePath, getBasename, getDirPath, getSpecialP
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
@ -20,8 +24,90 @@ export function extractSummary(content: string, length = 150) {
: plainText;
}
// 预渲染页面,但允许客户端导航
export const prerender = false;
// 生成所有可能的静态路径
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');
@ -36,15 +122,11 @@ const tags = [...new Set(allTags)].sort();
// 获取内容结构
const { sections } = contentStructure;
// 获取当前路径参数
const path = Astro.url.searchParams.get('path') || '';
const pathSegments = path ? decodeURIComponent(path).split('/') : [];
// 获取标签参数
const tagFilter = Astro.url.searchParams.get('tag') || '';
const tagFilter = tag;
// 获取视图模式参数
const viewMode = Astro.url.searchParams.get('view') || 'grid';
const viewMode = view;
// 根据路径获取当前目录
function getCurrentSection(pathSegments: string[]) {
@ -132,7 +214,7 @@ function getArticleUrl(articleId: string) {
<!-- 视图切换按钮 -->
<div class="flex items-center gap-px">
<a href={`/articles?${new URLSearchParams({...Object.fromEntries(Astro.url.searchParams), view: 'grid'}).toString()}`}
<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'
@ -142,7 +224,7 @@ function getArticleUrl(articleId: string) {
<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?${new URLSearchParams({...Object.fromEntries(Astro.url.searchParams), view: 'timeline'}).toString()}`}
<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'
@ -163,7 +245,7 @@ function getArticleUrl(articleId: string) {
<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 ? `?path=${encodeURIComponent(pathSegments.slice(0, -1).join('/'))}` : ''}`}
<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">
@ -190,7 +272,7 @@ function getArticleUrl(articleId: string) {
const dirLink = currentPath ? `${currentPath}/${section.name}` : section.name;
return (
<a href={`/articles?path=${encodeURIComponent(dirLink)}`}
<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">
@ -389,7 +471,7 @@ function getArticleUrl(articleId: string) {
{tags.map(tag => {
const isActive = tag === tagFilter;
return (
<a href={`/articles?tag=${tag}`}
<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'
@ -403,7 +485,7 @@ function getArticleUrl(articleId: string) {
</div>
</>
) : (
<ArticleTimeline title="" itemsPerPage={10} />
<ArticleTimeline title="" itemsPerPage={10} articles={sortedArticles} />
)}
</main>
</div>