diff --git a/src/views/Company/components/CompanyHeader/SearchBar.js b/src/views/Company/components/CompanyHeader/SearchBar.js
deleted file mode 100644
index f6989923..00000000
--- a/src/views/Company/components/CompanyHeader/SearchBar.js
+++ /dev/null
@@ -1,147 +0,0 @@
-// src/views/Company/components/CompanyHeader/SearchBar.js
-// 股票搜索栏组件 - 金色主题 + 模糊搜索下拉
-
-import React, { useState, useEffect, useRef } from 'react';
-import { useSelector } from 'react-redux';
-import {
- Box,
- HStack,
- Input,
- InputGroup,
- InputLeftElement,
- Text,
- VStack,
-} from '@chakra-ui/react';
-import { SearchIcon } from '@chakra-ui/icons';
-import { useStockSearch } from '../../hooks/useStockSearch';
-
-/**
- * 股票搜索栏组件(带模糊搜索下拉)
- *
- * @param {Object} props
- * @param {string} props.inputCode - 输入框当前值
- * @param {Function} props.onInputChange - 输入变化回调
- * @param {Function} props.onSearch - 搜索按钮点击回调
- * @param {Function} props.onKeyDown - 键盘事件回调
- */
-const SearchBar = ({
- inputCode,
- onInputChange,
- onSearch,
- onKeyDown,
-}) => {
- // 下拉状态
- const [showDropdown, setShowDropdown] = useState(false);
- const containerRef = useRef(null);
-
- // 从 Redux 获取全部股票列表
- const allStocks = useSelector(state => state.stock.allStocks);
-
- // 使用共享的搜索 Hook
- const filteredStocks = useStockSearch(allStocks, inputCode, { limit: 10 });
-
- // 根据搜索结果更新下拉显示状态
- useEffect(() => {
- setShowDropdown(filteredStocks.length > 0 && !!inputCode?.trim());
- }, [filteredStocks, inputCode]);
-
- // 点击外部关闭下拉
- useEffect(() => {
- const handleClickOutside = (event) => {
- if (containerRef.current && !containerRef.current.contains(event.target)) {
- setShowDropdown(false);
- }
- };
-
- document.addEventListener('mousedown', handleClickOutside);
- return () => document.removeEventListener('mousedown', handleClickOutside);
- }, []);
-
- // 选择股票 - 直接触发搜索跳转
- const handleSelectStock = (stock) => {
- onInputChange(stock.code);
- setShowDropdown(false);
- onSearch(stock.code);
- };
-
- // 处理键盘事件
- const handleKeyDownWrapper = (e) => {
- if (e.key === 'Enter') {
- setShowDropdown(false);
- }
- onKeyDown?.(e);
- };
-
- return (
-
-
-
-
-
- onInputChange(e.target.value)}
- onKeyDown={handleKeyDownWrapper}
- onFocus={() => inputCode && filteredStocks.length > 0 && setShowDropdown(true)}
- borderRadius="md"
- color="white"
- borderColor="#C9A961"
- _placeholder={{ color: '#C9A961' }}
- _focus={{
- borderColor: '#F4D03F',
- boxShadow: '0 0 0 1px #F4D03F',
- }}
- _hover={{
- borderColor: '#F4D03F',
- }}
- />
-
-
- {/* 模糊搜索下拉列表 */}
- {showDropdown && (
-
-
- {filteredStocks.map((stock) => (
- handleSelectStock(stock)}
- borderBottom="1px solid"
- borderColor="whiteAlpha.100"
- _last={{ borderBottom: 'none' }}
- >
-
-
- {stock.code}
-
-
- {stock.name}
-
-
-
- ))}
-
-
- )}
-
- );
-};
-
-export default SearchBar;
diff --git a/src/views/Company/components/CompanyHeader/index.tsx b/src/views/Company/components/CompanyHeader/index.tsx
index 73c0db1f..05e2bc9f 100644
--- a/src/views/Company/components/CompanyHeader/index.tsx
+++ b/src/views/Company/components/CompanyHeader/index.tsx
@@ -2,10 +2,9 @@
* Company 页面顶部搜索栏组件 - FUI 科幻风格
*
* 设计特点:
- * - Glassmorphism 毛玻璃背景
- * - 发光效果和微动画
- * - Ash Thorp 风格的数据展示
- * - James Turrell 柔和光影
+ * - 左侧固定标题 + 副标题
+ * - 右侧简洁搜索框
+ * - 深色背景 + 金色强调色
*/
import React, { memo, useMemo, useCallback, useState } from 'react';
@@ -15,150 +14,52 @@ import {
HStack,
VStack,
Text,
- Button,
- Icon,
- Skeleton,
} from '@chakra-ui/react';
-import { AutoComplete, Spin } from 'antd';
-import { Search, Star, TrendingUp, TrendingDown } from 'lucide-react';
+import { AutoComplete, Input, Spin } from 'antd';
+import { SearchOutlined } from '@ant-design/icons';
import { useStockSearch } from '@hooks/useStockSearch';
-import { THEME, getSearchBoxStyles } from '../../config';
+import { THEME } from '../../config';
import { FUI_COLORS, FUI_GLOW, FUI_ANIMATION, FUI_GLASS } from '../../theme/fui';
import type { CompanyHeaderProps, StockSearchResult } from '../../types';
/**
- * 股票信息展示组件 - FUI 风格
+ * 页面标题组件
*/
-const StockInfoDisplay = memo<{
- stockCode: string;
- stockName?: string;
- price?: number | null;
- change?: number | null;
- loading: boolean;
-}>(({ stockCode, stockName, price, change, loading }) => {
- if (loading) {
- return (
-
-
-
-
- );
- }
+const PageTitle = memo(() => (
+
+
+ 个股详情
+
+
+ 查看股票实时行情、财务数据和盈利预测
+
+
+));
- const isPositive = change !== null && change !== undefined && change >= 0;
- const TrendIcon = isPositive ? TrendingUp : TrendingDown;
-
- return (
-
- {/* 股票代码 & 名称 */}
-
-
- {stockCode}
-
- {stockName && (
-
- {stockName}
-
- )}
-
-
- {/* 价格 & 涨跌幅 */}
- {price !== null && price !== undefined && (
-
- {/* 价格 */}
-
-
- Price
-
-
- ¥{price.toFixed(2)}
-
-
-
- {/* 涨跌幅 Badge */}
- {change !== null && change !== undefined && (
-
-
-
- {isPositive ? '+' : ''}{change.toFixed(2)}%
-
-
- )}
-
- )}
-
- );
-});
-
-StockInfoDisplay.displayName = 'StockInfoDisplay';
+PageTitle.displayName = 'PageTitle';
/**
- * 搜索操作区组件(状态自管理,减少父组件重渲染)
+ * 搜索框组件(状态自管理,减少父组件重渲染)
*/
-const SearchActions = memo<{
+const SearchBox = memo<{
stockCode: string;
onStockChange: (value: string) => void;
- isInWatchlist: boolean;
- watchlistLoading: boolean;
- onWatchlistToggle: () => void;
}>(({
stockCode,
onStockChange,
- isInWatchlist,
- watchlistLoading,
- onWatchlistToggle,
}) => {
- // 输入状态自管理(避免父组件重渲染)
- const [inputCode, setInputCode] = useState(stockCode);
-
- // 同步外部 stockCode 变化
- React.useEffect(() => {
- setInputCode(stockCode);
- }, [stockCode]);
+ // 输入状态 - 默认为空,显示 placeholder
+ const [inputCode, setInputCode] = useState('');
// 股票搜索 Hook
const searchHook = useStockSearch({
@@ -194,13 +95,6 @@ const SearchActions = memo<{
}));
}, [searchResults]);
- // 处理搜索按钮点击
- const handleSearch = useCallback(() => {
- if (inputCode && inputCode !== stockCode) {
- onStockChange(inputCode);
- }
- }, [inputCode, stockCode, onStockChange]);
-
// 选中股票
const handleSelect = useCallback((value: string) => {
clearSearch();
@@ -210,167 +104,101 @@ const SearchActions = memo<{
}
}, [clearSearch, stockCode, onStockChange]);
- // 键盘事件
+ // 键盘事件 - 回车搜索
const handleKeyDown = useCallback((e: React.KeyboardEvent) => {
- if (e.key === 'Enter') {
- handleSearch();
+ if (e.key === 'Enter' && inputCode && inputCode !== stockCode) {
+ onStockChange(inputCode);
}
- }, [handleSearch]);
+ }, [inputCode, stockCode, onStockChange]);
return (
-
- {/* 搜索框 - FUI 风格 */}
-
+ : null}
>
- : null}
+ }
onKeyDown={handleKeyDown}
+ style={{
+ backgroundColor: 'transparent',
+ borderColor: FUI_COLORS.gold[400],
+ borderRadius: 6,
+ height: 44,
+ color: FUI_COLORS.gold[400],
+ }}
/>
-
-
- {/* 搜索按钮 - 发光效果 */}
- }
- fontWeight="bold"
- borderRadius="10px"
- transition={`all ${FUI_ANIMATION.duration.fast} ${FUI_ANIMATION.easing.default}`}
- >
- 查询
-
-
- {/* 自选按钮 - FUI 风格 */}
-
- }
- fontWeight="bold"
- borderRadius="10px"
- transition={`all ${FUI_ANIMATION.duration.fast} ${FUI_ANIMATION.easing.default}`}
- sx={isInWatchlist ? { animation: 'glowPulse 3s ease-in-out infinite' } : undefined}
- >
- {isInWatchlist ? '已自选' : '自选'}
-
-
+
+
);
});
-SearchActions.displayName = 'SearchActions';
+SearchBox.displayName = 'SearchBox';
/**
* Company 页面顶部组件
*/
const CompanyHeader: React.FC = memo(({
stockCode,
- stockInfo,
- stockInfoLoading,
- isInWatchlist,
- watchlistLoading,
onStockChange,
- onWatchlistToggle,
}) => {
return (
- {/* 顶部发光线(环境光效果由全局 AmbientGlow 提供) */}
-
-
= memo(({
mx="auto"
justify="space-between"
align="center"
- wrap="wrap"
- gap={4}
>
- {/* 左侧:股票信息 */}
-
+ {/* 左侧:页面标题 */}
+
- {/* 右侧:搜索和操作 */}
-
diff --git a/src/views/Company/components/StockQuoteCard/hooks/useStockQuoteData.ts b/src/views/Company/components/StockQuoteCard/hooks/useStockQuoteData.ts
index dd33e001..4de99506 100644
--- a/src/views/Company/components/StockQuoteCard/hooks/useStockQuoteData.ts
+++ b/src/views/Company/components/StockQuoteCard/hooks/useStockQuoteData.ts
@@ -74,45 +74,46 @@ export const useStockQuoteData = (stockCode?: string): UseStockQuoteDataResult =
const [basicLoading, setBasicLoading] = useState(false);
const [error, setError] = useState(null);
- // 用于手动刷新的 ref
+ // 用于手动刷新的 ref(并行请求)
const refetchRef = useCallback(async () => {
if (!stockCode) return;
// 标准化股票代码(去除后缀)
const baseCode = stockCode.split('.')[0];
- // 获取行情详情数据(使用新的 quote-detail 接口)
+ // 并行获取行情详情和基本信息
setQuoteLoading(true);
+ setBasicLoading(true);
setError(null);
+
+ logger.debug('useStockQuoteData', '刷新股票数据', { stockCode, baseCode });
+
try {
- logger.debug('useStockQuoteData', '获取股票行情详情', { stockCode, baseCode });
- const { data: result } = await axios.get(`/api/stock/${baseCode}/quote-detail`);
- if (result.success && result.data) {
- const transformedData = transformQuoteData(result.data, stockCode);
+ const [quoteResult, basicResult] = await Promise.all([
+ axios.get(`/api/stock/${baseCode}/quote-detail`),
+ axios.get(`/api/stock/${baseCode}/basic-info`),
+ ]);
+
+ // 处理行情数据
+ if (quoteResult.data.success && quoteResult.data.data) {
+ const transformedData = transformQuoteData(quoteResult.data.data, stockCode);
logger.debug('useStockQuoteData', '行情数据转换完成', { stockCode, hasData: !!transformedData });
setQuoteData(transformedData);
} else {
setError('获取行情数据失败');
setQuoteData(null);
}
+
+ // 处理基本信息
+ if (basicResult.data.success) {
+ setBasicInfo(basicResult.data.data);
+ }
} catch (err) {
- logger.error('useStockQuoteData', '获取行情失败', err);
- setError('获取行情数据失败');
+ logger.error('useStockQuoteData', '刷新数据失败', err);
+ setError('刷新数据失败');
setQuoteData(null);
} finally {
setQuoteLoading(false);
- }
-
- // 获取基本信息(公司简介等)
- setBasicLoading(true);
- try {
- const { data: result } = await axios.get(`/api/stock/${baseCode}/basic-info`);
- if (result.success) {
- setBasicInfo(result.data);
- }
- } catch (err) {
- logger.error('useStockQuoteData', '获取基本信息失败', err);
- } finally {
setBasicLoading(false);
}
}, [stockCode]);
@@ -132,47 +133,45 @@ export const useStockQuoteData = (stockCode?: string): UseStockQuoteDataResult =
const baseCode = stockCode.split('.')[0];
const fetchData = async () => {
- // 获取行情详情数据(使用新的 quote-detail 接口)
+ // 并行获取行情详情和基本信息(优化:原串行改为并行,节省 ~120ms)
setQuoteLoading(true);
+ setBasicLoading(true);
setError(null);
+
+ logger.debug('useStockQuoteData', '并行获取股票数据', { stockCode, baseCode });
+
try {
- logger.debug('useStockQuoteData', '获取股票行情详情', { stockCode, baseCode });
- const { data: result } = await axios.get(`/api/stock/${baseCode}/quote-detail`, {
- signal: controller.signal,
- });
+ const [quoteResult, basicResult] = await Promise.all([
+ axios.get(`/api/stock/${baseCode}/quote-detail`, { signal: controller.signal }),
+ axios.get(`/api/stock/${baseCode}/basic-info`, { signal: controller.signal }),
+ ]);
+
if (isCancelled) return;
- if (result.success && result.data) {
- const transformedData = transformQuoteData(result.data, stockCode);
+
+ // 处理行情数据
+ if (quoteResult.data.success && quoteResult.data.data) {
+ const transformedData = transformQuoteData(quoteResult.data.data, stockCode);
logger.debug('useStockQuoteData', '行情数据转换完成', { stockCode, hasData: !!transformedData });
setQuoteData(transformedData);
} else {
setError('获取行情数据失败');
setQuoteData(null);
}
- } catch (err: any) {
- if (isCancelled || err.name === 'CanceledError') return;
- logger.error('useStockQuoteData', '获取行情失败', err);
- setError('获取行情数据失败');
- setQuoteData(null);
- } finally {
- if (!isCancelled) setQuoteLoading(false);
- }
- // 获取基本信息(公司简介等)
- setBasicLoading(true);
- try {
- const { data: result } = await axios.get(`/api/stock/${baseCode}/basic-info`, {
- signal: controller.signal,
- });
- if (isCancelled) return;
- if (result.success) {
- setBasicInfo(result.data);
+ // 处理基本信息
+ if (basicResult.data.success) {
+ setBasicInfo(basicResult.data.data);
}
} catch (err: any) {
if (isCancelled || err.name === 'CanceledError') return;
- logger.error('useStockQuoteData', '获取基本信息失败', err);
+ logger.error('useStockQuoteData', '获取数据失败', err);
+ setError('获取数据失败');
+ setQuoteData(null);
} finally {
- if (!isCancelled) setBasicLoading(false);
+ if (!isCancelled) {
+ setQuoteLoading(false);
+ setBasicLoading(false);
+ }
}
};
diff --git a/src/views/Company/index.tsx b/src/views/Company/index.tsx
index 3483414f..0430e004 100644
--- a/src/views/Company/index.tsx
+++ b/src/views/Company/index.tsx
@@ -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 = memo(({
- stockCode,
- isInWatchlist,
- watchlistLoading,
- onWatchlistToggle,
- onTabChange,
-}) => {
- // 缓存 componentProps,避免每次渲染创建新对象
- const memoizedComponentProps = useMemo(() => ({ stockCode }), [stockCode]);
-
- return (
-
- {/* 股票行情卡片 - 放在 Tab 切换器上方,始终可见 */}
-
-
-
-
- {/* Tab 内容区 - 使用 FuiContainer */}
-
-
-
-
- );
-});
-
-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": 隐藏溢出内容,配合光效动画使用
+ */
- {/* 全局环境光效果 - James Turrell 风格 */}
+ {/* ========================================
+ 全局环境光效果
+ ========================================
+ 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);
diff --git a/src/views/Company/types.ts b/src/views/Company/types.ts
index b575a6e8..b2f1787a 100644
--- a/src/views/Company/types.ts
+++ b/src/views/Company/types.ts
@@ -111,12 +111,7 @@ export interface UseCompanyDataReturn {
export interface CompanyHeaderProps {
stockCode: string;
- stockInfo: StockInfo | null;
- stockInfoLoading: boolean;
- isInWatchlist: boolean;
- watchlistLoading: boolean;
onStockChange: (code: string) => void;
- onWatchlistToggle: () => void;
}
export interface CompanyPageProps {