feat(blog): PostArticle 全文阅读页(编辑/删除二次确认)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
cc 2026-06-27 16:53:03 +08:00
parent f1487f509e
commit 6620be379b

View File

@ -0,0 +1,123 @@
import { useEffect, useState } from 'react'
import { deletePost, getPost, readingTime, type Post } from '../api/posts'
import { Markdown } from './Markdown'
interface PostArticleProps {
token: string
id: string
onBack: () => void
onEdit: (post: Post) => void
onDeleted: () => void
}
function formatDate(secs: number): string {
return new Date(secs * 1000).toLocaleDateString('zh-CN', {
year: 'numeric',
month: 'short',
day: 'numeric',
})
}
export function PostArticle({ token, id, onBack, onEdit, onDeleted }: PostArticleProps) {
const [post, setPost] = useState<Post | null>(null)
const [status, setStatus] = useState<'loading' | 'ok' | 'notfound'>('loading')
const [confirmDel, setConfirmDel] = useState(false)
const [deleting, setDeleting] = useState(false)
useEffect(() => {
let alive = true
setStatus('loading')
getPost(token, id)
.then((p) => {
if (alive) {
setPost(p)
setStatus('ok')
}
})
.catch(() => {
if (alive) setStatus('notfound')
})
return () => {
alive = false
}
}, [token, id])
const doDelete = async () => {
if (!post) return
setDeleting(true)
try {
await deletePost(token, post.id)
onDeleted()
} catch {
setDeleting(false)
setConfirmDel(false)
}
}
if (status === 'loading') {
return <section className="w-full px-6 py-14 text-sm text-muted"></section>
}
if (status === 'notfound' || !post) {
return (
<section className="w-full px-6 py-14">
<p className="text-sm text-muted"></p>
<button
onClick={onBack}
className="mt-4 rounded-full border border-line px-4 py-1.5 text-xs uppercase tracking-[0.16em] text-muted transition-colors hover:border-accent hover:text-accent"
>
</button>
</section>
)
}
return (
<article className="w-full px-6 py-14">
<div className="mb-8 flex items-center justify-between">
<button
onClick={onBack}
className="text-xs uppercase tracking-[0.16em] text-muted transition-colors hover:text-accent"
>
Back
</button>
<div className="flex items-center gap-3">
<button
onClick={() => onEdit(post)}
className="text-xs uppercase tracking-[0.16em] text-muted transition-colors hover:text-accent"
>
</button>
{confirmDel ? (
<button
onClick={doDelete}
disabled={deleting}
className="text-xs uppercase tracking-[0.16em] text-[#ff8585] disabled:opacity-50"
>
{deleting ? '删除中…' : '确认删除'}
</button>
) : (
<button
onClick={() => setConfirmDel(true)}
className="text-xs uppercase tracking-[0.16em] text-muted transition-colors hover:text-[#ff8585]"
>
</button>
)}
</div>
</div>
<div className="mb-2 font-mono text-xs uppercase tracking-wider text-accent/70">
{post.tag}
</div>
<h1 className="rainbow-clip font-display text-4xl font-medium text-text">{post.title}</h1>
<div className="mt-3 font-mono text-xs uppercase tracking-wider text-muted">
{formatDate(post.createdAt)} · {readingTime(post.body)} read
</div>
<div className="rainbow-gradient mt-6 mb-8 h-px w-full opacity-60" aria-hidden />
<Markdown>{post.body}</Markdown>
</article>
)
}