Files
vf_react/src/views/Company/index.tsx
zdl 1730a59ca2 refactor(Company): 简化 CompanyHeader,添加详细代码注释
- CompanyHeader: 移除冗余的股票信息展示(已在 StockQuoteCard 中)
- index.tsx: 添加完整的 JSDoc 注释和架构说明
- types.ts: 简化 CompanyHeaderProps,移除不再需要的属性
- useStockQuoteData: 优化数据获取逻辑

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-12-19 10:58:49 +08:00

448 lines
16 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/**
* ============================================================================
* 公司详情页面 (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": 隐藏溢出内容,配合光效动画使用
*/
<Box
position="relative"
bg={THEME.bg}
minH="calc(100vh - 60px)"
overflow="hidden"
>
{/* ========================================
全局环境光效果
========================================
AmbientGlow 组件创建 James Turrell 风格的光影效果
- 在页面背景上渲染柔和的渐变光晕
- 增强科幻 UI 的氛围感
- variant="default" 使用默认的光效配置
*/}
<AmbientGlow variant="default" />
{/* ========================================
顶部搜索栏区域
========================================
zIndex={1} 确保 Header 在环境光效果之上显示
*/}
<Box position="relative" zIndex={1}>
{/*
CompanyHeader 组件
负责展示:
- 左侧:页面标题和副标题
- 右侧:股票搜索框 (支持代码/名称搜索)
Props 说明:
- stockCode: 当前股票代码,用于搜索框默认值
- onStockChange: 股票切换回调
*/}
<CompanyHeader
stockCode={stockCode}
onStockChange={handleStockChange}
/>
</Box>
{/* ========================================
主内容区
========================================
包含股票行情卡片和 Tab 内容区
*/}
<Box position="relative" zIndex={1}>
{/*
内容容器
- maxW="container.xl": 最大宽度限制,保持内容可读性
- mx="auto": 水平居中
- px={4}: 左右内边距 16px
- py={6}: 上下内边距 24px
*/}
<Box maxW="container.xl" mx="auto" px={4} py={6}>
{/* ========================================
股票行情卡片
========================================
放在 Tab 切换器上方,始终可见
显示实时股价、涨跌幅、成交量、换手率等核心行情数据
这个卡片独立于 Tab 系统,因为行情数据是用户
无论查看哪个 Tab 都需要看到的核心信息
mb={6}: 底部外边距 24px与下方 Tab 区域保持间距
*/}
<Box mb={6}>
<StockQuoteCard
stockCode={stockCode}
isInWatchlist={isInWatchlist}
isWatchlistLoading={watchlistLoading}
onWatchlistToggle={handleWatchlistToggle}
/>
</Box>
{/* ========================================
Tab 内容区
========================================
FuiContainer 提供 FUI 风格的容器样式:
- 毛玻璃背景效果
- 边框发光效果
- 科幻风格圆角
SubTabContainer 是通用的 Tab 切换组件:
- tabs: Tab 配置数组,定义每个 Tab 的名称、图标、组件
- componentProps: 传递给每个 Tab 组件的共享 props
- onTabChange: Tab 切换时的回调函数
- themePreset: 主题预设 ("blackGold" = 黑金配色)
- contentPadding: Tab 内容区内边距 (0 = 无内边距)
- isLazy: 懒加载,只有激活的 Tab 才渲染内容
*/}
<FuiContainer variant="default">
<SubTabContainer
tabs={TAB_CONFIG}
componentProps={memoizedComponentProps}
onTabChange={handleTabChange}
themePreset="blackGold"
contentPadding={0}
isLazy={true}
/>
</FuiContainer>
</Box>
</Box>
</Box>
);
};
/**
* 导出组件
*
* 使用 React.memo 包装组件
* memo 是一个高阶组件,用于性能优化:
* - 当组件的 props 没有变化时,跳过重新渲染
* - 对于这个页面级组件,可以避免父组件(如 MainLayout
* 更新时导致的不必要重渲染
*
* 注意: memo 只做浅比较,对于复杂 props 需要配合 useMemo
*/
export default memo(CompanyIndex);