diff --git a/.claude/settings.local.json b/.claude/settings.local.json index 07fef7db..790e08c6 100644 --- a/.claude/settings.local.json +++ b/.claude/settings.local.json @@ -1,18 +1,7 @@ { "permissions": { "allow": [ - "Bash(npm test:*)", - "Bash(xargs ls:*)", - "Bash(awk:*)", - "Bash(npm start)", - "Bash(python3:*)", - "Bash(rm -rf /Users/qiye/Desktop/jzqy/vf_react/src/views/Applications)", - "Bash(rm -rf /Users/qiye/Desktop/jzqy/vf_react/src/views/Ecommerce)", - "Bash(rm /Users/qiye/Desktop/jzqy/vf_react/src/views/Dashboard/Automotive.js)", - "Bash(rm /Users/qiye/Desktop/jzqy/vf_react/src/views/Dashboard/CRM.js)", - "Bash(rm /Users/qiye/Desktop/jzqy/vf_react/src/views/Dashboard/SmartHome.js)", - "Bash(rm /Users/qiye/Desktop/jzqy/vf_react/src/views/Dashboard/Landing.js)", - "mcp__ide__getDiagnostics" + "Bash(find src -name \"*.js\" -type f -exec grep -l \"process.env.REACT_APP_API_URL || [''''\"\"]http\" {})" ], "deny": [], "ask": [] diff --git a/.env.mock b/.env.mock index 94147c2c..6888e0d3 100644 --- a/.env.mock +++ b/.env.mock @@ -1,5 +1,20 @@ +# ======================================== # Mock 测试环境配置 +# ======================================== # 使用方式: npm run start:mock +# +# 工作原理: +# 1. 通过 env-cmd 加载此配置文件 +# 2. REACT_APP_ENABLE_MOCK=true 会在 src/index.js 中启动 MSW (Mock Service Worker) +# 3. MSW 在浏览器层面拦截所有 HTTP 请求 +# 4. 根据 src/mocks/handlers/* 中定义的规则返回 mock 数据 +# 5. 未定义 mock 的接口会继续请求真实后端 +# +# 适用场景: +# - 前端独立开发,无需后端支持 +# - 测试特定接口的 UI 表现 +# - 后端接口未就绪时的快速原型开发 +# ======================================== # React 构建优化配置 GENERATE_SOURCEMAP=false @@ -10,10 +25,12 @@ IMAGE_INLINE_SIZE_LIMIT=10000 NODE_OPTIONS=--max_old_space_size=4096 # API 配置 -# Mock 模式下不需要真实的后端地址 -REACT_APP_API_URL=http://localhost:3000 +# Mock 模式下使用空字符串,让请求使用相对路径 +# MSW 会在浏览器层拦截这些请求,不需要真实的后端地址 +REACT_APP_API_URL= # 启用 Mock 数据(核心配置) +# 此配置会触发 src/index.js 中的 MSW 初始化 REACT_APP_ENABLE_MOCK=true # Mock 环境标识 diff --git a/src/components/Citation/CitationMark.js b/src/components/Citation/CitationMark.js index 4d15eaee..873db81a 100644 --- a/src/components/Citation/CitationMark.js +++ b/src/components/Citation/CitationMark.js @@ -116,6 +116,8 @@ const CitationMark = ({ citationId, citation }) => { overlayInnerStyle={{ maxWidth: 340, padding: '8px' }} open={popoverVisible} onOpenChange={setPopoverVisible} + zIndex={2000} + getPopupContainer={(trigger) => trigger.parentElement || document.body} > - {/* 标题栏 */} - - - - - {title} - + {/* 标题栏 - 仅在需要时显示 */} + {showHeader && ( + + + + + {title} + + + {showAIBadge && ( + } + color="purple" + style={{ margin: 0 }} + > + AI 生成 + + )} - {showAIBadge && ( - } - color="purple" - style={{ margin: 0 }} - > - AI 生成 - - )} - + )} {/* 带引用的文本内容 */} -
+ {processed.segments.map((segment, index) => ( {/* 文本片段 */} - + {segment.text} @@ -96,12 +113,12 @@ const CitedContent = ({ {/* 在片段之间添加逗号分隔符(最后一个不加) */} {index < processed.segments.length - 1 && ( - + )} ))} -
- + + ); }; diff --git a/src/components/Navbars/HomeNavbar.js b/src/components/Navbars/HomeNavbar.js index 6d5d0c07..9e81281c 100644 --- a/src/components/Navbars/HomeNavbar.js +++ b/src/components/Navbars/HomeNavbar.js @@ -33,16 +33,18 @@ import { useToast, } from '@chakra-ui/react'; import { ChevronDownIcon, HamburgerIcon, SunIcon, MoonIcon } from '@chakra-ui/icons'; -import { FiStar, FiCalendar } from 'react-icons/fi'; +import { FiStar, FiCalendar, FiUser, FiSettings, FiHome, FiLogOut } from 'react-icons/fi'; +import { FaCrown } from 'react-icons/fa'; 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 { getApiBase } from '../../utils/apiConfig'; +import SubscriptionButton from '../Subscription/SubscriptionButton'; import SubscriptionModal from '../Subscription/SubscriptionModal'; /** 二级导航栏组件 - 显示当前一级菜单下的所有二级菜单项 */ -const SecondaryNav = () => { +const SecondaryNav = ({ showCompletenessAlert }) => { const navigate = useNavigate(); const location = useLocation(); const navbarBg = useColorModeValue('gray.50', 'gray.700'); @@ -107,7 +109,7 @@ const SecondaryNav = () => { borderColor={useColorModeValue('gray.200', 'gray.600')} py={2} position="sticky" - top="60px" + top={showCompletenessAlert ? "120px" : "60px"} zIndex={100} > @@ -352,9 +354,6 @@ const NavItems = ({ isAuthenticated, user }) => { } }; -// 计算 API 基础地址(移到组件外部,避免每次 render 重新创建) -const getApiBase = () => (process.env.NODE_ENV === 'production' ? '' : (process.env.REACT_APP_API_URL || 'http://49.232.185.254:5001')); - export default function HomeNavbar() { const { isOpen, onOpen, onClose } = useDisclosure(); const navigate = useNavigate(); @@ -762,50 +761,42 @@ export default function HomeNavbar() { size="sm" /> - {/* 订阅状态徽章 - 仅登录用户可见 */} - {isAuthenticated && user && ( - <> - setIsSubscriptionModalOpen(true)} - /> - setIsSubscriptionModalOpen(false)} - subscriptionInfo={subscriptionInfo} - /> - - )} - {/* 显示加载状态 */} {isLoading ? ( - - - 检查登录状态... - + ) : isAuthenticated && user ? ( // 已登录状态 - 用户菜单 + 功能菜单排列 - - - } - > - {getDisplayName()} - - + {/* 用户头像+订阅徽章组合 */} + + {/* 用户头像菜单 */} + + + } + bg="transparent" + _hover={{ bg: useColorModeValue('gray.100', 'gray.700') }} + borderRadius="full" + position="relative" + aria-label="用户菜单" + > + + {getDisplayName()} {user.email} @@ -816,24 +807,52 @@ export default function HomeNavbar() { 微信已绑定 )} - navigate('/home/profile')}> - 👤 个人资料 + {/* 账户管理组 */} + } onClick={() => navigate('/home/profile')}> + 个人资料 - navigate('/home/pages/account/subscription')}> - 💎 订阅管理 - - navigate('/home/settings')}> - ⚙️ 账户设置 - - navigate('/home/center')}> - 🏠 个人中心 + } onClick={() => navigate('/home/settings')}> + 账户设置 - - 🚪 退出登录 + {/* 功能入口组 */} + } onClick={() => navigate('/home/pages/account/subscription')}> + 订阅管理 + + + {/* 退出 */} + } onClick={handleLogout} color="red.500"> + 退出登录 - + + + {/* 订阅徽章按钮 - 点击打开订阅弹窗 */} + setIsSubscriptionModalOpen(true)} + /> + + {/* 订阅管理弹窗 - 只在打开时渲染 */} + {isSubscriptionModalOpen && ( + setIsSubscriptionModalOpen(false)} + subscriptionInfo={subscriptionInfo} + /> + )} + + + {/* 个人中心快捷按钮 */} + } + size="sm" + colorScheme="gray" + variant="ghost" + onClick={() => navigate('/home/center')} + aria-label="个人中心" + _hover={{ bg: 'gray.700' }} + /> {/* 自选股 - 头像右侧 */} @@ -1261,7 +1280,7 @@ export default function HomeNavbar() { {/* 二级导航栏 - 显示当前页面所属的二级菜单 */} - {!isMobile && } + {!isMobile && } ); } \ No newline at end of file diff --git a/src/components/Subscription/SubscriptionButton.js b/src/components/Subscription/SubscriptionButton.js new file mode 100644 index 00000000..012ff402 --- /dev/null +++ b/src/components/Subscription/SubscriptionButton.js @@ -0,0 +1,209 @@ +// src/components/Subscription/SubscriptionButton.js +import React from 'react'; +import { Box, VStack, HStack, Text, Tooltip, Divider, useColorModeValue } from '@chakra-ui/react'; +import PropTypes from 'prop-types'; + +/** + * 订阅徽章按钮组件 - 用于导航栏头像旁边 + * 简洁显示订阅等级,hover 显示详细卡片式 Tooltip + */ +export default function SubscriptionButton({ subscriptionInfo, onClick }) { + const tooltipBg = useColorModeValue('white', 'gray.800'); + const tooltipBorder = useColorModeValue('gray.200', 'gray.600'); + const tooltipText = useColorModeValue('gray.700', 'gray.100'); + const dividerColor = useColorModeValue('gray.200', 'gray.600'); + + // 根据订阅类型返回样式配置 + const getButtonStyles = () => { + if (subscriptionInfo.type === '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)', + border: 'none', + accentColor: '#764ba2', + }; + } + if (subscriptionInfo.type === '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)', + border: 'none', + accentColor: '#3182CE', + }; + } + // 基础版 + return { + bg: 'transparent', + color: useColorModeValue('gray.600', 'gray.400'), + icon: '✨', + label: '基础版', + shadow: 'none', + hoverShadow: '0 2px 8px rgba(0, 0, 0, 0.1)', + border: '1.5px solid', + borderColor: useColorModeValue('gray.300', 'gray.600'), + accentColor: useColorModeValue('#718096', '#A0AEC0'), + }; + }; + + const styles = getButtonStyles(); + + // 增强的卡片式 Tooltip 内容 + const TooltipContent = () => { + const { type, days_left, is_active } = subscriptionInfo; + + // 基础版用户 + if (type === 'free') { + return ( + + + ✨ 基础版用户 + + + + 解锁更多高级功能 + + + 🚀 立即升级 + + + ); + } + + // 付费用户 + const isExpired = !is_active; + const isUrgent = days_left < 7; + const isWarning = days_left < 30; + + return ( + + + + {type === 'pro' ? '💎 Pro 会员' : '👑 Max 会员'} + + {isExpired && 已过期} + + + + + {/* 状态信息 */} + {isExpired ? ( + + + + 会员已过期,续费恢复权益 + + + ) : ( + + + + {isUrgent ? '⚠️' : isWarning ? '⏰' : '📅'} + + + {isUrgent && 紧急!} + {' '}还有 {days_left} 天到期 + + + + 享受全部高级功能 + + + )} + + {/* 行动按钮 */} + + {isExpired ? '💳 立即续费' : isUrgent ? '⚡ 紧急续费' : '💼 管理订阅'} + + + ); + }; + + return ( + } + hasArrow + placement="bottom" + bg={tooltipBg} + color={tooltipText} + borderRadius="lg" + border="1px solid" + borderColor={tooltipBorder} + boxShadow="lg" + p={3} + > + + + {styles.icon} {styles.label} + + + + ); +} + +SubscriptionButton.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 index 94fef6b9..7131883f 100644 --- a/src/components/Subscription/SubscriptionContent.js +++ b/src/components/Subscription/SubscriptionContent.js @@ -21,6 +21,14 @@ import { Image, Progress, Divider, + Table, + Thead, + Tbody, + Tr, + Th, + Td, + Heading, + Collapse, } from '@chakra-ui/react'; import React, { useState, useEffect } from 'react'; import { logger } from '../../utils/logger'; @@ -35,6 +43,11 @@ import { FaClock, FaRedo, FaCrown, + FaStar, + FaTimes, + FaInfinity, + FaChevronDown, + FaChevronUp, } from 'react-icons/fa'; export default function SubscriptionContent() { @@ -61,6 +74,7 @@ export default function SubscriptionContent() { const [checkingPayment, setCheckingPayment] = useState(false); const [autoCheckInterval, setAutoCheckInterval] = useState(null); const [forceUpdating, setForceUpdating] = useState(false); + const [openFaqIndex, setOpenFaqIndex] = useState(null); // 加载订阅套餐数据 useEffect(() => { @@ -420,8 +434,34 @@ export default function SubscriptionContent() { return `年付节省 ${percentage}%`; }; + // 统一的功能列表定义 - 基于商业定价(10月15日)文档 + const allFeatures = [ + // 新闻催化分析模块 + { name: '新闻信息流', free: true, pro: true, max: true }, + { name: '历史事件对比', free: 'TOP3', pro: true, max: true }, + { name: '事件传导链分析(AI)', free: '有限体验', pro: true, max: true }, + { name: '事件-相关标的分析', free: false, pro: true, max: true }, + { name: '相关概念展示', free: false, pro: true, max: true }, + { name: '板块深度分析(AI)', free: false, pro: false, max: true }, + + // 个股中心模块 + { name: 'AI复盘功能', free: true, pro: true, max: true }, + { name: '企业概览', free: '限制预览', pro: true, max: true }, + { name: '个股深度分析(AI)', free: '10家/月', pro: '50家/月', max: true }, + { name: '高效数据筛选工具', free: false, pro: true, max: true }, + + // 概念中心模块 + { name: '概念中心(548大概念)', free: 'TOP5', pro: true, max: true }, + { name: '历史时间轴查询', free: false, pro: '100天', max: true }, + { name: '概念高频更新', free: false, pro: false, max: true }, + + // 涨停分析模块 + { name: '涨停板块数据分析', free: true, pro: true, max: true }, + { name: '个股涨停分析', free: true, pro: true, max: true }, + ]; + return ( - + {/* 当前订阅状态 */} {user && ( - - - 当前订阅状态 - - - - - - + + {/* 左侧:当前订阅状态标签 */} + + + 当前订阅: + - 1SubscriptionContent {user.subscription_type} {user.subscription_type === 'free' ? '基础版' : user.subscription_type === 'pro' ? 'Pro 专业版' : 'Max 旗舰版'} - - {user.subscription_status === 'active' ? '已激活' : '未激活'} - - + + {user.subscription_status === 'active' ? '已激活' : '未激活'} + + + + {/* 右侧:到期时间和图标 */} + {user.subscription_end_date && ( 到期时间: {new Date(user.subscription_end_date).toLocaleDateString('zh-CN')} )} - - {user.subscription_status === 'active' && user.subscription_type !== 'free' && ( - - )} + {user.subscription_status === 'active' && user.subscription_type !== 'free' && ( + + )} + )} @@ -525,15 +557,119 @@ export default function SubscriptionContent() { {/* 订阅套餐 */} {subscriptionPlans.length === 0 ? ( - + 正在加载订阅套餐... ) : ( - subscriptionPlans.filter(plan => plan && plan.name).map((plan) => ( + <> + {/* 免费版套餐 */} + + + {/* 套餐头部 - 图标与标题同行 */} + + + + + + 基础版 + + + + 免费 + + + + 免费体验核心功能,7项实用工具 + + + + + + {/* 功能列表 */} + + {allFeatures.map((feature, index) => { + const hasFreeAccess = feature.free === true || typeof feature.free === 'string'; + const freeLimit = typeof feature.free === 'string' ? feature.free : null; + + return ( + + + + {feature.name} + {freeLimit && ( + + ({freeLimit}) + + )} + + + ); + })} + + + {/* 订阅按钮 */} + + + + + {/* 付费套餐 */} + {subscriptionPlans.filter(plan => plan && plan.name).map((plan) => ( 🔥 最受欢迎 @@ -566,61 +703,77 @@ export default function SubscriptionContent() { )} - {/* 套餐头部 */} - - - - {plan.display_name} - - - {plan.description} - - - - {/* 价格 */} - - - ¥ - - {getCurrentPrice(plan).toFixed(0)} + {/* 套餐头部 - 图标与标题同行 */} + + + + + + {plan.display_name} + + + + ¥ + + {getCurrentPrice(plan).toFixed(0)} + + + /{selectedCycle === 'monthly' ? '月' : '年'} + + + + + + {plan.description} - - / {selectedCycle === 'monthly' ? '月' : '年'} - - - {getSavingsText(plan) && ( - - {getSavingsText(plan)} - - )} + {getSavingsText(plan) && ( + + {getSavingsText(plan)} + + )} + {/* 功能列表 */} - {plan.features.map((feature, index) => ( - - - - {feature} - - - ))} + {allFeatures.map((feature, index) => { + const featureValue = feature[plan.name]; + const isSupported = featureValue === true || typeof featureValue === 'string'; + const limitText = typeof featureValue === 'string' ? featureValue : null; + + return ( + + + + {feature.name} + {limitText && ( + + ({limitText}) + + )} + + + ); + })} {/* 订阅按钮 */} @@ -646,9 +799,197 @@ export default function SubscriptionContent() { - )))} + ))} + + )} + {/* FAQ 常见问题 */} + + + 常见问题 + + + {/* FAQ 1 */} + + setOpenFaqIndex(openFaqIndex === 0 ? null : 0)} + bg={openFaqIndex === 0 ? bgAccent : 'transparent'} + _hover={{ bg: bgAccent }} + transition="all 0.2s" + justify="space-between" + align="center" + > + + 如何取消订阅? + + + + + + + 您可以随时在账户设置中取消订阅。取消后,您的订阅将在当前计费周期结束时到期,期间您仍可继续使用付费功能。取消后不会立即扣款,也不会自动续费。 + + + + + + {/* FAQ 2 */} + + setOpenFaqIndex(openFaqIndex === 1 ? null : 1)} + bg={openFaqIndex === 1 ? bgAccent : 'transparent'} + _hover={{ bg: bgAccent }} + transition="all 0.2s" + justify="space-between" + align="center" + > + + 支持哪些支付方式? + + + + + + + 我们目前支持微信支付。扫描支付二维码后,系统会自动检测支付状态并激活您的订阅。支付过程安全可靠,所有交易都经过加密处理。 + + + + + + {/* FAQ 3 */} + + setOpenFaqIndex(openFaqIndex === 2 ? null : 2)} + bg={openFaqIndex === 2 ? bgAccent : 'transparent'} + _hover={{ bg: bgAccent }} + transition="all 0.2s" + justify="space-between" + align="center" + > + + 可以在月付和年付之间切换吗? + + + + + + + 可以。您可以随时更改计费周期。如果从月付切换到年付,系统会计算剩余价值并应用到新的订阅中。年付用户可享受20%的折扣优惠。 + + + + + + {/* FAQ 4 */} + + setOpenFaqIndex(openFaqIndex === 3 ? null : 3)} + bg={openFaqIndex === 3 ? bgAccent : 'transparent'} + _hover={{ bg: bgAccent }} + transition="all 0.2s" + justify="space-between" + align="center" + > + + 是否提供退款? + + + + + + + 我们提供7天无理由退款保证。如果您在订阅后7天内对服务不满意,可以申请全额退款。超过7天后,我们将根据实际使用情况进行评估。 + + + + + + {/* FAQ 5 */} + + setOpenFaqIndex(openFaqIndex === 4 ? null : 4)} + bg={openFaqIndex === 4 ? bgAccent : 'transparent'} + _hover={{ bg: bgAccent }} + transition="all 0.2s" + justify="space-between" + align="center" + > + + Pro版和Max版有什么区别? + + + + + + + Pro版适合个人专业用户,提供高级图表、历史数据分析等功能。Max版则是为团队和企业设计,额外提供实时数据推送、API访问、无限制的数据存储和团队协作功能,并享有优先技术支持。 + + + + + + + {/* 支付模态框 */} {isPaymentModalOpen && ( = start && eventDate <= end; }); } + +// ==================== 未来事件(投资日历)辅助函数 ==================== + +/** + * 获取指定日期的未来事件列表 + * @param {string} dateStr - 日期字符串 'YYYY-MM-DD' + * @param {string} type - 事件类型 'event' | 'data' | 'all' + * @returns {Array} 事件列表 + */ +export function getMockFutureEvents(dateStr, type = 'all') { + const targetDate = new Date(dateStr); + + return mockFutureEvents.filter(event => { + const eventDate = new Date(event.calendar_time); + const isSameDate = + eventDate.getFullYear() === targetDate.getFullYear() && + eventDate.getMonth() === targetDate.getMonth() && + eventDate.getDate() === targetDate.getDate(); + + if (!isSameDate) return false; + + if (type === 'all') return true; + return event.type === type; + }); +} + +/** + * 获取指定月份的事件统计 + * @param {number} year - 年份 + * @param {number} month - 月份 (1-12) + * @returns {Array} 事件统计数组 + */ +export function getMockEventCountsForMonth(year, month) { + const counts = {}; + + mockFutureEvents.forEach(event => { + const eventDate = new Date(event.calendar_time); + if (eventDate.getFullYear() === year && eventDate.getMonth() + 1 === month) { + const dateStr = eventDate.toISOString().split('T')[0]; + counts[dateStr] = (counts[dateStr] || 0) + 1; + } + }); + + return Object.entries(counts).map(([date, count]) => ({ + date, + count, + className: count >= 3 ? 'high-activity' : count >= 2 ? 'medium-activity' : 'low-activity' + })); +} diff --git a/src/mocks/data/users.js b/src/mocks/data/users.js index e9b10c79..45d832f0 100644 --- a/src/mocks/data/users.js +++ b/src/mocks/data/users.js @@ -91,8 +91,18 @@ export function generateWechatSessionId() { // 设置当前登录用户 export function setCurrentUser(user) { if (user) { - localStorage.setItem('mock_current_user', JSON.stringify(user)); - console.log('[Mock State] 设置当前登录用户:', user); + // 数据兼容处理:确保用户数据包含订阅信息字段 + const normalizedUser = { + ...user, + // 如果缺少订阅信息,添加默认值 + subscription_type: user.subscription_type || 'free', + subscription_status: user.subscription_status || 'active', + subscription_end_date: user.subscription_end_date || null, + is_subscription_active: user.is_subscription_active !== false, + subscription_days_left: user.subscription_days_left || 0 + }; + localStorage.setItem('mock_current_user', JSON.stringify(normalizedUser)); + console.log('[Mock State] 设置当前登录用户:', normalizedUser); } } diff --git a/src/mocks/handlers/auth.js b/src/mocks/handlers/auth.js index 1177e760..e130d8bf 100644 --- a/src/mocks/handlers/auth.js +++ b/src/mocks/handlers/auth.js @@ -94,7 +94,13 @@ export const authHandlers = [ email: null, avatar_url: `https://i.pravatar.cc/150?img=${id}`, has_wechat: false, - created_at: new Date().toISOString() + created_at: new Date().toISOString(), + // 默认订阅信息 - 免费用户 + subscription_type: 'free', + subscription_status: 'active', + subscription_end_date: null, + is_subscription_active: true, + subscription_days_left: 0 }; mockUsers[credential] = user; console.log('[Mock] 创建新用户:', user); diff --git a/src/mocks/handlers/event.js b/src/mocks/handlers/event.js index 1f905663..a263a1ba 100644 --- a/src/mocks/handlers/event.js +++ b/src/mocks/handlers/event.js @@ -3,6 +3,7 @@ import { http, HttpResponse } from 'msw'; import { getEventRelatedStocks } from '../data/events'; +import { getMockFutureEvents, getMockEventCountsForMonth } from '../data/account'; // 模拟网络延迟 const delay = (ms = 300) => new Promise(resolve => setTimeout(resolve, ms)); @@ -36,4 +37,718 @@ export const eventHandlers = [ ); } }), + + // 获取事件传导链分析数据 + http.get('/api/events/:eventId/transmission', async ({ params }) => { + await delay(500); + + const { eventId } = params; + + console.log('[Mock] 获取事件传导链分析, eventId:', eventId); + + // Mock数据:事件传导链 + const mockTransmissionData = { + success: true, + data: { + nodes: [ + { + id: '1', + name: '主要事件', + category: '事件', + value: 50, + extra: { + node_type: 'event', + description: '这是主要事件节点', + importance_score: 50, + is_main_event: true + } + }, + { + id: '2', + name: '半导体行业', + category: '行业', + value: 40, + extra: { + node_type: 'industry', + description: '受影响的半导体行业', + importance_score: 40, + is_main_event: false + } + }, + { + id: '3', + name: '芯片制造', + category: '行业', + value: 35, + extra: { + node_type: 'industry', + description: '芯片制造产业链', + importance_score: 35, + is_main_event: false + } + }, + { + id: '4', + name: 'A公司', + category: '公司', + value: 30, + extra: { + node_type: 'company', + description: '龙头企业A', + importance_score: 30, + stock_code: '600000', + is_main_event: false + } + }, + { + id: '5', + name: 'B公司', + category: '公司', + value: 25, + extra: { + node_type: 'company', + description: '龙头企业B', + importance_score: 25, + stock_code: '600001', + is_main_event: false + } + }, + { + id: '6', + name: '相关政策', + category: '政策', + value: 30, + extra: { + node_type: 'policy', + description: '国家产业政策支持', + importance_score: 30, + is_main_event: false + } + } + ], + edges: [ + { + source: '1', + target: '2', + value: 0.8, + extra: { + transmission_strength: 0.8, + transmission_type: '直接影响', + description: '主事件对半导体行业的直接影响' + } + }, + { + source: '2', + target: '3', + value: 0.7, + extra: { + transmission_strength: 0.7, + transmission_type: '产业链传导', + description: '半导体到芯片制造的传导' + } + }, + { + source: '3', + target: '4', + value: 0.6, + extra: { + transmission_strength: 0.6, + transmission_type: '企业影响', + description: '对龙头企业A的影响' + } + }, + { + source: '3', + target: '5', + value: 0.5, + extra: { + transmission_strength: 0.5, + transmission_type: '企业影响', + description: '对龙头企业B的影响' + } + }, + { + source: '6', + target: '1', + value: 0.7, + extra: { + transmission_strength: 0.7, + transmission_type: '政策驱动', + description: '政策对主事件的推动作用' + } + }, + { + source: '6', + target: '2', + value: 0.6, + extra: { + transmission_strength: 0.6, + transmission_type: '政策支持', + description: '政策对行业的支持' + } + } + ], + categories: ['事件', '行业', '公司', '政策', '技术', '市场', '其他'] + }, + message: '获取成功' + }; + + return HttpResponse.json(mockTransmissionData); + }), + + // 获取桑基图数据 + http.get('/api/events/:eventId/sankey-data', async ({ params }) => { + await delay(300); + const { eventId } = params; + console.log('[Mock] 获取桑基图数据, eventId:', eventId); + + const mockSankeyData = { + success: true, + data: { + nodes: [ + { + name: '相关政策', + type: 'policy', + level: 0, + color: '#10ac84' + }, + { + name: '主要事件', + type: 'event', + level: 0, + color: '#ff4757' + }, + { + name: '半导体行业', + type: 'industry', + level: 1, + color: '#00d2d3' + }, + { + name: '芯片制造', + type: 'industry', + level: 2, + color: '#00d2d3' + }, + { + name: 'A公司', + type: 'company', + level: 3, + color: '#54a0ff' + }, + { + name: 'B公司', + type: 'company', + level: 3, + color: '#54a0ff' + } + ], + links: [ + { source: 0, target: 1, value: 7 }, // 相关政策 -> 主要事件 + { source: 0, target: 2, value: 6 }, // 相关政策 -> 半导体行业 + { source: 1, target: 2, value: 8 }, // 主要事件 -> 半导体行业 + { source: 2, target: 3, value: 7 }, // 半导体行业 -> 芯片制造 + { source: 3, target: 4, value: 6 }, // 芯片制造 -> A公司 + { source: 3, target: 5, value: 5 } // 芯片制造 -> B公司 + ] + }, + message: '获取成功' + }; + + return HttpResponse.json(mockSankeyData); + }), + + // 获取传导链节点详情 + http.get('/api/events/:eventId/chain-node/:nodeId', async ({ params }) => { + await delay(300); + + const { eventId, nodeId } = params; + + console.log('[Mock] 获取节点详情, eventId:', eventId, 'nodeId:', nodeId); + + // 根据节点ID返回不同的详细信息 + const nodeDetailsMap = { + '1': { + success: true, + data: { + node: { + id: '1', + name: '主要事件', + type: 'event', + description: '这是影响整个产业链的重大事件,涉及政策调整和技术突破,对下游产业产生深远影响。', + importance_score: 50, + total_connections: 2, + incoming_connections: 1, + outgoing_connections: 1 + }, + parents: [ + { + id: '6', + name: '相关政策', + transmission_mechanism: { + data: [ + { + author: "国务院", + sentences: "为加快实施创新驱动发展战略,推动产业转型升级,国家将对重点领域给予财政补贴支持,单个项目最高补贴金额可达5000万元,同时享受研发费用加计扣除175%的税收优惠政策", + query_part: "国家财政补贴最高5000万元,研发费用加计扣除175%", + match_score: "好", + declare_date: "2024-01-15T00:00:00", + report_title: "关于促进产业高质量发展的若干政策措施" + }, + { + author: "工信部", + sentences: "根据《重点产业扶持目录》,对符合条件的企业和项目,将优先纳入政府采购名单,并提供专项资金支持,确保政策红利直接惠及实体经济", + query_part: "政府采购优先支持,专项资金直达企业", + match_score: "好", + declare_date: "2024-01-20T00:00:00", + report_title: "工业和信息化部关于落实产业扶持政策的通知" + } + ] + }, + direction: 'positive', + strength: 70, + is_circular: false + } + ], + children: [ + { + id: '2', + name: '半导体行业', + transmission_mechanism: { + data: [ + { + author: "中国电子信息产业发展研究院", + sentences: "在技术突破和应用场景快速扩张的双重驱动下,国内半导体市场呈现爆发式增长态势。据统计,2024年上半年半导体市场规模达到1.2万亿元,同比增长32%,其中新能源汽车和AI算力芯片需求贡献了超过60%的增量", + query_part: "技术突破和需求激增推动半导体市场增长32%", + match_score: "好", + declare_date: "2024-07-10T00:00:00", + report_title: "2024年上半年中国半导体产业发展报告" + }, + { + author: "工信部电子信息司", + sentences: "随着5G、人工智能、物联网等新一代信息技术的快速发展,半导体作为数字经济的基石,正迎来前所未有的发展机遇。预计未来三年,国内半导体市场年均增速将保持在25%以上", + query_part: "新兴技术推动半导体产业高速增长", + match_score: "好", + declare_date: "2024-05-20T00:00:00", + report_title: "新一代信息技术产业发展白皮书" + } + ] + }, + direction: 'positive', + strength: 80, + is_circular: false + } + ] + } + }, + '2': { + success: true, + data: { + node: { + id: '2', + name: '半导体行业', + type: 'industry', + description: '半导体行业是现代科技产业的基础,受到主事件和政策的双重推动,迎来新一轮发展机遇。', + importance_score: 40, + total_connections: 3, + incoming_connections: 2, + outgoing_connections: 1 + }, + parents: [ + { + id: '1', + name: '主要事件', + transmission_mechanism: { + data: [ + { + author: "中国半导体行业协会", + sentences: "受益于新能源汽车、5G通信等新兴应用领域的爆发式增长,国内半导体市场需求持续旺盛,2024年Q1市场规模同比增长28%,创历史新高", + query_part: "新兴应用推动半导体需求增长28%", + match_score: "好", + declare_date: "2024-04-05T00:00:00", + report_title: "2024年Q1中国半导体行业景气度报告" + } + ] + }, + direction: 'positive', + strength: 80, + is_circular: false + }, + { + id: '6', + name: '相关政策', + transmission_mechanism: { + data: [ + { + author: "国家发改委", + sentences: "《国家集成电路产业发展推进纲要》明确提出,到2025年半导体产业自给率要达到70%以上,国家将设立专项基金规模超过3000亿元,重点支持半导体设备、材料、设计等关键环节", + query_part: "半导体自给率目标70%,专项基金3000亿", + match_score: "好", + declare_date: "2024-02-01T00:00:00", + report_title: "国家集成电路产业发展推进纲要(2024-2030)" + } + ] + }, + direction: 'positive', + strength: 60, + is_circular: false + } + ], + children: [ + { + id: '3', + name: '芯片制造', + transmission_mechanism: { + data: [ + { + author: "张明", + sentences: "在半导体行业景气度持续提升的背景下,下游芯片制造企业订单饱满,产能利用率达到历史新高,预计2024年产能扩张将达到30%以上,技术工艺也将从28nm向14nm升级", + query_part: "半导体行业繁荣带动芯片制造产能扩张30%", + match_score: "好", + declare_date: "2024-03-15T00:00:00", + report_title: "半导体行业深度报告:产业链景气度传导分析" + }, + { + author: "李华", + sentences: "芯片制造环节作为半导体产业链核心,受益于上游材料供应稳定和下游应用需求旺盛,技术迭代速度明显加快,先进制程占比持续提升", + query_part: "技术迭代加快,先进制程占比提升", + match_score: "好", + declare_date: "2024-02-28T00:00:00", + report_title: "芯片制造行业跟踪报告" + } + ] + }, + direction: 'positive', + strength: 70, + is_circular: false + } + ] + } + }, + '3': { + success: true, + data: { + node: { + id: '3', + name: '芯片制造', + type: 'industry', + description: '芯片制造作为半导体产业链的核心环节,在上游需求推动下,产能利用率提升,技术迭代加快。', + importance_score: 35, + total_connections: 3, + incoming_connections: 1, + outgoing_connections: 2 + }, + parents: [ + { + id: '2', + name: '半导体行业', + transmission_mechanism: { + data: [ + { + author: "张明", + sentences: "在半导体行业景气度持续提升的背景下,下游芯片制造企业订单饱满,产能利用率达到历史新高,预计2024年产能扩张将达到30%以上,技术工艺也将从28nm向14nm升级", + query_part: "半导体行业繁荣带动芯片制造产能扩张30%", + match_score: "好", + declare_date: "2024-03-15T00:00:00", + report_title: "半导体行业深度报告:产业链景气度传导分析" + }, + { + author: "李华", + sentences: "芯片制造环节作为半导体产业链核心,受益于上游材料供应稳定和下游应用需求旺盛,技术迭代速度明显加快,先进制程占比持续提升", + query_part: "技术迭代加快,先进制程占比提升", + match_score: "好", + declare_date: "2024-02-28T00:00:00", + report_title: "芯片制造行业跟踪报告" + } + ] + }, + direction: 'positive', + strength: 70, + is_circular: false + } + ], + children: [ + { + id: '4', + name: 'A公司', + transmission_mechanism: { + data: [ + { + author: "王芳", + sentences: "A公司作为国内芯片制造龙头企业,在手订单已排至2024年Q4,预计全年营收增长45%,净利润增长60%以上。公司28nm及以下先进制程产能占比已达到40%,技术实力行业领先", + query_part: "A公司在手订单充足,预计营收增长45%", + match_score: "好", + declare_date: "2024-04-10T00:00:00", + report_title: "A公司深度研究:受益芯片制造景气周期" + } + ] + }, + direction: 'positive', + strength: 60, + is_circular: false + }, + { + id: '5', + name: 'B公司', + transmission_mechanism: { + data: [ + { + author: "赵强", + sentences: "随着芯片制造产能的大规模扩张,上游设备和材料供应商迎来历史性机遇。B公司作为核心配套企业,订单量同比增长55%,产品供不应求,预计2024年营收将突破百亿大关。公司在封装测试领域的市场份额已提升至国内第二位", + query_part: "B公司订单增长55%,营收将破百亿", + match_score: "好", + declare_date: "2024-05-08T00:00:00", + report_title: "B公司跟踪报告:芯片产业链配套龙头崛起" + }, + { + author: "国信证券", + sentences: "B公司深度受益于芯片制造产业链的景气度传导。公司凭借先进的封装技术和完善的产能布局,成功绑定多家头部芯片制造企业,形成稳定的供应关系。随着下游客户产能持续扩张,公司业绩增长确定性强", + query_part: "B公司受益产业链景气度,业绩增长确定性强", + match_score: "好", + declare_date: "2024-06-01T00:00:00", + report_title: "半导体封装测试行业专题:产业链景气度传导分析" + } + ] + }, + direction: 'positive', + strength: 50, + is_circular: false + } + ] + } + }, + '4': { + success: true, + data: { + node: { + id: '4', + name: 'A公司', + type: 'company', + description: 'A公司是行业龙头企业,拥有先进的芯片制造技术和完整的产业链布局,在本轮产业升级中占据有利位置。', + importance_score: 30, + stock_code: '600000', + total_connections: 1, + incoming_connections: 1, + outgoing_connections: 0 + }, + parents: [ + { + id: '3', + name: '芯片制造', + transmission_mechanism: { + data: [ + { + author: "王芳", + sentences: "A公司作为国内芯片制造龙头企业,在手订单已排至2024年Q4,预计全年营收增长45%,净利润增长60%以上。公司28nm及以下先进制程产能占比已达到40%,技术实力行业领先", + query_part: "A公司在手订单充足,预计营收增长45%", + match_score: "好", + declare_date: "2024-04-10T00:00:00", + report_title: "A公司深度研究:受益芯片制造景气周期" + } + ] + }, + direction: 'positive', + strength: 60, + is_circular: false + } + ], + children: [] + } + }, + '5': { + success: true, + data: { + node: { + id: '5', + name: 'B公司', + type: 'company', + description: 'B公司专注于芯片封装测试领域,随着上游制造产能释放,公司订单饱满,业绩稳步增长。', + importance_score: 25, + stock_code: '600001', + total_connections: 1, + incoming_connections: 1, + outgoing_connections: 0 + }, + parents: [ + { + id: '3', + name: '芯片制造', + transmission_mechanism: { + data: [ + { + author: "赵强", + sentences: "随着芯片制造产能的大规模扩张,上游设备和材料供应商迎来历史性机遇。B公司作为核心配套企业,订单量同比增长55%,产品供不应求,预计2024年营收将突破百亿大关", + query_part: "B公司订单增长55%,营收将破百亿", + match_score: "好", + declare_date: "2024-05-08T00:00:00", + report_title: "B公司跟踪报告:芯片产业链配套龙头崛起" + } + ] + }, + direction: 'positive', + strength: 50, + is_circular: false + } + ], + children: [] + } + }, + '6': { + success: true, + data: { + node: { + id: '6', + name: '相关政策', + type: 'policy', + description: '国家出台了一系列产业扶持政策,包括财政补贴、税收减免和研发支持,旨在推动产业自主创新和进口替代。', + importance_score: 30, + total_connections: 2, + incoming_connections: 0, + outgoing_connections: 2 + }, + parents: [], + children: [ + { + id: '1', + name: '主要事件', + transmission_mechanism: { + data: [ + { + author: "国务院", + sentences: "为加快实施创新驱动发展战略,推动产业转型升级,国家将对重点领域给予财政补贴支持,单个项目最高补贴金额可达5000万元,同时享受研发费用加计扣除175%的税收优惠政策", + query_part: "国家财政补贴最高5000万元,研发费用加计扣除175%", + match_score: "好", + declare_date: "2024-01-15T00:00:00", + report_title: "关于促进产业高质量发展的若干政策措施" + }, + { + author: "工信部", + sentences: "将重点支持关键核心技术攻关和产业化应用,建立产业发展专项基金,规模达到1000亿元,引导社会资本共同参与产业发展", + query_part: "设立1000亿元产业发展专项基金", + match_score: "好", + declare_date: "2024-02-01T00:00:00", + report_title: "产业发展专项基金管理办法" + } + ] + }, + direction: 'positive', + strength: 70, + is_circular: false + }, + { + id: '2', + name: '半导体行业', + transmission_mechanism: { + data: [ + { + author: "国家发改委", + sentences: "《国家集成电路产业发展推进纲要》明确提出,到2025年半导体产业自给率要达到70%以上,国家将设立专项基金规模超过3000亿元,重点支持半导体设备、材料、设计等关键环节。同时,通过进口替代战略,加快培育本土产业链", + query_part: "半导体自给率目标70%,专项基金3000亿", + match_score: "好", + declare_date: "2024-02-01T00:00:00", + report_title: "国家集成电路产业发展推进纲要(2024-2030)" + }, + { + author: "工信部", + sentences: "将重点支持关键核心技术攻关和产业化应用,建立产业发展专项基金,规模达到1000亿元,引导社会资本共同参与产业发展。通过税收优惠、研发补贴等政策工具,为半导体行业创造良好的发展环境", + query_part: "设立1000亿元产业发展专项基金", + match_score: "好", + declare_date: "2024-02-01T00:00:00", + report_title: "产业发展专项基金管理办法" + } + ] + }, + direction: 'positive', + strength: 60, + is_circular: false + } + ] + } + } + }; + + // 返回对应节点的详情,如果不存在则返回默认数据 + const nodeDetail = nodeDetailsMap[nodeId] || { + success: true, + data: { + node: { + id: nodeId, + name: '未知节点', + type: 'other', + description: '该节点暂无详细信息', + importance_score: 0, + total_connections: 0, + incoming_connections: 0, + outgoing_connections: 0 + }, + parents: [], + children: [] + } + }; + + return HttpResponse.json(nodeDetail); + }), + + // ==================== 投资日历相关 ==================== + + // 获取月度事件统计 + http.get('/api/v1/calendar/event-counts', async ({ request }) => { + await delay(300); + + const url = new URL(request.url); + const year = parseInt(url.searchParams.get('year')); + const month = parseInt(url.searchParams.get('month')); + + console.log('[Mock] 获取月度事件统计:', { year, month }); + + const eventCounts = getMockEventCountsForMonth(year, month); + + return HttpResponse.json({ + success: true, + data: eventCounts + }); + }), + + // 获取指定日期的事件列表 + http.get('/api/v1/calendar/events', async ({ request }) => { + await delay(300); + + const url = new URL(request.url); + const dateStr = url.searchParams.get('date'); + const type = url.searchParams.get('type') || 'all'; + + console.log('[Mock] 获取日历事件列表:', { date: dateStr, type }); + + if (!dateStr) { + return HttpResponse.json({ + success: false, + error: 'Date parameter required' + }, { status: 400 }); + } + + const events = getMockFutureEvents(dateStr, type); + + return HttpResponse.json({ + success: true, + data: events + }); + }), + + // 切换未来事件关注状态 + http.post('/api/v1/calendar/events/:eventId/follow', async ({ params }) => { + await delay(300); + + const { eventId } = params; + + console.log('[Mock] 切换事件关注状态, eventId:', eventId); + + // 简单返回成功,实际状态管理可以后续完善 + return HttpResponse.json({ + success: true, + data: { + is_following: true, + message: '关注成功' + } + }); + }), ]; diff --git a/src/services/authService.js b/src/services/authService.js index 86d4c48d..53c6ad46 100644 --- a/src/services/authService.js +++ b/src/services/authService.js @@ -1,3 +1,4 @@ +import { getApiBase } from '../utils/apiConfig'; // src/services/authService.js /** * 认证服务层 - 处理所有认证相关的 API 调用 @@ -6,7 +7,7 @@ import { logger } from '../utils/logger'; const isProduction = process.env.NODE_ENV === 'production'; -const API_BASE_URL = isProduction ? "" : process.env.REACT_APP_API_URL; +const API_BASE_URL = getApiBase(); /** * 统一的 API 请求处理 diff --git a/src/services/financialService.js b/src/services/financialService.js index 84feeb80..7fe04dfe 100644 --- a/src/services/financialService.js +++ b/src/services/financialService.js @@ -1,3 +1,4 @@ +import { getApiBase } from '../utils/apiConfig'; // src/services/financialService.js /** * 完整的财务数据服务层 @@ -7,7 +8,7 @@ import { logger } from '../utils/logger'; const isProduction = process.env.NODE_ENV === 'production'; -const API_BASE_URL = isProduction ? "" : process.env.REACT_APP_API_URL; +const API_BASE_URL = getApiBase(); const apiRequest = async (url, options = {}) => { try { diff --git a/src/services/industryService.js b/src/services/industryService.js index d89c42a1..f303e358 100755 --- a/src/services/industryService.js +++ b/src/services/industryService.js @@ -1,3 +1,4 @@ +import { getApiBase } from '../utils/apiConfig'; // src/services/industryService.js import axios from 'axios'; @@ -5,7 +6,7 @@ import axios from 'axios'; const isProduction = process.env.NODE_ENV === 'production'; -const API_BASE_URL = isProduction ? "" : process.env.REACT_APP_API_URL; +const API_BASE_URL = getApiBase(); // 配置 axios 默认包含 credentials axios.defaults.withCredentials = true; diff --git a/src/services/marketService.js b/src/services/marketService.js index 94e887a1..d8c45636 100644 --- a/src/services/marketService.js +++ b/src/services/marketService.js @@ -1,3 +1,4 @@ +import { getApiBase } from '../utils/apiConfig'; // src/services/marketService.js /** * 完整的市场行情数据服务层 @@ -7,7 +8,7 @@ import { logger } from '../utils/logger'; const isProduction = process.env.NODE_ENV === 'production'; -const API_BASE_URL = isProduction ? "" : process.env.REACT_APP_API_URL; +const API_BASE_URL = getApiBase(); const apiRequest = async (url, options = {}) => { try { diff --git a/src/utils/apiConfig.js b/src/utils/apiConfig.js new file mode 100644 index 00000000..03928679 --- /dev/null +++ b/src/utils/apiConfig.js @@ -0,0 +1,56 @@ +/** + * API 配置工具 + * 提供统一的 API 基础地址获取方法 + */ + +/** + * 获取 API 基础 URL + * + * 工作原理: + * - 生产环境: 返回空字符串,使用相对路径 + * - Mock 模式 (REACT_APP_API_URL=""): 返回空字符串,让 MSW 拦截请求 + * - 开发模式: 返回后端服务器地址 + * + * @returns {string} API 基础地址 + * + * @example + * const response = await fetch(getApiBase() + '/api/users'); + */ +export const getApiBase = () => { + // 生产环境使用相对路径 + if (process.env.NODE_ENV === 'production') { + return ''; + } + + // 检查是否定义了 REACT_APP_API_URL(包括空字符串) + // 使用 !== undefined 而不是 || 运算符,正确处理空字符串 + const apiUrl = process.env.REACT_APP_API_URL; + if (apiUrl !== undefined) { + return apiUrl; // Mock 模式下返回 '',其他情况返回配置的值 + } + + // 未配置时的默认后端地址 + return 'http://49.232.185.254:5001'; +}; + +/** + * 检查是否处于 Mock 模式 + * @returns {boolean} + */ +export const isMockMode = () => { + return process.env.REACT_APP_ENABLE_MOCK === 'true'; +}; + +/** + * 获取完整的 API URL + * @param {string} path - API 路径,应以 / 开头 + * @returns {string} 完整的 URL + * + * @example + * const url = getApiUrl('/api/users'); + * // Mock 模式: '/api/users' + * // 开发模式: 'http://49.232.185.254:5001/api/users' + */ +export const getApiUrl = (path) => { + return getApiBase() + path; +}; diff --git a/src/utils/axiosConfig.js b/src/utils/axiosConfig.js index 929de312..945fecdd 100644 --- a/src/utils/axiosConfig.js +++ b/src/utils/axiosConfig.js @@ -2,13 +2,14 @@ // Axios 全局配置和拦截器 import axios from 'axios'; +import { getApiBase } from './apiConfig'; import { logger } from './logger'; // 判断当前是否是生产环境 const isProduction = process.env.NODE_ENV === 'production'; // 配置基础 URL -const API_BASE_URL = isProduction ? '' : process.env.REACT_APP_API_URL; +const API_BASE_URL = getApiBase(); // 配置 axios 默认值 axios.defaults.baseURL = API_BASE_URL; diff --git a/src/views/Authentication/SignUp/SignUpIllustration.js b/src/views/Authentication/SignUp/SignUpIllustration.js index 209c0110..fa719b8d 100755 --- a/src/views/Authentication/SignUp/SignUpIllustration.js +++ b/src/views/Authentication/SignUp/SignUpIllustration.js @@ -1,5 +1,6 @@ // src\views\Authentication\SignUp/SignUpIllustration.js import React, { useState, useEffect, useRef } from "react"; +import { getApiBase } from '../../../utils/apiConfig'; import { Box, Button, @@ -31,7 +32,7 @@ import PrivacyPolicyModal from '../../../components/PrivacyPolicyModal'; import UserAgreementModal from '../../../components/UserAgreementModal'; const isProduction = process.env.NODE_ENV === 'production'; -const API_BASE_URL = isProduction ? "" : process.env.REACT_APP_API_URL; +const API_BASE_URL = getApiBase(); export default function SignUpPage() { const [showPassword, setShowPassword] = useState(false); diff --git a/src/views/Community/components/EventDetailModal.js b/src/views/Community/components/EventDetailModal.js index bee26b87..a39d4307 100644 --- a/src/views/Community/components/EventDetailModal.js +++ b/src/views/Community/components/EventDetailModal.js @@ -67,6 +67,28 @@ const EventDetailModal = ({ visible, event, onClose }) => { return colors[importance] || 'default'; }; + const getRelationDesc = (relationDesc) => { + // 处理空值 + if (!relationDesc) return ''; + + // 如果是字符串,直接返回 + if (typeof relationDesc === 'string') { + return relationDesc; + } + + // 如果是对象且包含data数组 + if (typeof relationDesc === 'object' && relationDesc.data && Array.isArray(relationDesc.data)) { + const firstItem = relationDesc.data[0]; + if (firstItem) { + // 优先使用 query_part,其次使用 sentences + return firstItem.query_part || firstItem.sentences || ''; + } + } + + // 其他情况返回空字符串 + return ''; + }; + const renderPriceTag = (value, label) => { if (value === null || value === undefined) return `${label}: --`; @@ -176,7 +198,7 @@ const EventDetailModal = ({ visible, event, onClose }) => { > {stock.change !== null && ( 0 ? 'red' : 'green'}> diff --git a/src/views/Community/components/EventList.js b/src/views/Community/components/EventList.js index 36a2f947..01adf502 100644 --- a/src/views/Community/components/EventList.js +++ b/src/views/Community/components/EventList.js @@ -52,6 +52,7 @@ import { import { useNavigate } from 'react-router-dom'; import moment from 'moment'; import { logger } from '../../../utils/logger'; +import { getApiBase } from '../../../utils/apiConfig'; // ========== 工具函数定义在组件外部 ========== // 涨跌颜色配置(中国A股配色:红涨绿跌)- 分档次显示 @@ -180,7 +181,7 @@ const EventList = ({ events, pagination, onPageChange, onEventClick, onViewDetai const loadFollowing = async () => { try { - const base = (process.env.NODE_ENV === 'production' ? '' : process.env.REACT_APP_API_URL || 'http://49.232.185.254:5001'); + const base = getApiBase(); const res = await fetch(base + '/api/account/events/following', { credentials: 'include' }); const data = await res.json(); if (res.ok && data.success) { @@ -201,7 +202,7 @@ const EventList = ({ events, pagination, onPageChange, onEventClick, onViewDetai const toggleFollow = async (eventId) => { try { - const base = (process.env.NODE_ENV === 'production' ? '' : process.env.REACT_APP_API_URL || 'http://49.232.185.254:5001'); + const base = getApiBase(); const res = await fetch(base + `/api/events/${eventId}/follow`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, diff --git a/src/views/Community/components/EventList.js.bak b/src/views/Community/components/EventList.js.bak new file mode 100644 index 00000000..36a2f947 --- /dev/null +++ b/src/views/Community/components/EventList.js.bak @@ -0,0 +1,818 @@ +// src/views/Community/components/EventList.js +import React, { useState, useEffect } from 'react'; +import { + Box, + VStack, + HStack, + Text, + Button, + Badge, + Tag, + TagLabel, + TagLeftIcon, + Flex, + Avatar, + Tooltip, + IconButton, + Divider, + Container, + useColorModeValue, + Circle, + Stat, + StatNumber, + StatHelpText, + StatArrow, + ButtonGroup, + Heading, + SimpleGrid, + Card, + CardBody, + Center, + Link, + Spacer, + Switch, + FormControl, + FormLabel, +} from '@chakra-ui/react'; +import { + ViewIcon, + ChatIcon, + StarIcon, + TimeIcon, + InfoIcon, + WarningIcon, + WarningTwoIcon, + CheckCircleIcon, + TriangleUpIcon, + TriangleDownIcon, + ArrowForwardIcon, + ExternalLinkIcon, + ViewOffIcon, +} from '@chakra-ui/icons'; +import { useNavigate } from 'react-router-dom'; +import moment from 'moment'; +import { logger } from '../../../utils/logger'; + +// ========== 工具函数定义在组件外部 ========== +// 涨跌颜色配置(中国A股配色:红涨绿跌)- 分档次显示 +const getPriceChangeColor = (value) => { + if (value === null || value === undefined) return 'gray.500'; + + const absValue = Math.abs(value); + + if (value > 0) { + // 上涨用红色,根据涨幅大小使用不同深浅 + if (absValue >= 3) return 'red.600'; // 深红色:3%以上 + if (absValue >= 1) return 'red.500'; // 中红色:1-3% + return 'red.400'; // 浅红色:0-1% + } else if (value < 0) { + // 下跌用绿色,根据跌幅大小使用不同深浅 + if (absValue >= 3) return 'green.600'; // 深绿色:3%以上 + if (absValue >= 1) return 'green.500'; // 中绿色:1-3% + return 'green.400'; // 浅绿色:0-1% + } + return 'gray.500'; +}; + +const getPriceChangeBg = (value) => { + if (value === null || value === undefined) return 'gray.50'; + + const absValue = Math.abs(value); + + if (value > 0) { + // 上涨背景色 + if (absValue >= 3) return 'red.100'; // 深色背景:3%以上 + if (absValue >= 1) return 'red.50'; // 中色背景:1-3% + return 'red.50'; // 浅色背景:0-1% + } else if (value < 0) { + // 下跌背景色 + if (absValue >= 3) return 'green.100'; // 深色背景:3%以上 + if (absValue >= 1) return 'green.50'; // 中色背景:1-3% + return 'green.50'; // 浅色背景:0-1% + } + return 'gray.50'; +}; + +const getPriceChangeBorderColor = (value) => { + if (value === null || value === undefined) return 'gray.300'; + + const absValue = Math.abs(value); + + if (value > 0) { + // 上涨边框色 + if (absValue >= 3) return 'red.500'; // 深边框:3%以上 + if (absValue >= 1) return 'red.400'; // 中边框:1-3% + return 'red.300'; // 浅边框:0-1% + } else if (value < 0) { + // 下跌边框色 + if (absValue >= 3) return 'green.500'; // 深边框:3%以上 + if (absValue >= 1) return 'green.400'; // 中边框:1-3% + return 'green.300'; // 浅边框:0-1% + } + return 'gray.300'; +}; + +// 重要性等级配置 - 金融配色方案 +const importanceLevels = { + 'S': { + color: 'purple.600', + bgColor: 'purple.50', + borderColor: 'purple.200', + icon: WarningIcon, + label: '极高', + dotBg: 'purple.500', + }, + 'A': { + color: 'red.600', + bgColor: 'red.50', + borderColor: 'red.200', + icon: WarningTwoIcon, + label: '高', + dotBg: 'red.500', + }, + 'B': { + color: 'orange.600', + bgColor: 'orange.50', + borderColor: 'orange.200', + icon: InfoIcon, + label: '中', + dotBg: 'orange.500', + }, + 'C': { + color: 'green.600', + bgColor: 'green.50', + borderColor: 'green.200', + icon: CheckCircleIcon, + label: '低', + dotBg: 'green.500', + } +}; + +const getImportanceConfig = (importance) => { + return importanceLevels[importance] || importanceLevels['C']; +}; + +// 自定义的涨跌箭头组件(修复颜色问题) +const PriceArrow = ({ value }) => { + if (value === null || value === undefined) return null; + + const Icon = value > 0 ? TriangleUpIcon : TriangleDownIcon; + const color = value > 0 ? 'red.500' : 'green.500'; + + return ; +}; + +// ========== 主组件 ========== +const EventList = ({ events, pagination, onPageChange, onEventClick, onViewDetail }) => { + const navigate = useNavigate(); + const [isCompactMode, setIsCompactMode] = useState(false); // 新增:紧凑模式状态 + const [followingMap, setFollowingMap] = useState({}); + const [followCountMap, setFollowCountMap] = useState({}); + + // 初始化关注状态与计数 + useEffect(() => { + // 初始化计数映射 + const initCounts = {}; + events.forEach(ev => { + initCounts[ev.id] = ev.follower_count || 0; + }); + setFollowCountMap(initCounts); + + const loadFollowing = async () => { + try { + const base = (process.env.NODE_ENV === 'production' ? '' : process.env.REACT_APP_API_URL || 'http://49.232.185.254:5001'); + const res = await fetch(base + '/api/account/events/following', { credentials: 'include' }); + const data = await res.json(); + if (res.ok && data.success) { + const map = {}; + (data.data || []).forEach(ev => { map[ev.id] = true; }); + setFollowingMap(map); + logger.debug('EventList', '关注状态加载成功', { + followingCount: Object.keys(map).length + }); + } + } catch (e) { + logger.warn('EventList', '加载关注状态失败', { error: e.message }); + } + }; + loadFollowing(); + // 仅在 events 更新时重跑 + }, [events]); + + const toggleFollow = async (eventId) => { + try { + const base = (process.env.NODE_ENV === 'production' ? '' : process.env.REACT_APP_API_URL || 'http://49.232.185.254:5001'); + const res = await fetch(base + `/api/events/${eventId}/follow`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + credentials: 'include' + }); + const data = await res.json(); + if (!res.ok || !data.success) throw new Error(data.error || '操作失败'); + const isFollowing = data.data?.is_following; + const count = data.data?.follower_count ?? 0; + setFollowingMap(prev => ({ ...prev, [eventId]: isFollowing })); + setFollowCountMap(prev => ({ ...prev, [eventId]: count })); + logger.debug('EventList', '关注状态切换成功', { + eventId, + isFollowing, + followerCount: count + }); + } catch (e) { + logger.warn('EventList', '关注操作失败', { + eventId, + error: e.message + }); + } + }; + + // 专业的金融配色方案 + const bgColor = useColorModeValue('gray.50', 'gray.900'); + const cardBg = useColorModeValue('white', 'gray.800'); + const borderColor = useColorModeValue('gray.200', 'gray.700'); + const textColor = useColorModeValue('gray.700', 'gray.200'); + const mutedColor = useColorModeValue('gray.500', 'gray.400'); + const linkColor = useColorModeValue('blue.600', 'blue.400'); + const hoverBg = useColorModeValue('gray.50', 'gray.700'); + + const renderPriceChange = (value, label) => { + if (value === null || value === undefined) { + return ( + + {label}: -- + + ); + } + + const absValue = Math.abs(value); + const isPositive = value > 0; + + // 根据涨跌幅大小选择不同的颜色深浅 + let colorScheme = 'gray'; + let variant = 'solid'; + + if (isPositive) { + // 上涨用红色系 + if (absValue >= 3) { + colorScheme = 'red'; + variant = 'solid'; // 深色 + } else if (absValue >= 1) { + colorScheme = 'red'; + variant = 'subtle'; // 中等 + } else { + colorScheme = 'red'; + variant = 'outline'; // 浅色 + } + } else { + // 下跌用绿色系 + if (absValue >= 3) { + colorScheme = 'green'; + variant = 'solid'; // 深色 + } else if (absValue >= 1) { + colorScheme = 'green'; + variant = 'subtle'; // 中等 + } else { + colorScheme = 'green'; + variant = 'outline'; // 浅色 + } + } + + const Icon = isPositive ? TriangleUpIcon : TriangleDownIcon; + + return ( + + + + {label}: {isPositive ? '+' : ''}{value.toFixed(2)}% + + + ); + }; + + const handleTitleClick = (e, event) => { + e.preventDefault(); + e.stopPropagation(); + onEventClick(event); + }; + + const handleViewDetailClick = (e, eventId) => { + e.stopPropagation(); + navigate(`/event-detail/${eventId}`); + }; + + // 精简模式的事件渲染 + const renderCompactEvent = (event) => { + const importance = getImportanceConfig(event.importance); + const isFollowing = !!followingMap[event.id]; + const followerCount = followCountMap[event.id] ?? (event.follower_count || 0); + + return ( + + {/* 时间线和重要性标记 */} + + + {event.importance || 'C'} + + + + + {/* 精简事件卡片 */} + onEventClick(event)} + mb={3} + > + + + {/* 左侧:标题和时间 */} + + handleTitleClick(e, event)} + cursor="pointer" + noOfLines={1} + > + {event.title} + + + + {moment(event.created_at).format('MM-DD HH:mm')} + + {event.creator?.username || 'Anonymous'} + + + + {/* 右侧:涨跌幅指标 */} + + + + + + + {event.related_avg_chg != null + ? `${event.related_avg_chg > 0 ? '+' : ''}${event.related_avg_chg.toFixed(2)}%` + : '--'} + + + + + + + + + + + + + ); + }; + + // 详细模式的事件渲染(原有的渲染方式,但修复了箭头颜色) + const renderDetailedEvent = (event) => { + const importance = getImportanceConfig(event.importance); + const isFollowing = !!followingMap[event.id]; + const followerCount = followCountMap[event.id] ?? (event.follower_count || 0); + + return ( + + {/* 时间线和重要性标记 */} + + + {event.importance || 'C'} + + + + + {/* 事件卡片 */} + onEventClick(event)} + mb={4} + > + + + {/* 标题和重要性标签 */} + + + handleTitleClick(e, event)} + cursor="pointer" + > + {event.title} + + + + {importance.label}优先级 + + + + {/* 元信息 */} + + + + {moment(event.created_at).format('YYYY-MM-DD HH:mm')} + + + {event.creator?.username || 'Anonymous'} + + + {/* 描述 */} + + {event.description} + + + {/* 价格变化指标 */} + + + + + + + 平均涨幅 + + + {event.related_avg_chg != null ? ( + + + + {event.related_avg_chg > 0 ? '+' : ''}{event.related_avg_chg.toFixed(2)}% + + + ) : ( + -- + )} + + + + + + + + + + 最大涨幅 + + + {event.related_max_chg != null ? ( + + + + {event.related_max_chg > 0 ? '+' : ''}{event.related_max_chg.toFixed(2)}% + + + ) : ( + -- + )} + + + + + + + + + + 周涨幅 + + + {event.related_week_chg != null ? ( + + + + {event.related_week_chg > 0 ? '+' : ''}{event.related_week_chg.toFixed(2)}% + + + ) : ( + -- + )} + + + + + + + + + + {/* 统计信息和操作按钮 */} + + + + + + {event.view_count || 0} + + + + + + {event.post_count || 0} + + + + + + {followerCount} + + + + + + + + + + + + + + + ); + }; + + // 分页组件 + const Pagination = ({ current, total, pageSize, onChange }) => { + const totalPages = Math.ceil(total / pageSize); + + return ( + + + + + {[...Array(Math.min(5, totalPages))].map((_, i) => { + const pageNum = i + 1; + return ( + + ); + })} + {totalPages > 5 && ...} + {totalPages > 5 && ( + + )} + + + + + + 共 {total} 条 + + + ); + }; + + return ( + + + {/* 视图切换控制 */} + + + + 精简模式 + + setIsCompactMode(e.target.checked)} + colorScheme="blue" + /> + + + + {events.length > 0 ? ( + + {events.map((event, index) => ( + + {isCompactMode + ? renderCompactEvent(event) + : renderDetailedEvent(event) + } + + ))} + + ) : ( +
+ + + + 暂无事件数据 + + +
+ )} + + {pagination.total > 0 && ( + + )} +
+
+ ); +}; + +export default EventList; \ No newline at end of file diff --git a/src/views/Community/components/InvestmentCalendar.js b/src/views/Community/components/InvestmentCalendar.js index 3e26ccf4..9c6966c3 100644 --- a/src/views/Community/components/InvestmentCalendar.js +++ b/src/views/Community/components/InvestmentCalendar.js @@ -15,6 +15,7 @@ import StockChartAntdModal from '../../../components/StockChart/StockChartAntdMo import { useSubscription } from '../../../hooks/useSubscription'; import SubscriptionUpgradeModal from '../../../components/SubscriptionUpgradeModal'; import CitationMark from '../../../components/Citation/CitationMark'; +import CitedContent from '../../../components/Citation/CitedContent'; import { processCitationData } from '../../../utils/citationUtils'; import { logger } from '../../../utils/logger'; import './InvestmentCalendar.css'; @@ -194,9 +195,49 @@ const InvestmentCalendar = () => { return {stars}; }; - // 显示内容详情 + /** + * 显示内容详情 + * 支持两种数据格式: + * 1. 字符串格式:直接显示文本,自动添加"(AI合成)"标识 + * 例如:showContentDetail("这是事件背景内容", "事件背景") + * + * 2. 引用格式:使用CitedContent组件渲染,显示引用来源 + * 例如:showContentDetail({ + * data: [ + * { sentence: "第一句话", citation: { source: "来源1", url: "..." } }, + * { sentence: "第二句话", citation: { source: "来源2", url: "..." } } + * ] + * }, "事件背景") + * + * 后端API返回数据格式说明: + * - 字符串格式:former字段直接返回字符串 + * - 引用格式:former字段返回 { data: [...] } 对象,其中data是引用数组 + */ const showContentDetail = (content, title) => { - setSelectedDetail({ content, title }); + let processedContent; + + // 判断content类型:字符串或引用格式 + if (typeof content === 'string') { + // 字符串类型:添加AI合成标识 + processedContent = { + type: 'text', + content: content + (content ? '\n\n(AI合成)' : '') + }; + } else if (content && content.data && Array.isArray(content.data)) { + // 引用格式:使用CitedContent渲染 + processedContent = { + type: 'citation', + content: content + }; + } else { + // 其他情况:转为字符串并添加AI标识 + processedContent = { + type: 'text', + content: String(content || '') + '\n\n(AI合成)' + }; + } + + setSelectedDetail({ content: processedContent, title }); setDetailDrawerVisible(true); }; @@ -332,7 +373,7 @@ const InvestmentCalendar = () => { type="link" size="small" icon={} - onClick={() => showContentDetail(text + (text ? '\n\n(AI合成)' : ''), '事件背景')} + onClick={() => showContentDetail(text, '事件背景')} disabled={!text} > 查看 @@ -495,8 +536,8 @@ const InvestmentCalendar = () => { })); }; - // 检查是否有引用数据(可能在 record.reason_citation 或 record[4]) - const citationData = record.reason; + // 检查是否有引用数据(reason 就是 record[2]) + const citationData = reason; const hasCitation = citationData && citationData.data && Array.isArray(citationData.data); if (hasCitation) { @@ -694,9 +735,17 @@ const InvestmentCalendar = () => { onClose={() => setDetailDrawerVisible(false)} visible={detailDrawerVisible} > -
- {selectedDetail?.content || '暂无内容'} -
+ {selectedDetail?.content?.type === 'citation' ? ( + + ) : ( +
+ {selectedDetail?.content?.content || '暂无内容'} +
+ )} {/* 相关股票模态框 */} diff --git a/src/views/Community/components/StockDetailPanel.js b/src/views/Community/components/StockDetailPanel.js index c32ec53b..38b13512 100644 --- a/src/views/Community/components/StockDetailPanel.js +++ b/src/views/Community/components/StockDetailPanel.js @@ -16,6 +16,7 @@ import { useSubscription } from '../../../hooks/useSubscription'; import SubscriptionUpgradeModal from '../../../components/SubscriptionUpgradeModal'; import moment from 'moment'; import { logger } from '../../../utils/logger'; +import { getApiBase } from '../../../utils/apiConfig'; const { Title, Text } = Typography; const { TabPane } = Tabs; @@ -394,7 +395,7 @@ function StockDetailPanel({ visible, event, onClose }) { const loadWatchlist = useCallback(async () => { try { const isProduction = process.env.NODE_ENV === 'production'; - const apiBase = isProduction ? '' : process.env.REACT_APP_API_URL || ''; + const apiBase = getApiBase(); const response = await fetch(`${apiBase}/api/account/watchlist`, { credentials: 'include' // 确保发送cookies }); @@ -415,7 +416,7 @@ function StockDetailPanel({ visible, event, onClose }) { const handleWatchlistToggle = async (stockCode, isInWatchlist) => { try { const isProduction = process.env.NODE_ENV === 'production'; - const apiBase = isProduction ? '' : process.env.REACT_APP_API_URL || ''; + const apiBase = getApiBase(); let response; if (isInWatchlist) { diff --git a/src/views/Company/CompanyOverview.js b/src/views/Company/CompanyOverview.js index 1010607b..83e80fff 100644 --- a/src/views/Company/CompanyOverview.js +++ b/src/views/Company/CompanyOverview.js @@ -37,9 +37,10 @@ import { import ReactECharts from 'echarts-for-react'; import { logger } from '../../utils/logger'; +import { getApiBase } from '../../utils/apiConfig'; // API配置 -const API_BASE_URL = process.env.NODE_ENV === 'production' ? "" : (process.env.REACT_APP_API_URL || 'http://localhost:5001'); +const API_BASE_URL = getApiBase(); // 格式化工具 const formatUtils = { diff --git a/src/views/Company/MarketDataView.js b/src/views/Company/MarketDataView.js index 4259d383..2aea7ebb 100644 --- a/src/views/Company/MarketDataView.js +++ b/src/views/Company/MarketDataView.js @@ -1,6 +1,7 @@ // src/views/Market/MarketDataPro.jsx import React, { useState, useEffect, useMemo } from 'react'; import { logger } from '../../utils/logger'; +import { getApiBase } from '../../utils/apiConfig'; import { Box, Container, @@ -97,8 +98,7 @@ import ReactECharts from 'echarts-for-react'; import ReactMarkdown from 'react-markdown'; // API服务配置 -const isProduction = process.env.NODE_ENV === 'production'; -const API_BASE_URL = isProduction ? "" : (process.env.REACT_APP_API_URL || 'http://localhost:5001'); +const API_BASE_URL = getApiBase(); // 主题配置 const themes = { diff --git a/src/views/Company/index.js b/src/views/Company/index.js index 2f9f4cd8..4a9cd4be 100644 --- a/src/views/Company/index.js +++ b/src/views/Company/index.js @@ -27,9 +27,9 @@ import { } from '@chakra-ui/react'; import { SearchIcon, MoonIcon, SunIcon, StarIcon } from '@chakra-ui/icons'; import { FaChartLine, FaMoneyBillWave, FaChartBar, FaInfoCircle } from 'react-icons/fa'; -import HomeNavbar from '../../components/Navbars/HomeNavbar'; import { useAuth } from '../../contexts/AuthContext'; import { logger } from '../../utils/logger'; +import { getApiBase } from '../../utils/apiConfig'; import FinancialPanorama from './FinancialPanorama'; import ForecastReport from './ForecastReport'; import MarketDataView from './MarketDataView'; @@ -46,8 +46,7 @@ const CompanyIndex = () => { const bgColor = useColorModeValue('white', 'gray.800'); const tabBg = useColorModeValue('gray.50', 'gray.700'); const activeBg = useColorModeValue('blue.500', 'blue.400'); - - const getApiBase = () => (process.env.NODE_ENV === 'production' ? '' : (process.env.REACT_APP_API_URL || 'http://49.232.185.254:5001')); + const [isInWatchlist, setIsInWatchlist] = useState(false); const [isWatchlistLoading, setIsWatchlistLoading] = useState(false); @@ -153,9 +152,7 @@ const CompanyIndex = () => { }; return ( - <> - - + {/* 页面标题和股票搜索 */} @@ -308,8 +305,7 @@ const CompanyIndex = () => { - - + ); }; diff --git a/src/views/Dashboard/Center.js b/src/views/Dashboard/Center.js index ed4deef1..56bebe09 100644 --- a/src/views/Dashboard/Center.js +++ b/src/views/Dashboard/Center.js @@ -1,6 +1,7 @@ // src/views/Dashboard/Center.js import React, { useEffect, useState, useCallback } from 'react'; import { logger } from '../../utils/logger'; +import { getApiBase } from '../../utils/apiConfig'; import { Box, Flex, @@ -85,7 +86,7 @@ export default function CenterDashboard() { const loadData = useCallback(async () => { try { - const base = (process.env.NODE_ENV === 'production' ? '' : process.env.REACT_APP_API_URL || 'http://49.232.185.254:5001'); + const base = getApiBase(); const ts = Date.now(); const [w, e, c] = await Promise.all([ fetch(base + `/api/account/watchlist?_=${ts}`, { credentials: 'include', cache: 'no-store', headers: { 'Cache-Control': 'no-cache' } }), @@ -118,7 +119,7 @@ export default function CenterDashboard() { const loadRealtimeQuotes = useCallback(async () => { try { setQuotesLoading(true); - const base = (process.env.NODE_ENV === 'production' ? '' : process.env.REACT_APP_API_URL || 'http://49.232.185.254:5001'); + const base = getApiBase(); const response = await fetch(base + '/api/account/watchlist/realtime', { credentials: 'include', cache: 'no-store', diff --git a/src/views/Dashboard/components/InvestmentCalendarChakra.js b/src/views/Dashboard/components/InvestmentCalendarChakra.js index a4c0b080..1ee18f9d 100644 --- a/src/views/Dashboard/components/InvestmentCalendarChakra.js +++ b/src/views/Dashboard/components/InvestmentCalendarChakra.js @@ -55,6 +55,7 @@ import interactionPlugin from '@fullcalendar/interaction'; import moment from 'moment'; import 'moment/locale/zh-cn'; import { logger } from '../../../utils/logger'; +import { getApiBase } from '../../../utils/apiConfig'; import './InvestmentCalendar.css'; moment.locale('zh-cn'); @@ -86,7 +87,7 @@ export default function InvestmentCalendarChakra() { const loadEvents = 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 base = getApiBase(); // 直接加载用户相关的事件(投资计划 + 关注的未来事件) const userResponse = await fetch(base + '/api/account/calendar/events', { @@ -168,7 +169,7 @@ export default function InvestmentCalendarChakra() { // 添加新事件 const handleAddEvent = async () => { try { - const base = (process.env.NODE_ENV === 'production' ? '' : process.env.REACT_APP_API_URL || 'http://49.232.185.254:5001'); + const base = getApiBase(); const eventData = { ...newEvent, @@ -235,7 +236,7 @@ export default function InvestmentCalendarChakra() { return; } try { - const base = (process.env.NODE_ENV === 'production' ? '' : process.env.REACT_APP_API_URL || 'http://49.232.185.254:5001'); + const base = getApiBase(); const response = await fetch(base + `/api/account/calendar/events/${eventId}`, { method: 'DELETE', diff --git a/src/views/Dashboard/components/InvestmentCalendarChakra.js.bak b/src/views/Dashboard/components/InvestmentCalendarChakra.js.bak new file mode 100644 index 00000000..a4c0b080 --- /dev/null +++ b/src/views/Dashboard/components/InvestmentCalendarChakra.js.bak @@ -0,0 +1,493 @@ +// src/views/Dashboard/components/InvestmentCalendarChakra.js +import React, { useState, useEffect, useCallback } 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, +} from '@chakra-ui/react'; +import { + FiCalendar, + FiClock, + FiStar, + FiTrendingUp, + FiPlus, + FiEdit2, + FiTrash2, + FiSave, + FiX, +} 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 './InvestmentCalendar.css'; + +moment.locale('zh-cn'); + +export default function InvestmentCalendarChakra() { + const { isOpen, onOpen, onClose } = useDisclosure(); + const { isOpen: isAddOpen, onOpen: onAddOpen, onClose: onAddClose } = useDisclosure(); + 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 [events, setEvents] = useState([]); + const [selectedDate, setSelectedDate] = useState(null); + const [selectedDateEvents, setSelectedDateEvents] = useState([]); + const [loading, setLoading] = useState(false); + const [newEvent, setNewEvent] = useState({ + title: '', + description: '', + type: 'plan', + importance: 3, + stocks: '', + }); + + // 加载事件数据 + const loadEvents = 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 userResponse = await fetch(base + '/api/account/calendar/events', { + credentials: 'include' + }); + + if (userResponse.ok) { + const userData = await userResponse.json(); + if (userData.success) { + const allEvents = (userData.data || []).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' : '#8B5CF6', + borderColor: event.source === 'future' ? '#3182CE' : '#8B5CF6', + extendedProps: { + ...event, + isSystem: event.source === 'future', + } + })); + + setEvents(allEvents); + logger.debug('InvestmentCalendar', '日历事件加载成功', { + count: allEvents.length + }); + } + } + } catch (error) { + logger.error('InvestmentCalendar', 'loadEvents', error); + // ❌ 移除数据加载失败 toast(非关键操作) + } finally { + setLoading(false); + } + }, []); // ✅ 移除 toast 依赖 + + useEffect(() => { + loadEvents(); + }, [loadEvents]); + + // 根据重要性获取颜色 + const getEventColor = (importance) => { + if (importance >= 5) return '#E53E3E'; // 红色 + if (importance >= 4) return '#ED8936'; // 橙色 + if (importance >= 3) return '#ECC94B'; // 黄色 + if (importance >= 2) return '#48BB78'; // 绿色 + return '#3182CE'; // 蓝色 + }; + + // 处理日期点击 + const handleDateClick = (info) => { + const clickedDate = moment(info.date); + setSelectedDate(clickedDate); + + // 筛选当天的事件 + const dayEvents = events.filter(event => + moment(event.start).isSame(clickedDate, 'day') + ); + setSelectedDateEvents(dayEvents); + onOpen(); + }; + + // 处理事件点击 + const handleEventClick = (info) => { + const event = info.event; + const clickedDate = moment(event.start); + setSelectedDate(clickedDate); + setSelectedDateEvents([{ + title: event.title, + start: event.start, + extendedProps: { + ...event.extendedProps, + }, + }]); + 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('InvestmentCalendar', '添加事件成功', { + eventTitle: eventData.title, + eventDate: eventData.event_date + }); + toast({ + title: '添加成功', + description: '投资计划已添加', + status: 'success', + duration: 3000, + }); + onAddClose(); + loadEvents(); + setNewEvent({ + title: '', + description: '', + type: 'plan', + importance: 3, + stocks: '', + }); + } + } + } catch (error) { + logger.error('InvestmentCalendar', 'handleAddEvent', error, { + eventTitle: newEvent?.title + }); + toast({ + title: '添加失败', + description: '无法添加投资计划', + status: 'error', + duration: 3000, + }); + } + }; + + // 删除用户事件 + const handleDeleteEvent = async (eventId) => { + if (!eventId) { + logger.warn('InvestmentCalendar', '删除事件失败', '缺少事件 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('InvestmentCalendar', '删除事件成功', { eventId }); + toast({ + title: '删除成功', + status: 'success', + duration: 2000, + }); + loadEvents(); + } + } catch (error) { + logger.error('InvestmentCalendar', 'handleDeleteEvent', error, { eventId }); + toast({ + title: '删除失败', + status: 'error', + duration: 3000, + }); + } + }; + + return ( + + + + + + 投资日历 + + + + + + {loading ? ( +
+ +
+ ) : ( + + + + )} +
+ + {/* 查看事件详情 Modal - 条件渲染 */} + {isOpen && ( + + + + + {selectedDate && selectedDate.format('YYYY年MM月DD日')} 的事件 + + + + {selectedDateEvents.length === 0 ? ( +
+ + 当天没有事件 + + +
+ ) : ( + + {selectedDateEvents.map((event, idx) => ( + + + + + + {event.title} + + {event.extendedProps?.isSystem ? ( + 系统事件 + ) : ( + 我的计划 + )} + + + + + 重要度: {event.extendedProps?.importance || 3}/5 + + + + {!event.extendedProps?.isSystem && ( + } + size="sm" + variant="ghost" + colorScheme="red" + onClick={() => handleDeleteEvent(event.extendedProps?.id)} + /> + )} + + + {event.extendedProps?.description && ( + + {event.extendedProps.description} + + )} + + {event.extendedProps?.stocks && event.extendedProps.stocks.length > 0 && ( + + 相关股票: + {event.extendedProps.stocks.map((stock, i) => ( + + + {stock} + + ))} + + )} + + ))} + + )} +
+ + + +
+
+ )} + + {/* 添加投资计划 Modal - 条件渲染 */} + {isAddOpen && ( + + + + + 添加投资计划 + + + + + + 标题 + setNewEvent({ ...newEvent, title: e.target.value })} + placeholder="例如:关注半导体板块" + /> + + + + 描述 +