feat: 添加评论功能
This commit is contained in:
77
src/components/EventCommentSection/CommentInput.js
Normal file
77
src/components/EventCommentSection/CommentInput.js
Normal file
@@ -0,0 +1,77 @@
|
|||||||
|
// src/components/EventCommentSection/CommentInput.js
|
||||||
|
/**
|
||||||
|
* 评论输入框组件
|
||||||
|
* 功能:输入评论内容、字数限制、发布按钮
|
||||||
|
*/
|
||||||
|
|
||||||
|
import React from 'react';
|
||||||
|
import {
|
||||||
|
Box,
|
||||||
|
Textarea,
|
||||||
|
Button,
|
||||||
|
HStack,
|
||||||
|
Text,
|
||||||
|
useColorModeValue,
|
||||||
|
} from '@chakra-ui/react';
|
||||||
|
|
||||||
|
const CommentInput = ({
|
||||||
|
value,
|
||||||
|
onChange,
|
||||||
|
onSubmit,
|
||||||
|
isSubmitting,
|
||||||
|
maxLength = 500,
|
||||||
|
placeholder = '说点什么...',
|
||||||
|
}) => {
|
||||||
|
const bgColor = useColorModeValue('white', 'gray.800');
|
||||||
|
const borderColor = useColorModeValue('gray.200', 'gray.600');
|
||||||
|
const textColor = useColorModeValue('gray.600', 'gray.400');
|
||||||
|
const countColor = useColorModeValue('gray.500', 'gray.500');
|
||||||
|
|
||||||
|
const handleKeyDown = (e) => {
|
||||||
|
// Ctrl/Cmd + Enter 快捷键提交
|
||||||
|
if ((e.ctrlKey || e.metaKey) && e.key === 'Enter') {
|
||||||
|
onSubmit();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Box>
|
||||||
|
<Textarea
|
||||||
|
value={value}
|
||||||
|
onChange={onChange}
|
||||||
|
onKeyDown={handleKeyDown}
|
||||||
|
placeholder={placeholder}
|
||||||
|
bg={bgColor}
|
||||||
|
borderColor={borderColor}
|
||||||
|
color={textColor}
|
||||||
|
rows={3}
|
||||||
|
maxLength={maxLength}
|
||||||
|
resize="vertical"
|
||||||
|
_hover={{
|
||||||
|
borderColor: 'blue.300',
|
||||||
|
}}
|
||||||
|
_focus={{
|
||||||
|
borderColor: 'blue.500',
|
||||||
|
boxShadow: '0 0 0 1px var(--chakra-colors-blue-500)',
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<HStack justify="space-between" mt={2}>
|
||||||
|
<Text fontSize="sm" color={countColor}>
|
||||||
|
{value.length}/{maxLength}
|
||||||
|
{value.length === 0 && ' · Ctrl+Enter 快速发布'}
|
||||||
|
</Text>
|
||||||
|
<Button
|
||||||
|
colorScheme="blue"
|
||||||
|
size="sm"
|
||||||
|
onClick={onSubmit}
|
||||||
|
isLoading={isSubmitting}
|
||||||
|
isDisabled={!value.trim() || isSubmitting}
|
||||||
|
>
|
||||||
|
发布
|
||||||
|
</Button>
|
||||||
|
</HStack>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default CommentInput;
|
||||||
89
src/components/EventCommentSection/CommentItem.js
Normal file
89
src/components/EventCommentSection/CommentItem.js
Normal file
@@ -0,0 +1,89 @@
|
|||||||
|
// src/components/EventCommentSection/CommentItem.js
|
||||||
|
/**
|
||||||
|
* 单条评论组件
|
||||||
|
* 功能:显示用户头像、昵称、时间、评论内容
|
||||||
|
*/
|
||||||
|
|
||||||
|
import React from 'react';
|
||||||
|
import {
|
||||||
|
Box,
|
||||||
|
HStack,
|
||||||
|
VStack,
|
||||||
|
Avatar,
|
||||||
|
Text,
|
||||||
|
useColorModeValue,
|
||||||
|
} from '@chakra-ui/react';
|
||||||
|
import moment from 'moment';
|
||||||
|
import 'moment/locale/zh-cn';
|
||||||
|
|
||||||
|
moment.locale('zh-cn');
|
||||||
|
|
||||||
|
const CommentItem = ({ comment }) => {
|
||||||
|
const itemBg = useColorModeValue('gray.50', 'gray.700');
|
||||||
|
const usernameColor = useColorModeValue('gray.800', 'gray.100');
|
||||||
|
const timeColor = useColorModeValue('gray.500', 'gray.400');
|
||||||
|
const contentColor = useColorModeValue('gray.700', 'gray.300');
|
||||||
|
|
||||||
|
// 格式化时间
|
||||||
|
const formatTime = (timestamp) => {
|
||||||
|
const now = moment();
|
||||||
|
const time = moment(timestamp);
|
||||||
|
const diffMinutes = now.diff(time, 'minutes');
|
||||||
|
const diffHours = now.diff(time, 'hours');
|
||||||
|
const diffDays = now.diff(time, 'days');
|
||||||
|
|
||||||
|
if (diffMinutes < 1) {
|
||||||
|
return '刚刚';
|
||||||
|
} else if (diffMinutes < 60) {
|
||||||
|
return `${diffMinutes}分钟前`;
|
||||||
|
} else if (diffHours < 24) {
|
||||||
|
return `${diffHours}小时前`;
|
||||||
|
} else if (diffDays < 7) {
|
||||||
|
return `${diffDays}天前`;
|
||||||
|
} else {
|
||||||
|
return time.format('MM-DD HH:mm');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Box
|
||||||
|
p={3}
|
||||||
|
bg={itemBg}
|
||||||
|
borderRadius="md"
|
||||||
|
transition="all 0.2s"
|
||||||
|
_hover={{
|
||||||
|
transform: 'translateY(-2px)',
|
||||||
|
boxShadow: 'sm',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<HStack align="start" spacing={3}>
|
||||||
|
{/* 用户头像 */}
|
||||||
|
<Avatar
|
||||||
|
size="sm"
|
||||||
|
name={comment.author?.username || 'Anonymous'}
|
||||||
|
src={comment.author?.avatar}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{/* 评论内容区 */}
|
||||||
|
<VStack align="stretch" flex={1} spacing={1}>
|
||||||
|
{/* 用户名和时间 */}
|
||||||
|
<HStack spacing={2}>
|
||||||
|
<Text fontSize="sm" fontWeight="bold" color={usernameColor}>
|
||||||
|
{comment.author?.username || 'Anonymous'}
|
||||||
|
</Text>
|
||||||
|
<Text fontSize="xs" color={timeColor}>
|
||||||
|
{formatTime(comment.created_at)}
|
||||||
|
</Text>
|
||||||
|
</HStack>
|
||||||
|
|
||||||
|
{/* 评论内容 */}
|
||||||
|
<Text fontSize="sm" color={contentColor} lineHeight="1.6">
|
||||||
|
{comment.content}
|
||||||
|
</Text>
|
||||||
|
</VStack>
|
||||||
|
</HStack>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default CommentItem;
|
||||||
67
src/components/EventCommentSection/CommentList.js
Normal file
67
src/components/EventCommentSection/CommentList.js
Normal file
@@ -0,0 +1,67 @@
|
|||||||
|
// src/components/EventCommentSection/CommentList.js
|
||||||
|
/**
|
||||||
|
* 评论列表组件
|
||||||
|
* 功能:展示评论列表、加载状态、空状态
|
||||||
|
*/
|
||||||
|
|
||||||
|
import React from 'react';
|
||||||
|
import {
|
||||||
|
VStack,
|
||||||
|
Spinner,
|
||||||
|
Center,
|
||||||
|
Text,
|
||||||
|
Box,
|
||||||
|
useColorModeValue,
|
||||||
|
} from '@chakra-ui/react';
|
||||||
|
import { ChatIcon } from '@chakra-ui/icons';
|
||||||
|
import CommentItem from './CommentItem';
|
||||||
|
|
||||||
|
const CommentList = ({ comments, loading }) => {
|
||||||
|
const emptyTextColor = useColorModeValue('gray.500', 'gray.400');
|
||||||
|
const emptyBgColor = useColorModeValue('gray.50', 'gray.700');
|
||||||
|
|
||||||
|
// 加载状态
|
||||||
|
if (loading) {
|
||||||
|
return (
|
||||||
|
<Center py={8}>
|
||||||
|
<VStack spacing={3}>
|
||||||
|
<Spinner size="md" color="blue.500" thickness="3px" />
|
||||||
|
<Text fontSize="sm" color={emptyTextColor}>
|
||||||
|
加载评论中...
|
||||||
|
</Text>
|
||||||
|
</VStack>
|
||||||
|
</Center>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 空状态
|
||||||
|
if (!comments || comments.length === 0) {
|
||||||
|
return (
|
||||||
|
<Center py={8}>
|
||||||
|
<VStack spacing={3}>
|
||||||
|
<Box
|
||||||
|
p={4}
|
||||||
|
bg={emptyBgColor}
|
||||||
|
borderRadius="full"
|
||||||
|
>
|
||||||
|
<ChatIcon boxSize={6} color={emptyTextColor} />
|
||||||
|
</Box>
|
||||||
|
<Text fontSize="sm" color={emptyTextColor}>
|
||||||
|
还没有评论,快来发表第一条吧~
|
||||||
|
</Text>
|
||||||
|
</VStack>
|
||||||
|
</Center>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 评论列表
|
||||||
|
return (
|
||||||
|
<VStack align="stretch" spacing={3}>
|
||||||
|
{comments.map((comment) => (
|
||||||
|
<CommentItem key={comment.id} comment={comment} />
|
||||||
|
))}
|
||||||
|
</VStack>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default CommentList;
|
||||||
194
src/components/EventCommentSection/EventCommentSection.js
Normal file
194
src/components/EventCommentSection/EventCommentSection.js
Normal file
@@ -0,0 +1,194 @@
|
|||||||
|
// src/components/EventCommentSection/EventCommentSection.js
|
||||||
|
/**
|
||||||
|
* 事件评论区主组件
|
||||||
|
* 功能:整合评论列表 + 评论输入框,管理评论数据
|
||||||
|
*/
|
||||||
|
|
||||||
|
import React, { useState, useEffect, useCallback } from 'react';
|
||||||
|
import {
|
||||||
|
Box,
|
||||||
|
VStack,
|
||||||
|
Heading,
|
||||||
|
Badge,
|
||||||
|
HStack,
|
||||||
|
Divider,
|
||||||
|
useColorModeValue,
|
||||||
|
useToast,
|
||||||
|
} from '@chakra-ui/react';
|
||||||
|
import { useAuth } from '../../contexts/AuthContext';
|
||||||
|
import { eventService } from '../../services/eventService';
|
||||||
|
import { logger } from '../../utils/logger';
|
||||||
|
import CommentList from './CommentList';
|
||||||
|
import CommentInput from './CommentInput';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 事件评论区组件
|
||||||
|
* @param {Object} props
|
||||||
|
* @param {number} props.eventId - 事件 ID
|
||||||
|
*/
|
||||||
|
const EventCommentSection = ({ eventId }) => {
|
||||||
|
const { user } = useAuth();
|
||||||
|
const toast = useToast();
|
||||||
|
const dividerColor = useColorModeValue('gray.200', 'gray.600');
|
||||||
|
const headingColor = useColorModeValue('gray.700', 'gray.200');
|
||||||
|
const sectionBg = useColorModeValue('gray.50', 'gray.750');
|
||||||
|
|
||||||
|
// 状态管理
|
||||||
|
const [comments, setComments] = useState([]);
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
|
const [commentText, setCommentText] = useState('');
|
||||||
|
const [submitting, setSubmitting] = useState(false);
|
||||||
|
const [totalCount, setTotalCount] = useState(0); // 总评论数(从后端获取)
|
||||||
|
|
||||||
|
// 加载评论列表
|
||||||
|
const loadComments = useCallback(async () => {
|
||||||
|
if (!eventId) return;
|
||||||
|
|
||||||
|
setLoading(true);
|
||||||
|
try {
|
||||||
|
// 加载第1页,每页5条评论
|
||||||
|
const result = await eventService.getPosts(eventId, 'latest', 1, 5);
|
||||||
|
if (result.success) {
|
||||||
|
setComments(result.data || []);
|
||||||
|
// 保存总评论数(从 pagination.total 读取)
|
||||||
|
setTotalCount(result.pagination?.total || result.data?.length || 0);
|
||||||
|
logger.info('EventCommentSection', '评论加载成功', {
|
||||||
|
eventId,
|
||||||
|
count: result.data?.length || 0,
|
||||||
|
total: result.pagination?.total || 0,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
logger.error('EventCommentSection', 'loadComments', error, { eventId });
|
||||||
|
toast({
|
||||||
|
title: '加载评论失败',
|
||||||
|
description: error.message || '请稍后重试',
|
||||||
|
status: 'error',
|
||||||
|
duration: 3000,
|
||||||
|
isClosable: true,
|
||||||
|
});
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
}, [eventId, toast]);
|
||||||
|
|
||||||
|
// 发表评论
|
||||||
|
const handleSubmitComment = useCallback(async () => {
|
||||||
|
if (!commentText.trim()) {
|
||||||
|
toast({
|
||||||
|
title: '请输入评论内容',
|
||||||
|
status: 'warning',
|
||||||
|
duration: 2000,
|
||||||
|
isClosable: true,
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setSubmitting(true);
|
||||||
|
try {
|
||||||
|
const result = await eventService.createPost(eventId, {
|
||||||
|
content: commentText.trim(),
|
||||||
|
content_type: 'text',
|
||||||
|
});
|
||||||
|
|
||||||
|
if (result.success) {
|
||||||
|
// 乐观更新:立即将新评论添加到本地 state,避免重新加载导致的闪烁
|
||||||
|
const newComment = {
|
||||||
|
id: result.data?.id || `comment_optimistic_${Date.now()}`,
|
||||||
|
content: commentText.trim(),
|
||||||
|
content_type: 'text',
|
||||||
|
author: {
|
||||||
|
id: user?.id || 'current_user',
|
||||||
|
username: user?.username || '当前用户',
|
||||||
|
avatar: user?.avatar || null,
|
||||||
|
},
|
||||||
|
created_at: new Date().toISOString(),
|
||||||
|
likes_count: 0,
|
||||||
|
is_liked: false,
|
||||||
|
};
|
||||||
|
|
||||||
|
// 将新评论追加到列表末尾(最新评论在底部)
|
||||||
|
setComments([...comments, newComment]);
|
||||||
|
// 总评论数 +1
|
||||||
|
setTotalCount(totalCount + 1);
|
||||||
|
|
||||||
|
toast({
|
||||||
|
title: '评论发布成功',
|
||||||
|
status: 'success',
|
||||||
|
duration: 2000,
|
||||||
|
isClosable: true,
|
||||||
|
});
|
||||||
|
setCommentText(''); // 清空输入框
|
||||||
|
// ✅ 不再调用 loadComments(),避免 loading 状态导致高度闪烁
|
||||||
|
|
||||||
|
logger.info('EventCommentSection', '评论发布成功(乐观更新)', {
|
||||||
|
eventId,
|
||||||
|
content: commentText.trim(),
|
||||||
|
commentId: newComment.id,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
throw new Error(result.message || '评论发布失败');
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
logger.error('EventCommentSection', 'handleSubmitComment', error, { eventId });
|
||||||
|
toast({
|
||||||
|
title: '评论发布失败',
|
||||||
|
description: error.message || '请稍后重试',
|
||||||
|
status: 'error',
|
||||||
|
duration: 3000,
|
||||||
|
isClosable: true,
|
||||||
|
});
|
||||||
|
} finally {
|
||||||
|
setSubmitting(false);
|
||||||
|
}
|
||||||
|
}, [eventId, commentText, toast, comments, user, totalCount]);
|
||||||
|
|
||||||
|
// 初始加载评论
|
||||||
|
useEffect(() => {
|
||||||
|
loadComments();
|
||||||
|
}, [loadComments]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Box>
|
||||||
|
{/* 标题栏 */}
|
||||||
|
<HStack
|
||||||
|
spacing={3}
|
||||||
|
mb={4}
|
||||||
|
p={3}
|
||||||
|
bg={sectionBg}
|
||||||
|
borderRadius="md"
|
||||||
|
>
|
||||||
|
<Heading size="sm" color={headingColor}>
|
||||||
|
讨论区
|
||||||
|
</Heading>
|
||||||
|
<Badge colorScheme="blue" fontSize="sm" borderRadius="full" px={2}>
|
||||||
|
{totalCount} 条评论
|
||||||
|
</Badge>
|
||||||
|
</HStack>
|
||||||
|
|
||||||
|
<Divider borderColor={dividerColor} mb={4} />
|
||||||
|
|
||||||
|
{/* 评论列表 */}
|
||||||
|
<Box mb={4}>
|
||||||
|
<CommentList comments={comments} loading={loading} />
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
{/* 评论输入框(仅登录用户显示) */}
|
||||||
|
{user && (
|
||||||
|
<Box>
|
||||||
|
<Divider borderColor={dividerColor} mb={4} />
|
||||||
|
<CommentInput
|
||||||
|
value={commentText}
|
||||||
|
onChange={(e) => setCommentText(e.target.value)}
|
||||||
|
onSubmit={handleSubmitComment}
|
||||||
|
isSubmitting={submitting}
|
||||||
|
maxLength={500}
|
||||||
|
placeholder="说点什么..."
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
)}
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default EventCommentSection;
|
||||||
10
src/components/EventCommentSection/index.js
Normal file
10
src/components/EventCommentSection/index.js
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
// src/components/EventCommentSection/index.js
|
||||||
|
/**
|
||||||
|
* 事件评论区组件统一导出
|
||||||
|
*/
|
||||||
|
|
||||||
|
export { default } from './EventCommentSection';
|
||||||
|
export { default as EventCommentSection } from './EventCommentSection';
|
||||||
|
export { default as CommentList } from './CommentList';
|
||||||
|
export { default as CommentItem } from './CommentItem';
|
||||||
|
export { default as CommentInput } from './CommentInput';
|
||||||
@@ -9,6 +9,77 @@ import { generatePopularConcepts } from './concept';
|
|||||||
// 模拟网络延迟
|
// 模拟网络延迟
|
||||||
const delay = (ms = 300) => new Promise(resolve => setTimeout(resolve, ms));
|
const delay = (ms = 300) => new Promise(resolve => setTimeout(resolve, ms));
|
||||||
|
|
||||||
|
// ==================== 评论内存存储 ====================
|
||||||
|
// 用于在 Mock 环境下持久化评论数据(按 eventId 分组)
|
||||||
|
const commentsStore = new Map();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 初始化某个事件的 mock 评论列表
|
||||||
|
* @param {string} eventId - 事件 ID
|
||||||
|
* @returns {Array} 初始的 8 条 mock 评论
|
||||||
|
*/
|
||||||
|
const initializeMockComments = (eventId) => {
|
||||||
|
const comments = [];
|
||||||
|
const users = [
|
||||||
|
{ username: '张三', avatar: null },
|
||||||
|
{ username: '李四', avatar: null },
|
||||||
|
{ username: '王五', avatar: null },
|
||||||
|
{ username: '赵六', avatar: null },
|
||||||
|
{ username: '投资达人', avatar: null },
|
||||||
|
{ username: '价值投资者', avatar: null },
|
||||||
|
{ username: '技术分析师', avatar: null },
|
||||||
|
{ username: '基本面研究员', avatar: null },
|
||||||
|
];
|
||||||
|
|
||||||
|
const commentTemplates = [
|
||||||
|
'这个事件对相关板块影响很大,值得关注后续发展',
|
||||||
|
'相关概念股已经开始异动了,市场反应很快',
|
||||||
|
'感谢分享,这个事件我之前没注意到',
|
||||||
|
'从基本面来看,这个事件会带来实质性利好',
|
||||||
|
'需要观察后续政策落地情况,现在下结论还太早',
|
||||||
|
'相关产业链的龙头企业值得重点关注',
|
||||||
|
'这类事件一般都是短期刺激,长期影响有限',
|
||||||
|
'建议大家理性对待,不要盲目追高',
|
||||||
|
];
|
||||||
|
|
||||||
|
for (let i = 0; i < 5; i++) {
|
||||||
|
const hoursAgo = Math.floor(Math.random() * 48) + 1; // 1-48 小时前
|
||||||
|
const createdAt = new Date(Date.now() - hoursAgo * 60 * 60 * 1000);
|
||||||
|
const user = users[i % users.length];
|
||||||
|
|
||||||
|
comments.push({
|
||||||
|
id: `comment_${eventId}_${i + 1}`,
|
||||||
|
content: commentTemplates[i % commentTemplates.length],
|
||||||
|
content_type: 'text',
|
||||||
|
author: {
|
||||||
|
id: `user_${i + 1}`,
|
||||||
|
username: user.username,
|
||||||
|
avatar: user.avatar,
|
||||||
|
},
|
||||||
|
created_at: createdAt.toISOString(),
|
||||||
|
likes_count: Math.floor(Math.random() * 20),
|
||||||
|
is_liked: false,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 按时间升序排序(最旧的在前)
|
||||||
|
comments.sort((a, b) => new Date(a.created_at) - new Date(b.created_at));
|
||||||
|
|
||||||
|
return comments;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取或初始化评论列表
|
||||||
|
* @param {string} eventId - 事件 ID
|
||||||
|
* @returns {Array} 评论列表
|
||||||
|
*/
|
||||||
|
const getOrInitComments = (eventId) => {
|
||||||
|
if (!commentsStore.has(eventId)) {
|
||||||
|
commentsStore.set(eventId, initializeMockComments(eventId));
|
||||||
|
}
|
||||||
|
return commentsStore.get(eventId);
|
||||||
|
};
|
||||||
|
|
||||||
export const eventHandlers = [
|
export const eventHandlers = [
|
||||||
// ==================== 事件列表相关 ====================
|
// ==================== 事件列表相关 ====================
|
||||||
|
|
||||||
@@ -1249,4 +1320,111 @@ export const eventHandlers = [
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
|
|
||||||
|
// ==================== 评论相关 ====================
|
||||||
|
|
||||||
|
// 获取事件评论列表
|
||||||
|
http.get('/api/events/:eventId/posts', async ({ params, request }) => {
|
||||||
|
await delay(300);
|
||||||
|
|
||||||
|
const { eventId } = params;
|
||||||
|
const url = new URL(request.url);
|
||||||
|
const sort = url.searchParams.get('sort') || 'latest';
|
||||||
|
const page = parseInt(url.searchParams.get('page') || '1');
|
||||||
|
const perPage = parseInt(url.searchParams.get('per_page') || '20');
|
||||||
|
|
||||||
|
console.log('[Mock] 获取评论列表, eventId:', eventId, 'sort:', sort);
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 从内存存储获取评论列表
|
||||||
|
const allComments = getOrInitComments(eventId);
|
||||||
|
|
||||||
|
// ✅ 创建副本并排序(避免直接修改原数组)
|
||||||
|
let sortedComments = [...allComments];
|
||||||
|
if (sort === 'hot') {
|
||||||
|
sortedComments.sort((a, b) => b.likes_count - a.likes_count);
|
||||||
|
} else {
|
||||||
|
// 默认按时间升序(oldest first)- 最旧评论在前,最新在后
|
||||||
|
sortedComments.sort((a, b) => new Date(a.created_at) - new Date(b.created_at));
|
||||||
|
}
|
||||||
|
|
||||||
|
// 分页处理(使用排序后的副本)
|
||||||
|
const startIndex = (page - 1) * perPage;
|
||||||
|
const endIndex = startIndex + perPage;
|
||||||
|
const paginatedComments = sortedComments.slice(startIndex, endIndex);
|
||||||
|
|
||||||
|
return HttpResponse.json({
|
||||||
|
success: true,
|
||||||
|
data: paginatedComments,
|
||||||
|
pagination: {
|
||||||
|
page: page,
|
||||||
|
per_page: perPage,
|
||||||
|
total: allComments.length,
|
||||||
|
pages: Math.ceil(allComments.length / perPage),
|
||||||
|
has_prev: page > 1,
|
||||||
|
has_next: endIndex < allComments.length,
|
||||||
|
},
|
||||||
|
message: '获取评论成功',
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error('[Mock] 获取评论列表失败:', error);
|
||||||
|
return HttpResponse.json(
|
||||||
|
{
|
||||||
|
success: false,
|
||||||
|
error: '获取评论失败',
|
||||||
|
data: [],
|
||||||
|
},
|
||||||
|
{ status: 500 }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
|
||||||
|
// 发表评论
|
||||||
|
http.post('/api/events/:eventId/posts', async ({ params, request }) => {
|
||||||
|
await delay(500);
|
||||||
|
|
||||||
|
const { eventId } = params;
|
||||||
|
const body = await request.json();
|
||||||
|
|
||||||
|
console.log('[Mock] 发表评论, eventId:', eventId, 'content:', body.content);
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 模拟创建新评论
|
||||||
|
const newComment = {
|
||||||
|
id: `comment_${eventId}_${Date.now()}`,
|
||||||
|
content: body.content,
|
||||||
|
content_type: body.content_type || 'text',
|
||||||
|
author: {
|
||||||
|
id: 'current_user',
|
||||||
|
username: '当前用户',
|
||||||
|
avatar: null,
|
||||||
|
},
|
||||||
|
created_at: new Date().toISOString(),
|
||||||
|
likes_count: 0,
|
||||||
|
is_liked: false,
|
||||||
|
};
|
||||||
|
|
||||||
|
// 将新评论添加到内存存储(插入到列表开头)
|
||||||
|
const comments = getOrInitComments(eventId);
|
||||||
|
comments.unshift(newComment);
|
||||||
|
|
||||||
|
console.log('[Mock] 评论已添加到内存存储, 当前评论总数:', comments.length);
|
||||||
|
|
||||||
|
return HttpResponse.json({
|
||||||
|
success: true,
|
||||||
|
data: newComment,
|
||||||
|
message: '评论发布成功',
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error('[Mock] 发表评论失败:', error);
|
||||||
|
return HttpResponse.json(
|
||||||
|
{
|
||||||
|
success: false,
|
||||||
|
error: '评论发布失败',
|
||||||
|
message: '系统错误,请稍后重试',
|
||||||
|
},
|
||||||
|
{ status: 500 }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}),
|
||||||
];
|
];
|
||||||
|
|||||||
@@ -4,6 +4,7 @@
|
|||||||
import React, { useState, useCallback, useEffect } from 'react';
|
import React, { useState, useCallback, useEffect } from 'react';
|
||||||
import { useDispatch, useSelector } from 'react-redux';
|
import { useDispatch, useSelector } from 'react-redux';
|
||||||
import {
|
import {
|
||||||
|
Box,
|
||||||
Card,
|
Card,
|
||||||
CardBody,
|
CardBody,
|
||||||
VStack,
|
VStack,
|
||||||
@@ -32,6 +33,7 @@ import TransmissionChainAnalysis from '../../../EventDetail/components/Transmiss
|
|||||||
import SubscriptionBadge from '../../../../components/SubscriptionBadge';
|
import SubscriptionBadge from '../../../../components/SubscriptionBadge';
|
||||||
import SubscriptionUpgradeModal from '../../../../components/SubscriptionUpgradeModal';
|
import SubscriptionUpgradeModal from '../../../../components/SubscriptionUpgradeModal';
|
||||||
import { PROFESSIONAL_COLORS } from '../../../../constants/professionalTheme';
|
import { PROFESSIONAL_COLORS } from '../../../../constants/professionalTheme';
|
||||||
|
import EventCommentSection from '../../../../components/EventCommentSection';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 动态新闻详情面板主组件
|
* 动态新闻详情面板主组件
|
||||||
@@ -414,6 +416,11 @@ const DynamicNewsDetailPanel = ({ event, showHeader = true }) => {
|
|||||||
eventService={eventService}
|
eventService={eventService}
|
||||||
/>
|
/>
|
||||||
</CollapsibleSection>
|
</CollapsibleSection>
|
||||||
|
|
||||||
|
{/* 讨论区(评论区) - 所有登录用户可用 */}
|
||||||
|
<Box mt={4}>
|
||||||
|
<EventCommentSection eventId={event.id} />
|
||||||
|
</Box>
|
||||||
</VStack>
|
</VStack>
|
||||||
</CardBody>
|
</CardBody>
|
||||||
|
|
||||||
|
|||||||
@@ -74,7 +74,9 @@ const KeywordsCarousel = ({
|
|||||||
whiteSpace="nowrap"
|
whiteSpace="nowrap"
|
||||||
textShadow="0 0 20px rgba(255, 195, 0, 0.3)"
|
textShadow="0 0 20px rgba(255, 195, 0, 0.3)"
|
||||||
>
|
>
|
||||||
{currentKeyword}
|
{typeof currentKeyword === 'string'
|
||||||
|
? currentKeyword
|
||||||
|
: currentKeyword?.concept || '未知标签'}
|
||||||
</Text>
|
</Text>
|
||||||
</Box>
|
</Box>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
|
|||||||
Reference in New Issue
Block a user