diff --git a/src/views/Company/components/CompanyHeader/index.js b/src/views/Company/components/CompanyHeader/index.js
deleted file mode 100644
index 16894da4..00000000
--- a/src/views/Company/components/CompanyHeader/index.js
+++ /dev/null
@@ -1,62 +0,0 @@
-// src/views/Company/components/CompanyHeader/index.js
-// 公司详情页面头部区域组件
-
-import React from 'react';
-import {
- Card,
- CardBody,
- HStack,
- VStack,
- Heading,
- Text,
-} from '@chakra-ui/react';
-
-import SearchBar from './SearchBar';
-
-/**
- * 公司详情页面头部区域组件
- *
- * 包含:
- * - 页面标题和描述(金色主题)
- * - 股票搜索栏(支持模糊搜索)
- *
- * @param {Object} props
- * @param {string} props.inputCode - 搜索输入框值
- * @param {Function} props.onInputChange - 输入变化回调
- * @param {Function} props.onSearch - 搜索回调
- * @param {Function} props.onKeyDown - 键盘事件回调
- * @param {string} props.bgColor - 背景颜色
- */
-const CompanyHeader = ({
- inputCode,
- onInputChange,
- onSearch,
- onKeyDown,
- bgColor,
-}) => {
- return (
-
-
-
- {/* 标题区域 - 金色主题 */}
-
- 个股详情
-
- 查看股票实时行情、财务数据和盈利预测
-
-
-
- {/* 搜索栏 */}
-
-
-
-
- );
-};
-
-export default CompanyHeader;
diff --git a/src/views/Company/components/CompanyHeader/index.tsx b/src/views/Company/components/CompanyHeader/index.tsx
new file mode 100644
index 00000000..fda56186
--- /dev/null
+++ b/src/views/Company/components/CompanyHeader/index.tsx
@@ -0,0 +1,288 @@
+/**
+ * Company 页面顶部搜索栏组件
+ * - 显示股票代码、名称、价格、涨跌幅
+ * - 股票搜索功能
+ * - 自选股操作
+ */
+
+import React, { memo, useMemo, useCallback, useState } from 'react';
+import {
+ Box,
+ Flex,
+ HStack,
+ VStack,
+ Text,
+ Button,
+ Icon,
+ Badge,
+ Skeleton,
+} from '@chakra-ui/react';
+import { AutoComplete, Spin } from 'antd';
+import { Search, Star } from 'lucide-react';
+import { useStockSearch } from '@hooks/useStockSearch';
+import { THEME, getSearchBoxStyles } from '../../config';
+import type { CompanyHeaderProps, StockSearchResult } from '../../types';
+
+/**
+ * 股票信息展示组件
+ */
+const StockInfoDisplay = memo<{
+ stockCode: string;
+ stockName?: string;
+ price?: number | null;
+ change?: number | null;
+ loading: boolean;
+}>(({ stockCode, stockName, price, change, loading }) => {
+ if (loading) {
+ return (
+
+
+
+
+ );
+ }
+
+ return (
+
+
+
+ {stockCode}
+
+ {stockName && (
+
+ {stockName}
+
+ )}
+
+ {price !== null && price !== undefined && (
+
+
+ ¥{price.toFixed(2)}
+
+ {change !== null && change !== undefined && (
+ = 0 ? THEME.positiveBg : THEME.negativeBg}
+ color={change >= 0 ? THEME.positive : THEME.negative}
+ fontSize="sm"
+ fontWeight="bold"
+ >
+ {change >= 0 ? '+' : ''}{change.toFixed(2)}%
+
+ )}
+
+ )}
+
+ );
+});
+
+StockInfoDisplay.displayName = 'StockInfoDisplay';
+
+/**
+ * 搜索操作区组件
+ */
+const SearchActions = memo<{
+ inputCode: string;
+ onInputChange: (value: string) => void;
+ onSearch: () => void;
+ onSelect: (value: string) => void;
+ isInWatchlist: boolean;
+ watchlistLoading: boolean;
+ onWatchlistToggle: () => void;
+}>(({
+ inputCode,
+ onInputChange,
+ onSearch,
+ onSelect,
+ isInWatchlist,
+ watchlistLoading,
+ onWatchlistToggle,
+}) => {
+ // 股票搜索 Hook
+ const {
+ searchResults,
+ isSearching,
+ handleSearch: doSearch,
+ clearSearch,
+ } = useStockSearch({
+ limit: 10,
+ debounceMs: 300,
+ });
+
+ // 转换为 AutoComplete options
+ const stockOptions = useMemo(() => {
+ return searchResults.map((stock: StockSearchResult) => ({
+ value: stock.stock_code,
+ label: (
+
+
+ {stock.stock_code}
+ {stock.stock_name}
+
+ {stock.pinyin_abbr && (
+
+ {stock.pinyin_abbr.toUpperCase()}
+
+ )}
+
+ ),
+ }));
+ }, [searchResults]);
+
+ // 选中股票
+ const handleSelect = useCallback((value: string) => {
+ clearSearch();
+ onSelect(value);
+ }, [clearSearch, onSelect]);
+
+ // 键盘事件
+ const handleKeyDown = useCallback((e: React.KeyboardEvent) => {
+ if (e.key === 'Enter') {
+ onSearch();
+ }
+ }, [onSearch]);
+
+ return (
+
+ {/* 搜索框 */}
+
+ : null}
+ onKeyDown={handleKeyDown}
+ />
+
+
+ {/* 搜索按钮 */}
+ }
+ fontWeight="bold"
+ >
+ 查询
+
+
+ {/* 自选按钮 */}
+
+ }
+ fontWeight="bold"
+ >
+ {isInWatchlist ? '已自选' : '自选'}
+
+
+ );
+});
+
+SearchActions.displayName = 'SearchActions';
+
+/**
+ * Company 页面顶部组件
+ */
+const CompanyHeader: React.FC = memo(({
+ stockCode,
+ stockInfo,
+ stockInfoLoading,
+ isInWatchlist,
+ watchlistLoading,
+ onStockChange,
+ onWatchlistToggle,
+}) => {
+ const [inputCode, setInputCode] = useState(stockCode);
+
+ // 处理搜索
+ const handleSearch = useCallback(() => {
+ if (inputCode && inputCode !== stockCode) {
+ onStockChange(inputCode);
+ }
+ }, [inputCode, stockCode, onStockChange]);
+
+ // 处理选中
+ const handleSelect = useCallback((value: string) => {
+ setInputCode(value);
+ if (value !== stockCode) {
+ onStockChange(value);
+ }
+ }, [stockCode, onStockChange]);
+
+ // 同步 stockCode 变化
+ React.useEffect(() => {
+ setInputCode(stockCode);
+ }, [stockCode]);
+
+ return (
+
+
+ {/* 左侧:股票信息 */}
+
+
+ {/* 右侧:搜索和操作 */}
+
+
+
+ );
+});
+
+CompanyHeader.displayName = 'CompanyHeader';
+
+export default CompanyHeader;
diff --git a/src/views/Company/config.ts b/src/views/Company/config.ts
new file mode 100644
index 00000000..a78305d1
--- /dev/null
+++ b/src/views/Company/config.ts
@@ -0,0 +1,115 @@
+/**
+ * Company 页面配置
+ * - 黑金主题配置
+ * - Tab 配置
+ */
+
+import { lazy } from 'react';
+import { Building2, TrendingUp, Wallet, FileBarChart } from 'lucide-react';
+import type { CompanyTheme, TabConfig } from './types';
+
+// ============================================
+// 黑金主题配置
+// ============================================
+
+export const THEME: CompanyTheme = {
+ // 背景色
+ bg: '#1A1A2E',
+ cardBg: '#16213E',
+
+ // 金色系
+ gold: '#D4AF37',
+ goldLight: '#F0D78C',
+ goldDark: '#B8960C',
+
+ // 文字色
+ textPrimary: '#FFFFFF',
+ textSecondary: 'rgba(255, 255, 255, 0.7)',
+ textMuted: 'rgba(255, 255, 255, 0.5)',
+
+ // 边框色
+ border: 'rgba(212, 175, 55, 0.2)',
+ borderHover: 'rgba(212, 175, 55, 0.4)',
+
+ // 涨跌色
+ positive: '#EF4444',
+ negative: '#22C55E',
+ positiveBg: 'rgba(239, 68, 68, 0.2)',
+ negativeBg: 'rgba(34, 197, 94, 0.2)',
+};
+
+// ============================================
+// Tab 懒加载组件
+// ============================================
+
+const CompanyOverview = lazy(() => import('./components/CompanyOverview'));
+const MarketDataView = lazy(() => import('./MarketDataView'));
+const FinancialPanorama = lazy(() => import('./components/FinancialPanorama'));
+const ForecastReport = lazy(() => import('./ForecastReport'));
+
+// ============================================
+// Tab 配置
+// ============================================
+
+export const TAB_CONFIG: TabConfig[] = [
+ {
+ key: 'overview',
+ name: '公司概览',
+ icon: Building2,
+ component: CompanyOverview,
+ },
+ {
+ key: 'market',
+ name: '股票行情',
+ icon: TrendingUp,
+ component: MarketDataView,
+ },
+ {
+ key: 'financial',
+ name: '财务全景',
+ icon: Wallet,
+ component: FinancialPanorama,
+ },
+ {
+ key: 'forecast',
+ name: '盈利预测',
+ icon: FileBarChart,
+ component: ForecastReport,
+ },
+];
+
+// ============================================
+// 搜索框样式配置(Ant Design AutoComplete)
+// ============================================
+
+export const getSearchBoxStyles = (theme: CompanyTheme) => ({
+ '.ant-select-selector': {
+ backgroundColor: `${theme.bg} !important`,
+ borderColor: `${theme.border} !important`,
+ borderRadius: '8px !important',
+ height: '40px !important',
+ '&:hover': {
+ borderColor: `${theme.borderHover} !important`,
+ },
+ },
+ '.ant-select-selection-search-input': {
+ color: `${theme.textPrimary} !important`,
+ height: '38px !important',
+ },
+ '.ant-select-selection-placeholder': {
+ color: `${theme.textMuted} !important`,
+ },
+ '.ant-select-dropdown': {
+ backgroundColor: `${theme.cardBg} !important`,
+ borderColor: `${theme.border} !important`,
+ },
+ '.ant-select-item': {
+ color: `${theme.textPrimary} !important`,
+ '&:hover': {
+ backgroundColor: `${theme.bg} !important`,
+ },
+ },
+ '.ant-select-item-option-selected': {
+ backgroundColor: 'rgba(212, 175, 55, 0.2) !important',
+ },
+});
diff --git a/src/views/Company/hooks/useCompanyData.ts b/src/views/Company/hooks/useCompanyData.ts
new file mode 100644
index 00000000..c374d7b4
--- /dev/null
+++ b/src/views/Company/hooks/useCompanyData.ts
@@ -0,0 +1,173 @@
+/**
+ * Company 页面数据加载 Hook
+ * - 使用 axios 请求
+ * - 懒加载策略
+ * - 自动取消请求
+ */
+
+import { useState, useEffect, useCallback, useRef } from 'react';
+import { useToast } from '@chakra-ui/react';
+import axios from '@utils/axiosConfig';
+import { logger } from '@utils/logger';
+import { useAuth } from '@contexts/AuthContext';
+import type {
+ StockInfo,
+ WatchlistItem,
+ UseCompanyDataReturn,
+ ApiResponse,
+} from '../types';
+
+interface UseCompanyDataOptions {
+ stockCode: string;
+ /** 是否自动加载股票信息 */
+ autoLoadStockInfo?: boolean;
+ /** 是否自动加载自选股状态 */
+ autoLoadWatchlist?: boolean;
+}
+
+/**
+ * Company 页面数据管理 Hook
+ */
+export const useCompanyData = ({
+ stockCode,
+ autoLoadStockInfo = true,
+ autoLoadWatchlist = true,
+}: UseCompanyDataOptions): UseCompanyDataReturn => {
+ // 状态
+ const [stockInfo, setStockInfo] = useState(null);
+ const [stockInfoLoading, setStockInfoLoading] = useState(false);
+ const [isInWatchlist, setIsInWatchlist] = useState(false);
+ const [watchlistLoading, setWatchlistLoading] = useState(false);
+
+ // Hooks
+ const toast = useToast();
+ const { isAuthenticated } = useAuth();
+
+ // AbortController 用于取消请求
+ const abortControllerRef = useRef(null);
+
+ /**
+ * 加载股票基本信息
+ */
+ const loadStockInfo = useCallback(async () => {
+ if (!stockCode || stockCode.length !== 6) return;
+
+ // 取消之前的请求
+ abortControllerRef.current?.abort();
+ abortControllerRef.current = new AbortController();
+
+ setStockInfoLoading(true);
+
+ try {
+ const { data } = await axios.get>(
+ `/api/financial/stock-info/${stockCode}`,
+ { signal: abortControllerRef.current.signal }
+ );
+
+ if (data.success && data.data) {
+ setStockInfo(data.data);
+ }
+ } catch (error: any) {
+ if (error.name === 'CanceledError') return;
+ logger.error('useCompanyData', 'loadStockInfo', error, { stockCode });
+ } finally {
+ setStockInfoLoading(false);
+ }
+ }, [stockCode]);
+
+ /**
+ * 加载自选股状态
+ */
+ const loadWatchlistStatus = useCallback(async () => {
+ if (!isAuthenticated) {
+ setIsInWatchlist(false);
+ return;
+ }
+
+ try {
+ const { data } = await axios.get>(
+ '/api/account/watchlist'
+ );
+
+ if (data.success && Array.isArray(data.data)) {
+ const codes = new Set(data.data.map((item) => item.stock_code));
+ setIsInWatchlist(codes.has(stockCode));
+ }
+ } catch (error: any) {
+ logger.error('useCompanyData', 'loadWatchlistStatus', error);
+ setIsInWatchlist(false);
+ }
+ }, [stockCode, isAuthenticated]);
+
+ /**
+ * 切换自选股状态
+ */
+ const toggleWatchlist = useCallback(async () => {
+ if (!stockCode) {
+ toast({ title: '无效的股票代码', status: 'error', duration: 2000 });
+ return;
+ }
+
+ if (!isAuthenticated) {
+ toast({ title: '请先登录后再加入自选', status: 'warning', duration: 2000 });
+ return;
+ }
+
+ setWatchlistLoading(true);
+
+ try {
+ if (isInWatchlist) {
+ // 移除自选
+ await axios.delete(`/api/account/watchlist/${stockCode}`);
+ setIsInWatchlist(false);
+ toast({ title: '已从自选移除', status: 'info', duration: 1500 });
+ } else {
+ // 添加自选
+ await axios.post('/api/account/watchlist', { stock_code: stockCode });
+ setIsInWatchlist(true);
+ toast({ title: '已加入自选', status: 'success', duration: 1500 });
+ }
+ } catch (error: any) {
+ logger.error('useCompanyData', 'toggleWatchlist', error, { stockCode });
+ toast({ title: '操作失败,请稍后重试', status: 'error', duration: 2000 });
+ } finally {
+ setWatchlistLoading(false);
+ }
+ }, [stockCode, isAuthenticated, isInWatchlist, toast]);
+
+ /**
+ * 刷新股票信息
+ */
+ const refreshStockInfo = useCallback(async () => {
+ await loadStockInfo();
+ }, [loadStockInfo]);
+
+ // 自动加载股票信息
+ useEffect(() => {
+ if (autoLoadStockInfo) {
+ loadStockInfo();
+ }
+
+ return () => {
+ abortControllerRef.current?.abort();
+ };
+ }, [autoLoadStockInfo, loadStockInfo]);
+
+ // 自动加载自选股状态
+ useEffect(() => {
+ if (autoLoadWatchlist) {
+ loadWatchlistStatus();
+ }
+ }, [autoLoadWatchlist, loadWatchlistStatus]);
+
+ return {
+ stockInfo,
+ stockInfoLoading,
+ isInWatchlist,
+ watchlistLoading,
+ toggleWatchlist,
+ refreshStockInfo,
+ };
+};
+
+export default useCompanyData;
diff --git a/src/views/Company/index.js b/src/views/Company/index.js
deleted file mode 100644
index eb6fd571..00000000
--- a/src/views/Company/index.js
+++ /dev/null
@@ -1,411 +0,0 @@
-import React, { useState, useEffect, useCallback, useMemo, useRef } from 'react';
-import { useSearchParams } from 'react-router-dom';
-import { AutoComplete, Spin } from 'antd';
-import { useStockSearch } from '@hooks/useStockSearch';
-import {
- Box,
- Flex,
- HStack,
- VStack,
- Text,
- Button,
- Icon,
- Badge,
- useToast,
- Skeleton,
-} from '@chakra-ui/react';
-import { Search, Star, Building2, TrendingUp, Wallet, FileBarChart } from 'lucide-react';
-import { useAuth } from '../../contexts/AuthContext';
-import { logger } from '../../utils/logger';
-import { getApiBase } from '../../utils/apiConfig';
-
-// Tab 内容组件
-import FinancialPanorama from './components/FinancialPanorama';
-import ForecastReport from './ForecastReport';
-import MarketDataView from './MarketDataView';
-import CompanyOverview from './components/CompanyOverview';
-
-// PostHog 追踪
-import { useCompanyEvents } from './hooks/useCompanyEvents';
-
-// 通用组件
-import SubTabContainer from '@components/SubTabContainer';
-
-// ============================================
-// 黑金主题配置
-// ============================================
-const THEME = {
- bg: '#1A1A2E', // 深蓝黑背景
- cardBg: '#16213E', // 卡片背景
- gold: '#D4AF37', // 金色
- goldLight: '#F0D78C', // 浅金色
- goldDark: '#B8960C', // 深金色
- textPrimary: '#FFFFFF',
- textSecondary: 'rgba(255, 255, 255, 0.7)',
- textMuted: 'rgba(255, 255, 255, 0.5)',
- border: 'rgba(212, 175, 55, 0.2)',
- borderHover: 'rgba(212, 175, 55, 0.4)',
-};
-
-// ============================================
-// Tab 配置
-// ============================================
-const TAB_CONFIG = [
- { key: 'overview', name: '公司概览', icon: Building2, component: CompanyOverview },
- { key: 'market', name: '股票行情', icon: TrendingUp, component: MarketDataView },
- { key: 'financial', name: '财务全景', icon: Wallet, component: FinancialPanorama },
- { key: 'forecast', name: '盈利预测', icon: FileBarChart, component: ForecastReport },
-];
-
-/**
- * 公司详情页面
- *
- * 使用黑金主题,紧凑布局
- */
-const CompanyIndex = () => {
- const [searchParams, setSearchParams] = useSearchParams();
- const [stockCode, setStockCode] = useState(searchParams.get('scode') || '000001');
- const [inputCode, setInputCode] = useState(stockCode);
- const [stockName, setStockName] = useState('');
- const [stockPrice, setStockPrice] = useState(null);
- const [priceChange, setPriceChange] = useState(null);
- const prevStockCodeRef = useRef(stockCode);
- const toast = useToast();
- const { isAuthenticated } = useAuth();
-
- // PostHog 事件追踪
- const {
- trackStockSearched,
- trackTabChanged,
- trackWatchlistAdded,
- trackWatchlistRemoved,
- } = useCompanyEvents({ stockCode });
-
- // 股票搜索 Hook
- const {
- searchResults,
- isSearching,
- handleSearch: doSearch,
- clearSearch,
- } = useStockSearch({
- limit: 10,
- debounceMs: 300,
- onSearch: (query, _count) => trackStockSearched(query, stockCode),
- });
-
- // 转换为 AutoComplete options
- const stockOptions = useMemo(() => {
- return searchResults.map((stock) => ({
- value: stock.stock_code,
- label: (
-
-
- {stock.stock_code}
- {stock.stock_name}
-
- {stock.pinyin_abbr && (
-
- {stock.pinyin_abbr.toUpperCase()}
-
- )}
-
- ),
- }));
- }, [searchResults]);
-
- // 自选股状态
- const [isInWatchlist, setIsInWatchlist] = useState(false);
- const [isWatchlistLoading, setIsWatchlistLoading] = useState(false);
-
- // 加载自选股状态
- const loadWatchlistStatus = useCallback(async () => {
- try {
- const base = getApiBase();
- const resp = await fetch(base + '/api/account/watchlist', {
- credentials: 'include',
- cache: 'no-store'
- });
- if (!resp.ok) {
- setIsInWatchlist(false);
- return;
- }
- const data = await resp.json();
- const list = Array.isArray(data?.data) ? data.data : [];
- const codes = new Set(list.map((item) => item.stock_code));
- setIsInWatchlist(codes.has(stockCode));
- } catch (e) {
- setIsInWatchlist(false);
- }
- }, [stockCode]);
-
- // 加载股票基本信息
- const loadStockInfo = useCallback(async () => {
- try {
- const base = getApiBase();
- const resp = await fetch(base + `/api/financial/stock-info/${stockCode}`, {
- credentials: 'include',
- });
- if (resp.ok) {
- const data = await resp.json();
- if (data.success && data.data) {
- setStockName(data.data.stock_name || '');
- setStockPrice(data.data.close_price);
- setPriceChange(data.data.change_pct);
- }
- }
- } catch (e) {
- logger.error('CompanyIndex', 'loadStockInfo', e);
- }
- }, [stockCode]);
-
- // URL 参数变化时更新股票代码
- useEffect(() => {
- if (stockCode !== prevStockCodeRef.current) {
- trackStockSearched(stockCode, prevStockCodeRef.current);
- prevStockCodeRef.current = stockCode;
- }
- }, [searchParams, stockCode, trackStockSearched]);
-
- useEffect(() => {
- loadWatchlistStatus();
- loadStockInfo();
- }, [loadWatchlistStatus, loadStockInfo]);
-
- // 搜索处理
- const handleSearch = () => {
- if (inputCode && inputCode !== stockCode) {
- trackStockSearched(inputCode, stockCode);
- setStockCode(inputCode);
- setSearchParams({ scode: inputCode });
- }
- };
-
- // 选中股票
- const handleStockSelect = (value) => {
- setInputCode(value);
- clearSearch();
- if (value !== stockCode) {
- trackStockSearched(value, stockCode);
- setStockCode(value);
- setSearchParams({ scode: value });
- }
- };
-
- // 自选股切换
- const handleWatchlistToggle = async () => {
- if (!stockCode) {
- toast({ title: '无效的股票代码', status: 'error', duration: 2000 });
- return;
- }
- if (!isAuthenticated) {
- toast({ title: '请先登录后再加入自选', status: 'warning', duration: 2000 });
- return;
- }
- try {
- setIsWatchlistLoading(true);
- const base = getApiBase();
- if (isInWatchlist) {
- const resp = await fetch(base + `/api/account/watchlist/${stockCode}`, {
- method: 'DELETE',
- credentials: 'include'
- });
- if (!resp.ok) throw new Error('删除失败');
- trackWatchlistRemoved(stockCode);
- setIsInWatchlist(false);
- toast({ title: '已从自选移除', status: 'info', duration: 1500 });
- } else {
- const resp = await fetch(base + '/api/account/watchlist', {
- method: 'POST',
- headers: { 'Content-Type': 'application/json' },
- credentials: 'include',
- body: JSON.stringify({ stock_code: stockCode })
- });
- if (!resp.ok) throw new Error('添加失败');
- trackWatchlistAdded(stockCode);
- setIsInWatchlist(true);
- toast({ title: '已加入自选', status: 'success', duration: 1500 });
- }
- } catch (error) {
- logger.error('CompanyIndex', 'handleWatchlistToggle', error);
- toast({ title: '操作失败,请稍后重试', status: 'error', duration: 2000 });
- } finally {
- setIsWatchlistLoading(false);
- }
- };
-
- // Tab 变更处理
- const handleTabChange = useCallback((index, tabKey) => {
- const tabNames = TAB_CONFIG.map(t => t.name);
- trackTabChanged(index, tabNames[index], index);
- }, [trackTabChanged]);
-
- return (
-
- {/* 顶部搜索栏 */}
-
-
- {/* 左侧:股票信息 */}
-
- {/* 股票代码和名称 */}
-
-
-
- {stockCode}
-
- {stockName && (
-
- {stockName}
-
- )}
-
- {stockPrice !== null && (
-
-
- ¥{stockPrice?.toFixed(2)}
-
- {priceChange !== null && (
- = 0 ? 'rgba(239, 68, 68, 0.2)' : 'rgba(34, 197, 94, 0.2)'}
- color={priceChange >= 0 ? '#EF4444' : '#22C55E'}
- fontSize="sm"
- fontWeight="bold"
- >
- {priceChange >= 0 ? '+' : ''}{priceChange?.toFixed(2)}%
-
- )}
-
- )}
-
-
-
- {/* 右侧:搜索和操作 */}
-
- {/* 搜索框 */}
-
- setInputCode(value)}
- placeholder="输入代码、名称或拼音"
- style={{ width: 220 }}
- notFoundContent={isSearching ? : null}
- onKeyDown={(e) => {
- if (e.key === 'Enter') handleSearch();
- }}
- />
-
-
- {/* 搜索按钮 */}
- }
- fontWeight="bold"
- >
- 查询
-
-
- {/* 自选按钮 */}
- }
- fontWeight="bold"
- >
- {isInWatchlist ? '已自选' : '自选'}
-
-
-
-
-
- {/* 主内容区 - Tab 切换 */}
-
-
-
-
-
-
- );
-};
-
-export default CompanyIndex;
diff --git a/src/views/Company/index.tsx b/src/views/Company/index.tsx
new file mode 100644
index 00000000..a183ae74
--- /dev/null
+++ b/src/views/Company/index.tsx
@@ -0,0 +1,150 @@
+/**
+ * 公司详情页面
+ *
+ * 特性:
+ * - 黑金主题设计
+ * - 懒加载 Tab 内容
+ * - memo 性能优化
+ * - axios 数据请求
+ */
+
+import React, { memo, useCallback, useRef, useEffect, Suspense } from 'react';
+import { useSearchParams } from 'react-router-dom';
+import { Box, Spinner, Center } from '@chakra-ui/react';
+import SubTabContainer from '@components/SubTabContainer';
+import { useCompanyEvents } from './hooks/useCompanyEvents';
+import { useCompanyData } from './hooks/useCompanyData';
+import CompanyHeader from './components/CompanyHeader';
+import { THEME, TAB_CONFIG } from './config';
+
+// ============================================
+// 加载状态组件
+// ============================================
+
+const TabLoadingFallback = memo(() => (
+
+
+
+));
+
+TabLoadingFallback.displayName = 'TabLoadingFallback';
+
+// ============================================
+// 主内容区组件
+// ============================================
+
+interface CompanyContentProps {
+ stockCode: string;
+ onTabChange: (index: number, tabKey: string) => void;
+}
+
+const CompanyContent = memo(({ stockCode, onTabChange }) => (
+
+
+ }>
+
+
+
+
+));
+
+CompanyContent.displayName = 'CompanyContent';
+
+// ============================================
+// 主页面组件
+// ============================================
+
+const CompanyIndex: React.FC = () => {
+ // URL 参数管理
+ const [searchParams, setSearchParams] = useSearchParams();
+ const stockCode = searchParams.get('scode') || '000001';
+ const prevStockCodeRef = useRef(stockCode);
+
+ // 数据加载 Hook
+ const {
+ stockInfo,
+ stockInfoLoading,
+ isInWatchlist,
+ watchlistLoading,
+ toggleWatchlist,
+ } = useCompanyData({ stockCode });
+
+ // 事件追踪 Hook
+ const {
+ trackStockSearched,
+ trackTabChanged,
+ trackWatchlistAdded,
+ trackWatchlistRemoved,
+ } = useCompanyEvents({ stockCode });
+
+ // 股票代码变化追踪
+ useEffect(() => {
+ if (stockCode !== prevStockCodeRef.current) {
+ trackStockSearched(stockCode, prevStockCodeRef.current);
+ prevStockCodeRef.current = stockCode;
+ }
+ }, [stockCode, trackStockSearched]);
+
+ // 处理股票切换
+ const handleStockChange = useCallback((newCode: string) => {
+ if (newCode && newCode !== stockCode) {
+ trackStockSearched(newCode, stockCode);
+ setSearchParams({ scode: newCode });
+ }
+ }, [stockCode, setSearchParams, trackStockSearched]);
+
+ // 处理自选股切换(带追踪)
+ const handleWatchlistToggle = useCallback(async () => {
+ const wasInWatchlist = isInWatchlist;
+ await toggleWatchlist();
+
+ // 追踪事件(根据操作前的状态判断)
+ if (wasInWatchlist) {
+ trackWatchlistRemoved(stockCode);
+ } else {
+ trackWatchlistAdded(stockCode);
+ }
+ }, [stockCode, isInWatchlist, toggleWatchlist, trackWatchlistAdded, trackWatchlistRemoved]);
+
+ // 处理 Tab 切换
+ const handleTabChange = useCallback((index: number, tabKey: string) => {
+ const tabName = TAB_CONFIG[index]?.name || tabKey;
+ trackTabChanged(index, tabName, index);
+ }, [trackTabChanged]);
+
+ return (
+
+ {/* 顶部搜索栏 */}
+
+
+ {/* 主内容区 */}
+
+
+ );
+};
+
+export default memo(CompanyIndex);
diff --git a/src/views/Company/types.ts b/src/views/Company/types.ts
new file mode 100644
index 00000000..b575a6e8
--- /dev/null
+++ b/src/views/Company/types.ts
@@ -0,0 +1,124 @@
+/**
+ * Company 页面类型定义
+ */
+
+import type { ComponentType } from 'react';
+import type { IconType } from 'react-icons';
+import type { LucideIcon } from 'lucide-react';
+
+// ============================================
+// 主题类型
+// ============================================
+
+export interface CompanyTheme {
+ bg: string;
+ cardBg: string;
+ gold: string;
+ goldLight: string;
+ goldDark: string;
+ textPrimary: string;
+ textSecondary: string;
+ textMuted: string;
+ border: string;
+ borderHover: string;
+ positive: string;
+ negative: string;
+ positiveBg: string;
+ negativeBg: string;
+}
+
+// ============================================
+// Tab 配置类型
+// ============================================
+
+export interface TabConfig {
+ key: string;
+ name: string;
+ icon: LucideIcon | IconType | ComponentType;
+ component: ComponentType;
+}
+
+export interface TabComponentProps {
+ stockCode: string;
+}
+
+// ============================================
+// 股票数据类型
+// ============================================
+
+export interface StockInfo {
+ stock_code: string;
+ stock_name: string;
+ close_price?: number;
+ change_pct?: number;
+ market?: string;
+ industry?: string;
+}
+
+export interface StockSearchResult {
+ stock_code: string;
+ stock_name: string;
+ pinyin_abbr?: string;
+ market?: string;
+}
+
+// ============================================
+// 自选股类型
+// ============================================
+
+export interface WatchlistItem {
+ stock_code: string;
+ stock_name?: string;
+ added_at?: string;
+}
+
+export interface WatchlistResponse {
+ success: boolean;
+ data: WatchlistItem[];
+}
+
+// ============================================
+// API 响应类型
+// ============================================
+
+export interface ApiResponse {
+ success: boolean;
+ data: T;
+ message?: string;
+}
+
+// ============================================
+// Hook 返回类型
+// ============================================
+
+export interface UseCompanyDataReturn {
+ // 股票信息
+ stockInfo: StockInfo | null;
+ stockInfoLoading: boolean;
+
+ // 自选股状态
+ isInWatchlist: boolean;
+ watchlistLoading: boolean;
+ toggleWatchlist: () => Promise;
+
+ // 刷新方法
+ refreshStockInfo: () => Promise;
+}
+
+// ============================================
+// 组件 Props 类型
+// ============================================
+
+export interface CompanyHeaderProps {
+ stockCode: string;
+ stockInfo: StockInfo | null;
+ stockInfoLoading: boolean;
+ isInWatchlist: boolean;
+ watchlistLoading: boolean;
+ onStockChange: (code: string) => void;
+ onWatchlistToggle: () => void;
+}
+
+export interface CompanyPageProps {
+ // 未来可扩展
+}