- 状态点自制徽标改用 Badge,回收站剩余天数紧急态改用红色 Tag - 危险按钮改用 danger 属性并补充语言,text+danger 不套实心红样式 - 后台搜索框改 antd Input 前缀形态,删除手写容器与聚焦环样式 - 加载态统一 Spin/Skeleton(Table 用 loading),空态统一 Empty - Segmented 选项计数改用 Badge count,弱化为 muted 灰 - 首页分页说明改 Pagination showTotal,笔记错误态改 Empty 承接布局 - 代码块复制按钮改 antd Button 并增加「已复制」反馈,修复悬浮不显示 - 博客/后台图标按钮与溢出菜单触发器统一为 antd Button type=text
118 lines
3.9 KiB
TypeScript
118 lines
3.9 KiB
TypeScript
import { Link, useNavigate, useParams } from 'react-router'
|
|
import { useQuery } from '@tanstack/react-query'
|
|
import { ArrowLeft, ArrowRight, Clock, Eye } from 'lucide-react'
|
|
import { Alert, Button, Card, Empty, Skeleton, Tag } from 'antd'
|
|
import { api } from '../lib/api'
|
|
import type { NoteDetail } from '../lib/api'
|
|
import { useAuth } from '../lib/auth'
|
|
import { useDocumentTitle } from '../features/site'
|
|
import MarkdownViewer from '../components/MarkdownViewer'
|
|
import { formatDateISO, readingMinutes } from '../lib/utils'
|
|
|
|
export default function Note() {
|
|
const { slug = '' } = useParams()
|
|
const navigate = useNavigate()
|
|
const { authenticated } = useAuth()
|
|
const { data: note, isLoading, error } = useQuery({
|
|
queryKey: ['note', slug],
|
|
queryFn: () => api<NoteDetail>(`/api/notes/${encodeURIComponent(slug)}`),
|
|
retry: false,
|
|
})
|
|
useDocumentTitle(note?.title)
|
|
|
|
if (isLoading) {
|
|
return (
|
|
<article className="pn-note-article">
|
|
<Skeleton active title paragraph={{ rows: 6 }} />
|
|
</article>
|
|
)
|
|
}
|
|
if (error || !note) {
|
|
return (
|
|
<article className="pn-note-article">
|
|
<Empty image={Empty.PRESENTED_IMAGE_SIMPLE} description="笔记不存在或未公开">
|
|
<Button
|
|
type="text"
|
|
size="small"
|
|
className="pn-note-back"
|
|
icon={<ArrowLeft size={14} />}
|
|
onClick={() => navigate('/')}
|
|
>
|
|
返回首页
|
|
</Button>
|
|
</Empty>
|
|
</article>
|
|
)
|
|
}
|
|
|
|
const privatePreview = note.status === 'private'
|
|
|
|
return (
|
|
<article className="pn-note-article">
|
|
<Button type="text" size="small" className="pn-note-back" icon={<ArrowLeft size={14} />} onClick={() => navigate('/')}>
|
|
返回首页
|
|
</Button>
|
|
|
|
{privatePreview && authenticated && (
|
|
<Alert
|
|
role="note"
|
|
type="info"
|
|
showIcon={false}
|
|
className="pn-note-preview-alert"
|
|
message={
|
|
<>
|
|
<Eye size={15} />
|
|
私有预览:此笔记未公开,仅管理员可见
|
|
</>
|
|
}
|
|
/>
|
|
)}
|
|
|
|
<h1 className="pn-note-title">{note.title}</h1>
|
|
|
|
<div className="pn-note-meta">
|
|
<time className="pn-note-meta-date" dateTime={formatDateISO(note.published_at)}>
|
|
{formatDateISO(note.published_at)}
|
|
</time>
|
|
<span className="pn-note-meta-sep">·</span>
|
|
<span className="pn-note-meta-read"><Clock size={14} />约 {readingMinutes(note.content)} 分钟阅读</span>
|
|
{note.tags.length > 0 && (
|
|
<>
|
|
<span className="pn-note-meta-sep">·</span>
|
|
{note.tags.map((t) => (
|
|
<Tag key={t}>
|
|
<Link className="pn-tag-link" to={`/tags/${encodeURIComponent(t)}`}>{t}</Link>
|
|
</Tag>
|
|
))}
|
|
</>
|
|
)}
|
|
</div>
|
|
|
|
<MarkdownViewer source={note.content} />
|
|
|
|
<nav className="pn-note-postnav" aria-label="笔记导航">
|
|
{note.prev ? (
|
|
<Link to={`/notes/${encodeURIComponent(note.prev.slug)}`} className="pn-note-nav-link">
|
|
<Card className="pn-note-nav-card" styles={{ body: { padding: 0 } }}>
|
|
<span className="pn-note-nav-label"><ArrowLeft size={14} />上一篇</span>
|
|
<span className="pn-note-nav-title">{note.prev.title}</span>
|
|
</Card>
|
|
</Link>
|
|
) : (
|
|
<span />
|
|
)}
|
|
{note.next ? (
|
|
<Link to={`/notes/${encodeURIComponent(note.next.slug)}`} className="pn-note-nav-link">
|
|
<Card className="pn-note-nav-card pn-note-nav-next" styles={{ body: { padding: 0 } }}>
|
|
<span className="pn-note-nav-label">下一篇<ArrowRight size={14} /></span>
|
|
<span className="pn-note-nav-title">{note.next.title}</span>
|
|
</Card>
|
|
</Link>
|
|
) : (
|
|
<span />
|
|
)}
|
|
</nav>
|
|
</article>
|
|
)
|
|
}
|