feat(blog): PostArticle 全文阅读页(编辑/删除二次确认)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
parent
f1487f509e
commit
6620be379b
123
Client/src/components/PostArticle.tsx
Normal file
123
Client/src/components/PostArticle.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user