Files
vf_react/src/components/Subscription/SubscriptionContent.js
zdl 02cd234def feat: 已完成的工作:
-  创建了4个P1优先级Hook(搜索、导航、个人资料、订阅)
  -  将其中3个Hook集成到5个组件中
  -  在个人资料、设置、搜索、订阅流程中添加了15+个追踪点
  -  覆盖了完整的收入漏斗(支付发起 → 成功 → 订阅创建)
  -  添加了留存追踪(个人资料更新、设置修改、搜索查询)

  影响:
  - 完整的用户订阅旅程可见性
  - 个人资料/设置参与度追踪
  - 搜索行为分析
  - 完整的支付漏斗追踪(微信支付)
2025-10-29 12:29:41 +08:00

1241 lines
41 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

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.

// src/components/Subscription/SubscriptionContent.js
import {
Box,
Button,
Flex,
Grid,
Icon,
Text,
Badge,
VStack,
HStack,
useColorModeValue,
useToast,
Modal,
ModalOverlay,
ModalContent,
ModalHeader,
ModalBody,
ModalCloseButton,
useDisclosure,
Image,
Progress,
Divider,
Table,
Thead,
Tbody,
Tr,
Th,
Td,
Heading,
Collapse,
} from '@chakra-ui/react';
import React, { useState, useEffect } from 'react';
import { logger } from '../../utils/logger';
import { useAuth } from '../../contexts/AuthContext';
import { useSubscriptionEvents } from '../../hooks/useSubscriptionEvents';
// Icons
import {
FaWeixin,
FaGem,
FaCheck,
FaQrcode,
FaClock,
FaRedo,
FaCrown,
FaStar,
FaTimes,
FaInfinity,
FaChevronDown,
FaChevronUp,
} from 'react-icons/fa';
export default function SubscriptionContent() {
// Auth context
const { user } = useAuth();
// 🎯 初始化订阅埋点Hook传入当前订阅信息
const subscriptionEvents = useSubscriptionEvents({
currentSubscription: {
plan: user?.subscription_plan || 'free',
status: user?.subscription_status || 'none'
}
});
// Chakra color mode
const textColor = useColorModeValue('gray.700', 'white');
const borderColor = useColorModeValue('gray.200', 'gray.600');
const bgCard = useColorModeValue('white', 'gray.800');
const bgAccent = useColorModeValue('blue.50', 'blue.900');
const secondaryText = useColorModeValue('gray.600', 'gray.400');
const toast = useToast();
const { isOpen: isPaymentModalOpen, onOpen: onPaymentModalOpen, onClose: onPaymentModalClose } = useDisclosure();
// State
const [subscriptionPlans, setSubscriptionPlans] = useState([]);
const [selectedPlan, setSelectedPlan] = useState(null);
const [selectedCycle, setSelectedCycle] = useState('monthly');
const [paymentOrder, setPaymentOrder] = useState(null);
const [loading, setLoading] = useState(false);
const [paymentCountdown, setPaymentCountdown] = useState(0);
const [checkingPayment, setCheckingPayment] = useState(false);
const [autoCheckInterval, setAutoCheckInterval] = useState(null);
const [forceUpdating, setForceUpdating] = useState(false);
const [openFaqIndex, setOpenFaqIndex] = useState(null);
// 加载订阅套餐数据
useEffect(() => {
fetchSubscriptionPlans();
// 不再需要 fetchCurrentUser(),直接使用 AuthContext 的 user
}, []);
// 倒计时更新
useEffect(() => {
let timer;
if (paymentCountdown > 0) {
timer = setInterval(() => {
setPaymentCountdown(prev => {
if (prev <= 1) {
handlePaymentExpired();
return 0;
}
return prev - 1;
});
}, 1000);
}
return () => clearInterval(timer);
}, [paymentCountdown]);
// 组件卸载时清理定时器
useEffect(() => {
return () => {
stopAutoPaymentCheck();
};
}, []);
const fetchSubscriptionPlans = async () => {
try {
logger.debug('SubscriptionContent', '正在获取订阅套餐');
const response = await fetch('/api/subscription/plans');
if (response.ok) {
const data = await response.json();
if (data.success && Array.isArray(data.data)) {
const validPlans = data.data.filter(plan =>
plan &&
plan.name &&
typeof plan.monthly_price === 'number' &&
typeof plan.yearly_price === 'number'
);
logger.debug('SubscriptionContent', '套餐加载成功', {
status: response.status,
validPlansCount: validPlans.length
});
setSubscriptionPlans(validPlans);
} else {
logger.warn('SubscriptionContent', '套餐数据格式异常', { data });
setSubscriptionPlans([]);
}
} else {
logger.error('SubscriptionContent', 'fetchSubscriptionPlans', new Error(`HTTP ${response.status}`));
setSubscriptionPlans([]);
}
} catch (error) {
logger.error('SubscriptionContent', 'fetchSubscriptionPlans', error);
setSubscriptionPlans([]);
}
};
const handleSubscribe = (plan) => {
if (!user) {
toast({
title: '请先登录',
status: 'warning',
duration: 3000,
isClosable: true,
});
return;
}
if (!plan || !plan.name) {
toast({
title: '套餐信息错误',
status: 'error',
duration: 3000,
isClosable: true,
});
return;
}
// 🎯 追踪定价方案选择
subscriptionEvents.trackPricingPlanSelected(
plan.name,
selectedCycle,
selectedCycle === 'monthly' ? plan.monthly_price : plan.yearly_price
);
setSelectedPlan(plan);
onPaymentModalOpen();
};
const handleCreateOrder = async () => {
if (!selectedPlan) return;
setLoading(true);
try {
const price = selectedCycle === 'monthly' ? selectedPlan.monthly_price : selectedPlan.yearly_price;
// 🎯 追踪支付发起
subscriptionEvents.trackPaymentInitiated({
planName: selectedPlan.name,
paymentMethod: 'wechat_pay',
amount: price,
billingCycle: selectedCycle,
orderId: null // Will be set after order creation
});
const response = await fetch('/api/payment/create-order', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
credentials: 'include',
body: JSON.stringify({
plan_name: selectedPlan.name,
billing_cycle: selectedCycle
})
});
if (response.ok) {
const data = await response.json();
if (data.success) {
setPaymentOrder(data.data);
setPaymentCountdown(30 * 60);
startAutoPaymentCheck(data.data.id);
toast({
title: '订单创建成功',
description: '请使用微信扫描二维码完成支付',
status: 'success',
duration: 3000,
isClosable: true,
});
} else {
throw new Error(data.message || '创建订单失败');
}
} else {
throw new Error('网络错误');
}
} catch (error) {
// 🎯 追踪支付失败
subscriptionEvents.trackPaymentFailed({
planName: selectedPlan.name,
paymentMethod: 'wechat_pay',
amount: selectedCycle === 'monthly' ? selectedPlan.monthly_price : selectedPlan.yearly_price
}, error.message);
toast({
title: '创建订单失败',
description: error.message,
status: 'error',
duration: 3000,
isClosable: true,
});
} finally {
setLoading(false);
}
};
const handlePaymentExpired = () => {
setPaymentOrder(null);
setPaymentCountdown(0);
stopAutoPaymentCheck();
toast({
title: '支付二维码已过期',
description: '请重新创建订单',
status: 'warning',
duration: 3000,
isClosable: true,
});
};
const startAutoPaymentCheck = (orderId) => {
logger.info('SubscriptionContent', '开始自动检查支付状态', { orderId });
const checkInterval = setInterval(async () => {
try {
const response = await fetch(`/api/payment/order/${orderId}/status`, {
credentials: 'include'
});
if (response.ok) {
const data = await response.json();
logger.debug('SubscriptionContent', '支付状态检查结果', {
orderId,
paymentSuccess: data.payment_success,
data
});
if (data.success && data.payment_success) {
clearInterval(checkInterval);
setAutoCheckInterval(null);
logger.info('SubscriptionContent', '自动检测到支付成功', { orderId });
// 🎯 追踪支付成功
subscriptionEvents.trackPaymentSuccessful({
planName: selectedPlan?.name,
paymentMethod: 'wechat_pay',
amount: paymentOrder?.amount,
billingCycle: selectedCycle,
orderId: orderId,
transactionId: data.transaction_id
});
// 🎯 追踪订阅创建
subscriptionEvents.trackSubscriptionCreated({
plan: selectedPlan?.name,
billingCycle: selectedCycle,
amount: paymentOrder?.amount,
startDate: new Date().toISOString(),
endDate: null // Will be calculated by backend
});
toast({
title: '支付成功!',
description: '订阅已激活,正在跳转...',
status: 'success',
duration: 3000,
isClosable: true,
});
setTimeout(() => {
onPaymentModalClose();
window.location.reload();
}, 2000);
}
}
} catch (error) {
logger.error('SubscriptionContent', 'startAutoPaymentCheck', error, { orderId });
}
}, 10000);
setAutoCheckInterval(checkInterval);
};
const stopAutoPaymentCheck = () => {
if (autoCheckInterval) {
clearInterval(autoCheckInterval);
setAutoCheckInterval(null);
logger.debug('SubscriptionContent', '停止自动检查支付状态');
}
};
const handleRefreshUserStatus = () => {
// 刷新页面以重新加载用户数据
window.location.reload();
};
const handleForceUpdatePayment = async () => {
if (!paymentOrder) return;
setForceUpdating(true);
try {
const response = await fetch(`/api/payment/order/${paymentOrder.id}/force-update`, {
method: 'POST',
credentials: 'include'
});
if (response.ok) {
const data = await response.json();
logger.info('SubscriptionContent', '强制更新支付状态结果', {
orderId: paymentOrder.id,
paymentSuccess: data.payment_success,
data
});
if (data.success && data.payment_success) {
stopAutoPaymentCheck();
toast({
title: '状态更新成功!',
description: '订阅已激活,正在刷新页面...',
status: 'success',
duration: 3000,
isClosable: true,
});
setTimeout(() => {
onPaymentModalClose();
window.location.reload();
}, 2000);
} else {
toast({
title: '无法更新状态',
description: data.error || '支付状态未更新',
status: 'warning',
duration: 5000,
isClosable: true,
});
}
} else {
throw new Error('网络错误');
}
} catch (error) {
logger.error('SubscriptionContent', 'handleForceUpdatePayment', error, {
orderId: paymentOrder?.id
});
toast({
title: '强制更新失败',
description: error.message,
status: 'error',
duration: 3000,
isClosable: true,
});
} finally {
setForceUpdating(false);
}
};
const handleCheckPaymentStatus = async () => {
if (!paymentOrder) return;
setCheckingPayment(true);
try {
const response = await fetch(`/api/payment/order/${paymentOrder.id}/status`, {
credentials: 'include'
});
if (response.ok) {
const data = await response.json();
logger.info('SubscriptionContent', '手动检查支付状态结果', {
orderId: paymentOrder.id,
paymentSuccess: data.payment_success,
data: data.data
});
if (data.success) {
if (data.payment_success) {
stopAutoPaymentCheck();
logger.info('SubscriptionContent', '手动检测到支付成功', {
orderId: paymentOrder.id
});
toast({
title: '支付成功!',
description: '订阅已激活,正在跳转...',
status: 'success',
duration: 3000,
isClosable: true,
});
setTimeout(() => {
onPaymentModalClose();
window.location.reload();
}, 2000);
} else {
toast({
title: '支付状态检查',
description: data.message || '还未检测到支付,请继续等待',
status: 'info',
duration: 5000,
isClosable: true,
});
}
} else {
throw new Error(data.error || '查询失败');
}
} else {
throw new Error('网络错误');
}
} catch (error) {
logger.error('SubscriptionContent', 'handleCheckPaymentStatus', error, {
orderId: paymentOrder?.id
});
toast({
title: '查询失败',
description: error.message,
status: 'error',
duration: 3000,
isClosable: true,
});
} finally {
setCheckingPayment(false);
}
};
const formatTime = (seconds) => {
const minutes = Math.floor(seconds / 60);
const remainingSeconds = seconds % 60;
return `${minutes.toString().padStart(2, '0')}:${remainingSeconds.toString().padStart(2, '0')}`;
};
const getCurrentPrice = (plan) => {
if (!plan) return 0;
return selectedCycle === 'monthly' ? plan.monthly_price : plan.yearly_price;
};
const getSavingsText = (plan) => {
if (!plan || selectedCycle !== 'yearly') return null;
const yearlyTotal = plan.monthly_price * 12;
const savings = yearlyTotal - plan.yearly_price;
const percentage = Math.round((savings / yearlyTotal) * 100);
return `年付节省 ${percentage}%`;
};
// 统一的功能列表定义 - 基于商业定价(10月15日)文档
const allFeatures = [
// 新闻催化分析模块
{ name: '新闻信息流', free: true, pro: true, max: true },
{ name: '历史事件对比', free: 'TOP3', pro: true, max: true },
{ name: '事件传导链分析(AI)', free: '有限体验', pro: true, max: true },
{ name: '事件-相关标的分析', free: false, pro: true, max: true },
{ name: '相关概念展示', free: false, pro: true, max: true },
{ name: '板块深度分析(AI)', free: false, pro: false, max: true },
// 个股中心模块
{ name: 'AI复盘功能', free: true, pro: true, max: true },
{ name: '企业概览', free: '限制预览', pro: true, max: true },
{ name: '个股深度分析(AI)', free: '10家/月', pro: '50家/月', max: true },
{ name: '高效数据筛选工具', free: false, pro: true, max: true },
// 概念中心模块
{ name: '概念中心(548大概念)', free: 'TOP5', pro: true, max: true },
{ name: '历史时间轴查询', free: false, pro: '100天', max: true },
{ name: '概念高频更新', free: false, pro: false, max: true },
// 涨停分析模块
{ name: '涨停板块数据分析', free: true, pro: true, max: true },
{ name: '个股涨停分析', free: true, pro: true, max: true },
];
return (
<VStack spacing={6} align="stretch" w="100%" py={{base: 4, md: 6}}>
{/* 当前订阅状态 */}
{user && (
<Box
p={6}
borderRadius="xl"
bg={bgCard}
border="1px solid"
borderColor={borderColor}
shadow="sm"
>
<Flex align="center" justify="space-between" flexWrap="wrap" gap={3}>
{/* 左侧:当前订阅状态标签 */}
<HStack spacing={3}>
<Text fontSize="md" fontWeight="bold" color={textColor}>
当前订阅:
</Text>
<Badge
colorScheme={
user.subscription_type === 'max' ? 'purple' :
user.subscription_type === 'pro' ? 'blue' : 'gray'
}
variant="subtle"
px={3}
py={1}
borderRadius="full"
fontSize="sm"
>
{user.subscription_type === 'free' ? '基础版' :
user.subscription_type === 'pro' ? 'Pro 专业版' : 'Max 旗舰版'}
</Badge>
<Badge
colorScheme={user.subscription_status === 'active' ? 'green' : 'red'}
variant="subtle"
px={3}
py={1}
borderRadius="full"
fontSize="sm"
>
{user.subscription_status === 'active' ? '已激活' : '未激活'}
</Badge>
</HStack>
{/* 右侧:到期时间和图标 */}
<HStack spacing={4}>
{user.subscription_end_date && (
<Text fontSize="sm" color={secondaryText}>
到期时间: {new Date(user.subscription_end_date).toLocaleDateString('zh-CN')}
</Text>
)}
{user.subscription_status === 'active' && user.subscription_type !== 'free' && (
<Icon
as={user.subscription_type === 'max' ? FaCrown : FaGem}
color={user.subscription_type === 'max' ? 'purple.400' : 'blue.400'}
boxSize={6}
/>
)}
</HStack>
</Flex>
</Box>
)}
{/* 计费周期选择 */}
<Flex justify="center">
<HStack
spacing={0}
bg={bgAccent}
borderRadius="lg"
p={1}
border="1px solid"
borderColor={borderColor}
>
<Button
variant={selectedCycle === 'monthly' ? 'solid' : 'ghost'}
colorScheme={selectedCycle === 'monthly' ? 'blue' : 'gray'}
size="md"
onClick={() => setSelectedCycle('monthly')}
borderRadius="md"
>
按月付费
</Button>
<Button
variant={selectedCycle === 'yearly' ? 'solid' : 'ghost'}
colorScheme={selectedCycle === 'yearly' ? 'blue' : 'gray'}
size="md"
onClick={() => setSelectedCycle('yearly')}
borderRadius="md"
>
按年付费
<Badge ml={2} colorScheme="red" fontSize="xs">省20%</Badge>
</Button>
</HStack>
</Flex>
{/* 订阅套餐 */}
<Grid
templateColumns={{ base: '1fr', md: 'repeat(2, 1fr)', lg: 'repeat(3, 1fr)' }}
gap={6}
>
{subscriptionPlans.length === 0 ? (
<Box gridColumn={{ base: '1', md: '1 / -1', lg: '1 / -1' }} textAlign="center" py={8}>
<Text color={secondaryText}>正在加载订阅套餐...</Text>
</Box>
) : (
<>
{/* 免费版套餐 */}
<Box
position="relative"
borderRadius="2xl"
overflow="hidden"
border="2px solid"
borderColor="gray.300"
bg={bgCard}
transition="all 0.3s ease"
_hover={{
transform: 'translateY(-4px)',
shadow: 'xl',
}}
>
<VStack
spacing={4}
align="stretch"
p={6}
>
{/* 套餐头部 - 图标与标题同行 */}
<VStack spacing={2} align="stretch">
<Flex justify="space-between" align="center">
<HStack spacing={3}>
<Icon
as={FaStar}
boxSize={8}
color="gray.400"
/>
<Text fontSize="xl" fontWeight="bold" color={textColor}>
基础版
</Text>
</HStack>
<Badge
colorScheme="gray"
variant="outline"
px={4}
py={1}
borderRadius="full"
fontSize="md"
fontWeight="bold"
>
免费
</Badge>
</Flex>
<Text fontSize="xs" color={secondaryText} pl={11}>
免费体验核心功能,7项实用工具
</Text>
</VStack>
<Divider />
{/* 功能列表 */}
<VStack spacing={3} align="stretch" minH="200px">
{allFeatures.map((feature, index) => {
const hasFreeAccess = feature.free === true || typeof feature.free === 'string';
const freeLimit = typeof feature.free === 'string' ? feature.free : null;
return (
<HStack key={index} spacing={3} align="start">
<Icon
as={hasFreeAccess ? FaCheck : FaTimes}
color={hasFreeAccess ? 'blue.500' : 'gray.300'}
boxSize={4}
mt={0.5}
/>
<Text
fontSize="sm"
color={hasFreeAccess ? textColor : secondaryText}
flex={1}
>
{feature.name}
{freeLimit && (
<Text as="span" fontSize="xs" color="blue.500" ml={1}>
({freeLimit})
</Text>
)}
</Text>
</HStack>
);
})}
</VStack>
{/* 订阅按钮 */}
<Button
size="lg"
colorScheme="gray"
variant="solid"
isDisabled={true}
_hover={{
transform: 'scale(1.02)',
}}
transition="all 0.2s"
>
{user?.subscription_type === 'free' &&
user?.subscription_status === 'active'
? '✓ 当前套餐'
: '免费使用'
}
</Button>
</VStack>
</Box>
{/* 付费套餐 */}
{subscriptionPlans.filter(plan => plan && plan.name).map((plan) => (
<Box
key={plan.id}
position="relative"
borderRadius="2xl"
overflow="hidden"
border="2px solid"
borderColor={plan.name === 'max' ? 'purple.400' : 'blue.300'}
bg={bgCard}
transition="all 0.3s ease"
_hover={{
transform: 'translateY(-4px)',
shadow: 'xl',
}}
>
{/* 推荐标签 */}
{plan.name === 'max' && (
<Box
position="absolute"
top={0}
left={0}
right={0}
bg="linear-gradient(135deg, #667eea 0%, #764ba2 100%)"
py={1}
textAlign="center"
zIndex={1}
>
<Text color="white" fontSize="xs" fontWeight="bold">
🔥 最受欢迎
</Text>
</Box>
)}
<VStack
spacing={4}
align="stretch"
p={6}
>
{/* 套餐头部 - 图标与标题同行 */}
<VStack spacing={2} align="stretch">
<Flex justify="space-between" align="center">
<HStack spacing={3}>
<Icon
as={plan.name === 'pro' ? FaGem : FaCrown}
boxSize={8}
color={plan.name === 'pro' ? 'blue.400' : 'purple.400'}
/>
<Text fontSize="xl" fontWeight="bold" color={textColor}>
{plan.display_name}
</Text>
</HStack>
<HStack spacing={0} align="baseline">
<Text fontSize="md" color={secondaryText}>¥</Text>
<Text fontSize="2xl" fontWeight="extrabold" color={plan.name === 'pro' ? 'blue.500' : 'purple.500'}>
{getCurrentPrice(plan).toFixed(0)}
</Text>
<Text fontSize="sm" color={secondaryText}>
/{selectedCycle === 'monthly' ? '' : ''}
</Text>
</HStack>
</Flex>
<Flex justify="space-between" align="center">
<Text fontSize="xs" color={secondaryText} pl={11}>
{plan.description}
</Text>
{getSavingsText(plan) && (
<Badge colorScheme="green" fontSize="xs" px={2} py={1}>
{getSavingsText(plan)}
</Badge>
)}
</Flex>
</VStack>
<Divider />
{/* 功能列表 */}
<VStack spacing={3} align="stretch" minH="200px">
{allFeatures.map((feature, index) => {
const featureValue = feature[plan.name];
const isSupported = featureValue === true || typeof featureValue === 'string';
const limitText = typeof featureValue === 'string' ? featureValue : null;
return (
<HStack key={index} spacing={3} align="start">
<Icon
as={isSupported ? FaCheck : FaTimes}
color={isSupported ? 'blue.500' : 'gray.300'}
boxSize={4}
mt={0.5}
/>
<Text
fontSize="sm"
color={isSupported ? textColor : secondaryText}
flex={1}
>
{feature.name}
{limitText && (
<Text as="span" fontSize="xs" color={plan.name === 'pro' ? 'blue.500' : 'purple.500'} ml={1}>
({limitText})
</Text>
)}
</Text>
</HStack>
);
})}
</VStack>
{/* 订阅按钮 */}
<Button
size="lg"
colorScheme={plan.name === 'max' ? 'purple' : 'blue'}
variant="solid"
onClick={() => handleSubscribe(plan)}
isDisabled={
user?.subscription_type === plan.name &&
user?.subscription_status === 'active'
}
_hover={{
transform: 'scale(1.02)',
}}
transition="all 0.2s"
>
{user?.subscription_type === plan.name &&
user?.subscription_status === 'active'
? '✓ 已订阅'
: `选择 ${plan.display_name}`
}
</Button>
</VStack>
</Box>
))}
</>
)}
</Grid>
{/* FAQ 常见问题 */}
<Box
mt={12}
p={6}
borderRadius="xl"
bg={bgCard}
border="1px solid"
borderColor={borderColor}
shadow="sm"
>
<Heading size="lg" mb={6} textAlign="center" color={textColor}>
常见问题
</Heading>
<VStack spacing={4} align="stretch">
{/* FAQ 1 */}
<Box
border="1px solid"
borderColor={borderColor}
borderRadius="lg"
overflow="hidden"
>
<Flex
p={4}
cursor="pointer"
onClick={() => setOpenFaqIndex(openFaqIndex === 0 ? null : 0)}
bg={openFaqIndex === 0 ? bgAccent : 'transparent'}
_hover={{ bg: bgAccent }}
transition="all 0.2s"
justify="space-between"
align="center"
>
<Text fontWeight="semibold" color={textColor}>
如何取消订阅
</Text>
<Icon
as={openFaqIndex === 0 ? FaChevronUp : FaChevronDown}
color={textColor}
/>
</Flex>
<Collapse in={openFaqIndex === 0}>
<Box p={4} pt={0} color={secondaryText}>
<Text>
您可以随时在账户设置中取消订阅取消后您的订阅将在当前计费周期结束时到期期间您仍可继续使用付费功能取消后不会立即扣款也不会自动续费
</Text>
</Box>
</Collapse>
</Box>
{/* FAQ 2 */}
<Box
border="1px solid"
borderColor={borderColor}
borderRadius="lg"
overflow="hidden"
>
<Flex
p={4}
cursor="pointer"
onClick={() => setOpenFaqIndex(openFaqIndex === 1 ? null : 1)}
bg={openFaqIndex === 1 ? bgAccent : 'transparent'}
_hover={{ bg: bgAccent }}
transition="all 0.2s"
justify="space-between"
align="center"
>
<Text fontWeight="semibold" color={textColor}>
支持哪些支付方式
</Text>
<Icon
as={openFaqIndex === 1 ? FaChevronUp : FaChevronDown}
color={textColor}
/>
</Flex>
<Collapse in={openFaqIndex === 1}>
<Box p={4} pt={0} color={secondaryText}>
<Text>
我们目前支持微信支付扫描支付二维码后系统会自动检测支付状态并激活您的订阅支付过程安全可靠所有交易都经过加密处理
</Text>
</Box>
</Collapse>
</Box>
{/* FAQ 3 */}
<Box
border="1px solid"
borderColor={borderColor}
borderRadius="lg"
overflow="hidden"
>
<Flex
p={4}
cursor="pointer"
onClick={() => setOpenFaqIndex(openFaqIndex === 2 ? null : 2)}
bg={openFaqIndex === 2 ? bgAccent : 'transparent'}
_hover={{ bg: bgAccent }}
transition="all 0.2s"
justify="space-between"
align="center"
>
<Text fontWeight="semibold" color={textColor}>
可以在月付和年付之间切换吗
</Text>
<Icon
as={openFaqIndex === 2 ? FaChevronUp : FaChevronDown}
color={textColor}
/>
</Flex>
<Collapse in={openFaqIndex === 2}>
<Box p={4} pt={0} color={secondaryText}>
<Text>
可以您可以随时更改计费周期如果从月付切换到年付系统会计算剩余价值并应用到新的订阅中年付用户可享受20%的折扣优惠
</Text>
</Box>
</Collapse>
</Box>
{/* FAQ 4 */}
<Box
border="1px solid"
borderColor={borderColor}
borderRadius="lg"
overflow="hidden"
>
<Flex
p={4}
cursor="pointer"
onClick={() => setOpenFaqIndex(openFaqIndex === 3 ? null : 3)}
bg={openFaqIndex === 3 ? bgAccent : 'transparent'}
_hover={{ bg: bgAccent }}
transition="all 0.2s"
justify="space-between"
align="center"
>
<Text fontWeight="semibold" color={textColor}>
是否提供退款
</Text>
<Icon
as={openFaqIndex === 3 ? FaChevronUp : FaChevronDown}
color={textColor}
/>
</Flex>
<Collapse in={openFaqIndex === 3}>
<Box p={4} pt={0} color={secondaryText}>
<Text>
我们提供7天无理由退款保证如果您在订阅后7天内对服务不满意可以申请全额退款超过7天后我们将根据实际使用情况进行评估
</Text>
</Box>
</Collapse>
</Box>
{/* FAQ 5 */}
<Box
border="1px solid"
borderColor={borderColor}
borderRadius="lg"
overflow="hidden"
>
<Flex
p={4}
cursor="pointer"
onClick={() => setOpenFaqIndex(openFaqIndex === 4 ? null : 4)}
bg={openFaqIndex === 4 ? bgAccent : 'transparent'}
_hover={{ bg: bgAccent }}
transition="all 0.2s"
justify="space-between"
align="center"
>
<Text fontWeight="semibold" color={textColor}>
Pro版和Max版有什么区别
</Text>
<Icon
as={openFaqIndex === 4 ? FaChevronUp : FaChevronDown}
color={textColor}
/>
</Flex>
<Collapse in={openFaqIndex === 4}>
<Box p={4} pt={0} color={secondaryText}>
<Text>
Pro版适合个人专业用户提供高级图表历史数据分析等功能Max版则是为团队和企业设计额外提供实时数据推送API访问无限制的数据存储和团队协作功能并享有优先技术支持
</Text>
</Box>
</Collapse>
</Box>
</VStack>
</Box>
{/* 支付模态框 */}
{isPaymentModalOpen && (
<Modal
isOpen={isPaymentModalOpen}
onClose={() => {
stopAutoPaymentCheck();
setPaymentOrder(null);
setPaymentCountdown(0);
onPaymentModalClose();
}}
size="lg"
closeOnOverlayClick={false}
>
<ModalOverlay />
<ModalContent>
<ModalHeader>
<HStack>
<Icon as={FaWeixin} color="green.500" />
<Text>微信支付</Text>
</HStack>
</ModalHeader>
<ModalCloseButton />
<ModalBody pb={6}>
{!paymentOrder ? (
/* 订单确认 */
<VStack spacing={4} align="stretch">
{selectedPlan ? (
<Box p={4} bg={bgAccent} borderRadius="lg">
<Text fontSize="lg" fontWeight="bold" mb={3}>
订单确认
</Text>
<VStack spacing={2} align="stretch">
<Flex justify="space-between">
<Text color={secondaryText}>套餐:</Text>
<Text fontWeight="bold">{selectedPlan.display_name}</Text>
</Flex>
<Flex justify="space-between">
<Text color={secondaryText}>计费周期:</Text>
<Text>{selectedCycle === 'monthly' ? '按月付费' : '按年付费'}</Text>
</Flex>
<Divider />
<Flex justify="space-between" align="baseline">
<Text color={secondaryText}>应付金额:</Text>
<Text fontSize="2xl" fontWeight="bold" color="blue.500">
¥{getCurrentPrice(selectedPlan).toFixed(2)}
</Text>
</Flex>
{getSavingsText(selectedPlan) && (
<Badge colorScheme="green" alignSelf="flex-end" fontSize="xs">
{getSavingsText(selectedPlan)}
</Badge>
)}
</VStack>
</Box>
) : (
<Box p={4} bg="red.50" borderRadius="lg">
<Text color="red.600">请先选择一个订阅套餐</Text>
</Box>
)}
<Button
colorScheme="green"
size="lg"
leftIcon={<Icon as={FaWeixin} />}
onClick={handleCreateOrder}
isLoading={loading}
loadingText="创建订单中..."
isDisabled={!selectedPlan}
>
创建微信支付订单
</Button>
</VStack>
) : (
/* 支付二维码 */
<VStack spacing={4} align="stretch">
<Text textAlign="center" fontSize="lg" fontWeight="bold">
请使用微信扫码支付
</Text>
{/* 倒计时 */}
<Box p={3} bg="orange.50" borderRadius="lg">
<HStack justify="center" spacing={2} mb={2}>
<Icon as={FaClock} color="orange.500" />
<Text color="orange.700" fontSize="sm">
二维码有效时间: {formatTime(paymentCountdown)}
</Text>
</HStack>
<Progress
value={(paymentCountdown / (30 * 60)) * 100}
colorScheme="orange"
size="sm"
borderRadius="full"
/>
</Box>
{/* 二维码 */}
<Box textAlign="center">
{paymentOrder.qr_code_url ? (
<Image
src={paymentOrder.qr_code_url}
alt="微信支付二维码"
mx="auto"
maxW="200px"
border="1px solid"
borderColor={borderColor}
borderRadius="lg"
/>
) : (
<Flex
w="200px"
h="200px"
mx="auto"
bg="gray.100"
alignItems="center"
justifyContent="center"
border="1px solid"
borderColor={borderColor}
borderRadius="lg"
>
<Icon as={FaQrcode} color="gray.400" boxSize={12} />
</Flex>
)}
</Box>
{/* 订单信息 */}
<Box p={4} bg={bgAccent} borderRadius="lg">
<Text fontSize="xs" color={secondaryText} mb={2}>
订单号: {paymentOrder.order_no}
</Text>
<Flex justify="space-between" align="baseline">
<Text color={secondaryText}>支付金额:</Text>
<Text fontSize="xl" fontWeight="bold" color="green.500">
¥{paymentOrder.amount}
</Text>
</Flex>
</Box>
{/* 操作按钮 */}
<VStack spacing={3}>
<HStack spacing={3} w="100%">
<Button
flex={1}
variant="outline"
leftIcon={<Icon as={FaRedo} />}
onClick={handleCheckPaymentStatus}
isLoading={checkingPayment}
loadingText="检查中..."
>
检查支付状态
</Button>
</HStack>
<Button
w="100%"
colorScheme="orange"
variant="solid"
size="sm"
onClick={handleForceUpdatePayment}
isLoading={forceUpdating}
loadingText="强制更新中..."
>
强制更新支付状态
</Button>
<Text fontSize="xs" color={secondaryText} textAlign="center">
支付完成但页面未更新?点击上方"强制更新"按钮
</Text>
</VStack>
{/* 支付状态提示 */}
{autoCheckInterval && (
<Box p={3} bg="blue.50" borderRadius="lg" borderWidth="1px" borderColor="blue.200">
<Text fontSize="sm" color="blue.700" textAlign="center">
🔄 正在自动检查支付状态...
</Text>
</Box>
)}
{/* 支付说明 */}
<VStack align="stretch" spacing={1} fontSize="xs" color={secondaryText}>
<Text> 使用微信"扫一扫"功能扫描上方二维码</Text>
<Text> 支付完成后系统将自动检测并激活订阅</Text>
<Text> 系统每10秒自动检查一次支付状态</Text>
<Text> 如遇问题请联系客服支持</Text>
</VStack>
</VStack>
)}
</ModalBody>
</ModalContent>
</Modal>
)}
</VStack>
);
}