feat: 添加导航徽章

This commit is contained in:
zdl
2025-10-20 13:28:37 +08:00
parent 923611f3a8
commit 44f9fea624
10 changed files with 2677 additions and 1113 deletions

View File

@@ -36,7 +36,7 @@ import {
} from '@chakra-ui/icons';
import ReactECharts from 'echarts-for-react';
import { logger } from '../../../utils/logger';
import { logger } from '../../utils/logger';
// API配置
const API_BASE_URL = process.env.NODE_ENV === 'production' ? "" : (process.env.REACT_APP_API_URL || 'http://localhost:5001');

View File

@@ -59,8 +59,7 @@ import {
FiAlertCircle,
} from 'react-icons/fi';
import MyFutureEvents from './components/MyFutureEvents';
import InvestmentCalendarChakra from './components/InvestmentCalendarChakra';
import InvestmentPlansAndReviews from './components/InvestmentPlansAndReviews';
import InvestmentPlanningCenter from './components/InvestmentPlanningCenter';
export default function CenterDashboard() {
const { user } = useAuth();
@@ -81,26 +80,21 @@ export default function CenterDashboard() {
const [realtimeQuotes, setRealtimeQuotes] = useState({});
const [followingEvents, setFollowingEvents] = useState([]);
const [eventComments, setEventComments] = useState([]);
const [subscriptionInfo, setSubscriptionInfo] = useState({ type: 'free', status: 'active', days_left: 999, is_active: true });
const [loading, setLoading] = useState(true);
const [refreshing, setRefreshing] = useState(false);
const [quotesLoading, setQuotesLoading] = useState(false);
const loadData = useCallback(async () => {
try {
setRefreshing(true);
const base = (process.env.NODE_ENV === 'production' ? '' : process.env.REACT_APP_API_URL || 'http://49.232.185.254:5001');
const ts = Date.now();
const [w, e, c, s] = await Promise.all([
const [w, e, c] = await Promise.all([
fetch(base + `/api/account/watchlist?_=${ts}`, { credentials: 'include', cache: 'no-store', headers: { 'Cache-Control': 'no-cache' } }),
fetch(base + `/api/account/events/following?_=${ts}`, { credentials: 'include', cache: 'no-store', headers: { 'Cache-Control': 'no-cache' } }),
fetch(base + `/api/account/events/comments?_=${ts}`, { credentials: 'include', cache: 'no-store', headers: { 'Cache-Control': 'no-cache' } }),
fetch(base + `/api/subscription/current?_=${ts}`, { credentials: 'include', cache: 'no-store', headers: { 'Cache-Control': 'no-cache' } }),
]);
const jw = await w.json();
const je = await e.json();
const jc = await c.json();
const js = await s.json();
if (jw.success) {
setWatchlist(Array.isArray(jw.data) ? jw.data : []);
// 加载实时行情
@@ -110,18 +104,15 @@ export default function CenterDashboard() {
}
if (je.success) setFollowingEvents(Array.isArray(je.data) ? je.data : []);
if (jc.success) setEventComments(Array.isArray(jc.data) ? jc.data : []);
if (js.success) setSubscriptionInfo(js.data);
} catch (err) {
// ❌ 移除 toast仅 console 输出
logger.error('Center', 'loadData', err, {
userId: user?.id,
timestamp: new Date().toISOString()
});
} finally {
setLoading(false);
setRefreshing(false);
}
}, [user]); // ✅ 移除 toast 依赖
}, [user]);
// 加载实时行情
const loadRealtimeQuotes = useCallback(async () => {
@@ -235,96 +226,11 @@ export default function CenterDashboard() {
return (
<Box bg={sectionBg} minH="100vh">
<Box px={{ base: 4, md: 8 }} py={6} maxW="1400px" mx="auto">
{/* 头部 */}
<Flex justify="space-between" align="center" mb={8}>
<VStack align="start" spacing={1}>
<Heading size="lg" color={textColor}>
个人中心
</Heading>
<Text color={secondaryText} fontSize="sm">
管理您的自选股事件关注和互动记录
</Text>
</VStack>
<Button
leftIcon={<FiRefreshCw />}
onClick={loadData}
isLoading={refreshing}
loadingText="刷新中"
variant="solid"
colorScheme="blue"
size="sm"
>
刷新数据
</Button>
</Flex>
{/* 统计卡片 */}
<SimpleGrid columns={{ base: 1, sm: 2, md: 4 }} spacing={4} mb={8}>
<Card bg={cardBg} shadow="sm">
<CardBody>
<Stat>
<StatLabel color={secondaryText}>自选股票</StatLabel>
<StatNumber fontSize="2xl">{watchlist.length}</StatNumber>
<StatHelpText>
<Icon as={FiTrendingUp} color="green.500" mr={1} />
关注市场动态
</StatHelpText>
</Stat>
</CardBody>
</Card>
<Card bg={cardBg} shadow="sm">
<CardBody>
<Stat>
<StatLabel color={secondaryText}>关注事件</StatLabel>
<StatNumber fontSize="2xl">{followingEvents.length}</StatNumber>
<StatHelpText>
<Icon as={FiActivity} color="blue.500" mr={1} />
追踪热点事件
</StatHelpText>
</Stat>
</CardBody>
</Card>
<Card bg={cardBg} shadow="sm">
<CardBody>
<Stat>
<StatLabel color={secondaryText}>我的评论</StatLabel>
<StatNumber fontSize="2xl">{eventComments.length}</StatNumber>
<StatHelpText>
<Icon as={FiMessageSquare} color="purple.500" mr={1} />
参与讨论
</StatHelpText>
</Stat>
</CardBody>
</Card>
<Card bg={cardBg} shadow="sm" cursor="pointer" onClick={() => navigate('/home/pages/account/subscription')} _hover={{ transform: 'translateY(-2px)', shadow: 'lg' }} transition="all 0.2s">
<CardBody>
<Stat>
<StatLabel color={secondaryText}>订阅状态</StatLabel>
<StatNumber fontSize="xl" color={subscriptionInfo.type === 'free' ? 'gray.500' : subscriptionInfo.type === 'pro' ? 'blue.500' : 'purple.500'}>
{subscriptionInfo.type === 'free' ? '免费版' : subscriptionInfo.type === 'pro' ? 'Pro版' : 'Max版'}
</StatNumber>
<StatHelpText>
<Icon as={FiStar} color={subscriptionInfo.type === 'free' ? 'gray.400' : 'orange.400'} mr={1} />
{subscriptionInfo.type === 'free' ? '点击升级' : `剩余${subscriptionInfo.days_left}`}
</StatHelpText>
</Stat>
</CardBody>
</Card>
</SimpleGrid>
{/* 投资日历 */}
<Box mb={8}>
<InvestmentCalendarChakra />
</Box>
{/* 主要内容区域 */}
<Grid templateColumns={{ base: '1fr', lg: '1fr 2fr' }} gap={6}>
{/* 左:自选股 */}
<Grid templateColumns={{ base: '1fr', md: '1fr 1fr', lg: 'repeat(3, 1fr)' }} gap={6} mb={8}>
{/* 左:自选股 */}
<VStack spacing={6} align="stretch">
<Card bg={cardBg} shadow="md">
<Card bg={cardBg} shadow="md" height="600px" display="flex" flexDirection="column">
<CardHeader pb={4}>
<Flex justify="space-between" align="center">
<HStack>
@@ -335,26 +241,16 @@ export default function CenterDashboard() {
</Badge>
{quotesLoading && <Spinner size="sm" color="blue.500" />}
</HStack>
<HStack>
<IconButton
icon={<FiRefreshCw />}
variant="ghost"
size="sm"
onClick={loadRealtimeQuotes}
isLoading={quotesLoading}
aria-label="刷新行情"
/>
<IconButton
icon={<FiPlus />}
variant="ghost"
size="sm"
onClick={() => navigate('/stock-analysis/overview')}
aria-label="添加自选股"
/>
</HStack>
<IconButton
icon={<FiPlus />}
variant="ghost"
size="sm"
onClick={() => navigate('/stock-analysis/overview')}
aria-label="添加自选股"
/>
</Flex>
</CardHeader>
<CardBody pt={0}>
<CardBody pt={0} flex="1" overflowY="auto">
{watchlist.length === 0 ? (
<Center py={8}>
<VStack spacing={3}>
@@ -440,86 +336,12 @@ export default function CenterDashboard() {
)}
</CardBody>
</Card>
{/* 订阅管理 */}
<Card bg={cardBg} shadow="md">
<CardHeader pb={4}>
<Flex justify="space-between" align="center">
<HStack>
<Icon as={FiStar} color={subscriptionInfo.type === 'free' ? 'gray.500' : subscriptionInfo.type === 'pro' ? 'blue.500' : 'purple.500'} boxSize={5} />
<Heading size="md">我的订阅</Heading>
<Badge
colorScheme={subscriptionInfo.type === 'free' ? 'gray' : subscriptionInfo.type === 'pro' ? 'blue' : 'purple'}
variant="subtle"
>
{subscriptionInfo.type === 'free' ? '免费版' : subscriptionInfo.type === 'pro' ? 'Pro版' : 'Max版'}
</Badge>
</HStack>
<Button
size="sm"
variant="ghost"
colorScheme={subscriptionInfo.type === 'free' ? 'blue' : 'purple'}
onClick={() => navigate('/home/pages/account/subscription')}
>
{subscriptionInfo.type === 'free' ? '升级' : '管理'}
</Button>
</Flex>
</CardHeader>
<CardBody pt={0}>
<VStack align="stretch" spacing={4}>
<Box p={4} borderRadius="md" bg={subscriptionInfo.type === 'free' ? 'gray.50' : subscriptionInfo.type === 'pro' ? 'blue.50' : 'purple.50'} border="1px" borderColor={subscriptionInfo.type === 'free' ? 'gray.200' : subscriptionInfo.type === 'pro' ? 'blue.200' : 'purple.200'}>
<HStack justify="space-between">
<VStack align="start" spacing={1}>
<Text fontSize="sm" fontWeight="medium" color={textColor}>
当前套餐
</Text>
<Text fontSize="lg" fontWeight="bold" color={subscriptionInfo.type === 'free' ? 'gray.600' : subscriptionInfo.type === 'pro' ? 'blue.600' : 'purple.600'}>
{subscriptionInfo.type === 'free' ? '免费版' : subscriptionInfo.type === 'pro' ? 'Pro版本' : 'Max版本'}
</Text>
</VStack>
<VStack align="end" spacing={1}>
<Text fontSize="sm" color={secondaryText}>
{subscriptionInfo.type === 'free' ? '永久免费' : subscriptionInfo.is_active ? '已激活' : '已过期'}
</Text>
{subscriptionInfo.type !== 'free' && (
<Text fontSize="xs" color={subscriptionInfo.days_left > 7 ? 'green.500' : 'orange.500'}>
剩余 {subscriptionInfo.days_left}
</Text>
)}
</VStack>
</HStack>
</Box>
{subscriptionInfo.type === 'free' ? (
<VStack spacing={2}>
<Text fontSize="sm" color={secondaryText} textAlign="center">
升级到Pro或Max版本解锁更多功能
</Text>
<HStack spacing={2}>
<Button size="xs" colorScheme="blue" variant="outline" onClick={() => navigate('/home/pages/account/subscription')}>
Pro ¥0.01/
</Button>
<Button size="xs" colorScheme="purple" variant="outline" onClick={() => navigate('/home/pages/account/subscription')}>
Max ¥0.1/
</Button>
</HStack>
</VStack>
) : (
<Box textAlign="center">
<Text fontSize="sm" color={subscriptionInfo.is_active ? 'green.600' : 'orange.600'}>
{subscriptionInfo.is_active ? '订阅服务正常' : '订阅已过期,请续费'}
</Text>
</Box>
)}
</VStack>
</CardBody>
</Card>
</VStack>
{/* 右侧:事件相关 */}
{/* 中列:关注事件 */}
<VStack spacing={6} align="stretch">
{/* 关注事件 */}
<Card bg={cardBg} shadow="md">
<Card bg={cardBg} shadow="md" height="600px" display="flex" flexDirection="column">
<CardHeader pb={4}>
<Flex justify="space-between" align="center">
<HStack>
@@ -538,7 +360,7 @@ export default function CenterDashboard() {
</Button>
</Flex>
</CardHeader>
<CardBody pt={0}>
<CardBody pt={0} flex="1" overflowY="auto">
{followingEvents.length === 0 ? (
<Center py={8}>
<VStack spacing={3}>
@@ -651,10 +473,12 @@ export default function CenterDashboard() {
</CardBody>
</Card>
{/* 移除“未来事件”板块,根据需求不再展示 */}
</VStack>
{/* 右列:我的评论 */}
<VStack spacing={6} align="stretch">
{/* 我的评论 */}
<Card bg={cardBg} shadow="md">
<Card bg={cardBg} shadow="md" height="600px" display="flex" flexDirection="column">
<CardHeader pb={4}>
<Flex justify="space-between" align="center">
<HStack>
@@ -666,7 +490,7 @@ export default function CenterDashboard() {
</HStack>
</Flex>
</CardHeader>
<CardBody pt={0}>
<CardBody pt={0} flex="1" overflowY="auto">
{eventComments.length === 0 ? (
<Center py={8}>
<VStack spacing={3}>
@@ -723,9 +547,9 @@ export default function CenterDashboard() {
</VStack>
</Grid>
{/* 我的复盘和计划 */}
<Box mt={8}>
<InvestmentPlansAndReviews />
{/* 投资规划中心(整合了日历、计划、复盘) */}
<Box>
<InvestmentPlanningCenter />
</Box>
</Box>
</Box>

File diff suppressed because it is too large Load Diff

View File

@@ -1,916 +1,11 @@
import {
Box,
Button,
Flex,
Grid,
Icon,
Text,
Badge,
Spacer,
VStack,
HStack,
useColorMode,
useColorModeValue,
useToast,
Modal,
ModalOverlay,
ModalContent,
ModalHeader,
ModalBody,
ModalCloseButton,
useDisclosure,
Image,
Progress,
Divider,
} from '@chakra-ui/react';
import React, { useState, useEffect } from 'react';
import { logger } from '../../../utils/logger';
// Custom components
import Card from 'components/Card/Card.js';
import CardHeader from 'components/Card/CardHeader.js';
import IconBox from 'components/Icons/IconBox';
import { HSeparator } from 'components/Separator/Separator';
// Icons
import {
FaWeixin,
FaGem,
FaStar,
FaCheck,
FaQrcode,
FaClock,
FaRedo
} from 'react-icons/fa';
import { BiScan } from 'react-icons/bi';
import { Flex } from '@chakra-ui/react';
import React from 'react';
import SubscriptionContent from 'components/Subscription/SubscriptionContent';
function Subscription() {
// Chakra color mode
const { colorMode } = useColorMode();
const textColor = useColorModeValue('gray.700', 'white');
const borderColor = useColorModeValue('#dee2e6', 'transparent');
const iconBlue = useColorModeValue('blue.500', 'blue.500');
const iconGreen = useColorModeValue('green.500', 'green.500');
const bgCard = useColorModeValue('white', 'gray.800');
const bgAccent = useColorModeValue('blue.50', 'blue.900');
const toast = useToast();
const { isOpen: isPaymentModalOpen, onOpen: onPaymentModalOpen, onClose: onPaymentModalClose } = useDisclosure();
// State
const [subscriptionPlans, setSubscriptionPlans] = useState([]);
const [currentUser, setCurrentUser] = useState(null);
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);
// 加载订阅套餐数据
useEffect(() => {
fetchSubscriptionPlans();
fetchCurrentUser();
}, []);
// 倒计时更新
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('Subscription', '正在获取订阅套餐');
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('Subscription', '套餐加载成功', {
status: response.status,
validPlansCount: validPlans.length
});
setSubscriptionPlans(validPlans);
} else {
logger.warn('Subscription', '套餐数据格式异常', { data });
setSubscriptionPlans([]);
}
} else {
logger.error('Subscription', 'fetchSubscriptionPlans', new Error(`HTTP ${response.status}`));
setSubscriptionPlans([]);
}
} catch (error) {
logger.error('Subscription', 'fetchSubscriptionPlans', error);
setSubscriptionPlans([]);
}
};
const fetchCurrentUser = async () => {
try {
const response = await fetch('/api/auth/session', {
credentials: 'include'
});
if (response.ok) {
const data = await response.json();
logger.debug('Subscription', '用户数据获取成功', { data });
if (data.success) {
setCurrentUser(data.user);
logger.debug('Subscription', '用户信息已更新', {
userId: data.user?.id,
subscriptionType: data.user?.subscription_type,
subscriptionStatus: data.user?.subscription_status
});
}
}
} catch (error) {
logger.error('Subscription', 'fetchCurrentUser', error);
}
};
const handleSubscribe = (plan) => {
if (!currentUser) {
toast({
title: '请先登录',
status: 'warning',
duration: 3000,
isClosable: true,
});
return;
}
if (!plan || !plan.name) {
toast({
title: '套餐信息错误',
status: 'error',
duration: 3000,
isClosable: true,
});
return;
}
setSelectedPlan(plan);
onPaymentModalOpen();
};
const handleCreateOrder = async () => {
if (!selectedPlan) return;
setLoading(true);
try {
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);
// 设置30分钟倒计时
setPaymentCountdown(30 * 60);
// 开始自动检查支付状态每10秒检查一次
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) {
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('Subscription', '开始自动检查支付状态', { 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('Subscription', '支付状态检查结果', {
orderId,
paymentSuccess: data.payment_success,
data
});
if (data.success && data.payment_success) {
// 支付成功
clearInterval(checkInterval);
setAutoCheckInterval(null);
logger.info('Subscription', '自动检测到支付成功', { orderId });
toast({
title: '支付成功!',
description: '订阅已激活,正在跳转...',
status: 'success',
duration: 3000,
isClosable: true,
});
// 延迟2秒后跳转到个人中心
setTimeout(() => {
onPaymentModalClose();
window.location.reload(); // 刷新页面以更新订阅状态
}, 2000);
}
}
} catch (error) {
logger.error('Subscription', 'startAutoPaymentCheck', error, { orderId });
}
}, 10000); // 每10秒检查一次
setAutoCheckInterval(checkInterval);
};
const stopAutoPaymentCheck = () => {
if (autoCheckInterval) {
clearInterval(autoCheckInterval);
setAutoCheckInterval(null);
logger.debug('Subscription', '停止自动检查支付状态');
}
};
// 强制刷新用户状态
const handleRefreshUserStatus = async () => {
try {
await fetchCurrentUser();
toast({
title: '用户状态已刷新',
description: '订阅信息已更新',
status: 'success',
duration: 3000,
isClosable: true,
});
} catch (error) {
toast({
title: '刷新失败',
description: '请稍后重试',
status: 'error',
duration: 3000,
isClosable: true,
});
}
};
// 强制更新支付状态
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('Subscription', '强制更新支付状态结果', {
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('Subscription', '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('Subscription', '手动检查支付状态结果', {
orderId: paymentOrder.id,
paymentSuccess: data.payment_success,
data: data.data
});
if (data.success) {
if (data.payment_success) {
// 支付成功
stopAutoPaymentCheck();
logger.info('Subscription', '手动检测到支付成功', {
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('Subscription', '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}%`;
};
return (
<Flex direction='column' pt={{ base: '120px', md: '75px' }}>
{/* 当前订阅状态 */}
{currentUser && (
<Card p='20px' mb='20px'>
<CardHeader pb='12px'>
<Flex justify='space-between' align='center'>
<Text fontSize='lg' color={textColor} fontWeight='bold'>
当前订阅状态
</Text>
<Button
size='sm'
leftIcon={<Icon as={FaRedo} />}
onClick={handleRefreshUserStatus}
colorScheme='blue'
variant='outline'
>
刷新状态
</Button>
</Flex>
</CardHeader>
<Flex align='center' justify='space-between'>
<Box>
<HStack spacing={2} mb={2}>
<Badge
colorScheme={currentUser.subscription_status === 'active' ? 'green' : 'gray'}
variant='solid'
>
{currentUser.subscription_type === 'free' ? '免费版' :
currentUser.subscription_type === 'pro' ? 'Pro版' : 'Max版'}
</Badge>
<Badge
colorScheme={currentUser.subscription_status === 'active' ? 'green' : 'red'}
>
{currentUser.subscription_status === 'active' ? '已激活' : '未激活'}
</Badge>
</HStack>
{currentUser.subscription_end_date && (
<Text fontSize='sm' color='gray.500'>
到期时间: {new Date(currentUser.subscription_end_date).toLocaleDateString()}
</Text>
)}
{/* 调试信息 */}
<Text fontSize='xs' color='gray.400' mt={2}>
用户ID: {currentUser.id} | 类型: {currentUser.subscription_type} | 状态: {currentUser.subscription_status}
</Text>
</Box>
{currentUser.subscription_status === 'active' && (
<Icon as={FaGem} color='yellow.400' boxSize={6} />
)}
</Flex>
</Card>
)}
{/* 计费周期选择 */}
<Card p='20px' mb='20px'>
<Flex justify='center' mb={6}>
<HStack spacing={0} bg={bgAccent} borderRadius='lg' p={1}>
<Button
variant={selectedCycle === 'monthly' ? 'solid' : 'ghost'}
colorScheme={selectedCycle === 'monthly' ? 'blue' : 'gray'}
size='sm'
onClick={() => setSelectedCycle('monthly')}
>
按月付费
</Button>
<Button
variant={selectedCycle === 'yearly' ? 'solid' : 'ghost'}
colorScheme={selectedCycle === 'yearly' ? 'blue' : 'gray'}
size='sm'
onClick={() => setSelectedCycle('yearly')}
rightIcon={<Badge colorScheme='red' fontSize='xs'>省20%</Badge>}
>
按年付费
</Button>
</HStack>
</Flex>
</Card>
{/* 订阅套餐 */}
<Grid
templateColumns={{ base: '1fr', md: 'repeat(2, 1fr)' }}
gap='24px'
mb='24px'
>
{subscriptionPlans.length === 0 ? (
// 加载状态或空状态
<Box gridColumn={{ base: '1', md: '1 / -1' }} textAlign='center' py={8}>
<Text color='gray.500'>正在加载订阅套餐...</Text>
</Box>
) : (
subscriptionPlans.filter(plan => plan && plan.name).map((plan) => (
<Card
key={plan.id}
p='24px'
position='relative'
border={plan.name === 'max' ? '2px solid' : '1px solid'}
borderColor={plan.name === 'max' ? 'blue.500' : borderColor}
bg={bgCard}
>
{plan.name === 'max' && (
<Badge
position='absolute'
top='-12px'
left='50%'
transform='translateX(-50%)'
colorScheme='blue'
variant='solid'
px={3}
py={1}
fontSize='xs'
>
推荐
</Badge>
)}
<VStack spacing={4} align='stretch'>
{/* 套餐头部 */}
<Flex align='center' justify='space-between'>
<Box>
<Text fontSize='xl' fontWeight='bold' color={textColor}>
{plan.display_name}
</Text>
<Text fontSize='sm' color='gray.500' mt={1}>
{plan.description}
</Text>
</Box>
<IconBox bg={plan.name === 'pro' ? iconBlue : iconGreen} color='white'>
<Icon as={plan.name === 'pro' ? FaStar : FaGem} />
</IconBox>
</Flex>
{/* 价格 */}
<Box textAlign='center' py={4}>
<HStack justify='center' align='baseline'>
<Text fontSize='3xl' fontWeight='bold' color={textColor}>
¥{getCurrentPrice(plan).toFixed(2)}
</Text>
<Text color='gray.500'>
/ {selectedCycle === 'monthly' ? '' : ''}
</Text>
</HStack>
{getSavingsText(plan) && (
<Badge colorScheme='green' mt={2}>
{getSavingsText(plan)}
</Badge>
)}
</Box>
<Divider />
{/* 功能列表 */}
<VStack spacing={3} align='stretch'>
<Text fontSize='sm' fontWeight='semibold' color={textColor}>
包含功能:
</Text>
{plan.features.map((feature, index) => (
<HStack key={index} spacing={3}>
<Icon as={FaCheck} color='green.500' boxSize={3} />
<Text fontSize='sm' color={textColor}>
{feature}
</Text>
</HStack>
))}
</VStack>
{/* 订阅按钮 */}
<Button
colorScheme={plan.name === 'max' ? 'blue' : 'gray'}
variant={plan.name === 'max' ? 'solid' : 'outline'}
size='lg'
onClick={() => handleSubscribe(plan)}
isDisabled={
currentUser?.subscription_type === plan.name &&
currentUser?.subscription_status === 'active'
}
>
{currentUser?.subscription_type === plan.name &&
currentUser?.subscription_status === 'active'
? '已订阅'
: `选择 ${plan.display_name}`
}
</Button>
</VStack>
</Card>
)))}
</Grid>
{/* 支付模态框 - 条件渲染 */}
{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 ? (
<Card p={4} bg={bgAccent}>
<Text fontSize='lg' fontWeight='bold' mb={2}>
订单确认
</Text>
<HStack justify='space-between' mb={2}>
<Text>套餐:</Text>
<Text fontWeight='bold'>{selectedPlan.display_name}</Text>
</HStack>
<HStack justify='space-between' mb={2}>
<Text>计费周期:</Text>
<Text>{selectedCycle === 'monthly' ? '按月付费' : '按年付费'}</Text>
</HStack>
<HStack justify='space-between' mb={2}>
<Text>价格:</Text>
<Text fontSize='lg' fontWeight='bold' color='blue.500'>
¥{getCurrentPrice(selectedPlan).toFixed(2)}
</Text>
</HStack>
{getSavingsText(selectedPlan) && (
<Badge colorScheme='green' alignSelf='flex-end'>
{getSavingsText(selectedPlan)}
</Badge>
)}
</Card>
) : (
<Card p={4} bg='red.50'>
<Text color='red.600'>请先选择一个订阅套餐</Text>
</Card>
)}
<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>
{/* 倒计时 */}
<Card p={4} bg='orange.50'>
<HStack justify='center' spacing={2}>
<Icon as={FaClock} color='orange.500' />
<Text color='orange.700'>
二维码有效时间: {formatTime(paymentCountdown)}
</Text>
</HStack>
<Progress
value={(paymentCountdown / (30 * 60)) * 100}
colorScheme='orange'
size='sm'
mt={2}
/>
</Card>
{/* 二维码 */}
<Box textAlign='center'>
{paymentOrder.qr_code_url ? (
<Image
src={paymentOrder.qr_code_url}
alt='微信支付二维码'
mx='auto'
maxW='200px'
border='1px solid'
borderColor={borderColor}
borderRadius='md'
/>
) : (
<Box
w='200px'
h='200px'
mx='auto'
bg='gray.100'
display='flex'
alignItems='center'
justifyContent='center'
border='1px solid'
borderColor={borderColor}
borderRadius='md'
>
<Icon as={FaQrcode} color='gray.400' boxSize={12} />
</Box>
)}
</Box>
{/* 订单信息 */}
<Card p={4} bg={bgAccent}>
<Text fontSize='sm' color='gray.600' mb={2}>订单号: {paymentOrder.order_no}</Text>
<HStack justify='space-between'>
<Text>支付金额:</Text>
<Text fontSize='lg' fontWeight='bold' color='green.500'>
¥{paymentOrder.amount}
</Text>
</HStack>
</Card>
{/* 操作按钮 */}
<VStack spacing={3}>
<HStack spacing={3} w="100%">
<Button
variant='outline'
leftIcon={<Icon as={FaRedo} />}
onClick={handleCheckPaymentStatus}
isLoading={checkingPayment}
loadingText='检查中...'
flex={1}
>
检查支付状态
</Button>
<Button
size='sm'
variant='ghost'
onClick={() => {
logger.debug('Subscription', '调试信息 - 当前支付订单', { paymentOrder });
logger.debug('Subscription', '调试信息 - 用户信息', { currentUser });
}}
>
调试信息
</Button>
</HStack>
{/* 强制更新按钮 */}
<Button
colorScheme='orange'
variant='solid'
size='sm'
onClick={handleForceUpdatePayment}
isLoading={forceUpdating}
loadingText='强制更新中...'
w="100%"
>
🚀 强制更新支付状态
</Button>
<Text fontSize='xs' color='gray.500' textAlign='center'>
如果支付完成但页面未更新请点击上方"强制更新"按钮
</Text>
</VStack>
{/* 支付状态提示 */}
{autoCheckInterval && (
<Card p={3} bg='blue.50' borderColor='blue.200'>
<HStack justify='center' spacing={2}>
<Text fontSize='sm' color='blue.700'>
🔄 正在自动检查支付状态...
</Text>
</HStack>
</Card>
)}
{/* 支付说明 */}
<Box fontSize='sm' color='gray.500'>
<Text mb={1}> 请使用微信"扫一扫"功能扫描上方二维码</Text>
<Text mb={1}> 支付完成后系统将自动检测并激活订阅</Text>
<Text mb={1}> 系统每10秒自动检查一次支付状态</Text>
<Text> 如遇问题请联系客服支持</Text>
</Box>
</VStack>
)}
</ModalBody>
</ModalContent>
</Modal>
)}
{/* 调试面板 */}
{process.env.NODE_ENV === 'development' && (
<Card p='20px' mt='20px' bg='gray.50' borderColor='gray.200'>
<Text fontSize='md' fontWeight='bold' mb={3} color='gray.700'>
🔧 调试信息
</Text>
<VStack spacing={2} align='stretch' fontSize='sm'>
<HStack justify='space-between'>
<Text color='gray.600'>支付订单:</Text>
<Text color={paymentOrder ? 'green.600' : 'gray.400'}>
{paymentOrder ? `ID: ${paymentOrder.id}` : '无'}
</Text>
</HStack>
<HStack justify='space-between'>
<Text color='gray.600'>自动检查:</Text>
<Text color={autoCheckInterval ? 'blue.600' : 'gray.400'}>
{autoCheckInterval ? '运行中' : '已停止'}
</Text>
</HStack>
<HStack justify='space-between'>
<Text color='gray.600'>订阅状态:</Text>
<Text color={currentUser?.subscription_status === 'active' ? 'green.600' : 'red.600'}>
{currentUser?.subscription_status || '未知'}
</Text>
</HStack>
<HStack justify='space-between'>
<Text color='gray.600'>订阅类型:</Text>
<Text>{currentUser?.subscription_type || '未知'}</Text>
</HStack>
</VStack>
<Divider my={3} />
<HStack spacing={2}>
<Button size='sm' onClick={() => logger.debug('Subscription', '调试 - 当前状态', {
currentUser,
paymentOrder,
autoCheckInterval: autoCheckInterval ? '运行中' : '已停止'
})}>
打印状态
</Button>
<Button size='sm' onClick={handleRefreshUserStatus}>
强制刷新
</Button>
</HStack>
</Card>
)}
<Flex direction='column' pt={{ base: '120px', md: '75px' }} px={{ base: '20px', md: '40px' }}>
<SubscriptionContent />
</Flex>
);
}