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 && ( + + + + 当前订阅状态 + + + + + + + + {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)} + + )} + + + ) : ( + + 请先选择一个订阅套餐 + + )} + + + + ) : ( + /* 支付二维码 */ + + + 请使用微信扫码支付 + + + {/* 倒计时 */} + + + + + 二维码有效时间: {formatTime(paymentCountdown)} + + + + + + {/* 二维码 */} + + {paymentOrder.qr_code_url ? ( + 微信支付二维码 + ) : ( + + + + )} + + + {/* 订单信息 */} + + + 订单号: {paymentOrder.order_no} + + + 支付金额: + + ¥{paymentOrder.amount} + + + + + {/* 操作按钮 */} + + + + + + + + + 支付完成但页面未更新?点击上方"强制更新"按钮 + + + + {/* 支付状态提示 */} + {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 ( - {/* 头部 */} - - - - 个人中心 - - - 管理您的自选股、事件关注和互动记录 - - - - - - {/* 统计卡片 */} - - - - - 自选股票 - {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 ( + + + + + + {loading ? ( +
+ +
+ ) : ( + + + + )} + + {/* 查看事件详情 Modal */} + {isOpen && ( + + + + + {selectedDate && selectedDate.format('YYYY年MM月DD日')} 的事件 + + + + {selectedDateEvents.length === 0 ? ( +
+ + 当天没有事件 + + +
+ ) : ( + + {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="例如:关注半导体板块" + /> + + + + 描述 +