一、项目架构
XHBlogs/ ← 博客前端(展示)
app/anime/page.tsx → 入口
app/anime/AnimeClient.tsx → 展示页组件
app/api/anime/image/route.ts → 图片代理
data/anime-list.json → 番剧数据(管理端同步过来)
#文件操-作说明1my-blog-manager/app/anime/page.tsx新建追番管理页(400+ 行)2my-blog-manager/app/api/anime/route.ts新建Next.js API 路由3my-blog-manager/app/api/anime/image/route.ts新建图片代理(防盗链)4my-blog-manager/data/anime-list.json新建初始数据(可空数组 [])5my-blog-manager/components/Navbar.tsx修改加一行 { name: '动漫', href: '/anime' }6my-blog-manager/cms_core/api/sync.py修改SYNC_FILES 加 "data/anime-list.json"7XHBlogs/app/anime/page.tsx新建博客展示页入口8XHBlogs/app/anime/AnimeClient.tsx新建博客展示页组件9XHBlogs/app/api/anime/image/route.ts新建博客端图片代理10XHBlogs/components/Navbar.tsx修改加一行 { name: '动漫', href: '/anime' }
共 6 个新文件 + 4 个修改
三、管理端 — 追番管理页面
my-blog-manager/app/anime/page.tsx
页面结构
- 标题栏:"追番管理"标题 + 副标题 + "添加番剧"按钮
- 统计卡片:总追番 / 平均评分 / TV 动画 / 剧场版(4栏)
- 搜索筛选栏:搜索框 + 类型筛选(全部/TV/剧场版)+ 排序下拉
- 卡片网格:响应式 2~5 列,每页 10 张,分页控件始终显示
- 添加弹窗:粘贴链接 → 获取信息 → 预览编辑 → 添加到列表
- 删除弹窗:确认删除
核心功能列表
功能实现📊 统计4 个 StatCard 组件,读取 animeList 计算🔍 搜索标题、标签模糊匹配🏷️ 筛选useState typeFilter: 'all'|'tv'|'movie'📋 排序默认不排序(保持自定义顺序)/ 评分 / 年份📄 分页perPage=10,始终显示分页控件➕ 添加弹窗 → 粘贴链接 → POST /api/anime → 自动抓取 → 预览 → 保存✏️ 预览编辑标题、封面链接、年份、标签(用/分隔)、TV↔剧场版切换🗑️ 删除确认弹窗 → DELETE /api/anime?id=xxx❤️ 收藏封面左上爱心按钮 → PATCH /api/anime?id=xxx🔄 拖拽排序HTML5 Drag & Drop + POST /api/anime {ids:[...]}
关键 State
animeList: Anime[] — 番剧列表
loading: boolean — 加载中
search: string — 搜索词
typeFilter: 'all'|'tv'|'movie'
sortBy: 'default'|'rating-desc'|'rating-asc'|'year-desc'|'year-asc'
page: number — 当前页
addOpen: boolean — 添加弹窗
url: string — 输入框的链接
fetching/saving: boolean — 加载状态
preview: Anime | null — 抓取到的预览数据
deleteTarget: Anime | null — 待删除项
dragItem: Anime | null — 拖拽中的卡片
接口定义
interface Anime {
id: string; title: string; originalTitle: string; poster: string;
type: 'tv' | 'movie'; rating: number; year: string; overview: string;
link: string; source: string; epStatus?: string; tags?: string[];
favorite?: boolean;
}
来源名称映射: bilibili→哔哩哔哩, tencent→腾讯视频, iqiyi→爱奇艺, youku→优酷, douban→豆瓣
四、管理端 — Next.js API 路由
my-blog-manager/app/api/anime/route.ts
GET → 读取 data/anime-list.json → { animeList: [...] }
POST → body.ids → 重排序(保存拖拽后的新顺序)
→ body.url → 抓取番剧信息
├─ Bilibili: 调 api.bilibili.com/pgc/view/web/season?season_id={ssid}
└─ 其他平台: 抓取网页 HTML → 解析 og:title/og:image/og:description
→ body.anime → 添加到列表(去重检查)
PATCH → ?id=xxx → 切换 favorite 状态
DELETE → ?id=xxx → 从列表删除
Bilibili API 调用
const url = `https://api.bilibili.com/pgc/view/web/season?season_id=${ssid}`;
const res = await fetch(url, {
headers: { 'User-Agent': 'Mozilla/5.0', 'Referer': 'https://www.bilibili.com/' }
});
// 返回: title, cover, rating, evaluate, season_type_name, new_ep, styles 等
通用 Open Graph 抓取
const html = await fetch(pageUrl).then(r => r.text());
const title = og('title') || html.match(/<title>([^<]+)<\/title>/)?.[1];
const poster = og('image');
const description = og('description');
五、管理端 — 图片代理
my-blog-manager/app/api/anime/image/route.ts
export async function GET(req: Request) {
const url = new URL(req.url).searchParams.get('url');
const res = await fetch(url, {
headers: { 'Referer': 'https://www.bilibili.com/', 'User-Agent': 'Mozilla/5.0' }
});
const buffer = await res.arrayBuffer();
return new Response(buffer, {
headers: { 'Content-Type': res.headers.get('content-type') || 'image/jpeg',
'Cache-Control': 'public, max-age=86400' }
});
}
**为什么需要代理:**Bilibili 图片 CDN 检查 Referer 头。浏览器直接 不传 Referer 会被拒。服务端代理加上 Referer 头即可正常获取。
**判断是否需要代理:**检查 URL 是否包含 hdslb.com 或 bilibili,不要只凭 source 字段判断(用户可能手动粘贴 Bilibili 图片 URL 但 source 是 other)。
六、导航栏修改
my-blog-manager/components/Navbar.tsx
const navLinks = [
...
{ name: '音乐', href: '/music' },
{ name: '动漫', href: '/anime' }, // ← 加这一行
{ name: '🌳 灵境', href: '/tree' },
...
];
XHBlogs/components/Navbar.tsx 同样加一行。
七、同步配置
my-blog-manager/cms_core/api/sync.py
SYNC_FILES = [
...
"data/anime-list.json", // ← 加这一行
...
]
八、博客前端 — 展示页
入口 XHBlogs/app/anime/page.tsx
import AnimeClient from "./AnimeClient";
export const metadata = { title: "追番列表 | " + siteConfig.title };
export default function AnimePage() { return <AnimeClient />; }
组件 XHBlogs/app/anime/AnimeClient.tsx
读取 /api/anime(或直接读 data/anime-list.json),展示统计卡片 + 卡片网格 + 搜索筛选 + 分页。
**图片处理:**Bilibili 封面走 /api/anime/image?url=... 代理
**卡片特性:**骨架占位(onLoad opacity 切换)、评分角标、类型/来源标签、进度、悬停遮罩
来源名映射:{ bilibili:'哔哩哔哩', tencent:'腾讯', iqiyi:'爱奇艺', youku:'优酷', douban:'豆瓣' }
图片代理 XHBlogs/app/api/anime/image/route.ts
同管理端,复制即可。
九、避坑清单
坑解决方案webview 没有地址栏,用户找不到管理页管理页做成独立页面 + Navbar 入口,不要塞到 admin tabwindow_config.json 不存在API 端口配置在 public/backend_config.json依赖 CMS FastAPI 动态端口经常连不上用 Next.js API 直接读写本地 JSON,不依赖 CMS 后端Bilibili 封面图片不显示必须走代理加 Referer 头非 Bilibili 平台没有年份预览区加可编辑年份输入框非 Bilibili 平台封面为空 →
报错封面为空时不渲染 img,显示占位图标手动粘贴的 Bilibili 封面不显示判断是否走代理要看 URL 内容(含 hdslb.com),不是看 source 字段标签不匹配 导致按钮失效仔细检查每一个 HTML 标签闭合拖拽排序后又被评分排序覆盖默认 sortBy 改为 'default'(不排序),保持手动顺序卡片图片可拖拽 → 抢走卡片拖拽事件图片加 draggable={false}收藏卡片排名被评分排序覆盖收藏排序始终在最外层(不管选什么排序)分页不显示 → 新加的番剧看不到perPage=10,分页始终显示。添加后自动跳到最后一页
十、数据流
┌─────────────────────────────────────────────────────┐
│ 管理端 (my-blog-manager) │
│ │
│ Navbar "动漫" → /anime │
│ ├─ 点"添加番剧" → 弹窗 │
│ │ ├─ 粘贴链接 → POST /api/anime {url} │
│ │ │ ├─ Bilibili → API 抓取 │
│ │ │ └─ 其他平台 → Open Graph 抓取 │
│ │ ├─ 预览编辑(封面/年份/标签/类型) │
│ │ └─ 添加到列表 → POST /api/anime {anime} │
│ │ → 写入 data/anime-list.json │
│ ├─ 爱心收藏 → PATCH /api/anime?id=xxx │
│ ├─ 拖拽排序 → POST /api/anime {ids:[...]} │
│ └─ 删除 → DELETE /api/anime?id=xxx │
│ │
│ Navbar "同步本地" → /api/sync/execute │
│ → 复制 data/anime-list.json 到 XHBlogs/data/ │
└─────────────────────────────────────────────────────┘
↓
┌─────────────────────────────────────────────────────┐
│ 博客前端 (XHBlogs) │
│ │
│ Navbar "动漫" → /anime │
│ → AnimeClient 读取 data/anime-list.json │
│ → 展示卡片网格 + 搜索 + 筛选 + 分页 │
└─────────────────────────────────────────────────────┘
十一、初始化数据示例
data/anime-list.json 初始内容:
[]
空数组即可。第一条数据由管理端添加。

