Files
pure-note/web/src/pages/Note.tsx
T
wangairnan 87d9bab5dc feat(web): 整体替换 UI 框架为 Ant Design 6 并接入自定义主题
以 antd ConfigProvider + antd-style 承载 zinc 风格自定义主题(明暗双套 token),
button/input/select/notification 用 createStyles 定制;三态主题经
darkAlgorithm 无刷新切换;use-toast 改基于 antd notification;
移除 shadcn/ui 组件目录与 radix 依赖;样式桥接层保证排版与多端布局不变。
2026-09-09 10:35:24 +08:00

107 lines
3.7 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, 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 <p className="pn-home-list-empty">加载中…</p>
if (error || !note) {
return (
<div className="pn-note-article">
<p className="pn-home-list-empty">笔记不存在或未公开</p>
<p style={{ textAlign: 'center', marginTop: -40 }}>
<Button type="text" size="small" className="pn-note-back" icon={<ArrowLeft size={14} />} onClick={() => navigate('/')}>
返回首页
</Button>
</p>
</div>
)
}
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.created_at)}>
{formatDateISO(note.created_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>
)
}