/** * ============================================================================ * 公司详情页面 (Company Detail Page) * ============================================================================ * * 📍 路由: /company?scode=000001 * * 📋 功能概述: * - 展示个股详情信息,包括股票行情、公司资料、财务数据等 * - 支持通过 URL 参数 `scode` 指定股票代码 * - 提供自选股添加/移除功能 * - 多 Tab 切换展示不同维度的公司信息 * * 🎨 设计风格: * - FUI (Futuristic User Interface) 科幻风格 * - Ash Thorp 风格 - 电影级 UI 设计美学 * - James Turrell 光影效果 - 环境光渲染 * - Glassmorphism 毛玻璃卡片效果 * - Linear.app 风格微交互动画 * * 🏗️ 组件架构: * CompanyIndex (本文件) * ├── AmbientGlow - 全局环境光效果背景 * ├── CompanyHeader - 顶部区域 (页面标题 + 搜索栏) * ├── StockQuoteCard - 股票实时行情卡片 (价格、涨跌幅等) * └── SubTabContainer - Tab 切换容器 * ├── 概览 Tab * ├── 财务 Tab * ├── 公告 Tab * └── ... 其他 Tab (由 TAB_CONFIG 配置) * * 📊 数据流: * 1. URL 参数 scode → stockCode 状态 * 2. stockCode → useCompanyData Hook → 获取股票信息、自选股状态 * 3. stockCode → useCompanyEvents Hook → 用户行为追踪 * 4. 数据传递给子组件进行渲染 * * 🔧 性能优化: * - 使用 memo() 包装组件,避免父组件更新时不必要的重渲染 * - 使用 useCallback 缓存事件处理函数 * - 使用 useMemo 缓存传递给子组件的 props 对象 * - Tab 内容使用 isLazy 延迟加载,减少首屏渲染负担 * - 使用 useRef 追踪前一个股票代码,避免重复触发事件 */ import React, { memo, useCallback, useRef, useEffect, useMemo } from 'react'; // ============================================ // 样式导入 // ============================================ // FUI 动画样式 - 包含扫描线、发光效果等科幻动画 import './theme/fui-animations.css'; // ============================================ // 第三方库导入 // ============================================ // React Router - 用于读取和修改 URL 查询参数 import { useSearchParams } from 'react-router-dom'; // Chakra UI - 基础布局组件 import { Box } from '@chakra-ui/react'; // ============================================ // 内部组件和工具导入 // ============================================ // 通用 Tab 切换容器组件 - 支持懒加载和主题配置 import SubTabContainer from '@components/SubTabContainer'; // FUI 风格组件 - 科幻容器和环境光效果 import { FuiContainer, AmbientGlow } from '@components/FUI'; // 动态网页标题 Hook - 根据股票名称更新浏览器标签页标题 import { useStockDocumentTitle } from '@hooks/useDocumentTitle'; // ============================================ // 页面级 Hooks // ============================================ // 用户行为事件追踪 Hook - 发送分析数据到 PostHog import { useCompanyEvents } from './hooks/useCompanyEvents'; // 公司数据获取 Hook - 封装股票信息和自选股相关 API import { useCompanyData } from './hooks/useCompanyData'; // ============================================ // 页面子组件 // ============================================ // 顶部 Header - 包含页面标题和搜索框 import CompanyHeader from './components/CompanyHeader'; // 股票行情卡片 - 显示实时价格、涨跌幅、成交量等 import StockQuoteCard from './components/StockQuoteCard'; // ============================================ // 配置常量 // ============================================ // THEME - 页面主题配置 (背景色、文字色等) // TAB_CONFIG - Tab 页签配置数组 (名称、图标、对应组件) import { THEME, TAB_CONFIG } from './config'; // ============================================ // 主页面组件 // ============================================ /** * CompanyIndex - 公司详情页主组件 * * 这是一个容器组件 (Container Component),主要负责: * 1. 状态管理 - 管理 URL 参数、数据加载状态 * 2. 数据获取 - 通过自定义 Hook 获取股票数据 * 3. 事件处理 - 处理用户交互(搜索、Tab 切换、自选操作) * 4. 布局编排 - 组合子组件构成完整页面 * * 具体的 UI 渲染和业务逻辑委托给各个子组件处理 */ const CompanyIndex: React.FC = () => { // ========================================== // URL 参数管理 // ========================================== /** * useSearchParams - React Router v6 的 Hook * 用于读取和修改 URL 中的查询参数 (query string) * * 示例 URL: /company?scode=600519 * searchParams.get('scode') 返回 '600519' */ const [searchParams, setSearchParams] = useSearchParams(); /** * 当前股票代码 * - 从 URL 参数 `scode` 读取 * - 默认值 '000001' (平安银行) 作为兜底 */ const stockCode = searchParams.get('scode') || '000001'; /** * 前一个股票代码的引用 * - 用于检测股票代码是否发生变化 * - 避免在股票未变化时重复触发追踪事件 * - 使用 useRef 而非 useState,因为不需要触发重渲染 */ const prevStockCodeRef = useRef(stockCode); // ========================================== // 数据加载 Hook // ========================================== /** * useCompanyData - 自定义 Hook,封装公司数据获取逻辑 * * 返回值说明: * @property {Object} stockInfo - 股票基础信息对象 * - stock_name: 股票名称 (如 "贵州茅台") * - stock_code: 股票代码 (如 "600519") * - industry: 所属行业 * - ... 其他字段 * * @property {boolean} stockInfoLoading - 股票信息加载中状态 * - true: 正在请求数据,显示骨架屏/loading * - false: 数据加载完成 * * @property {boolean} isInWatchlist - 是否已添加到自选股 * - true: 已在自选列表中,显示"已自选"状态 * - false: 未添加,显示"添加自选"按钮 * * @property {boolean} watchlistLoading - 自选股操作加载中 * - 用于禁用按钮,防止重复点击 * * @property {Function} toggleWatchlist - 切换自选股状态 * - 异步函数,调用后台 API 添加/移除自选 */ const { stockInfo, isInWatchlist, watchlistLoading, toggleWatchlist, } = useCompanyData({ stockCode }); // ========================================== // 事件追踪 Hook // ========================================== /** * useCompanyEvents - 用户行为追踪 Hook * * 用于记录用户在页面上的关键操作,发送到分析平台 (PostHog) * 这些数据用于产品分析、用户行为研究、功能优化等 * * 追踪的事件类型: * - trackStockSearched: 用户搜索/切换股票 * - trackTabChanged: 用户切换 Tab 页签 * - trackWatchlistAdded: 用户添加自选股 * - trackWatchlistRemoved: 用户移除自选股 */ const companyEvents = useCompanyEvents({ stockCode }) as { trackStockSearched: (newCode: string, oldCode: string | null) => void; trackTabChanged: (index: number, name: string, prevIndex: number) => void; trackWatchlistAdded: (code: string) => void; trackWatchlistRemoved: (code: string) => void; }; // 解构追踪函数,方便使用 const { trackStockSearched, trackTabChanged, trackWatchlistAdded, trackWatchlistRemoved } = companyEvents; // ========================================== // 副作用 Effects // ========================================== /** * 设置网页标题 * * 根据当前股票代码和名称动态更新浏览器标签页标题 * 示例: "600519 贵州茅台 - 公司详情" * * 这提升了用户体验,特别是当用户打开多个标签页时 * 可以通过标题快速识别每个页面展示的股票 */ useStockDocumentTitle(stockCode, stockInfo?.stock_name); /** * 股票代码变化追踪 * * 当股票代码发生变化时(通过 URL 参数改变), * 触发追踪事件记录用户的浏览行为 * * 注意: * - 只在代码真正变化时触发,避免初始化时的重复追踪 * - 使用 useRef 存储前值,而非 usePrevious Hook,减少依赖 */ useEffect(() => { // 只有当股票代码真正发生变化时才触发追踪 if (stockCode !== prevStockCodeRef.current) { // 记录用户从哪只股票切换到哪只股票 trackStockSearched(stockCode, prevStockCodeRef.current); // 更新引用值 prevStockCodeRef.current = stockCode; } }, [stockCode, trackStockSearched]); // ========================================== // 事件处理函数 // ========================================== /** * 处理股票切换 * * 当用户通过搜索框选择新股票时调用 * 1. 验证新代码有效且与当前不同 * 2. 触发追踪事件 * 3. 更新 URL 参数(触发组件重新渲染,加载新数据) * * @param {string} newCode - 用户选择的新股票代码 * * 使用 useCallback 缓存函数引用,避免子组件不必要的重渲染 */ const handleStockChange = useCallback((newCode: string) => { // 验证: 新代码存在 且 与当前代码不同 if (newCode && newCode !== stockCode) { // 追踪: 记录股票切换行为 trackStockSearched(newCode, stockCode); // 更新 URL: 这会触发组件重新渲染,进而重新获取数据 setSearchParams({ scode: newCode }); } }, [stockCode, setSearchParams, trackStockSearched]); /** * 处理自选股切换(带追踪) * * 当用户点击"添加/移除自选"按钮时调用 * 1. 记录操作前的状态(用于判断是添加还是移除) * 2. 调用 API 执行实际操作 * 3. 根据操作类型触发对应的追踪事件 * * 注意: 使用 async/await 确保 API 调用完成后再触发追踪 */ const handleWatchlistToggle = useCallback(async () => { // 记录操作前的状态 const wasInWatchlist = isInWatchlist; // 执行 API 调用(添加或移除自选股) await toggleWatchlist(); // 追踪事件(根据操作前的状态判断是添加还是移除) if (wasInWatchlist) { // 之前在自选中 → 现在移除了 trackWatchlistRemoved(stockCode); } else { // 之前不在自选中 → 现在添加了 trackWatchlistAdded(stockCode); } }, [stockCode, isInWatchlist, toggleWatchlist, trackWatchlistAdded, trackWatchlistRemoved]); /** * 处理 Tab 切换 * * 当用户点击不同的 Tab 页签时调用 * 记录用户查看了哪个 Tab,用于分析用户最关注的信息类型 * * @param {number} index - Tab 的索引位置 (0, 1, 2, ...) * @param {string} tabKey - Tab 的唯一标识符 */ const handleTabChange = useCallback((index: number, tabKey: string) => { // 从配置中获取 Tab 的显示名称,如果没找到则使用 tabKey const tabName = TAB_CONFIG[index]?.name || tabKey; // 触发追踪事件 trackTabChanged(index, tabName, index); }, [trackTabChanged]); // ========================================== // 性能优化: 缓存 Props // ========================================== /** * 缓存传递给 SubTabContainer 的 componentProps * * 为什么需要 useMemo? * - 每次组件渲染时,`{ stockCode }` 会创建一个新对象 * - 即使 stockCode 值没变,新对象的引用也不同 * - 这会导致 SubTabContainer 认为 props 变了,触发不必要的重渲染 * * 使用 useMemo 后: * - 只有当 stockCode 真正变化时,才创建新对象 * - 保持对象引用稳定,避免子组件重渲染 */ const memoizedComponentProps = useMemo(() => ({ stockCode }), [stockCode]); // ========================================== // 渲染 UI // ========================================== return ( /** * 最外层容器 * - position="relative": 为内部绝对定位元素提供定位上下文 * - bg={THEME.bg}: 使用主题配置的背景色 * - minH: 最小高度 = 视口高度 - 顶部导航栏高度 (60px) * - overflow="hidden": 隐藏溢出内容,配合光效动画使用 */ {/* ======================================== 全局环境光效果 ======================================== AmbientGlow 组件创建 James Turrell 风格的光影效果 - 在页面背景上渲染柔和的渐变光晕 - 增强科幻 UI 的氛围感 - variant="default" 使用默认的光效配置 */} {/* ======================================== 顶部搜索栏区域 ======================================== zIndex={1} 确保 Header 在环境光效果之上显示 */} {/* CompanyHeader 组件 负责展示: - 左侧:页面标题和副标题 - 右侧:股票搜索框 (支持代码/名称搜索) Props 说明: - stockCode: 当前股票代码,用于搜索框默认值 - onStockChange: 股票切换回调 */} {/* ======================================== 主内容区 ======================================== 包含股票行情卡片和 Tab 内容区 */} {/* 内容容器 - maxW="container.xl": 最大宽度限制,保持内容可读性 - mx="auto": 水平居中 - px={4}: 左右内边距 16px - py={6}: 上下内边距 24px */} {/* ======================================== 股票行情卡片 ======================================== 放在 Tab 切换器上方,始终可见 显示实时股价、涨跌幅、成交量、换手率等核心行情数据 这个卡片独立于 Tab 系统,因为行情数据是用户 无论查看哪个 Tab 都需要看到的核心信息 mb={6}: 底部外边距 24px,与下方 Tab 区域保持间距 */} {/* ======================================== Tab 内容区 ======================================== FuiContainer 提供 FUI 风格的容器样式: - 毛玻璃背景效果 - 边框发光效果 - 科幻风格圆角 SubTabContainer 是通用的 Tab 切换组件: - tabs: Tab 配置数组,定义每个 Tab 的名称、图标、组件 - componentProps: 传递给每个 Tab 组件的共享 props - onTabChange: Tab 切换时的回调函数 - themePreset: 主题预设 ("blackGold" = 黑金配色) - contentPadding: Tab 内容区内边距 (0 = 无内边距) - isLazy: 懒加载,只有激活的 Tab 才渲染内容 */} ); }; /** * 导出组件 * * 使用 React.memo 包装组件 * memo 是一个高阶组件,用于性能优化: * - 当组件的 props 没有变化时,跳过重新渲染 * - 对于这个页面级组件,可以避免父组件(如 MainLayout) * 更新时导致的不必要重渲染 * * 注意: memo 只做浅比较,对于复杂 props 需要配合 useMemo */ export default memo(CompanyIndex);