add forum

This commit is contained in:
2025-11-15 09:10:26 +08:00
parent 2753fbc37f
commit 05b497de29
13 changed files with 2693 additions and 11 deletions

View File

@@ -0,0 +1,419 @@
/**
* 发帖模态框组件
* 用于创建新帖子
*/
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 handleImageUpload = (e) => {
const files = Array.from(e.target.files);
files.forEach((file) => {
const reader = new FileReader();
reader.onloadend = () => {
setFormData((prev) => ({
...prev,
images: [...prev.images, reader.result],
}));
};
reader.readAsDataURL(file);
});
};
// 移除图片
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 (
<Modal isOpen={isOpen} onClose={onClose} size="2xl">
<ModalOverlay bg="blackAlpha.800" />
<ModalContent
bg={forumColors.background.elevated}
borderColor={forumColors.border.gold}
borderWidth="1px"
maxH="90vh"
>
<ModalHeader
color={forumColors.text.primary}
borderBottomWidth="1px"
borderBottomColor={forumColors.border.default}
>
<Text
bgGradient={forumColors.text.goldGradient}
bgClip="text"
fontWeight="bold"
fontSize="xl"
>
发布新帖
</Text>
</ModalHeader>
<ModalCloseButton color={forumColors.text.secondary} />
<ModalBody py="6" overflowY="auto">
<VStack spacing="5" align="stretch">
{/* 标题输入 */}
<FormControl isInvalid={errors.title}>
<FormLabel color={forumColors.text.secondary} fontSize="sm">
标题
</FormLabel>
<Input
placeholder="给你的帖子起个吸引人的标题..."
value={formData.title}
onChange={(e) =>
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}`,
}}
/>
<FormErrorMessage>{errors.title}</FormErrorMessage>
</FormControl>
{/* 内容输入 */}
<FormControl isInvalid={errors.content}>
<FormLabel color={forumColors.text.secondary} fontSize="sm">
内容
</FormLabel>
<Textarea
placeholder="分享你的投资见解、市场观点或交易心得..."
value={formData.content}
onChange={(e) =>
setFormData((prev) => ({ ...prev, content: e.target.value }))
}
minH="200px"
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}`,
}}
resize="vertical"
/>
<FormErrorMessage>{errors.content}</FormErrorMessage>
<Text
fontSize="xs"
color={forumColors.text.muted}
mt="2"
textAlign="right"
>
{formData.content.length} / 5000
</Text>
</FormControl>
{/* 图片上传 */}
<Box>
<FormLabel color={forumColors.text.secondary} fontSize="sm">
图片最多9张
</FormLabel>
<HStack spacing="3" flexWrap="wrap">
{formData.images.map((img, index) => (
<Box key={index} position="relative" w="100px" h="100px">
<Image
src={img}
alt={`预览 ${index + 1}`}
w="100%"
h="100%"
objectFit="cover"
borderRadius="md"
border="1px solid"
borderColor={forumColors.border.default}
/>
<IconButton
icon={<X size={14} />}
size="xs"
position="absolute"
top="-2"
right="-2"
borderRadius="full"
bg={forumColors.background.main}
color={forumColors.text.primary}
border="1px solid"
borderColor={forumColors.border.gold}
onClick={() => removeImage(index)}
_hover={{ bg: forumColors.background.hover }}
/>
</Box>
))}
{formData.images.length < 9 && (
<Box
as="label"
w="100px"
h="100px"
display="flex"
alignItems="center"
justifyContent="center"
bg={forumColors.background.secondary}
border="2px dashed"
borderColor={forumColors.border.default}
borderRadius="md"
cursor="pointer"
_hover={{ borderColor: forumColors.border.gold }}
>
<Input
type="file"
accept="image/*"
multiple
display="none"
onChange={handleImageUpload}
/>
<ImagePlus size={24} color={forumColors.text.tertiary} />
</Box>
)}
</HStack>
</Box>
{/* 标签输入 */}
<Box>
<FormLabel color={forumColors.text.secondary} fontSize="sm">
标签最多5个
</FormLabel>
<HStack mb="3">
<Input
placeholder="输入标签后按回车"
value={currentTag}
onChange={(e) => setCurrentTag(e.target.value)}
onKeyPress={(e) => {
if (e.key === 'Enter') {
e.preventDefault();
addTag();
}
}}
bg={forumColors.background.secondary}
border="1px solid"
borderColor={forumColors.border.default}
color={forumColors.text.primary}
_placeholder={{ color: forumColors.text.tertiary }}
_focus={{
borderColor: forumColors.border.gold,
boxShadow: `0 0 0 1px ${forumColors.border.goldGlow}`,
}}
/>
<IconButton
icon={<Hash size={18} />}
onClick={addTag}
bg={forumColors.gradients.goldPrimary}
color={forumColors.background.main}
_hover={{ opacity: 0.9 }}
/>
</HStack>
<HStack spacing="2" flexWrap="wrap">
{formData.tags.map((tag) => (
<Tag
key={tag}
size="md"
bg={forumColors.gradients.goldSubtle}
color={forumColors.primary[500]}
border="1px solid"
borderColor={forumColors.border.gold}
borderRadius="full"
>
<TagLabel>#{tag}</TagLabel>
<TagCloseButton onClick={() => removeTag(tag)} />
</Tag>
))}
</HStack>
</Box>
</VStack>
</ModalBody>
<ModalFooter
borderTopWidth="1px"
borderTopColor={forumColors.border.default}
>
<HStack spacing="3">
<Button
variant="ghost"
onClick={onClose}
color={forumColors.text.secondary}
_hover={{ bg: forumColors.background.hover }}
>
取消
</Button>
<Button
bg={forumColors.gradients.goldPrimary}
color={forumColors.background.main}
fontWeight="bold"
onClick={handleSubmit}
isLoading={isSubmitting}
loadingText="发布中..."
_hover={{
transform: 'translateY(-2px)',
boxShadow: forumColors.shadows.goldHover,
}}
_active={{ transform: 'translateY(0)' }}
>
发布帖子
</Button>
</HStack>
</ModalFooter>
</ModalContent>
</Modal>
);
};
export default CreatePostModal;