1447 lines
51 KiB
TypeScript
1447 lines
51 KiB
TypeScript
import React, { useState, useEffect } from 'react';
|
||
import { motion, AnimatePresence } from 'framer-motion';
|
||
import {
|
||
Box,
|
||
Button,
|
||
Flex,
|
||
Text,
|
||
Badge,
|
||
VStack,
|
||
HStack,
|
||
useToast,
|
||
Modal,
|
||
ModalOverlay,
|
||
ModalContent,
|
||
ModalHeader,
|
||
ModalBody,
|
||
ModalCloseButton,
|
||
useDisclosure,
|
||
Image,
|
||
Progress,
|
||
Divider,
|
||
Input,
|
||
Icon,
|
||
Container,
|
||
} from '@chakra-ui/react';
|
||
import {
|
||
FaWeixin,
|
||
FaGem,
|
||
FaCheck,
|
||
FaQrcode,
|
||
FaClock,
|
||
FaRedo,
|
||
FaCrown,
|
||
FaStar,
|
||
FaTimes,
|
||
FaChevronDown,
|
||
FaChevronUp,
|
||
} from 'react-icons/fa';
|
||
|
||
import { logger } from '../../utils/logger';
|
||
import { useAuth } from '../../contexts/AuthContext';
|
||
import { useSubscriptionEvents } from '../../hooks/useSubscriptionEvents';
|
||
import { subscriptionConfig, themeColors } from '../../views/Pages/Account/subscription-content';
|
||
|
||
export default function SubscriptionContentNew() {
|
||
const { user } = useAuth();
|
||
const subscriptionEvents = useSubscriptionEvents({
|
||
currentSubscription: {
|
||
plan: user?.subscription_plan || 'free',
|
||
status: user?.subscription_status || 'inactive',
|
||
},
|
||
});
|
||
|
||
const [selectedCycle, setSelectedCycle] = useState('monthly');
|
||
const [selectedPlan, setSelectedPlan] = useState(null);
|
||
const [subscriptionPlans, setSubscriptionPlans] = useState([]);
|
||
const [priceInfo, setPriceInfo] = useState(null);
|
||
const [loading, setLoading] = useState(false);
|
||
const [promoCode, setPromoCode] = useState('');
|
||
const [promoCodeApplied, setPromoCodeApplied] = useState(false);
|
||
const [promoCodeError, setPromoCodeError] = useState('');
|
||
const [validatingPromo, setValidatingPromo] = useState(false);
|
||
|
||
const [paymentOrder, setPaymentOrder] = useState(null);
|
||
const [paymentCountdown, setPaymentCountdown] = useState(300);
|
||
const [autoCheckInterval, setAutoCheckInterval] = useState(null);
|
||
const [forceUpdating, setForceUpdating] = useState(false);
|
||
|
||
const [openFaqIndex, setOpenFaqIndex] = useState(null);
|
||
|
||
const { isOpen, onOpen, onClose } = useDisclosure();
|
||
const toast = useToast();
|
||
|
||
// 倒计时更新
|
||
useEffect(() => {
|
||
let timer: any;
|
||
if (paymentCountdown > 0) {
|
||
timer = setInterval(() => {
|
||
setPaymentCountdown((prev) => {
|
||
if (prev <= 1) {
|
||
handlePaymentExpire();
|
||
return 0;
|
||
}
|
||
return prev - 1;
|
||
});
|
||
}, 1000);
|
||
}
|
||
return () => clearInterval(timer);
|
||
}, [paymentCountdown]);
|
||
|
||
// 组件卸载时清理定时器
|
||
useEffect(() => {
|
||
return () => {
|
||
stopAutoPaymentCheck();
|
||
};
|
||
}, []);
|
||
|
||
// 组件加载时获取套餐数据
|
||
useEffect(() => {
|
||
fetchSubscriptionPlans();
|
||
}, []);
|
||
|
||
const fetchSubscriptionPlans = async () => {
|
||
try {
|
||
logger.debug('SubscriptionContentNew', '正在获取订阅套餐');
|
||
const response = await fetch('/api/subscription/plans');
|
||
|
||
if (response.ok) {
|
||
const data = await response.json();
|
||
|
||
if (data.success && Array.isArray(data.data)) {
|
||
const validPlans = data.data.filter(
|
||
(plan: any) =>
|
||
plan &&
|
||
plan.name &&
|
||
typeof plan.monthly_price === 'number' &&
|
||
typeof plan.yearly_price === 'number'
|
||
);
|
||
logger.debug('SubscriptionContentNew', '套餐加载成功', {
|
||
status: response.status,
|
||
validPlansCount: validPlans.length,
|
||
});
|
||
setSubscriptionPlans(validPlans);
|
||
} else {
|
||
logger.warn('SubscriptionContentNew', '套餐数据格式异常', { data });
|
||
setSubscriptionPlans([]);
|
||
}
|
||
} else {
|
||
logger.error('SubscriptionContentNew', 'fetchSubscriptionPlans', new Error(`HTTP ${response.status}`));
|
||
setSubscriptionPlans([]);
|
||
}
|
||
} catch (error) {
|
||
logger.error('SubscriptionContentNew', 'fetchSubscriptionPlans', error);
|
||
setSubscriptionPlans([]);
|
||
}
|
||
};
|
||
|
||
const handlePaymentExpire = () => {
|
||
stopAutoPaymentCheck();
|
||
toast({
|
||
title: '支付二维码已过期',
|
||
description: '请重新创建订单',
|
||
status: 'warning',
|
||
duration: 3000,
|
||
isClosable: true,
|
||
});
|
||
};
|
||
|
||
const stopAutoPaymentCheck = () => {
|
||
if (autoCheckInterval) {
|
||
clearInterval(autoCheckInterval);
|
||
setAutoCheckInterval(null);
|
||
}
|
||
};
|
||
|
||
const formatTime = (seconds: number) => {
|
||
const minutes = Math.floor(seconds / 60);
|
||
const remainingSeconds = seconds % 60;
|
||
return `${minutes.toString().padStart(2, '0')}:${remainingSeconds.toString().padStart(2, '0')}`;
|
||
};
|
||
|
||
// 计算价格
|
||
const calculatePrice = async (plan: any, cycle: string, promoCodeValue: any = null) => {
|
||
try {
|
||
const validPromoCode = promoCodeValue && typeof promoCodeValue === 'string' && promoCodeValue.trim()
|
||
? promoCodeValue.trim()
|
||
: null;
|
||
|
||
const response = await fetch('/api/subscription/calculate-price', {
|
||
method: 'POST',
|
||
headers: {
|
||
'Content-Type': 'application/json',
|
||
},
|
||
credentials: 'include',
|
||
body: JSON.stringify({
|
||
to_plan: plan.name,
|
||
to_cycle: cycle,
|
||
promo_code: validPromoCode,
|
||
}),
|
||
});
|
||
|
||
if (response.ok) {
|
||
const data = await response.json();
|
||
if (data.success) {
|
||
setPriceInfo(data.data);
|
||
return data.data;
|
||
}
|
||
}
|
||
return null;
|
||
} catch (error) {
|
||
logger.error('SubscriptionContent', 'calculatePrice', error);
|
||
return null;
|
||
}
|
||
};
|
||
|
||
// 验证优惠码
|
||
const handleValidatePromoCode = async () => {
|
||
const trimmedCode = promoCode.trim();
|
||
if (!trimmedCode) {
|
||
setPromoCodeError('请输入优惠码');
|
||
return;
|
||
}
|
||
if (!selectedPlan) {
|
||
setPromoCodeError('请先选择套餐');
|
||
return;
|
||
}
|
||
|
||
setValidatingPromo(true);
|
||
setPromoCodeError('');
|
||
|
||
try {
|
||
const result = await calculatePrice(selectedPlan, selectedCycle, trimmedCode);
|
||
if (result && !result.promo_error) {
|
||
setPromoCodeApplied(true);
|
||
toast({
|
||
title: '优惠码已应用',
|
||
description: `节省 ¥${result.discount_amount.toFixed(2)}`,
|
||
status: 'success',
|
||
duration: 3000,
|
||
isClosable: true,
|
||
});
|
||
} else {
|
||
setPromoCodeError(result?.promo_error || '优惠码无效');
|
||
setPromoCodeApplied(false);
|
||
}
|
||
} catch (error) {
|
||
setPromoCodeError('验证失败,请重试');
|
||
setPromoCodeApplied(false);
|
||
} finally {
|
||
setValidatingPromo(false);
|
||
}
|
||
};
|
||
|
||
const handleRemovePromoCode = async () => {
|
||
setPromoCode('');
|
||
setPromoCodeApplied(false);
|
||
setPromoCodeError('');
|
||
if (selectedPlan) {
|
||
await calculatePrice(selectedPlan, selectedCycle, null);
|
||
}
|
||
};
|
||
|
||
const handleSubscribe = async (plan: any) => {
|
||
if (!user) {
|
||
toast({
|
||
title: '请先登录',
|
||
description: '登录后即可订阅',
|
||
status: 'warning',
|
||
duration: 3000,
|
||
isClosable: true,
|
||
});
|
||
return;
|
||
}
|
||
|
||
subscriptionEvents.trackPricingPlanSelected(
|
||
plan.name,
|
||
selectedCycle,
|
||
getCurrentPrice(plan)
|
||
);
|
||
|
||
setSelectedPlan(plan);
|
||
await calculatePrice(plan, selectedCycle, promoCodeApplied ? promoCode : null);
|
||
onOpen();
|
||
};
|
||
|
||
const handleCreatePaymentOrder = async () => {
|
||
if (!selectedPlan || !user) return;
|
||
|
||
setLoading(true);
|
||
try {
|
||
const price = priceInfo?.final_amount || getCurrentPrice(selectedPlan);
|
||
|
||
// 检查是否为免费升级(剩余价值足够抵扣新套餐价格)
|
||
if (price === 0 && priceInfo?.is_upgrade) {
|
||
const response = await fetch('/api/subscription/free-upgrade', {
|
||
method: 'POST',
|
||
headers: {
|
||
'Content-Type': 'application/json',
|
||
},
|
||
credentials: 'include',
|
||
body: JSON.stringify({
|
||
plan_name: selectedPlan.name,
|
||
billing_cycle: selectedCycle,
|
||
}),
|
||
});
|
||
|
||
const data = await response.json();
|
||
if (data.success) {
|
||
subscriptionEvents.trackPaymentSuccessful({
|
||
planName: selectedPlan.name,
|
||
paymentMethod: 'free_upgrade',
|
||
amount: 0,
|
||
orderId: 'free_upgrade',
|
||
transactionId: 'free_upgrade',
|
||
});
|
||
|
||
toast({
|
||
title: '升级成功!',
|
||
description: data.message,
|
||
status: 'success',
|
||
duration: 5000,
|
||
isClosable: true,
|
||
});
|
||
|
||
onClose();
|
||
setTimeout(() => window.location.reload(), 2000);
|
||
return;
|
||
} else {
|
||
throw new Error(data.error || '免费升级失败');
|
||
}
|
||
}
|
||
|
||
subscriptionEvents.trackPaymentInitiated({
|
||
planName: selectedPlan.name,
|
||
paymentMethod: 'wechat_pay',
|
||
amount: price,
|
||
billingCycle: selectedCycle,
|
||
});
|
||
|
||
const response = await fetch('/api/payment/create-order', {
|
||
method: 'POST',
|
||
headers: {
|
||
'Content-Type': 'application/json',
|
||
},
|
||
credentials: 'include',
|
||
body: JSON.stringify({
|
||
plan_name: selectedPlan.name,
|
||
billing_cycle: selectedCycle,
|
||
promo_code: promoCodeApplied ? promoCode : null,
|
||
}),
|
||
});
|
||
|
||
if (response.ok) {
|
||
const data = await response.json();
|
||
if (data.success) {
|
||
setPaymentOrder(data.data);
|
||
setPaymentCountdown(30 * 60); // 30分钟
|
||
startAutoPaymentCheck(data.data.id);
|
||
|
||
toast({
|
||
title: '订单创建成功',
|
||
description: '请使用微信扫描二维码完成支付',
|
||
status: 'success',
|
||
duration: 3000,
|
||
isClosable: true,
|
||
});
|
||
} else {
|
||
throw new Error(data.message || '创建订单失败');
|
||
}
|
||
} else {
|
||
throw new Error('网络请求失败');
|
||
}
|
||
} catch (error: any) {
|
||
subscriptionEvents.trackPaymentFailed(
|
||
{
|
||
planName: selectedPlan.name,
|
||
paymentMethod: 'wechat_pay',
|
||
amount: getCurrentPrice(selectedPlan),
|
||
},
|
||
error.message
|
||
);
|
||
|
||
toast({
|
||
title: '创建订单失败',
|
||
description: error.message,
|
||
status: 'error',
|
||
duration: 3000,
|
||
isClosable: true,
|
||
});
|
||
} finally {
|
||
setLoading(false);
|
||
}
|
||
};
|
||
|
||
const startAutoPaymentCheck = (orderId: string) => {
|
||
const checkInterval = setInterval(async () => {
|
||
try {
|
||
const response = await fetch(`/api/payment/order/${orderId}/status`, {
|
||
credentials: 'include',
|
||
});
|
||
|
||
if (response.ok) {
|
||
const data = await response.json();
|
||
if (data.success && data.data.status === 'paid') {
|
||
clearInterval(checkInterval);
|
||
setAutoCheckInterval(null);
|
||
|
||
subscriptionEvents.trackPaymentSuccessful({
|
||
planName: selectedPlan?.name,
|
||
paymentMethod: 'wechat_pay',
|
||
amount: paymentOrder?.amount,
|
||
orderId: orderId,
|
||
transactionId: data.data.transaction_id,
|
||
});
|
||
|
||
toast({
|
||
title: '支付成功!',
|
||
description: '您的订阅已激活',
|
||
status: 'success',
|
||
duration: 5000,
|
||
isClosable: true,
|
||
});
|
||
|
||
onClose();
|
||
setTimeout(() => window.location.reload(), 2000);
|
||
}
|
||
}
|
||
} catch (error) {
|
||
logger.error('SubscriptionContent', 'checkPaymentStatus', error);
|
||
}
|
||
}, 3000);
|
||
|
||
setAutoCheckInterval(checkInterval as any);
|
||
};
|
||
|
||
const handleForceUpdate = async () => {
|
||
if (!paymentOrder) return;
|
||
|
||
setForceUpdating(true);
|
||
try {
|
||
const response = await fetch(`/api/payment/order/${paymentOrder.order_id}/status`, {
|
||
credentials: 'include',
|
||
});
|
||
|
||
if (response.ok) {
|
||
const data = await response.json();
|
||
if (data.success && data.data.status === 'paid') {
|
||
toast({
|
||
title: '支付成功!',
|
||
description: '您的订阅已激活',
|
||
status: 'success',
|
||
duration: 5000,
|
||
isClosable: true,
|
||
});
|
||
|
||
onClose();
|
||
setTimeout(() => window.location.reload(), 2000);
|
||
} else {
|
||
toast({
|
||
title: '未检测到支付',
|
||
description: '请确认已完成支付后重试',
|
||
status: 'info',
|
||
duration: 3000,
|
||
isClosable: true,
|
||
});
|
||
}
|
||
}
|
||
} catch (error: any) {
|
||
toast({
|
||
title: '查询失败',
|
||
description: error.message,
|
||
status: 'error',
|
||
duration: 3000,
|
||
isClosable: true,
|
||
});
|
||
} finally {
|
||
setForceUpdating(false);
|
||
}
|
||
};
|
||
|
||
// 合并数据库数据和前端配置
|
||
const getMergedPlans = () => {
|
||
// 如果数据库还没有加载数据,使用静态配置
|
||
if (subscriptionPlans.length === 0) {
|
||
return subscriptionConfig.plans;
|
||
}
|
||
|
||
// 合并数据库价格和前端UI配置
|
||
return subscriptionConfig.plans.map((configPlan: any) => {
|
||
const dbPlan = subscriptionPlans.find((p: any) => p.name === configPlan.name);
|
||
|
||
if (!dbPlan) {
|
||
return configPlan; // 如果数据库中没有,使用前端配置
|
||
}
|
||
|
||
// 解析数据库中的 pricing_options JSON
|
||
let pricingOptions = configPlan.pricingOptions;
|
||
if (dbPlan.pricing_options) {
|
||
try {
|
||
const parsedOptions = typeof dbPlan.pricing_options === 'string'
|
||
? JSON.parse(dbPlan.pricing_options)
|
||
: dbPlan.pricing_options;
|
||
|
||
if (Array.isArray(parsedOptions) && parsedOptions.length > 0) {
|
||
pricingOptions = parsedOptions.map((opt: any) => ({
|
||
cycleKey: opt.cycle_key,
|
||
label: opt.label,
|
||
months: opt.months,
|
||
price: parseFloat(opt.price),
|
||
originalPrice: opt.original_price ? parseFloat(opt.original_price) : null,
|
||
discountPercent: opt.discount_percent || 0,
|
||
}));
|
||
}
|
||
} catch (error) {
|
||
logger.error('SubscriptionContentNew', '解析pricing_options失败', error);
|
||
}
|
||
}
|
||
|
||
// 合并数据,数据库价格优先
|
||
return {
|
||
...configPlan,
|
||
monthly_price: dbPlan.monthly_price,
|
||
yearly_price: dbPlan.yearly_price,
|
||
pricingOptions: pricingOptions,
|
||
displayName: dbPlan.display_name || configPlan.displayName,
|
||
description: dbPlan.description || configPlan.description,
|
||
};
|
||
});
|
||
};
|
||
|
||
const getCurrentPrice = (plan: any) => {
|
||
if (!plan || plan.name === 'free') return 0;
|
||
|
||
const option = plan.pricingOptions?.find(
|
||
(opt: any) => opt.cycleKey === selectedCycle
|
||
);
|
||
return option ? option.price : plan.pricingOptions?.[0]?.price || 0;
|
||
};
|
||
|
||
const getCurrentPriceOption = (plan: any) => {
|
||
if (!plan || plan.name === 'free') return null;
|
||
return plan.pricingOptions?.find((opt: any) => opt.cycleKey === selectedCycle);
|
||
};
|
||
|
||
const getIconComponent = (iconName: string) => {
|
||
const icons: any = {
|
||
star: FaStar,
|
||
gem: FaGem,
|
||
crown: FaCrown,
|
||
};
|
||
return icons[iconName] || FaStar;
|
||
};
|
||
|
||
// 获取按钮文字
|
||
const getButtonText = (plan: any) => {
|
||
const currentPlanName = user?.subscription_type;
|
||
const isActive = user?.subscription_status === 'active';
|
||
|
||
if (!isActive || !currentPlanName || currentPlanName === 'free') {
|
||
return `选择${plan.displayName}`;
|
||
}
|
||
|
||
if (currentPlanName === plan.name) {
|
||
// 同级续费
|
||
return `续费${plan.displayName}`;
|
||
}
|
||
|
||
// 升级或降级
|
||
if (currentPlanName === 'pro' && plan.name === 'max') {
|
||
return `升级为${plan.displayName}`;
|
||
}
|
||
|
||
if (currentPlanName === 'max' && plan.name === 'pro') {
|
||
return `到期后切换到${plan.displayName}`;
|
||
}
|
||
|
||
return `选择${plan.displayName}`;
|
||
};
|
||
|
||
// 判断按钮是否可点击
|
||
const isButtonDisabled = (plan: any) => {
|
||
return false; // 所有套餐都可以选择,包括当前套餐(续费)
|
||
};
|
||
|
||
return (
|
||
<Box
|
||
minH="100vh"
|
||
bg="#0a0a0a"
|
||
pt={{ base: 8, md: 16 }}
|
||
pb={{ base: 16, md: 24 }}
|
||
position="relative"
|
||
overflow="hidden"
|
||
>
|
||
{/* 背景光晕 */}
|
||
<Box
|
||
position="absolute"
|
||
top="20%"
|
||
left="10%"
|
||
w="400px"
|
||
h="400px"
|
||
bg="rgba(212, 175, 55, 0.15)"
|
||
borderRadius="full"
|
||
filter="blur(100px)"
|
||
pointerEvents="none"
|
||
zIndex={0}
|
||
/>
|
||
<Box
|
||
position="absolute"
|
||
bottom="10%"
|
||
right="10%"
|
||
w="500px"
|
||
h="500px"
|
||
bg="rgba(212, 175, 55, 0.1)"
|
||
borderRadius="full"
|
||
filter="blur(120px)"
|
||
pointerEvents="none"
|
||
zIndex={0}
|
||
/>
|
||
|
||
<Container maxW="1400px" px={{ base: 4, md: 6 }} position="relative" zIndex={1}>
|
||
{/* 标题区域 */}
|
||
<VStack spacing={4} mb={12} textAlign="center">
|
||
<motion.div
|
||
initial={{ opacity: 0, y: -20 }}
|
||
animate={{ opacity: 1, y: 0 }}
|
||
transition={{ duration: 0.6 }}
|
||
>
|
||
<Text
|
||
fontSize="sm"
|
||
color="rgba(212, 175, 55, 0.8)"
|
||
fontWeight="medium"
|
||
letterSpacing="wider"
|
||
textTransform="uppercase"
|
||
>
|
||
订阅方案
|
||
</Text>
|
||
</motion.div>
|
||
|
||
<motion.div
|
||
initial={{ opacity: 0, y: -20 }}
|
||
animate={{ opacity: 1, y: 0 }}
|
||
transition={{ duration: 0.6, delay: 0.1 }}
|
||
>
|
||
<Text
|
||
fontSize={{ base: '3xl', md: '5xl', lg: '6xl' }}
|
||
fontWeight="bold"
|
||
bgGradient="radial-gradient(circle at center, #FFFFFF 0%, rgba(255,255,255,0.6) 100%)"
|
||
bgClip="text"
|
||
lineHeight="1.2"
|
||
>
|
||
立即开启智能决策
|
||
</Text>
|
||
</motion.div>
|
||
</VStack>
|
||
|
||
{/* 当前订阅状态 */}
|
||
{user && user.subscription_type && user.subscription_type !== 'free' && user.subscription_status === 'active' && (
|
||
<motion.div
|
||
initial={{ opacity: 0, scale: 0.95 }}
|
||
animate={{ opacity: 1, scale: 1 }}
|
||
transition={{ duration: 0.5, delay: 0.2 }}
|
||
>
|
||
<Box
|
||
mb={12}
|
||
p={6}
|
||
borderRadius="2xl"
|
||
bg="rgba(212, 175, 55, 0.05)"
|
||
border="2px solid"
|
||
borderColor="rgba(212, 175, 55, 0.3)"
|
||
backdropFilter="blur(20px)"
|
||
maxW="600px"
|
||
mx="auto"
|
||
position="relative"
|
||
overflow="hidden"
|
||
_before={{
|
||
content: '""',
|
||
position: 'absolute',
|
||
top: 0,
|
||
left: 0,
|
||
right: 0,
|
||
height: '4px',
|
||
bgGradient: 'linear-gradient(90deg, rgba(212, 175, 55, 0.5), rgba(212, 175, 55, 1), rgba(212, 175, 55, 0.5))',
|
||
}}
|
||
>
|
||
<VStack spacing={4} align="stretch">
|
||
<HStack justify="space-between" align="center">
|
||
<HStack spacing={3}>
|
||
<Icon
|
||
as={user.subscription_type === 'max' ? FaCrown : FaGem}
|
||
color="#D4AF37"
|
||
boxSize={6}
|
||
/>
|
||
<VStack align="start" spacing={0}>
|
||
<Text color="white" fontSize="lg" fontWeight="bold">
|
||
当前订阅: {user.subscription_type === 'max' ? 'Max 旗舰版' : 'Pro 专业版'}
|
||
</Text>
|
||
{user.billing_cycle && (
|
||
<Text color="rgba(255, 255, 255, 0.6)" fontSize="xs">
|
||
{user.billing_cycle === 'monthly' ? '月付' :
|
||
user.billing_cycle === 'quarterly' ? '季付' :
|
||
user.billing_cycle === 'semiannual' ? '半年付' :
|
||
user.billing_cycle === 'yearly' ? '年付' : user.billing_cycle}
|
||
</Text>
|
||
)}
|
||
</VStack>
|
||
</HStack>
|
||
<Badge
|
||
px={3}
|
||
py={1}
|
||
borderRadius="full"
|
||
bg="rgba(76, 175, 80, 0.2)"
|
||
border="1px solid rgba(76, 175, 80, 0.4)"
|
||
color="green.300"
|
||
fontSize="xs"
|
||
fontWeight="medium"
|
||
>
|
||
使用中
|
||
</Badge>
|
||
</HStack>
|
||
|
||
<Divider borderColor="rgba(212, 175, 55, 0.2)" />
|
||
|
||
<Flex justify="space-between" align="center">
|
||
<HStack spacing={2}>
|
||
<Icon as={FaClock} color="rgba(212, 175, 55, 0.8)" boxSize={4} />
|
||
<Text color="rgba(255, 255, 255, 0.7)" fontSize="sm">
|
||
到期时间
|
||
</Text>
|
||
</HStack>
|
||
<Text color="#D4AF37" fontSize="md" fontWeight="bold">
|
||
{user.subscription_end_date
|
||
? new Date(user.subscription_end_date).toLocaleDateString('zh-CN', {
|
||
year: 'numeric',
|
||
month: 'long',
|
||
day: 'numeric'
|
||
})
|
||
: '永久有效'
|
||
}
|
||
</Text>
|
||
</Flex>
|
||
|
||
{user.subscription_end_date && (() => {
|
||
const endDate = new Date(user.subscription_end_date);
|
||
const today = new Date();
|
||
const daysLeft = Math.ceil((endDate.getTime() - today.getTime()) / (1000 * 60 * 60 * 24));
|
||
|
||
if (daysLeft > 0 && daysLeft <= 30) {
|
||
return (
|
||
<Box
|
||
p={2}
|
||
borderRadius="md"
|
||
bg="rgba(255, 165, 0, 0.1)"
|
||
border="1px solid rgba(255, 165, 0, 0.3)"
|
||
>
|
||
<Text color="orange.300" fontSize="xs" textAlign="center">
|
||
⚠️ 还有 {daysLeft} 天到期,记得及时续费哦
|
||
</Text>
|
||
</Box>
|
||
);
|
||
}
|
||
return null;
|
||
})()}
|
||
</VStack>
|
||
</Box>
|
||
</motion.div>
|
||
)}
|
||
|
||
{/* 计费周期选择器 */}
|
||
<VStack spacing={6} mb={16}>
|
||
<Text fontSize="md" color="rgba(255, 255, 255, 0.7)">
|
||
选择计费周期 · 时长越长优惠越大
|
||
</Text>
|
||
|
||
<HStack
|
||
spacing={3}
|
||
p={2}
|
||
bg="rgba(255, 255, 255, 0.03)"
|
||
borderRadius="xl"
|
||
border="1px solid"
|
||
borderColor="rgba(255, 255, 255, 0.1)"
|
||
backdropFilter="blur(10px)"
|
||
flexWrap="wrap"
|
||
justify="center"
|
||
>
|
||
{getMergedPlans()[1]?.pricingOptions?.map((option: any, index: number) => (
|
||
<Box key={index} position="relative">
|
||
{option.discountPercent > 0 && (
|
||
<Badge
|
||
position="absolute"
|
||
top="-10px"
|
||
right="-10px"
|
||
colorScheme="red"
|
||
fontSize="xs"
|
||
px={2}
|
||
py={1}
|
||
borderRadius="full"
|
||
fontWeight="bold"
|
||
zIndex={1}
|
||
>
|
||
省{option.discountPercent}%
|
||
</Badge>
|
||
)}
|
||
|
||
<Button
|
||
size="lg"
|
||
px={6}
|
||
py={6}
|
||
borderRadius="lg"
|
||
bg={selectedCycle === option.cycleKey ? 'linear-gradient(135deg, #D4AF37, #B8941F)' : 'transparent'}
|
||
color={selectedCycle === option.cycleKey ? '#000' : '#fff'}
|
||
border="1px solid"
|
||
borderColor={selectedCycle === option.cycleKey ? 'rgba(212, 175, 55, 0.3)' : 'rgba(255, 255, 255, 0.1)'}
|
||
onClick={() => setSelectedCycle(option.cycleKey)}
|
||
_hover={{
|
||
transform: 'translateY(-2px)',
|
||
borderColor: 'rgba(212, 175, 55, 0.5)',
|
||
shadow: selectedCycle === option.cycleKey
|
||
? '0 0 20px rgba(212, 175, 55, 0.3)'
|
||
: '0 4px 12px rgba(0, 0, 0, 0.5)',
|
||
}}
|
||
transition="all 0.3s"
|
||
fontWeight="bold"
|
||
>
|
||
{option.label}
|
||
</Button>
|
||
</Box>
|
||
))}
|
||
</HStack>
|
||
|
||
{(() => {
|
||
const currentOption = getMergedPlans()[1]?.pricingOptions?.find(
|
||
(opt: any) => opt.cycleKey === selectedCycle
|
||
);
|
||
if (currentOption && currentOption.discountPercent > 0) {
|
||
return (
|
||
<HStack spacing={2}>
|
||
<Icon as={FaStar} color="#D4AF37" boxSize={4} />
|
||
<Text fontSize="sm" color="#D4AF37" fontWeight="medium">
|
||
当前选择可节省 {currentOption.discountPercent}% 的费用
|
||
</Text>
|
||
</HStack>
|
||
);
|
||
}
|
||
return null;
|
||
})()}
|
||
</VStack>
|
||
|
||
{/* 套餐卡片 - 借鉴 index.pug 设计 */}
|
||
<Flex
|
||
justify="center"
|
||
gap={4}
|
||
mb={16}
|
||
flexWrap={{ base: 'wrap', lg: 'nowrap' }}
|
||
maxW="1200px"
|
||
mx="auto"
|
||
>
|
||
{getMergedPlans().slice(1).map((plan: any, index: number) => {
|
||
const IconComponent = getIconComponent(plan.icon);
|
||
const currentPriceOption = getCurrentPriceOption(plan);
|
||
const isCurrentPlan =
|
||
user?.subscription_type === plan.name &&
|
||
user?.subscription_status === 'active';
|
||
|
||
const isPremium = plan.name === 'max';
|
||
|
||
return (
|
||
<motion.div
|
||
key={plan.name}
|
||
initial={{ opacity: 0, y: 50 }}
|
||
animate={{ opacity: 1, y: 0 }}
|
||
transition={{ delay: index * 0.1, duration: 0.6 }}
|
||
style={{ flex: 1, maxWidth: '500px', width: '100%' }}
|
||
>
|
||
<Box
|
||
position="relative"
|
||
h="100%"
|
||
borderRadius="20px"
|
||
overflow="hidden"
|
||
bg={isPremium ? 'rgba(10, 10, 10, 0.5)' : 'rgba(10, 10, 10, 0.3)'}
|
||
border={isPremium ? '1px solid rgba(212, 175, 55, 0.3)' : '1px solid rgba(255, 255, 255, 0.1)'}
|
||
boxShadow={isPremium ? '0 20px 60px rgba(212, 175, 55, 0.2)' : '0 20px 60px rgba(0, 0, 0, 0.3)'}
|
||
transition="all 0.4s cubic-bezier(0.4, 0, 0.2, 1)"
|
||
_hover={{
|
||
transform: 'translateY(-8px)',
|
||
borderColor: isPremium ? 'rgba(212, 175, 55, 0.5)' : 'rgba(255, 255, 255, 0.2)',
|
||
boxShadow: isPremium
|
||
? '0 30px 80px rgba(212, 175, 55, 0.3)'
|
||
: '0 30px 80px rgba(0, 0, 0, 0.5)',
|
||
}}
|
||
_before={
|
||
isPremium
|
||
? {
|
||
content: '""',
|
||
position: 'absolute',
|
||
top: '50%',
|
||
left: '50%',
|
||
transform: 'translate(-50%, -50%)',
|
||
width: '120%',
|
||
height: '120%',
|
||
background: 'radial-gradient(circle at center, rgba(212, 175, 55, 0.1) 0%, transparent 70%)',
|
||
pointerEvents: 'none',
|
||
zIndex: 0,
|
||
}
|
||
: {}
|
||
}
|
||
_after={{
|
||
content: '""',
|
||
position: 'absolute',
|
||
inset: 0,
|
||
borderRadius: '20px',
|
||
border: '1px solid',
|
||
borderColor: 'rgba(255, 255, 255, 0.1)',
|
||
pointerEvents: 'none',
|
||
}}
|
||
>
|
||
{/* 套餐标题 */}
|
||
<Box
|
||
position="relative"
|
||
zIndex={2}
|
||
py={3}
|
||
px={8.5}
|
||
fontSize="lg"
|
||
fontWeight="bold"
|
||
bgGradient={
|
||
isPremium
|
||
? 'linear-gradient(to right, rgba(212, 175, 55, 0.2), rgba(212, 175, 55, 0.2))'
|
||
: 'linear-gradient(to right, rgba(255, 255, 255, 0.05), rgba(255, 255, 255, 0.05))'
|
||
}
|
||
borderTopRadius="20px"
|
||
color={isPremium ? '#D4AF37' : 'white'}
|
||
>
|
||
{plan.displayName}
|
||
</Box>
|
||
|
||
<VStack
|
||
spacing={0}
|
||
align="stretch"
|
||
position="relative"
|
||
zIndex={3}
|
||
flex={1}
|
||
mt={-1}
|
||
p={3.5}
|
||
pb={8}
|
||
backdropFilter={isPremium ? 'blur(32px)' : 'blur(20px)'}
|
||
bg={isPremium ? 'rgba(255, 255, 255, 0.07)' : 'rgba(255, 255, 255, 0.01)'}
|
||
borderRadius="20px"
|
||
_after={{
|
||
content: '""',
|
||
position: 'absolute',
|
||
inset: 0,
|
||
borderRadius: '20px',
|
||
border: '1px solid',
|
||
borderColor: 'rgba(255, 255, 255, 0.1)',
|
||
pointerEvents: 'none',
|
||
}}
|
||
>
|
||
{/* 价格卡片 */}
|
||
<Box
|
||
position="relative"
|
||
mb={6}
|
||
p={6}
|
||
borderRadius="16px"
|
||
bg={isPremium ? 'rgba(212, 175, 55, 0.1)' : 'rgba(255, 255, 255, 0.03)'}
|
||
backdropFilter="blur(20px)"
|
||
boxShadow="0 8px 32px rgba(0, 0, 0, 0.4)"
|
||
_after={{
|
||
content: '""',
|
||
position: 'absolute',
|
||
inset: 0,
|
||
borderRadius: '16px',
|
||
border: '1px solid',
|
||
borderColor: isPremium ? 'rgba(212, 175, 55, 0.2)' : 'rgba(255, 255, 255, 0.1)',
|
||
pointerEvents: 'none',
|
||
}}
|
||
>
|
||
<Flex align="baseline" justify="center" mb={5}>
|
||
<Text
|
||
fontSize="5xl"
|
||
fontWeight="bold"
|
||
bgGradient="radial-gradient(circle at center, #FFFFFF 0%, rgba(255,255,255,0.6) 100%)"
|
||
bgClip="text"
|
||
lineHeight="1"
|
||
letterSpacing="-0.02em"
|
||
>
|
||
¥{getCurrentPrice(plan)}
|
||
</Text>
|
||
<Text fontSize="lg" color="rgba(255, 255, 255, 0.6)" ml={2}>
|
||
/ {currentPriceOption?.label || '月'}
|
||
</Text>
|
||
</Flex>
|
||
|
||
<Button
|
||
w="full"
|
||
size="lg"
|
||
h="56px"
|
||
bg={
|
||
isPremium
|
||
? 'linear-gradient(135deg, #D4AF37 0%, #B8941F 100%)'
|
||
: 'rgba(255, 255, 255, 0.05)'
|
||
}
|
||
color={
|
||
isPremium
|
||
? '#000'
|
||
: '#fff'
|
||
}
|
||
border={
|
||
isPremium
|
||
? 'none'
|
||
: '1px solid rgba(255, 255, 255, 0.1)'
|
||
}
|
||
fontWeight="bold"
|
||
fontSize="md"
|
||
borderRadius="lg"
|
||
onClick={() => handleSubscribe(plan)}
|
||
isDisabled={isButtonDisabled(plan)}
|
||
cursor="pointer"
|
||
_hover={{
|
||
transform: 'translateY(-2px)',
|
||
shadow: isPremium
|
||
? '0 8px 30px rgba(212, 175, 55, 0.4)'
|
||
: '0 8px 20px rgba(255, 255, 255, 0.1)',
|
||
bg: isPremium
|
||
? 'linear-gradient(135deg, #E5C047 0%, #C9A52F 100%)'
|
||
: 'rgba(255, 255, 255, 0.08)',
|
||
}}
|
||
_active={{
|
||
transform: 'translateY(0)',
|
||
}}
|
||
transition="all 0.3s cubic-bezier(0.4, 0, 0.2, 1)"
|
||
>
|
||
{getButtonText(plan)}
|
||
</Button>
|
||
</Box>
|
||
|
||
{/* 功能列表 */}
|
||
<VStack spacing={4} align="stretch" px={2}>
|
||
{plan.features.map((feature: any, idx: number) => (
|
||
<Flex key={idx} align="start" gap={3}>
|
||
<Flex
|
||
justify="center"
|
||
align="center"
|
||
flexShrink={0}
|
||
w={5}
|
||
h={5}
|
||
mt={0.5}
|
||
bg={feature.enabled ? (isPremium ? '#D4AF37' : '#00ff88') : 'transparent'}
|
||
borderRadius="full"
|
||
boxShadow={
|
||
feature.enabled
|
||
? isPremium
|
||
? '0 0 0 1px rgba(212,175,55,0.30) inset, 0 0 10px rgba(212,175,55,0.50) inset'
|
||
: '0 0 0 1px rgba(255,255,255,0.20) inset, 0 0 10px rgba(255,255,255,0.50) inset'
|
||
: 'none'
|
||
}
|
||
>
|
||
<Icon
|
||
as={feature.enabled ? FaCheck : FaTimes}
|
||
color={feature.enabled ? '#000' : 'rgba(255, 255, 255, 0.3)'}
|
||
boxSize={3}
|
||
/>
|
||
</Flex>
|
||
<Text
|
||
fontSize="sm"
|
||
color={feature.enabled ? 'rgba(255, 255, 255, 0.85)' : 'rgba(255, 255, 255, 0.35)'}
|
||
flex={1}
|
||
fontWeight={feature.enabled && isPremium && idx === 0 ? 'semibold' : 'normal'}
|
||
lineHeight="1.6"
|
||
>
|
||
{feature.name}
|
||
{feature.limit && (
|
||
<Text as="span" fontSize="xs" color={isPremium ? '#E5C047' : '#00ff88'} ml={1.5} fontWeight="medium">
|
||
({feature.limit})
|
||
</Text>
|
||
)}
|
||
</Text>
|
||
</Flex>
|
||
))}
|
||
</VStack>
|
||
</VStack>
|
||
</Box>
|
||
</motion.div>
|
||
);
|
||
})}
|
||
</Flex>
|
||
|
||
{/* FAQ 区域 */}
|
||
<motion.div
|
||
initial={{ opacity: 0 }}
|
||
animate={{ opacity: 1 }}
|
||
transition={{ duration: 0.6 }}
|
||
>
|
||
<Box
|
||
p={8}
|
||
borderRadius="2xl"
|
||
bg="rgba(30, 30, 30, 0.5)"
|
||
backdropFilter="blur(20px)"
|
||
border="1px solid"
|
||
borderColor="rgba(255, 255, 255, 0.1)"
|
||
maxW="900px"
|
||
mx="auto"
|
||
>
|
||
<Text fontSize="2xl" fontWeight="bold" color="white" mb={6} textAlign="center">
|
||
常见问题
|
||
</Text>
|
||
|
||
<VStack spacing={4} align="stretch">
|
||
{subscriptionConfig.faqs.map((faq: any, index: number) => (
|
||
<Box
|
||
key={index}
|
||
borderRadius="lg"
|
||
bg="rgba(255, 255, 255, 0.03)"
|
||
border="1px solid"
|
||
borderColor="rgba(255, 255, 255, 0.1)"
|
||
overflow="hidden"
|
||
transition="all 0.3s"
|
||
_hover={{
|
||
bg: 'rgba(255, 255, 255, 0.05)',
|
||
borderColor: 'rgba(212, 175, 55, 0.3)',
|
||
}}
|
||
>
|
||
<Flex
|
||
p={5}
|
||
justify="space-between"
|
||
align="center"
|
||
cursor="pointer"
|
||
onClick={() => setOpenFaqIndex(openFaqIndex === index ? null : index)}
|
||
>
|
||
<Text fontSize="md" fontWeight="medium" color="white">
|
||
{faq.question}
|
||
</Text>
|
||
<Icon
|
||
as={openFaqIndex === index ? FaChevronUp : FaChevronDown}
|
||
color="#D4AF37"
|
||
boxSize={5}
|
||
/>
|
||
</Flex>
|
||
|
||
<AnimatePresence>
|
||
{openFaqIndex === index && (
|
||
<motion.div
|
||
initial={{ height: 0, opacity: 0 }}
|
||
animate={{ height: 'auto', opacity: 1 }}
|
||
exit={{ height: 0, opacity: 0 }}
|
||
transition={{ duration: 0.3 }}
|
||
>
|
||
<Box p={5} pt={0} color="rgba(255, 255, 255, 0.7)" fontSize="sm">
|
||
{faq.answer.split('\n').map((line: string, idx: number) => (
|
||
<Text key={idx} mb={line.startsWith('•') ? 1 : 2}>
|
||
{line}
|
||
</Text>
|
||
))}
|
||
</Box>
|
||
</motion.div>
|
||
)}
|
||
</AnimatePresence>
|
||
</Box>
|
||
))}
|
||
</VStack>
|
||
</Box>
|
||
</motion.div>
|
||
</Container>
|
||
|
||
{/* 支付模态框 */}
|
||
<Modal isOpen={isOpen} onClose={onClose} size="lg" isCentered>
|
||
<ModalOverlay bg="rgba(0, 0, 0, 0.8)" backdropFilter="blur(10px)" />
|
||
<ModalContent bg="#1e1e1e" borderRadius="2xl" border="1px solid rgba(255, 255, 255, 0.1)">
|
||
<ModalHeader color="white">微信支付</ModalHeader>
|
||
<ModalCloseButton color="white" />
|
||
<ModalBody pb={6}>
|
||
{!paymentOrder ? (
|
||
<VStack spacing={6} align="stretch">
|
||
{/* 订阅类型提示 */}
|
||
{selectedPlan && priceInfo && (
|
||
<>
|
||
{priceInfo.is_upgrade && (
|
||
<Box
|
||
p={3}
|
||
bg="rgba(76, 175, 80, 0.1)"
|
||
borderRadius="lg"
|
||
border="1px solid rgba(76, 175, 80, 0.3)"
|
||
>
|
||
<HStack spacing={2}>
|
||
<Icon as={FaCheck} color="green.400" />
|
||
<Text color="green.400" fontSize="sm" fontWeight="medium">
|
||
{priceInfo.final_amount === 0
|
||
? `恭喜!您的当前订阅剩余价值足够直接升级到${selectedPlan.displayName},无需支付额外费用!`
|
||
: `升级到${selectedPlan.displayName},立即生效!按差价补缴费用`}
|
||
</Text>
|
||
</HStack>
|
||
</Box>
|
||
)}
|
||
{priceInfo.is_downgrade && (
|
||
<Box
|
||
p={3}
|
||
bg="rgba(255, 165, 0, 0.1)"
|
||
borderRadius="lg"
|
||
border="1px solid rgba(255, 165, 0, 0.3)"
|
||
>
|
||
<HStack spacing={2}>
|
||
<Icon as={FaClock} color="orange.400" />
|
||
<Text color="orange.400" fontSize="sm" fontWeight="medium">
|
||
当前{priceInfo.current_plan?.toUpperCase()}订阅到期后自动切换到{selectedPlan.displayName}
|
||
</Text>
|
||
</HStack>
|
||
</Box>
|
||
)}
|
||
{priceInfo.is_renewal && (
|
||
<Box
|
||
p={3}
|
||
bg="rgba(33, 150, 243, 0.1)"
|
||
borderRadius="lg"
|
||
border="1px solid rgba(33, 150, 243, 0.3)"
|
||
>
|
||
<HStack spacing={2}>
|
||
<Icon as={FaRedo} color="blue.400" />
|
||
<Text color="blue.400" fontSize="sm" fontWeight="medium">
|
||
续费{selectedPlan.displayName},在当前到期日基础上延长时长
|
||
</Text>
|
||
</HStack>
|
||
</Box>
|
||
)}
|
||
</>
|
||
)}
|
||
|
||
{/* 价格明细 */}
|
||
{selectedPlan && priceInfo && (
|
||
<Box
|
||
p={4}
|
||
bg="rgba(255, 255, 255, 0.05)"
|
||
borderRadius="lg"
|
||
border="1px solid rgba(255, 255, 255, 0.1)"
|
||
>
|
||
<VStack spacing={3} align="stretch">
|
||
<Flex justify="space-between" align="center">
|
||
<Text color="rgba(255, 255, 255, 0.7)" fontSize="sm">
|
||
{selectedPlan.displayName} · {selectedCycle === 'monthly' ? '月付' : selectedCycle === 'quarterly' ? '季付' : selectedCycle === 'semiannual' ? '半年付' : '年付'}
|
||
</Text>
|
||
<Text color="white" fontWeight="medium">
|
||
¥{priceInfo.original_price?.toFixed(2) || getCurrentPrice(selectedPlan).toFixed(2)}
|
||
</Text>
|
||
</Flex>
|
||
|
||
{/* 升级抵扣价值 */}
|
||
{priceInfo.is_upgrade && priceInfo.remaining_value > 0 && (
|
||
<Flex justify="space-between" align="center">
|
||
<Text color="rgba(255, 255, 255, 0.7)" fontSize="sm">
|
||
当前订阅剩余价值抵扣
|
||
</Text>
|
||
<Text color="green.400" fontWeight="medium">
|
||
-¥{priceInfo.remaining_value.toFixed(2)}
|
||
</Text>
|
||
</Flex>
|
||
)}
|
||
|
||
{/* 优惠码折扣 */}
|
||
{promoCodeApplied && priceInfo.discount_amount > 0 && (
|
||
<Flex justify="space-between" align="center">
|
||
<HStack spacing={2}>
|
||
<Icon as={FaCheck} color="green.400" boxSize={3} />
|
||
<Text color="rgba(255, 255, 255, 0.7)" fontSize="sm">
|
||
优惠码折扣
|
||
</Text>
|
||
</HStack>
|
||
<Text color="green.400" fontWeight="medium">
|
||
-¥{priceInfo.discount_amount.toFixed(2)}
|
||
</Text>
|
||
</Flex>
|
||
)}
|
||
|
||
<Divider borderColor="rgba(255, 255, 255, 0.1)" />
|
||
|
||
<Flex justify="space-between" align="baseline">
|
||
<Text fontSize="lg" fontWeight="bold" color="white">实付金额:</Text>
|
||
<Text fontSize="2xl" fontWeight="bold" color="#D4AF37">
|
||
¥{priceInfo.final_amount.toFixed(2)}
|
||
</Text>
|
||
</Flex>
|
||
</VStack>
|
||
</Box>
|
||
)}
|
||
|
||
{/* 优惠码输入 */}
|
||
{selectedPlan && (
|
||
<Box>
|
||
<HStack spacing={2}>
|
||
<Input
|
||
placeholder="输入优惠码(可选)"
|
||
value={promoCode}
|
||
onChange={(e) => {
|
||
setPromoCode(e.target.value.toUpperCase());
|
||
setPromoCodeError('');
|
||
}}
|
||
size="md"
|
||
isDisabled={promoCodeApplied}
|
||
bg="rgba(255, 255, 255, 0.05)"
|
||
border="1px solid rgba(255, 255, 255, 0.1)"
|
||
color="white"
|
||
_placeholder={{ color: 'rgba(255, 255, 255, 0.4)' }}
|
||
_hover={{ borderColor: 'rgba(212, 175, 55, 0.3)' }}
|
||
_focus={{ borderColor: '#D4AF37', boxShadow: '0 0 0 1px #D4AF37' }}
|
||
/>
|
||
<Button
|
||
bgGradient="linear-gradient(135deg, rgba(138, 43, 226, 0.8), rgba(123, 31, 162, 0.8))"
|
||
color="white"
|
||
onClick={handleValidatePromoCode}
|
||
isLoading={validatingPromo}
|
||
isDisabled={!promoCode || promoCodeApplied}
|
||
minW="80px"
|
||
_hover={{
|
||
bgGradient: 'linear-gradient(135deg, rgba(138, 43, 226, 1), rgba(123, 31, 162, 1))',
|
||
}}
|
||
>
|
||
应用
|
||
</Button>
|
||
</HStack>
|
||
{promoCodeError && (
|
||
<Text color="red.400" fontSize="sm" mt={2}>
|
||
{promoCodeError}
|
||
</Text>
|
||
)}
|
||
{promoCodeApplied && priceInfo && (
|
||
<HStack
|
||
mt={2}
|
||
p={2}
|
||
bg="rgba(72, 187, 120, 0.1)"
|
||
borderRadius="md"
|
||
border="1px solid rgba(72, 187, 120, 0.3)"
|
||
>
|
||
<Icon as={FaCheck} color="green.400" />
|
||
<Text color="green.400" fontSize="sm" fontWeight="medium" flex={1}>
|
||
优惠码已应用!节省 ¥{priceInfo.discount_amount.toFixed(2)}
|
||
</Text>
|
||
<Icon
|
||
as={FaTimes}
|
||
color="rgba(255, 255, 255, 0.5)"
|
||
cursor="pointer"
|
||
onClick={handleRemovePromoCode}
|
||
_hover={{ color: 'red.400' }}
|
||
/>
|
||
</HStack>
|
||
)}
|
||
</Box>
|
||
)}
|
||
|
||
<Button
|
||
w="full"
|
||
size="lg"
|
||
bgGradient="linear-gradient(135deg, #D4AF37, #B8941F)"
|
||
color="#000"
|
||
fontWeight="bold"
|
||
onClick={handleCreatePaymentOrder}
|
||
isLoading={loading}
|
||
isDisabled={!selectedPlan}
|
||
_hover={{
|
||
bgGradient: 'linear-gradient(135deg, #F4E3A7, #D4AF37)',
|
||
}}
|
||
>
|
||
{priceInfo?.is_upgrade && priceInfo?.final_amount === 0
|
||
? '立即免费升级'
|
||
: '创建微信支付订单'}
|
||
</Button>
|
||
</VStack>
|
||
) : (
|
||
<VStack spacing={4}>
|
||
<Text color="rgba(255, 255, 255, 0.7)" fontSize="lg" fontWeight="bold">
|
||
请使用微信扫描二维码完成支付
|
||
</Text>
|
||
|
||
{/* 倒计时 */}
|
||
<Box
|
||
p={3}
|
||
bg="rgba(255, 165, 0, 0.1)"
|
||
borderRadius="lg"
|
||
border="1px solid rgba(255, 165, 0, 0.3)"
|
||
w="full"
|
||
>
|
||
<HStack justify="center" spacing={2} mb={2}>
|
||
<Icon as={FaClock} color="orange.400" />
|
||
<Text color="orange.300" fontSize="sm">
|
||
二维码有效时间: {formatTime(paymentCountdown)}
|
||
</Text>
|
||
</HStack>
|
||
<Progress
|
||
value={(paymentCountdown / (30 * 60)) * 100}
|
||
colorScheme="orange"
|
||
size="sm"
|
||
borderRadius="full"
|
||
/>
|
||
</Box>
|
||
|
||
{/* 二维码 */}
|
||
<Box textAlign="center">
|
||
{paymentOrder.qr_code_url ? (
|
||
<Image
|
||
src={paymentOrder.qr_code_url}
|
||
alt="微信支付二维码"
|
||
mx="auto"
|
||
maxW="200px"
|
||
border="2px solid"
|
||
borderColor="rgba(255, 255, 255, 0.2)"
|
||
borderRadius="lg"
|
||
bg="white"
|
||
p={2}
|
||
/>
|
||
) : (
|
||
<Flex
|
||
w="200px"
|
||
h="200px"
|
||
mx="auto"
|
||
bg="rgba(255, 255, 255, 0.05)"
|
||
alignItems="center"
|
||
justifyContent="center"
|
||
border="2px solid"
|
||
borderColor="rgba(255, 255, 255, 0.2)"
|
||
borderRadius="lg"
|
||
>
|
||
<Icon as={FaQrcode} color="rgba(255, 255, 255, 0.3)" boxSize={12} />
|
||
</Flex>
|
||
)}
|
||
</Box>
|
||
|
||
{/* 订单信息 */}
|
||
<Box
|
||
p={4}
|
||
bg="rgba(255, 255, 255, 0.05)"
|
||
borderRadius="lg"
|
||
border="1px solid rgba(255, 255, 255, 0.1)"
|
||
w="full"
|
||
>
|
||
<Text fontSize="xs" color="rgba(255, 255, 255, 0.5)" mb={2}>
|
||
订单号: {paymentOrder.order_no}
|
||
</Text>
|
||
<Flex justify="space-between" align="baseline">
|
||
<Text color="rgba(255, 255, 255, 0.7)">支付金额:</Text>
|
||
<Text fontSize="xl" fontWeight="bold" color="#D4AF37">
|
||
¥{paymentOrder.amount}
|
||
</Text>
|
||
</Flex>
|
||
</Box>
|
||
|
||
{/* 操作按钮 */}
|
||
<VStack spacing={3} w="full">
|
||
<Button
|
||
w="full"
|
||
size="lg"
|
||
bgGradient="linear-gradient(135deg, #D4AF37, #B8941F)"
|
||
color="#000"
|
||
fontWeight="bold"
|
||
leftIcon={<Icon as={FaRedo} />}
|
||
onClick={handleForceUpdate}
|
||
isLoading={forceUpdating}
|
||
_hover={{
|
||
bgGradient: 'linear-gradient(135deg, #F4E3A7, #D4AF37)',
|
||
}}
|
||
>
|
||
已完成支付
|
||
</Button>
|
||
</VStack>
|
||
</VStack>
|
||
)}
|
||
</ModalBody>
|
||
</ModalContent>
|
||
</Modal>
|
||
</Box>
|
||
);
|
||
}
|