import React, { useState, useEffect } from 'react'; import { useParams, useNavigate } from 'react-router-dom'; import { Typography, Card, Avatar, Tag, Space, Button, Divider, Input, message, Upload, Tooltip, Grid, Modal } from 'antd'; import { UserOutlined, ClockCircleOutlined, EyeOutlined, CheckCircleFilled, LeftOutlined, UploadOutlined, EditOutlined, StarFilled, CloseOutlined, LikeOutlined, LikeFilled } from '@ant-design/icons'; import { getTopicDetail, createReply, uploadMedia, getStarUsers, likeTopic, likeReply } from '../api'; import { useAuth } from '../context/AuthContext'; import LoginModal from '../components/LoginModal'; import CreateTopicModal from '../components/CreateTopicModal'; import ReactMarkdown from 'react-markdown'; import remarkMath from 'remark-math'; import rehypeKatex from 'rehype-katex'; import remarkGfm from 'remark-gfm'; import rehypeRaw from 'rehype-raw'; import { Prism as SyntaxHighlighter } from 'react-syntax-highlighter'; import { vscDarkPlus } from 'react-syntax-highlighter/dist/esm/styles/prism'; import 'katex/dist/katex.min.css'; import styles from './ForumDetail.module.less'; import CodeBlock from '../components/CodeBlock'; const { Title, Text } = Typography; const { TextArea } = Input; const { useBreakpoint } = Grid; const ForumDetail = () => { const { id } = useParams(); const navigate = useNavigate(); const { user, login } = useAuth(); const screens = useBreakpoint(); const isMobile = !screens.md; const [loading, setLoading] = useState(true); const [topic, setTopic] = useState(null); const [replyContent, setReplyContent] = useState(''); const [submitting, setSubmitting] = useState(false); const [loginModalVisible, setLoginModalVisible] = useState(false); // Edit Topic State const [editModalVisible, setEditModalVisible] = useState(false); // Reply Image State const [replyUploading, setReplyUploading] = useState(false); const [replyMediaIds, setReplyMediaIds] = useState([]); // Star Users State const [starUsers, setStarUsers] = useState([]); const fetchTopic = async () => { try { const res = await getTopicDetail(id); setTopic(res.data); } catch (error) { console.error(error); message.error('加载失败'); } finally { setLoading(false); } }; const fetchStarUsers = async () => { try { const res = await getStarUsers(); setStarUsers(res.data || []); } catch (error) { console.error('Fetch star users failed', error); } }; const hasFetched = React.useRef(false); useEffect(() => { if (!hasFetched.current) { fetchTopic(); fetchStarUsers(); hasFetched.current = true; } }, [id]); const handleReplyToUser = (nickname) => { const mentionText = `@${nickname} `; setReplyContent(prev => prev + mentionText); // Focus logic if needed, but simple append works message.info(`已添加 @${nickname}`); }; // Expert Info Modal const [expertModalVisible, setExpertModalVisible] = useState(false); const [selectedExpert, setSelectedExpert] = useState(null); const showUserTitle = (author) => { if (author?.is_star) { setSelectedExpert(author); setExpertModalVisible(true); } else if (author?.title) { message.info(author.title); } }; const handleLikeTopic = async () => { if (!user) { setLoginModalVisible(true); return; } try { const res = await likeTopic(topic.id); setTopic(prev => ({ ...prev, is_liked: res.data.liked, like_count: res.data.count })); } catch (error) { message.error('操作失败'); } }; const handleLikeReply = async (replyId) => { if (!user) { setLoginModalVisible(true); return; } try { const res = await likeReply(replyId); setTopic(prev => ({ ...prev, replies: prev.replies.map(r => { if (r.id === replyId) { return { ...r, is_liked: res.data.liked, like_count: res.data.count }; } return r; }) })); } catch (error) { message.error('操作失败'); } }; const handleSubmitReply = async () => { if (!user) { setLoginModalVisible(true); return; } if (!replyContent.trim()) { message.warning('请输入回复内容'); return; } setSubmitting(true); try { await createReply({ topic: id, content: replyContent, media_ids: replyMediaIds // Send uploaded media IDs }); message.success('回复成功'); setReplyContent(''); setReplyMediaIds([]); // Reset media IDs fetchTopic(); // Refresh to show new reply } catch (error) { console.error(error); message.error('回复失败'); } finally { setSubmitting(false); } }; const handleReplyUpload = async (file) => { const formData = new FormData(); formData.append('file', file); formData.append('media_type', file.type.startsWith('video') ? 'video' : 'image'); setReplyUploading(true); try { const res = await uploadMedia(formData); if (res.data.id) { setReplyMediaIds(prev => [...prev, res.data.id]); } let url = res.data.file; if (url) url = url.replace(/\\/g, '/'); if (url && !url.startsWith('http')) { const baseURL = import.meta.env.VITE_API_URL || 'http://localhost:8000'; const host = baseURL.replace(/\/api\/?$/, ''); if (!url.startsWith('/')) url = '/' + url; url = `${host}${url}`; } url = url.replace(/([^:]\/)\/+/g, '$1'); const insertText = file.type.startsWith('video') ? `\n\n` : `\n\n`; setReplyContent(prev => prev + insertText); message.success('上传成功'); } catch (error) { console.error(error); message.error('上传失败'); } finally { setReplyUploading(false); } return false; }; if (loading) return
{children}
)
},
// eslint-disable-next-line no-unused-vars
img({node, ...props}) {
return (