功能变更: - 将 StockQuoteCard 从 CompanyOverview 移至 Company/index.tsx - 股票行情卡片现在在切换 Tab 时始终可见 TypeScript 警告修复: - SubTabContainer: WebkitBackdropFilter 改用 sx 属性 - DetailTable: 重新定义 TableRowData 类型,支持 boolean 索引 - SubscriptionContentNew: 添加类型安全的 AGREEMENT_URLS 索引访问 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
1885 lines
70 KiB
TypeScript
1885 lines
70 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,
|
||
useBreakpointValue,
|
||
Checkbox,
|
||
Link as ChakraLink,
|
||
} from '@chakra-ui/react';
|
||
import {
|
||
FaWeixin,
|
||
FaGem,
|
||
FaCheck,
|
||
FaQrcode,
|
||
FaClock,
|
||
FaRedo,
|
||
FaCrown,
|
||
FaStar,
|
||
FaTimes,
|
||
FaChevronDown,
|
||
FaChevronUp,
|
||
FaExternalLinkAlt,
|
||
} from 'react-icons/fa';
|
||
import { AlipayCircleOutlined } from '@ant-design/icons';
|
||
|
||
import { logger } from '../../utils/logger';
|
||
import { useAuth } from '../../contexts/AuthContext';
|
||
import { useSubscriptionEvents } from '../../hooks/useSubscriptionEvents';
|
||
import { subscriptionConfig, themeColors } from '../../views/Pages/Account/subscription-content';
|
||
import { getApiBase } from '../../utils/apiConfig';
|
||
|
||
// 会员协议 URL 配置
|
||
const AGREEMENT_URLS = {
|
||
pro: 'https://valuefrontier.cn/htmls/pro-member-agreement.html',
|
||
max: 'https://valuefrontier.cn/htmls/max-member-agreement.html',
|
||
};
|
||
|
||
// 计费周期选择器组件 - 移动端垂直布局(年付在上),桌面端水平布局
|
||
interface CycleSelectorProps {
|
||
options: any[];
|
||
selectedCycle: string;
|
||
onSelectCycle: (cycle: string) => void;
|
||
}
|
||
|
||
function CycleSelector({ options, selectedCycle, onSelectCycle }: CycleSelectorProps) {
|
||
// 使用 useBreakpointValue 动态获取是否是移动端
|
||
const isMobile = useBreakpointValue({ base: true, md: false });
|
||
|
||
// 移动端倒序显示(年付在上),桌面端正常顺序
|
||
const displayOptions = isMobile ? [...options].reverse() : options;
|
||
|
||
return (
|
||
<Flex
|
||
direction={{ base: 'column', md: 'row' }}
|
||
gap={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)"
|
||
justify="center"
|
||
align="center"
|
||
w={{ base: 'full', md: 'auto' }}
|
||
maxW={{ base: '320px', md: 'none' }}
|
||
mx="auto"
|
||
>
|
||
{displayOptions.map((option: any) => (
|
||
<Box key={option.cycleKey} position="relative" w={{ base: 'full', md: 'auto' }}>
|
||
{option.discountPercent > 0 && (
|
||
<Badge
|
||
position="absolute"
|
||
top={{ base: '50%', md: '-10px' }}
|
||
right={{ base: '10px', md: '-10px' }}
|
||
transform={{ base: 'translateY(-50%)', md: 'none' }}
|
||
colorScheme="red"
|
||
fontSize="xs"
|
||
px={2}
|
||
py={1}
|
||
borderRadius="full"
|
||
fontWeight="bold"
|
||
zIndex={1}
|
||
>
|
||
省{option.discountPercent}%
|
||
</Badge>
|
||
)}
|
||
|
||
<Button
|
||
size="lg"
|
||
w={{ base: 'full', md: 'auto' }}
|
||
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={() => onSelectCycle(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"
|
||
justifyContent={{ base: 'flex-start', md: 'center' }}
|
||
pl={{ base: 6, md: 6 }}
|
||
>
|
||
{option.label}
|
||
</Button>
|
||
</Box>
|
||
))}
|
||
</Flex>
|
||
);
|
||
}
|
||
|
||
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 [paymentMethod, setPaymentMethod] = useState<'wechat' | 'alipay'>('wechat'); // 支付方式
|
||
|
||
const [openFaqIndex, setOpenFaqIndex] = useState(null);
|
||
|
||
// 会员协议确认状态
|
||
const [agreementChecked, setAgreementChecked] = useState(false);
|
||
|
||
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();
|
||
}, []);
|
||
|
||
// 检查是否从支付宝支付返回(手机端支付完成后会跳转回来)
|
||
useEffect(() => {
|
||
const checkAlipayReturn = async () => {
|
||
// 检查 URL 参数是否包含支付宝返回标记
|
||
const urlParams = new URLSearchParams(window.location.search);
|
||
const paymentReturn = urlParams.get('payment_return');
|
||
// 支付宝返回的参数是 out_trade_no,后端重定向时会转成 order_no
|
||
const orderNo = urlParams.get('order_no') || urlParams.get('out_trade_no');
|
||
|
||
if (paymentReturn === 'alipay' && orderNo) {
|
||
// 从支付宝返回,检查支付状态
|
||
toast({
|
||
title: '正在确认支付结果...',
|
||
status: 'info',
|
||
duration: 2000,
|
||
isClosable: true,
|
||
});
|
||
|
||
try {
|
||
// 优先使用 sessionStorage 中的 orderId,否则使用 order_no 查询
|
||
const orderId = sessionStorage.getItem('alipay_order_id');
|
||
const statusUrl = orderId
|
||
? `${getApiBase()}/api/payment/alipay/order/${orderId}/status`
|
||
: `${getApiBase()}/api/payment/alipay/order-by-no/${orderNo}/status`;
|
||
|
||
const response = await fetch(statusUrl, {
|
||
credentials: 'include',
|
||
});
|
||
|
||
if (response.ok) {
|
||
const data = await response.json();
|
||
if (data.success && (data.data?.status === 'paid' || data.payment_success)) {
|
||
toast({
|
||
title: '支付成功!',
|
||
description: '您的订阅已激活',
|
||
status: 'success',
|
||
duration: 5000,
|
||
isClosable: true,
|
||
});
|
||
|
||
// 清理 sessionStorage
|
||
sessionStorage.removeItem('alipay_order_id');
|
||
sessionStorage.removeItem('alipay_order_no');
|
||
|
||
// 清除 URL 参数并刷新页面
|
||
window.history.replaceState({}, document.title, window.location.pathname);
|
||
setTimeout(() => window.location.reload(), 2000);
|
||
} else {
|
||
toast({
|
||
title: '支付状态待确认',
|
||
description: '如已完成支付,请稍候或刷新页面',
|
||
status: 'warning',
|
||
duration: 5000,
|
||
isClosable: true,
|
||
});
|
||
// 清除 URL 参数
|
||
window.history.replaceState({}, document.title, window.location.pathname);
|
||
}
|
||
} else {
|
||
// 清除 URL 参数
|
||
window.history.replaceState({}, document.title, window.location.pathname);
|
||
}
|
||
} catch (error) {
|
||
logger.error('SubscriptionContentNew', 'checkAlipayReturn', error);
|
||
// 清除 URL 参数
|
||
window.history.replaceState({}, document.title, window.location.pathname);
|
||
}
|
||
}
|
||
};
|
||
|
||
checkAlipayReturn();
|
||
}, [toast]);
|
||
|
||
const fetchSubscriptionPlans = async () => {
|
||
try {
|
||
logger.debug('SubscriptionContentNew', '正在获取订阅套餐');
|
||
const response = await fetch(`${getApiBase()}/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(`${getApiBase()}/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);
|
||
|
||
// 切换套餐时重置协议勾选状态
|
||
setAgreementChecked(false);
|
||
|
||
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(`${getApiBase()}/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 || '免费升级失败');
|
||
}
|
||
}
|
||
|
||
const paymentMethodName = paymentMethod === 'alipay' ? 'alipay' : 'wechat_pay';
|
||
|
||
subscriptionEvents.trackPaymentInitiated({
|
||
planName: selectedPlan.name,
|
||
paymentMethod: paymentMethodName,
|
||
amount: price,
|
||
billingCycle: selectedCycle,
|
||
});
|
||
|
||
// 根据支付方式选择不同的 API
|
||
const apiUrl = paymentMethod === 'alipay'
|
||
? `${getApiBase()}/api/payment/alipay/create-order`
|
||
: `${getApiBase()}/api/payment/create-order`;
|
||
|
||
// 检测是否为移动端设备(多重检测,确保 Safari 兼容性)
|
||
const userAgent = navigator.userAgent;
|
||
|
||
// 方式1:User Agent 检测
|
||
const uaCheck = /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(userAgent);
|
||
|
||
// 方式2:触摸屏检测(Safari 兼容)
|
||
const touchCheck = 'ontouchstart' in window || navigator.maxTouchPoints > 0;
|
||
|
||
// 方式3:屏幕宽度检测
|
||
const screenCheck = window.innerWidth <= 768;
|
||
|
||
// 方式4:Safari 移动端特定检测(iOS Safari 有 standalone 属性)
|
||
const isSafariMobile = /Safari/i.test(userAgent) && /Apple/i.test(navigator.vendor) && touchCheck;
|
||
|
||
// 综合判断:UA 匹配 或 (触摸屏 + 小屏幕) 或 Safari 移动端
|
||
const isMobileDevice = uaCheck || (touchCheck && screenCheck) || isSafariMobile;
|
||
|
||
// 调试日志
|
||
console.log('[支付] User-Agent:', userAgent);
|
||
console.log('[支付] 移动端检测 - UA:', uaCheck, 'Touch:', touchCheck, 'Screen:', screenCheck, 'SafariMobile:', isSafariMobile);
|
||
console.log('[支付] 最终判断为移动端:', isMobileDevice);
|
||
console.log('[支付] 支付方式:', paymentMethod);
|
||
|
||
const response = await fetch(apiUrl, {
|
||
method: 'POST',
|
||
headers: {
|
||
'Content-Type': 'application/json',
|
||
},
|
||
credentials: 'include',
|
||
body: JSON.stringify({
|
||
plan_name: selectedPlan.name,
|
||
billing_cycle: selectedCycle,
|
||
promo_code: promoCodeApplied ? promoCode : null,
|
||
is_mobile: isMobileDevice, // 传递设备类型,用于支付宝选择 page/wap 支付
|
||
}),
|
||
});
|
||
|
||
if (response.ok) {
|
||
const data = await response.json();
|
||
if (data.success) {
|
||
if (paymentMethod === 'alipay') {
|
||
// 支付宝:跳转到支付页面
|
||
if (data.data.pay_url) {
|
||
// 使用与上面相同的移动端检测结果(已在上面计算过)
|
||
console.log('[支付宝] 订单创建成功');
|
||
console.log('[支付宝] pay_url:', data.data.pay_url.substring(0, 100) + '...');
|
||
console.log('[支付宝] 是否移动端跳转:', isMobileDevice);
|
||
|
||
if (isMobileDevice) {
|
||
// 手机端:尝试自动跳转到支付宝
|
||
// 保存订单信息到 sessionStorage,支付完成后返回时可以用来检查状态
|
||
sessionStorage.setItem('alipay_order_id', data.data.id);
|
||
sessionStorage.setItem('alipay_order_no', data.data.order_no);
|
||
|
||
// 检测是否在微信内置浏览器中
|
||
const isWechatBrowser = /MicroMessenger/i.test(navigator.userAgent);
|
||
|
||
// 设置订单状态(用于支付完成后返回时显示)
|
||
setPaymentOrder({
|
||
...data.data,
|
||
payment_method: 'alipay',
|
||
is_mobile: true,
|
||
is_wechat_browser: isWechatBrowser,
|
||
});
|
||
setPaymentCountdown(30 * 60);
|
||
startAutoPaymentCheck(data.data.id, 'alipay');
|
||
|
||
if (isWechatBrowser) {
|
||
// 微信内置浏览器:无法直接跳转支付宝,需要引导用户在外部浏览器打开
|
||
toast({
|
||
title: '订单创建成功',
|
||
description: '微信内无法直接打开支付宝,请点击按钮复制链接后在浏览器中打开',
|
||
status: 'warning',
|
||
duration: 5000,
|
||
isClosable: true,
|
||
});
|
||
} else {
|
||
// 非微信浏览器(Safari、Chrome 等):直接跳转
|
||
toast({
|
||
title: '正在跳转支付宝',
|
||
description: '如未自动跳转,请点击下方按钮',
|
||
status: 'success',
|
||
duration: 3000,
|
||
isClosable: true,
|
||
});
|
||
|
||
// 延迟 300ms 后自动跳转,让 toast 显示出来
|
||
setTimeout(() => {
|
||
window.location.href = data.data.pay_url;
|
||
}, 300);
|
||
}
|
||
} else {
|
||
// PC端:新窗口打开
|
||
setPaymentOrder(data.data);
|
||
setPaymentCountdown(30 * 60);
|
||
startAutoPaymentCheck(data.data.id, 'alipay');
|
||
|
||
toast({
|
||
title: '订单创建成功',
|
||
description: '正在跳转到支付宝支付页面...',
|
||
status: 'success',
|
||
duration: 3000,
|
||
isClosable: true,
|
||
});
|
||
|
||
// 延迟跳转,让用户看到提示
|
||
setTimeout(() => {
|
||
window.open(data.data.pay_url, '_blank');
|
||
}, 500);
|
||
}
|
||
} else {
|
||
throw new Error('支付链接获取失败');
|
||
}
|
||
} else {
|
||
// 微信:显示二维码
|
||
setPaymentOrder(data.data);
|
||
setPaymentCountdown(30 * 60);
|
||
startAutoPaymentCheck(data.data.id, 'wechat');
|
||
|
||
toast({
|
||
title: '订单创建成功',
|
||
description: '请使用微信扫描二维码完成支付',
|
||
status: 'success',
|
||
duration: 3000,
|
||
isClosable: true,
|
||
});
|
||
}
|
||
} else {
|
||
throw new Error(data.error || data.message || '创建订单失败');
|
||
}
|
||
} else {
|
||
const errorData = await response.json().catch(() => ({}));
|
||
throw new Error(errorData.error || '网络请求失败');
|
||
}
|
||
} catch (error: any) {
|
||
subscriptionEvents.trackPaymentFailed(
|
||
{
|
||
planName: selectedPlan.name,
|
||
paymentMethod: paymentMethod === 'alipay' ? 'alipay' : '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, method: 'wechat' | 'alipay' = 'wechat') => {
|
||
// 根据支付方式选择不同的状态查询 API
|
||
const statusApiUrl = method === 'alipay'
|
||
? `${getApiBase()}/api/payment/alipay/order/${orderId}/status`
|
||
: `${getApiBase()}/api/payment/order/${orderId}/status`;
|
||
|
||
const checkInterval = setInterval(async () => {
|
||
try {
|
||
const response = await fetch(statusApiUrl, {
|
||
credentials: 'include',
|
||
});
|
||
|
||
if (response.ok) {
|
||
const data = await response.json();
|
||
if (data.success && (data.data.status === 'paid' || data.payment_success)) {
|
||
clearInterval(checkInterval);
|
||
setAutoCheckInterval(null);
|
||
|
||
subscriptionEvents.trackPaymentSuccessful({
|
||
planName: selectedPlan?.name,
|
||
paymentMethod: method === 'alipay' ? 'alipay' : 'wechat_pay',
|
||
amount: paymentOrder?.amount,
|
||
orderId: orderId,
|
||
transactionId: data.data.transaction_id || data.data.alipay_trade_no,
|
||
});
|
||
|
||
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 {
|
||
// 根据订单的支付方式选择不同的查询 API
|
||
const orderPaymentMethod = (paymentOrder as any).payment_method || paymentMethod;
|
||
const statusApiUrl = orderPaymentMethod === 'alipay'
|
||
? `${getApiBase()}/api/payment/alipay/order/${(paymentOrder as any).id}/status`
|
||
: `${getApiBase()}/api/payment/order/${(paymentOrder as any).id}/status`;
|
||
|
||
const response = await fetch(statusApiUrl, {
|
||
credentials: 'include',
|
||
});
|
||
|
||
if (response.ok) {
|
||
const data = await response.json();
|
||
if (data.success && (data.data.status === 'paid' || data.payment_success)) {
|
||
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 handleReopenAlipay = () => {
|
||
if (paymentOrder && (paymentOrder as any).pay_url) {
|
||
window.open((paymentOrder as any).pay_url, '_blank');
|
||
}
|
||
};
|
||
|
||
// 合并数据库数据和前端配置
|
||
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>
|
||
|
||
<CycleSelector
|
||
options={getMergedPlans()[1]?.pricingOptions || []}
|
||
selectedCycle={selectedCycle}
|
||
onSelectCycle={setSelectedCycle}
|
||
/>
|
||
|
||
{(() => {
|
||
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">
|
||
{paymentMethod === 'alipay' ? '支付宝支付' : '微信支付'}
|
||
</ModalHeader>
|
||
<ModalCloseButton color="white" />
|
||
<ModalBody pb={6}>
|
||
{!paymentOrder ? (
|
||
<VStack spacing={6} align="stretch">
|
||
{/* 支付方式选择 */}
|
||
<Box>
|
||
<Text color="rgba(255, 255, 255, 0.7)" fontSize="sm" mb={3}>
|
||
选择支付方式
|
||
</Text>
|
||
<HStack spacing={3}>
|
||
<Button
|
||
flex={1}
|
||
h="60px"
|
||
bg={paymentMethod === 'wechat' ? 'rgba(7, 193, 96, 0.15)' : 'rgba(255, 255, 255, 0.05)'}
|
||
border="2px solid"
|
||
borderColor={paymentMethod === 'wechat' ? '#07C160' : 'rgba(255, 255, 255, 0.1)'}
|
||
borderRadius="xl"
|
||
onClick={() => setPaymentMethod('wechat')}
|
||
_hover={{
|
||
borderColor: paymentMethod === 'wechat' ? '#07C160' : 'rgba(255, 255, 255, 0.3)',
|
||
bg: paymentMethod === 'wechat' ? 'rgba(7, 193, 96, 0.2)' : 'rgba(255, 255, 255, 0.08)',
|
||
}}
|
||
transition="all 0.2s"
|
||
>
|
||
<HStack spacing={3}>
|
||
<Icon as={FaWeixin} color="#07C160" boxSize={6} />
|
||
<Text color="white" fontWeight="medium">微信支付</Text>
|
||
</HStack>
|
||
</Button>
|
||
<Button
|
||
flex={1}
|
||
h="60px"
|
||
bg={paymentMethod === 'alipay' ? 'rgba(22, 119, 255, 0.15)' : 'rgba(255, 255, 255, 0.05)'}
|
||
border="2px solid"
|
||
borderColor={paymentMethod === 'alipay' ? '#1677FF' : 'rgba(255, 255, 255, 0.1)'}
|
||
borderRadius="xl"
|
||
onClick={() => setPaymentMethod('alipay')}
|
||
_hover={{
|
||
borderColor: paymentMethod === 'alipay' ? '#1677FF' : 'rgba(255, 255, 255, 0.3)',
|
||
bg: paymentMethod === 'alipay' ? 'rgba(22, 119, 255, 0.2)' : 'rgba(255, 255, 255, 0.08)',
|
||
}}
|
||
transition="all 0.2s"
|
||
>
|
||
<HStack spacing={3}>
|
||
<Box as={AlipayCircleOutlined} fontSize="24px" color="#1677FF" />
|
||
<Text color="white" fontWeight="medium">支付宝</Text>
|
||
</HStack>
|
||
</Button>
|
||
</HStack>
|
||
</Box>
|
||
|
||
{/* 订阅类型提示 */}
|
||
{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>
|
||
)}
|
||
|
||
{/* 会员协议确认 */}
|
||
<Checkbox
|
||
isChecked={agreementChecked}
|
||
onChange={(e) => setAgreementChecked(e.target.checked)}
|
||
colorScheme="green"
|
||
size="md"
|
||
>
|
||
<Text fontSize="sm" color="rgba(255, 255, 255, 0.7)">
|
||
我已阅读并同意
|
||
<ChakraLink
|
||
href={(() => {
|
||
const planName = (selectedPlan as { name?: string } | null)?.name?.toLowerCase();
|
||
return planName === 'pro' || planName === 'max' ? AGREEMENT_URLS[planName] : AGREEMENT_URLS.pro;
|
||
})()}
|
||
isExternal
|
||
color="#3182CE"
|
||
textDecoration="underline"
|
||
mx={1}
|
||
onClick={(e) => e.stopPropagation()}
|
||
>
|
||
《{(selectedPlan as { name?: string } | null)?.name?.toLowerCase() === 'max' ? 'MAX' : 'PRO'}会员服务协议》
|
||
</ChakraLink>
|
||
</Text>
|
||
</Checkbox>
|
||
|
||
<Button
|
||
w="full"
|
||
size="lg"
|
||
bgGradient={paymentMethod === 'alipay'
|
||
? 'linear-gradient(135deg, #1677FF, #0958d9)'
|
||
: 'linear-gradient(135deg, #07C160, #059048)'}
|
||
color="white"
|
||
fontWeight="bold"
|
||
onClick={() => {
|
||
if (!agreementChecked) {
|
||
toast({
|
||
title: '请先阅读并同意会员服务协议',
|
||
status: 'warning',
|
||
duration: 3000,
|
||
isClosable: true,
|
||
position: 'top',
|
||
});
|
||
return;
|
||
}
|
||
handleCreatePaymentOrder();
|
||
}}
|
||
isLoading={loading}
|
||
isDisabled={!selectedPlan}
|
||
leftIcon={paymentMethod === 'alipay'
|
||
? <Box as={AlipayCircleOutlined} fontSize="20px" />
|
||
: <Icon as={FaWeixin} boxSize={5} />}
|
||
_hover={{
|
||
bgGradient: paymentMethod === 'alipay'
|
||
? 'linear-gradient(135deg, #4096ff, #1677FF)'
|
||
: 'linear-gradient(135deg, #10d76e, #07C160)',
|
||
transform: 'translateY(-2px)',
|
||
boxShadow: paymentMethod === 'alipay'
|
||
? '0 8px 25px rgba(22, 119, 255, 0.4)'
|
||
: '0 8px 25px rgba(7, 193, 96, 0.4)',
|
||
}}
|
||
transition="all 0.3s"
|
||
>
|
||
{priceInfo?.is_upgrade && priceInfo?.final_amount === 0
|
||
? '立即免费升级'
|
||
: paymentMethod === 'alipay'
|
||
? '支付宝支付'
|
||
: '微信扫码支付'}
|
||
</Button>
|
||
</VStack>
|
||
) : (
|
||
<VStack spacing={4}>
|
||
{/* 根据支付方式显示不同提示 */}
|
||
{(paymentOrder as any).payment_method === 'alipay' ? (
|
||
<>
|
||
<Box
|
||
p={4}
|
||
bg="rgba(22, 119, 255, 0.1)"
|
||
borderRadius="lg"
|
||
border="1px solid rgba(22, 119, 255, 0.3)"
|
||
w="full"
|
||
textAlign="center"
|
||
>
|
||
<HStack justify="center" spacing={2} mb={2}>
|
||
<Box as={AlipayCircleOutlined} fontSize="24px" color="#1677FF" />
|
||
<Text color="#1677FF" fontSize="lg" fontWeight="bold">
|
||
支付宝支付
|
||
</Text>
|
||
</HStack>
|
||
{(paymentOrder as any).is_mobile ? (
|
||
<>
|
||
{(paymentOrder as any).is_wechat_browser ? (
|
||
<>
|
||
<Text color="orange.300" fontSize="sm" mb={2}>
|
||
检测到您在微信中打开,请点击右上角「...」选择「在浏览器中打开」后再支付
|
||
</Text>
|
||
<Text color="rgba(255, 255, 255, 0.5)" fontSize="xs" mb={3}>
|
||
或点击下方按钮尝试跳转
|
||
</Text>
|
||
</>
|
||
) : (
|
||
<Text color="rgba(255, 255, 255, 0.7)" fontSize="sm" mb={3}>
|
||
点击下方按钮打开支付宝完成支付
|
||
</Text>
|
||
)}
|
||
<Button
|
||
w="full"
|
||
size="lg"
|
||
bgGradient="linear-gradient(135deg, #1677FF, #0958d9)"
|
||
color="white"
|
||
fontWeight="bold"
|
||
leftIcon={<Box as={AlipayCircleOutlined} fontSize="20px" />}
|
||
onClick={() => {
|
||
window.location.href = (paymentOrder as any).pay_url;
|
||
}}
|
||
_hover={{
|
||
bgGradient: 'linear-gradient(135deg, #4096ff, #1677FF)',
|
||
}}
|
||
>
|
||
打开支付宝付款
|
||
</Button>
|
||
</>
|
||
) : (
|
||
<>
|
||
<Text color="rgba(255, 255, 255, 0.7)" fontSize="sm">
|
||
请在新打开的页面中完成支付
|
||
</Text>
|
||
<Text color="rgba(255, 255, 255, 0.5)" fontSize="xs" mt={1}>
|
||
支付完成后点击下方按钮确认
|
||
</Text>
|
||
</>
|
||
)}
|
||
</Box>
|
||
</>
|
||
) : (
|
||
<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>
|
||
|
||
{/* 微信二维码(仅微信支付显示) */}
|
||
{(paymentOrder as any).payment_method !== 'alipay' && (
|
||
<Box textAlign="center">
|
||
{(paymentOrder as any).qr_code_url ? (
|
||
<Image
|
||
src={(paymentOrder as any).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 as any).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 as any).amount}
|
||
</Text>
|
||
</Flex>
|
||
</Box>
|
||
|
||
{/* 操作按钮 */}
|
||
<VStack spacing={3} w="full">
|
||
{/* 支付宝:重新打开支付页面按钮 */}
|
||
{(paymentOrder as any).payment_method === 'alipay' && (paymentOrder as any).pay_url && (
|
||
<Button
|
||
w="full"
|
||
size="lg"
|
||
bgGradient="linear-gradient(135deg, #1677FF, #0958d9)"
|
||
color="white"
|
||
fontWeight="bold"
|
||
leftIcon={<Icon as={FaExternalLinkAlt} />}
|
||
onClick={handleReopenAlipay}
|
||
_hover={{
|
||
bgGradient: 'linear-gradient(135deg, #4096ff, #1677FF)',
|
||
transform: 'translateY(-2px)',
|
||
boxShadow: '0 8px 25px rgba(22, 119, 255, 0.4)',
|
||
}}
|
||
transition="all 0.3s"
|
||
>
|
||
重新打开支付页面
|
||
</Button>
|
||
)}
|
||
|
||
<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>
|
||
);
|
||
}
|