Skip to content

Commit 890b3a7

Browse files
committed
feat: 添加 DocHistoryPanel 组件并挂载至文档页,展示最近更新列表
1 parent eb4edfe commit 890b3a7

2 files changed

Lines changed: 137 additions & 0 deletions

File tree

app/components/DocHistoryPanel.tsx

Lines changed: 133 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,133 @@
1+
"use client";
2+
3+
import { useEffect, useState } from "react";
4+
import Image from "next/image";
5+
import type { HistoryItem } from "@/app/api/docs/history/route";
6+
7+
interface DocHistoryPanelProps {
8+
path: string;
9+
}
10+
11+
// 将 ISO 日期转为相对时间描述(中文)
12+
function relativeTime(dateStr: string): string {
13+
const diff = Date.now() - new Date(dateStr).getTime();
14+
const minutes = Math.floor(diff / 60_000);
15+
if (minutes < 1) return "刚刚";
16+
if (minutes < 60) return `${minutes} 分钟前`;
17+
const hours = Math.floor(minutes / 60);
18+
if (hours < 24) return `${hours} 小时前`;
19+
const days = Math.floor(hours / 24);
20+
if (days < 30) return `${days} 天前`;
21+
const months = Math.floor(days / 30);
22+
if (months < 12) return `${months} 个月前`;
23+
return `${Math.floor(months / 12)} 年前`;
24+
}
25+
26+
// 骨架屏占位行
27+
function SkeletonRow() {
28+
return (
29+
<div className="flex items-center gap-3 py-2.5 animate-pulse">
30+
<div className="w-6 h-6 rounded-full bg-neutral-200 dark:bg-neutral-700 shrink-0" />
31+
<div className="flex-1 flex flex-col gap-1">
32+
<div className="h-3 w-2/3 rounded bg-neutral-200 dark:bg-neutral-700" />
33+
<div className="h-2.5 w-1/3 rounded bg-neutral-100 dark:bg-neutral-800" />
34+
</div>
35+
</div>
36+
);
37+
}
38+
39+
export function DocHistoryPanel({ path }: DocHistoryPanelProps) {
40+
const [items, setItems] = useState<HistoryItem[] | null>(null);
41+
const [error, setError] = useState<string | null>(null);
42+
43+
useEffect(() => {
44+
let cancelled = false;
45+
fetch(`/api/docs/history?path=${encodeURIComponent(path)}`)
46+
.then((r) => r.json())
47+
.then((json) => {
48+
if (cancelled) return;
49+
if (json.success) {
50+
setItems(json.data);
51+
} else {
52+
setError(json.error ?? "无法加载历史");
53+
}
54+
})
55+
.catch(() => {
56+
if (!cancelled) setError("无法加载历史");
57+
});
58+
return () => {
59+
cancelled = true;
60+
};
61+
}, [path]);
62+
63+
return (
64+
<div className="font-serif">
65+
{/* 报纸风格标题 */}
66+
<h2 className="text-xs font-mono uppercase tracking-widest text-neutral-400 dark:text-neutral-500 mb-3 border-b border-neutral-200 dark:border-neutral-700 pb-2">
67+
最近更新
68+
</h2>
69+
70+
{/* 加载中 */}
71+
{items === null && error === null && (
72+
<div className="divide-y divide-neutral-100 dark:divide-neutral-800">
73+
<SkeletonRow />
74+
<SkeletonRow />
75+
<SkeletonRow />
76+
</div>
77+
)}
78+
79+
{/* 错误 */}
80+
{error !== null && (
81+
<p className="text-xs font-mono text-neutral-400 dark:text-neutral-500 py-2">
82+
{error}
83+
</p>
84+
)}
85+
86+
{/* 空结果 */}
87+
{items !== null && items.length === 0 && (
88+
<p className="text-xs font-mono text-neutral-400 dark:text-neutral-500 py-2">
89+
暂无更新记录
90+
</p>
91+
)}
92+
93+
{/* 历史列表 */}
94+
{items !== null && items.length > 0 && (
95+
<ol className="divide-y divide-neutral-100 dark:divide-neutral-800">
96+
{items.map((item) => (
97+
<li key={item.sha}>
98+
<a
99+
href={item.htmlUrl}
100+
target="_blank"
101+
rel="noopener noreferrer"
102+
className="flex items-start gap-3 py-2.5 group hover:bg-neutral-50 dark:hover:bg-neutral-900 rounded transition-colors px-1 -mx-1"
103+
>
104+
{/* 头像 */}
105+
<Image
106+
src={item.avatarUrl}
107+
alt={item.authorLogin}
108+
width={24}
109+
height={24}
110+
className="rounded-full mt-0.5 shrink-0"
111+
unoptimized
112+
/>
113+
114+
<div className="flex-1 min-w-0">
115+
{/* commit message,截断超长内容 */}
116+
<p className="text-sm leading-snug text-neutral-800 dark:text-neutral-200 truncate group-hover:text-[#CC0000] transition-colors">
117+
{item.message}
118+
</p>
119+
{/* 作者 + 时间,monospace 风格 */}
120+
<p className="text-[11px] font-mono text-neutral-400 dark:text-neutral-500 mt-0.5">
121+
{item.authorName}
122+
<span className="mx-1 opacity-40">·</span>
123+
{relativeTime(item.date)}
124+
</p>
125+
</div>
126+
</a>
127+
</li>
128+
))}
129+
</ol>
130+
)}
131+
</div>
132+
);
133+
}

app/docs/[...slug]/page.tsx

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import { Contributors } from "@/app/components/Contributors";
1414
import { DocsAssistant } from "@/app/components/DocsAssistant";
1515
import { LicenseNotice } from "@/app/components/LicenseNotice";
1616
import { PageFeedback } from "@/app/components/PageFeedback";
17+
import { DocHistoryPanel } from "@/app/components/DocHistoryPanel";
1718
// Extract clean text content from MDX - no longer used on client/page side
1819
// content fetching moved to API route for performance
1920

@@ -60,6 +61,9 @@ export default async function DocPage({ params }: Param) {
6061
<section className="mt-16">
6162
<GiscusComments docId={docIdFromPage ?? null} />
6263
</section>
64+
<section className="mt-12">
65+
<DocHistoryPanel path={page.file.path} />
66+
</section>
6367
<LicenseNotice className="mt-16" />
6468
</DocsBody>
6569
</DocsPage>

0 commit comments

Comments
 (0)