Files
rainblogweb/frontend/src/pages/BlogDetail.jsx
T

127 lines
4.3 KiB
React
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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>
);
}