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} + /> + + + {/* 搜索按钮 */} + + + {/* 自选按钮 */} + + + ); +}); + +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(); - }} - /> - - - {/* 搜索按钮 */} - - - {/* 自选按钮 */} - - - - - - {/* 主内容区 - 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 { + // 未来可扩展 +}