- Blog 列表: 搜索框 + 标签云 + 归档入口 - 新路由: /tag/:name、/archive.html - 详情: views/字数/标签 chips/点赞(乐观更新)/前后篇/可折叠 TOC - 嵌套评论任意层级递归渲染 + 回复预填; Write 加 tags 输入
92 lines
3.6 KiB
React
92 lines
3.6 KiB
React
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>
|
||
);
|
||
}
|