127 lines
4.3 KiB
React
127 lines
4.3 KiB
React
import React, { useCallback, useEffect, useState } from 'react';
|
||
import { useParams, Link } from 'react-router-dom';
|
||
import * as blogApi from '../api/blog.js';
|
||
import { me } from '../api/auth.js';
|
||
import { getToken } from '../api/client.js';
|
||
import MarkdownRenderer from '../components/MarkdownRenderer.jsx';
|
||
import { showSnackbar } from '../lib/utils.js';
|
||
|
||
/**
|
||
* 博客详情页(路由 /blog/:id,SPA 内走前端路由,SSR 直链由后端处理):
|
||
* 文章正文(MarkdownRenderer)+ 评论列表/发表 + 编辑按钮(作者/管理员可见)。
|
||
*/
|
||
export default function BlogDetail() {
|
||
const { id } = useParams();
|
||
const [post, setPost] = useState(null);
|
||
const [comments, setComments] = useState([]);
|
||
const [user, setUser] = useState(null);
|
||
const [loading, setLoading] = useState(true);
|
||
const [error, setError] = useState('');
|
||
const [commentText, setCommentText] = useState('');
|
||
const [submitting, setSubmitting] = useState(false);
|
||
|
||
const load = useCallback(async () => {
|
||
setLoading(true);
|
||
setError('');
|
||
try {
|
||
const p = await blogApi.getPost(id);
|
||
setPost(p);
|
||
const cs = await blogApi.listComments(id);
|
||
setComments(cs || []);
|
||
} catch (e) {
|
||
setError(e.message || '加载失败');
|
||
} finally {
|
||
setLoading(false);
|
||
}
|
||
}, [id]);
|
||
|
||
useEffect(() => {
|
||
load();
|
||
if (getToken()) {
|
||
me().then(setUser).catch(() => setUser(null));
|
||
} else {
|
||
setUser(null);
|
||
}
|
||
}, [load]);
|
||
|
||
const canEdit = user && post && (user.role === 'admin' || user.id === post.author_id);
|
||
|
||
const submitComment = async () => {
|
||
const content = commentText.trim();
|
||
if (!content) { showSnackbar('评论不能为空'); return; }
|
||
setSubmitting(true);
|
||
try {
|
||
await blogApi.createComment(id, content);
|
||
showSnackbar('评论已发表');
|
||
setCommentText('');
|
||
await load();
|
||
} catch (e) {
|
||
showSnackbar(e.message);
|
||
}
|
||
setSubmitting(false);
|
||
};
|
||
|
||
if (loading) {
|
||
return <div className="loading"><div className="spinner"></div></div>;
|
||
}
|
||
|
||
if (error || !post) {
|
||
return (
|
||
<div className="empty-state">
|
||
<div className="empty-icon">⚠️</div>
|
||
<p>{error || '文章不存在'}</p>
|
||
<Link to="/blog.html" className="btn btn-tonal btn-sm" style={{ marginTop: 12 }}>返回博客</Link>
|
||
</div>
|
||
);
|
||
}
|
||
|
||
return (
|
||
<div className="blog-article">
|
||
<Link to="/blog.html" className="btn btn-text btn-sm" style={{ marginBottom: 16 }}>
|
||
<span className="material-icons" style={{ fontSize: 16 }}>arrow_back</span> 返回列表
|
||
</Link>
|
||
<h1 className="article-title">{post.title}</h1>
|
||
<div className="article-meta">
|
||
{post.author_name || '管理员'} · {post.created_at}
|
||
{canEdit && (
|
||
<Link to={`/write.html?edit=${post.id}`} className="btn btn-tonal btn-sm" style={{ marginLeft: 12 }}>编辑</Link>
|
||
)}
|
||
</div>
|
||
|
||
<MarkdownRenderer content={post.content} useMarkdown={post.use_markdown} />
|
||
|
||
<hr style={{ border: 'none', borderTop: '1px solid var(--md-ref-outline-variant)', margin: '32px 0' }} />
|
||
<h4 style={{ fontWeight: 500, marginBottom: 16 }}>评论 ({comments.length})</h4>
|
||
|
||
{comments.length === 0 ? (
|
||
<p className="text-muted" style={{ fontSize: 14 }}>暂无评论</p>
|
||
) : (
|
||
comments.map((c) => (
|
||
<div className="reply-item" key={c.id}>
|
||
<div className="reply-meta"><strong>{c.author_name || '游客'}</strong> · {c.created_at}</div>
|
||
<div className="reply-body">{c.content}</div>
|
||
</div>
|
||
))
|
||
)}
|
||
|
||
{user ? (
|
||
<div style={{ display: 'flex', gap: 8, marginTop: 12 }}>
|
||
<textarea
|
||
value={commentText}
|
||
onChange={(e) => setCommentText(e.target.value)}
|
||
placeholder="写下你的评论..."
|
||
style={{ flex: 1, minHeight: 60, fontSize: 14 }}
|
||
/>
|
||
<button className="btn btn-filled btn-sm" style={{ alignSelf: 'flex-end' }} onClick={submitComment} disabled={submitting}>
|
||
发表评论
|
||
</button>
|
||
</div>
|
||
) : (
|
||
<p className="text-muted" style={{ marginTop: 12, fontSize: 14 }}>
|
||
<Link to="/login.html" style={{ color: 'var(--md-ref-primary)' }}>登录</Link>后可以评论
|
||
</p>
|
||
)}
|
||
</div>
|
||
);
|
||
}
|