- 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>
196 lines
6.6 KiB
TypeScript
196 lines
6.6 KiB
TypeScript
/**
|
||
* useStockQuoteData - 股票行情数据获取 Hook
|
||
*
|
||
* 使用 /api/stock/{code}/quote-detail 接口获取完整行情数据
|
||
* 供 StockQuoteCard 内部使用
|
||
*/
|
||
|
||
import { useState, useEffect, useCallback } from 'react';
|
||
import { logger } from '@utils/logger';
|
||
import axios from '@utils/axiosConfig';
|
||
import type { StockQuoteCardData } from '../types';
|
||
import type { BasicInfo } from '../../CompanyOverview/types';
|
||
|
||
/**
|
||
* 将 API 响应数据转换为 StockQuoteCard 所需格式
|
||
*/
|
||
const transformQuoteData = (apiData: any, stockCode: string): StockQuoteCardData | null => {
|
||
if (!apiData) return null;
|
||
|
||
return {
|
||
// 基础信息
|
||
name: apiData.name || apiData.stock_name || '未知',
|
||
code: apiData.code || apiData.stock_code || stockCode,
|
||
indexTags: apiData.index_tags || apiData.indexTags || [],
|
||
industry: apiData.industry || apiData.sw_industry_l2 || '',
|
||
industryL1: apiData.industry_l1 || apiData.sw_industry_l1 || '',
|
||
|
||
// 价格信息
|
||
currentPrice: apiData.current_price || apiData.currentPrice || apiData.close || 0,
|
||
changePercent: apiData.change_percent || apiData.changePercent || apiData.pct_chg || 0,
|
||
todayOpen: apiData.today_open || apiData.todayOpen || apiData.open || 0,
|
||
yesterdayClose: apiData.yesterday_close || apiData.yesterdayClose || apiData.pre_close || 0,
|
||
todayHigh: apiData.today_high || apiData.todayHigh || apiData.high || 0,
|
||
todayLow: apiData.today_low || apiData.todayLow || apiData.low || 0,
|
||
|
||
// 关键指标
|
||
pe: apiData.pe || apiData.pe_ttm || 0,
|
||
marketCap: apiData.market_cap || apiData.marketCap || apiData.circ_mv || '0',
|
||
totalShares: apiData.total_shares || apiData.totalShares || undefined,
|
||
floatShares: apiData.float_shares || apiData.floatShares || undefined,
|
||
turnoverRate: apiData.turnover_rate || apiData.turnoverRate || undefined,
|
||
week52Low: apiData.week52_low || apiData.week52Low || 0,
|
||
week52High: apiData.week52_high || apiData.week52High || 0,
|
||
|
||
// 主力动态
|
||
mainNetInflow: apiData.main_net_inflow || apiData.mainNetInflow || 0,
|
||
institutionHolding: apiData.institution_holding || apiData.institutionHolding || 0,
|
||
buyRatio: apiData.buy_ratio || apiData.buyRatio || 50,
|
||
sellRatio: apiData.sell_ratio || apiData.sellRatio || 50,
|
||
|
||
// 更新时间
|
||
updateTime: apiData.update_time || apiData.updateTime || new Date().toLocaleString(),
|
||
};
|
||
};
|
||
|
||
interface UseStockQuoteDataResult {
|
||
quoteData: StockQuoteCardData | null;
|
||
basicInfo: BasicInfo | null;
|
||
isLoading: boolean;
|
||
error: string | null;
|
||
refetch: () => void;
|
||
}
|
||
|
||
/**
|
||
* 股票行情数据获取 Hook
|
||
* 合并获取行情数据和基本信息
|
||
*
|
||
* @param stockCode - 股票代码
|
||
*/
|
||
export const useStockQuoteData = (stockCode?: string): UseStockQuoteDataResult => {
|
||
const [quoteData, setQuoteData] = useState<StockQuoteCardData | null>(null);
|
||
const [basicInfo, setBasicInfo] = useState<BasicInfo | null>(null);
|
||
const [quoteLoading, setQuoteLoading] = useState(false);
|
||
const [basicLoading, setBasicLoading] = useState(false);
|
||
const [error, setError] = useState<string | null>(null);
|
||
|
||
// 用于手动刷新的 ref(并行请求)
|
||
const refetchRef = useCallback(async () => {
|
||
if (!stockCode) return;
|
||
|
||
// 标准化股票代码(去除后缀)
|
||
const baseCode = stockCode.split('.')[0];
|
||
|
||
// 并行获取行情详情和基本信息
|
||
setQuoteLoading(true);
|
||
setBasicLoading(true);
|
||
setError(null);
|
||
|
||
logger.debug('useStockQuoteData', '刷新股票数据', { stockCode, baseCode });
|
||
|
||
try {
|
||
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('刷新数据失败');
|
||
setQuoteData(null);
|
||
} finally {
|
||
setQuoteLoading(false);
|
||
setBasicLoading(false);
|
||
}
|
||
}, [stockCode]);
|
||
|
||
// stockCode 变化时重新获取数据(带取消支持)
|
||
useEffect(() => {
|
||
if (!stockCode) {
|
||
setQuoteData(null);
|
||
setBasicInfo(null);
|
||
return;
|
||
}
|
||
|
||
const controller = new AbortController();
|
||
let isCancelled = false;
|
||
|
||
// 标准化股票代码(去除后缀)
|
||
const baseCode = stockCode.split('.')[0];
|
||
|
||
const fetchData = async () => {
|
||
// 并行获取行情详情和基本信息(优化:原串行改为并行,节省 ~120ms)
|
||
setQuoteLoading(true);
|
||
setBasicLoading(true);
|
||
setError(null);
|
||
|
||
logger.debug('useStockQuoteData', '并行获取股票数据', { stockCode, baseCode });
|
||
|
||
try {
|
||
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 (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: any) {
|
||
if (isCancelled || err.name === 'CanceledError') return;
|
||
logger.error('useStockQuoteData', '获取数据失败', err);
|
||
setError('获取数据失败');
|
||
setQuoteData(null);
|
||
} finally {
|
||
if (!isCancelled) {
|
||
setQuoteLoading(false);
|
||
setBasicLoading(false);
|
||
}
|
||
}
|
||
};
|
||
|
||
fetchData();
|
||
|
||
return () => {
|
||
isCancelled = true;
|
||
controller.abort();
|
||
};
|
||
}, [stockCode]);
|
||
|
||
return {
|
||
quoteData,
|
||
basicInfo,
|
||
isLoading: quoteLoading || basicLoading,
|
||
error,
|
||
refetch: refetchRef,
|
||
};
|
||
};
|
||
|
||
export default useStockQuoteData;
|