feat(StockQuoteCard): 新增内部数据获取 hooks

- useStockQuoteData: 合并行情数据和基本信息获取
- useStockCompare: 股票对比逻辑封装
- 为数据下沉优化做准备

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
zdl
2025-12-17 11:12:14 +08:00
parent 5a0d6e1569
commit 348d8a0ec3
3 changed files with 249 additions and 0 deletions

View File

@@ -0,0 +1,6 @@
/**
* StockQuoteCard Hooks 导出索引
*/
export { useStockQuoteData } from './useStockQuoteData';
export { useStockCompare } from './useStockCompare';

View File

@@ -0,0 +1,91 @@
/**
* useStockCompare - 股票对比逻辑 Hook
*
* 管理股票对比所需的数据获取和状态
*/
import { useState, useEffect, useCallback } from 'react';
import { useToast } from '@chakra-ui/react';
import { financialService } from '@services/financialService';
import { logger } from '@utils/logger';
import type { StockInfo } from '../../FinancialPanorama/types';
interface UseStockCompareResult {
currentStockInfo: StockInfo | null;
compareStockInfo: StockInfo | null;
isCompareLoading: boolean;
handleCompare: (compareCode: string) => Promise<void>;
clearCompare: () => void;
}
/**
* 股票对比 Hook
*
* @param stockCode - 当前股票代码
*/
export const useStockCompare = (stockCode?: string): UseStockCompareResult => {
const toast = useToast();
const [currentStockInfo, setCurrentStockInfo] = useState<StockInfo | null>(null);
const [compareStockInfo, setCompareStockInfo] = useState<StockInfo | null>(null);
const [isCompareLoading, setIsCompareLoading] = useState(false);
// 加载当前股票财务信息(用于对比)
useEffect(() => {
const loadCurrentStockInfo = async () => {
if (!stockCode) {
setCurrentStockInfo(null);
return;
}
try {
const res = await financialService.getStockInfo(stockCode);
setCurrentStockInfo(res.data);
} catch (error) {
logger.error('useStockCompare', 'loadCurrentStockInfo', error, { stockCode });
}
};
loadCurrentStockInfo();
// 股票代码变化时清除对比数据
setCompareStockInfo(null);
}, [stockCode]);
// 处理股票对比
const handleCompare = useCallback(async (compareCode: string) => {
if (!compareCode) return;
logger.debug('useStockCompare', '开始加载对比数据', { stockCode, compareCode });
setIsCompareLoading(true);
try {
const res = await financialService.getStockInfo(compareCode);
setCompareStockInfo(res.data);
logger.info('useStockCompare', '对比数据加载成功', { stockCode, compareCode });
} catch (error) {
logger.error('useStockCompare', 'handleCompare', error, { stockCode, compareCode });
toast({
title: '加载对比数据失败',
description: '请检查股票代码是否正确',
status: 'error',
duration: 3000,
});
} finally {
setIsCompareLoading(false);
}
}, [stockCode, toast]);
// 清除对比数据
const clearCompare = useCallback(() => {
setCompareStockInfo(null);
}, []);
return {
currentStockInfo,
compareStockInfo,
isCompareLoading,
handleCompare,
clearCompare,
};
};
export default useStockCompare;

View File

@@ -0,0 +1,152 @@
/**
* useStockQuoteData - 股票行情数据获取 Hook
*
* 合并获取行情数据和基本信息,供 StockQuoteCard 内部使用
*/
import { useState, useEffect, useCallback } from 'react';
import { stockService } from '@services/eventService';
import { logger } from '@utils/logger';
import { getApiBase } from '@utils/apiConfig';
import type { StockQuoteCardData } from '../types';
import type { BasicInfo } from '../../CompanyOverview/types';
const API_BASE_URL = getApiBase();
/**
* 将 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,
eps: apiData.eps || apiData.basic_eps || undefined,
pb: apiData.pb || apiData.pb_mrq || 0,
marketCap: apiData.market_cap || apiData.marketCap || apiData.circ_mv || '0',
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);
// 获取行情数据
const fetchQuote = useCallback(async () => {
if (!stockCode) {
setQuoteData(null);
return;
}
setQuoteLoading(true);
setError(null);
try {
logger.debug('useStockQuoteData', '获取股票行情', { stockCode });
const quotes = await stockService.getQuotes([stockCode]);
// API 返回格式: { [stockCode]: quoteData }
const quoteResult = quotes?.[stockCode] || quotes;
const transformedData = transformQuoteData(quoteResult, stockCode);
logger.debug('useStockQuoteData', '行情数据转换完成', { stockCode, hasData: !!transformedData });
setQuoteData(transformedData);
} catch (err) {
logger.error('useStockQuoteData', '获取行情失败', err);
setError('获取行情数据失败');
setQuoteData(null);
} finally {
setQuoteLoading(false);
}
}, [stockCode]);
// 获取基本信息
const fetchBasicInfo = useCallback(async () => {
if (!stockCode) {
setBasicInfo(null);
return;
}
setBasicLoading(true);
try {
const response = await fetch(`${API_BASE_URL}/api/stock/${stockCode}/basic-info`);
const result = await response.json();
if (result.success) {
setBasicInfo(result.data);
}
} catch (err) {
logger.error('useStockQuoteData', '获取基本信息失败', err);
// 基本信息获取失败不影响主流程,只记录日志
} finally {
setBasicLoading(false);
}
}, [stockCode]);
// stockCode 变化时重新获取数据
useEffect(() => {
fetchQuote();
fetchBasicInfo();
}, [fetchQuote, fetchBasicInfo]);
// 手动刷新
const refetch = useCallback(() => {
fetchQuote();
fetchBasicInfo();
}, [fetchQuote, fetchBasicInfo]);
return {
quoteData,
basicInfo,
isLoading: quoteLoading || basicLoading,
error,
refetch,
};
};
export default useStockQuoteData;