perf(MarketDataView): 优化加载状态,使用骨架屏避免布局跳动

- useMarketData: 新增 hasLoaded 状态,优化首次加载 loading 逻辑
- 导出 SummaryCardSkeleton 组件用于概览卡片占位
- MarketDataView: 使用骨架屏替代空白占位
- DeepAnalysisTab: 使用 skeleton 变体替代 spinner

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

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
zdl
2025-12-22 12:15:39 +08:00
parent 88b836e75a
commit 9e271747da
5 changed files with 26 additions and 20 deletions

View File

@@ -77,7 +77,7 @@ const DeepAnalysisTab: React.FC<DeepAnalysisTabProps> = ({
themePreset="blackGold" themePreset="blackGold"
size="sm" size="sm"
/> />
<LoadingState message="加载数据中..." height="200px" /> <LoadingState variant="skeleton" height="300px" skeletonRows={6} />
</CardBody> </CardBody>
</Card> </Card>
); );

View File

@@ -136,5 +136,5 @@ const MarketDataSkeleton: React.FC = memo(() => (
MarketDataSkeleton.displayName = 'MarketDataSkeleton'; MarketDataSkeleton.displayName = 'MarketDataSkeleton';
export { MarketDataSkeleton }; export { MarketDataSkeleton, SummaryCardSkeleton };
export default MarketDataSkeleton; export default MarketDataSkeleton;

View File

@@ -5,4 +5,4 @@ export { default as ThemedCard } from './ThemedCard';
export { default as MarkdownRenderer } from './MarkdownRenderer'; export { default as MarkdownRenderer } from './MarkdownRenderer';
export { default as StockSummaryCard } from './StockSummaryCard'; export { default as StockSummaryCard } from './StockSummaryCard';
export { default as AnalysisModal, AnalysisContent } from './AnalysisModal'; export { default as AnalysisModal, AnalysisContent } from './AnalysisModal';
export { MarketDataSkeleton } from './MarketDataSkeleton'; export { MarketDataSkeleton, SummaryCardSkeleton } from './MarketDataSkeleton';

View File

@@ -33,8 +33,9 @@ export const useMarketData = (
period: number = DEFAULT_PERIOD period: number = DEFAULT_PERIOD
): UseMarketDataReturn => { ): UseMarketDataReturn => {
// 主数据状态 // 主数据状态
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(true);
const [tradeLoading, setTradeLoading] = useState(false); const [tradeLoading, setTradeLoading] = useState(false);
const [hasLoaded, setHasLoaded] = useState(false);
const [summary, setSummary] = useState<MarketSummary | null>(null); const [summary, setSummary] = useState<MarketSummary | null>(null);
const [tradeData, setTradeData] = useState<TradeDayData[]>([]); const [tradeData, setTradeData] = useState<TradeDayData[]>([]);
const [fundingData, setFundingData] = useState<FundingDayData[]>([]); const [fundingData, setFundingData] = useState<FundingDayData[]>([]);
@@ -153,15 +154,17 @@ export const useMarketData = (
if (loadedTradeData.length > 0) { if (loadedTradeData.length > 0) {
loadRiseAnalysis(loadedTradeData); loadRiseAnalysis(loadedTradeData);
} }
} catch (error) {
// 取消请求不作为错误处理
if (isCancelError(error)) return;
logger.error('useMarketData', 'loadCoreData', error, { stockCode, period });
} finally {
// 只有当前请求没有被取消时才设置 loading 状态
if (!controller.signal.aborted) {
setLoading(false); setLoading(false);
setHasLoaded(true);
} catch (error) {
// 请求被取消时,不更新任何状态
if (isCancelError(error)) {
return;
} }
logger.error('useMarketData', 'loadCoreData', error, { stockCode, period });
setLoading(false);
setHasLoaded(true);
} }
}, [stockCode, period, loadRiseAnalysis]); }, [stockCode, period, loadRiseAnalysis]);
@@ -363,8 +366,11 @@ export const useMarketData = (
}; };
}, []); }, []);
// 派生 loading 状态stockCode 存在但尚未完成首次加载时,视为 loading
const isLoading = loading || (!!stockCode && !hasLoaded);
return { return {
loading, loading: isLoading,
tradeLoading, tradeLoading,
summary, summary,
tradeData, tradeData,

View File

@@ -22,6 +22,7 @@ import { useMarketData } from './hooks/useMarketData';
import { import {
ThemedCard, ThemedCard,
StockSummaryCard, StockSummaryCard,
SummaryCardSkeleton,
AnalysisModal, AnalysisModal,
AnalysisContent, AnalysisContent,
} from './components'; } from './components';
@@ -89,13 +90,12 @@ const MarketDataView: React.FC<MarketDataViewProps> = ({ stockCode: propStockCod
} }
}, [propStockCode, stockCode]); }, [propStockCode, stockCode]);
// 首次渲染时加载默认 Tab融资融券的数据 // 首次挂载时加载默认 Tab融资融券的数据
// 注意SubTabContainer 的 onChange 只在切换时触发,首次渲染不会触发
useEffect(() => { useEffect(() => {
// 默认 Tab 是融资融券index 0
if (activeTab === 0) {
loadDataByType('funding'); loadDataByType('funding');
} // eslint-disable-next-line react-hooks/exhaustive-deps
}, [loadDataByType, activeTab]); }, []); // 只在首次挂载时执行
// 处理图表点击事件 // 处理图表点击事件
const handleChartClick = useCallback( const handleChartClick = useCallback(
@@ -137,8 +137,8 @@ const MarketDataView: React.FC<MarketDataViewProps> = ({ stockCode: propStockCod
<Box bg={'#1A202C'} minH="100vh" color={theme.textPrimary}> <Box bg={'#1A202C'} minH="100vh" color={theme.textPrimary}>
<Container maxW="container.xl" py={4}> <Container maxW="container.xl" py={4}>
<VStack align="stretch" spacing={4}> <VStack align="stretch" spacing={4}>
{/* 股票概览 */} {/* 股票概览 - 未加载时显示骨架屏占位,避免布局跳动 */}
{summary && <StockSummaryCard summary={summary} theme={theme} />} {summary ? <StockSummaryCard summary={summary} theme={theme} /> : <SummaryCardSkeleton />}
{/* 交易数据 - 日K/分钟K线独立显示在 Tab 上方) */} {/* 交易数据 - 日K/分钟K线独立显示在 Tab 上方) */}
<TradeDataPanel <TradeDataPanel