476 lines
15 KiB
JavaScript
476 lines
15 KiB
JavaScript
/**
|
||
* 预测话题评论区组件
|
||
* 支持发布评论、嵌套回复、点赞、庄主标识、观点IPO投资
|
||
*/
|
||
|
||
import React, { useState, useEffect } from 'react';
|
||
import {
|
||
Box,
|
||
VStack,
|
||
HStack,
|
||
Text,
|
||
Avatar,
|
||
Textarea,
|
||
Button,
|
||
Flex,
|
||
IconButton,
|
||
Divider,
|
||
useToast,
|
||
Badge,
|
||
Tooltip,
|
||
} from '@chakra-ui/react';
|
||
import { motion, AnimatePresence } from 'framer-motion';
|
||
import { Heart, MessageCircle, Send, TrendingUp, Crown, Pin } from 'lucide-react';
|
||
import { forumColors } from '@theme/forumTheme';
|
||
import {
|
||
createComment,
|
||
getComments,
|
||
likeComment,
|
||
} from '@services/predictionMarketService.api';
|
||
import { useAuth } from '@contexts/AuthContext';
|
||
|
||
const MotionBox = motion(Box);
|
||
|
||
const CommentItem = ({ comment, topicId, topic, onReply, onInvest }) => {
|
||
const [isLiked, setIsLiked] = useState(false);
|
||
const [likes, setLikes] = useState(comment.likes_count || 0);
|
||
const [showReply, setShowReply] = useState(false);
|
||
|
||
// 处理点赞
|
||
const handleLike = async () => {
|
||
try {
|
||
const response = await likeComment(comment.id);
|
||
if (response.success) {
|
||
setLikes(response.likes_count);
|
||
setIsLiked(response.action === 'like');
|
||
}
|
||
} 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',
|
||
});
|
||
};
|
||
|
||
// 判断是否是庄主
|
||
const isYesLord = comment.user?.id === topic?.yes_lord_id;
|
||
const isNoLord = comment.user?.id === topic?.no_lord_id;
|
||
const isLord = isYesLord || isNoLord;
|
||
|
||
return (
|
||
<MotionBox
|
||
initial={{ opacity: 0, y: 10 }}
|
||
animate={{ opacity: 1, y: 0 }}
|
||
transition={{ duration: 0.3 }}
|
||
>
|
||
<Flex gap={{ base: "2", sm: "3" }} py={{ base: "3", sm: "4" }}>
|
||
{/* 头像 */}
|
||
<Avatar
|
||
size={{ base: "sm", sm: "md" }}
|
||
name={comment.user?.nickname || comment.user?.username}
|
||
src={comment.user?.avatar_url}
|
||
bg={forumColors.gradients.goldPrimary}
|
||
color={forumColors.background.main}
|
||
/>
|
||
|
||
{/* 评论内容 */}
|
||
<VStack align="stretch" flex="1" spacing={{ base: "1.5", sm: "2" }}>
|
||
{/* 用户名和时间 */}
|
||
<HStack justify="space-between" flexWrap="wrap">
|
||
<HStack spacing="2">
|
||
<Text fontSize={{ base: "sm", sm: "md" }} fontWeight="600" color={forumColors.text.primary}>
|
||
{comment.user?.nickname || comment.user?.username || '匿名用户'}
|
||
</Text>
|
||
|
||
{/* 庄主标识 */}
|
||
{isLord && (
|
||
<Tooltip label={isYesLord ? 'YES方庄主' : 'NO方庄主'} placement="top">
|
||
<Badge
|
||
bg={isYesLord ? 'green.500' : 'red.500'}
|
||
color="white"
|
||
fontSize="2xs"
|
||
px="2"
|
||
py="0.5"
|
||
borderRadius="full"
|
||
display="flex"
|
||
alignItems="center"
|
||
gap="1"
|
||
>
|
||
<Crown size={10} />
|
||
{isYesLord ? 'YES庄' : 'NO庄'}
|
||
</Badge>
|
||
</Tooltip>
|
||
)}
|
||
|
||
{/* 置顶标识 */}
|
||
{comment.is_pinned && (
|
||
<Badge
|
||
bg={forumColors.primary[500]}
|
||
color="white"
|
||
fontSize="2xs"
|
||
px="2"
|
||
py="0.5"
|
||
borderRadius="full"
|
||
display="flex"
|
||
alignItems="center"
|
||
gap="1"
|
||
>
|
||
<Pin size={10} />
|
||
置顶
|
||
</Badge>
|
||
)}
|
||
|
||
<Text fontSize={{ base: "2xs", sm: "xs" }} color={forumColors.text.muted}>
|
||
{formatTime(comment.created_at)}
|
||
</Text>
|
||
</HStack>
|
||
</HStack>
|
||
|
||
{/* 评论正文 */}
|
||
<Text fontSize={{ base: "sm", sm: "md" }} color={forumColors.text.secondary} lineHeight="1.6">
|
||
{comment.content}
|
||
</Text>
|
||
|
||
{/* 观点IPO投资统计 */}
|
||
{comment.total_investment > 0 && (
|
||
<Box
|
||
bg={forumColors.gradients.goldSubtle}
|
||
border="1px solid"
|
||
borderColor={forumColors.border.gold}
|
||
borderRadius="md"
|
||
px="3"
|
||
py="2"
|
||
mt="1"
|
||
>
|
||
<HStack spacing="4" fontSize="xs">
|
||
<HStack spacing="1">
|
||
<TrendingUp size={12} color={forumColors.primary[500]} />
|
||
<Text color={forumColors.text.secondary}>
|
||
{comment.investor_count || 0}人投资
|
||
</Text>
|
||
</HStack>
|
||
<Text color={forumColors.text.secondary}>
|
||
总投资:<Text as="span" fontWeight="600" color={forumColors.primary[500]}>
|
||
{comment.total_investment}
|
||
</Text> 积分
|
||
</Text>
|
||
{comment.is_verified && (
|
||
<Badge
|
||
colorScheme={comment.verification_result === 'correct' ? 'green' : 'red'}
|
||
fontSize="2xs"
|
||
>
|
||
{comment.verification_result === 'correct' ? '✓ 预测正确' : '✗ 预测错误'}
|
||
</Badge>
|
||
)}
|
||
</HStack>
|
||
</Box>
|
||
)}
|
||
|
||
{/* 操作按钮 */}
|
||
<HStack spacing={{ base: "3", sm: "4" }} fontSize="xs" color={forumColors.text.tertiary} flexWrap="wrap">
|
||
<HStack
|
||
spacing="1"
|
||
cursor="pointer"
|
||
onClick={handleLike}
|
||
_hover={{ color: forumColors.primary[500] }}
|
||
color={isLiked ? forumColors.primary[500] : forumColors.text.tertiary}
|
||
>
|
||
<Heart size={14} fill={isLiked ? 'currentColor' : 'none'} />
|
||
<Text>{likes > 0 ? likes : '点赞'}</Text>
|
||
</HStack>
|
||
|
||
<HStack
|
||
spacing="1"
|
||
cursor="pointer"
|
||
onClick={() => setShowReply(!showReply)}
|
||
_hover={{ color: forumColors.primary[500] }}
|
||
>
|
||
<MessageCircle size={14} />
|
||
<Text>回复</Text>
|
||
</HStack>
|
||
|
||
{/* 投资观点按钮 */}
|
||
{!comment.is_verified && onInvest && (
|
||
<Button
|
||
size="xs"
|
||
variant="ghost"
|
||
leftIcon={<TrendingUp size={14} />}
|
||
color={forumColors.primary[500]}
|
||
_hover={{ bg: forumColors.gradients.goldSubtle }}
|
||
onClick={() => onInvest(comment)}
|
||
>
|
||
投资观点
|
||
</Button>
|
||
)}
|
||
</HStack>
|
||
|
||
{/* 回复输入框 */}
|
||
{showReply && (
|
||
<MotionBox
|
||
initial={{ opacity: 0, height: 0 }}
|
||
animate={{ opacity: 1, height: 'auto' }}
|
||
exit={{ opacity: 0, height: 0 }}
|
||
transition={{ duration: 0.2 }}
|
||
mt="2"
|
||
>
|
||
<ReplyInput
|
||
topicId={topicId}
|
||
parentId={comment.id}
|
||
placeholder={`回复 @${comment.user?.nickname || comment.user?.username || '匿名用户'}`}
|
||
onSubmit={() => {
|
||
setShowReply(false);
|
||
if (onReply) onReply();
|
||
}}
|
||
/>
|
||
</MotionBox>
|
||
)}
|
||
|
||
{/* 回复列表 */}
|
||
{comment.replies && comment.replies.length > 0 && (
|
||
<VStack
|
||
align="stretch"
|
||
spacing="2"
|
||
pl={{ base: "3", sm: "4" }}
|
||
mt="2"
|
||
borderLeft="2px solid"
|
||
borderColor={forumColors.border.default}
|
||
>
|
||
{comment.replies.map((reply) => (
|
||
<Box key={reply.id}>
|
||
<HStack spacing="2" mb="1">
|
||
<Avatar
|
||
size="xs"
|
||
name={reply.user?.nickname || reply.user?.username}
|
||
src={reply.user?.avatar_url}
|
||
/>
|
||
<Text fontSize="xs" fontWeight="600" color={forumColors.text.primary}>
|
||
{reply.user?.nickname || reply.user?.username || '匿名用户'}
|
||
</Text>
|
||
<Text fontSize="2xs" color={forumColors.text.muted}>
|
||
{formatTime(reply.created_at)}
|
||
</Text>
|
||
</HStack>
|
||
<Text fontSize="sm" color={forumColors.text.secondary} pl="6">
|
||
{reply.content}
|
||
</Text>
|
||
</Box>
|
||
))}
|
||
</VStack>
|
||
)}
|
||
</VStack>
|
||
</Flex>
|
||
</MotionBox>
|
||
);
|
||
};
|
||
|
||
const ReplyInput = ({ topicId, 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 {
|
||
const response = await createComment(topicId, {
|
||
content: content.trim(),
|
||
parent_id: parentId,
|
||
});
|
||
|
||
if (response.success) {
|
||
toast({
|
||
title: '评论成功',
|
||
status: 'success',
|
||
duration: 2000,
|
||
});
|
||
|
||
setContent('');
|
||
if (onSubmit) onSubmit();
|
||
}
|
||
} catch (error) {
|
||
console.error('评论失败:', error);
|
||
toast({
|
||
title: '评论失败',
|
||
description: error.response?.data?.error || error.message,
|
||
status: 'error',
|
||
duration: 3000,
|
||
});
|
||
} finally {
|
||
setIsSubmitting(false);
|
||
}
|
||
};
|
||
|
||
return (
|
||
<Flex gap="2" align="end">
|
||
<Textarea
|
||
value={content}
|
||
onChange={(e) => setContent(e.target.value)}
|
||
placeholder={placeholder || '写下你的评论...'}
|
||
size="sm"
|
||
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}`,
|
||
}}
|
||
minH={{ base: "70px", sm: "80px" }}
|
||
resize="vertical"
|
||
fontSize={{ base: "sm", sm: "md" }}
|
||
/>
|
||
<IconButton
|
||
icon={<Send size={18} />}
|
||
onClick={handleSubmit}
|
||
isLoading={isSubmitting}
|
||
bg={forumColors.gradients.goldPrimary}
|
||
color={forumColors.background.main}
|
||
_hover={{ opacity: 0.9 }}
|
||
size="sm"
|
||
h={{ base: "9", sm: "10" }}
|
||
/>
|
||
</Flex>
|
||
);
|
||
};
|
||
|
||
const PredictionCommentSection = ({ topicId, topic, onInvest }) => {
|
||
const [comments, setComments] = useState([]);
|
||
const [loading, setLoading] = useState(true);
|
||
const [total, setTotal] = useState(0);
|
||
const [page, setPage] = useState(1);
|
||
const [hasMore, setHasMore] = useState(false);
|
||
|
||
// 加载评论
|
||
const loadComments = async (pageNum = 1) => {
|
||
try {
|
||
setLoading(true);
|
||
const response = await getComments(topicId, { page: pageNum, per_page: 20 });
|
||
|
||
if (response.success) {
|
||
if (pageNum === 1) {
|
||
setComments(response.data);
|
||
} else {
|
||
setComments((prev) => [...prev, ...response.data]);
|
||
}
|
||
setTotal(response.pagination?.total || response.data.length);
|
||
setHasMore(response.pagination?.has_next || false);
|
||
setPage(pageNum);
|
||
}
|
||
} catch (error) {
|
||
console.error('加载评论失败:', error);
|
||
} finally {
|
||
setLoading(false);
|
||
}
|
||
};
|
||
|
||
useEffect(() => {
|
||
loadComments();
|
||
}, [topicId]);
|
||
|
||
return (
|
||
<Box
|
||
bg={forumColors.background.card}
|
||
borderRadius="xl"
|
||
border="1px solid"
|
||
borderColor={forumColors.border.default}
|
||
p={{ base: "4", sm: "6" }}
|
||
>
|
||
{/* 标题 */}
|
||
<Flex justify="space-between" align="center" mb={{ base: "4", sm: "6" }}>
|
||
<HStack spacing="2">
|
||
<MessageCircle size={20} color={forumColors.primary[500]} />
|
||
<Text fontSize={{ base: "md", sm: "lg" }} fontWeight="bold" color={forumColors.text.primary}>
|
||
评论
|
||
</Text>
|
||
</HStack>
|
||
|
||
<Text fontSize="sm" color={forumColors.text.tertiary}>
|
||
共 {total} 条
|
||
</Text>
|
||
</Flex>
|
||
|
||
{/* 发表评论 */}
|
||
<Box mb={{ base: "4", sm: "6" }}>
|
||
<ReplyInput topicId={topicId} onSubmit={() => loadComments(1)} />
|
||
</Box>
|
||
|
||
<Divider borderColor={forumColors.border.default} mb="4" />
|
||
|
||
{/* 评论列表 */}
|
||
{loading && page === 1 ? (
|
||
<Text color={forumColors.text.secondary} textAlign="center" py="8">
|
||
加载中...
|
||
</Text>
|
||
) : comments.length === 0 ? (
|
||
<Text color={forumColors.text.secondary} textAlign="center" py="8">
|
||
暂无评论,快来抢沙发吧!
|
||
</Text>
|
||
) : (
|
||
<>
|
||
<VStack align="stretch" spacing="0" divider={<Divider borderColor={forumColors.border.default} />}>
|
||
<AnimatePresence>
|
||
{comments.map((comment) => (
|
||
<CommentItem
|
||
key={comment.id}
|
||
comment={comment}
|
||
topicId={topicId}
|
||
topic={topic}
|
||
onReply={() => loadComments(1)}
|
||
onInvest={onInvest}
|
||
/>
|
||
))}
|
||
</AnimatePresence>
|
||
</VStack>
|
||
|
||
{/* 加载更多 */}
|
||
{hasMore && (
|
||
<Flex justify="center" mt="6">
|
||
<Button
|
||
variant="ghost"
|
||
onClick={() => loadComments(page + 1)}
|
||
isLoading={loading}
|
||
color={forumColors.text.secondary}
|
||
_hover={{ bg: forumColors.background.hover }}
|
||
>
|
||
加载更多
|
||
</Button>
|
||
</Flex>
|
||
)}
|
||
</>
|
||
)}
|
||
</Box>
|
||
);
|
||
};
|
||
|
||
export default PredictionCommentSection;
|