1.全部改成参数式部件

2.可以用脚本创建文章
This commit is contained in:
lsy 2025-03-10 17:22:18 +08:00
parent ad02d7b38b
commit f110beb5dc
17 changed files with 504 additions and 436 deletions

55
create_post.sh Normal file
View File

@ -0,0 +1,55 @@
#!/bin/bash
# 获取脚本所在目录的上级目录(假设脚本在项目根目录)
PROJECT_ROOT="$(cd "$(dirname "$0")" && pwd)"
# 如果没有提供参数,使用交互式输入
if [ "$#" -lt 2 ]; then
read -rp "请输入文章标题: " TITLE
read -rp "请输入文章路径 (例如: web/my-post): " PATH_ARG
else
TITLE=$1
PATH_ARG=$2
fi
# 检查输入是否为空
if [ -z "$TITLE" ] || [ -z "$PATH_ARG" ]; then
echo "错误: 标题和路径不能为空"
echo "使用方法: $0 <标题> <路径>"
echo "示例: $0 \"我的新文章\" \"web/my-post\""
exit 1
fi
# 获取当前时间格式化为ISO 8601格式
CURRENT_DATE=$(date +"%Y-%m-%dT%H:%M:%S%:z")
# 构建完整的文件路径
CONTENT_DIR="$PROJECT_ROOT/src/content"
FULL_PATH="$CONTENT_DIR/$PATH_ARG"
# 确保路径存在
mkdir -p "$FULL_PATH"
# 构建最终的文件路径
FILENAME="$FULL_PATH/$(basename "$PATH_ARG").md"
ABSOLUTE_PATH="$(cd "$(dirname "$FILENAME")" 2>/dev/null && pwd)/$(basename "$FILENAME")"
# 检查文件是否已存在
if [ -f "$FILENAME" ]; then
echo "错误: 文章已存在: $ABSOLUTE_PATH"
read -rp "按回车键退出..."
exit 1
fi
# 创建markdown文件
cat > "$FILENAME" << EOF
---
title: "$TITLE"
date: $CURRENT_DATE
tags: []
---
hello,world
EOF
echo "已创建新文章: $ABSOLUTE_PATH"
read -rp "按回车键退出..."

View File

