/** * 发帖模态框组件 * 用于创建新帖子 */ import React, { useState } from 'react'; import { Modal, ModalOverlay, ModalContent, ModalHeader, ModalBody, ModalFooter, ModalCloseButton, Button, Input, Textarea, VStack, HStack, Text, Box, Image, IconButton, Tag, TagLabel, TagCloseButton, useToast, FormControl, FormLabel, FormErrorMessage, } from '@chakra-ui/react'; import { ImagePlus, X, Hash } from 'lucide-react'; import { forumColors } from '@theme/forumTheme'; import { createPost } from '@services/elasticsearchService'; import { useAuth } from '@contexts/AuthContext'; const CreatePostModal = ({ isOpen, onClose, onPostCreated }) => { const toast = useToast(); const { user } = useAuth(); const [formData, setFormData] = useState({ title: '', content: '', images: [], tags: [], category: '', }); const [currentTag, setCurrentTag] = useState(''); const [errors, setErrors] = useState({}); const [isSubmitting, setIsSubmitting] = useState(false); // 表单验证 const validateForm = () => { const newErrors = {}; if (!formData.title.trim()) { newErrors.title = '请输入标题'; } else if (formData.title.length > 100) { newErrors.title = '标题不能超过100个字符'; } if (!formData.content.trim()) { newErrors.content = '请输入内容'; } else if (formData.content.length > 5000) { newErrors.content = '内容不能超过5000个字符'; } setErrors(newErrors); return Object.keys(newErrors).length === 0; }; // 压缩图片 const compressImage = (file) => { return new Promise((resolve, reject) => { // 检查文件类型 if (!file.type.startsWith('image/')) { reject(new Error('请选择图片文件')); return; } // 检查文件大小(10MB 限制,给压缩留空间) if (file.size > 10 * 1024 * 1024) { reject(new Error('图片大小不能超过 10MB')); return; } const reader = new FileReader(); reader.onload = function(e) { const img = document.createElement('img'); img.onload = function() { try { // 创建 canvas 进行压缩 const canvas = document.createElement('canvas'); let width = img.width; let height = img.height; // 如果图片尺寸过大,等比缩放到最大 1920px const maxDimension = 1920; if (width > maxDimension || height > maxDimension) { if (width > height) { height = Math.round((height * maxDimension) / width); width = maxDimension; } else { width = Math.round((width * maxDimension) / height); height = maxDimension; } } canvas.width = width; canvas.height = height; const ctx = canvas.getContext('2d'); ctx.drawImage(img, 0, 0, width, height); // 转换为 Data URL(JPEG 格式,质量 0.8) try { const compressedDataUrl = canvas.toDataURL('image/jpeg', 0.8); // 计算压缩率 const originalSize = file.size; const compressedSize = Math.round((compressedDataUrl.length * 3) / 4); // Base64 解码后的大小 console.log( `图片压缩: ${(originalSize / 1024).toFixed(2)}KB -> ${(compressedSize / 1024).toFixed(2)}KB` ); resolve(compressedDataUrl); } catch (error) { reject(new Error('图片压缩失败')); } } catch (error) { reject(new Error(`图片处理失败: ${error.message}`)); } }; img.onerror = function() { reject(new Error('图片加载失败')); }; img.src = e.target.result; }; reader.onerror = function() { reject(new Error('文件读取失败')); }; reader.readAsDataURL(file); }); }; // 处理图片上传 const handleImageUpload = async (e) => { const files = Array.from(e.target.files); // 清空 input 以支持重复上传同一文件 e.target.value = ''; if (files.length === 0) return; // 检查总数量限制 if (formData.images.length + files.length > 9) { toast({ title: '图片数量超限', description: '最多只能上传 9 张图片', status: 'warning', duration: 3000, }); return; } // 逐个处理图片,而不是使用 Promise.all const compressedImages = []; let hasError = false; for (let i = 0; i < files.length; i++) { try { const compressed = await compressImage(files[i]); compressedImages.push(compressed); } catch (error) { console.error('图片压缩失败:', error); hasError = true; toast({ title: '图片处理失败', description: error.message || `第 ${i + 1} 张图片处理失败`, status: 'error', duration: 3000, }); break; // 遇到错误就停止 } } // 如果有成功压缩的图片,添加到表单 if (compressedImages.length > 0) { setFormData((prev) => ({ ...prev, images: [...prev.images, ...compressedImages], })); if (!hasError) { toast({ title: '上传成功', description: `成功添加 ${compressedImages.length} 张图片`, status: 'success', duration: 2000, }); } } }; // 移除图片 const removeImage = (index) => { setFormData((prev) => ({ ...prev, images: prev.images.filter((_, i) => i !== index), })); }; // 添加标签 const addTag = () => { if (currentTag.trim() && !formData.tags.includes(currentTag.trim())) { if (formData.tags.length >= 5) { toast({ title: '标签数量已达上限', description: '最多只能添加5个标签', status: 'warning', duration: 2000, }); return; } setFormData((prev) => ({ ...prev, tags: [...prev.tags, currentTag.trim()], })); setCurrentTag(''); } }; // 移除标签 const removeTag = (tag) => { setFormData((prev) => ({ ...prev, tags: prev.tags.filter((t) => t !== tag), })); }; // 提交帖子 const handleSubmit = async () => { if (!validateForm()) return; setIsSubmitting(true); try { const postData = { ...formData, author_id: user?.id || 'anonymous', author_name: user?.name || '匿名用户', author_avatar: user?.avatar || '', }; const newPost = await createPost(postData); toast({ title: '发布成功', description: '帖子已成功发布到论坛', status: 'success', duration: 3000, }); // 重置表单 setFormData({ title: '', content: '', images: [], tags: [], category: '', }); onClose(); // 通知父组件刷新 if (onPostCreated) { onPostCreated(newPost); } } catch (error) { console.error('发布帖子失败:', error); toast({ title: '发布失败', description: error.message || '发布帖子时出错,请稍后重试', status: 'error', duration: 3000, }); } finally { setIsSubmitting(false); } }; return ( 发布新帖 {/* 标题输入 */} 标题 setFormData((prev) => ({ ...prev, title: e.target.value })) } bg={forumColors.background.secondary} border="1px solid" borderColor={forumColors.border.default} color={forumColors.text.primary} _placeholder={{ color: forumColors.text.tertiary }} _hover={{ borderColor: forumColors.border.light }} _focus={{ borderColor: forumColors.border.gold, boxShadow: `0 0 0 1px ${forumColors.border.goldGlow}`, }} /> {errors.title} {/* 内容输入 */} 内容