-
- } loading={uploading} size="small" style={{ marginBottom: 8 }}>
- 插入图片/视频
-
-
-
- {/* Media Preview Area */}
- {mediaList.length > 0 && (
-
- {mediaList.map((item, index) => (
-
- {item.type === 'video' ? (
-
- ) : (
-

- )}
-
- ))}
-
- )}
+
+
+
+ } loading={uploading} size="small">
+ 插入图片/视频
+
+
+
-
@@ -238,7 +268,7 @@ const CreateTopicModal = ({ visible, onClose, onSuccess }) => {
@@ -247,4 +277,4 @@ const CreateTopicModal = ({ visible, onClose, onSuccess }) => {
);
};
-export default CreateTopicModal;
+export default CreateTopicModal;
\ No newline at end of file
diff --git a/frontend/src/pages/ForumDetail.jsx b/frontend/src/pages/ForumDetail.jsx
index 22e1b2e..3a108e3 100644
--- a/frontend/src/pages/ForumDetail.jsx
+++ b/frontend/src/pages/ForumDetail.jsx
@@ -1,12 +1,21 @@
import React, { useState, useEffect } from 'react';
import { useParams, useNavigate } from 'react-router-dom';
-import { Typography, Card, Avatar, Tag, Space, Button, Divider, Input, message, Breadcrumb, Tooltip } from 'antd';
-import { UserOutlined, ClockCircleOutlined, EyeOutlined, CheckCircleFilled, LeftOutlined, LikeOutlined } from '@ant-design/icons';
-import { getTopicDetail, createReply } from '../api';
+import { Typography, Card, Avatar, Tag, Space, Button, Divider, Input, message, Upload, Tooltip } from 'antd';
+import { UserOutlined, ClockCircleOutlined, EyeOutlined, CheckCircleFilled, LeftOutlined, UploadOutlined, EditOutlined } from '@ant-design/icons';
+import { getTopicDetail, createReply, uploadMedia } 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';
-const { Title, Text, Paragraph } = Typography;
+const { Title, Text } = Typography;
const { TextArea } = Input;
const ForumDetail = () => {
@@ -19,6 +28,13 @@ const ForumDetail = () => {
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([]);
const fetchTopic = async () => {
try {
@@ -54,10 +70,12 @@ const ForumDetail = () => {
try {
await createReply({
topic: id,
- content: replyContent
+ 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);
@@ -67,19 +85,99 @@ const ForumDetail = () => {
}
};
+ 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
Loading...
;
if (!topic) return
Topic not found
;
+ const markdownComponents = {
+ // eslint-disable-next-line no-unused-vars
+ code({node, inline, className, children, ...props}) {
+ const match = /language-(\w+)/.exec(className || '')
+ return !inline && match ? (
+
+ {String(children).replace(/\n$/, '')}
+
+ ) : (
+
+ {children}
+
+ )
+ },
+ // eslint-disable-next-line no-unused-vars
+ img({node, ...props}) {
+ return (
+
![]()
+ );
+ }
+ };
+
return (
-
-
}
- style={{ color: '#fff', marginBottom: 20 }}
- onClick={() => navigate('/forum')}
- >
- 返回列表
-
+
+
+ }
+ style={{ color: '#fff' }}
+ onClick={() => navigate('/forum')}
+ >
+ 返回列表
+
+
+ {user && topic.author === user.id && (
+ }
+ onClick={() => setEditModalVisible(true)}
+ >
+ 编辑帖子
+
+ )}
+
{/* Topic Content */}
{
fontSize: 16,
lineHeight: 1.8,
minHeight: 200,
- whiteSpace: 'pre-wrap' // Preserve formatting
- }}>
- {topic.content.replace(/!\[.*?\]\(.*?\)/g, '[图片]')}
+ }} className="markdown-body">
+
+ {topic.content}
+
{(() => {
- const regexMatches = topic.content.match(/!\[.*?\]\((.*?)\)/g);
- const regexImages = regexMatches ? regexMatches.map(match => match.match(/!\[.*?\]\((.*?)\)/)[1]) : [];
-
- // 优先使用 Markdown 中解析出的图片(保持顺序)
- if (regexImages.length > 0) {
- return regexImages.map((url, index) => (
-
-

-
- ));
- }
-
- // 兜底:如果 Markdown 解析失败或未插入但已上传,显示关联的媒体资源
if (topic.media && topic.media.length > 0) {
- return topic.media.map((media) => (
+ return topic.media.filter(m => m.media_type === 'video').map((media) => (
- {media.media_type === 'video' ? (
-
- ) : (
-

- )}
+
));
}
-
return null;
})()}
@@ -193,7 +270,15 @@ const ForumDetail = () => {
#{index + 1}
-
{reply.content}
+
+
+ {reply.content}
+
+
@@ -214,10 +299,20 @@ const ForumDetail = () => {
rows={4}
value={replyContent}
onChange={e => setReplyContent(e.target.value)}
- placeholder="友善回复,分享你的见解..."
+ placeholder="友善回复,分享你的见解... (支持 Markdown)"
style={{ marginBottom: 16, background: '#111', border: '1px solid #333', color: '#fff' }}
/>
-