Files
vf_react/src/components/Subscription/SubscriptionContentNew.tsx
zdl 4b3588e8de feat: 将 StockQuoteCard 提升到 Tab 容器上方 + 修复 TS 警告
功能变更:
- 将 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>
2025-12-18 17:25:21 +08:00

1885 lines
70 KiB
TypeScript
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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;
// 方式1User 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;
// 方式4Safari 移动端特定检测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>
);
}