Files
rainblogweb/frontend/src/pages/Archive.jsx
T
miaomiao 3cf560f932 feat: 博客功能前台 - 搜索/标签云/标签页/归档/详情增强/嵌套评论
- Blog 列表: 搜索框 + 标签云 + 归档入口
- 新路由: /tag/:name、/archive.html
- 详情: views/字数/标签 chips/点赞(乐观更新)/前后篇/可折叠 TOC
- 嵌套评论任意层级递归渲染 + 回复预填; Write 加 tags 输入
2026-08-07 02:06:01 +08:00

92 lines
3.6 KiB
React
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 React, { useEffect, useState } from 'react';
import { Link } from 'react-router-dom';
import { getArchive, listPosts } from '../api/blog.js';
/** 归档页(路由 /archive.html):
* archive API 只返回 {month, count},无按月文章列表接口;
* 列表接口 /blog/posts 无分页返回全部已发布文章(文章量小),
* 前端按 created_at 客户端分组,配合 archive API 的月份与计数展示。
*/
export default function Archive() {
const [months, setMonths] = useState([]); // [{month, count, posts:[]}]
const [loading, setLoading] = useState(true);
const [error, setError] = useState('');
useEffect(() => {
Promise.all([getArchive(), listPosts()])
.then(([arc, posts]) => {
const byMonth = new Map();
(posts || []).forEach((p) => {
const m = (p.created_at || '').slice(0, 7); // YYYY-MM
if (!m) return;
if (!byMonth.has(m)) byMonth.set(m, []);
byMonth.get(m).push(p);
});
// 以 archive API 的月份顺序为准(desc),count 以实际分组统计为准
const countMap = new Map();
(arc || []).forEach((a) => countMap.set(a.month, a.count));
const list = Array.from(byMonth.entries()).map(([month, ps]) => ({
month,
count: ps.length,
posts: ps,
}));
// 补齐 archive 里有、分组里没有的月份(理论上不会出现)
(arc || []).forEach((a) => {
if (!byMonth.has(a.month)) list.push({ month: a.month, count: a.count, posts: [] });
});
list.sort((a, b) => (a.month < b.month ? 1 : -1));
setMonths(list);
})
.catch((e) => setError(e.message || '加载失败'))
.finally(() => setLoading(false));
}, []);
const fmtMonth = (m) => {
const [y, mm] = m.split('-');
return `${y}${mm}月`;
};
return (
<div className="blog-article" style={{ maxWidth: 720, margin: '0 auto' }}>
<Link to="/blog.html" className="btn btn-text btn-sm" style={{ marginBottom: 16 }}>
<span className="material-icons" style={{ fontSize: 16 }}>arrow_back</span> 返回博客
</Link>
<h1 className="article-title" style={{ fontSize: 26 }}>
文章归档
{!loading && months.length > 0 && (
<span className="tag-title-count"> {months.reduce((s, m) => s + m.count, 0)} </span>
)}
</h1>
{loading && <div className="loading"><div className="spinner"></div></div>}
{error && (
<div className="empty-state"><div className="empty-icon">⚠️</div><p>{error}</p></div>
)}
{!loading && !error && months.length === 0 && (
<div className="empty-state"><div className="empty-icon">🗂️</div><p>暂无文章</p></div>
)}
<div className="archive-list">
{months.map((m) => (
<section key={m.month} className="archive-month">
<h2 className="archive-month-title">
{fmtMonth(m.month)}
<span className="archive-month-count">{m.count} </span>
</h2>
{m.posts.length === 0 ? (
<p className="text-muted" style={{ fontSize: 13 }}>暂无文章</p>
) : (
m.posts.map((p) => (
<Link key={p.id} to={`/blog/${p.id}`} className="archive-item">
<span className="archive-item-title">{p.title}</span>
<span className="archive-item-date">{(p.created_at || '').slice(0, 10)}</span>
</Link>
))
)}
</section>
))}
</div>
</div>
);
}