1723 lines
61 KiB
JavaScript
1723 lines
61 KiB
JavaScript
// 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,
|
||
Input,
|
||
InputGroup,
|
||
InputRightElement,
|
||
} 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 [selectedCycleOption, setSelectedCycleOption] = useState(null); // 当前选中的pricing_option对象
|
||
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);
|
||
|
||
// 优惠码相关state
|
||
const [promoCode, setPromoCode] = useState('');
|
||
const [promoCodeApplied, setPromoCodeApplied] = useState(false);
|
||
const [promoCodeError, setPromoCodeError] = useState('');
|
||
const [validatingPromo, setValidatingPromo] = useState(false);
|
||
const [priceInfo, setPriceInfo] = 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 calculatePrice = async (plan, cycle, promoCodeValue = null) => {
|
||
try {
|
||
// 确保优惠码值正确:只接受非空字符串,其他情况传null
|
||
const validPromoCode = promoCodeValue && typeof promoCodeValue === 'string' && promoCodeValue.trim()
|
||
? promoCodeValue.trim()
|
||
: null;
|
||
|
||
logger.debug('SubscriptionContent', '计算价格', {
|
||
plan: plan.name,
|
||
cycle,
|
||
promoCodeValue,
|
||
validPromoCode
|
||
});
|
||
|
||
const response = await fetch('/api/subscription/calculate-price', {
|
||
method: 'POST',
|
||
headers: {
|
||
'Content-Type': 'application/json',
|
||
},
|
||
credentials: 'include',
|
||
body: JSON.stringify({
|
||
to_plan: plan.name,
|
||
to_cycle: cycle,
|
||
promo_code: validPromoCode
|
||
})
|
||
});
|
||
|
||
if (response.ok) {
|
||
const data = await response.json();
|
||
if (data.success) {
|
||
setPriceInfo(data.data);
|
||
return data.data;
|
||
}
|
||
}
|
||
return null;
|
||
} catch (error) {
|
||
logger.error('SubscriptionContent', 'calculatePrice', error);
|
||
return null;
|
||
}
|
||
};
|
||
|
||
// 验证优惠码
|
||
const handleValidatePromoCode = async () => {
|
||
const trimmedCode = promoCode.trim();
|
||
|
||
if (!trimmedCode) {
|
||
setPromoCodeError('请输入优惠码');
|
||
return;
|
||
}
|
||
|
||
if (!selectedPlan) {
|
||
setPromoCodeError('请先选择套餐');
|
||
return;
|
||
}
|
||
|
||
setValidatingPromo(true);
|
||
setPromoCodeError('');
|
||
|
||
try {
|
||
// 重新计算价格,包含优惠码(使用去除空格后的值)
|
||
const result = await calculatePrice(selectedPlan, selectedCycle, trimmedCode);
|
||
|
||
if (result && !result.promo_error) {
|
||
setPromoCodeApplied(true);
|
||
toast({
|
||
title: '优惠码已应用',
|
||
description: `节省 ¥${result.discount_amount.toFixed(2)}`,
|
||
status: 'success',
|
||
duration: 3000,
|
||
isClosable: true,
|
||
});
|
||
} else {
|
||
setPromoCodeError(result?.promo_error || '优惠码无效');
|
||
setPromoCodeApplied(false);
|
||
}
|
||
} catch (error) {
|
||
setPromoCodeError('验证失败,请重试');
|
||
setPromoCodeApplied(false);
|
||
} finally {
|
||
setValidatingPromo(false);
|
||
}
|
||
};
|
||
|
||
// 移除优惠码
|
||
const handleRemovePromoCode = async () => {
|
||
setPromoCode('');
|
||
setPromoCodeApplied(false);
|
||
setPromoCodeError('');
|
||
// 重新计算价格(不含优惠码)
|
||
if (selectedPlan) {
|
||
await calculatePrice(selectedPlan, selectedCycle, null);
|
||
}
|
||
};
|
||
|
||
const handleSubscribe = async (plan) => {
|
||
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);
|
||
|
||
// 计算价格(包含升级判断)
|
||
await calculatePrice(plan, selectedCycle, promoCodeApplied ? promoCode : null);
|
||
|
||
onPaymentModalOpen();
|
||
};
|
||
|
||
const handleCreateOrder = async () => {
|
||
if (!selectedPlan) return;
|
||
|
||
setLoading(true);
|
||
try {
|
||
const price = priceInfo?.final_amount || (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,
|
||
promo_code: promoCodeApplied ? promoCode : null
|
||
})
|
||
});
|
||
|
||
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;
|
||
|
||
// 如果有pricing_options,使用它
|
||
if (plan.pricing_options && plan.pricing_options.length > 0) {
|
||
// 查找当前选中的周期选项
|
||
const option = plan.pricing_options.find(opt =>
|
||
opt.cycle_key === selectedCycle ||
|
||
(selectedCycle === 'monthly' && opt.months === 1) ||
|
||
(selectedCycle === 'yearly' && opt.months === 12)
|
||
);
|
||
return option ? option.price : plan.monthly_price;
|
||
}
|
||
|
||
// 向后兼容:回退到monthly_price/yearly_price
|
||
return selectedCycle === 'monthly' ? plan.monthly_price : plan.yearly_price;
|
||
};
|
||
|
||
const getSavingsText = (plan) => {
|
||
if (!plan) return null;
|
||
|
||
// 如果有pricing_options,从中查找discount_percent
|
||
if (plan.pricing_options && plan.pricing_options.length > 0) {
|
||
const currentOption = plan.pricing_options.find(opt =>
|
||
opt.cycle_key === selectedCycle ||
|
||
(selectedCycle === 'monthly' && opt.months === 1) ||
|
||
(selectedCycle === 'yearly' && opt.months === 12)
|
||
);
|
||
|
||
if (currentOption && currentOption.discount_percent) {
|
||
return `立减 ${currentOption.discount_percent}%`;
|
||
}
|
||
|
||
// 如果没有discount_percent,尝试计算节省金额
|
||
if (currentOption && currentOption.months > 1) {
|
||
const monthlyOption = plan.pricing_options.find(opt => opt.months === 1);
|
||
if (monthlyOption) {
|
||
const expectedTotal = monthlyOption.price * currentOption.months;
|
||
const savings = expectedTotal - currentOption.price;
|
||
if (savings > 0) {
|
||
const percentage = Math.round((savings / expectedTotal) * 100);
|
||
return `${currentOption.label || `${currentOption.months}个月`}节省 ${percentage}%`;
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
// 向后兼容:计算年付节省
|
||
if (selectedCycle === 'yearly') {
|
||
const yearlyTotal = plan.monthly_price * 12;
|
||
const savings = yearlyTotal - plan.yearly_price;
|
||
if (savings > 0) {
|
||
const percentage = Math.round((savings / yearlyTotal) * 100);
|
||
return `年付节省 ${percentage}%`;
|
||
}
|
||
}
|
||
|
||
return null;
|
||
};
|
||
|
||
// 获取按钮文字(根据用户当前订阅判断是升级还是新订阅)
|
||
const getButtonText = (plan, user) => {
|
||
if (!user || user.subscription_type === 'free') {
|
||
return `选择 ${plan.display_name}`;
|
||
}
|
||
|
||
// 判断是否为升级
|
||
const planLevels = { 'free': 0, 'pro': 1, 'max': 2 };
|
||
const currentLevel = planLevels[user.subscription_type] || 0;
|
||
const targetLevel = planLevels[plan.name] || 0;
|
||
|
||
if (targetLevel > currentLevel) {
|
||
return `升级至 ${plan.display_name}`;
|
||
} else if (targetLevel < currentLevel) {
|
||
return `切换至 ${plan.display_name}`;
|
||
} else {
|
||
// 同级别,可能是切换周期
|
||
return `切换至 ${plan.display_name}`;
|
||
}
|
||
};
|
||
|
||
// 统一的功能列表定义 - 基于商业定价(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>
|
||
)}
|
||
|
||
{/* 计费周期选择 */}
|
||
<Box>
|
||
<Text textAlign="center" fontSize="sm" color={secondaryText} mb={3}>
|
||
选择计费周期 · 时长越长优惠越大
|
||
</Text>
|
||
<Flex justify="center" mb={2}>
|
||
<HStack
|
||
spacing={2}
|
||
bg={bgAccent}
|
||
borderRadius="xl"
|
||
p={2}
|
||
border="1px solid"
|
||
borderColor={borderColor}
|
||
flexWrap="wrap"
|
||
justify="center"
|
||
>
|
||
{(() => {
|
||
// 获取第一个套餐的pricing_options作为周期选项(假设所有套餐都有相同的周期)
|
||
const firstPlan = subscriptionPlans.find(plan => plan.pricing_options);
|
||
const cycleOptions = firstPlan?.pricing_options || [
|
||
{ cycle_key: 'monthly', label: '月付', months: 1 },
|
||
{ cycle_key: 'yearly', label: '年付', months: 12, discount_percent: 20 }
|
||
];
|
||
|
||
return cycleOptions.map((option, index) => {
|
||
const cycleKey = option.cycle_key || (option.months === 1 ? 'monthly' : option.months === 12 ? 'yearly' : `${option.months}months`);
|
||
const isSelected = selectedCycle === cycleKey;
|
||
const hasDiscount = option.discount_percent && option.discount_percent > 0;
|
||
|
||
return (
|
||
<VStack
|
||
key={index}
|
||
spacing={0}
|
||
position="relative"
|
||
>
|
||
{/* 折扣标签 */}
|
||
{hasDiscount && (
|
||
<Badge
|
||
position="absolute"
|
||
top="-8px"
|
||
colorScheme="red"
|
||
fontSize="xs"
|
||
px={2}
|
||
borderRadius="full"
|
||
fontWeight="bold"
|
||
>
|
||
省{option.discount_percent}%
|
||
</Badge>
|
||
)}
|
||
|
||
<Button
|
||
variant={isSelected ? 'solid' : 'outline'}
|
||
colorScheme={isSelected ? 'blue' : 'gray'}
|
||
size="md"
|
||
onClick={() => setSelectedCycle(cycleKey)}
|
||
borderRadius="lg"
|
||
minW="80px"
|
||
h="50px"
|
||
position="relative"
|
||
_hover={{
|
||
transform: 'translateY(-2px)',
|
||
shadow: 'md'
|
||
}}
|
||
transition="all 0.2s"
|
||
>
|
||
<VStack spacing={0}>
|
||
<Text fontSize="md" fontWeight="bold">
|
||
{option.label || `${option.months}个月`}
|
||
</Text>
|
||
{hasDiscount && (
|
||
<Text fontSize="xs" color={isSelected ? 'white' : 'gray.500'}>
|
||
更优惠
|
||
</Text>
|
||
)}
|
||
</VStack>
|
||
</Button>
|
||
</VStack>
|
||
);
|
||
});
|
||
})()}
|
||
</HStack>
|
||
</Flex>
|
||
{/* 提示文字 */}
|
||
{(() => {
|
||
const firstPlan = subscriptionPlans.find(plan => plan.pricing_options);
|
||
const cycleOptions = firstPlan?.pricing_options || [];
|
||
const currentOption = cycleOptions.find(opt =>
|
||
opt.cycle_key === selectedCycle ||
|
||
(selectedCycle === 'monthly' && opt.months === 1) ||
|
||
(selectedCycle === 'yearly' && opt.months === 12)
|
||
);
|
||
|
||
if (currentOption && currentOption.discount_percent > 0) {
|
||
return (
|
||
<Text textAlign="center" fontSize="sm" color="green.600" fontWeight="medium">
|
||
🎉 当前选择可节省 {currentOption.discount_percent}% 的费用
|
||
</Text>
|
||
);
|
||
}
|
||
return null;
|
||
})()}
|
||
</Box>
|
||
|
||
{/* 订阅套餐 */}
|
||
<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}>
|
||
{(() => {
|
||
if (plan.pricing_options) {
|
||
const option = plan.pricing_options.find(opt =>
|
||
opt.cycle_key === selectedCycle ||
|
||
(selectedCycle === 'monthly' && opt.months === 1) ||
|
||
(selectedCycle === 'yearly' && opt.months === 12)
|
||
);
|
||
if (option) {
|
||
// 如果months是1,显示"/月";如果是12,显示"/年";否则显示周期label
|
||
if (option.months === 1) return '/月';
|
||
if (option.months === 12) return '/年';
|
||
return `/${option.months}个月`;
|
||
}
|
||
}
|
||
return selectedCycle === 'monthly' ? '/月' : '/年';
|
||
})()}
|
||
</Text>
|
||
</HStack>
|
||
</Flex>
|
||
<Flex justify="space-between" align="center" flexWrap="wrap" gap={2}>
|
||
<Text fontSize="xs" color={secondaryText} pl={11} flex={1}>
|
||
{plan.description}
|
||
</Text>
|
||
{(() => {
|
||
// 获取当前选中的周期信息
|
||
if (plan.pricing_options) {
|
||
const currentOption = plan.pricing_options.find(opt =>
|
||
opt.cycle_key === selectedCycle ||
|
||
(selectedCycle === 'monthly' && opt.months === 1) ||
|
||
(selectedCycle === 'yearly' && opt.months === 12)
|
||
);
|
||
|
||
if (currentOption && currentOption.discount_percent > 0) {
|
||
// 计算原价和节省金额
|
||
const monthlyOption = plan.pricing_options.find(opt => opt.months === 1);
|
||
if (monthlyOption) {
|
||
const originalPrice = monthlyOption.price * currentOption.months;
|
||
const savedAmount = originalPrice - currentOption.price;
|
||
|
||
return (
|
||
<VStack spacing={0} align="flex-end">
|
||
<Badge colorScheme="red" fontSize="xs" px={3} py={1} borderRadius="full">
|
||
立省 {currentOption.discount_percent}%
|
||
</Badge>
|
||
<Text fontSize="xs" color="gray.500" textDecoration="line-through">
|
||
原价¥{originalPrice.toFixed(0)}
|
||
</Text>
|
||
<Text fontSize="xs" color="green.600" fontWeight="bold">
|
||
省¥{savedAmount.toFixed(0)}
|
||
</Text>
|
||
</VStack>
|
||
);
|
||
}
|
||
|
||
return (
|
||
<Badge colorScheme="green" fontSize="xs" px={3} py={1} borderRadius="full">
|
||
{getSavingsText(plan)}
|
||
</Badge>
|
||
);
|
||
}
|
||
}
|
||
return null;
|
||
})()}
|
||
</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' &&
|
||
user?.billing_cycle === selectedCycle
|
||
}
|
||
_hover={{
|
||
transform: 'scale(1.02)',
|
||
}}
|
||
transition="all 0.2s"
|
||
>
|
||
{user?.subscription_type === plan.name &&
|
||
user?.subscription_status === 'active' &&
|
||
user?.billing_cycle === selectedCycle
|
||
? '✓ 当前套餐'
|
||
: getButtonText(plan, user)
|
||
}
|
||
</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}>
|
||
<VStack spacing={2} align="stretch">
|
||
<Text>
|
||
当您升级套餐或切换计费周期时,系统会自动计算您当前订阅的剩余价值并用于抵扣新套餐的费用。
|
||
</Text>
|
||
<Text fontWeight="medium" fontSize="sm">
|
||
计算方式:
|
||
</Text>
|
||
<Text fontSize="sm" pl={3}>
|
||
• <strong>剩余价值</strong> = 原套餐价格 × (剩余天数 / 总天数)
|
||
</Text>
|
||
<Text fontSize="sm" pl={3}>
|
||
• <strong>实付金额</strong> = 新套餐价格 - 剩余价值 - 优惠码折扣
|
||
</Text>
|
||
<Text fontSize="sm" color="blue.600" mt={2}>
|
||
例如:您购买了年付Pro版(¥999),使用了180天后升级到Max版(¥1999/年),剩余价值约¥500将自动抵扣,实付约¥1499。
|
||
</Text>
|
||
</VStack>
|
||
</Box>
|
||
</Collapse>
|
||
</Box>
|
||
|
||
{/* FAQ 4 - 原FAQ 3 */}
|
||
<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>
|
||
可以。您可以随时更改计费周期。如果从月付切换到年付,系统会计算剩余价值并应用到新的订阅中。年付用户可享受20%的折扣优惠。
|
||
</Text>
|
||
</Box>
|
||
</Collapse>
|
||
</Box>
|
||
|
||
{/* FAQ 5 - 原FAQ 4 */}
|
||
<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}>
|
||
是否支持退款?
|
||
</Text>
|
||
<Icon
|
||
as={openFaqIndex === 4 ? FaChevronUp : FaChevronDown}
|
||
color={textColor}
|
||
/>
|
||
</Flex>
|
||
<Collapse in={openFaqIndex === 4}>
|
||
<Box p={4} pt={0} color={secondaryText}>
|
||
<VStack spacing={2} align="stretch">
|
||
<Text>
|
||
为了保障服务质量和维护公平的商业环境,我们<strong>不支持退款</strong>。
|
||
</Text>
|
||
<Text fontSize="sm">
|
||
建议您在订阅前:
|
||
</Text>
|
||
<Text fontSize="sm" pl={3}>
|
||
• 充分了解各套餐的功能差异
|
||
</Text>
|
||
<Text fontSize="sm" pl={3}>
|
||
• 使用免费版体验基础功能
|
||
</Text>
|
||
<Text fontSize="sm" pl={3}>
|
||
• 根据实际需求选择合适的计费周期
|
||
</Text>
|
||
<Text fontSize="sm" pl={3}>
|
||
• 如有疑问可联系客服咨询
|
||
</Text>
|
||
<Text fontSize="sm" color="blue.600" mt={2}>
|
||
提示:选择长期套餐(如半年付、年付)可享受更大折扣,性价比更高。
|
||
</Text>
|
||
</VStack>
|
||
</Box>
|
||
</Collapse>
|
||
</Box>
|
||
|
||
{/* FAQ 6 - 原FAQ 5 */}
|
||
<Box
|
||
border="1px solid"
|
||
borderColor={borderColor}
|
||
borderRadius="lg"
|
||
overflow="hidden"
|
||
>
|
||
<Flex
|
||
p={4}
|
||
cursor="pointer"
|
||
onClick={() => setOpenFaqIndex(openFaqIndex === 5 ? null : 5)}
|
||
bg={openFaqIndex === 5 ? 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 === 5 ? FaChevronUp : FaChevronDown}
|
||
color={textColor}
|
||
/>
|
||
</Flex>
|
||
<Collapse in={openFaqIndex === 5}>
|
||
<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);
|
||
// 清空优惠码状态
|
||
setPromoCode('');
|
||
setPromoCodeApplied(false);
|
||
setPromoCodeError('');
|
||
setPriceInfo(null);
|
||
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>
|
||
{(() => {
|
||
if (selectedPlan?.pricing_options) {
|
||
const option = selectedPlan.pricing_options.find(opt =>
|
||
opt.cycle_key === selectedCycle ||
|
||
(selectedCycle === 'monthly' && opt.months === 1) ||
|
||
(selectedCycle === 'yearly' && opt.months === 12)
|
||
);
|
||
return option?.label || (selectedCycle === 'monthly' ? '按月付费' : '按年付费');
|
||
}
|
||
return selectedCycle === 'monthly' ? '按月付费' : '按年付费';
|
||
})()}
|
||
</Text>
|
||
</Flex>
|
||
|
||
{/* 价格明细 */}
|
||
<Divider my={2} />
|
||
|
||
{priceInfo && priceInfo.is_upgrade && (
|
||
<Box bg="blue.50" p={3} borderRadius="md" mb={2}>
|
||
<HStack spacing={2} mb={2}>
|
||
<Icon as={FaCheck} color="blue.500" boxSize={4} />
|
||
<Text fontSize="sm" fontWeight="bold" color="blue.700">
|
||
{priceInfo.upgrade_type === 'plan_upgrade' ? '套餐升级' :
|
||
priceInfo.upgrade_type === 'cycle_change' ? '周期变更' : '套餐和周期调整'}
|
||
</Text>
|
||
</HStack>
|
||
<VStack spacing={1} align="stretch" fontSize="xs">
|
||
<Flex justify="space-between" color="gray.600">
|
||
<Text>当前订阅: {priceInfo.current_plan === 'pro' ? 'Pro版' : 'Max版'} ({priceInfo.current_cycle === 'monthly' ? '月付' : '年付'})</Text>
|
||
</Flex>
|
||
<Flex justify="space-between" color="gray.600">
|
||
<Text>剩余价值:</Text>
|
||
<Text>¥{priceInfo.remaining_value.toFixed(2)}</Text>
|
||
</Flex>
|
||
</VStack>
|
||
</Box>
|
||
)}
|
||
|
||
<Flex justify="space-between">
|
||
<Text color={secondaryText}>
|
||
{priceInfo && priceInfo.is_upgrade ? '新套餐价格:' : '套餐价格:'}
|
||
</Text>
|
||
<Text fontWeight="medium">
|
||
¥{priceInfo ? priceInfo.new_plan_price.toFixed(2) : getCurrentPrice(selectedPlan).toFixed(2)}
|
||
</Text>
|
||
</Flex>
|
||
|
||
{priceInfo && priceInfo.is_upgrade && priceInfo.remaining_value > 0 && (
|
||
<Flex justify="space-between" color="blue.600">
|
||
<Text>已付剩余抵扣:</Text>
|
||
<Text>-¥{priceInfo.remaining_value.toFixed(2)}</Text>
|
||
</Flex>
|
||
)}
|
||
|
||
{priceInfo && priceInfo.discount_amount > 0 && (
|
||
<Flex justify="space-between" color="green.600">
|
||
<Text>优惠码折扣:</Text>
|
||
<Text>-¥{priceInfo.discount_amount.toFixed(2)}</Text>
|
||
</Flex>
|
||
)}
|
||
|
||
<Divider />
|
||
<Flex justify="space-between" align="baseline">
|
||
<Text fontSize="lg" fontWeight="bold">实付金额:</Text>
|
||
<Text fontSize="2xl" fontWeight="bold" color="blue.500">
|
||
¥{priceInfo ? priceInfo.final_amount.toFixed(2) : getCurrentPrice(selectedPlan).toFixed(2)}
|
||
</Text>
|
||
</Flex>
|
||
{getSavingsText(selectedPlan) && !priceInfo?.is_upgrade && (
|
||
<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>
|
||
)}
|
||
|
||
{/* 优惠码输入 */}
|
||
{selectedPlan && (
|
||
<Box>
|
||
<HStack spacing={2}>
|
||
<Input
|
||
placeholder="输入优惠码(可选)"
|
||
value={promoCode}
|
||
onChange={(e) => {
|
||
setPromoCode(e.target.value.toUpperCase());
|
||
setPromoCodeError('');
|
||
}}
|
||
size="md"
|
||
isDisabled={promoCodeApplied}
|
||
/>
|
||
<Button
|
||
colorScheme="purple"
|
||
onClick={handleValidatePromoCode}
|
||
isLoading={validatingPromo}
|
||
isDisabled={!promoCode || promoCodeApplied}
|
||
minW="80px"
|
||
>
|
||
应用
|
||
</Button>
|
||
</HStack>
|
||
{promoCodeError && (
|
||
<Text color="red.500" fontSize="sm" mt={2}>
|
||
{promoCodeError}
|
||
</Text>
|
||
)}
|
||
{promoCodeApplied && priceInfo && (
|
||
<HStack mt={2} p={2} bg="green.50" borderRadius="md">
|
||
<Icon as={FaCheck} color="green.500" />
|
||
<Text color="green.700" fontSize="sm" fontWeight="medium" flex={1}>
|
||
优惠码已应用!节省 ¥{priceInfo.discount_amount.toFixed(2)}
|
||
</Text>
|
||
<Icon
|
||
as={FaTimes}
|
||
color="gray.500"
|
||
cursor="pointer"
|
||
onClick={handleRemovePromoCode}
|
||
_hover={{ color: 'red.500' }}
|
||
/>
|
||
</HStack>
|
||
)}
|
||
</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>
|
||
);
|
||
}
|