update pay function

This commit is contained in:
2025-11-23 18:11:48 +08:00
parent b582de9bc2
commit 7b3907a3bd
9 changed files with 3229 additions and 91 deletions

View File

@@ -0,0 +1,386 @@
/**
* 创建预测话题模态框
* 用户可以发起新的预测市场话题
*/
import React, { useState } from 'react';
import {
Modal,
ModalOverlay,
ModalContent,
ModalHeader,
ModalBody,
ModalFooter,
ModalCloseButton,
Button,
VStack,
FormControl,
FormLabel,
Input,
Textarea,
Select,
HStack,
Text,
Box,
Icon,
Alert,
AlertIcon,
useToast,
} from '@chakra-ui/react';
import { Zap, Calendar, DollarSign } from 'lucide-react';
import { forumColors } from '@theme/forumTheme';
import { createTopic } from '@services/predictionMarketService';
import { getUserAccount, CREDIT_CONFIG } from '@services/creditSystemService';
import { useAuth } from '@contexts/AuthContext';
const CreatePredictionModal = ({ isOpen, onClose, onTopicCreated }) => {
const toast = useToast();
const { user } = useAuth();
// 表单状态
const [formData, setFormData] = useState({
title: '',
description: '',
category: 'stock',
deadline_days: 7,
});
const [isSubmitting, setIsSubmitting] = useState(false);
// 获取用户余额
const userAccount = user ? getUserAccount(user.id) : null;
// 处理表单变化
const handleChange = (field, value) => {
setFormData((prev) => ({ ...prev, [field]: value }));
};
// 提交表单
const handleSubmit = async () => {
try {
setIsSubmitting(true);
// 验证
if (!formData.title.trim()) {
toast({
title: '请填写话题标题',
status: 'warning',
duration: 3000,
});
return;
}
if (!formData.description.trim()) {
toast({
title: '请填写话题描述',
status: 'warning',
duration: 3000,
});
return;
}
// 检查余额
if (userAccount.balance < CREDIT_CONFIG.CREATE_TOPIC_COST) {
toast({
title: '积分不足',
description: `创建话题需要${CREDIT_CONFIG.CREATE_TOPIC_COST}积分`,
status: 'error',
duration: 3000,
});
return;
}
// 计算截止时间
const deadline = new Date();
deadline.setDate(deadline.getDate() + parseInt(formData.deadline_days));
const settlement_date = new Date(deadline);
settlement_date.setDate(settlement_date.getDate() + 1);
// 创建话题
const newTopic = createTopic({
author_id: user.id,
author_name: user.name || user.username,
author_avatar: user.avatar,
title: formData.title,
description: formData.description,
category: formData.category,
deadline: deadline.toISOString(),
settlement_date: settlement_date.toISOString(),
});
toast({
title: '创建成功!',
description: `话题已发布,扣除${CREDIT_CONFIG.CREATE_TOPIC_COST}积分`,
status: 'success',
duration: 3000,
});
// 重置表单
setFormData({
title: '',
description: '',
category: 'stock',
deadline_days: 7,
});
// 通知父组件
if (onTopicCreated) {
onTopicCreated(newTopic);
}
onClose();
} catch (error) {
console.error('创建话题失败:', error);
toast({
title: '创建失败',
description: error.message,
status: 'error',
duration: 3000,
});
} finally {
setIsSubmitting(false);
}
};
return (
<Modal isOpen={isOpen} onClose={onClose} size="xl" isCentered>
<ModalOverlay backdropFilter="blur(4px)" />
<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={Zap} 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">
{/* 提示信息 */}
<Alert
status="info"
bg={forumColors.background.hover}
borderRadius="lg"
border="1px solid"
borderColor={forumColors.border.default}
>
<AlertIcon color={forumColors.primary[500]} />
<VStack align="start" spacing="1" flex="1">
<Text fontSize="sm" color={forumColors.text.primary} fontWeight="600">
创建预测话题
</Text>
<Text fontSize="xs" color={forumColors.text.secondary}>
创建费用{CREDIT_CONFIG.CREATE_TOPIC_COST}积分进入奖池
</Text>
<Text fontSize="xs" color={forumColors.text.secondary}>
作者不能参与自己发起的话题
</Text>
<Text fontSize="xs" color={forumColors.text.secondary}>
截止后由作者提交结果进行结算
</Text>
</VStack>
</Alert>
{/* 话题标题 */}
<FormControl isRequired>
<FormLabel fontSize="sm" color={forumColors.text.primary}>
话题标题
</FormLabel>
<Input
placeholder="例如:贵州茅台下周会涨吗?"
value={formData.title}
onChange={(e) => handleChange('title', e.target.value)}
bg={forumColors.background.main}
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}`,
}}
/>
</FormControl>
{/* 话题描述 */}
<FormControl isRequired>
<FormLabel fontSize="sm" color={forumColors.text.primary}>
话题描述
</FormLabel>
<Textarea
placeholder="详细描述预测的内容、判断标准、数据来源等..."
value={formData.description}
onChange={(e) => handleChange('description', e.target.value)}
rows={4}
bg={forumColors.background.main}
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}`,
}}
/>
</FormControl>
{/* 分类 */}
<FormControl>
<FormLabel fontSize="sm" color={forumColors.text.primary}>
分类
</FormLabel>
<Select
value={formData.category}
onChange={(e) => handleChange('category', 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}`,
}}
>
<option value="stock">股票行情</option>
<option value="index">指数走势</option>
<option value="concept">概念板块</option>
<option value="policy">政策影响</option>
<option value="event">事件预测</option>
<option value="other">其他</option>
</Select>
</FormControl>
{/* 截止时间 */}
<FormControl>
<FormLabel fontSize="sm" color={forumColors.text.primary}>
<HStack spacing="2">
<Icon as={Calendar} boxSize="16px" />
<Text>交易截止时间</Text>
</HStack>
</FormLabel>
<Select
value={formData.deadline_days}
onChange={(e) => handleChange('deadline_days', 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}`,
}}
>
<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>
<Text fontSize="xs" color={forumColors.text.tertiary} mt="2">
截止后次日可提交结果进行结算
</Text>
</FormControl>
{/* 费用说明 */}
<Box
bg={forumColors.gradients.goldSubtle}
border="1px solid"
borderColor={forumColors.border.gold}
borderRadius="lg"
p="4"
>
<HStack justify="space-between">
<VStack align="start" spacing="1">
<Text fontSize="sm" fontWeight="600" color={forumColors.text.primary}>
创建费用
</Text>
<Text fontSize="xs" color={forumColors.text.secondary}>
将进入奖池奖励给获胜者
</Text>
</VStack>
<HStack spacing="1">
<Icon as={DollarSign} boxSize="20px" color={forumColors.primary[500]} />
<Text fontSize="2xl" fontWeight="bold" color={forumColors.primary[500]}>
{CREDIT_CONFIG.CREATE_TOPIC_COST}
</Text>
<Text fontSize="sm" color={forumColors.text.secondary}>
积分
</Text>
</HStack>
</HStack>
<Box mt="3" pt="3" borderTop="1px solid" borderColor={forumColors.border.default}>
<HStack justify="space-between" fontSize="sm">
<Text color={forumColors.text.secondary}>你的余额</Text>
<Text fontWeight="600" color={forumColors.text.primary}>
{userAccount?.balance || 0} 积分
</Text>
</HStack>
<HStack justify="space-between" fontSize="sm" mt="1">
<Text color={forumColors.text.secondary}>创建后</Text>
<Text
fontWeight="600"
color={
(userAccount?.balance || 0) >= CREDIT_CONFIG.CREATE_TOPIC_COST
? forumColors.success[500]
: forumColors.error[500]
}
>
{(userAccount?.balance || 0) - CREDIT_CONFIG.CREATE_TOPIC_COST} 积分
</Text>
</HStack>
</Box>
</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={(userAccount?.balance || 0) < CREDIT_CONFIG.CREATE_TOPIC_COST}
_hover={{
opacity: 0.9,
transform: 'translateY(-2px)',
}}
_active={{ transform: 'translateY(0)' }}
>
发布话题
</Button>
</HStack>
</ModalFooter>
</ModalContent>
</Modal>
);
};
export default CreatePredictionModal;

