update pay function
This commit is contained in:
@@ -39,6 +39,8 @@ import { forumColors } from '@theme/forumTheme';
|
||||
import { getTopicDetail, getUserAccount } from '@services/predictionMarketService.api';
|
||||
import { useAuth } from '@contexts/AuthContext';
|
||||
import TradeModal from './components/TradeModal';
|
||||
import PredictionCommentSection from './components/PredictionCommentSection';
|
||||
import CommentInvestModal from './components/CommentInvestModal';
|
||||
|
||||
const MotionBox = motion(Box);
|
||||
|
||||
@@ -52,6 +54,8 @@ const PredictionTopicDetail = () => {
|
||||
const [topic, setTopic] = useState(null);
|
||||
const [userAccount, setUserAccount] = useState(null);
|
||||
const [tradeMode, setTradeMode] = useState('buy');
|
||||
const [selectedComment, setSelectedComment] = useState(null);
|
||||
const [commentSectionKey, setCommentSectionKey] = useState(0);
|
||||
|
||||
// 模态框
|
||||
const {
|
||||
@@ -60,6 +64,12 @@ const PredictionTopicDetail = () => {
|
||||
onClose: onTradeModalClose,
|
||||
} = useDisclosure();
|
||||
|
||||
const {
|
||||
isOpen: isInvestModalOpen,
|
||||
onOpen: onInvestModalOpen,
|
||||
onClose: onInvestModalClose,
|
||||
} = useDisclosure();
|
||||
|
||||
// 加载话题数据
|
||||
useEffect(() => {
|
||||
const loadTopic = async () => {
|
||||
@@ -136,6 +146,44 @@ const PredictionTopicDetail = () => {
|
||||
}
|
||||
};
|
||||
|
||||
// 打开投资弹窗
|
||||
const handleOpenInvest = (comment) => {
|
||||
if (!user) {
|
||||
toast({
|
||||
title: '请先登录',
|
||||
status: 'warning',
|
||||
duration: 3000,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
setSelectedComment(comment);
|
||||
onInvestModalOpen();
|
||||
};
|
||||
|
||||
// 投资成功回调
|
||||
const handleInvestSuccess = async () => {
|
||||
// 刷新账户数据
|
||||
try {
|
||||
const accountResponse = await getUserAccount();
|
||||
if (accountResponse.success) {
|
||||
setUserAccount(accountResponse.data);
|
||||
}
|
||||
|
||||
// 刷新评论区(通过更新key触发重新加载)
|
||||
setCommentSectionKey((prev) => prev + 1);
|
||||
|
||||
toast({
|
||||
title: '投资成功',
|
||||
description: '评论列表已刷新',
|
||||
status: 'success',
|
||||
duration: 2000,
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('刷新数据失败:', error);
|
||||
}
|
||||
};
|
||||
|
||||
if (!topic) {
|
||||
return null;
|
||||
}
|
||||
@@ -561,6 +609,22 @@ const PredictionTopicDetail = () => {
|
||||
)}
|
||||
</VStack>
|
||||
</Box>
|
||||
|
||||
{/* 评论区 - 占据全宽 */}
|
||||
<Box gridColumn={{ base: "1", lg: "1 / -1" }}>
|
||||
<MotionBox
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ duration: 0.5, delay: 0.3 }}
|
||||
>
|
||||
<PredictionCommentSection
|
||||
key={commentSectionKey}
|
||||
topicId={topicId}
|
||||
topic={topic}
|
||||
onInvest={handleOpenInvest}
|
||||
/>
|
||||
</MotionBox>
|
||||
</Box>
|
||||
</SimpleGrid>
|
||||
</Container>
|
||||
|
||||
@@ -572,6 +636,15 @@ const PredictionTopicDetail = () => {
|
||||
mode={tradeMode}
|
||||
onTradeSuccess={handleTradeSuccess}
|
||||
/>
|
||||
|
||||
{/* 观点投资模态框 */}
|
||||
<CommentInvestModal
|
||||
isOpen={isInvestModalOpen}
|
||||
onClose={onInvestModalClose}
|
||||
comment={selectedComment}
|
||||
topic={topic}
|
||||
onInvestSuccess={handleInvestSuccess}
|
||||
/>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
464
src/views/ValueForum/components/CommentInvestModal.js
Normal file
464
src/views/ValueForum/components/CommentInvestModal.js
Normal file
@@ -0,0 +1,464 @@
|
||||
/**
|
||||
* 观点IPO投资弹窗组件
|
||||
* 用于投资评论观点
|
||||
*/
|
||||
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import {
|
||||
Modal,
|
||||
ModalOverlay,
|
||||
ModalContent,
|
||||
ModalHeader,
|
||||
ModalBody,
|
||||
ModalFooter,
|
||||
ModalCloseButton,
|
||||
Button,
|
||||
VStack,
|
||||
HStack,
|
||||
Text,
|
||||
Box,
|
||||
Icon,
|
||||
Slider,
|
||||
SliderTrack,
|
||||
SliderFilledTrack,
|
||||
SliderThumb,
|
||||
Flex,
|
||||
useToast,
|
||||
Avatar,
|
||||
Badge,
|
||||
Divider,
|
||||
} from '@chakra-ui/react';
|
||||
import { TrendingUp, DollarSign, AlertCircle, Lightbulb, Crown } from 'lucide-react';
|
||||
import { motion } from 'framer-motion';
|
||||
import { forumColors } from '@theme/forumTheme';
|
||||
import {
|
||||
investComment,
|
||||
getUserAccount,
|
||||
} from '@services/predictionMarketService.api';
|
||||
import { useAuth } from '@contexts/AuthContext';
|
||||
|
||||
const MotionBox = motion(Box);
|
||||
|
||||
const CommentInvestModal = ({ isOpen, onClose, comment, topic, onInvestSuccess }) => {
|
||||
const toast = useToast();
|
||||
const { user } = useAuth();
|
||||
|
||||
const [shares, setShares] = useState(1);
|
||||
const [isSubmitting, setIsSubmitting] = useState(false);
|
||||
const [userAccount, setUserAccount] = useState(null);
|
||||
|
||||
// 异步获取用户账户
|
||||
useEffect(() => {
|
||||
const fetchAccount = async () => {
|
||||
if (!user || !isOpen) return;
|
||||
try {
|
||||
const response = await getUserAccount();
|
||||
if (response.success) {
|
||||
setUserAccount(response.data);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('获取账户失败:', error);
|
||||
}
|
||||
};
|
||||
fetchAccount();
|
||||
}, [user, isOpen]);
|
||||
|
||||
// 重置状态
|
||||
useEffect(() => {
|
||||
if (isOpen) {
|
||||
setShares(1);
|
||||
}
|
||||
}, [isOpen]);
|
||||
|
||||
if (!comment || !userAccount) return null;
|
||||
|
||||
// 计算投资成本(后端算法:基础价格100 + 已有投资额/10)
|
||||
const basePrice = 100;
|
||||
const priceIncrease = (comment.total_investment || 0) / 10;
|
||||
const pricePerShare = basePrice + priceIncrease;
|
||||
const totalCost = Math.round(pricePerShare * shares);
|
||||
|
||||
// 预期收益(如果预测正确,获得1.5倍回报)
|
||||
const expectedReturn = Math.round(totalCost * 1.5);
|
||||
const expectedProfit = expectedReturn - totalCost;
|
||||
|
||||
// 检查是否可以投资
|
||||
const canInvest = () => {
|
||||
// 检查余额
|
||||
if (userAccount.balance < totalCost) {
|
||||
return { ok: false, reason: '积分不足' };
|
||||
}
|
||||
|
||||
// 不能投资自己的评论
|
||||
if (comment.user?.id === user?.id) {
|
||||
return { ok: false, reason: '不能投资自己的评论' };
|
||||
}
|
||||
|
||||
// 检查是否已结算
|
||||
if (comment.is_verified) {
|
||||
return { ok: false, reason: '该评论已结算' };
|
||||
}
|
||||
|
||||
return { ok: true };
|
||||
};
|
||||
|
||||
const investCheck = canInvest();
|
||||
|
||||
// 处理投资
|
||||
const handleInvest = async () => {
|
||||
try {
|
||||
setIsSubmitting(true);
|
||||
|
||||
const response = await investComment(comment.id, shares);
|
||||
|
||||
if (response.success) {
|
||||
toast({
|
||||
title: '投资成功!',
|
||||
description: `投资${totalCost}积分,剩余 ${response.data.new_balance} 积分`,
|
||||
status: 'success',
|
||||
duration: 3000,
|
||||
});
|
||||
|
||||
// 刷新账户数据
|
||||
const accountResponse = await getUserAccount();
|
||||
if (accountResponse.success) {
|
||||
setUserAccount(accountResponse.data);
|
||||
}
|
||||
|
||||
// 通知父组件刷新
|
||||
if (onInvestSuccess) {
|
||||
onInvestSuccess(response.data);
|
||||
}
|
||||
|
||||
onClose();
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('投资失败:', error);
|
||||
toast({
|
||||
title: '投资失败',
|
||||
description: error.response?.data?.error || error.message,
|
||||
status: 'error',
|
||||
duration: 3000,
|
||||
});
|
||||
} finally {
|
||||
setIsSubmitting(false);
|
||||
}
|
||||
};
|
||||
|
||||
// 判断是否是庄主
|
||||
const isYesLord = comment.user?.id === topic?.yes_lord_id;
|
||||
const isNoLord = comment.user?.id === topic?.no_lord_id;
|
||||
const isLord = isYesLord || isNoLord;
|
||||
|
||||
return (
|
||||
<Modal isOpen={isOpen} onClose={onClose} size={{ base: "full", sm: "lg" }} isCentered>
|
||||
<ModalOverlay backdropFilter="blur(4px)" />
|
||||
<ModalContent
|
||||
bg={forumColors.background.card}
|
||||
borderRadius={{ base: "0", sm: "xl" }}
|
||||
border="1px solid"
|
||||
borderColor={forumColors.border.default}
|
||||
maxH={{ base: "100vh", sm: "90vh" }}
|
||||
m={{ base: "0", sm: "4" }}
|
||||
>
|
||||
<ModalHeader
|
||||
bg={forumColors.gradients.goldSubtle}
|
||||
borderTopRadius={{ base: "0", sm: "xl" }}
|
||||
borderBottom="1px solid"
|
||||
borderColor={forumColors.border.default}
|
||||
py={{ base: "4", sm: "3" }}
|
||||
>
|
||||
<HStack spacing="2">
|
||||
<Icon
|
||||
as={Lightbulb}
|
||||
boxSize={{ base: "18px", sm: "20px" }}
|
||||
color={forumColors.primary[500]}
|
||||
/>
|
||||
<Text color={forumColors.text.primary} fontSize={{ base: "md", sm: "lg" }}>
|
||||
投资观点
|
||||
</Text>
|
||||
</HStack>
|
||||
</ModalHeader>
|
||||
<ModalCloseButton color={forumColors.text.primary} />
|
||||
|
||||
<ModalBody py={{ base: "4", sm: "6" }} px={{ base: "4", sm: "6" }}>
|
||||
<VStack spacing="5" align="stretch">
|
||||
{/* 评论作者信息 */}
|
||||
<Box
|
||||
bg={forumColors.background.hover}
|
||||
borderRadius="lg"
|
||||
p={{ base: "3", sm: "4" }}
|
||||
border="1px solid"
|
||||
borderColor={forumColors.border.default}
|
||||
>
|
||||
<HStack spacing="3" mb="2">
|
||||
<Avatar
|
||||
size="sm"
|
||||
name={comment.user?.nickname || comment.user?.username}
|
||||
src={comment.user?.avatar_url}
|
||||
bg={forumColors.gradients.goldPrimary}
|
||||
/>
|
||||
<VStack align="start" spacing="0" flex="1">
|
||||
<HStack spacing="2">
|
||||
<Text fontSize="sm" fontWeight="600" color={forumColors.text.primary}>
|
||||
{comment.user?.nickname || comment.user?.username || '匿名用户'}
|
||||
</Text>
|
||||
{isLord && (
|
||||
<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>
|
||||
)}
|
||||
</HStack>
|
||||
</VStack>
|
||||
</HStack>
|
||||
|
||||
{/* 评论内容 */}
|
||||
<Text fontSize={{ base: "sm", sm: "md" }} color={forumColors.text.secondary} lineHeight="1.6">
|
||||
{comment.content}
|
||||
</Text>
|
||||
</Box>
|
||||
|
||||
{/* 当前投资统计 */}
|
||||
<Box
|
||||
bg={forumColors.gradients.goldSubtle}
|
||||
border="1px solid"
|
||||
borderColor={forumColors.border.gold}
|
||||
borderRadius="lg"
|
||||
p={{ base: "3", sm: "4" }}
|
||||
>
|
||||
<VStack spacing="2" align="stretch">
|
||||
<Flex justify="space-between" fontSize={{ base: "xs", sm: "sm" }}>
|
||||
<Text color={forumColors.text.secondary}>已有投资人数</Text>
|
||||
<Text fontWeight="600" color={forumColors.text.primary}>
|
||||
{comment.investor_count || 0} 人
|
||||
</Text>
|
||||
</Flex>
|
||||
<Flex justify="space-between" fontSize={{ base: "xs", sm: "sm" }}>
|
||||
<Text color={forumColors.text.secondary}>总投资额</Text>
|
||||
<Text fontWeight="600" color={forumColors.text.primary}>
|
||||
{comment.total_investment || 0} 积分
|
||||
</Text>
|
||||
</Flex>
|
||||
<Flex justify="space-between" fontSize={{ base: "xs", sm: "sm" }}>
|
||||
<Text color={forumColors.text.secondary}>当前价格</Text>
|
||||
<Text fontWeight="600" color={forumColors.primary[500]}>
|
||||
{Math.round(pricePerShare)} 积分/份
|
||||
</Text>
|
||||
</Flex>
|
||||
</VStack>
|
||||
</Box>
|
||||
|
||||
{/* 投资份额 */}
|
||||
<Box>
|
||||
<Flex justify="space-between" mb={{ base: "2", sm: "3" }}>
|
||||
<Text fontSize={{ base: "sm", sm: "sm" }} fontWeight="600" color={forumColors.text.primary}>
|
||||
投资份额
|
||||
</Text>
|
||||
<Text fontSize={{ base: "sm", sm: "sm" }} color={forumColors.text.secondary}>
|
||||
{shares} 份
|
||||
</Text>
|
||||
</Flex>
|
||||
|
||||
<Slider
|
||||
value={shares}
|
||||
onChange={setShares}
|
||||
min={1}
|
||||
max={10}
|
||||
step={1}
|
||||
focusThumbOnChange={false}
|
||||
>
|
||||
<SliderTrack bg={forumColors.background.hover} h={{ base: "2", sm: "1.5" }}>
|
||||
<SliderFilledTrack bg={forumColors.gradients.goldPrimary} />
|
||||
</SliderTrack>
|
||||
<SliderThumb boxSize={{ base: "7", sm: "6" }} bg={forumColors.primary[500]}>
|
||||
<Box as={Icon} as={DollarSign} boxSize={{ base: "14px", sm: "12px" }} color="white" />
|
||||
</SliderThumb>
|
||||
</Slider>
|
||||
|
||||
<HStack justify="space-between" mt="2" fontSize={{ base: "2xs", sm: "xs" }} color={forumColors.text.tertiary}>
|
||||
<Text>1份</Text>
|
||||
<Text>10份 (最大)</Text>
|
||||
</HStack>
|
||||
</Box>
|
||||
|
||||
<Divider borderColor={forumColors.border.default} />
|
||||
|
||||
{/* 费用明细 */}
|
||||
<Box
|
||||
bg={forumColors.background.hover}
|
||||
border="1px solid"
|
||||
borderColor={forumColors.border.default}
|
||||
borderRadius="lg"
|
||||
p={{ base: "3", sm: "4" }}
|
||||
>
|
||||
<VStack spacing={{ base: "1.5", sm: "2" }} align="stretch">
|
||||
<Flex justify="space-between" fontSize={{ base: "xs", sm: "sm" }}>
|
||||
<Text color={forumColors.text.secondary}>单价</Text>
|
||||
<Text fontWeight="600" color={forumColors.text.primary}>
|
||||
{Math.round(pricePerShare)} 积分/份
|
||||
</Text>
|
||||
</Flex>
|
||||
|
||||
<Flex justify="space-between" fontSize={{ base: "xs", sm: "sm" }}>
|
||||
<Text color={forumColors.text.secondary}>份额</Text>
|
||||
<Text fontWeight="600" color={forumColors.text.primary}>
|
||||
{shares} 份
|
||||
</Text>
|
||||
</Flex>
|
||||
|
||||
<Box borderTop="1px solid" borderColor={forumColors.border.default} pt={{ base: "1.5", sm: "2" }} mt="1">
|
||||
<Flex justify="space-between">
|
||||
<Text fontWeight="bold" color={forumColors.text.primary} fontSize={{ base: "sm", sm: "md" }}>
|
||||
投资总额
|
||||
</Text>
|
||||
<HStack spacing="1">
|
||||
<Icon as={DollarSign} boxSize={{ base: "16px", sm: "20px" }} color={forumColors.primary[500]} />
|
||||
<Text fontSize={{ base: "xl", sm: "2xl" }} fontWeight="bold" color={forumColors.primary[500]}>
|
||||
{totalCost}
|
||||
</Text>
|
||||
<Text fontSize={{ base: "xs", sm: "sm" }} color={forumColors.text.secondary}>
|
||||
积分
|
||||
</Text>
|
||||
</HStack>
|
||||
</Flex>
|
||||
</Box>
|
||||
|
||||
{/* 预期收益 */}
|
||||
<Box borderTop="1px solid" borderColor={forumColors.border.default} pt={{ base: "1.5", sm: "2" }}>
|
||||
<Flex justify="space-between" fontSize={{ base: "xs", sm: "sm" }}>
|
||||
<Text color="green.600" fontWeight="600">
|
||||
<Icon as={TrendingUp} size={14} display="inline" mr="1" />
|
||||
预测正确收益(1.5倍)
|
||||
</Text>
|
||||
<Text fontWeight="600" color="green.600">
|
||||
+{expectedProfit} 积分
|
||||
</Text>
|
||||
</Flex>
|
||||
<Text fontSize="2xs" color={forumColors.text.muted} mt="1">
|
||||
预测正确将获得 {expectedReturn} 积分(含本金)
|
||||
</Text>
|
||||
</Box>
|
||||
</VStack>
|
||||
</Box>
|
||||
|
||||
{/* 余额提示 */}
|
||||
<Box
|
||||
bg={forumColors.background.hover}
|
||||
borderRadius="lg"
|
||||
p={{ base: "2.5", sm: "3" }}
|
||||
>
|
||||
<Flex justify="space-between" fontSize={{ base: "xs", sm: "sm" }}>
|
||||
<Text color={forumColors.text.secondary}>你的余额:</Text>
|
||||
<Text fontWeight="600" color={forumColors.text.primary}>
|
||||
{userAccount.balance} 积分
|
||||
</Text>
|
||||
</Flex>
|
||||
<Flex justify="space-between" fontSize={{ base: "xs", sm: "sm" }} mt="1">
|
||||
<Text color={forumColors.text.secondary}>投资后:</Text>
|
||||
<Text
|
||||
fontWeight="600"
|
||||
color={
|
||||
userAccount.balance >= totalCost
|
||||
? forumColors.success[500]
|
||||
: forumColors.error[500]
|
||||
}
|
||||
>
|
||||
{userAccount.balance - totalCost} 积分
|
||||
</Text>
|
||||
</Flex>
|
||||
</Box>
|
||||
|
||||
{/* 警告提示 */}
|
||||
{!investCheck.ok && (
|
||||
<Box
|
||||
bg="red.50"
|
||||
border="1px solid"
|
||||
borderColor="red.200"
|
||||
borderRadius="lg"
|
||||
p={{ base: "2.5", sm: "3" }}
|
||||
>
|
||||
<HStack spacing="2">
|
||||
<Icon as={AlertCircle} boxSize={{ base: "14px", sm: "16px" }} color="red.500" />
|
||||
<Text fontSize={{ base: "xs", sm: "sm" }} color="red.600" fontWeight="600">
|
||||
{investCheck.reason}
|
||||
</Text>
|
||||
</HStack>
|
||||
</Box>
|
||||
)}
|
||||
|
||||
{/* 风险提示 */}
|
||||
<Box
|
||||
bg="orange.50"
|
||||
border="1px solid"
|
||||
borderColor="orange.200"
|
||||
borderRadius="lg"
|
||||
p={{ base: "2.5", sm: "3" }}
|
||||
>
|
||||
<HStack spacing="2" mb="1">
|
||||
<Icon as={AlertCircle} boxSize="14px" color="orange.500" />
|
||||
<Text fontSize="xs" color="orange.700" fontWeight="600">
|
||||
投资风险提示
|
||||
</Text>
|
||||
</HStack>
|
||||
<Text fontSize="2xs" color="orange.600" lineHeight="1.5">
|
||||
观点预测存在不确定性,预测错误将损失全部投资。请谨慎评估后再投资。
|
||||
</Text>
|
||||
</Box>
|
||||
</VStack>
|
||||
</ModalBody>
|
||||
|
||||
<ModalFooter
|
||||
borderTop="1px solid"
|
||||
borderColor={forumColors.border.default}
|
||||
py={{ base: "3", sm: "4" }}
|
||||
px={{ base: "4", sm: "6" }}
|
||||
>
|
||||
<HStack spacing={{ base: "2", sm: "3" }} w="full" justify="flex-end">
|
||||
<Button
|
||||
variant="ghost"
|
||||
onClick={onClose}
|
||||
color={forumColors.text.secondary}
|
||||
_hover={{ bg: forumColors.background.hover }}
|
||||
h={{ base: "10", sm: "auto" }}
|
||||
fontSize={{ base: "sm", sm: "md" }}
|
||||
>
|
||||
取消
|
||||
</Button>
|
||||
<Button
|
||||
bg={forumColors.gradients.goldPrimary}
|
||||
color="white"
|
||||
fontWeight="bold"
|
||||
onClick={handleInvest}
|
||||
isLoading={isSubmitting}
|
||||
loadingText="投资中..."
|
||||
isDisabled={!investCheck.ok}
|
||||
_hover={{
|
||||
opacity: 0.9,
|
||||
transform: 'translateY(-2px)',
|
||||
}}
|
||||
_active={{ transform: 'translateY(0)' }}
|
||||
h={{ base: "11", sm: "auto" }}
|
||||
fontSize={{ base: "sm", sm: "md" }}
|
||||
px={{ base: "6", sm: "4" }}
|
||||
>
|
||||
投资 {shares} 份
|
||||
</Button>
|
||||
</HStack>
|
||||
</ModalFooter>
|
||||
</ModalContent>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
||||
export default CommentInvestModal;
|
||||
475
src/views/ValueForum/components/PredictionCommentSection.js
Normal file
475
src/views/ValueForum/components/PredictionCommentSection.js
Normal file
@@ -0,0 +1,475 @@
|
||||
/**
|
||||
* 预测话题评论区组件
|
||||
* 支持发布评论、嵌套回复、点赞、庄主标识、观点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;
|
||||
Reference in New Issue
Block a user