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,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;