feat: 添加导航徽章
This commit is contained in:
@@ -190,6 +190,12 @@ export default function AuthFormContent() {
|
|||||||
credential: credential.substring(0, 3) + '****' + credential.substring(7),
|
credential: credential.substring(0, 3) + '****' + credential.substring(7),
|
||||||
dev_code: data.dev_code
|
dev_code: data.dev_code
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// ✅ 开发环境下在控制台显示验证码
|
||||||
|
if (data.dev_code) {
|
||||||
|
console.log(`%c✅ [验证码] ${credential} -> ${data.dev_code}`, 'color: #16a34a; font-weight: bold; font-size: 14px;');
|
||||||
|
}
|
||||||
|
|
||||||
setVerificationCodeSent(true);
|
setVerificationCodeSent(true);
|
||||||
setCountdown(60);
|
setCountdown(60);
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -55,8 +55,12 @@ import {
|
|||||||
ChakraLogoLight,
|
ChakraLogoLight,
|
||||||
} from "components/Icons/Icons";
|
} from "components/Icons/Icons";
|
||||||
import { useAuth } from "contexts/AuthContext";
|
import { useAuth } from "contexts/AuthContext";
|
||||||
|
import SubscriptionBadge from "components/Subscription/SubscriptionBadge";
|
||||||
|
import SubscriptionModal from "components/Subscription/SubscriptionModal";
|
||||||
|
|
||||||
export default function HeaderLinks(props) {
|
export default function HeaderLinks(props) {
|
||||||
|
console.log('🚀 [AdminNavbarLinks] 组件已加载');
|
||||||
|
|
||||||
const {
|
const {
|
||||||
variant,
|
variant,
|
||||||
children,
|
children,
|
||||||
@@ -71,6 +75,68 @@ export default function HeaderLinks(props) {
|
|||||||
const { user, isAuthenticated, logout } = useAuth();
|
const { user, isAuthenticated, logout } = useAuth();
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
|
|
||||||
|
console.log('👤 [AdminNavbarLinks] 用户状态:', { user, isAuthenticated });
|
||||||
|
|
||||||
|
// 订阅信息状态
|
||||||
|
const [subscriptionInfo, setSubscriptionInfo] = React.useState({
|
||||||
|
type: 'free',
|
||||||
|
status: 'active',
|
||||||
|
days_left: 0,
|
||||||
|
is_active: true
|
||||||
|
});
|
||||||
|
const [isSubscriptionModalOpen, setIsSubscriptionModalOpen] = React.useState(false);
|
||||||
|
|
||||||
|
// 加载订阅信息
|
||||||
|
React.useEffect(() => {
|
||||||
|
console.log('🔍 [AdminNavbarLinks] 订阅徽章 - 认证状态:', isAuthenticated, 'userId:', user?.id);
|
||||||
|
|
||||||
|
if (isAuthenticated && user) {
|
||||||
|
const loadSubscriptionInfo = async () => {
|
||||||
|
try {
|
||||||
|
const base = (process.env.NODE_ENV === 'production' ? '' : process.env.REACT_APP_API_URL || 'http://49.232.185.254:5001');
|
||||||
|
console.log('🌐 [AdminNavbarLinks] 订阅徽章 - API:', base + '/api/subscription/current');
|
||||||
|
const response = await fetch(base + '/api/subscription/current', {
|
||||||
|
credentials: 'include',
|
||||||
|
});
|
||||||
|
console.log('📡 [AdminNavbarLinks] 订阅徽章 - 响应:', response.status);
|
||||||
|
if (response.ok) {
|
||||||
|
const data = await response.json();
|
||||||
|
console.log('✅ [AdminNavbarLinks] 订阅徽章 - 完整响应数据:', data);
|
||||||
|
console.log('🔍 [AdminNavbarLinks] 订阅徽章 - data.data:', data.data);
|
||||||
|
console.log('🔍 [AdminNavbarLinks] 订阅徽章 - type值:', data.data?.type, '类型:', typeof data.data?.type);
|
||||||
|
|
||||||
|
if (data.success && data.data) {
|
||||||
|
// 数据标准化处理:确保type字段是小写的 'free', 'pro', 或 'max'
|
||||||
|
const normalizedData = {
|
||||||
|
type: (data.data.type || data.data.subscription_type || 'free').toLowerCase(),
|
||||||
|
status: data.data.status || 'active',
|
||||||
|
days_left: data.data.days_left || 0,
|
||||||
|
is_active: data.data.is_active !== false,
|
||||||
|
end_date: data.data.end_date || null
|
||||||
|
};
|
||||||
|
console.log('✨ [AdminNavbarLinks] 订阅徽章 - 标准化后:', normalizedData);
|
||||||
|
setSubscriptionInfo(normalizedData);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
console.warn('⚠️ [AdminNavbarLinks] 订阅徽章 - API 失败,使用默认值');
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('❌ [AdminNavbarLinks] 订阅徽章 - 错误:', error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
loadSubscriptionInfo();
|
||||||
|
} else {
|
||||||
|
// 用户未登录时,重置为免费版
|
||||||
|
console.warn('🚫 [AdminNavbarLinks] 订阅徽章 - 用户未登录,重置为免费版');
|
||||||
|
setSubscriptionInfo({
|
||||||
|
type: 'free',
|
||||||
|
status: 'active',
|
||||||
|
days_left: 0,
|
||||||
|
is_active: true
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}, [isAuthenticated, user?.id]); // 只依赖 user.id 而不是整个 user 对象
|
||||||
|
|
||||||
// Chakra Color Mode
|
// Chakra Color Mode
|
||||||
let navbarIcon =
|
let navbarIcon =
|
||||||
fixed && scrolled
|
fixed && scrolled
|
||||||
@@ -94,7 +160,23 @@ export default function HeaderLinks(props) {
|
|||||||
flexDirection="row"
|
flexDirection="row"
|
||||||
>
|
>
|
||||||
<SearchBar me="18px" />
|
<SearchBar me="18px" />
|
||||||
|
|
||||||
|
{/* 订阅状态徽章 - 仅登录用户可见 */}
|
||||||
|
{console.log('🎨 [订阅徽章] 渲染:', { isAuthenticated, subscriptionInfo })}
|
||||||
|
{isAuthenticated && (
|
||||||
|
<>
|
||||||
|
<SubscriptionBadge
|
||||||
|
subscriptionInfo={subscriptionInfo}
|
||||||
|
onClick={() => setIsSubscriptionModalOpen(true)}
|
||||||
|
/>
|
||||||
|
<SubscriptionModal
|
||||||
|
isOpen={isSubscriptionModalOpen}
|
||||||
|
onClose={() => setIsSubscriptionModalOpen(false)}
|
||||||
|
subscriptionInfo={subscriptionInfo}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
|
||||||
{/* 用户认证状态 */}
|
{/* 用户认证状态 */}
|
||||||
{isAuthenticated ? (
|
{isAuthenticated ? (
|
||||||
// 已登录用户 - 显示用户菜单
|
// 已登录用户 - 显示用户菜单
|
||||||
|
|||||||
@@ -38,6 +38,8 @@ import { useNavigate, useLocation } from 'react-router-dom';
|
|||||||
import { useAuth } from '../../contexts/AuthContext';
|
import { useAuth } from '../../contexts/AuthContext';
|
||||||
import { useAuthModal } from '../../contexts/AuthModalContext';
|
import { useAuthModal } from '../../contexts/AuthModalContext';
|
||||||
import { logger } from '../../utils/logger';
|
import { logger } from '../../utils/logger';
|
||||||
|
import SubscriptionBadge from '../Subscription/SubscriptionBadge';
|
||||||
|
import SubscriptionModal from '../Subscription/SubscriptionModal';
|
||||||
|
|
||||||
/** 二级导航栏组件 - 显示当前一级菜单下的所有二级菜单项 */
|
/** 二级导航栏组件 - 显示当前一级菜单下的所有二级菜单项 */
|
||||||
const SecondaryNav = () => {
|
const SecondaryNav = () => {
|
||||||
@@ -417,6 +419,15 @@ export default function HomeNavbar() {
|
|||||||
// 添加标志位:追踪是否已经检查过资料完整性(避免重复请求)
|
// 添加标志位:追踪是否已经检查过资料完整性(避免重复请求)
|
||||||
const hasCheckedCompleteness = React.useRef(false);
|
const hasCheckedCompleteness = React.useRef(false);
|
||||||
|
|
||||||
|
// 订阅信息状态
|
||||||
|
const [subscriptionInfo, setSubscriptionInfo] = React.useState({
|
||||||
|
type: 'free',
|
||||||
|
status: 'active',
|
||||||
|
days_left: 0,
|
||||||
|
is_active: true
|
||||||
|
});
|
||||||
|
const [isSubscriptionModalOpen, setIsSubscriptionModalOpen] = React.useState(false);
|
||||||
|
|
||||||
const loadWatchlistQuotes = useCallback(async () => {
|
const loadWatchlistQuotes = useCallback(async () => {
|
||||||
try {
|
try {
|
||||||
setWatchlistLoading(true);
|
setWatchlistLoading(true);
|
||||||
@@ -613,6 +624,57 @@ export default function HomeNavbar() {
|
|||||||
}
|
}
|
||||||
}, [isAuthenticated, user, checkProfileCompleteness]);
|
}, [isAuthenticated, user, checkProfileCompleteness]);
|
||||||
|
|
||||||
|
// 加载订阅信息
|
||||||
|
React.useEffect(() => {
|
||||||
|
console.log('🔍 [HomeNavbar] 订阅徽章 - 认证状态:', isAuthenticated, 'userId:', user?.id);
|
||||||
|
|
||||||
|
if (isAuthenticated && user) {
|
||||||
|
const loadSubscriptionInfo = async () => {
|
||||||
|
try {
|
||||||
|
const base = getApiBase();
|
||||||
|
console.log('🌐 [HomeNavbar] 订阅徽章 - API:', base + '/api/subscription/current');
|
||||||
|
const response = await fetch(base + '/api/subscription/current', {
|
||||||
|
credentials: 'include',
|
||||||
|
});
|
||||||
|
console.log('📡 [HomeNavbar] 订阅徽章 - 响应:', response.status);
|
||||||
|
if (response.ok) {
|
||||||
|
const data = await response.json();
|
||||||
|
console.log('✅ [HomeNavbar] 订阅徽章 - 完整响应数据:', data);
|
||||||
|
console.log('🔍 [HomeNavbar] 订阅徽章 - data.data:', data.data);
|
||||||
|
console.log('🔍 [HomeNavbar] 订阅徽章 - type值:', data.data?.type, '类型:', typeof data.data?.type);
|
||||||
|
|
||||||
|
if (data.success && data.data) {
|
||||||
|
// 数据标准化处理:确保type字段是小写的 'free', 'pro', 或 'max'
|
||||||
|
const normalizedData = {
|
||||||
|
type: (data.data.type || data.data.subscription_type || 'free').toLowerCase(),
|
||||||
|
status: data.data.status || 'active',
|
||||||
|
days_left: data.data.days_left || 0,
|
||||||
|
is_active: data.data.is_active !== false,
|
||||||
|
end_date: data.data.end_date || null
|
||||||
|
};
|
||||||
|
console.log('✨ [HomeNavbar] 订阅徽章 - 标准化后:', normalizedData);
|
||||||
|
setSubscriptionInfo(normalizedData);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
console.warn('⚠️ [HomeNavbar] 订阅徽章 - API 失败,使用默认值');
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('❌ [HomeNavbar] 订阅徽章 - 错误:', error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
loadSubscriptionInfo();
|
||||||
|
} else {
|
||||||
|
// 用户未登录时,重置为免费版
|
||||||
|
console.warn('🚫 [HomeNavbar] 订阅徽章 - 用户未登录,重置为免费版');
|
||||||
|
setSubscriptionInfo({
|
||||||
|
type: 'free',
|
||||||
|
status: 'active',
|
||||||
|
days_left: 0,
|
||||||
|
is_active: true
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}, [isAuthenticated, user?.id]); // 只依赖 user.id 而不是整个 user 对象
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{/* 资料完整性提醒横幅 */}
|
{/* 资料完整性提醒横幅 */}
|
||||||
@@ -711,6 +773,23 @@ export default function HomeNavbar() {
|
|||||||
variant="ghost"
|
variant="ghost"
|
||||||
size="sm"
|
size="sm"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
{/* 订阅状态徽章 - 仅登录用户可见 */}
|
||||||
|
{console.log('🎨 [HomeNavbar] 订阅徽章 - 渲染:', { isAuthenticated, user: !!user, subscriptionInfo })}
|
||||||
|
{isAuthenticated && user && (
|
||||||
|
<>
|
||||||
|
<SubscriptionBadge
|
||||||
|
subscriptionInfo={subscriptionInfo}
|
||||||
|
onClick={() => setIsSubscriptionModalOpen(true)}
|
||||||
|
/>
|
||||||
|
<SubscriptionModal
|
||||||
|
isOpen={isSubscriptionModalOpen}
|
||||||
|
onClose={() => setIsSubscriptionModalOpen(false)}
|
||||||
|
subscriptionInfo={subscriptionInfo}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
|
||||||
{/* 显示加载状态 */}
|
{/* 显示加载状态 */}
|
||||||
{isLoading ? (
|
{isLoading ? (
|
||||||
<HStack spacing={2}>
|
<HStack spacing={2}>
|
||||||
|
|||||||
137
src/components/Subscription/SubscriptionBadge.js
Normal file
137
src/components/Subscription/SubscriptionBadge.js
Normal file
@@ -0,0 +1,137 @@
|
|||||||
|
// src/components/Subscription/SubscriptionBadge.js
|
||||||
|
import React from 'react';
|
||||||
|
import { Box, Text, Tooltip, useColorModeValue } from '@chakra-ui/react';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
|
||||||
|
export default function SubscriptionBadge({ subscriptionInfo, onClick }) {
|
||||||
|
// 🔍 调试:输出接收到的 props
|
||||||
|
console.log('🎯 [SubscriptionBadge] 接收到的 subscriptionInfo:', subscriptionInfo);
|
||||||
|
console.log('🎯 [SubscriptionBadge] subscriptionInfo.type:', subscriptionInfo?.type, '类型:', typeof subscriptionInfo?.type);
|
||||||
|
|
||||||
|
// 根据订阅类型返回样式配置
|
||||||
|
const getBadgeStyles = () => {
|
||||||
|
console.log('🔧 [SubscriptionBadge] getBadgeStyles 执行, type:', subscriptionInfo.type);
|
||||||
|
|
||||||
|
if (subscriptionInfo.type === 'max') {
|
||||||
|
console.log('✅ [SubscriptionBadge] 匹配到 MAX');
|
||||||
|
return {
|
||||||
|
bg: 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)',
|
||||||
|
color: 'white',
|
||||||
|
icon: '👑',
|
||||||
|
label: 'Max',
|
||||||
|
shadow: '0 4px 12px rgba(118, 75, 162, 0.4)',
|
||||||
|
hoverShadow: '0 6px 16px rgba(118, 75, 162, 0.5)',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
if (subscriptionInfo.type === 'pro') {
|
||||||
|
console.log('✅ [SubscriptionBadge] 匹配到 PRO');
|
||||||
|
return {
|
||||||
|
bg: 'linear-gradient(135deg, #667eea 0%, #3182CE 100%)',
|
||||||
|
color: 'white',
|
||||||
|
icon: '💎',
|
||||||
|
label: 'Pro',
|
||||||
|
shadow: '0 4px 12px rgba(49, 130, 206, 0.4)',
|
||||||
|
hoverShadow: '0 6px 16px rgba(49, 130, 206, 0.5)',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
// 基础版
|
||||||
|
console.log('⚠️ [SubscriptionBadge] 使用默认基础版');
|
||||||
|
return {
|
||||||
|
bg: 'transparent',
|
||||||
|
color: useColorModeValue('gray.600', 'gray.400'),
|
||||||
|
icon: '',
|
||||||
|
label: '基础版',
|
||||||
|
border: '1.5px solid',
|
||||||
|
borderColor: useColorModeValue('gray.300', 'gray.600'),
|
||||||
|
shadow: 'none',
|
||||||
|
hoverShadow: 'none',
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const styles = getBadgeStyles();
|
||||||
|
console.log('🎨 [SubscriptionBadge] styles 对象:', styles);
|
||||||
|
|
||||||
|
// 智能动态 Tooltip 文本
|
||||||
|
const getTooltipText = () => {
|
||||||
|
const { type, days_left, is_active } = subscriptionInfo;
|
||||||
|
|
||||||
|
// 基础版用户
|
||||||
|
if (type === 'free') {
|
||||||
|
return '💡 升级到 Pro 或 Max\n解锁高级功能';
|
||||||
|
}
|
||||||
|
|
||||||
|
// 已过期
|
||||||
|
if (!is_active) {
|
||||||
|
return `❌ ${type === 'pro' ? 'Pro' : 'Max'} 会员已过期\n点击续费恢复权益`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 紧急状态 (<7 天)
|
||||||
|
if (days_left < 7) {
|
||||||
|
return `⚠️ ${type === 'pro' ? 'Pro' : 'Max'} 会员 ${days_left} 天后到期!\n立即续费保持权益`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 提醒状态 (7-30 天)
|
||||||
|
if (days_left < 30) {
|
||||||
|
return `⏰ ${type === 'pro' ? 'Pro' : 'Max'} 会员即将到期\n还有 ${days_left} 天 · 点击续费`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 正常状态 (>30 天)
|
||||||
|
return `${type === 'pro' ? '💎' : '👑'} ${type === 'pro' ? 'Pro' : 'Max'} 会员 · ${days_left} 天后到期\n点击查看详情`;
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Tooltip
|
||||||
|
label={getTooltipText()}
|
||||||
|
hasArrow
|
||||||
|
placement="bottom"
|
||||||
|
fontSize="sm"
|
||||||
|
px={3}
|
||||||
|
py={2}
|
||||||
|
borderRadius="md"
|
||||||
|
bg={useColorModeValue('gray.700', 'gray.300')}
|
||||||
|
color={useColorModeValue('white', 'gray.800')}
|
||||||
|
whiteSpace="pre-line"
|
||||||
|
textAlign="center"
|
||||||
|
maxW="250px"
|
||||||
|
>
|
||||||
|
<Box
|
||||||
|
as="button"
|
||||||
|
onClick={onClick}
|
||||||
|
px={3}
|
||||||
|
py={1.5}
|
||||||
|
borderRadius="full"
|
||||||
|
bg={styles.bg}
|
||||||
|
color={styles.color}
|
||||||
|
border={styles.border}
|
||||||
|
borderColor={styles.borderColor}
|
||||||
|
fontWeight="600"
|
||||||
|
fontSize="sm"
|
||||||
|
cursor="pointer"
|
||||||
|
transition="all 0.3s cubic-bezier(0.4, 0, 0.2, 1)"
|
||||||
|
boxShadow={styles.shadow}
|
||||||
|
_hover={{
|
||||||
|
transform: 'translateY(-2px)',
|
||||||
|
boxShadow: styles.hoverShadow || styles.shadow,
|
||||||
|
}}
|
||||||
|
_active={{
|
||||||
|
transform: 'translateY(0)',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{styles.icon && <span style={{ marginRight: '4px' }}>{styles.icon}</span>}
|
||||||
|
{(() => {
|
||||||
|
console.log('📝 [SubscriptionBadge] 渲染文本:', styles.label);
|
||||||
|
return styles.label;
|
||||||
|
})()}
|
||||||
|
</Box>
|
||||||
|
</Tooltip>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
SubscriptionBadge.propTypes = {
|
||||||
|
subscriptionInfo: PropTypes.shape({
|
||||||
|
type: PropTypes.oneOf(['free', 'pro', 'max']).isRequired,
|
||||||
|
days_left: PropTypes.number,
|
||||||
|
is_active: PropTypes.bool,
|
||||||
|
}).isRequired,
|
||||||
|
onClick: PropTypes.func.isRequired,
|
||||||
|
};
|
||||||
880
src/components/Subscription/SubscriptionContent.js
Normal file
880
src/components/Subscription/SubscriptionContent.js
Normal file
@@ -0,0 +1,880 @@
|
|||||||
|
// 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,
|
||||||
|
} from '@chakra-ui/react';
|
||||||
|
import React, { useState, useEffect } from 'react';
|
||||||
|
import { logger } from '../../utils/logger';
|
||||||
|
|
||||||
|
// Icons
|
||||||
|
import {
|
||||||
|
FaWeixin,
|
||||||
|
FaGem,
|
||||||
|
FaStar,
|
||||||
|
FaCheck,
|
||||||
|
FaQrcode,
|
||||||
|
FaClock,
|
||||||
|
FaRedo,
|
||||||
|
FaCrown,
|
||||||
|
} from 'react-icons/fa';
|
||||||
|
|
||||||
|
export default function SubscriptionContent() {
|
||||||
|
// 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 [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('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 fetchCurrentUser = async () => {
|
||||||
|
try {
|
||||||
|
const response = await fetch('/api/auth/session', {
|
||||||
|
credentials: 'include'
|
||||||
|
});
|
||||||
|
if (response.ok) {
|
||||||
|
const data = await response.json();
|
||||||
|
logger.debug('SubscriptionContent', '用户数据获取成功', { data });
|
||||||
|
if (data.success) {
|
||||||
|
setCurrentUser(data.user);
|
||||||
|
logger.debug('SubscriptionContent', '用户信息已更新', {
|
||||||
|
userId: data.user?.id,
|
||||||
|
subscriptionType: data.user?.subscription_type,
|
||||||
|
subscriptionStatus: data.user?.subscription_status
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
logger.error('SubscriptionContent', '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);
|
||||||
|
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) {
|
||||||
|
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 });
|
||||||
|
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 = 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('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}%`;
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<VStack spacing={6} align="stretch" w="100%">
|
||||||
|
{/* 当前订阅状态 */}
|
||||||
|
{currentUser && (
|
||||||
|
<Box
|
||||||
|
p={6}
|
||||||
|
borderRadius="xl"
|
||||||
|
bg={bgCard}
|
||||||
|
border="1px solid"
|
||||||
|
borderColor={borderColor}
|
||||||
|
shadow="sm"
|
||||||
|
>
|
||||||
|
<Flex justify="space-between" align="center" mb={4}>
|
||||||
|
<Text fontSize="lg" fontWeight="bold" color={textColor}>
|
||||||
|
当前订阅状态
|
||||||
|
</Text>
|
||||||
|
<Button
|
||||||
|
size="sm"
|
||||||
|
leftIcon={<Icon as={FaRedo} />}
|
||||||
|
onClick={handleRefreshUserStatus}
|
||||||
|
variant="ghost"
|
||||||
|
colorScheme="blue"
|
||||||
|
>
|
||||||
|
刷新
|
||||||
|
</Button>
|
||||||
|
</Flex>
|
||||||
|
<Flex align="center" justify="space-between">
|
||||||
|
<Box>
|
||||||
|
<HStack spacing={2} mb={2}>
|
||||||
|
<Badge
|
||||||
|
colorScheme={
|
||||||
|
currentUser.subscription_type === 'max' ? 'purple' :
|
||||||
|
currentUser.subscription_type === 'pro' ? 'blue' : 'gray'
|
||||||
|
}
|
||||||
|
variant="subtle"
|
||||||
|
px={3}
|
||||||
|
py={1}
|
||||||
|
borderRadius="full"
|
||||||
|
fontSize="sm"
|
||||||
|
>
|
||||||
|
{currentUser.subscription_type === 'free' ? '基础版' :
|
||||||
|
currentUser.subscription_type === 'pro' ? 'Pro 专业版' : 'Max 旗舰版'}
|
||||||
|
</Badge>
|
||||||
|
<Badge
|
||||||
|
colorScheme={currentUser.subscription_status === 'active' ? 'green' : 'red'}
|
||||||
|
variant="subtle"
|
||||||
|
px={3}
|
||||||
|
py={1}
|
||||||
|
borderRadius="full"
|
||||||
|
>
|
||||||
|
{currentUser.subscription_status === 'active' ? '已激活' : '未激活'}
|
||||||
|
</Badge>
|
||||||
|
</HStack>
|
||||||
|
{currentUser.subscription_end_date && (
|
||||||
|
<Text fontSize="sm" color={secondaryText}>
|
||||||
|
到期时间: {new Date(currentUser.subscription_end_date).toLocaleDateString('zh-CN')}
|
||||||
|
</Text>
|
||||||
|
)}
|
||||||
|
</Box>
|
||||||
|
{currentUser.subscription_status === 'active' && currentUser.subscription_type !== 'free' && (
|
||||||
|
<Icon
|
||||||
|
as={currentUser.subscription_type === 'max' ? FaCrown : FaGem}
|
||||||
|
color={currentUser.subscription_type === 'max' ? 'purple.400' : 'blue.400'}
|
||||||
|
boxSize={8}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</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)' }}
|
||||||
|
gap={6}
|
||||||
|
>
|
||||||
|
{subscriptionPlans.length === 0 ? (
|
||||||
|
<Box gridColumn={{ base: '1', md: '1 / -1' }} textAlign="center" py={8}>
|
||||||
|
<Text color={secondaryText}>正在加载订阅套餐...</Text>
|
||||||
|
</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"
|
||||||
|
>
|
||||||
|
<Text color="white" fontSize="xs" fontWeight="bold">
|
||||||
|
🔥 最受欢迎
|
||||||
|
</Text>
|
||||||
|
</Box>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<VStack
|
||||||
|
spacing={5}
|
||||||
|
align="stretch"
|
||||||
|
p={6}
|
||||||
|
pt={plan.name === 'max' ? 10 : 6}
|
||||||
|
>
|
||||||
|
{/* 套餐头部 */}
|
||||||
|
<VStack spacing={2} align="center">
|
||||||
|
<Icon
|
||||||
|
as={plan.name === 'pro' ? FaGem : FaCrown}
|
||||||
|
boxSize={12}
|
||||||
|
color={plan.name === 'pro' ? 'blue.400' : 'purple.400'}
|
||||||
|
/>
|
||||||
|
<Text fontSize="2xl" fontWeight="bold" color={textColor}>
|
||||||
|
{plan.display_name}
|
||||||
|
</Text>
|
||||||
|
<Text fontSize="sm" color={secondaryText} textAlign="center" minH="40px">
|
||||||
|
{plan.description}
|
||||||
|
</Text>
|
||||||
|
</VStack>
|
||||||
|
|
||||||
|
{/* 价格 */}
|
||||||
|
<VStack spacing={2}>
|
||||||
|
<HStack justify="center" align="baseline" spacing={1}>
|
||||||
|
<Text fontSize="sm" color={secondaryText}>¥</Text>
|
||||||
|
<Text fontSize="4xl" fontWeight="bold" color={textColor}>
|
||||||
|
{getCurrentPrice(plan).toFixed(0)}
|
||||||
|
</Text>
|
||||||
|
<Text fontSize="sm" color={secondaryText}>
|
||||||
|
/ {selectedCycle === 'monthly' ? '月' : '年'}
|
||||||
|
</Text>
|
||||||
|
</HStack>
|
||||||
|
{getSavingsText(plan) && (
|
||||||
|
<Badge colorScheme="green" fontSize="xs" px={2} py={1}>
|
||||||
|
{getSavingsText(plan)}
|
||||||
|
</Badge>
|
||||||
|
)}
|
||||||
|
</VStack>
|
||||||
|
|
||||||
|
<Divider />
|
||||||
|
|
||||||
|
{/* 功能列表 */}
|
||||||
|
<VStack spacing={3} align="stretch" minH="200px">
|
||||||
|
{plan.features.map((feature, index) => (
|
||||||
|
<HStack key={index} spacing={3} align="start">
|
||||||
|
<Icon
|
||||||
|
as={FaCheck}
|
||||||
|
color={plan.name === 'max' ? 'purple.400' : 'blue.400'}
|
||||||
|
boxSize={4}
|
||||||
|
mt={0.5}
|
||||||
|
/>
|
||||||
|
<Text fontSize="sm" color={textColor} flex={1}>
|
||||||
|
{feature}
|
||||||
|
</Text>
|
||||||
|
</HStack>
|
||||||
|
))}
|
||||||
|
</VStack>
|
||||||
|
|
||||||
|
{/* 订阅按钮 */}
|
||||||
|
<Button
|
||||||
|
size="lg"
|
||||||
|
colorScheme={plan.name === 'max' ? 'purple' : 'blue'}
|
||||||
|
variant="solid"
|
||||||
|
onClick={() => handleSubscribe(plan)}
|
||||||
|
isDisabled={
|
||||||
|
currentUser?.subscription_type === plan.name &&
|
||||||
|
currentUser?.subscription_status === 'active'
|
||||||
|
}
|
||||||
|
_hover={{
|
||||||
|
transform: 'scale(1.02)',
|
||||||
|
}}
|
||||||
|
transition="all 0.2s"
|
||||||
|
>
|
||||||
|
{currentUser?.subscription_type === plan.name &&
|
||||||
|
currentUser?.subscription_status === 'active'
|
||||||
|
? '✓ 已订阅'
|
||||||
|
: `选择 ${plan.display_name}`
|
||||||
|
}
|
||||||
|
</Button>
|
||||||
|
</VStack>
|
||||||
|
</Box>
|
||||||
|
)))}
|
||||||
|
</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 ? (
|
||||||
|
<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>
|
||||||
|
);
|
||||||
|
}
|
||||||
42
src/components/Subscription/SubscriptionModal.js
Normal file
42
src/components/Subscription/SubscriptionModal.js
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
// src/components/Subscription/SubscriptionModal.js
|
||||||
|
import React from 'react';
|
||||||
|
import {
|
||||||
|
Modal,
|
||||||
|
ModalOverlay,
|
||||||
|
ModalContent,
|
||||||
|
ModalHeader,
|
||||||
|
ModalBody,
|
||||||
|
ModalCloseButton,
|
||||||
|
Icon,
|
||||||
|
HStack,
|
||||||
|
Text,
|
||||||
|
useColorModeValue,
|
||||||
|
} from '@chakra-ui/react';
|
||||||
|
import { FiStar } from 'react-icons/fi';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import SubscriptionContent from './SubscriptionContent';
|
||||||
|
|
||||||
|
export default function SubscriptionModal({ isOpen, onClose }) {
|
||||||
|
return (
|
||||||
|
<Modal isOpen={isOpen} onClose={onClose} size="4xl" isCentered scrollBehavior="inside">
|
||||||
|
<ModalOverlay backdropFilter="blur(4px)" />
|
||||||
|
<ModalContent maxH="90vh">
|
||||||
|
<ModalHeader borderBottomWidth="1px" borderColor={useColorModeValue('gray.200', 'gray.600')}>
|
||||||
|
<HStack>
|
||||||
|
<Icon as={FiStar} color="blue.500" boxSize={5} />
|
||||||
|
<Text>订阅管理</Text>
|
||||||
|
</HStack>
|
||||||
|
</ModalHeader>
|
||||||
|
<ModalCloseButton />
|
||||||
|
<ModalBody py={6}>
|
||||||
|
<SubscriptionContent />
|
||||||
|
</ModalBody>
|
||||||
|
</ModalContent>
|
||||||
|
</Modal>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
SubscriptionModal.propTypes = {
|
||||||
|
isOpen: PropTypes.bool.isRequired,
|
||||||
|
onClose: PropTypes.func.isRequired,
|
||||||
|
};
|
||||||
@@ -36,7 +36,7 @@ import {
|
|||||||
} from '@chakra-ui/icons';
|
} from '@chakra-ui/icons';
|
||||||
|
|
||||||
import ReactECharts from 'echarts-for-react';
|
import ReactECharts from 'echarts-for-react';
|
||||||
import { logger } from '../../../utils/logger';
|
import { logger } from '../../utils/logger';
|
||||||
|
|
||||||
// API配置
|
// API配置
|
||||||
const API_BASE_URL = process.env.NODE_ENV === 'production' ? "" : (process.env.REACT_APP_API_URL || 'http://localhost:5001');
|
const API_BASE_URL = process.env.NODE_ENV === 'production' ? "" : (process.env.REACT_APP_API_URL || 'http://localhost:5001');
|
||||||
|
|||||||
@@ -59,8 +59,7 @@ import {
|
|||||||
FiAlertCircle,
|
FiAlertCircle,
|
||||||
} from 'react-icons/fi';
|
} from 'react-icons/fi';
|
||||||
import MyFutureEvents from './components/MyFutureEvents';
|
import MyFutureEvents from './components/MyFutureEvents';
|
||||||
import InvestmentCalendarChakra from './components/InvestmentCalendarChakra';
|
import InvestmentPlanningCenter from './components/InvestmentPlanningCenter';
|
||||||
import InvestmentPlansAndReviews from './components/InvestmentPlansAndReviews';
|
|
||||||
|
|
||||||
export default function CenterDashboard() {
|
export default function CenterDashboard() {
|
||||||
const { user } = useAuth();
|
const { user } = useAuth();
|
||||||
@@ -81,26 +80,21 @@ export default function CenterDashboard() {
|
|||||||
const [realtimeQuotes, setRealtimeQuotes] = useState({});
|
const [realtimeQuotes, setRealtimeQuotes] = useState({});
|
||||||
const [followingEvents, setFollowingEvents] = useState([]);
|
const [followingEvents, setFollowingEvents] = useState([]);
|
||||||
const [eventComments, setEventComments] = 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 [loading, setLoading] = useState(true);
|
||||||
const [refreshing, setRefreshing] = useState(false);
|
|
||||||
const [quotesLoading, setQuotesLoading] = useState(false);
|
const [quotesLoading, setQuotesLoading] = useState(false);
|
||||||
|
|
||||||
const loadData = useCallback(async () => {
|
const loadData = useCallback(async () => {
|
||||||
try {
|
try {
|
||||||
setRefreshing(true);
|
|
||||||
const base = (process.env.NODE_ENV === 'production' ? '' : process.env.REACT_APP_API_URL || 'http://49.232.185.254:5001');
|
const base = (process.env.NODE_ENV === 'production' ? '' : process.env.REACT_APP_API_URL || 'http://49.232.185.254:5001');
|
||||||
const ts = Date.now();
|
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/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/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/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 jw = await w.json();
|
||||||
const je = await e.json();
|
const je = await e.json();
|
||||||
const jc = await c.json();
|
const jc = await c.json();
|
||||||
const js = await s.json();
|
|
||||||
if (jw.success) {
|
if (jw.success) {
|
||||||
setWatchlist(Array.isArray(jw.data) ? jw.data : []);
|
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 (je.success) setFollowingEvents(Array.isArray(je.data) ? je.data : []);
|
||||||
if (jc.success) setEventComments(Array.isArray(jc.data) ? jc.data : []);
|
if (jc.success) setEventComments(Array.isArray(jc.data) ? jc.data : []);
|
||||||
if (js.success) setSubscriptionInfo(js.data);
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
// ❌ 移除 toast,仅 console 输出
|
|
||||||
logger.error('Center', 'loadData', err, {
|
logger.error('Center', 'loadData', err, {
|
||||||
userId: user?.id,
|
userId: user?.id,
|
||||||
timestamp: new Date().toISOString()
|
timestamp: new Date().toISOString()
|
||||||
});
|
});
|
||||||
} finally {
|
} finally {
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
setRefreshing(false);
|
|
||||||
}
|
}
|
||||||
}, [user]); // ✅ 移除 toast 依赖
|
}, [user]);
|
||||||
|
|
||||||
// 加载实时行情
|
// 加载实时行情
|
||||||
const loadRealtimeQuotes = useCallback(async () => {
|
const loadRealtimeQuotes = useCallback(async () => {
|
||||||
@@ -235,96 +226,11 @@ export default function CenterDashboard() {
|
|||||||
return (
|
return (
|
||||||
<Box bg={sectionBg} minH="100vh">
|
<Box bg={sectionBg} minH="100vh">
|
||||||
<Box px={{ base: 4, md: 8 }} py={6} maxW="1400px" mx="auto">
|
<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">
|
<VStack spacing={6} align="stretch">
|
||||||
<Card bg={cardBg} shadow="md">
|
<Card bg={cardBg} shadow="md" height="600px" display="flex" flexDirection="column">
|
||||||
<CardHeader pb={4}>
|
<CardHeader pb={4}>
|
||||||
<Flex justify="space-between" align="center">
|
<Flex justify="space-between" align="center">
|
||||||
<HStack>
|
<HStack>
|
||||||
@@ -335,26 +241,16 @@ export default function CenterDashboard() {
|
|||||||
</Badge>
|
</Badge>
|
||||||
{quotesLoading && <Spinner size="sm" color="blue.500" />}
|
{quotesLoading && <Spinner size="sm" color="blue.500" />}
|
||||||
</HStack>
|
</HStack>
|
||||||
<HStack>
|
<IconButton
|
||||||
<IconButton
|
icon={<FiPlus />}
|
||||||
icon={<FiRefreshCw />}
|
variant="ghost"
|
||||||
variant="ghost"
|
size="sm"
|
||||||
size="sm"
|
onClick={() => navigate('/stock-analysis/overview')}
|
||||||
onClick={loadRealtimeQuotes}
|
aria-label="添加自选股"
|
||||||
isLoading={quotesLoading}
|
/>
|
||||||
aria-label="刷新行情"
|
|
||||||
/>
|
|
||||||
<IconButton
|
|
||||||
icon={<FiPlus />}
|
|
||||||
variant="ghost"
|
|
||||||
size="sm"
|
|
||||||
onClick={() => navigate('/stock-analysis/overview')}
|
|
||||||
aria-label="添加自选股"
|
|
||||||
/>
|
|
||||||
</HStack>
|
|
||||||
</Flex>
|
</Flex>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardBody pt={0}>
|
<CardBody pt={0} flex="1" overflowY="auto">
|
||||||
{watchlist.length === 0 ? (
|
{watchlist.length === 0 ? (
|
||||||
<Center py={8}>
|
<Center py={8}>
|
||||||
<VStack spacing={3}>
|
<VStack spacing={3}>
|
||||||
@@ -440,86 +336,12 @@ export default function CenterDashboard() {
|
|||||||
)}
|
)}
|
||||||
</CardBody>
|
</CardBody>
|
||||||
</Card>
|
</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>
|
||||||
|
|
||||||
{/* 右侧:事件相关 */}
|
{/* 中列:关注事件 */}
|
||||||
<VStack spacing={6} align="stretch">
|
<VStack spacing={6} align="stretch">
|
||||||
{/* 关注事件 */}
|
{/* 关注事件 */}
|
||||||
<Card bg={cardBg} shadow="md">
|
<Card bg={cardBg} shadow="md" height="600px" display="flex" flexDirection="column">
|
||||||
<CardHeader pb={4}>
|
<CardHeader pb={4}>
|
||||||
<Flex justify="space-between" align="center">
|
<Flex justify="space-between" align="center">
|
||||||
<HStack>
|
<HStack>
|
||||||
@@ -538,7 +360,7 @@ export default function CenterDashboard() {
|
|||||||
</Button>
|
</Button>
|
||||||
</Flex>
|
</Flex>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardBody pt={0}>
|
<CardBody pt={0} flex="1" overflowY="auto">
|
||||||
{followingEvents.length === 0 ? (
|
{followingEvents.length === 0 ? (
|
||||||
<Center py={8}>
|
<Center py={8}>
|
||||||
<VStack spacing={3}>
|
<VStack spacing={3}>
|
||||||
@@ -651,10 +473,12 @@ export default function CenterDashboard() {
|
|||||||
</CardBody>
|
</CardBody>
|
||||||
</Card>
|
</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}>
|
<CardHeader pb={4}>
|
||||||
<Flex justify="space-between" align="center">
|
<Flex justify="space-between" align="center">
|
||||||
<HStack>
|
<HStack>
|
||||||
@@ -666,7 +490,7 @@ export default function CenterDashboard() {
|
|||||||
</HStack>
|
</HStack>
|
||||||
</Flex>
|
</Flex>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardBody pt={0}>
|
<CardBody pt={0} flex="1" overflowY="auto">
|
||||||
{eventComments.length === 0 ? (
|
{eventComments.length === 0 ? (
|
||||||
<Center py={8}>
|
<Center py={8}>
|
||||||
<VStack spacing={3}>
|
<VStack spacing={3}>
|
||||||
@@ -723,9 +547,9 @@ export default function CenterDashboard() {
|
|||||||
</VStack>
|
</VStack>
|
||||||
</Grid>
|
</Grid>
|
||||||
|
|
||||||
{/* 我的复盘和计划 */}
|
{/* 投资规划中心(整合了日历、计划、复盘) */}
|
||||||
<Box mt={8}>
|
<Box>
|
||||||
<InvestmentPlansAndReviews />
|
<InvestmentPlanningCenter />
|
||||||
</Box>
|
</Box>
|
||||||
</Box>
|
</Box>
|
||||||
</Box>
|
</Box>
|
||||||
|
|||||||
1419
src/views/Dashboard/components/InvestmentPlanningCenter.js
Normal file
1419
src/views/Dashboard/components/InvestmentPlanningCenter.js
Normal file
File diff suppressed because it is too large
Load Diff
@@ -1,916 +1,11 @@
|
|||||||
import {
|
import { Flex } from '@chakra-ui/react';
|
||||||
Box,
|
import React from 'react';
|
||||||
Button,
|
import SubscriptionContent from 'components/Subscription/SubscriptionContent';
|
||||||
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';
|
|
||||||
|
|
||||||
function Subscription() {
|
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 (
|
return (
|
||||||
<Flex direction='column' pt={{ base: '120px', md: '75px' }}>
|
<Flex direction='column' pt={{ base: '120px', md: '75px' }} px={{ base: '20px', md: '40px' }}>
|
||||||
{/* 当前订阅状态 */}
|
<SubscriptionContent />
|
||||||
{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>
|
</Flex>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user