diff --git a/src/components/Auth/AuthFormContent.js b/src/components/Auth/AuthFormContent.js
index 644b6b97..562cdb9a 100644
--- a/src/components/Auth/AuthFormContent.js
+++ b/src/components/Auth/AuthFormContent.js
@@ -190,6 +190,12 @@ export default function AuthFormContent() {
credential: credential.substring(0, 3) + '****' + credential.substring(7),
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);
setCountdown(60);
} else {
diff --git a/src/components/Navbars/AdminNavbarLinks.js b/src/components/Navbars/AdminNavbarLinks.js
index 83d09f76..ab778961 100755
--- a/src/components/Navbars/AdminNavbarLinks.js
+++ b/src/components/Navbars/AdminNavbarLinks.js
@@ -55,8 +55,12 @@ import {
ChakraLogoLight,
} from "components/Icons/Icons";
import { useAuth } from "contexts/AuthContext";
+import SubscriptionBadge from "components/Subscription/SubscriptionBadge";
+import SubscriptionModal from "components/Subscription/SubscriptionModal";
export default function HeaderLinks(props) {
+ console.log('🚀 [AdminNavbarLinks] 组件已加载');
+
const {
variant,
children,
@@ -71,6 +75,68 @@ export default function HeaderLinks(props) {
const { user, isAuthenticated, logout } = useAuth();
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
let navbarIcon =
fixed && scrolled
@@ -94,7 +160,23 @@ export default function HeaderLinks(props) {
flexDirection="row"
>
-
+
+ {/* 订阅状态徽章 - 仅登录用户可见 */}
+ {console.log('🎨 [订阅徽章] 渲染:', { isAuthenticated, subscriptionInfo })}
+ {isAuthenticated && (
+ <>
+ setIsSubscriptionModalOpen(true)}
+ />
+ setIsSubscriptionModalOpen(false)}
+ subscriptionInfo={subscriptionInfo}
+ />
+ >
+ )}
+
{/* 用户认证状态 */}
{isAuthenticated ? (
// 已登录用户 - 显示用户菜单
diff --git a/src/components/Navbars/HomeNavbar.js b/src/components/Navbars/HomeNavbar.js
index 1f6008b0..7e1fc4b8 100644
--- a/src/components/Navbars/HomeNavbar.js
+++ b/src/components/Navbars/HomeNavbar.js
@@ -38,6 +38,8 @@ import { useNavigate, useLocation } from 'react-router-dom';
import { useAuth } from '../../contexts/AuthContext';
import { useAuthModal } from '../../contexts/AuthModalContext';
import { logger } from '../../utils/logger';
+import SubscriptionBadge from '../Subscription/SubscriptionBadge';
+import SubscriptionModal from '../Subscription/SubscriptionModal';
/** 二级导航栏组件 - 显示当前一级菜单下的所有二级菜单项 */
const SecondaryNav = () => {
@@ -417,6 +419,15 @@ export default function HomeNavbar() {
// 添加标志位:追踪是否已经检查过资料完整性(避免重复请求)
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 () => {
try {
setWatchlistLoading(true);
@@ -613,6 +624,57 @@ export default function HomeNavbar() {
}
}, [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 (
<>
{/* 资料完整性提醒横幅 */}
@@ -711,6 +773,23 @@ export default function HomeNavbar() {
variant="ghost"
size="sm"
/>
+
+ {/* 订阅状态徽章 - 仅登录用户可见 */}
+ {console.log('🎨 [HomeNavbar] 订阅徽章 - 渲染:', { isAuthenticated, user: !!user, subscriptionInfo })}
+ {isAuthenticated && user && (
+ <>
+ setIsSubscriptionModalOpen(true)}
+ />
+ setIsSubscriptionModalOpen(false)}
+ subscriptionInfo={subscriptionInfo}
+ />
+ >
+ )}
+
{/* 显示加载状态 */}
{isLoading ? (
diff --git a/src/components/Subscription/SubscriptionBadge.js b/src/components/Subscription/SubscriptionBadge.js
new file mode 100644
index 00000000..0bf1d239
--- /dev/null
+++ b/src/components/Subscription/SubscriptionBadge.js
@@ -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 (
+
+
+ {styles.icon && {styles.icon}}
+ {(() => {
+ console.log('📝 [SubscriptionBadge] 渲染文本:', styles.label);
+ return styles.label;
+ })()}
+
+
+ );
+}
+
+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,
+};
diff --git a/src/components/Subscription/SubscriptionContent.js b/src/components/Subscription/SubscriptionContent.js
new file mode 100644
index 00000000..341a5d98
--- /dev/null
+++ b/src/components/Subscription/SubscriptionContent.js
@@ -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 (
+
+ {/* 当前订阅状态 */}
+ {currentUser && (
+
+
+
+ 当前订阅状态
+
+ }
+ onClick={handleRefreshUserStatus}
+ variant="ghost"
+ colorScheme="blue"
+ >
+ 刷新
+
+
+
+
+
+
+ {currentUser.subscription_type === 'free' ? '基础版' :
+ currentUser.subscription_type === 'pro' ? 'Pro 专业版' : 'Max 旗舰版'}
+
+
+ {currentUser.subscription_status === 'active' ? '已激活' : '未激活'}
+
+
+ {currentUser.subscription_end_date && (
+
+ 到期时间: {new Date(currentUser.subscription_end_date).toLocaleDateString('zh-CN')}
+
+ )}
+
+ {currentUser.subscription_status === 'active' && currentUser.subscription_type !== 'free' && (
+
+ )}
+
+
+ )}
+
+ {/* 计费周期选择 */}
+
+
+
+
+
+
+
+ {/* 订阅套餐 */}
+
+ {subscriptionPlans.length === 0 ? (
+
+ 正在加载订阅套餐...
+
+ ) : (
+ subscriptionPlans.filter(plan => plan && plan.name).map((plan) => (
+
+ {/* 推荐标签 */}
+ {plan.name === 'max' && (
+
+
+ 🔥 最受欢迎
+
+
+ )}
+
+
+ {/* 套餐头部 */}
+
+
+
+ {plan.display_name}
+
+
+ {plan.description}
+
+
+
+ {/* 价格 */}
+
+
+ ¥
+
+ {getCurrentPrice(plan).toFixed(0)}
+
+
+ / {selectedCycle === 'monthly' ? '月' : '年'}
+
+
+ {getSavingsText(plan) && (
+
+ {getSavingsText(plan)}
+
+ )}
+
+
+
+
+ {/* 功能列表 */}
+
+ {plan.features.map((feature, index) => (
+
+
+
+ {feature}
+
+
+ ))}
+
+
+ {/* 订阅按钮 */}
+
+
+
+ )))}
+
+
+ {/* 支付模态框 */}
+ {isPaymentModalOpen && (
+ {
+ stopAutoPaymentCheck();
+ setPaymentOrder(null);
+ setPaymentCountdown(0);
+ onPaymentModalClose();
+ }}
+ size="lg"
+ closeOnOverlayClick={false}
+ >
+
+
+
+
+
+ 微信支付
+
+
+
+
+ {!paymentOrder ? (
+ /* 订单确认 */
+
+ {selectedPlan ? (
+
+
+ 订单确认
+
+
+
+ 套餐:
+ {selectedPlan.display_name}
+
+
+ 计费周期:
+ {selectedCycle === 'monthly' ? '按月付费' : '按年付费'}
+
+
+
+ 应付金额:
+
+ ¥{getCurrentPrice(selectedPlan).toFixed(2)}
+
+
+ {getSavingsText(selectedPlan) && (
+
+ {getSavingsText(selectedPlan)}
+
+ )}
+
+
+ ) : (
+
+ 请先选择一个订阅套餐
+
+ )}
+
+ }
+ onClick={handleCreateOrder}
+ isLoading={loading}
+ loadingText="创建订单中..."
+ isDisabled={!selectedPlan}
+ >
+ 创建微信支付订单
+
+
+ ) : (
+ /* 支付二维码 */
+
+
+ 请使用微信扫码支付
+
+
+ {/* 倒计时 */}
+
+
+
+
+ 二维码有效时间: {formatTime(paymentCountdown)}
+
+
+
+
+
+ {/* 二维码 */}
+
+ {paymentOrder.qr_code_url ? (
+
+ ) : (
+
+
+
+ )}
+
+
+ {/* 订单信息 */}
+
+
+ 订单号: {paymentOrder.order_no}
+
+
+ 支付金额:
+
+ ¥{paymentOrder.amount}
+
+
+
+
+ {/* 操作按钮 */}
+
+
+ }
+ onClick={handleCheckPaymentStatus}
+ isLoading={checkingPayment}
+ loadingText="检查中..."
+ >
+ 检查支付状态
+
+
+
+
+
+
+ 支付完成但页面未更新?点击上方"强制更新"按钮
+
+
+
+ {/* 支付状态提示 */}
+ {autoCheckInterval && (
+
+
+ 🔄 正在自动检查支付状态...
+
+
+ )}
+
+ {/* 支付说明 */}
+
+ • 使用微信"扫一扫"功能扫描上方二维码
+ • 支付完成后系统将自动检测并激活订阅
+ • 系统每10秒自动检查一次支付状态
+ • 如遇问题请联系客服支持
+
+
+ )}
+
+
+
+ )}
+
+ );
+}
diff --git a/src/components/Subscription/SubscriptionModal.js b/src/components/Subscription/SubscriptionModal.js
new file mode 100644
index 00000000..3b1f96eb
--- /dev/null
+++ b/src/components/Subscription/SubscriptionModal.js
@@ -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 (
+
+
+
+
+
+
+ 订阅管理
+
+
+
+
+
+
+
+
+ );
+}
+
+SubscriptionModal.propTypes = {
+ isOpen: PropTypes.bool.isRequired,
+ onClose: PropTypes.func.isRequired,
+};
diff --git a/src/views/Company/CompanyOverview.js b/src/views/Company/CompanyOverview.js
index aaae364f..1010607b 100644
--- a/src/views/Company/CompanyOverview.js
+++ b/src/views/Company/CompanyOverview.js
@@ -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');
diff --git a/src/views/Dashboard/Center.js b/src/views/Dashboard/Center.js
index ac8c44de..503461ab 100644
--- a/src/views/Dashboard/Center.js
+++ b/src/views/Dashboard/Center.js
@@ -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 (
- {/* 头部 */}
-
-
-
- 个人中心
-
-
- 管理您的自选股、事件关注和互动记录
-
-
- }
- onClick={loadData}
- isLoading={refreshing}
- loadingText="刷新中"
- variant="solid"
- colorScheme="blue"
- size="sm"
- >
- 刷新数据
-
-
-
- {/* 统计卡片 */}
-
-
-
-
- 自选股票
- {watchlist.length}
-
-
- 关注市场动态
-
-
-
-
-
-
-
-
- 关注事件
- {followingEvents.length}
-
-
- 追踪热点事件
-
-
-
-
-
-
-
-
- 我的评论
- {eventComments.length}
-
-
- 参与讨论
-
-
-
-
-
- navigate('/home/pages/account/subscription')} _hover={{ transform: 'translateY(-2px)', shadow: 'lg' }} transition="all 0.2s">
-
-
- 订阅状态
-
- {subscriptionInfo.type === 'free' ? '免费版' : subscriptionInfo.type === 'pro' ? 'Pro版' : 'Max版'}
-
-
-
- {subscriptionInfo.type === 'free' ? '点击升级' : `剩余${subscriptionInfo.days_left}天`}
-
-
-
-
-
-
- {/* 投资日历 */}
-
-
-
-
{/* 主要内容区域 */}
-
- {/* 左侧:自选股 */}
+
+ {/* 左列:自选股票 */}
-
+
@@ -335,26 +241,16 @@ export default function CenterDashboard() {
{quotesLoading && }
-
- }
- variant="ghost"
- size="sm"
- onClick={loadRealtimeQuotes}
- isLoading={quotesLoading}
- aria-label="刷新行情"
- />
- }
- variant="ghost"
- size="sm"
- onClick={() => navigate('/stock-analysis/overview')}
- aria-label="添加自选股"
- />
-
+ }
+ variant="ghost"
+ size="sm"
+ onClick={() => navigate('/stock-analysis/overview')}
+ aria-label="添加自选股"
+ />
-
+
{watchlist.length === 0 ? (
@@ -440,86 +336,12 @@ export default function CenterDashboard() {
)}
-
- {/* 订阅管理 */}
-
-
-
-
-
- 我的订阅
-
- {subscriptionInfo.type === 'free' ? '免费版' : subscriptionInfo.type === 'pro' ? 'Pro版' : 'Max版'}
-
-
-
-
-
-
-
-
-
-
-
- 当前套餐
-
-
- {subscriptionInfo.type === 'free' ? '免费版' : subscriptionInfo.type === 'pro' ? 'Pro版本' : 'Max版本'}
-
-
-
-
- {subscriptionInfo.type === 'free' ? '永久免费' : subscriptionInfo.is_active ? '已激活' : '已过期'}
-
- {subscriptionInfo.type !== 'free' && (
- 7 ? 'green.500' : 'orange.500'}>
- 剩余 {subscriptionInfo.days_left} 天
-
- )}
-
-
-
-
- {subscriptionInfo.type === 'free' ? (
-
-
- 升级到Pro或Max版本,解锁更多功能
-
-
-
-
-
-
- ) : (
-
-
- {subscriptionInfo.is_active ? '订阅服务正常' : '订阅已过期,请续费'}
-
-
- )}
-
-
-
- {/* 右侧:事件相关 */}
+ {/* 中列:关注事件 */}
{/* 关注事件 */}
-
+
@@ -538,7 +360,7 @@ export default function CenterDashboard() {
-
+
{followingEvents.length === 0 ? (
@@ -651,10 +473,12 @@ export default function CenterDashboard() {
- {/* 移除“未来事件”板块,根据需求不再展示 */}
+
+ {/* 右列:我的评论 */}
+
{/* 我的评论 */}
-
+
@@ -666,7 +490,7 @@ export default function CenterDashboard() {
-
+
{eventComments.length === 0 ? (
@@ -723,9 +547,9 @@ export default function CenterDashboard() {
- {/* 我的复盘和计划 */}
-
-
+ {/* 投资规划中心(整合了日历、计划、复盘) */}
+
+
diff --git a/src/views/Dashboard/components/InvestmentPlanningCenter.js b/src/views/Dashboard/components/InvestmentPlanningCenter.js
new file mode 100644
index 00000000..7d2baae7
--- /dev/null
+++ b/src/views/Dashboard/components/InvestmentPlanningCenter.js
@@ -0,0 +1,1419 @@
+// src/views/Dashboard/components/InvestmentPlanningCenter.js
+import React, { useState, useEffect, useCallback, createContext, useContext } from 'react';
+import {
+ Box,
+ Card,
+ CardHeader,
+ CardBody,
+ Heading,
+ VStack,
+ HStack,
+ Text,
+ Button,
+ Modal,
+ ModalOverlay,
+ ModalContent,
+ ModalHeader,
+ ModalFooter,
+ ModalBody,
+ ModalCloseButton,
+ useDisclosure,
+ Badge,
+ IconButton,
+ Flex,
+ Grid,
+ useColorModeValue,
+ Divider,
+ Tooltip,
+ Icon,
+ Input,
+ FormControl,
+ FormLabel,
+ Textarea,
+ Select,
+ useToast,
+ Spinner,
+ Center,
+ Tag,
+ TagLabel,
+ TagLeftIcon,
+ TagCloseButton,
+ Tabs,
+ TabList,
+ TabPanels,
+ Tab,
+ TabPanel,
+ InputGroup,
+ InputLeftElement,
+} from '@chakra-ui/react';
+import {
+ FiCalendar,
+ FiClock,
+ FiStar,
+ FiTrendingUp,
+ FiPlus,
+ FiEdit2,
+ FiTrash2,
+ FiSave,
+ FiX,
+ FiTarget,
+ FiFileText,
+ FiHash,
+ FiCheckCircle,
+ FiXCircle,
+ FiAlertCircle,
+} from 'react-icons/fi';
+import FullCalendar from '@fullcalendar/react';
+import dayGridPlugin from '@fullcalendar/daygrid';
+import interactionPlugin from '@fullcalendar/interaction';
+import moment from 'moment';
+import 'moment/locale/zh-cn';
+import { logger } from '../../../utils/logger';
+import '../components/InvestmentCalendar.css';
+
+moment.locale('zh-cn');
+
+// 创建 Context 用于跨标签页共享数据
+const PlanningDataContext = createContext();
+
+export default function InvestmentPlanningCenter() {
+ const toast = useToast();
+
+ // 颜色主题
+ const bgColor = useColorModeValue('white', 'gray.800');
+ const borderColor = useColorModeValue('gray.200', 'gray.600');
+ const textColor = useColorModeValue('gray.700', 'white');
+ const secondaryText = useColorModeValue('gray.600', 'gray.400');
+ const cardBg = useColorModeValue('gray.50', 'gray.700');
+
+ // 全局数据状态
+ const [allEvents, setAllEvents] = useState([]);
+ const [loading, setLoading] = useState(false);
+ const [activeTab, setActiveTab] = useState(0);
+
+ // 加载所有事件数据(日历事件 + 计划 + 复盘)
+ const loadAllData = useCallback(async () => {
+ try {
+ setLoading(true);
+ const base = (process.env.NODE_ENV === 'production' ? '' : process.env.REACT_APP_API_URL || 'http://49.232.185.254:5001');
+
+ const response = await fetch(base + '/api/account/calendar/events', {
+ credentials: 'include'
+ });
+
+ if (response.ok) {
+ const data = await response.json();
+ if (data.success) {
+ setAllEvents(data.data || []);
+ logger.debug('InvestmentPlanningCenter', '数据加载成功', {
+ count: data.data?.length || 0
+ });
+ }
+ }
+ } catch (error) {
+ logger.error('InvestmentPlanningCenter', 'loadAllData', error);
+ } finally {
+ setLoading(false);
+ }
+ }, []);
+
+ useEffect(() => {
+ loadAllData();
+ }, [loadAllData]);
+
+ // 提供给子组件的 Context 值
+ const contextValue = {
+ allEvents,
+ setAllEvents,
+ loadAllData,
+ loading,
+ setLoading,
+ activeTab,
+ setActiveTab,
+ toast,
+ bgColor,
+ borderColor,
+ textColor,
+ secondaryText,
+ cardBg,
+ };
+
+ return (
+
+
+
+
+
+
+ 投资规划中心
+
+
+
+
+
+
+
+
+ 日历视图
+
+
+
+ 我的计划 ({allEvents.filter(e => e.type === 'plan').length})
+
+
+
+ 我的复盘 ({allEvents.filter(e => e.type === 'review').length})
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ );
+}
+
+// 日历视图面板
+function CalendarPanel() {
+ const {
+ allEvents,
+ loadAllData,
+ loading,
+ setActiveTab,
+ toast,
+ borderColor,
+ secondaryText,
+ } = useContext(PlanningDataContext);
+
+ const { isOpen, onOpen, onClose } = useDisclosure();
+ const { isOpen: isAddOpen, onOpen: onAddOpen, onClose: onAddClose } = useDisclosure();
+
+ const [selectedDate, setSelectedDate] = useState(null);
+ const [selectedDateEvents, setSelectedDateEvents] = useState([]);
+ const [newEvent, setNewEvent] = useState({
+ title: '',
+ description: '',
+ type: 'plan',
+ importance: 3,
+ stocks: '',
+ });
+
+ // 转换数据为 FullCalendar 格式
+ const calendarEvents = allEvents.map(event => ({
+ ...event,
+ id: `${event.source || 'user'}-${event.id}`,
+ title: event.title,
+ start: event.event_date,
+ date: event.event_date,
+ backgroundColor: event.source === 'future' ? '#3182CE' : event.type === 'plan' ? '#8B5CF6' : '#38A169',
+ borderColor: event.source === 'future' ? '#3182CE' : event.type === 'plan' ? '#8B5CF6' : '#38A169',
+ extendedProps: {
+ ...event,
+ isSystem: event.source === 'future',
+ }
+ }));
+
+ // 处理日期点击
+ const handleDateClick = (info) => {
+ const clickedDate = moment(info.date);
+ setSelectedDate(clickedDate);
+
+ const dayEvents = allEvents.filter(event =>
+ moment(event.event_date).isSame(clickedDate, 'day')
+ );
+ setSelectedDateEvents(dayEvents);
+ onOpen();
+ };
+
+ // 处理事件点击
+ const handleEventClick = (info) => {
+ const event = info.event;
+ const clickedDate = moment(event.start);
+ setSelectedDate(clickedDate);
+
+ const dayEvents = allEvents.filter(ev =>
+ moment(ev.event_date).isSame(clickedDate, 'day')
+ );
+ setSelectedDateEvents(dayEvents);
+ onOpen();
+ };
+
+ // 添加新事件
+ const handleAddEvent = async () => {
+ try {
+ const base = (process.env.NODE_ENV === 'production' ? '' : process.env.REACT_APP_API_URL || 'http://49.232.185.254:5001');
+
+ const eventData = {
+ ...newEvent,
+ event_date: (selectedDate ? selectedDate.format('YYYY-MM-DD') : moment().format('YYYY-MM-DD')),
+ stocks: newEvent.stocks.split(',').map(s => s.trim()).filter(s => s),
+ };
+
+ const response = await fetch(base + '/api/account/calendar/events', {
+ method: 'POST',
+ headers: {
+ 'Content-Type': 'application/json',
+ },
+ credentials: 'include',
+ body: JSON.stringify(eventData),
+ });
+
+ if (response.ok) {
+ const data = await response.json();
+ if (data.success) {
+ logger.info('CalendarPanel', '添加事件成功', {
+ eventTitle: eventData.title,
+ eventDate: eventData.event_date
+ });
+ toast({
+ title: '添加成功',
+ description: '投资计划已添加',
+ status: 'success',
+ duration: 3000,
+ });
+ onAddClose();
+ loadAllData();
+ setNewEvent({
+ title: '',
+ description: '',
+ type: 'plan',
+ importance: 3,
+ stocks: '',
+ });
+ }
+ }
+ } catch (error) {
+ logger.error('CalendarPanel', 'handleAddEvent', error, {
+ eventTitle: newEvent?.title
+ });
+ toast({
+ title: '添加失败',
+ description: '无法添加投资计划',
+ status: 'error',
+ duration: 3000,
+ });
+ }
+ };
+
+ // 删除事件
+ const handleDeleteEvent = async (eventId) => {
+ if (!eventId) {
+ logger.warn('CalendarPanel', '删除事件失败', '缺少事件 ID', { eventId });
+ toast({
+ title: '无法删除',
+ description: '缺少事件 ID',
+ status: 'error',
+ duration: 3000,
+ });
+ return;
+ }
+ try {
+ const base = (process.env.NODE_ENV === 'production' ? '' : process.env.REACT_APP_API_URL || 'http://49.232.185.254:5001');
+
+ const response = await fetch(base + `/api/account/calendar/events/${eventId}`, {
+ method: 'DELETE',
+ credentials: 'include',
+ });
+
+ if (response.ok) {
+ logger.info('CalendarPanel', '删除事件成功', { eventId });
+ toast({
+ title: '删除成功',
+ status: 'success',
+ duration: 2000,
+ });
+ loadAllData();
+ }
+ } catch (error) {
+ logger.error('CalendarPanel', 'handleDeleteEvent', error, { eventId });
+ toast({
+ title: '删除失败',
+ status: 'error',
+ duration: 3000,
+ });
+ }
+ };
+
+ // 跳转到计划或复盘标签页
+ const handleViewDetails = (event) => {
+ if (event.type === 'plan') {
+ setActiveTab(1); // 跳转到"我的计划"标签页
+ } else if (event.type === 'review') {
+ setActiveTab(2); // 跳转到"我的复盘"标签页
+ }
+ onClose();
+ };
+
+ return (
+
+
+ }
+ onClick={() => { if (!selectedDate) setSelectedDate(moment()); onAddOpen(); }}
+ >
+ 添加计划
+
+
+
+ {loading ? (
+
+
+
+ ) : (
+
+
+
+ )}
+
+ {/* 查看事件详情 Modal */}
+ {isOpen && (
+
+
+
+
+ {selectedDate && selectedDate.format('YYYY年MM月DD日')} 的事件
+
+
+
+ {selectedDateEvents.length === 0 ? (
+
+
+ 当天没有事件
+ }
+ onClick={() => {
+ onClose();
+ onAddOpen();
+ }}
+ >
+ 添加投资计划
+
+
+
+ ) : (
+
+ {selectedDateEvents.map((event, idx) => (
+
+
+
+
+
+ {event.title}
+
+ {event.source === 'future' ? (
+ 系统事件
+ ) : event.type === 'plan' ? (
+ 我的计划
+ ) : (
+ 我的复盘
+ )}
+
+ {event.importance && (
+
+
+
+ 重要度: {event.importance}/5
+
+
+ )}
+
+
+ {!event.source || event.source === 'user' ? (
+ <>
+
+ }
+ size="sm"
+ variant="ghost"
+ colorScheme="blue"
+ onClick={() => handleViewDetails(event)}
+ />
+
+ }
+ size="sm"
+ variant="ghost"
+ colorScheme="red"
+ onClick={() => handleDeleteEvent(event.id)}
+ />
+ >
+ ) : null}
+
+
+
+ {event.description && (
+
+ {event.description}
+
+ )}
+
+ {event.stocks && event.stocks.length > 0 && (
+
+ 相关股票:
+ {event.stocks.map((stock, i) => (
+
+
+ {stock}
+
+ ))}
+
+ )}
+
+ ))}
+
+ )}
+
+
+
+
+
+
+ )}
+
+ {/* 添加投资计划 Modal */}
+ {isAddOpen && (
+
+
+
+
+ 添加投资计划
+
+
+
+
+
+ 标题
+ setNewEvent({ ...newEvent, title: e.target.value })}
+ placeholder="例如:关注半导体板块"
+ />
+
+
+
+ 描述
+
+
+
+ 类型
+
+
+
+
+ 重要度
+
+
+
+
+ 相关股票(用逗号分隔)
+ setNewEvent({ ...newEvent, stocks: e.target.value })}
+ placeholder="例如:600519,000858,002415"
+ />
+
+
+
+
+
+
+
+
+
+ )}
+
+ );
+}
+
+// 计划列表面板
+function PlansPanel() {
+ const {
+ allEvents,
+ loadAllData,
+ loading,
+ toast,
+ textColor,
+ secondaryText,
+ cardBg,
+ borderColor,
+ } = useContext(PlanningDataContext);
+
+ const { isOpen, onOpen, onClose } = useDisclosure();
+ const [editingItem, setEditingItem] = useState(null);
+ const [formData, setFormData] = useState({
+ date: moment().format('YYYY-MM-DD'),
+ title: '',
+ content: '',
+ type: 'plan',
+ stocks: [],
+ tags: [],
+ status: 'active',
+ });
+ const [stockInput, setStockInput] = useState('');
+ const [tagInput, setTagInput] = useState('');
+
+ const plans = allEvents.filter(event => event.type === 'plan' && event.source !== 'future');
+
+ // 打开编辑/新建模态框
+ const handleOpenModal = (item = null) => {
+ if (item) {
+ setEditingItem(item);
+ setFormData({
+ ...item,
+ date: moment(item.event_date || item.date).format('YYYY-MM-DD'),
+ content: item.description || item.content || '',
+ });
+ } else {
+ setEditingItem(null);
+ setFormData({
+ date: moment().format('YYYY-MM-DD'),
+ title: '',
+ content: '',
+ type: 'plan',
+ stocks: [],
+ tags: [],
+ status: 'active',
+ });
+ }
+ onOpen();
+ };
+
+ // 保存数据
+ const handleSave = async () => {
+ try {
+ const base = (process.env.NODE_ENV === 'production' ? '' : process.env.REACT_APP_API_URL || 'http://49.232.185.254:5001');
+
+ const url = editingItem
+ ? base + `/api/account/investment-plans/${editingItem.id}`
+ : base + '/api/account/investment-plans';
+
+ const method = editingItem ? 'PUT' : 'POST';
+
+ const response = await fetch(url, {
+ method,
+ headers: {
+ 'Content-Type': 'application/json',
+ },
+ credentials: 'include',
+ body: JSON.stringify(formData),
+ });
+
+ if (response.ok) {
+ logger.info('PlansPanel', `${editingItem ? '更新' : '创建'}成功`, {
+ itemId: editingItem?.id,
+ title: formData.title,
+ });
+ toast({
+ title: editingItem ? '更新成功' : '创建成功',
+ status: 'success',
+ duration: 2000,
+ });
+ onClose();
+ loadAllData();
+ } else {
+ throw new Error('保存失败');
+ }
+ } catch (error) {
+ logger.error('PlansPanel', 'handleSave', error, {
+ itemId: editingItem?.id,
+ title: formData?.title
+ });
+ toast({
+ title: '保存失败',
+ description: '无法保存数据',
+ status: 'error',
+ duration: 3000,
+ });
+ }
+ };
+
+ // 删除数据
+ const handleDelete = async (id) => {
+ if (!window.confirm('确定要删除吗?')) return;
+
+ try {
+ const base = (process.env.NODE_ENV === 'production' ? '' : process.env.REACT_APP_API_URL || 'http://49.232.185.254:5001');
+
+ const response = await fetch(base + `/api/account/investment-plans/${id}`, {
+ method: 'DELETE',
+ credentials: 'include',
+ });
+
+ if (response.ok) {
+ logger.info('PlansPanel', '删除成功', { itemId: id });
+ toast({
+ title: '删除成功',
+ status: 'success',
+ duration: 2000,
+ });
+ loadAllData();
+ }
+ } catch (error) {
+ logger.error('PlansPanel', 'handleDelete', error, { itemId: id });
+ toast({
+ title: '删除失败',
+ status: 'error',
+ duration: 3000,
+ });
+ }
+ };
+
+ // 添加股票
+ const handleAddStock = () => {
+ if (stockInput.trim() && !formData.stocks.includes(stockInput.trim())) {
+ setFormData({
+ ...formData,
+ stocks: [...formData.stocks, stockInput.trim()],
+ });
+ setStockInput('');
+ }
+ };
+
+ // 添加标签
+ const handleAddTag = () => {
+ if (tagInput.trim() && !formData.tags.includes(tagInput.trim())) {
+ setFormData({
+ ...formData,
+ tags: [...formData.tags, tagInput.trim()],
+ });
+ setTagInput('');
+ }
+ };
+
+ // 获取状态信息
+ const getStatusInfo = (status) => {
+ switch (status) {
+ case 'completed':
+ return { icon: FiCheckCircle, color: 'green', text: '已完成' };
+ case 'cancelled':
+ return { icon: FiXCircle, color: 'red', text: '已取消' };
+ default:
+ return { icon: FiAlertCircle, color: 'blue', text: '进行中' };
+ }
+ };
+
+ // 渲染单个卡片
+ const renderCard = (item) => {
+ const statusInfo = getStatusInfo(item.status);
+
+ return (
+
+
+
+
+
+
+
+
+ {item.title}
+
+
+
+
+
+ {moment(item.event_date || item.date).format('YYYY年MM月DD日')}
+
+
+ {statusInfo.text}
+
+
+
+
+ }
+ size="sm"
+ variant="ghost"
+ onClick={() => handleOpenModal(item)}
+ />
+ }
+ size="sm"
+ variant="ghost"
+ colorScheme="red"
+ onClick={() => handleDelete(item.id)}
+ />
+
+
+
+ {(item.content || item.description) && (
+
+ {item.content || item.description}
+
+ )}
+
+
+ {item.stocks && item.stocks.length > 0 && (
+ <>
+ {item.stocks.map((stock, idx) => (
+
+
+ {stock}
+
+ ))}
+ >
+ )}
+ {item.tags && item.tags.length > 0 && (
+ <>
+ {item.tags.map((tag, idx) => (
+
+
+ {tag}
+
+ ))}
+ >
+ )}
+
+
+
+
+ );
+ };
+
+ return (
+
+
+
+ }
+ onClick={() => handleOpenModal(null)}
+ >
+ 新建计划
+
+
+
+ {loading ? (
+
+
+
+ ) : plans.length === 0 ? (
+
+
+
+ 暂无投资计划
+ }
+ onClick={() => handleOpenModal(null)}
+ >
+ 创建第一个计划
+
+
+
+ ) : (
+
+ {plans.map(renderCard)}
+
+ )}
+
+
+ {/* 编辑/新建模态框 */}
+ {isOpen && (
+
+
+
+
+ {editingItem ? '编辑' : '新建'}投资计划
+
+
+
+
+
+ 日期
+
+
+
+
+ setFormData({ ...formData, date: e.target.value })}
+ />
+
+
+
+
+ 标题
+ setFormData({ ...formData, title: e.target.value })}
+ placeholder="例如:布局新能源板块"
+ />
+
+
+
+ 内容
+
+
+
+ 相关股票
+
+ setStockInput(e.target.value)}
+ placeholder="输入股票代码"
+ onKeyPress={(e) => e.key === 'Enter' && handleAddStock()}
+ />
+
+
+
+ {(formData.stocks || []).map((stock, idx) => (
+
+
+ {stock}
+ setFormData({
+ ...formData,
+ stocks: formData.stocks.filter((_, i) => i !== idx)
+ })}
+ />
+
+ ))}
+
+
+
+
+ 标签
+
+ setTagInput(e.target.value)}
+ placeholder="输入标签"
+ onKeyPress={(e) => e.key === 'Enter' && handleAddTag()}
+ />
+
+
+
+ {(formData.tags || []).map((tag, idx) => (
+
+
+ {tag}
+ setFormData({
+ ...formData,
+ tags: formData.tags.filter((_, i) => i !== idx)
+ })}
+ />
+
+ ))}
+
+
+
+
+ 状态
+
+
+
+
+
+
+ }
+ >
+ 保存
+
+
+
+
+ )}
+
+ );
+}
+
+// 复盘列表面板
+function ReviewsPanel() {
+ const {
+ allEvents,
+ loadAllData,
+ loading,
+ toast,
+ textColor,
+ secondaryText,
+ cardBg,
+ borderColor,
+ } = useContext(PlanningDataContext);
+
+ const { isOpen, onOpen, onClose } = useDisclosure();
+ const [editingItem, setEditingItem] = useState(null);
+ const [formData, setFormData] = useState({
+ date: moment().format('YYYY-MM-DD'),
+ title: '',
+ content: '',
+ type: 'review',
+ stocks: [],
+ tags: [],
+ status: 'active',
+ });
+ const [stockInput, setStockInput] = useState('');
+ const [tagInput, setTagInput] = useState('');
+
+ const reviews = allEvents.filter(event => event.type === 'review' && event.source !== 'future');
+
+ // 打开编辑/新建模态框
+ const handleOpenModal = (item = null) => {
+ if (item) {
+ setEditingItem(item);
+ setFormData({
+ ...item,
+ date: moment(item.event_date || item.date).format('YYYY-MM-DD'),
+ content: item.description || item.content || '',
+ });
+ } else {
+ setEditingItem(null);
+ setFormData({
+ date: moment().format('YYYY-MM-DD'),
+ title: '',
+ content: '',
+ type: 'review',
+ stocks: [],
+ tags: [],
+ status: 'active',
+ });
+ }
+ onOpen();
+ };
+
+ // 保存数据
+ const handleSave = async () => {
+ try {
+ const base = (process.env.NODE_ENV === 'production' ? '' : process.env.REACT_APP_API_URL || 'http://49.232.185.254:5001');
+
+ const url = editingItem
+ ? base + `/api/account/investment-plans/${editingItem.id}`
+ : base + '/api/account/investment-plans';
+
+ const method = editingItem ? 'PUT' : 'POST';
+
+ const response = await fetch(url, {
+ method,
+ headers: {
+ 'Content-Type': 'application/json',
+ },
+ credentials: 'include',
+ body: JSON.stringify(formData),
+ });
+
+ if (response.ok) {
+ logger.info('ReviewsPanel', `${editingItem ? '更新' : '创建'}成功`, {
+ itemId: editingItem?.id,
+ title: formData.title,
+ });
+ toast({
+ title: editingItem ? '更新成功' : '创建成功',
+ status: 'success',
+ duration: 2000,
+ });
+ onClose();
+ loadAllData();
+ } else {
+ throw new Error('保存失败');
+ }
+ } catch (error) {
+ logger.error('ReviewsPanel', 'handleSave', error, {
+ itemId: editingItem?.id,
+ title: formData?.title
+ });
+ toast({
+ title: '保存失败',
+ description: '无法保存数据',
+ status: 'error',
+ duration: 3000,
+ });
+ }
+ };
+
+ // 删除数据
+ const handleDelete = async (id) => {
+ if (!window.confirm('确定要删除吗?')) return;
+
+ try {
+ const base = (process.env.NODE_ENV === 'production' ? '' : process.env.REACT_APP_API_URL || 'http://49.232.185.254:5001');
+
+ const response = await fetch(base + `/api/account/investment-plans/${id}`, {
+ method: 'DELETE',
+ credentials: 'include',
+ });
+
+ if (response.ok) {
+ logger.info('ReviewsPanel', '删除成功', { itemId: id });
+ toast({
+ title: '删除成功',
+ status: 'success',
+ duration: 2000,
+ });
+ loadAllData();
+ }
+ } catch (error) {
+ logger.error('ReviewsPanel', 'handleDelete', error, { itemId: id });
+ toast({
+ title: '删除失败',
+ status: 'error',
+ duration: 3000,
+ });
+ }
+ };
+
+ // 添加股票
+ const handleAddStock = () => {
+ if (stockInput.trim() && !formData.stocks.includes(stockInput.trim())) {
+ setFormData({
+ ...formData,
+ stocks: [...formData.stocks, stockInput.trim()],
+ });
+ setStockInput('');
+ }
+ };
+
+ // 添加标签
+ const handleAddTag = () => {
+ if (tagInput.trim() && !formData.tags.includes(tagInput.trim())) {
+ setFormData({
+ ...formData,
+ tags: [...formData.tags, tagInput.trim()],
+ });
+ setTagInput('');
+ }
+ };
+
+ // 渲染单个卡片
+ const renderCard = (item) => {
+ return (
+
+
+
+
+
+
+
+
+ {item.title}
+
+
+
+
+
+ {moment(item.event_date || item.date).format('YYYY年MM月DD日')}
+
+
+
+
+ }
+ size="sm"
+ variant="ghost"
+ onClick={() => handleOpenModal(item)}
+ />
+ }
+ size="sm"
+ variant="ghost"
+ colorScheme="red"
+ onClick={() => handleDelete(item.id)}
+ />
+
+
+
+ {(item.content || item.description) && (
+
+ {item.content || item.description}
+
+ )}
+
+
+ {item.stocks && item.stocks.length > 0 && (
+ <>
+ {item.stocks.map((stock, idx) => (
+
+
+ {stock}
+
+ ))}
+ >
+ )}
+ {item.tags && item.tags.length > 0 && (
+ <>
+ {item.tags.map((tag, idx) => (
+
+
+ {tag}
+
+ ))}
+ >
+ )}
+
+
+
+
+ );
+ };
+
+ return (
+
+
+
+ }
+ onClick={() => handleOpenModal(null)}
+ >
+ 新建复盘
+
+
+
+ {loading ? (
+
+
+
+ ) : reviews.length === 0 ? (
+
+
+
+ 暂无复盘记录
+ }
+ onClick={() => handleOpenModal(null)}
+ >
+ 创建第一个复盘
+
+
+
+ ) : (
+
+ {reviews.map(renderCard)}
+
+ )}
+
+
+ {/* 编辑/新建模态框 */}
+ {isOpen && (
+
+
+
+
+ {editingItem ? '编辑' : '新建'}复盘记录
+
+
+
+
+
+ 日期
+
+
+
+
+ setFormData({ ...formData, date: e.target.value })}
+ />
+
+
+
+
+ 标题
+ setFormData({ ...formData, title: e.target.value })}
+ placeholder="例如:本周交易复盘"
+ />
+
+
+
+ 内容
+
+
+
+ 相关股票
+
+ setStockInput(e.target.value)}
+ placeholder="输入股票代码"
+ onKeyPress={(e) => e.key === 'Enter' && handleAddStock()}
+ />
+
+
+
+ {(formData.stocks || []).map((stock, idx) => (
+
+
+ {stock}
+ setFormData({
+ ...formData,
+ stocks: formData.stocks.filter((_, i) => i !== idx)
+ })}
+ />
+
+ ))}
+
+
+
+
+ 标签
+
+ setTagInput(e.target.value)}
+ placeholder="输入标签"
+ onKeyPress={(e) => e.key === 'Enter' && handleAddTag()}
+ />
+
+
+
+ {(formData.tags || []).map((tag, idx) => (
+
+
+ {tag}
+ setFormData({
+ ...formData,
+ tags: formData.tags.filter((_, i) => i !== idx)
+ })}
+ />
+
+ ))}
+
+
+
+
+
+
+ }
+ >
+ 保存
+
+
+
+
+ )}
+
+ );
+}
diff --git a/src/views/Pages/Account/Subscription.js b/src/views/Pages/Account/Subscription.js
index f0b98831..b65a80d6 100644
--- a/src/views/Pages/Account/Subscription.js
+++ b/src/views/Pages/Account/Subscription.js
@@ -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 (
-
- {/* 当前订阅状态 */}
- {currentUser && (
-
-
-
-
- 当前订阅状态
-
- }
- onClick={handleRefreshUserStatus}
- colorScheme='blue'
- variant='outline'
- >
- 刷新状态
-
-
-
-
-
-
-
- {currentUser.subscription_type === 'free' ? '免费版' :
- currentUser.subscription_type === 'pro' ? 'Pro版' : 'Max版'}
-
-
- {currentUser.subscription_status === 'active' ? '已激活' : '未激活'}
-
-
- {currentUser.subscription_end_date && (
-
- 到期时间: {new Date(currentUser.subscription_end_date).toLocaleDateString()}
-
- )}
- {/* 调试信息 */}
-
- 用户ID: {currentUser.id} | 类型: {currentUser.subscription_type} | 状态: {currentUser.subscription_status}
-
-
- {currentUser.subscription_status === 'active' && (
-
- )}
-
-
- )}
-
- {/* 计费周期选择 */}
-
-
-
-
-
-
-
-
-
- {/* 订阅套餐 */}
-
- {subscriptionPlans.length === 0 ? (
- // 加载状态或空状态
-
- 正在加载订阅套餐...
-
- ) : (
- subscriptionPlans.filter(plan => plan && plan.name).map((plan) => (
-
- {plan.name === 'max' && (
-
- 推荐
-
- )}
-
-
- {/* 套餐头部 */}
-
-
-
- {plan.display_name}
-
-
- {plan.description}
-
-
-
-
-
-
-
- {/* 价格 */}
-
-
-
- ¥{getCurrentPrice(plan).toFixed(2)}
-
-
- / {selectedCycle === 'monthly' ? '月' : '年'}
-
-
- {getSavingsText(plan) && (
-
- {getSavingsText(plan)}
-
- )}
-
-
-
-
- {/* 功能列表 */}
-
-
- 包含功能:
-
- {plan.features.map((feature, index) => (
-
-
-
- {feature}
-
-
- ))}
-
-
- {/* 订阅按钮 */}
-
-
-
- )))}
-
-
- {/* 支付模态框 - 条件渲染 */}
- {isPaymentModalOpen && (
- {
- stopAutoPaymentCheck();
- setPaymentOrder(null);
- setPaymentCountdown(0);
- onPaymentModalClose();
- }}
- size='lg'
- closeOnOverlayClick={false}
- >
-
-
-
-
-
- 微信支付
-
-
-
-
- {!paymentOrder ? (
- /* 订单确认 */
-
- {selectedPlan ? (
-
-
- 订单确认
-
-
- 套餐:
- {selectedPlan.display_name}
-
-
- 计费周期:
- {selectedCycle === 'monthly' ? '按月付费' : '按年付费'}
-
-
- 价格:
-
- ¥{getCurrentPrice(selectedPlan).toFixed(2)}
-
-
- {getSavingsText(selectedPlan) && (
-
- {getSavingsText(selectedPlan)}
-
- )}
-
- ) : (
-
- 请先选择一个订阅套餐
-
- )}
-
- }
- onClick={handleCreateOrder}
- isLoading={loading}
- loadingText='创建订单中...'
- isDisabled={!selectedPlan}
- >
- 创建微信支付订单
-
-
- ) : (
- /* 支付二维码 */
-
-
- 请使用微信扫码支付
-
-
- {/* 倒计时 */}
-
-
-
-
- 二维码有效时间: {formatTime(paymentCountdown)}
-
-
-
-
-
- {/* 二维码 */}
-
- {paymentOrder.qr_code_url ? (
-
- ) : (
-
-
-
- )}
-
-
- {/* 订单信息 */}
-
- 订单号: {paymentOrder.order_no}
-
- 支付金额:
-
- ¥{paymentOrder.amount}
-
-
-
-
- {/* 操作按钮 */}
-
-
- }
- onClick={handleCheckPaymentStatus}
- isLoading={checkingPayment}
- loadingText='检查中...'
- flex={1}
- >
- 检查支付状态
-
-
-
-
- {/* 强制更新按钮 */}
-
-
-
- 如果支付完成但页面未更新,请点击上方"强制更新"按钮
-
-
-
- {/* 支付状态提示 */}
- {autoCheckInterval && (
-
-
-
- 🔄 正在自动检查支付状态...
-
-
-
- )}
-
- {/* 支付说明 */}
-
- • 请使用微信"扫一扫"功能扫描上方二维码
- • 支付完成后系统将自动检测并激活订阅
- • 系统每10秒自动检查一次支付状态
- • 如遇问题请联系客服支持
-
-
- )}
-
-
-
- )}
-
- {/* 调试面板 */}
- {process.env.NODE_ENV === 'development' && (
-
-
- 🔧 调试信息
-
-
-
- 支付订单:
-
- {paymentOrder ? `ID: ${paymentOrder.id}` : '无'}
-
-
-
- 自动检查:
-
- {autoCheckInterval ? '运行中' : '已停止'}
-
-
-
- 订阅状态:
-
- {currentUser?.subscription_status || '未知'}
-
-
-
- 订阅类型:
- {currentUser?.subscription_type || '未知'}
-
-
-
-
-
-
-
-
- )}
+
+
);
}