Files
vf_react/src/views/Company/components/MarketDataView/index.tsx
zdl 9e271747da 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>
2025-12-22 13:04:55 +08:00

182 lines
5.6 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// src/views/Company/components/MarketDataView/index.tsx
// MarketDataView 主组件 - 股票市场数据综合展示
import React, { useState, useEffect, ReactNode, useMemo, useCallback, memo } from 'react';
import {
Box,
Card,
CardBody,
Container,
VStack,
useDisclosure,
} from '@chakra-ui/react';
import { Unlock, ArrowUp, Star, Lock } from 'lucide-react';
// 通用组件
import SubTabContainer from '@components/SubTabContainer';
import type { SubTabConfig } from '@components/SubTabContainer';
// 内部模块导入
import { themes, DEFAULT_PERIOD } from './constants';
import { useMarketData } from './hooks/useMarketData';
import {
ThemedCard,
StockSummaryCard,
SummaryCardSkeleton,
AnalysisModal,
AnalysisContent,
} from './components';
import {
TradeDataPanel,
FundingPanel,
BigDealPanel,
UnusualPanel,
PledgePanel,
} from './components/panels';
import type { MarketDataViewProps } from './types';
/**
* MarketDataView 主组件
* 展示股票的市场数据:交易数据、融资融券、大宗交易、龙虎榜、股权质押
*/
const MarketDataView: React.FC<MarketDataViewProps> = ({ stockCode: propStockCode }) => {
const { isOpen, onOpen, onClose } = useDisclosure();
const [modalContent, setModalContent] = useState<ReactNode>(null);
// 获取当前主题
const theme = themes.light;
// 状态管理
const [stockCode, setStockCode] = useState(propStockCode || '600000');
const [activeTab, setActiveTab] = useState(0);
const [selectedPeriod, setSelectedPeriod] = useState(DEFAULT_PERIOD);
// 使用自定义 Hook 获取数据
const {
loading,
summary,
tradeData,
fundingData,
bigDealData,
unusualData,
pledgeData,
minuteData,
minuteLoading,
analysisMap,
loadMinuteData,
loadDataByType,
} = useMarketData(stockCode, selectedPeriod);
// Tab 切换时按需加载数据
const handleTabChange = useCallback((index: number) => {
setActiveTab(index);
// 根据 tab index 加载对应数据
const tabDataMap: Record<number, 'funding' | 'bigDeal' | 'unusual' | 'pledge'> = {
0: 'funding',
1: 'bigDeal',
2: 'unusual',
3: 'pledge',
};
const dataType = tabDataMap[index];
if (dataType) {
loadDataByType(dataType);
}
}, [loadDataByType]);
// 监听 props 中的 stockCode 变化
useEffect(() => {
if (propStockCode && propStockCode !== stockCode) {
setStockCode(propStockCode);
}
}, [propStockCode, stockCode]);
// 首次挂载时加载默认 Tab融资融券的数据
// 注意SubTabContainer 的 onChange 只在切换时触发,首次渲染不会触发
useEffect(() => {
loadDataByType('funding');
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []); // 只在首次挂载时执行
// 处理图表点击事件
const handleChartClick = useCallback(
(params: { seriesName?: string; data?: [number, number] }) => {
if (params.seriesName === '涨幅分析' && params.data) {
const dataIndex = params.data[0];
const analysis = analysisMap[dataIndex];
if (analysis) {
setModalContent(<AnalysisContent analysis={analysis} theme={theme} />);
onOpen();
}
}
},
[analysisMap, theme, onOpen]
);
// Tab 配置 - 使用通用 SubTabContainer不含交易数据交易数据单独显示在上方
const tabConfigs: SubTabConfig[] = [
{ key: 'funding', name: '融资融券', icon: Unlock, component: FundingPanel },
{ key: 'bigDeal', name: '大宗交易', icon: ArrowUp, component: BigDealPanel },
{ key: 'unusual', name: '龙虎榜', icon: Star, component: UnusualPanel },
{ key: 'pledge', name: '股权质押', icon: Lock, component: PledgePanel },
];
// 传递给 Tab 组件的 props - 只传递各 Tab 需要的数据
const componentProps = useMemo(
() => ({
// 各 Tab 只使用自己需要的数据
fundingData,
bigDealData,
unusualData,
pledgeData,
}),
[fundingData, bigDealData, unusualData, pledgeData]
);
return (
<Box bg={'#1A202C'} minH="100vh" color={theme.textPrimary}>
<Container maxW="container.xl" py={4}>
<VStack align="stretch" spacing={4}>
{/* 股票概览 - 未加载时显示骨架屏占位,避免布局跳动 */}
{summary ? <StockSummaryCard summary={summary} theme={theme} /> : <SummaryCardSkeleton />}
{/* 交易数据 - 日K/分钟K线独立显示在 Tab 上方) */}
<TradeDataPanel
theme={theme}
tradeData={tradeData}
minuteData={minuteData}
minuteLoading={minuteLoading}
analysisMap={analysisMap}
onLoadMinuteData={loadMinuteData}
onChartClick={handleChartClick}
selectedPeriod={selectedPeriod}
onPeriodChange={setSelectedPeriod}
stockCode={stockCode}
loading={loading}
/>
{/* 主要内容区域 - Tab */}
<Card bg="gray.900" shadow="md" border="1px solid" borderColor="rgba(212, 175, 55, 0.3)">
<CardBody p={0}>
<SubTabContainer
tabs={tabConfigs}
componentProps={componentProps}
themePreset="blackGold"
index={activeTab}
onTabChange={handleTabChange}
isLazy
size="sm"
/>
</CardBody>
</Card>
</VStack>
</Container>
{/* 涨幅分析模态框 */}
<AnalysisModal isOpen={isOpen} onClose={onClose} content={modalContent} theme={theme} />
</Box>
);
};
export default memo(MarketDataView);