diff --git a/src/components/Navbars/components/MobileDrawer/MobileDrawer.js b/src/components/Navbars/components/MobileDrawer/MobileDrawer.js index 89d7d7ad..590c3f1f 100644 --- a/src/components/Navbars/components/MobileDrawer/MobileDrawer.js +++ b/src/components/Navbars/components/MobileDrawer/MobileDrawer.js @@ -264,15 +264,20 @@ const MobileDrawer = memo(({ handleNavigate('/value-forum')} py={1} px={3} borderRadius="md" - _hover={{}} - cursor="not-allowed" - color="gray.400" - pointerEvents="none" + _hover={{ bg: 'gray.50' }} + bg={location.pathname.includes('/value-forum') ? 'blue.50' : 'transparent'} > - 今日热议 + + 价值论坛 + + 黑金 + NEW + + { { + navEvents.trackMenuItemClicked('价值论坛', 'dropdown', '/value-forum'); + navigate('/value-forum'); + }} + borderRadius="md" + bg={location.pathname.includes('/value-forum') ? 'blue.50' : 'transparent'} + borderLeft={location.pathname.includes('/value-forum') ? '3px solid' : 'none'} + borderColor="blue.600" + fontWeight={location.pathname.includes('/value-forum') ? 'bold' : 'normal'} > - 今日热议 + + 价值论坛 + + 黑金 + NEW + + { - - 今日热议 + { + moreMenu.onClose(); // 先关闭菜单 + navigate('/value-forum'); + }} + borderRadius="md" + bg={location.pathname.includes('/value-forum') ? 'blue.50' : 'transparent'} + > + + 价值论坛 + + 黑金 + NEW + + 个股社区 diff --git a/src/routes/lazy-components.js b/src/routes/lazy-components.js index 2fcaa578..0f48bbed 100644 --- a/src/routes/lazy-components.js +++ b/src/routes/lazy-components.js @@ -38,6 +38,10 @@ export const lazyComponents = { // Agent模块 AgentChat: React.lazy(() => import('../views/AgentChat')), + + // 价值论坛模块 + ValueForum: React.lazy(() => import('../views/ValueForum')), + ForumPostDetail: React.lazy(() => import('../views/ValueForum/PostDetail')), }; /** @@ -63,4 +67,6 @@ export const { FinancialPanorama, MarketDataView, AgentChat, + ValueForum, + ForumPostDetail, } = lazyComponents; diff --git a/src/routes/routeConfig.js b/src/routes/routeConfig.js index 9ae9911d..da8a848d 100644 --- a/src/routes/routeConfig.js +++ b/src/routes/routeConfig.js @@ -150,6 +150,28 @@ export const routeConfig = [ } }, + // ==================== 价值论坛模块 ==================== + { + path: 'value-forum', + component: lazyComponents.ValueForum, + protection: PROTECTION_MODES.MODAL, + layout: 'main', + meta: { + title: '价值论坛', + description: '投资者价值讨论社区' + } + }, + { + path: 'value-forum/post/:postId', + component: lazyComponents.ForumPostDetail, + protection: PROTECTION_MODES.MODAL, + layout: 'main', + meta: { + title: '帖子详情', + description: '论坛帖子详细内容' + } + }, + // ==================== Agent模块 ==================== { path: 'agent-chat', diff --git a/src/services/elasticsearchService.js b/src/services/elasticsearchService.js new file mode 100644 index 00000000..a66eb7e4 --- /dev/null +++ b/src/services/elasticsearchService.js @@ -0,0 +1,429 @@ +/** + * Elasticsearch 服务层 + * 用于价值论坛的帖子、评论存储和搜索 + */ + +import axios from 'axios'; + +// Elasticsearch 配置 +const ES_CONFIG = { + baseURL: 'http://222.128.1.157:19200', + timeout: 10000, + headers: { + 'Content-Type': 'application/json', + }, +}; + +// 创建 axios 实例 +const esClient = axios.create(ES_CONFIG); + +// 索引名称 +const INDICES = { + POSTS: 'forum_posts', + COMMENTS: 'forum_comments', + EVENTS: 'forum_events', +}; + +/** + * 初始化索引(创建索引和映射) + */ +export const initializeIndices = async () => { + try { + // 创建帖子索引 + await esClient.put(`/${INDICES.POSTS}`, { + mappings: { + properties: { + id: { type: 'keyword' }, + author_id: { type: 'keyword' }, + author_name: { type: 'text' }, + author_avatar: { type: 'keyword' }, + title: { type: 'text', analyzer: 'ik_max_word' }, + content: { type: 'text', analyzer: 'ik_max_word' }, + images: { type: 'keyword' }, + tags: { type: 'keyword' }, + category: { type: 'keyword' }, + likes_count: { type: 'integer' }, + comments_count: { type: 'integer' }, + views_count: { type: 'integer' }, + created_at: { type: 'date' }, + updated_at: { type: 'date' }, + is_pinned: { type: 'boolean' }, + status: { type: 'keyword' }, // active, deleted, hidden + }, + }, + }); + + // 创建评论索引 + await esClient.put(`/${INDICES.COMMENTS}`, { + mappings: { + properties: { + id: { type: 'keyword' }, + post_id: { type: 'keyword' }, + author_id: { type: 'keyword' }, + author_name: { type: 'text' }, + author_avatar: { type: 'keyword' }, + content: { type: 'text', analyzer: 'ik_max_word' }, + parent_id: { type: 'keyword' }, // 用于嵌套评论 + likes_count: { type: 'integer' }, + created_at: { type: 'date' }, + status: { type: 'keyword' }, + }, + }, + }); + + // 创建事件时间轴索引 + await esClient.put(`/${INDICES.EVENTS}`, { + mappings: { + properties: { + id: { type: 'keyword' }, + post_id: { type: 'keyword' }, + event_type: { type: 'keyword' }, // news, price_change, announcement, etc. + title: { type: 'text' }, + description: { type: 'text', analyzer: 'ik_max_word' }, + source: { type: 'keyword' }, + source_url: { type: 'keyword' }, + related_stocks: { type: 'keyword' }, + occurred_at: { type: 'date' }, + created_at: { type: 'date' }, + importance: { type: 'keyword' }, // high, medium, low + }, + }, + }); + + console.log('Elasticsearch 索引初始化成功'); + } catch (error) { + if (error.response?.status === 400 && error.response?.data?.error?.type === 'resource_already_exists_exception') { + console.log('索引已存在,跳过创建'); + } else { + console.error('初始化索引失败:', error); + throw error; + } + } +}; + +// ==================== 帖子相关操作 ==================== + +/** + * 创建新帖子 + */ +export const createPost = async (postData) => { + try { + const post = { + id: `post_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`, + ...postData, + likes_count: 0, + comments_count: 0, + views_count: 0, + created_at: new Date().toISOString(), + updated_at: new Date().toISOString(), + is_pinned: false, + status: 'active', + }; + + const response = await esClient.post(`/${INDICES.POSTS}/_doc/${post.id}`, post); + return { ...post, _id: response.data._id }; + } catch (error) { + console.error('创建帖子失败:', error); + throw error; + } +}; + +/** + * 获取帖子列表(支持分页、排序、筛选) + */ +export const getPosts = async ({ page = 1, size = 20, sort = 'created_at', order = 'desc', category = null, tags = [] }) => { + try { + const from = (page - 1) * size; + + const query = { + bool: { + must: [{ match: { status: 'active' } }], + }, + }; + + if (category) { + query.bool.must.push({ term: { category } }); + } + + if (tags.length > 0) { + query.bool.must.push({ terms: { tags } }); + } + + const response = await esClient.post(`/${INDICES.POSTS}/_search`, { + from, + size, + query, + sort: [ + { is_pinned: { order: 'desc' } }, + { [sort]: { order } }, + ], + }); + + return { + total: response.data.hits.total.value, + posts: response.data.hits.hits.map((hit) => ({ ...hit._source, _id: hit._id })), + page, + size, + }; + } catch (error) { + console.error('获取帖子列表失败:', error); + throw error; + } +}; + +/** + * 获取单个帖子详情 + */ +export const getPostById = async (postId) => { + try { + const response = await esClient.get(`/${INDICES.POSTS}/_doc/${postId}`); + + // 增加浏览量 + await esClient.post(`/${INDICES.POSTS}/_update/${postId}`, { + script: { + source: 'ctx._source.views_count += 1', + lang: 'painless', + }, + }); + + return { ...response.data._source, _id: response.data._id }; + } catch (error) { + console.error('获取帖子详情失败:', error); + throw error; + } +}; + +/** + * 更新帖子 + */ +export const updatePost = async (postId, updateData) => { + try { + const response = await esClient.post(`/${INDICES.POSTS}/_update/${postId}`, { + doc: { + ...updateData, + updated_at: new Date().toISOString(), + }, + }); + return response.data; + } catch (error) { + console.error('更新帖子失败:', error); + throw error; + } +}; + +/** + * 删除帖子(软删除) + */ +export const deletePost = async (postId) => { + try { + await updatePost(postId, { status: 'deleted' }); + } catch (error) { + console.error('删除帖子失败:', error); + throw error; + } +}; + +/** + * 点赞帖子 + */ +export const likePost = async (postId) => { + try { + await esClient.post(`/${INDICES.POSTS}/_update/${postId}`, { + script: { + source: 'ctx._source.likes_count += 1', + lang: 'painless', + }, + }); + } catch (error) { + console.error('点赞帖子失败:', error); + throw error; + } +}; + +/** + * 搜索帖子 + */ +export const searchPosts = async (keyword, { page = 1, size = 20 }) => { + try { + const from = (page - 1) * size; + + const response = await esClient.post(`/${INDICES.POSTS}/_search`, { + from, + size, + query: { + bool: { + must: [ + { + multi_match: { + query: keyword, + fields: ['title^3', 'content', 'tags^2'], + type: 'best_fields', + }, + }, + { match: { status: 'active' } }, + ], + }, + }, + highlight: { + fields: { + title: {}, + content: {}, + }, + }, + }); + + return { + total: response.data.hits.total.value, + posts: response.data.hits.hits.map((hit) => ({ + ...hit._source, + _id: hit._id, + highlight: hit.highlight, + })), + page, + size, + }; + } catch (error) { + console.error('搜索帖子失败:', error); + throw error; + } +}; + +// ==================== 评论相关操作 ==================== + +/** + * 创建评论 + */ +export const createComment = async (commentData) => { + try { + const comment = { + id: `comment_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`, + ...commentData, + likes_count: 0, + created_at: new Date().toISOString(), + status: 'active', + }; + + const response = await esClient.post(`/${INDICES.COMMENTS}/_doc/${comment.id}`, comment); + + // 增加帖子评论数 + await esClient.post(`/${INDICES.POSTS}/_update/${commentData.post_id}`, { + script: { + source: 'ctx._source.comments_count += 1', + lang: 'painless', + }, + }); + + return { ...comment, _id: response.data._id }; + } catch (error) { + console.error('创建评论失败:', error); + throw error; + } +}; + +/** + * 获取帖子的评论列表 + */ +export const getCommentsByPostId = async (postId, { page = 1, size = 50 }) => { + try { + const from = (page - 1) * size; + + const response = await esClient.post(`/${INDICES.COMMENTS}/_search`, { + from, + size, + query: { + bool: { + must: [ + { term: { post_id: postId } }, + { match: { status: 'active' } }, + ], + }, + }, + sort: [{ created_at: { order: 'asc' } }], + }); + + return { + total: response.data.hits.total.value, + comments: response.data.hits.hits.map((hit) => ({ ...hit._source, _id: hit._id })), + }; + } catch (error) { + console.error('获取评论列表失败:', error); + throw error; + } +}; + +/** + * 点赞评论 + */ +export const likeComment = async (commentId) => { + try { + await esClient.post(`/${INDICES.COMMENTS}/_update/${commentId}`, { + script: { + source: 'ctx._source.likes_count += 1', + lang: 'painless', + }, + }); + } catch (error) { + console.error('点赞评论失败:', error); + throw error; + } +}; + +// ==================== 事件时间轴相关操作 ==================== + +/** + * 创建事件 + */ +export const createEvent = async (eventData) => { + try { + const event = { + id: `event_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`, + ...eventData, + created_at: new Date().toISOString(), + }; + + const response = await esClient.post(`/${INDICES.EVENTS}/_doc/${event.id}`, event); + return { ...event, _id: response.data._id }; + } catch (error) { + console.error('创建事件失败:', error); + throw error; + } +}; + +/** + * 获取帖子的事件时间轴 + */ +export const getEventsByPostId = async (postId) => { + try { + const response = await esClient.post(`/${INDICES.EVENTS}/_search`, { + size: 100, + query: { + term: { post_id: postId }, + }, + sort: [{ occurred_at: { order: 'desc' } }], + }); + + return response.data.hits.hits.map((hit) => ({ ...hit._source, _id: hit._id })); + } catch (error) { + console.error('获取事件时间轴失败:', error); + throw error; + } +}; + +export default { + initializeIndices, + // 帖子操作 + createPost, + getPosts, + getPostById, + updatePost, + deletePost, + likePost, + searchPosts, + // 评论操作 + createComment, + getCommentsByPostId, + likeComment, + // 事件操作 + createEvent, + getEventsByPostId, +}; diff --git a/src/theme/forumTheme.js b/src/theme/forumTheme.js new file mode 100644 index 00000000..d887d901 --- /dev/null +++ b/src/theme/forumTheme.js @@ -0,0 +1,227 @@ +/** + * 价值论坛黑金主题配置 + * 采用深色背景 + 金色点缀的高端配色方案 + */ + +export const forumColors = { + // 主色调 - 黑金渐变 + primary: { + 50: '#FFF9E6', + 100: '#FFEEBA', + 200: '#FFE38D', + 300: '#FFD860', + 400: '#FFCD33', + 500: '#FFC107', // 主金色 + 600: '#FFB300', + 700: '#FFA000', + 800: '#FF8F00', + 900: '#FF6F00', + }, + + // 背景色系 - 深黑渐变 + background: { + main: '#0A0A0A', // 主背景 - 极黑 + secondary: '#121212', // 次级背景 + card: '#1A1A1A', // 卡片背景 + hover: '#222222', // 悬停背景 + elevated: '#2A2A2A', // 提升背景(模态框等) + }, + + // 文字色系 + text: { + primary: '#FFFFFF', // 主文字 - 纯白 + secondary: '#B8B8B8', // 次要文字 - 灰色 + tertiary: '#808080', // 三级文字 - 深灰 + muted: '#5A5A5A', // 弱化文字 + gold: '#FFC107', // 金色强调文字 + goldGradient: 'linear-gradient(135deg, #FFD700 0%, #FFA500 100%)', // 金色渐变 + }, + + // 边框色系 + border: { + default: '#333333', + light: '#404040', + gold: '#FFC107', + goldGlow: 'rgba(255, 193, 7, 0.3)', + }, + + // 功能色 + semantic: { + success: '#4CAF50', + warning: '#FF9800', + error: '#F44336', + info: '#2196F3', + }, + + // 金色渐变系列 + gradients: { + goldPrimary: 'linear-gradient(135deg, #FFD700 0%, #FFA500 100%)', + goldSecondary: 'linear-gradient(135deg, #FFC107 0%, #FF8F00 100%)', + goldSubtle: 'linear-gradient(135deg, rgba(255, 215, 0, 0.1) 0%, rgba(255, 165, 0, 0.05) 100%)', + blackGold: 'linear-gradient(135deg, #0A0A0A 0%, #1A1A1A 50%, #2A2020 100%)', + cardHover: 'linear-gradient(135deg, #1A1A1A 0%, #252525 100%)', + }, + + // 阴影色系 + shadows: { + sm: '0 1px 2px 0 rgba(0, 0, 0, 0.5)', + md: '0 4px 6px -1px rgba(0, 0, 0, 0.6), 0 2px 4px -1px rgba(0, 0, 0, 0.4)', + lg: '0 10px 15px -3px rgba(0, 0, 0, 0.7), 0 4px 6px -2px rgba(0, 0, 0, 0.5)', + xl: '0 20px 25px -5px rgba(0, 0, 0, 0.8), 0 10px 10px -5px rgba(0, 0, 0, 0.6)', + gold: '0 0 20px rgba(255, 193, 7, 0.3), 0 0 40px rgba(255, 193, 7, 0.1)', + goldHover: '0 0 30px rgba(255, 193, 7, 0.5), 0 0 60px rgba(255, 193, 7, 0.2)', + }, +}; + +/** + * 论坛组件样式配置 + */ +export const forumComponentStyles = { + // 按钮样式 + Button: { + baseStyle: { + fontWeight: '600', + borderRadius: 'md', + transition: 'all 0.3s ease', + }, + variants: { + gold: { + bg: forumColors.gradients.goldPrimary, + color: '#0A0A0A', + _hover: { + transform: 'translateY(-2px)', + boxShadow: forumColors.shadows.goldHover, + _disabled: { + transform: 'none', + }, + }, + _active: { + transform: 'translateY(0)', + }, + }, + goldOutline: { + bg: 'transparent', + color: forumColors.primary[500], + border: '2px solid', + borderColor: forumColors.primary[500], + _hover: { + bg: forumColors.gradients.goldSubtle, + boxShadow: forumColors.shadows.gold, + }, + }, + dark: { + bg: forumColors.background.card, + color: forumColors.text.primary, + border: '1px solid', + borderColor: forumColors.border.default, + _hover: { + bg: forumColors.background.hover, + borderColor: forumColors.border.light, + }, + }, + }, + }, + + // 卡片样式 + Card: { + baseStyle: { + container: { + bg: forumColors.background.card, + borderRadius: 'lg', + border: '1px solid', + borderColor: forumColors.border.default, + transition: 'all 0.3s ease', + _hover: { + borderColor: forumColors.border.gold, + boxShadow: forumColors.shadows.gold, + transform: 'translateY(-4px)', + }, + }, + }, + }, + + // 输入框样式 + Input: { + variants: { + forum: { + field: { + 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}`, + }, + }, + }, + }, + }, + + // 标签样式 + Tag: { + variants: { + gold: { + container: { + bg: forumColors.gradients.goldSubtle, + color: forumColors.primary[500], + border: '1px solid', + borderColor: forumColors.border.gold, + }, + }, + }, + }, +}; + +/** + * 论坛专用动画配置 + */ +export const forumAnimations = { + fadeIn: { + initial: { opacity: 0, y: 20 }, + animate: { opacity: 1, y: 0 }, + exit: { opacity: 0, y: -20 }, + transition: { duration: 0.3 }, + }, + + slideIn: { + initial: { opacity: 0, x: -20 }, + animate: { opacity: 1, x: 0 }, + exit: { opacity: 0, x: 20 }, + transition: { duration: 0.3 }, + }, + + scaleIn: { + initial: { opacity: 0, scale: 0.9 }, + animate: { opacity: 1, scale: 1 }, + exit: { opacity: 0, scale: 0.9 }, + transition: { duration: 0.2 }, + }, + + goldGlow: { + animate: { + boxShadow: [ + forumColors.shadows.gold, + forumColors.shadows.goldHover, + forumColors.shadows.gold, + ], + }, + transition: { + duration: 2, + repeat: Infinity, + repeatType: 'reverse', + }, + }, +}; + +export default { + colors: forumColors, + components: forumComponentStyles, + animations: forumAnimations, +}; diff --git a/src/views/ValueForum/PostDetail.js b/src/views/ValueForum/PostDetail.js new file mode 100644 index 00000000..1e9bd5e7 --- /dev/null +++ b/src/views/ValueForum/PostDetail.js @@ -0,0 +1,370 @@ +/** + * 帖子详情页 + * 展示帖子完整内容、事件时间轴、评论区 + */ + +import React, { useState, useEffect } from 'react'; +import { + Box, + Container, + Heading, + Text, + HStack, + VStack, + Avatar, + Badge, + Button, + Image, + SimpleGrid, + Spinner, + Center, + Flex, + IconButton, + Divider, +} from '@chakra-ui/react'; +import { useParams, useNavigate } from 'react-router-dom'; +import { motion } from 'framer-motion'; +import { + ArrowLeft, + Heart, + MessageCircle, + Eye, + Share2, + Bookmark, +} from 'lucide-react'; +import { forumColors } from '@theme/forumTheme'; +import { + getPostById, + likePost, + getEventsByPostId, +} from '@services/elasticsearchService'; +import EventTimeline from './components/EventTimeline'; +import CommentSection from './components/CommentSection'; + +const MotionBox = motion(Box); + +const PostDetail = () => { + const { postId } = useParams(); + const navigate = useNavigate(); + + const [post, setPost] = useState(null); + const [events, setEvents] = useState([]); + const [loading, setLoading] = useState(true); + const [isLiked, setIsLiked] = useState(false); + const [likes, setLikes] = useState(0); + + // 加载帖子数据 + useEffect(() => { + const loadPostData = async () => { + try { + setLoading(true); + + // 并行加载帖子和事件 + const [postData, eventsData] = await Promise.all([ + getPostById(postId), + getEventsByPostId(postId), + ]); + + setPost(postData); + setLikes(postData.likes_count || 0); + setEvents(eventsData); + } catch (error) { + console.error('加载帖子失败:', error); + } finally { + setLoading(false); + } + }; + + loadPostData(); + }, [postId]); + + // 处理点赞 + const handleLike = async () => { + try { + if (!isLiked) { + await likePost(postId); + setLikes((prev) => prev + 1); + setIsLiked(true); + } + } catch (error) { + console.error('点赞失败:', error); + } + }; + + // 格式化时间 + const formatTime = (dateString) => { + const date = new Date(dateString); + return date.toLocaleString('zh-CN', { + year: 'numeric', + month: '2-digit', + day: '2-digit', + hour: '2-digit', + minute: '2-digit', + }); + }; + + if (loading) { + return ( + +
+ + + 加载中... + +
+
+ ); + } + + if (!post) { + return ( + +
+ + + 帖子不存在或已被删除 + + + +
+
+ ); + } + + return ( + + + {/* 返回按钮 */} + + + + {/* 左侧:帖子内容 + 评论 */} + + + {/* 帖子主体 */} + + {/* 作者信息 */} + + + + + + + {post.author_name} + + + 发布于 {formatTime(post.created_at)} + + + + + {/* 操作按钮 */} + + } + variant="ghost" + color={forumColors.text.tertiary} + _hover={{ color: forumColors.primary[500] }} + aria-label="分享" + /> + } + variant="ghost" + color={forumColors.text.tertiary} + _hover={{ color: forumColors.primary[500] }} + aria-label="收藏" + /> + + + + + {/* 帖子内容 */} + + {/* 标题 */} + + {post.title} + + + {/* 标签 */} + {post.tags && post.tags.length > 0 && ( + + {post.tags.map((tag, index) => ( + + #{tag} + + ))} + + )} + + {/* 正文 */} + + {post.content} + + + {/* 图片 */} + {post.images && post.images.length > 0 && ( + + {post.images.map((img, index) => ( + {`图片 + ))} + + )} + + + {/* 互动栏 */} + + + + + + + {likes} + + + + + + + {post.comments_count || 0} + + + + + + + {post.views_count || 0} + + + + + + + + + + {/* 评论区 */} + + + + + + + {/* 右侧:事件时间轴 */} + + + + + + + + + ); +}; + +export default PostDetail; diff --git a/src/views/ValueForum/components/CommentSection.js b/src/views/ValueForum/components/CommentSection.js new file mode 100644 index 00000000..37709432 --- /dev/null +++ b/src/views/ValueForum/components/CommentSection.js @@ -0,0 +1,318 @@ +/** + * 评论区组件 + * 支持发布评论、嵌套回复、点赞等功能 + */ + +import React, { useState, useEffect } from 'react'; +import { + Box, + VStack, + HStack, + Text, + Avatar, + Textarea, + Button, + Flex, + IconButton, + Divider, + useToast, +} from '@chakra-ui/react'; +import { motion, AnimatePresence } from 'framer-motion'; +import { Heart, MessageCircle, Send } from 'lucide-react'; +import { forumColors } from '@theme/forumTheme'; +import { + getCommentsByPostId, + createComment, + likeComment, +} from '@services/elasticsearchService'; +import { useAuth } from '@contexts/AuthContext'; + +const MotionBox = motion(Box); + +const CommentItem = ({ comment, postId, onReply }) => { + const [isLiked, setIsLiked] = useState(false); + const [likes, setLikes] = useState(comment.likes_count || 0); + const [showReply, setShowReply] = useState(false); + + // 处理点赞 + const handleLike = async () => { + try { + if (!isLiked) { + await likeComment(comment.id); + setLikes((prev) => prev + 1); + setIsLiked(true); + } + } catch (error) { + console.error('点赞失败:', error); + } + }; + + // 格式化时间 + const formatTime = (dateString) => { + const date = new Date(dateString); + const now = new Date(); + const diff = now - date; + + const minutes = Math.floor(diff / 60000); + const hours = Math.floor(diff / 3600000); + const days = Math.floor(diff / 86400000); + + if (minutes < 1) return '刚刚'; + if (minutes < 60) return `${minutes}分钟前`; + if (hours < 24) return `${hours}小时前`; + if (days < 7) return `${days}天前`; + + return date.toLocaleDateString('zh-CN', { + month: '2-digit', + day: '2-digit', + }); + }; + + return ( + + + {/* 头像 */} + + + {/* 评论内容 */} + + {/* 用户名和时间 */} + + + + {comment.author_name} + + + {formatTime(comment.created_at)} + + + + + {/* 评论正文 */} + + {comment.content} + + + {/* 操作按钮 */} + + + + {likes > 0 ? likes : '点赞'} + + + setShowReply(!showReply)} + _hover={{ color: forumColors.primary[500] }} + > + + 回复 + + + + {/* 回复输入框 */} + {showReply && ( + + { + setShowReply(false); + if (onReply) onReply(); + }} + /> + + )} + + + + ); +}; + +const ReplyInput = ({ postId, parentId = null, placeholder, onSubmit }) => { + const toast = useToast(); + const { user } = useAuth(); + const [content, setContent] = useState(''); + const [isSubmitting, setIsSubmitting] = useState(false); + + const handleSubmit = async () => { + if (!content.trim()) { + toast({ + title: '请输入评论内容', + status: 'warning', + duration: 2000, + }); + return; + } + + setIsSubmitting(true); + + try { + await createComment({ + post_id: postId, + parent_id: parentId, + content: content.trim(), + author_id: user?.id || 'anonymous', + author_name: user?.name || '匿名用户', + author_avatar: user?.avatar || '', + }); + + toast({ + title: '评论成功', + status: 'success', + duration: 2000, + }); + + setContent(''); + if (onSubmit) onSubmit(); + } catch (error) { + console.error('评论失败:', error); + toast({ + title: '评论失败', + description: error.message, + status: 'error', + duration: 3000, + }); + } finally { + setIsSubmitting(false); + } + }; + + return ( + +