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>
This commit is contained in:
zdl
2025-12-19 10:58:49 +08:00
parent 986ec05eb1
commit 1730a59ca2
5 changed files with 493 additions and 564 deletions

View File

@@ -1,100 +1,189 @@
/**
* 公司详情页面 - FUI 科幻风格
* ============================================================================
* 公司详情页面 (Company Detail Page)
* ============================================================================
*
* 特性:
* - Ash Thorp 风格 FUI 设计
* - James Turrell 光影效果
* - Glassmorphism 毛玻璃卡片
* - Linear.app 风格微交互
* - HeroUI 现代组件风格
* 📍 路由: /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 动画样式
// ============================================
// 样式导入
// ============================================
// 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';
// ============================================
// 主内容区组件 - FUI 风格
// ============================================
interface CompanyContentProps {
stockCode: string;
isInWatchlist: boolean;
watchlistLoading: boolean;
onWatchlistToggle: () => void;
onTabChange: (index: number, tabKey: string) => void;
}
const CompanyContent: React.FC<CompanyContentProps> = memo(({
stockCode,
isInWatchlist,
watchlistLoading,
onWatchlistToggle,
onTabChange,
}) => {
// 缓存 componentProps避免每次渲染创建新对象
const memoizedComponentProps = useMemo(() => ({ stockCode }), [stockCode]);
return (
<Box maxW="container.xl" mx="auto" px={4} py={6}>
{/* 股票行情卡片 - 放在 Tab 切换器上方,始终可见 */}
<Box mb={6}>
<StockQuoteCard
stockCode={stockCode}
isInWatchlist={isInWatchlist}
isWatchlistLoading={watchlistLoading}
onWatchlistToggle={onWatchlistToggle}
/>
</Box>
{/* Tab 内容区 - 使用 FuiContainer */}
<FuiContainer variant="default">
<SubTabContainer
tabs={TAB_CONFIG}
componentProps={memoizedComponentProps}
onTabChange={onTabChange}
themePreset="blackGold"
contentPadding={0}
isLazy={true}
/>
</FuiContainer>
</Box>
);
});
CompanyContent.displayName = 'CompanyContent';
// ============================================
// 主页面组件
// ============================================
/**
* 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,
stockInfoLoading,
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;
@@ -102,81 +191,257 @@ const CompanyIndex: React.FC = () => {
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 页签时调用
* 记录用户查看了哪个 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"
>
{/* 全局环境光效果 - James Turrell 风格 */}
{/* ========================================
全局环境光效果
========================================
AmbientGlow 组件创建 James Turrell 风格的光影效果
- 在页面背景上渲染柔和的渐变光晕
- 增强科幻 UI 的氛围感
- variant="default" 使用默认的光效配置
*/}
<AmbientGlow variant="default" />
{/* 顶部搜索栏 */}
{/* ========================================
顶部搜索栏区域
========================================
zIndex={1} 确保 Header 在环境光效果之上显示
*/}
<Box position="relative" zIndex={1}>
{/*
CompanyHeader 组件
负责展示:
- 左侧:页面标题和副标题
- 右侧:股票搜索框 (支持代码/名称搜索)
Props 说明:
- stockCode: 当前股票代码,用于搜索框默认值
- onStockChange: 股票切换回调
*/}
<CompanyHeader
stockCode={stockCode}
stockInfo={stockInfo}
stockInfoLoading={stockInfoLoading}
isInWatchlist={isInWatchlist}
watchlistLoading={watchlistLoading}
onStockChange={handleStockChange}
onWatchlistToggle={handleWatchlistToggle}
/>
</Box>
{/* 主内容区 */}
{/* ========================================
主内容区
========================================
包含股票行情卡片和 Tab 内容区
*/}
<Box position="relative" zIndex={1}>
<CompanyContent
stockCode={stockCode}
isInWatchlist={isInWatchlist}
watchlistLoading={watchlistLoading}
onWatchlistToggle={handleWatchlistToggle}
onTabChange={handleTabChange}
/>
{/*
内容容器
- 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);