View File

@@ -0,0 +1,327 @@
/**
* 预测话题卡片组件
* 展示预测市场的话题概览
*/
import React from 'react';
import {
Box,
Text,
HStack,
VStack,
Badge,
Progress,
Flex,
Avatar,
Icon,
useColorModeValue,
} from '@chakra-ui/react';
import { motion } from 'framer-motion';
import {
TrendingUp,
TrendingDown,
Crown,
Users,
Clock,
DollarSign,
Zap,
} from 'lucide-react';
import { useNavigate } from 'react-router-dom';
import { forumColors } from '@theme/forumTheme';
const MotionBox = motion(Box);
const PredictionTopicCard = ({ topic }) => {
const navigate = useNavigate();
// 处理卡片点击
const handleCardClick = () => {
navigate(`/value-forum/prediction/${topic.id}`);
};
// 格式化数字
const formatNumber = (num) => {
if (num >= 10000) return `${(num / 10000).toFixed(1)}`;
if (num >= 1000) return `${(num / 1000).toFixed(1)}K`;
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 yesData = topic.positions?.yes || { total_shares: 0, current_price: 500, lord_id: null };
const noData = topic.positions?.no || { total_shares: 0, current_price: 500, lord_id: null };
// 计算总份额
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 statusColorMap = {
active: forumColors.success[500],
trading_closed: forumColors.warning[500],
settled: forumColors.text.secondary,
};
const statusLabelMap = {
active: '交易中',
trading_closed: '已截止',
settled: '已结算',
};
return (
<MotionBox
bg={forumColors.background.card}
borderRadius="xl"
overflow="hidden"
border="2px solid"
borderColor={forumColors.border.default}
cursor="pointer"
onClick={handleCardClick}
whileHover={{ y: -8, scale: 1.02 }}
transition={{ duration: 0.3 }}
_hover={{
borderColor: forumColors.border.gold,
boxShadow: forumColors.shadows.gold,
}}
>
{/* 头部:状态标识 */}
<Box
bg={forumColors.gradients.goldSubtle}
px="4"
py="2"
borderBottom="1px solid"
borderColor={forumColors.border.default}
>
<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]}>
预测市场
</Text>
</HStack>
<Badge
bg={statusColorMap[topic.status]}
color="white"
px="3"
py="1"
borderRadius="full"
fontSize="xs"
fontWeight="bold"
>
{statusLabelMap[topic.status]}
</Badge>
</Flex>
</Box>
{/* 内容区域 */}
<VStack align="stretch" p="5" spacing="4">
{/* 话题标题 */}
<Text
fontSize="lg"
fontWeight="700"
color={forumColors.text.primary}
noOfLines={2}
lineHeight="1.4"
>
{topic.title}
</Text>
{/* 描述 */}
{topic.description && (
<Text
fontSize="sm"
color={forumColors.text.secondary}
noOfLines={2}
lineHeight="1.6"
>
{topic.description}
</Text>
)}
{/* 双向价格卡片 */}
<HStack spacing="3" w="full">
{/* Yes 方 */}
<Box
flex="1"
bg="linear-gradient(135deg, rgba(72, 187, 120, 0.1) 0%, rgba(72, 187, 120, 0.05) 100%)"
border="2px solid"
borderColor="green.400"
borderRadius="lg"
p="3"
position="relative"
overflow="hidden"
>
{/* 领主徽章 */}
{yesData.lord_id && (
<Icon
as={Crown}
position="absolute"
top="2"
right="2"
boxSize="16px"
color="yellow.400"
/>
)}
<VStack spacing="1" align="start">
<HStack spacing="1">
<Icon as={TrendingUp} boxSize="14px" color="green.500" />
<Text fontSize="xs" fontWeight="600" color="green.600">
看涨 / Yes
</Text>
</HStack>
<Text fontSize="2xl" fontWeight="bold" color="green.600">
{Math.round(yesData.current_price)}
<Text as="span" fontSize="xs" ml="1">
积分
</Text>
</Text>
<Text fontSize="xs" color={forumColors.text.secondary}>
{yesData.total_shares} · {yesPercent.toFixed(0)}%
</Text>
</VStack>
</Box>
{/* No 方 */}
<Box
flex="1"
bg="linear-gradient(135deg, rgba(245, 101, 101, 0.1) 0%, rgba(245, 101, 101, 0.05) 100%)"
border="2px solid"
borderColor="red.400"
borderRadius="lg"
p="3"
position="relative"
overflow="hidden"
>
{/* 领主徽章 */}
{noData.lord_id && (
<Icon
as={Crown}
position="absolute"
top="2"
right="2"
boxSize="16px"
color="yellow.400"
/>
)}
<VStack spacing="1" align="start">
<HStack spacing="1">
<Icon as={TrendingDown} boxSize="14px" color="red.500" />
<Text fontSize="xs" fontWeight="600" color="red.600">
看跌 / No
</Text>
</HStack>
<Text fontSize="2xl" fontWeight="bold" color="red.600">
{Math.round(noData.current_price)}
<Text as="span" fontSize="xs" ml="1">
积分
</Text>
</Text>
<Text fontSize="xs" color={forumColors.text.secondary}>
{noData.total_shares} · {noPercent.toFixed(0)}%
</Text>
</VStack>
</Box>
</HStack>
{/* 市场情绪进度条 */}
<Box>
<Flex justify="space-between" mb="1">
<Text fontSize="xs" color={forumColors.text.tertiary}>
市场情绪
</Text>
<Text fontSize="xs" color={forumColors.text.tertiary}>
{yesPercent.toFixed(0)}% vs {noPercent.toFixed(0)}%
</Text>
</Flex>
<Progress
value={yesPercent}
size="sm"
borderRadius="full"
bg="red.100"
sx={{
'& > div': {
bg: 'linear-gradient(90deg, #48BB78 0%, #38A169 100%)',
},
}}
/>
</Box>
{/* 奖池和数据 */}
<HStack spacing="4" fontSize="sm" color={forumColors.text.secondary}>
<HStack spacing="1">
<Icon as={DollarSign} boxSize="16px" color={forumColors.primary[500]} />
<Text fontWeight="600" color={forumColors.primary[500]}>
{formatNumber(topic.total_pool)}
</Text>
<Text fontSize="xs">奖池</Text>
</HStack>
<HStack spacing="1">
<Icon as={Users} boxSize="16px" />
<Text>{topic.stats?.unique_traders?.size || 0}</Text>
</HStack>
<HStack spacing="1">
<Icon as={Clock} boxSize="16px" />
<Text>{formatTime(topic.deadline)}</Text>
</HStack>
</HStack>
{/* 底部:作者信息 */}
<Flex justify="space-between" align="center" pt="2" borderTop="1px solid" borderColor={forumColors.border.default}>
<HStack spacing="2">
<Avatar
size="xs"
name={topic.author_name}
src={topic.author_avatar}
bg={forumColors.gradients.goldPrimary}
color={forumColors.background.main}
/>
<Text fontSize="xs" color={forumColors.text.tertiary}>
{topic.author_name}
</Text>
</HStack>
{/* 分类标签 */}
{topic.category && (
<Badge
bg={forumColors.background.hover}
color={forumColors.text.primary}
px="2"
py="1"
borderRadius="md"
fontSize="xs"
>
{topic.category}
</Badge>
)}
</Flex>
</VStack>
</MotionBox>
);
};
export default PredictionTopicCard;

