事件中心的涨停原因里面和事件相关
This commit is contained in:
@@ -3,7 +3,7 @@
|
||||
* 展示预测市场的完整信息、交易、评论等
|
||||
*/
|
||||
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import React, { useState, useEffect, useMemo } from 'react';
|
||||
import {
|
||||
Box,
|
||||
Heading,
|
||||
@@ -20,17 +20,21 @@ import {
|
||||
useDisclosure,
|
||||
useToast,
|
||||
SimpleGrid,
|
||||
Alert,
|
||||
AlertIcon,
|
||||
} from '@chakra-ui/react';
|
||||
import {
|
||||
TrendingUp,
|
||||
TrendingDown,
|
||||
Crown,
|
||||
Users,
|
||||
Clock,
|
||||
Coins,
|
||||
ShoppingCart,
|
||||
ArrowLeftRight,
|
||||
CheckCircle2,
|
||||
Lock,
|
||||
Gavel,
|
||||
Trophy,
|
||||
} from 'lucide-react';
|
||||
import { useParams, useNavigate } from 'react-router-dom';
|
||||
import { motion } from 'framer-motion';
|
||||
@@ -41,6 +45,8 @@ import { LAYOUT_SIZE } from '@/layouts/config/layoutConfig';
|
||||
import TradeModal from './components/TradeModal';
|
||||
import PredictionCommentSection from './components/PredictionCommentSection';
|
||||
import CommentInvestModal from './components/CommentInvestModal';
|
||||
import SettleTopicModal from './components/SettleTopicModal';
|
||||
import CountdownTimer, { useCountdown } from './components/CountdownTimer';
|
||||
|
||||
const MotionBox = motion(Box);
|
||||
|
||||
@@ -70,6 +76,12 @@ const PredictionTopicDetail = () => {
|
||||
onClose: onInvestModalClose,
|
||||
} = useDisclosure();
|
||||
|
||||
const {
|
||||
isOpen: isSettleModalOpen,
|
||||
onOpen: onSettleModalOpen,
|
||||
onClose: onSettleModalClose,
|
||||
} = useDisclosure();
|
||||
|
||||
// 加载话题数据
|
||||
useEffect(() => {
|
||||
const loadTopic = async () => {
|
||||
@@ -184,6 +196,47 @@ const PredictionTopicDetail = () => {
|
||||
}
|
||||
};
|
||||
|
||||
// 使用倒计时 Hook
|
||||
const timeLeft = useCountdown(topic?.deadline);
|
||||
|
||||
// 计算实际状态(基于截止时间自动判断)
|
||||
const effectiveStatus = useMemo(() => {
|
||||
if (!topic) return 'active';
|
||||
// 如果已经是结算状态,保持不变
|
||||
if (topic.status === 'settled') return 'settled';
|
||||
// 如果已过截止时间,自动变为已截止
|
||||
if (timeLeft.expired && topic.status === 'active') return 'trading_closed';
|
||||
return topic.status;
|
||||
}, [topic?.status, timeLeft.expired]);
|
||||
|
||||
// 判断当前用户是否是作者
|
||||
const isAuthor = useMemo(() => {
|
||||
if (!user || !topic) return false;
|
||||
return user.id === topic.author_id || user.username === topic.author_name;
|
||||
}, [user, topic]);
|
||||
|
||||
// 判断是否可以结算(作者 + 已截止 + 未结算)
|
||||
const canSettle = useMemo(() => {
|
||||
return isAuthor && effectiveStatus === 'trading_closed';
|
||||
}, [isAuthor, effectiveStatus]);
|
||||
|
||||
// 判断是否可以交易
|
||||
const canTrade = useMemo(() => {
|
||||
return effectiveStatus === 'active' && !isAuthor;
|
||||
}, [effectiveStatus, isAuthor]);
|
||||
|
||||
// 结算成功回调
|
||||
const handleSettleSuccess = async () => {
|
||||
try {
|
||||
const topicResponse = await getTopicDetail(topicId);
|
||||
if (topicResponse.success) {
|
||||
setTopic(topicResponse.data);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('刷新数据失败:', error);
|
||||
}
|
||||
};
|
||||
|
||||
if (!topic) {
|
||||
return null;
|
||||
}
|
||||
@@ -207,20 +260,19 @@ const PredictionTopicDetail = () => {
|
||||
const yesPercent = totalShares > 0 ? (yesData.total_shares / totalShares) * 100 : 50;
|
||||
const noPercent = totalShares > 0 ? (noData.total_shares / totalShares) * 100 : 50;
|
||||
|
||||
// 格式化时间
|
||||
const formatTime = (dateString) => {
|
||||
const date = new Date(dateString);
|
||||
const now = new Date();
|
||||
const diff = date - now;
|
||||
|
||||
const days = Math.floor(diff / 86400000);
|
||||
const hours = Math.floor(diff / 3600000);
|
||||
|
||||
if (days > 0) return `${days}天后`;
|
||||
if (hours > 0) return `${hours}小时后`;
|
||||
return '即将截止';
|
||||
// 获取结算结果显示
|
||||
const getResultDisplay = () => {
|
||||
if (topic.status !== 'settled' || !topic.result) return null;
|
||||
const resultMap = {
|
||||
yes: { label: '看涨方获胜', color: 'green', icon: TrendingUp },
|
||||
no: { label: '看跌方获胜', color: 'red', icon: TrendingDown },
|
||||
draw: { label: '平局', color: 'gray', icon: null },
|
||||
};
|
||||
return resultMap[topic.result];
|
||||
};
|
||||
|
||||
const resultDisplay = getResultDisplay();
|
||||
|
||||
return (
|
||||
<Box minH="100vh" bg={forumColors.background.main} pt={LAYOUT_SIZE.navbarHeight} pb={{ base: "6", md: "20" }}>
|
||||
{/* 头部:返回按钮 */}
|
||||
@@ -258,26 +310,73 @@ const PredictionTopicDetail = () => {
|
||||
borderBottom="1px solid"
|
||||
borderColor={forumColors.border.default}
|
||||
>
|
||||
<HStack justify="space-between">
|
||||
<Badge
|
||||
bg={forumColors.primary[500]}
|
||||
color="white"
|
||||
px="3"
|
||||
py="1"
|
||||
borderRadius="full"
|
||||
fontSize="sm"
|
||||
>
|
||||
{topic.category}
|
||||
</Badge>
|
||||
<HStack justify="space-between" flexWrap="wrap" gap="2">
|
||||
<HStack spacing="2">
|
||||
<Badge
|
||||
bg={forumColors.primary[500]}
|
||||
color="white"
|
||||
px="3"
|
||||
py="1"
|
||||
borderRadius="full"
|
||||
fontSize="sm"
|
||||
>
|
||||
{topic.category}
|
||||
</Badge>
|
||||
|
||||
<HStack spacing="3">
|
||||
<Icon as={Clock} boxSize="16px" color={forumColors.text.secondary} />
|
||||
<Text fontSize="sm" color={forumColors.text.secondary}>
|
||||
{formatTime(topic.deadline)} 截止
|
||||
</Text>
|
||||
{/* 状态标识 */}
|
||||
<Badge
|
||||
bg={
|
||||
effectiveStatus === 'active' ? forumColors.success[500] :
|
||||
effectiveStatus === 'trading_closed' ? forumColors.warning[500] :
|
||||
forumColors.text.secondary
|
||||
}
|
||||
color="white"
|
||||
px="3"
|
||||
py="1"
|
||||
borderRadius="full"
|
||||
fontSize="sm"
|
||||
display="flex"
|
||||
alignItems="center"
|
||||
gap="1"
|
||||
>
|
||||
<Icon
|
||||
as={
|
||||
effectiveStatus === 'active' ? ShoppingCart :
|
||||
effectiveStatus === 'trading_closed' ? Lock :
|
||||
CheckCircle2
|
||||
}
|
||||
boxSize="12px"
|
||||
/>
|
||||
{effectiveStatus === 'active' ? '交易中' :
|
||||
effectiveStatus === 'trading_closed' ? '等待结算' : '已结算'}
|
||||
</Badge>
|
||||
</HStack>
|
||||
|
||||
{/* 倒计时 */}
|
||||
<CountdownTimer
|
||||
deadline={topic.deadline}
|
||||
status={effectiveStatus}
|
||||
size="md"
|
||||
variant="badge"
|
||||
showIcon={true}
|
||||
/>
|
||||
</HStack>
|
||||
|
||||
{/* 结算结果展示 */}
|
||||
{resultDisplay && (
|
||||
<Alert
|
||||
status={topic.result === 'yes' ? 'success' : topic.result === 'no' ? 'error' : 'info'}
|
||||
mt="4"
|
||||
borderRadius="lg"
|
||||
bg={`${resultDisplay.color}.50`}
|
||||
>
|
||||
<Icon as={Trophy} boxSize="20px" color={`${resultDisplay.color}.500`} mr="2" />
|
||||
<Text fontWeight="600" color={`${resultDisplay.color}.700`}>
|
||||
结算结果:{resultDisplay.label}
|
||||
</Text>
|
||||
</Alert>
|
||||
)}
|
||||
|
||||
<Heading
|
||||
as="h1"
|
||||
fontSize={{ base: "lg", md: "2xl" }}
|
||||
@@ -538,8 +637,8 @@ const PredictionTopicDetail = () => {
|
||||
</VStack>
|
||||
</Box>
|
||||
|
||||
{/* 交易按钮 */}
|
||||
{topic.status === 'active' && (
|
||||
{/* 交易按钮 - 只在可交易时显示 */}
|
||||
{canTrade && (
|
||||
<VStack spacing="3">
|
||||
<Button
|
||||
leftIcon={<ShoppingCart size={18} />}
|
||||
@@ -581,6 +680,104 @@ const PredictionTopicDetail = () => {
|
||||
</VStack>
|
||||
)}
|
||||
|
||||
{/* 作者提示 - 不能参与自己的话题 */}
|
||||
{isAuthor && effectiveStatus === 'active' && (
|
||||
<Alert
|
||||
status="info"
|
||||
bg={forumColors.background.hover}
|
||||
borderRadius="lg"
|
||||
border="1px solid"
|
||||
borderColor={forumColors.border.default}
|
||||
>
|
||||
<AlertIcon color={forumColors.primary[500]} />
|
||||
<Text fontSize="sm" color={forumColors.text.secondary}>
|
||||
作为话题创建者,你不能参与自己发起的预测
|
||||
</Text>
|
||||
</Alert>
|
||||
)}
|
||||
|
||||
{/* 结算按钮 - 作者在截止后可见 */}
|
||||
{canSettle && (
|
||||
<VStack spacing="3">
|
||||
<Alert
|
||||
status="warning"
|
||||
bg="rgba(221, 107, 32, 0.1)"
|
||||
borderRadius="lg"
|
||||
border="1px solid"
|
||||
borderColor="orange.300"
|
||||
>
|
||||
<AlertIcon color="orange.400" />
|
||||
<VStack align="start" spacing="1" flex="1">
|
||||
<Text fontSize="sm" fontWeight="600" color={forumColors.text.primary}>
|
||||
交易已截止
|
||||
</Text>
|
||||
<Text fontSize="xs" color={forumColors.text.secondary}>
|
||||
请根据实际结果进行结算
|
||||
</Text>
|
||||
</VStack>
|
||||
</Alert>
|
||||
|
||||
<Button
|
||||
leftIcon={<Gavel size={18} />}
|
||||
bg="linear-gradient(135deg, #DD6B20 0%, #C05621 100%)"
|
||||
color="white"
|
||||
size="lg"
|
||||
w="full"
|
||||
h={{ base: "12", md: "auto" }}
|
||||
fontWeight="bold"
|
||||
fontSize={{ base: "md", md: "lg" }}
|
||||
onClick={onSettleModalOpen}
|
||||
_hover={{
|
||||
transform: 'translateY(-2px)',
|
||||
boxShadow: '0 4px 12px rgba(221, 107, 32, 0.4)',
|
||||
}}
|
||||
_active={{ transform: 'translateY(0)' }}
|
||||
>
|
||||
结算话题
|
||||
</Button>
|
||||
</VStack>
|
||||
)}
|
||||
|
||||
{/* 已结算状态提示 */}
|
||||
{effectiveStatus === 'settled' && (
|
||||
<Box
|
||||
bg={forumColors.background.hover}
|
||||
borderRadius="lg"
|
||||
p="4"
|
||||
border="1px solid"
|
||||
borderColor={forumColors.border.default}
|
||||
textAlign="center"
|
||||
>
|
||||
<Icon as={CheckCircle2} boxSize="32px" color={forumColors.success[500]} mb="2" />
|
||||
<Text fontSize="md" fontWeight="600" color={forumColors.text.primary}>
|
||||
话题已结算
|
||||
</Text>
|
||||
<Text fontSize="sm" color={forumColors.text.secondary} mt="1">
|
||||
奖池已分配给获胜方
|
||||
</Text>
|
||||
</Box>
|
||||
)}
|
||||
|
||||
{/* 等待结算状态提示(非作者) */}
|
||||
{effectiveStatus === 'trading_closed' && !isAuthor && (
|
||||
<Box
|
||||
bg="rgba(221, 107, 32, 0.1)"
|
||||
borderRadius="lg"
|
||||
p="4"
|
||||
border="1px solid"
|
||||
borderColor="orange.300"
|
||||
textAlign="center"
|
||||
>
|
||||
<Icon as={Lock} boxSize="32px" color="orange.400" mb="2" />
|
||||
<Text fontSize="md" fontWeight="600" color={forumColors.text.primary}>
|
||||
交易已截止
|
||||
</Text>
|
||||
<Text fontSize="sm" color={forumColors.text.secondary} mt="1">
|
||||
等待话题作者提交结算结果
|
||||
</Text>
|
||||
</Box>
|
||||
)}
|
||||
|
||||
{/* 用户余额 */}
|
||||
{user && userAccount && (
|
||||
<Box
|
||||
@@ -643,6 +840,14 @@ const PredictionTopicDetail = () => {
|
||||
topic={topic}
|
||||
onInvestSuccess={handleInvestSuccess}
|
||||
/>
|
||||
|
||||
{/* 结算模态框 */}
|
||||
<SettleTopicModal
|
||||
isOpen={isSettleModalOpen}
|
||||
onClose={onSettleModalClose}
|
||||
topic={topic}
|
||||
onSettleSuccess={handleSettleSuccess}
|
||||
/>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
277
src/views/ValueForum/components/CountdownTimer.js
Normal file
277
src/views/ValueForum/components/CountdownTimer.js
Normal file
@@ -0,0 +1,277 @@
|
||||
/**
|
||||
* 倒计时组件
|
||||
* 实时显示距离截止时间的倒计时,参考 Polymarket 设计
|
||||
*/
|
||||
|
||||
import React, { useState, useEffect, useMemo } from 'react';
|
||||
import { HStack, Text, Box, Icon, Badge } from '@chakra-ui/react';
|
||||
import { Clock, AlertTriangle, CheckCircle2, Lock } from 'lucide-react';
|
||||
import { forumColors } from '@theme/forumTheme';
|
||||
|
||||
/**
|
||||
* 计算时间差
|
||||
* @param {string|Date} deadline - 截止时间
|
||||
* @returns {object} 时间差对象
|
||||
*/
|
||||
const calculateTimeLeft = (deadline) => {
|
||||
const now = new Date();
|
||||
const end = new Date(deadline);
|
||||
const diff = end - now;
|
||||
|
||||
if (diff <= 0) {
|
||||
return { expired: true, days: 0, hours: 0, minutes: 0, seconds: 0, totalMs: 0 };
|
||||
}
|
||||
|
||||
return {
|
||||
expired: false,
|
||||
days: Math.floor(diff / (1000 * 60 * 60 * 24)),
|
||||
hours: Math.floor((diff % (1000 * 60 * 60 * 24)) / (1000 * 60 * 60)),
|
||||
minutes: Math.floor((diff % (1000 * 60 * 60)) / (1000 * 60)),
|
||||
seconds: Math.floor((diff % (1000 * 60)) / 1000),
|
||||
totalMs: diff,
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* 倒计时 Hook
|
||||
*/
|
||||
export const useCountdown = (deadline) => {
|
||||
const [timeLeft, setTimeLeft] = useState(() => calculateTimeLeft(deadline));
|
||||
|
||||
useEffect(() => {
|
||||
// 已过期则不需要更新
|
||||
if (timeLeft.expired) return;
|
||||
|
||||
// 根据剩余时间决定更新频率
|
||||
const interval = timeLeft.totalMs < 60000 ? 1000 : 60000; // 小于1分钟时秒级更新
|
||||
|
||||
const timer = setInterval(() => {
|
||||
const newTimeLeft = calculateTimeLeft(deadline);
|
||||
setTimeLeft(newTimeLeft);
|
||||
|
||||
if (newTimeLeft.expired) {
|
||||
clearInterval(timer);
|
||||
}
|
||||
}, interval);
|
||||
|
||||
return () => clearInterval(timer);
|
||||
}, [deadline, timeLeft.expired, timeLeft.totalMs]);
|
||||
|
||||
return timeLeft;
|
||||
};
|
||||
|
||||
/**
|
||||
* 格式化倒计时显示
|
||||
*/
|
||||
const formatCountdown = (timeLeft) => {
|
||||
if (timeLeft.expired) {
|
||||
return '已截止';
|
||||
}
|
||||
|
||||
if (timeLeft.days > 0) {
|
||||
return `${timeLeft.days}天 ${timeLeft.hours}小时`;
|
||||
}
|
||||
|
||||
if (timeLeft.hours > 0) {
|
||||
return `${timeLeft.hours}小时 ${timeLeft.minutes}分`;
|
||||
}
|
||||
|
||||
if (timeLeft.minutes > 0) {
|
||||
return `${timeLeft.minutes}分 ${timeLeft.seconds}秒`;
|
||||
}
|
||||
|
||||
return `${timeLeft.seconds}秒`;
|
||||
};
|
||||
|
||||
/**
|
||||
* 获取倒计时状态和颜色
|
||||
*/
|
||||
const getCountdownStatus = (timeLeft) => {
|
||||
if (timeLeft.expired) {
|
||||
return {
|
||||
status: 'expired',
|
||||
color: forumColors.text.secondary,
|
||||
bgColor: forumColors.background.hover,
|
||||
icon: Lock,
|
||||
label: '已截止',
|
||||
};
|
||||
}
|
||||
|
||||
// 小于1小时
|
||||
if (timeLeft.days === 0 && timeLeft.hours === 0) {
|
||||
return {
|
||||
status: 'critical',
|
||||
color: '#E53E3E',
|
||||
bgColor: 'rgba(229, 62, 62, 0.1)',
|
||||
icon: AlertTriangle,
|
||||
label: '即将截止',
|
||||
};
|
||||
}
|
||||
|
||||
// 小于24小时
|
||||
if (timeLeft.days === 0) {
|
||||
return {
|
||||
status: 'warning',
|
||||
color: '#DD6B20',
|
||||
bgColor: 'rgba(221, 107, 32, 0.1)',
|
||||
icon: Clock,
|
||||
label: '今日截止',
|
||||
};
|
||||
}
|
||||
|
||||
// 小于3天
|
||||
if (timeLeft.days < 3) {
|
||||
return {
|
||||
status: 'soon',
|
||||
color: '#D69E2E',
|
||||
bgColor: 'rgba(214, 158, 46, 0.1)',
|
||||
icon: Clock,
|
||||
label: '即将截止',
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
status: 'normal',
|
||||
color: forumColors.text.secondary,
|
||||
bgColor: 'transparent',
|
||||
icon: Clock,
|
||||
label: '交易中',
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* 倒计时显示组件
|
||||
*/
|
||||
const CountdownTimer = ({
|
||||
deadline,
|
||||
status = 'active', // active, trading_closed, settled
|
||||
size = 'md', // sm, md, lg
|
||||
showIcon = true,
|
||||
showLabel = false,
|
||||
variant = 'inline', // inline, badge, card
|
||||
onExpire,
|
||||
}) => {
|
||||
const timeLeft = useCountdown(deadline);
|
||||
const countdownStatus = getCountdownStatus(timeLeft);
|
||||
|
||||
// 当倒计时结束时触发回调
|
||||
useEffect(() => {
|
||||
if (timeLeft.expired && onExpire) {
|
||||
onExpire();
|
||||
}
|
||||
}, [timeLeft.expired, onExpire]);
|
||||
|
||||
// 如果话题已结算,显示结算状态
|
||||
if (status === 'settled') {
|
||||
return (
|
||||
<HStack spacing="1">
|
||||
<Icon as={CheckCircle2} boxSize={size === 'sm' ? '14px' : '16px'} color={forumColors.success[500]} />
|
||||
<Text fontSize={size} color={forumColors.success[500]} fontWeight="600">
|
||||
已结算
|
||||
</Text>
|
||||
</HStack>
|
||||
);
|
||||
}
|
||||
|
||||
// 如果话题已截止(手动设置的状态)
|
||||
if (status === 'trading_closed') {
|
||||
return (
|
||||
<HStack spacing="1">
|
||||
<Icon as={Lock} boxSize={size === 'sm' ? '14px' : '16px'} color={forumColors.warning[500]} />
|
||||
<Text fontSize={size} color={forumColors.warning[500]} fontWeight="600">
|
||||
等待结算
|
||||
</Text>
|
||||
</HStack>
|
||||
);
|
||||
}
|
||||
|
||||
const sizeMap = {
|
||||
sm: { fontSize: 'xs', iconSize: '12px', px: 2, py: 1 },
|
||||
md: { fontSize: 'sm', iconSize: '14px', px: 3, py: 1 },
|
||||
lg: { fontSize: 'md', iconSize: '16px', px: 4, py: 2 },
|
||||
};
|
||||
|
||||
const sizeConfig = sizeMap[size];
|
||||
|
||||
// Badge 变体
|
||||
if (variant === 'badge') {
|
||||
return (
|
||||
<Badge
|
||||
display="flex"
|
||||
alignItems="center"
|
||||
gap="1"
|
||||
bg={countdownStatus.bgColor}
|
||||
color={countdownStatus.color}
|
||||
px={sizeConfig.px}
|
||||
py={sizeConfig.py}
|
||||
borderRadius="full"
|
||||
fontSize={sizeConfig.fontSize}
|
||||
fontWeight="600"
|
||||
border="1px solid"
|
||||
borderColor={countdownStatus.color}
|
||||
>
|
||||
{showIcon && <Icon as={countdownStatus.icon} boxSize={sizeConfig.iconSize} />}
|
||||
{showLabel && <Text>{countdownStatus.label}</Text>}
|
||||
<Text>{formatCountdown(timeLeft)}</Text>
|
||||
</Badge>
|
||||
);
|
||||
}
|
||||
|
||||
// Card 变体 - 更详细的显示
|
||||
if (variant === 'card') {
|
||||
return (
|
||||
<Box
|
||||
bg={countdownStatus.bgColor}
|
||||
border="1px solid"
|
||||
borderColor={countdownStatus.color}
|
||||
borderRadius="lg"
|
||||
p="3"
|
||||
>
|
||||
<HStack justify="space-between" align="center">
|
||||
<HStack spacing="2">
|
||||
<Icon as={countdownStatus.icon} boxSize="18px" color={countdownStatus.color} />
|
||||
<Text fontSize="sm" color={forumColors.text.secondary}>
|
||||
{timeLeft.expired ? '已截止' : '距离截止'}
|
||||
</Text>
|
||||
</HStack>
|
||||
<Text fontSize="lg" fontWeight="bold" color={countdownStatus.color}>
|
||||
{formatCountdown(timeLeft)}
|
||||
</Text>
|
||||
</HStack>
|
||||
|
||||
{!timeLeft.expired && timeLeft.days === 0 && (
|
||||
<Text fontSize="xs" color={countdownStatus.color} mt="2">
|
||||
⚠️ 交易即将关闭,请尽快完成交易
|
||||
</Text>
|
||||
)}
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
// 默认 inline 变体
|
||||
return (
|
||||
<HStack spacing="1">
|
||||
{showIcon && (
|
||||
<Icon
|
||||
as={countdownStatus.icon}
|
||||
boxSize={sizeConfig.iconSize}
|
||||
color={countdownStatus.color}
|
||||
/>
|
||||
)}
|
||||
{showLabel && (
|
||||
<Text fontSize={sizeConfig.fontSize} color={countdownStatus.color}>
|
||||
{countdownStatus.label}:
|
||||
</Text>
|
||||
)}
|
||||
<Text
|
||||
fontSize={sizeConfig.fontSize}
|
||||
color={countdownStatus.color}
|
||||
fontWeight={timeLeft.days < 1 ? '600' : '400'}
|
||||
>
|
||||
{formatCountdown(timeLeft)}
|
||||
</Text>
|
||||
</HStack>
|
||||
);
|
||||
};
|
||||
|
||||
export default CountdownTimer;
|
||||
@@ -3,7 +3,7 @@
|
||||
* 用户可以发起新的预测市场话题
|
||||
*/
|
||||
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import React, { useState, useEffect, useMemo } from 'react';
|
||||
import {
|
||||
Modal,
|
||||
ModalOverlay,
|
||||
@@ -26,8 +26,15 @@ import {
|
||||
Alert,
|
||||
AlertIcon,
|
||||
useToast,
|
||||
RadioGroup,
|
||||
Radio,
|
||||
Tabs,
|
||||
TabList,
|
||||
Tab,
|
||||
TabPanels,
|
||||
TabPanel,
|
||||
} from '@chakra-ui/react';
|
||||
import { Zap, Calendar, DollarSign } from 'lucide-react';
|
||||
import { Zap, Calendar, DollarSign, Clock, AlertTriangle } from 'lucide-react';
|
||||
import { forumColors } from '@theme/forumTheme';
|
||||
import { createTopic, getUserAccount } from '@services/predictionMarketService.api';
|
||||
import { CREDIT_CONFIG } from '@services/creditSystemService';
|
||||
@@ -43,7 +50,10 @@ const CreatePredictionModal = ({ isOpen, onClose, onTopicCreated }) => {
|
||||
title: '',
|
||||
description: '',
|
||||
category: 'stock',
|
||||
deadline_type: 'preset', // 'preset' 或 'custom'
|
||||
deadline_days: 7,
|
||||
custom_date: '',
|
||||
custom_time: '15:00', // 默认下午3点(A股收盘时间)
|
||||
});
|
||||
|
||||
const [isSubmitting, setIsSubmitting] = useState(false);
|
||||
@@ -70,6 +80,63 @@ const CreatePredictionModal = ({ isOpen, onClose, onTopicCreated }) => {
|
||||
setFormData((prev) => ({ ...prev, [field]: value }));
|
||||
};
|
||||
|
||||
// 计算截止时间
|
||||
const calculatedDeadline = useMemo(() => {
|
||||
if (formData.deadline_type === 'preset') {
|
||||
const deadline = new Date();
|
||||
deadline.setDate(deadline.getDate() + parseInt(formData.deadline_days));
|
||||
deadline.setHours(15, 0, 0, 0); // 默认下午3点
|
||||
return deadline;
|
||||
} else {
|
||||
if (!formData.custom_date) return null;
|
||||
const [hours, minutes] = formData.custom_time.split(':');
|
||||
const deadline = new Date(formData.custom_date);
|
||||
deadline.setHours(parseInt(hours), parseInt(minutes), 0, 0);
|
||||
return deadline;
|
||||
}
|
||||
}, [formData.deadline_type, formData.deadline_days, formData.custom_date, formData.custom_time]);
|
||||
|
||||
// 格式化截止时间显示
|
||||
const formatDeadlineDisplay = (date) => {
|
||||
if (!date) return '请选择日期';
|
||||
const now = new Date();
|
||||
const diff = date - now;
|
||||
const days = Math.floor(diff / (1000 * 60 * 60 * 24));
|
||||
const hours = Math.floor((diff % (1000 * 60 * 60 * 24)) / (1000 * 60 * 60));
|
||||
|
||||
const dateStr = date.toLocaleDateString('zh-CN', {
|
||||
year: 'numeric',
|
||||
month: 'long',
|
||||
day: 'numeric',
|
||||
weekday: 'short',
|
||||
});
|
||||
const timeStr = date.toLocaleTimeString('zh-CN', {
|
||||
hour: '2-digit',
|
||||
minute: '2-digit',
|
||||
});
|
||||
|
||||
if (days > 0) {
|
||||
return `${dateStr} ${timeStr}(${days}天${hours}小时后)`;
|
||||
} else if (hours > 0) {
|
||||
return `${dateStr} ${timeStr}(${hours}小时后)`;
|
||||
}
|
||||
return `${dateStr} ${timeStr}`;
|
||||
};
|
||||
|
||||
// 获取最小日期(明天)
|
||||
const getMinDate = () => {
|
||||
const tomorrow = new Date();
|
||||
tomorrow.setDate(tomorrow.getDate() + 1);
|
||||
return tomorrow.toISOString().split('T')[0];
|
||||
};
|
||||
|
||||
// 获取最大日期(90天后)
|
||||
const getMaxDate = () => {
|
||||
const maxDate = new Date();
|
||||
maxDate.setDate(maxDate.getDate() + 90);
|
||||
return maxDate.toISOString().split('T')[0];
|
||||
};
|
||||
|
||||
// 提交表单
|
||||
const handleSubmit = async () => {
|
||||
try {
|
||||
@@ -105,9 +172,27 @@ const CreatePredictionModal = ({ isOpen, onClose, onTopicCreated }) => {
|
||||
return;
|
||||
}
|
||||
|
||||
// 计算截止时间
|
||||
const deadline = new Date();
|
||||
deadline.setDate(deadline.getDate() + parseInt(formData.deadline_days));
|
||||
// 验证截止时间
|
||||
if (!calculatedDeadline) {
|
||||
toast({
|
||||
title: '请设置截止时间',
|
||||
status: 'warning',
|
||||
duration: 3000,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// 检查截止时间是否在未来
|
||||
if (calculatedDeadline <= new Date()) {
|
||||
toast({
|
||||
title: '截止时间必须在未来',
|
||||
status: 'warning',
|
||||
duration: 3000,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const deadline = calculatedDeadline;
|
||||
|
||||
// 调用 API 创建话题
|
||||
const response = await createTopic({
|
||||
@@ -130,7 +215,10 @@ const CreatePredictionModal = ({ isOpen, onClose, onTopicCreated }) => {
|
||||
title: '',
|
||||
description: '',
|
||||
category: 'stock',
|
||||
deadline_type: 'preset',
|
||||
deadline_days: 7,
|
||||
custom_date: '',
|
||||
custom_time: '15:00',
|
||||
});
|
||||
|
||||
// 通知父组件
|
||||
@@ -288,27 +376,175 @@ const CreatePredictionModal = ({ isOpen, onClose, onTopicCreated }) => {
|
||||
<Text>交易截止时间</Text>
|
||||
</HStack>
|
||||
</FormLabel>
|
||||
<Select
|
||||
value={formData.deadline_days}
|
||||
onChange={(e) => handleChange('deadline_days', e.target.value)}
|
||||
|
||||
<Tabs
|
||||
variant="soft-rounded"
|
||||
colorScheme="yellow"
|
||||
size="sm"
|
||||
index={formData.deadline_type === 'preset' ? 0 : 1}
|
||||
onChange={(index) => handleChange('deadline_type', index === 0 ? 'preset' : 'custom')}
|
||||
mb="3"
|
||||
>
|
||||
<TabList bg={forumColors.background.main} borderRadius="lg" p="1">
|
||||
<Tab
|
||||
_selected={{ bg: forumColors.primary[500], color: 'white' }}
|
||||
color={forumColors.text.secondary}
|
||||
>
|
||||
快速选择
|
||||
</Tab>
|
||||
<Tab
|
||||
_selected={{ bg: forumColors.primary[500], color: 'white' }}
|
||||
color={forumColors.text.secondary}
|
||||
>
|
||||
自定义日期
|
||||
</Tab>
|
||||
</TabList>
|
||||
|
||||
<TabPanels>
|
||||
{/* 快速选择 */}
|
||||
<TabPanel px="0" py="3">
|
||||
<RadioGroup
|
||||
value={String(formData.deadline_days)}
|
||||
onChange={(value) => handleChange('deadline_days', parseInt(value))}
|
||||
>
|
||||
<VStack spacing="2" align="stretch">
|
||||
{[
|
||||
{ value: '1', label: '1天后', desc: '短期预测' },
|
||||
{ value: '3', label: '3天后', desc: '适合事件预测' },
|
||||
{ value: '7', label: '7天后', desc: '推荐,适合周度预测' },
|
||||
{ value: '14', label: '14天后', desc: '适合趋势预测' },
|
||||
{ value: '30', label: '30天后', desc: '长期预测' },
|
||||
].map((option) => (
|
||||
<Box
|
||||
key={option.value}
|
||||
as="label"
|
||||
cursor="pointer"
|
||||
bg={formData.deadline_days === parseInt(option.value)
|
||||
? forumColors.gradients.goldSubtle
|
||||
: forumColors.background.main
|
||||
}
|
||||
border="1px solid"
|
||||
borderColor={formData.deadline_days === parseInt(option.value)
|
||||
? forumColors.border.gold
|
||||
: forumColors.border.default
|
||||
}
|
||||
borderRadius="lg"
|
||||
p="3"
|
||||
transition="all 0.2s"
|
||||
_hover={{
|
||||
borderColor: forumColors.border.gold,
|
||||
}}
|
||||
>
|
||||
<HStack justify="space-between">
|
||||
<HStack spacing="3">
|
||||
<Radio value={option.value} colorScheme="yellow" />
|
||||
<VStack align="start" spacing="0">
|
||||
<Text fontSize="sm" fontWeight="600" color={forumColors.text.primary}>
|
||||
{option.label}
|
||||
</Text>
|
||||
<Text fontSize="xs" color={forumColors.text.tertiary}>
|
||||
{option.desc}
|
||||
</Text>
|
||||
</VStack>
|
||||
</HStack>
|
||||
{option.value === '7' && (
|
||||
<Text fontSize="xs" color={forumColors.primary[500]} fontWeight="600">
|
||||
推荐
|
||||
</Text>
|
||||
)}
|
||||
</HStack>
|
||||
</Box>
|
||||
))}
|
||||
</VStack>
|
||||
</RadioGroup>
|
||||
</TabPanel>
|
||||
|
||||
{/* 自定义日期时间 */}
|
||||
<TabPanel px="0" py="3">
|
||||
<VStack spacing="3" align="stretch">
|
||||
<HStack spacing="3">
|
||||
<FormControl flex="2">
|
||||
<FormLabel fontSize="xs" color={forumColors.text.secondary}>
|
||||
日期
|
||||
</FormLabel>
|
||||
<Input
|
||||
type="date"
|
||||
value={formData.custom_date}
|
||||
onChange={(e) => handleChange('custom_date', e.target.value)}
|
||||
min={getMinDate()}
|
||||
max={getMaxDate()}
|
||||
bg={forumColors.background.main}
|
||||
border="1px solid"
|
||||
borderColor={forumColors.border.default}
|
||||
color={forumColors.text.primary}
|
||||
_hover={{ borderColor: forumColors.border.light }}
|
||||
_focus={{
|
||||
borderColor: forumColors.border.gold,
|
||||
boxShadow: `0 0 0 1px ${forumColors.border.goldGlow}`,
|
||||
}}
|
||||
/>
|
||||
</FormControl>
|
||||
|
||||
<FormControl flex="1">
|
||||
<FormLabel fontSize="xs" color={forumColors.text.secondary}>
|
||||
时间
|
||||
</FormLabel>
|
||||
<Input
|
||||
type="time"
|
||||
value={formData.custom_time}
|
||||
onChange={(e) => handleChange('custom_time', e.target.value)}
|
||||
bg={forumColors.background.main}
|
||||
border="1px solid"
|
||||
borderColor={forumColors.border.default}
|
||||
color={forumColors.text.primary}
|
||||
_hover={{ borderColor: forumColors.border.light }}
|
||||
_focus={{
|
||||
borderColor: forumColors.border.gold,
|
||||
boxShadow: `0 0 0 1px ${forumColors.border.goldGlow}`,
|
||||
}}
|
||||
/>
|
||||
</FormControl>
|
||||
</HStack>
|
||||
|
||||
<Alert
|
||||
status="info"
|
||||
bg={forumColors.background.main}
|
||||
borderRadius="md"
|
||||
border="1px solid"
|
||||
borderColor={forumColors.border.default}
|
||||
py="2"
|
||||
>
|
||||
<Icon as={Clock} boxSize="14px" color={forumColors.primary[500]} mr="2" />
|
||||
<Text fontSize="xs" color={forumColors.text.secondary}>
|
||||
建议选择 15:00(A股收盘时间)作为截止时间
|
||||
</Text>
|
||||
</Alert>
|
||||
</VStack>
|
||||
</TabPanel>
|
||||
</TabPanels>
|
||||
</Tabs>
|
||||
|
||||
{/* 截止时间预览 */}
|
||||
<Box
|
||||
bg={forumColors.background.main}
|
||||
border="1px solid"
|
||||
borderColor={forumColors.border.default}
|
||||
color={forumColors.text.primary}
|
||||
_hover={{ borderColor: forumColors.border.light }}
|
||||
_focus={{
|
||||
borderColor: forumColors.border.gold,
|
||||
boxShadow: `0 0 0 1px ${forumColors.border.goldGlow}`,
|
||||
}}
|
||||
borderRadius="lg"
|
||||
p="3"
|
||||
>
|
||||
<option value="1">1天后</option>
|
||||
<option value="3">3天后</option>
|
||||
<option value="7">7天后(推荐)</option>
|
||||
<option value="14">14天后</option>
|
||||
<option value="30">30天后</option>
|
||||
</Select>
|
||||
<HStack spacing="2">
|
||||
<Icon as={Clock} boxSize="16px" color={forumColors.primary[500]} />
|
||||
<Text fontSize="sm" color={forumColors.text.primary} fontWeight="500">
|
||||
截止时间:
|
||||
</Text>
|
||||
<Text fontSize="sm" color={forumColors.text.secondary}>
|
||||
{formatDeadlineDisplay(calculatedDeadline)}
|
||||
</Text>
|
||||
</HStack>
|
||||
</Box>
|
||||
|
||||
<Text fontSize="xs" color={forumColors.text.tertiary} mt="2">
|
||||
截止后次日可提交结果进行结算
|
||||
截止后交易自动锁定,由作者提交结果进行结算
|
||||
</Text>
|
||||
</FormControl>
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
* 展示预测市场的话题概览
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import React, { useMemo } from 'react';
|
||||
import {
|
||||
Box,
|
||||
Text,
|
||||
@@ -14,7 +14,6 @@ import {
|
||||
Flex,
|
||||
Avatar,
|
||||
Icon,
|
||||
useColorModeValue,
|
||||
} from '@chakra-ui/react';
|
||||
import { motion } from 'framer-motion';
|
||||
import {
|
||||
@@ -22,18 +21,23 @@ import {
|
||||
TrendingDown,
|
||||
Crown,
|
||||
Users,
|
||||
Clock,
|
||||
Coins,
|
||||
Zap,
|
||||
Lock,
|
||||
CheckCircle2,
|
||||
} from 'lucide-react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { forumColors } from '@theme/forumTheme';
|
||||
import CountdownTimer, { useCountdown } from './CountdownTimer';
|
||||
|
||||
const MotionBox = motion(Box);
|
||||
|
||||
const PredictionTopicCard = ({ topic }) => {
|
||||
const navigate = useNavigate();
|
||||
|
||||
// 使用倒计时 Hook 获取实时状态
|
||||
const timeLeft = useCountdown(topic.deadline);
|
||||
|
||||
// 处理卡片点击
|
||||
const handleCardClick = () => {
|
||||
navigate(`/value-forum/prediction/${topic.id}`);
|
||||
@@ -46,19 +50,14 @@ const PredictionTopicCard = ({ topic }) => {
|
||||
return num;
|
||||
};
|
||||
|
||||
// 格式化时间
|
||||
const formatTime = (dateString) => {
|
||||
const date = new Date(dateString);
|
||||
const now = new Date();
|
||||
const diff = date - now;
|
||||
|
||||
const days = Math.floor(diff / 86400000);
|
||||
const hours = Math.floor(diff / 3600000);
|
||||
|
||||
if (days > 0) return `${days}天后`;
|
||||
if (hours > 0) return `${hours}小时后`;
|
||||
return '即将截止';
|
||||
};
|
||||
// 计算实际状态(基于截止时间自动判断)
|
||||
const effectiveStatus = useMemo(() => {
|
||||
// 如果已经是结算状态,保持不变
|
||||
if (topic.status === 'settled') return 'settled';
|
||||
// 如果已过截止时间,自动变为已截止
|
||||
if (timeLeft.expired && topic.status === 'active') return 'trading_closed';
|
||||
return topic.status;
|
||||
}, [topic.status, timeLeft.expired]);
|
||||
|
||||
// 获取选项数据
|
||||
const yesData = topic.positions?.yes || { total_shares: 0, current_price: 500, lord_id: null };
|
||||
@@ -71,7 +70,7 @@ const PredictionTopicCard = ({ topic }) => {
|
||||
const yesPercent = totalShares > 0 ? (yesData.total_shares / totalShares) * 100 : 50;
|
||||
const noPercent = totalShares > 0 ? (noData.total_shares / totalShares) * 100 : 50;
|
||||
|
||||
// 状态颜色
|
||||
// 状态颜色(使用 effectiveStatus)
|
||||
const statusColorMap = {
|
||||
active: forumColors.success[500],
|
||||
trading_closed: forumColors.warning[500],
|
||||
@@ -80,10 +79,16 @@ const PredictionTopicCard = ({ topic }) => {
|
||||
|
||||
const statusLabelMap = {
|
||||
active: '交易中',
|
||||
trading_closed: '已截止',
|
||||
trading_closed: '等待结算',
|
||||
settled: '已结算',
|
||||
};
|
||||
|
||||
const statusIconMap = {
|
||||
active: Zap,
|
||||
trading_closed: Lock,
|
||||
settled: CheckCircle2,
|
||||
};
|
||||
|
||||
return (
|
||||
<MotionBox
|
||||
bg={forumColors.background.card}
|
||||
@@ -110,14 +115,14 @@ const PredictionTopicCard = ({ topic }) => {
|
||||
>
|
||||
<Flex justify="space-between" align="center">
|
||||
<HStack spacing="2">
|
||||
<Icon as={Zap} boxSize="16px" color={forumColors.primary[500]} />
|
||||
<Text fontSize="xs" fontWeight="bold" color={forumColors.primary[500]}>
|
||||
预测市场
|
||||
<Icon as={statusIconMap[effectiveStatus]} boxSize="16px" color={statusColorMap[effectiveStatus]} />
|
||||
<Text fontSize="xs" fontWeight="bold" color={statusColorMap[effectiveStatus]}>
|
||||
{statusLabelMap[effectiveStatus]}
|
||||
</Text>
|
||||
</HStack>
|
||||
|
||||
<Badge
|
||||
bg={statusColorMap[topic.status]}
|
||||
bg={statusColorMap[effectiveStatus]}
|
||||
color="white"
|
||||
px="3"
|
||||
py="1"
|
||||
@@ -125,7 +130,7 @@ const PredictionTopicCard = ({ topic }) => {
|
||||
fontSize="xs"
|
||||
fontWeight="bold"
|
||||
>
|
||||
{statusLabelMap[topic.status]}
|
||||
{effectiveStatus === 'active' ? '可交易' : effectiveStatus === 'trading_closed' ? '待结算' : '已完成'}
|
||||
</Badge>
|
||||
</Flex>
|
||||
</Box>
|
||||
@@ -270,7 +275,7 @@ const PredictionTopicCard = ({ topic }) => {
|
||||
</Box>
|
||||
|
||||
{/* 奖池和数据 */}
|
||||
<HStack spacing="4" fontSize="sm" color={forumColors.text.secondary}>
|
||||
<HStack spacing="4" fontSize="sm" color={forumColors.text.secondary} flexWrap="wrap">
|
||||
<HStack spacing="1">
|
||||
<Icon as={Coins} boxSize="16px" color={forumColors.primary[500]} />
|
||||
<Text fontWeight="600" color={forumColors.primary[500]}>
|
||||
@@ -281,13 +286,16 @@ const PredictionTopicCard = ({ topic }) => {
|
||||
|
||||
<HStack spacing="1">
|
||||
<Icon as={Users} boxSize="16px" />
|
||||
<Text>{topic.stats?.unique_traders?.size || 0}人</Text>
|
||||
<Text>{topic.participants_count || topic.stats?.unique_traders?.size || 0}人</Text>
|
||||
</HStack>
|
||||
|
||||
<HStack spacing="1">
|
||||
<Icon as={Clock} boxSize="16px" />
|
||||
<Text>{formatTime(topic.deadline)}</Text>
|
||||
</HStack>
|
||||
{/* 使用新的倒计时组件 */}
|
||||
<CountdownTimer
|
||||
deadline={topic.deadline}
|
||||
status={effectiveStatus}
|
||||
size="sm"
|
||||
showIcon={true}
|
||||
/>
|
||||
</HStack>
|
||||
|
||||
{/* 底部:作者信息 */}
|
||||
|
||||
332
src/views/ValueForum/components/SettleTopicModal.js
Normal file
332
src/views/ValueForum/components/SettleTopicModal.js
Normal file
@@ -0,0 +1,332 @@
|
||||
/**
|
||||
* 话题结算模态框
|
||||
* 允许话题创建者在截止后提交结算结果
|
||||
*/
|
||||
|
||||
import React, { useState } from 'react';
|
||||
import {
|
||||
Modal,
|
||||
ModalOverlay,
|
||||
ModalContent,
|
||||
ModalHeader,
|
||||
ModalBody,
|
||||
ModalFooter,
|
||||
ModalCloseButton,
|
||||
Button,
|
||||
VStack,
|
||||
HStack,
|
||||
Text,
|
||||
Box,
|
||||
Icon,
|
||||
Alert,
|
||||
AlertIcon,
|
||||
useToast,
|
||||
RadioGroup,
|
||||
Radio,
|
||||
Divider,
|
||||
} from '@chakra-ui/react';
|
||||
import { CheckCircle2, TrendingUp, TrendingDown, Scale, AlertTriangle, Coins } from 'lucide-react';
|
||||
import { forumColors } from '@theme/forumTheme';
|
||||
import { settleTopic } from '@services/predictionMarketService.api';
|
||||
import { GLASS_BLUR } from '@/constants/glassConfig';
|
||||
|
||||
const SettleTopicModal = ({ isOpen, onClose, topic, onSettleSuccess }) => {
|
||||
const toast = useToast();
|
||||
const [selectedResult, setSelectedResult] = useState('');
|
||||
const [isSubmitting, setIsSubmitting] = useState(false);
|
||||
|
||||
// 获取选项数据
|
||||
const yesData = {
|
||||
total_shares: topic?.yes_total_shares || 0,
|
||||
current_price: topic?.yes_price || 500,
|
||||
};
|
||||
const noData = {
|
||||
total_shares: topic?.no_total_shares || 0,
|
||||
current_price: topic?.no_price || 500,
|
||||
};
|
||||
|
||||
const totalShares = yesData.total_shares + noData.total_shares;
|
||||
const yesPercent = totalShares > 0 ? (yesData.total_shares / totalShares) * 100 : 50;
|
||||
const noPercent = totalShares > 0 ? (noData.total_shares / totalShares) * 100 : 50;
|
||||
|
||||
// 结算选项配置
|
||||
const resultOptions = [
|
||||
{
|
||||
value: 'yes',
|
||||
label: '看涨方获胜 (Yes)',
|
||||
icon: TrendingUp,
|
||||
color: 'green',
|
||||
description: `${yesData.total_shares} 份持仓将获得奖池分红`,
|
||||
poolShare: yesData.total_shares > 0 ? topic?.total_pool : 0,
|
||||
},
|
||||
{
|
||||
value: 'no',
|
||||
label: '看跌方获胜 (No)',
|
||||
icon: TrendingDown,
|
||||
color: 'red',
|
||||
description: `${noData.total_shares} 份持仓将获得奖池分红`,
|
||||
poolShare: noData.total_shares > 0 ? topic?.total_pool : 0,
|
||||
},
|
||||
{
|
||||
value: 'draw',
|
||||
label: '平局 / 无效',
|
||||
icon: Scale,
|
||||
color: 'gray',
|
||||
description: '所有参与者按持仓比例退回积分',
|
||||
poolShare: 0,
|
||||
},
|
||||
];
|
||||
|
||||
const handleSubmit = async () => {
|
||||
if (!selectedResult) {
|
||||
toast({
|
||||
title: '请选择结算结果',
|
||||
status: 'warning',
|
||||
duration: 3000,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
setIsSubmitting(true);
|
||||
|
||||
const response = await settleTopic(topic.id, selectedResult);
|
||||
|
||||
if (response.success) {
|
||||
toast({
|
||||
title: '结算成功!',
|
||||
description: `话题已结算,${resultOptions.find(o => o.value === selectedResult)?.label}`,
|
||||
status: 'success',
|
||||
duration: 5000,
|
||||
});
|
||||
|
||||
if (onSettleSuccess) {
|
||||
onSettleSuccess(response.data);
|
||||
}
|
||||
|
||||
onClose();
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('结算失败:', error);
|
||||
toast({
|
||||
title: '结算失败',
|
||||
description: error.message || '请稍后重试',
|
||||
status: 'error',
|
||||
duration: 3000,
|
||||
});
|
||||
} finally {
|
||||
setIsSubmitting(false);
|
||||
}
|
||||
};
|
||||
|
||||
if (!topic) return null;
|
||||
|
||||
return (
|
||||
<Modal isOpen={isOpen} onClose={onClose} size="lg" isCentered>
|
||||
<ModalOverlay backdropFilter={GLASS_BLUR.xs} />
|
||||
<ModalContent
|
||||
bg={forumColors.background.card}
|
||||
borderRadius="xl"
|
||||
border="1px solid"
|
||||
borderColor={forumColors.border.default}
|
||||
>
|
||||
<ModalHeader
|
||||
bg={forumColors.gradients.goldSubtle}
|
||||
borderTopRadius="xl"
|
||||
borderBottom="1px solid"
|
||||
borderColor={forumColors.border.default}
|
||||
>
|
||||
<HStack spacing="2">
|
||||
<Icon as={CheckCircle2} boxSize="20px" color={forumColors.primary[500]} />
|
||||
<Text color={forumColors.text.primary}>结算预测话题</Text>
|
||||
</HStack>
|
||||
</ModalHeader>
|
||||
<ModalCloseButton color={forumColors.text.primary} />
|
||||
|
||||
<ModalBody py="6">
|
||||
<VStack spacing="5" align="stretch">
|
||||
{/* 话题信息 */}
|
||||
<Box
|
||||
bg={forumColors.background.hover}
|
||||
borderRadius="lg"
|
||||
p="4"
|
||||
border="1px solid"
|
||||
borderColor={forumColors.border.default}
|
||||
>
|
||||
<Text fontSize="md" fontWeight="600" color={forumColors.text.primary} mb="2">
|
||||
{topic.title}
|
||||
</Text>
|
||||
<Text fontSize="sm" color={forumColors.text.secondary} noOfLines={2}>
|
||||
{topic.description}
|
||||
</Text>
|
||||
</Box>
|
||||
|
||||
{/* 市场数据摘要 */}
|
||||
<HStack spacing="4" justify="center">
|
||||
<VStack spacing="1">
|
||||
<Text fontSize="xs" color={forumColors.text.tertiary}>看涨方</Text>
|
||||
<Text fontSize="lg" fontWeight="bold" color="green.500">
|
||||
{yesPercent.toFixed(0)}%
|
||||
</Text>
|
||||
<Text fontSize="xs" color={forumColors.text.secondary}>
|
||||
{yesData.total_shares} 份
|
||||
</Text>
|
||||
</VStack>
|
||||
|
||||
<Divider orientation="vertical" h="60px" />
|
||||
|
||||
<VStack spacing="1">
|
||||
<Text fontSize="xs" color={forumColors.text.tertiary}>看跌方</Text>
|
||||
<Text fontSize="lg" fontWeight="bold" color="red.500">
|
||||
{noPercent.toFixed(0)}%
|
||||
</Text>
|
||||
<Text fontSize="xs" color={forumColors.text.secondary}>
|
||||
{noData.total_shares} 份
|
||||
</Text>
|
||||
</VStack>
|
||||
|
||||
<Divider orientation="vertical" h="60px" />
|
||||
|
||||
<VStack spacing="1">
|
||||
<Text fontSize="xs" color={forumColors.text.tertiary}>奖池</Text>
|
||||
<HStack spacing="1">
|
||||
<Icon as={Coins} boxSize="16px" color={forumColors.primary[500]} />
|
||||
<Text fontSize="lg" fontWeight="bold" color={forumColors.primary[500]}>
|
||||
{topic.total_pool}
|
||||
</Text>
|
||||
</HStack>
|
||||
<Text fontSize="xs" color={forumColors.text.secondary}>积分</Text>
|
||||
</VStack>
|
||||
</HStack>
|
||||
|
||||
{/* 警告提示 */}
|
||||
<Alert
|
||||
status="warning"
|
||||
bg="rgba(221, 107, 32, 0.1)"
|
||||
borderRadius="lg"
|
||||
border="1px solid"
|
||||
borderColor="orange.300"
|
||||
>
|
||||
<AlertIcon color="orange.400" />
|
||||
<VStack align="start" spacing="1" flex="1">
|
||||
<Text fontSize="sm" color={forumColors.text.primary} fontWeight="600">
|
||||
结算操作不可撤销
|
||||
</Text>
|
||||
<Text fontSize="xs" color={forumColors.text.secondary}>
|
||||
请确认预测结果后再提交,结算后将自动分配奖池给获胜方
|
||||
</Text>
|
||||
</VStack>
|
||||
</Alert>
|
||||
|
||||
{/* 结算选项 */}
|
||||
<Box>
|
||||
<Text fontSize="sm" fontWeight="600" color={forumColors.text.primary} mb="3">
|
||||
选择结算结果
|
||||
</Text>
|
||||
|
||||
<RadioGroup value={selectedResult} onChange={setSelectedResult}>
|
||||
<VStack spacing="3" align="stretch">
|
||||
{resultOptions.map((option) => (
|
||||
<Box
|
||||
key={option.value}
|
||||
as="label"
|
||||
cursor="pointer"
|
||||
bg={selectedResult === option.value
|
||||
? `${option.color}.50`
|
||||
: forumColors.background.hover
|
||||
}
|
||||
border="2px solid"
|
||||
borderColor={selectedResult === option.value
|
||||
? `${option.color}.400`
|
||||
: forumColors.border.default
|
||||
}
|
||||
borderRadius="lg"
|
||||
p="4"
|
||||
transition="all 0.2s"
|
||||
_hover={{
|
||||
borderColor: `${option.color}.300`,
|
||||
bg: `${option.color}.50`,
|
||||
}}
|
||||
>
|
||||
<HStack justify="space-between" align="start">
|
||||
<HStack spacing="3" align="start">
|
||||
<Radio
|
||||
value={option.value}
|
||||
colorScheme={option.color}
|
||||
mt="1"
|
||||
/>
|
||||
<VStack align="start" spacing="1">
|
||||
<HStack spacing="2">
|
||||
<Icon
|
||||
as={option.icon}
|
||||
boxSize="18px"
|
||||
color={`${option.color}.500`}
|
||||
/>
|
||||
<Text
|
||||
fontSize="md"
|
||||
fontWeight="600"
|
||||
color={forumColors.text.primary}
|
||||
>
|
||||
{option.label}
|
||||
</Text>
|
||||
</HStack>
|
||||
<Text fontSize="xs" color={forumColors.text.secondary}>
|
||||
{option.description}
|
||||
</Text>
|
||||
</VStack>
|
||||
</HStack>
|
||||
|
||||
{option.poolShare > 0 && (
|
||||
<VStack align="end" spacing="0">
|
||||
<Text fontSize="xs" color={forumColors.text.tertiary}>
|
||||
奖池分配
|
||||
</Text>
|
||||
<Text fontSize="md" fontWeight="bold" color={forumColors.primary[500]}>
|
||||
{option.poolShare} 积分
|
||||
</Text>
|
||||
</VStack>
|
||||
)}
|
||||
</HStack>
|
||||
</Box>
|
||||
))}
|
||||
</VStack>
|
||||
</RadioGroup>
|
||||
</Box>
|
||||
</VStack>
|
||||
</ModalBody>
|
||||
|
||||
<ModalFooter borderTop="1px solid" borderColor={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="结算中..."
|
||||
isDisabled={!selectedResult}
|
||||
leftIcon={<CheckCircle2 size={18} />}
|
||||
_hover={{
|
||||
opacity: 0.9,
|
||||
transform: 'translateY(-2px)',
|
||||
}}
|
||||
_active={{ transform: 'translateY(0)' }}
|
||||
>
|
||||
确认结算
|
||||
</Button>
|
||||
</HStack>
|
||||
</ModalFooter>
|
||||
</ModalContent>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
||||
export default SettleTopicModal;
|
||||
Reference in New Issue
Block a user