@ -1,6 +1,5 @@
import React, { useState, useEffect } from 'react';
import ReactMasonryCss from 'react-masonry-css';
import { GIT_CONFIG } from '@/consts';
// Git 平台类型枚举
export enum GitPlatform {
@ -9,22 +8,31 @@ export enum GitPlatform {
GITEE = 'gitee'
}
// 内部使用的平台配置 - 用户不需要修改
// Git 配置接口
export type GitConfig = {
username: string;
token?: string;
perPage?: number;
url?: string;
};
// 平台默认配置
export const DEFAULT_GIT_CONFIG = {
perPage: 10,
giteaUrl: ''
};
// 内部使用的平台配置
export const GIT_PLATFORM_CONFIG = {
platforms: {
[GitPlatform.GITHUB]: {
...GIT_CONFIG.github,
apiUrl: 'https://api.github.com'
},
[GitPlatform.GITEA]: {
...GIT_CONFIG.gitea
},
[GitPlatform.GITEA]: {},
[GitPlatform.GITEE]: {
...GIT_CONFIG.gitee,
apiUrl: 'https://gitee.com/api/v5'
}
},
enabledPlatforms: [GitPlatform.GITHUB, GitPlatform.GITEA, GitPlatform.GITEE],
platformNames: {
[GitPlatform.GITHUB]: 'GitHub',
[GitPlatform.GITEA]: 'Gitea',
@ -32,8 +40,6 @@ export const GIT_PLATFORM_CONFIG = {
}
};
interface GitProject {
name: string;
description: string;
@ -59,13 +65,15 @@ interface GitProjectCollectionProps {
username?: string;
organization?: string;
title?: string;
config: GitConfig;
}
const GitProjectCollection: React.FC<GitProjectCollectionProps> = ({
platform,
username,
organization,
title
title,
config
}) => {
const [projects, setProjects] = useState<GitProject[]>([]);
const [pagination, setPagination] = useState<Pagination>({ current: 1, total: 1, hasNext: false, hasPrev: false });
@ -73,36 +81,49 @@ const GitProjectCollection: React.FC<GitProjectCollectionProps> = ({
const [error, setError] = useState<string | null>(null);
const [isPageChanging, setIsPageChanging] = useState(false);
// 获取默认用户名
const defaultUsername = GIT_PLATFORM_CONFIG.platforms[platform].username;
// 使用提供的用户名或默认用户名
const effectiveUsername = username || defaultUsername;
const effectiveUsername = username || config.username;
const fetchData = async (page = 1) => {
setLoading(true);
const params = new URLSearchParams();
params.append('platform', platform);
params.append('page', page.toString());
if (!platform || !Object.values(GitPlatform).includes(platform)) {
setError('无效的平台参数');
setLoading(false);
return;
}
try {
const baseUrl = new URL('/api/git-projects', window.location.origin);
baseUrl.searchParams.append('platform', platform);
baseUrl.searchParams.append('page', page.toString());
baseUrl.searchParams.append('config', JSON.stringify(config));
if (effectiveUsername) {
params.append('username', effectiveUsername);
baseUrl.searchParams.append('username', effectiveUsername);
}
if (organization) {
params.append('organization', organization);
baseUrl.searchParams.append('organization', organization);
}
const url = `/api/git-projects?${params.toString()}`;
const response = await fetch(baseUrl.toString(), {
method: 'GET',
headers: {
'Accept': 'application/json'
}
});
try {
const response = await fetch(url);
if (!response.ok) {
throw new Error(`获取数据失败: ${response.status} ${response.statusText}`);
const errorData = await response.json();
throw new Error(`请求失败: ${response.status} ${response.statusText}\n${JSON.stringify(errorData, null, 2)}`);
}
const data = await response.json();
setProjects(data.projects);
setPagination(data.pagination);
} catch (err) {
console.error('请求错误:', err);
setError(err instanceof Error ? err.message : '未知错误');
} finally {
setLoading(false);

View File

@ -2,7 +2,7 @@
import "@/styles/global.css";
import Header from "@/components/header.astro";
import Footer from "@/components/Footer.astro";
import { ICP, PSB_ICP, PSB_ICP_URL, SITE_NAME } from "@/consts";
import { ICP, PSB_ICP, PSB_ICP_URL, SITE_NAME, SITE_DESCRIPTION } from "@/consts";
// 定义Props接口
interface Props {
@ -18,7 +18,7 @@ interface Props {
const canonicalURL = new URL(Astro.url.pathname, Astro.site);
// 从props中获取页面特定信息
const { title = SITE_NAME, description, date, author, tags, image } = Astro.props;
const { title = SITE_NAME, description = SITE_DESCRIPTION, date, author, tags, image } = Astro.props;
---
<!doctype html>
<html lang="zh-CN" class="m-0 w-full h-full">

View File

@ -2,9 +2,10 @@
interface Props {
type: 'movie' | 'book';
title: string;
doubanId: string;
}
const { type, title } = Astro.props;
const { type, title, doubanId } = Astro.props;
---
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
@ -24,7 +25,7 @@ const { type, title } = Astro.props;
</div>
</div>
<script is:inline define:vars={{ type }}>
<script is:inline define:vars={{ type, doubanId }}>
let currentPage = 1;
let isLoading = false;
@ -41,7 +42,7 @@ const { type, title } = Astro.props;
const start = (page - 1) * itemsPerPage;
try {
const response = await fetch(`/api/douban?type=${type}&start=${start}`);
const response = await fetch(`/api/douban?type=${type}&start=${start}&doubanId=${doubanId}`);
if (!response.ok) {
throw new Error(`获取${type === 'movie' ? '电影' : '图书'}数据失败`);
}

View File

@ -2,9 +2,12 @@ import React, { useEffect, useRef } from 'react';
import * as echarts from 'echarts';
import worldData from '@/assets/world.zh.json';
import chinaData from '@/assets/china.json';
import { VISITED_PLACES } from '@/consts';
const WorldHeatmap: React.FC = () => {
interface WorldHeatmapProps {
visitedPlaces: string[];
}
const WorldHeatmap: React.FC<WorldHeatmapProps> = ({ visitedPlaces }) => {
const chartRef = useRef<HTMLDivElement>(null);
useEffect(() => {
@ -12,7 +15,6 @@ const WorldHeatmap: React.FC = () => {
const chart = echarts.init(chartRef.current);
// 合并中国省份到世界地图
const mergedWorldData = {
...worldData,
features: worldData.features.map((feature: any) => {
@ -21,13 +23,12 @@ const WorldHeatmap: React.FC = () => {
...feature,
geometry: {
type: 'MultiPolygon',
coordinates: [] // 清空中国的轮廓
coordinates: []
}
};
}
return feature;
}).concat(
// 添加中国省份数据
chinaData.features.map((feature: any) => ({
...feature,
properties: {
@ -49,7 +50,7 @@ const WorldHeatmap: React.FC = () => {
tooltip: {
trigger: 'item',
formatter: ({name}: {name: string}) => {
const visited = VISITED_PLACES.includes(name);
const visited = visitedPlaces.includes(name);
return `${name}<br/>${visited ? '✓ 已去过' : '尚未去过'}`;
}
},
@ -85,7 +86,7 @@ const WorldHeatmap: React.FC = () => {
},
data: mergedWorldData.features.map((feature: any) => ({
name: feature.properties.name,
value: VISITED_PLACES.includes(feature.properties.name) ? 1 : 0
value: visitedPlaces.includes(feature.properties.name) ? 1 : 0
})),
nameProperty: 'name'
}]
@ -103,7 +104,7 @@ const WorldHeatmap: React.FC = () => {
chart.resize();
});
};
}, []);
}, [visitedPlaces]);
return <div ref={chartRef} style={{ width: '100%', height: '600px' }} />;
};

View File

@ -1,5 +1,6 @@
export const SITE_URL = 'https://lsy22.com';
export const SITE_NAME = "echoes";
export const SITE_DESCRIPTION = "记录生活,分享所思";
export const NAV_LINKS = [
{ href: '/', text: '首页' },
@ -18,24 +19,3 @@ export const VISITED_PLACES = [ '黑龙江', '吉林', '辽宁', '北京', '天
export const DOUBAN_ID = 'lsy22';
// Git 配置 - 只包含用户需要修改的内容
export const GIT_CONFIG = {
// 每页显示的项目数量
perPage: 10,
// 用户配置 - 用户只需修改这部分
github: {
username: 'lsy2246', // GitHub 用户名
token: '' // GitHub 访问令牌(可选)
},
gitea: {
url: 'https://g.lsy22.com', // Gitea 实例 URL
username: 'lsy', // Gitea 用户名
token: '' // Gitea 访问令牌(可选)
},
gitee: {
username: 'lsy22', // Gitee 用户名
token: '' // Gitee 访问令牌(可选)
}
};

View File

@ -63,32 +63,6 @@ export function getSpecialPath(originalPath: string): string {
return originalPath;
}
// 辅助函数:标准化文件名
function normalizeFileName(fileName: string): string {
// 先转换为小写
let normalized = fileName.toLowerCase();
// 保存括号中的内容
const bracketContent = normalized.match(/[(](.*?)[)]/)?.[1] || '';
// 标准化处理
normalized = normalized
.replace(/[(].*?[)]/g, '') // 移除括号及其内容
.replace(/[【】\[\]]/g, '') // 移除方括号
.replace(/[—–]/g, '-') // 统一全角横线为半角
.replace(/\s+/g, '-') // 空格转换为连字符
.replace(/[.:;,'"!?`]/g, '') // 移除标点符号
.replace(/-+/g, '-') // 合并多个连字符
.replace(/^-|-$/g, ''); // 移除首尾连字符
// 如果括号中有内容,将其添加回去
if (bracketContent) {
normalized = `${normalized}-${bracketContent}`;
}
return normalized;
}
// 3. 定义目录结构处理函数
async function getContentStructure(): Promise<ContentStructure> {
// 获取所有文章
@ -101,7 +75,6 @@ async function getContentStructure(): Promise<ContentStructure> {
// 处理每个文章路径
for (const articlePath of articlePaths) {
const parts = articlePath.split('/');
const fileName = parts[parts.length - 1];
const dirPath = parts.slice(0, -1);
// 为每一级目录创建或更新节点

View File

@ -0,0 +1,255 @@
---
title: "echoes博客使用说明"
date: 2025-03-09T01:07:23Z
tags: []
---
这是一个基于 Astro + React 构建的个人博客系统,具有文章管理、项目展示、观影记录、读书记录等功能。本文将详细介绍如何使用和配置这个博客系统。
## 功能特点
1. **响应式设计**:完美适配桌面端和移动端
2. **深色模式**:支持自动和手动切换深色/浅色主题
3. **文章系统**:支持 Markdown 写作,带有标签和分类
4. **项目展示**:支持展示 GitHub、Gitea 和 Gitee 的项目
5. **观影记录**:集成豆瓣观影数据
6. **读书记录**:集成豆瓣读书数据
## 基础配置
主要配置文件位于 `src/consts.ts`,你需要修改以下内容:
```typescript
// 网站基本信息
export const SITE_URL = 'https://your-domain.com';
export const SITE_NAME = "你的网站名称";
export const SITE_DESCRIPTION = "网站描述";
// 导航链接
export const NAV_LINKS = [
{ href: '/', text: '首页' },
{ href: '/articles', text: '文章' },
{ href: '/movies', text: '观影' },
{ href: '/books', text: '读书' },
{ href: '/projects', text: '项目' },
{ href: '/other', text: '其他' }
];
// 备案信息(如果需要)
export const ICP = '你的ICP备案号';
export const PSB_ICP = '你的公安备案号';
export const PSB_ICP_URL = '备案链接';
// 豆瓣配置
export const DOUBAN_ID = '你的豆瓣ID';
```
## 文章写作
### 创建新文章
你可以通过以下两种方式创建新文章:
#### 1. 使用创建脚本(推荐)
项目根目录下提供了 `create_post.sh` 脚本来快速创建文章:
```bash
# 添加执行权限(首次使用时)
chmod +x create_post.sh
# 方式1交互式创建
./create_post.sh
# 按提示输入文章标题和路径
# 方式2命令行参数创建
./create_post.sh "文章标题" "目录/文章路径"
# 例如:./create_post.sh "我的新文章" "web/my-post"
```
脚本会自动:
- 在指定位置创建文章文件
- 添加必要的 frontmatter标题、日期、标签
- 检查文件是否已存在
- 显示文件的绝对路径
#### 2. 手动创建
`src/content/articles` 目录下创建 `.md``.mdx` 文件。文章需要包含以下前置信息:
```markdown
---
title: "文章标题"
date: YYYY-MM-DD
tags: ["标签1", "标签2"]
---
文章内容...
```
### 文章列表展示
文章列表页面会自动获取所有文章并按日期排序展示,支持:
- 文章标题和摘要
- 发布日期
- 标签系统
- 阅读时间估算
## 项目展示
项目展示页面支持从 GitHub、Gitea 和 Gitee 获取和展示项目信息。
### GitProjectCollection 组件
用于展示 Git 平台的项目列表。
基本用法:
```astro
---
import GitProjectCollection from '@/components/GitProjectCollection';
import { GitPlatform, type GitConfig } from '@/components/GitProjectCollection';
// Gitea 配置示例
const giteaConfig: GitConfig = {
username: 'your-username', // 必填:用户名
token: 'your-token', // 可选:访问令牌,用于访问私有仓库
perPage: 10, // 可选:每页显示数量,默认 10
url: 'your-git-url' // Gitea 必填GitHub/Gitee 无需填写
};
---
<GitProjectCollection
platform={GitPlatform.GITEA} // 平台类型GITHUB、GITEA、GITEE
username="your-username" // 可选:覆盖 config 中的用户名
title="Git 项目" // 显示标题
config={giteaConfig} // 平台配置
client:load // Astro 指令:客户端加载
/>
```
## 观影和读书记录
### MediaGrid 组件
`MediaGrid` 组件用于展示豆瓣的观影和读书记录。
#### 基本用法
```astro
---
import MediaGrid from '@/components/MediaGrid.astro';
---
// 展示电影记录
<MediaGrid
type="movie" // 类型movie 或 book
title="我看过的电影" // 显示标题
doubanId={DOUBAN_ID} // 使用配置文件中的豆瓣ID
/>
// 展示读书记录
<MediaGrid
type="book"
title="我读过的书"
doubanId={DOUBAN_ID}
/>
```
## 主题切换
系统支持三种主题模式:
1. 跟随系统
2. 手动切换浅色模式
3. 手动切换深色模式
主题设置会被保存在浏览器的 localStorage 中。
## 快速开始
### 环境要求
- Node.js 18+
- npm 或 pnpm
### 安装步骤
1. 克隆项目
```bash
git clone https://github.com/your-username/echoes.git
cd echoes
```
2. 安装依赖
```bash
npm install
# 或者使用 pnpm
pnpm install
```
3. 修改配置
编辑 `src/consts.ts` 文件,更新网站配置信息。
4. 本地运行
```bash
npm run dev
# 或者使用 pnpm
pnpm dev
```
访问 `http://localhost:4321` 查看效果。
## 部署说明
### 本地构建部署
```bash
npm run build
```
构建产物位于 `dist` 目录,将其部署到你的服务器即可。
### Vercel 部署
本项目完全支持 Vercel 部署,你可以通过以下步骤快速部署:
1. Fork 本项目到你的 GitHub 账号
2. 在 Vercel 控制台中点击 "New Project"
3. 导入你 fork 的 GitHub 仓库
4. 配置构建选项:
- Framework Preset: Astro
- Build Command: `astro build`
- Output Directory: `dist`
- Install Command: `npm install``pnpm install`
5. 点击 "Deploy" 开始部署
Vercel 会自动检测项目类型并应用正确的构建配置。每次你推送代码到 main 分支时Vercel 都会自动重新部署。
#### 环境变量配置
如果你使用了需要环境变量的功能(如 API tokens需要在 Vercel 项目设置中的 "Environment Variables" 部分添加相应的环境变量。
## 常见问题
1. **图片无法显示**
- 检查图片路径是否正确
- 确保图片已放入 `public` 目录
2. **豆瓣数据无法获取**
- 确认豆瓣 ID 配置正确
- 检查豆瓣记录是否公开
3. **Git 项目无法显示**
- 验证用户名配置
- 确认 API 访问限制

View File

@ -1,226 +0,0 @@
---
title: "echoes博客使用说明"
date: 2025-03-09T01:07:23Z
tags: []
---
这是一个基于 Astro + React 构建的个人博客系统,具有文章管理、项目展示、观影记录、读书记录等功能。本文将详细介绍如何使用和配置这个博客系统。
## 功能特点
1. **响应式设计**:完美适配桌面端和移动端
2. **深色模式**:支持自动和手动切换深色/浅色主题
3. **文章系统**:支持 Markdown 写作,带有标签和分类
4. **项目展示**:支持展示 GitHub、Gitea 和 Gitee 的项目
5. **观影记录**:集成豆瓣观影数据
6. **读书记录**:集成豆瓣读书数据
7. **旅行地图**:世界地图展示旅行足迹
## 基础配置
主要配置文件位于 `src/consts.ts`,你需要修改以下内容:
```typescript
// 网站基本信息
export const SITE_URL = 'https://your-domain.com';
export const SITE_NAME = "你的网站名称";
// 导航链接
export const NAV_LINKS = [
{ href: '/', text: '首页' },
{ href: '/articles', text: '文章' },
// ... 可以根据需要修改
];
// 备案信息(如果需要)
export const ICP = '你的ICP备案号';
export const PSB_ICP = '你的公安备案号';
export const PSB_ICP_URL = '备案链接';
// 旅行足迹
export const VISITED_PLACES = ['你去过的地方'];
// 豆瓣ID
export const DOUBAN_ID = '你的豆瓣ID';
// Git平台配置
export const GIT_CONFIG = {
github: {
username: '你的GitHub用户名',
token: '你的GitHub令牌' // 可选
},
gitea: {
url: '你的Gitea实例地址',
username: '你的Gitea用户名',
token: '你的Gitea令牌' // 可选
},
gitee: {
username: '你的Gitee用户名',
token: '你的Gitee令牌' // 可选
}
};
```
## 文章写作
### 创建新文章
`src/content/articles` 目录下创建 `.md``.mdx` 文件。文章需要包含以下前置信息:
```markdown
---
title: "文章标题"
date: YYYY-MM-DD
tags: ["标签1", "标签2"]
---
文章内容...
```
### 文章列表展示
文章列表页面会自动获取所有文章并按日期排序展示,支持:
- 文章标题和摘要
- 发布日期
- 标签系统
- 阅读时间估算
## 项目展示
项目展示页面会自动从配置的 Git 平台获取你的项目信息,展示:
- 项目名称和描述
- Star 和 Fork 数
- 主要编程语言
- 最后更新时间
要启用项目展示,需要:
1. 在 `consts.ts` 中配置相应平台的用户名
2. 如果需要访问私有仓库,配置相应的访问令牌
## 观影和读书记录
系统会自动从豆瓣获取你的观影和读书记录,展示:
- 电影/书籍封面
- 标题
- 评分
- 观看/阅读日期
要启用此功能,需要:
1. 在 `consts.ts` 中配置你的豆瓣 ID
2. 确保你的豆瓣观影/读书记录是公开的
## 旅行地图
世界地图会根据 `VISITED_PLACES` 配置自动标记你去过的地方。支持:
- 中国省份级别的标记
- 世界国家级别的标记
- 交互式缩放和平移
- 鼠标悬停显示地名
## 主题切换
系统支持三种主题模式:
1. 跟随系统
2. 手动切换浅色模式
3. 手动切换深色模式
主题设置会被保存在浏览器的 localStorage 中。
## 性能优化
本博客系统采用了多项性能优化措施:
1. 静态页面生成
2. 图片懒加载
3. 代码分割
4. 样式按需加载
5. 响应式图片
## 部署说明
### 传统部署
1. 构建项目:
```bash
npm run build
```
2. 构建产物位于 `dist` 目录
3. 将 `dist` 目录部署到你的服务器或静态托管平台
### Vercel 部署
Vercel 提供了最简单的部署方式,只需要几个步骤:
1. 在 GitHub 上创建你的项目仓库并推送代码
2. 访问 [Vercel](https://vercel.com) 并使用 GitHub 账号登录
3. 点击 "New Project",然后选择你的博客仓库
4. 配置部署选项:
- Framework Preset: 选择 "Astro"
- Build Command: 保持默认的 `npm run build`
- Output Directory: 保持默认的 `dist`
- Install Command: 保持默认的 `npm install`
5. 点击 "Deploy" 按钮
部署完成后Vercel 会自动为你的项目分配一个域名。你也可以在项目设置中添加自定义域名。
**优势:**
- 自动构建和部署
- 自动 HTTPS
- 全球 CDN
- 自动预览部署(每个 PR 都会生成预览链接)
- 零配置持续部署
## 常见问题
1. **图片无法显示**
- 检查图片路径是否正确
- 确保图片已放入 `public` 目录
2. **豆瓣数据无法获取**
- 确认豆瓣 ID 配置正确
- 检查豆瓣记录是否公开
3. **Git 项目无法显示**
- 验证用户名配置
- 检查访问令牌是否有效
- 确认 API 访问限制
## 更新日志
### 2024-03-21
- 初始版本发布
- 支持基本的博客功能
- 集成豆瓣数据展示
- 添加旅行地图功能
## 后续计划
1. 添加评论系统
2. 优化移动端体验
3. 增加更多自定义主题选项
4. 添加文章搜索功能
5. 支持更多外部服务集成
## 贡献指南
欢迎提交 Issue 和 Pull Request 来改进这个博客系统。在提交之前,请确保:
1. 代码符合项目的代码风格
2. 新功能有适当的测试覆盖
3. 文档已经更新

View File

@ -18,12 +18,6 @@ export const GET: APIRoute = async ({ request }) => {
// 获取所有文章
const articles = await getCollection('articles');
// 打印所有文章的ID用于调试
console.log('所有文章的ID:');
articles.forEach(article => {
console.log(`- ${article.id}`);
});
// 根据条件过滤文章
let filteredArticles = articles;
@ -37,17 +31,10 @@ export const GET: APIRoute = async ({ request }) => {
// 如果有路径过滤直接使用文章ID来判断
if (path) {
const normalizedPath = path.toLowerCase();
console.log('当前过滤路径:', normalizedPath);
filteredArticles = filteredArticles.filter(article => {
const articlePath = article.id.split('/');
console.log('处理文章:', article.id, '分割后:', articlePath);
// 检查文章路径的每一部分
return article.id.toLowerCase().includes(normalizedPath);
});
console.log('过滤后的文章数量:', filteredArticles.length);
}
// 按日期排序(最新的在前面)

View File

@ -1,6 +1,5 @@
import type { APIRoute } from 'astro';
import { load } from 'cheerio';
import { DOUBAN_ID } from '@/consts';
// 添加服务器渲染标记
export const prerender = false;
@ -9,13 +8,21 @@ export const GET: APIRoute = async ({ request }) => {
const url = new URL(request.url);
const type = url.searchParams.get('type') || 'movie';
const start = parseInt(url.searchParams.get('start') || '0');
const doubanId = url.searchParams.get('doubanId'); // 从查询参数获取 doubanId
if (!doubanId) {
return new Response(JSON.stringify({ error: '缺少豆瓣ID' }), {
status: 400,
headers: { 'Content-Type': 'application/json' }
});
}
try {
let doubanUrl = '';
if (type === 'book') {
doubanUrl = `https://book.douban.com/people/${DOUBAN_ID}/collect?start=${start}&sort=time&rating=all&filter=all&mode=grid`;
doubanUrl = `https://book.douban.com/people/${doubanId}/collect?start=${start}&sort=time&rating=all&filter=all&mode=grid`;
} else {
doubanUrl = `https://movie.douban.com/people/${DOUBAN_ID}/collect?start=${start}&sort=time&rating=all&filter=all&mode=grid`;
doubanUrl = `https://movie.douban.com/people/${doubanId}/collect?start=${start}&sort=time&rating=all&filter=all&mode=grid`;
}
const response = await fetch(doubanUrl, {

View File

@ -1,8 +1,7 @@
import type { APIRoute } from 'astro';
import type { APIContext } from 'astro';
import { Octokit } from 'octokit';
import fetch from 'node-fetch';
import { GIT_CONFIG } from '@/consts';
import { GitPlatform, GIT_PLATFORM_CONFIG } from '@/components/GitProjectCollection';
import { GitPlatform } from '@/components/GitProjectCollection';
interface GitProject {
name: string;
@ -24,75 +23,108 @@ interface Pagination {
hasPrev: boolean;
}
export const GET: APIRoute = async ({ request }) => {
export const prerender = false;
export async function GET({ request }: APIContext) {
try {
const url = new URL(request.url);
const platformParam = url.searchParams.get('platform') || 'github';
const platform = platformParam as GitPlatform;
const headers = {
'Access-Control-Allow-Origin': '*',
'Access-Control-Allow-Methods': 'GET, OPTIONS',
'Access-Control-Allow-Headers': 'Content-Type',
'Content-Type': 'application/json'
};
const platformParam = url.searchParams.get('platform');
const page = parseInt(url.searchParams.get('page') || '1');
const username = url.searchParams.get('username') || '';
const organization = url.searchParams.get('organization') || '';
const configStr = url.searchParams.get('config');
try {
if (!platformParam) {
return new Response(JSON.stringify({
error: '无效的平台参数',
receivedPlatform: platformParam,
}), { status: 400, headers });
}
if (!configStr) {
return new Response(JSON.stringify({
error: '缺少配置参数'
}), { status: 400, headers });
}
const config = JSON.parse(configStr);
if (!Object.values(GitPlatform).includes(platformParam as GitPlatform)) {
return new Response(JSON.stringify({
error: '无效的平台参数',
receivedPlatform: platformParam,
}), { status: 400, headers });
}
const platform = platformParam as GitPlatform;
let projects: GitProject[] = [];
let pagination: Pagination = { current: page, total: 1, hasNext: false, hasPrev: page > 1 };
if (platform === GitPlatform.GITHUB) {
const result = await fetchGithubProjects(username, organization, page);
const result = await fetchGithubProjects(username, organization, page, config);
projects = result.projects;
pagination = result.pagination;
} else if (platform === GitPlatform.GITEA) {
const result = await fetchGiteaProjects(username, organization, page);
const result = await fetchGiteaProjects(username, organization, page, config);
projects = result.projects;
pagination = result.pagination;
} else if (platform === GitPlatform.GITEE) {
try {
const result = await fetchGiteeProjects(username, organization, page);
const result = await fetchGiteeProjects(username, organization, page, config);
projects = result.projects;
pagination = result.pagination;
} catch (giteeError) {
// 返回空数据而不是抛出错误
}
}
return new Response(JSON.stringify({ projects, pagination }), {
status: 200,
headers: {
'Content-Type': 'application/json'
}
headers
});
} catch (error) {
let errorMessage = '获取数据失败';
if (error instanceof Error) {
errorMessage = error.message;
}
return new Response(JSON.stringify({
error: errorMessage,
platform
error: '处理请求错误',
message: error instanceof Error ? error.message : '未知错误'
}), {
status: 500,
headers: {
'Content-Type': 'application/json'
'Content-Type': 'application/json',
'Access-Control-Allow-Origin': '*'
}
});
}
};
}
async function fetchGithubProjects(username: string, organization: string, page: number) {
// 添加重试逻辑
export function OPTIONS() {
return new Response(null, {
status: 204,
headers: {
'Access-Control-Allow-Origin': '*',
'Access-Control-Allow-Methods': 'GET, OPTIONS',
'Access-Control-Allow-Headers': 'Content-Type'
}
});
}
async function fetchGithubProjects(username: string, organization: string, page: number, config: any) {
const maxRetries = 3;
let retryCount = 0;
while (retryCount < maxRetries) {
try {
const octokit = new Octokit({
auth: GIT_PLATFORM_CONFIG.platforms[GitPlatform.GITHUB].token || process.env.GITHUB_TOKEN,
auth: process.env.GITHUB_TOKEN,
request: {
timeout: 10000 // 增加超时时间到10秒
timeout: 10000
}
});
const perPage = GIT_CONFIG.perPage;
const perPage = config.perPage || 10;
let repos;
if (organization) {
@ -114,10 +146,8 @@ async function fetchGithubProjects(username: string, organization: string, page:
});
repos = data;
} else {
// 如果没有指定用户或组织,使用默认用户名
const defaultUsername = GIT_PLATFORM_CONFIG.platforms[GitPlatform.GITHUB].username;
const { data } = await octokit.request('GET /users/{username}/repos', {
username: defaultUsername,
username: config.username,
per_page: perPage,
page: page,
sort: 'updated',
@ -126,20 +156,16 @@ async function fetchGithubProjects(username: string, organization: string, page:
repos = data;
}
// 替换获取分页信息的代码
let hasNext = false;
let hasPrev = page > 1;
let totalPages = 1;
// 使用响应头中的 Link 信息
if (repos.length === perPage) {
hasNext = true;
totalPages = page + 1;
}
// 或者使用 GitHub API 的 repository_count 估算
if (repos.length > 0 && repos[0].owner) {
// 简单估算:如果有结果且等于每页数量,则可能有下一页
hasNext = repos.length === perPage;
totalPages = hasNext ? page + 1 : page;
}
@ -173,12 +199,10 @@ async function fetchGithubProjects(username: string, organization: string, page:
throw error;
}
// 等待一段时间后重试
await new Promise(resolve => setTimeout(resolve, 2000 * retryCount));
}
}
// 添加默认返回值,防止 undefined
return {
projects: [],
pagination: {
@ -190,17 +214,10 @@ async function fetchGithubProjects(username: string, organization: string, page:
};
}
async function fetchGiteaProjects(username: string, organization: string, page: number) {
async function fetchGiteaProjects(username: string, organization: string, page: number, config: any) {
try {
// 使用consts中的配置
const perPage = GIT_CONFIG.perPage;
const platformConfig = GIT_PLATFORM_CONFIG.platforms[GitPlatform.GITEA];
if (!platformConfig) {
throw new Error('Gitea 平台配置不存在');
}
const giteaUrl = platformConfig.url;
const perPage = config.perPage || 10;
const giteaUrl = config.url;
if (!giteaUrl) {
throw new Error('Gitea URL 不存在');
@ -212,16 +229,13 @@ async function fetchGiteaProjects(username: string, organization: string, page:
} else if (username) {
apiUrl = `${giteaUrl}/api/v1/users/${username}/repos?page=${page}&per_page=${perPage}`;
} else {
const defaultUsername = GIT_PLATFORM_CONFIG.platforms[GitPlatform.GITEA].username;
apiUrl = `${giteaUrl}/api/v1/users/${defaultUsername}/repos?page=${page}&per_page=${perPage}`;
apiUrl = `${giteaUrl}/api/v1/users/${config.username}/repos?page=${page}&per_page=${perPage}`;
}
const response = await fetch(apiUrl, {
headers: {
'Accept': 'application/json',
...(GIT_PLATFORM_CONFIG.platforms[GitPlatform.GITEA].token ?
{ 'Authorization': `token ${GIT_PLATFORM_CONFIG.platforms[GitPlatform.GITEA].token}` } :
{})
...(config.token ? { 'Authorization': `token ${config.token}` } : {})
}
});
@ -231,10 +245,8 @@ async function fetchGiteaProjects(username: string, organization: string, page:
const data = await response.json() as any;
// Gitea API 返回的是数组
const repos = Array.isArray(data) ? data : [];
// 获取分页信息
const totalCount = parseInt(response.headers.get('X-Total-Count') || '0');
const totalPages = Math.ceil(totalCount / perPage) || 1;
@ -261,7 +273,6 @@ async function fetchGiteaProjects(username: string, organization: string, page:
}
};
} catch (error) {
// 返回空数据而不是抛出错误
return {
projects: [],
pagination: {
@ -274,19 +285,16 @@ async function fetchGiteaProjects(username: string, organization: string, page:
}
}
async function fetchGiteeProjects(username: string, organization: string, page: number) {
async function fetchGiteeProjects(username: string, organization: string, page: number, config: any) {
try {
// 使用consts中的配置
const perPage = GIT_CONFIG.perPage;
const perPage = config.perPage || 10;
// 确定用户名
const giteeUsername = username || GIT_CONFIG.gitee.username;
const giteeUsername = username || config.username;
if (!giteeUsername) {
throw new Error('Gitee 用户名未配置');
}
// 构建API URL
let apiUrl;
if (organization) {
apiUrl = `https://gitee.com/api/v5/orgs/${organization}/repos?page=${page}&per_page=${perPage}&sort=updated&direction=desc`;
@ -294,9 +302,8 @@ async function fetchGiteeProjects(username: string, organization: string, page:
apiUrl = `https://gitee.com/api/v5/users/${giteeUsername}/repos?page=${page}&per_page=${perPage}&sort=updated&direction=desc`;
}
// 添加访问令牌(如果有)
if (GIT_CONFIG.gitee.token) {
apiUrl += `&access_token=${GIT_CONFIG.gitee.token}`;
if (config.token) {
apiUrl += `&access_token=${config.token}`;
}
const response = await fetch(apiUrl);
@ -307,7 +314,6 @@ async function fetchGiteeProjects(username: string, organization: string, page:
const data = await response.json() as any[];
// 转换数据格式
const projects: GitProject[] = data.map(repo => ({
name: repo.name || '',
description: repo.description || '',
@ -321,7 +327,6 @@ async function fetchGiteeProjects(username: string, organization: string, page:
platform: GitPlatform.GITEE
}));
// 获取分页信息
const totalCount = parseInt(response.headers.get('total_count') || '0');
const totalPages = Math.ceil(totalCount / perPage) || 1;
@ -335,7 +340,6 @@ async function fetchGiteeProjects(username: string, organization: string, page:
}
};
} catch (error) {
// 返回空结果
return {
projects: [],
pagination: {

View File

@ -1,9 +1,13 @@
---
import Layout from '@/components/Layout.astro';
import MediaGrid from '@/components/MediaGrid.astro';
import { SITE_NAME } from '@/consts';
import { SITE_NAME, DOUBAN_ID } from '@/consts';
---
<Layout title={`图书 - ${SITE_NAME}`}>
<MediaGrid type="book" title="我读过的书" />
<MediaGrid
type="book"
title="我读过的书"
doubanId={DOUBAN_ID}
/>
</Layout>

View File

@ -1,9 +1,13 @@
---
import Layout from '@/components/Layout.astro';
import MediaGrid from '@/components/MediaGrid.astro';
import { SITE_NAME } from '@/consts.ts';
import { SITE_NAME, DOUBAN_ID } from '@/consts.ts';
---
<Layout title={`电影 - ${SITE_NAME}`}>
<MediaGrid type="movie" title="我看过的电影" />
<MediaGrid
type="movie"
title="我看过的电影"
doubanId={DOUBAN_ID}
/>
</Layout>

View File

@ -1,11 +1,8 @@
---
import Layout from "@/components/Layout.astro";
import { Countdown } from "@/components/Countdown";
// 恢复静态导入
import WorldHeatmap from '@/components/WorldHeatmap';
// 移除动态导入
// const WorldHeatmap = await import('@/components/WorldHeatmap').then(mod => mod.default);
import { VISITED_PLACES } from '@/consts';
---
<Layout title="退休倒计时">
@ -20,7 +17,10 @@ import WorldHeatmap from '@/components/WorldHeatmap';
<section class="mb-16">
<h2 class="text-3xl font-semibold text-center mb-6">我的旅行足迹</h2>
<div class="mx-auto bg-white dark:bg-gray-800 rounded-xl shadow-lg p-6 transition-all hover:shadow-xl">
<WorldHeatmap client:only="react" />
<WorldHeatmap
client:only="react"
visitedPlaces={VISITED_PLACES}
/>
</div>
</section>
</div>

View File

@ -1,22 +1,24 @@
---
import Layout from '@/components/Layout.astro';
import { GitPlatform } from '@/components/GitProjectCollection';
import GitProjectCollection from '@/components/GitProjectCollection';
import { GitPlatform, type GitConfig } from '@/components/GitProjectCollection';
const giteaConfig: GitConfig = {
username: 'lsy',
url: 'https://g.lsy22.com',
};
---
<Layout title="项目 | echoes">
<main class="container mx-auto px-4 py-8">
<div class="space-y-12">
<!-- Gitea 项目集合 -->
<GitProjectCollection
platform={GitPlatform.GITEA}
username="lsy"
title="Gitea 个人项目"
config={giteaConfig}
client:load
/>
</div>
</main>
</Layout>