View File

@@ -0,0 +1,494 @@
/**
* 交易模态框组件
* 用于买入/卖出预测市场席位
*/
import React, { useState, useEffect } from 'react';
import {
Modal,
ModalOverlay,
ModalContent,
ModalHeader,
ModalBody,
ModalFooter,
ModalCloseButton,
Button,
VStack,
HStack,
Text,
Box,
Icon,
Slider,
SliderTrack,
SliderFilledTrack,
SliderThumb,
RadioGroup,
Radio,
Stack,
Flex,
useToast,
Badge,
} from '@chakra-ui/react';
import { TrendingUp, TrendingDown, DollarSign, AlertCircle, Zap } from 'lucide-react';
import { motion } from 'framer-motion';
import { forumColors } from '@theme/forumTheme';
import {
buyPosition,
sellPosition,
calculateBuyCost,
calculateSellRevenue,
calculateTax,
getTopic,
} from '@services/predictionMarketService';
import { getUserAccount, CREDIT_CONFIG } from '@services/creditSystemService';
import { useAuth } from '@contexts/AuthContext';
const MotionBox = motion(Box);
const TradeModal = ({ isOpen, onClose, topic, mode = 'buy', onTradeSuccess }) => {
const toast = useToast();
const { user } = useAuth();
// 状态
const [selectedOption, setSelectedOption] = useState('yes');
const [shares, setShares] = useState(1);
const [isSubmitting, setIsSubmitting] = useState(false);
// 获取用户账户
const userAccount = user ? getUserAccount(user.id) : null;
// 重置状态
useEffect(() => {
if (isOpen) {
setSelectedOption('yes');
setShares(1);
}
}, [isOpen]);
if (!topic || !userAccount) return null;
// 获取市场数据
const selectedSide = topic.positions[selectedOption];
const otherOption = selectedOption === 'yes' ? 'no' : 'yes';
const otherSide = topic.positions[otherOption];
// 计算交易数据
let cost = 0;
let tax = 0;
let totalCost = 0;
let avgPrice = 0;
if (mode === 'buy') {
cost = calculateBuyCost(selectedSide.total_shares, otherSide.total_shares, shares);
tax = calculateTax(cost);
totalCost = cost + tax;
avgPrice = cost / shares;
} else {
cost = calculateSellRevenue(selectedSide.total_shares, otherSide.total_shares, shares);
tax = calculateTax(cost);
totalCost = cost - tax;
avgPrice = cost / shares;
}
// 获取用户在该方向的持仓
const userPosition = userAccount.active_positions?.find(
(p) => p.topic_id === topic.id && p.option_id === selectedOption
);
const maxShares = mode === 'buy' ? 10 : userPosition?.shares || 0;
// 检查是否可以交易
const canTrade = () => {
if (mode === 'buy') {
// 检查余额
if (userAccount.balance < totalCost) {
return { ok: false, reason: '积分不足' };
}
// 检查单次上限
if (totalCost > CREDIT_CONFIG.MAX_SINGLE_BET) {
return { ok: false, reason: `单次购买上限${CREDIT_CONFIG.MAX_SINGLE_BET}积分` };
}
return { ok: true };
} else {
// 检查持仓
if (!userPosition || userPosition.shares < shares) {
return { ok: false, reason: '持仓不足' };
}
return { ok: true };
}
};
const tradeCheck = canTrade();
// 处理交易
const handleTrade = async () => {
try {
setIsSubmitting(true);
let result;
if (mode === 'buy') {
result = buyPosition({
user_id: user.id,
user_name: user.name || user.username,
user_avatar: user.avatar,
topic_id: topic.id,
option_id: selectedOption,
shares,
});
} else {
result = sellPosition({
user_id: user.id,
topic_id: topic.id,
option_id: selectedOption,
shares,
});
}
toast({
title: mode === 'buy' ? '购买成功!' : '卖出成功!',
description: mode === 'buy' ? `花费${totalCost}积分` : `获得${totalCost}积分`,
status: 'success',
duration: 3000,
});
// 通知父组件刷新
if (onTradeSuccess) {
onTradeSuccess(result);
}
onClose();
} catch (error) {
console.error('交易失败:', error);
toast({
title: '交易失败',
description: error.message,
status: 'error',
duration: 3000,
});
} finally {
setIsSubmitting(false);
}
};
return (
<Modal isOpen={isOpen} onClose={onClose} size="lg" isCentered>
<ModalOverlay backdropFilter="blur(4px)" />
<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={mode === 'buy' ? Zap : DollarSign}
boxSize="20px"
color={forumColors.primary[500]}
/>
<Text color={forumColors.text.primary}>
{mode === 'buy' ? '购买席位' : '卖出席位'}
</Text>
</HStack>
</ModalHeader>
<ModalCloseButton color={forumColors.text.primary} />
<ModalBody py="6">
<VStack spacing="5" align="stretch">
{/* 话题标题 */}
<Box
bg={forumColors.background.hover}
borderRadius="lg"
p="3"
border="1px solid"
borderColor={forumColors.border.default}
>
<Text fontSize="sm" fontWeight="600" color={forumColors.text.primary}>
{topic.title}
</Text>
</Box>
{/* 选择方向 */}
<Box>
<Text fontSize="sm" fontWeight="600" color={forumColors.text.primary} mb="3">
选择方向
</Text>
<RadioGroup value={selectedOption} onChange={setSelectedOption}>
<Stack spacing="3">
{/* Yes 选项 */}
<MotionBox
whileHover={{ scale: 1.02 }}
whileTap={{ scale: 0.98 }}
>
<Box
bg={
selectedOption === 'yes'
? 'linear-gradient(135deg, rgba(72, 187, 120, 0.2) 0%, rgba(72, 187, 120, 0.1) 100%)'
: forumColors.background.hover
}
border="2px solid"
borderColor={selectedOption === 'yes' ? 'green.400' : forumColors.border.default}
borderRadius="lg"
p="4"
cursor="pointer"
onClick={() => setSelectedOption('yes')}
>
<Flex justify="space-between" align="center">
<HStack spacing="3">
<Radio value="yes" colorScheme="green" />
<VStack align="start" spacing="0">
<HStack spacing="2">
<Icon as={TrendingUp} boxSize="16px" color="green.500" />
<Text fontWeight="600" color="green.600">
看涨 / Yes
</Text>
</HStack>
<Text fontSize="xs" color={forumColors.text.secondary}>
{topic.positions.yes.total_shares}份持仓
</Text>
</VStack>
</HStack>
<VStack align="end" spacing="0">
<Text fontSize="xl" fontWeight="bold" color="green.600">
{Math.round(topic.positions.yes.current_price)}
</Text>
<Text fontSize="xs" color={forumColors.text.secondary}>
积分/
</Text>
</VStack>
</Flex>
</Box>
</MotionBox>
{/* No 选项 */}
<MotionBox
whileHover={{ scale: 1.02 }}
whileTap={{ scale: 0.98 }}
>
<Box
bg={
selectedOption === 'no'
? 'linear-gradient(135deg, rgba(245, 101, 101, 0.2) 0%, rgba(245, 101, 101, 0.1) 100%)'
: forumColors.background.hover
}
border="2px solid"
borderColor={selectedOption === 'no' ? 'red.400' : forumColors.border.default}
borderRadius="lg"
p="4"
cursor="pointer"
onClick={() => setSelectedOption('no')}
>
<Flex justify="space-between" align="center">
<HStack spacing="3">
<Radio value="no" colorScheme="red" />
<VStack align="start" spacing="0">
<HStack spacing="2">
<Icon as={TrendingDown} boxSize="16px" color="red.500" />
<Text fontWeight="600" color="red.600">
看跌 / No
</Text>
</HStack>
<Text fontSize="xs" color={forumColors.text.secondary}>
{topic.positions.no.total_shares}份持仓
</Text>
</VStack>
</HStack>
<VStack align="end" spacing="0">
<Text fontSize="xl" fontWeight="bold" color="red.600">
{Math.round(topic.positions.no.current_price)}
</Text>
<Text fontSize="xs" color={forumColors.text.secondary}>
积分/
</Text>
</VStack>
</Flex>
</Box>
</MotionBox>
</Stack>
</RadioGroup>
</Box>
{/* 购买份额 */}
<Box>
<Flex justify="space-between" mb="3">
<Text fontSize="sm" fontWeight="600" color={forumColors.text.primary}>
{mode === 'buy' ? '购买份额' : '卖出份额'}
</Text>
<Text fontSize="sm" color={forumColors.text.secondary}>
{shares}
</Text>
</Flex>
<Slider
value={shares}
onChange={setShares}
min={1}
max={maxShares}
step={1}
focusThumbOnChange={false}
>
<SliderTrack bg={forumColors.background.hover}>
<SliderFilledTrack bg={forumColors.gradients.goldPrimary} />
</SliderTrack>
<SliderThumb boxSize="6" bg={forumColors.primary[500]}>
<Box as={Icon} as={DollarSign} boxSize="12px" color="white" />
</SliderThumb>
</Slider>
<HStack justify="space-between" mt="2" fontSize="xs" color={forumColors.text.tertiary}>
<Text>1</Text>
<Text>{maxShares} (最大)</Text>
</HStack>
{mode === 'sell' && userPosition && (
<Text fontSize="xs" color={forumColors.text.secondary} mt="2">
你的持仓{userPosition.shares} · 平均成本{Math.round(userPosition.avg_cost)}积分/
</Text>
)}
</Box>
{/* 费用明细 */}
<Box
bg={forumColors.gradients.goldSubtle}
border="1px solid"
borderColor={forumColors.border.gold}
borderRadius="lg"
p="4"
>
<VStack spacing="2" align="stretch">
<Flex justify="space-between" fontSize="sm">
<Text color={forumColors.text.secondary}>
{mode === 'buy' ? '购买成本' : '卖出收益'}
</Text>
<Text fontWeight="600" color={forumColors.text.primary}>
{Math.round(cost)} 积分
</Text>
</Flex>
<Flex justify="space-between" fontSize="sm">
<Text color={forumColors.text.secondary}>平均价格</Text>
<Text fontWeight="600" color={forumColors.text.primary}>
{Math.round(avgPrice)} 积分/
</Text>
</Flex>
<Flex justify="space-between" fontSize="sm">
<Text color={forumColors.text.secondary}>交易税 (2%)</Text>
<Text fontWeight="600" color={forumColors.text.primary}>
{Math.round(tax)} 积分
</Text>
</Flex>
<Box borderTop="1px solid" borderColor={forumColors.border.default} pt="2" mt="1">
<Flex justify="space-between">
<Text fontWeight="bold" color={forumColors.text.primary}>
{mode === 'buy' ? '总计' : '净收益'}
</Text>
<HStack spacing="1">
<Icon as={DollarSign} boxSize="20px" color={forumColors.primary[500]} />
<Text fontSize="2xl" fontWeight="bold" color={forumColors.primary[500]}>
{Math.round(totalCost)}
</Text>
<Text fontSize="sm" color={forumColors.text.secondary}>
积分
</Text>
</HStack>
</Flex>
</Box>
{/* 余额提示 */}
<Box borderTop="1px solid" borderColor={forumColors.border.default} pt="2">
<Flex justify="space-between" fontSize="sm">
<Text color={forumColors.text.secondary}>你的余额</Text>
<Text fontWeight="600" color={forumColors.text.primary}>
{userAccount.balance} 积分
</Text>
</Flex>
<Flex justify="space-between" fontSize="sm" mt="1">
<Text color={forumColors.text.secondary}>
{mode === 'buy' ? '交易后:' : '交易后:'}
</Text>
<Text
fontWeight="600"
color={
mode === 'buy'
? userAccount.balance >= totalCost
? forumColors.success[500]
: forumColors.error[500]
: forumColors.success[500]
}
>
{mode === 'buy'
? userAccount.balance - totalCost
: userAccount.balance + totalCost}{' '}
积分
</Text>
</Flex>
</Box>
</VStack>
</Box>
{/* 警告提示 */}
{!tradeCheck.ok && (
<Box
bg="red.50"
border="1px solid"
borderColor="red.200"
borderRadius="lg"
p="3"
>
<HStack spacing="2">
<Icon as={AlertCircle} boxSize="16px" color="red.500" />
<Text fontSize="sm" color="red.600" fontWeight="600">
{tradeCheck.reason}
</Text>
</HStack>
</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={mode === 'buy' ? forumColors.gradients.goldPrimary : 'red.500'}
color="white"
fontWeight="bold"
onClick={handleTrade}
isLoading={isSubmitting}
loadingText={mode === 'buy' ? '购买中...' : '卖出中...'}
isDisabled={!tradeCheck.ok}
_hover={{
opacity: 0.9,
transform: 'translateY(-2px)',
}}
_active={{ transform: 'translateY(0)' }}
>
{mode === 'buy' ? `购买 ${shares}` : `卖出 ${shares}`}
</Button>
</HStack>
</ModalFooter>
</ModalContent>
</Modal>
);
};
export default TradeModal;