import React, { useState, useEffect } from 'react'; import { Modal, Form, Input, Button, message, Upload, Select } from 'antd'; import { UploadOutlined } from '@ant-design/icons'; import { createTopic, updateTopic, uploadMedia, getMyPaidItems } from '../api'; import MDEditor from '@uiw/react-md-editor'; import rehypeKatex from 'rehype-katex'; import remarkMath from 'remark-math'; import 'katex/dist/katex.css'; const { Option } = Select; const CreateTopicModal = ({ visible, onClose, onSuccess, initialValues, isEditMode, topicId }) => { const [form] = Form.useForm(); const [loading, setLoading] = useState(false); const [paidItems, setPaidItems] = useState({ configs: [], courses: [], services: [] }); const [uploading, setUploading] = useState(false); const [mediaIds, setMediaIds] = useState([]); // eslint-disable-next-line no-unused-vars const [mediaList, setMediaList] = useState([]); // Store uploaded media details for preview const [content, setContent] = useState(""); useEffect(() => { if (visible) { fetchPaidItems(); if (isEditMode && initialValues) { // Edit Mode: Populate form with initial values form.setFieldsValue({ title: initialValues.title, category: initialValues.category, }); setContent(initialValues.content); form.setFieldValue('content', initialValues.content); // Handle related item let relatedVal = null; if (initialValues.related_product) relatedVal = `config_${initialValues.related_product.id || initialValues.related_product}`; else if (initialValues.related_course) relatedVal = `course_${initialValues.related_course.id || initialValues.related_course}`; else if (initialValues.related_service) relatedVal = `service_${initialValues.related_service.id || initialValues.related_service}`; if (relatedVal) form.setFieldValue('related_item', relatedVal); // Note: We start with empty *new* media IDs. // Existing media is embedded in content or stored in DB, we don't need to re-upload or track them here unless we want to delete them (which is complex). // For now, we just allow adding NEW media. setMediaIds([]); setMediaList([]); } else { // Create Mode: Reset form setMediaIds([]); setMediaList([]); setContent(""); form.resetFields(); form.setFieldsValue({ content: "", category: 'discussion' }); } } }, [visible, isEditMode, initialValues, form]); const fetchPaidItems = async () => { try { const res = await getMyPaidItems(); setPaidItems(res.data); } catch (error) { console.error("Failed to fetch paid items", error); } }; const handleUpload = async (file) => { const formData = new FormData(); formData.append('file', file); // 默认为 image,如果需要支持视频需根据 file.type 判断 formData.append('media_type', file.type.startsWith('video') ? 'video' : 'image'); setUploading(true); try { const res = await uploadMedia(formData); // 记录上传的媒体 ID if (res.data.id) { setMediaIds(prev => [...prev, res.data.id]); } // 确保 URL 是完整的 // 由于后端现在是转发到外部OSS,返回的URL通常是完整的,但也可能是相对的,这里统一处理 let url = res.data.file; // 处理反斜杠问题(防止 Windows 路径风格影响 URL) if (url) { url = url.replace(/\\/g, '/'); } if (url && !url.startsWith('http')) { // 如果返回的是相对路径,拼接 API URL 或 Base URL const baseURL = import.meta.env.VITE_API_URL || 'http://localhost:8000'; // 移除 baseURL 末尾的 /api 或 / const host = baseURL.replace(/\/api\/?$/, ''); // 确保 url 以 / 开头 if (!url.startsWith('/')) url = '/' + url; url = `${host}${url}`; } // 清理 URL 中的双斜杠 (除协议头外) url = url.replace(/([^:]\/)\/+/g, '$1'); // Add to media list for preview setMediaList(prev => [...prev, { id: res.data.id, url: url, type: file.type.startsWith('video') ? 'video' : 'image', name: file.name }]); // 插入到编辑器 const insertText = file.type.startsWith('video') ? `\n\n` : `\n![${file.name}](${url})\n`; const newContent = content + insertText; setContent(newContent); form.setFieldsValue({ content: newContent }); message.success('上传成功'); } catch (error) { console.error(error); message.error('上传失败'); } finally { setUploading(false); } return false; // 阻止默认上传行为 }; const handleSubmit = async (values) => { setLoading(true); try { // 处理关联项目 ID (select value format: "type_id") const relatedValue = values.related_item; // Use content state instead of form value to ensure consistency const payload = { ...values, content: content, media_ids: mediaIds }; delete payload.related_item; if (relatedValue) { const [type, id] = relatedValue.split('_'); if (type === 'config') payload.related_product = id; if (type === 'course') payload.related_course = id; if (type === 'service') payload.related_service = id; } else { // If cleared, set to null payload.related_product = null; payload.related_course = null; payload.related_service = null; } if (isEditMode && topicId) { await updateTopic(topicId, payload); message.success('修改成功'); } else { await createTopic(payload); message.success('发布成功'); } form.resetFields(); if (onSuccess) onSuccess(); onClose(); } catch (error) { console.error(error); message.error((isEditMode ? '修改' : '发布') + '失败: ' + (error.response?.data?.detail || '网络错误')); } finally { setLoading(false); } }; return (
{ setContent(val); form.setFieldsValue({ content: val }); }} height={400} previewOptions={{ rehypePlugins: [[rehypeKatex]], remarkPlugins: [[remarkMath]], }} />
); }; export default CreateTopicModal;