fix: 优化加载状态和布局

MarketDataView:
- 移除重复的 LoadingState,改用 KLineModule 内部骨架屏
- 修复点击股票行情后数据不显示的问题

FinancialPanorama:
- 移除表格右上角"显示 6 期"标签
- 优化 loadingTab 状态处理

SubTabContainer:
- 重构布局:Tab 区域可滚动,右侧元素固定

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

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
zdl
2025-12-19 16:51:08 +08:00
parent 0ad0287f7b
commit 08842b9097
6 changed files with 140 additions and 128 deletions

View File

@@ -22,6 +22,7 @@
import React, { useState, useCallback, memo, Suspense } from 'react'; import React, { useState, useCallback, memo, Suspense } from 'react';
import { import {
Box, Box,
Flex,
Tabs, Tabs,
TabList, TabList,
TabPanels, TabPanels,
@@ -30,7 +31,6 @@ import {
Icon, Icon,
HStack, HStack,
Text, Text,
Spacer,
Center, Center,
Spinner, Spinner,
} from '@chakra-ui/react'; } from '@chakra-ui/react';
@@ -202,8 +202,8 @@ const SubTabContainer: React.FC<SubTabContainerProps> = memo(({
index={currentIndex} index={currentIndex}
onChange={handleTabChange} onChange={handleTabChange}
> >
{/* TabList - 玻璃态导航栏 */} {/* 导航栏容器:左侧 Tab 可滚动,右侧元素固定 */}
<TabList <Flex
bg={theme.bg} bg={theme.bg}
backdropFilter="blur(20px)" backdropFilter="blur(20px)"
borderBottom="1px solid" borderBottom="1px solid"
@@ -211,18 +211,9 @@ const SubTabContainer: React.FC<SubTabContainerProps> = memo(({
borderRadius={compact ? 0 : DEEP_SPACE.radiusLG} borderRadius={compact ? 0 : DEEP_SPACE.radiusLG}
mx={compact ? 0 : 2} mx={compact ? 0 : 2}
mb={compact ? 0 : 2} mb={compact ? 0 : 2}
px={3}
py={compact ? 2 : 3}
flexWrap="nowrap"
gap={2}
alignItems="center"
overflowX="auto"
position="relative" position="relative"
boxShadow={compact ? 'none' : DEEP_SPACE.innerGlow} boxShadow={compact ? 'none' : DEEP_SPACE.innerGlow}
css={{ alignItems="center"
'&::-webkit-scrollbar': { display: 'none' },
scrollbarWidth: 'none',
}}
> >
{/* 顶部金色光条 */} {/* 顶部金色光条 */}
<Box <Box
@@ -235,75 +226,102 @@ const SubTabContainer: React.FC<SubTabContainerProps> = memo(({
background={`linear-gradient(90deg, transparent, rgba(212, 175, 55, 0.4), transparent)`} background={`linear-gradient(90deg, transparent, rgba(212, 175, 55, 0.4), transparent)`}
/> />
{tabs.map((tab, idx) => { {/* 左侧:可滚动的 Tab 区域 */}
const isSelected = idx === currentIndex; <Box
flex="1"
minW={0}
overflowX="auto"
css={{
'&::-webkit-scrollbar': { display: 'none' },
scrollbarWidth: 'none',
}}
>
<TabList
border="none"
px={3}
py={compact ? 2 : 3}
flexWrap="nowrap"
gap={2}
>
{tabs.map((tab, idx) => {
const isSelected = idx === currentIndex;
return ( return (
<Tab <Tab
key={tab.key} key={tab.key}
color={theme.tabUnselectedColor} color={theme.tabUnselectedColor}
borderRadius={DEEP_SPACE.radius} borderRadius={DEEP_SPACE.radius}
px={6} px={6}
py={3} py={3}
fontSize="15px" fontSize="15px"
fontWeight="500" fontWeight="500"
whiteSpace="nowrap" whiteSpace="nowrap"
flexShrink={0} flexShrink={0}
border="1px solid transparent" border="1px solid transparent"
position="relative" position="relative"
letterSpacing="0.03em" letterSpacing="0.03em"
transition={DEEP_SPACE.transition} transition={DEEP_SPACE.transition}
_before={{ _before={{
content: '""', content: '""',
position: 'absolute', position: 'absolute',
bottom: '-1px', bottom: '-1px',
left: '50%', left: '50%',
transform: 'translateX(-50%)', transform: 'translateX(-50%)',
width: isSelected ? '70%' : '0%', width: isSelected ? '70%' : '0%',
height: '2px', height: '2px',
bg: '#D4AF37', bg: '#D4AF37',
borderRadius: 'full', borderRadius: 'full',
transition: 'width 0.3s ease', transition: 'width 0.3s ease',
boxShadow: isSelected ? '0 0 10px rgba(212, 175, 55, 0.5)' : 'none', boxShadow: isSelected ? '0 0 10px rgba(212, 175, 55, 0.5)' : 'none',
}} }}
_selected={{ _selected={{
bg: theme.tabSelectedBg, bg: theme.tabSelectedBg,
color: theme.tabSelectedColor, color: theme.tabSelectedColor,
fontWeight: '700', fontWeight: '700',
boxShadow: DEEP_SPACE.glowGold, boxShadow: DEEP_SPACE.glowGold,
border: `1px solid ${DEEP_SPACE.borderGoldHover}`, border: `1px solid ${DEEP_SPACE.borderGoldHover}`,
transform: 'translateY(-2px)', transform: 'translateY(-2px)',
}} }}
_hover={{ _hover={{
bg: isSelected ? undefined : theme.tabHoverBg, bg: isSelected ? undefined : theme.tabHoverBg,
border: isSelected ? undefined : `1px solid ${DEEP_SPACE.borderGold}`, border: isSelected ? undefined : `1px solid ${DEEP_SPACE.borderGold}`,
transform: 'translateY(-1px)', transform: 'translateY(-1px)',
}} }}
_active={{ _active={{
transform: 'translateY(0)', transform: 'translateY(0)',
}} }}
> >
<HStack spacing={2}> <HStack spacing={2}>
{tab.icon && ( {tab.icon && (
<Icon <Icon
as={tab.icon} as={tab.icon}
boxSize={4} boxSize={4}
opacity={isSelected ? 1 : 0.7} opacity={isSelected ? 1 : 0.7}
transition="opacity 0.2s" transition="opacity 0.2s"
/> />
)} )}
<Text>{tab.name}</Text> <Text>{tab.name}</Text>
</HStack> </HStack>
</Tab> </Tab>
); );
})} })}
</TabList>
</Box>
{/* 右侧:固定的自定义元素(如期数选择器) */}
{rightElement && ( {rightElement && (
<> <Box
<Spacer /> flexShrink={0}
<Box flexShrink={0}>{rightElement}</Box> pr={3}
</> pl={2}
py={compact ? 2 : 3}
borderLeft="1px solid"
borderColor={DEEP_SPACE.borderGold}
>
{rightElement}
</Box>
)} )}
</TabList> </Flex>
<TabPanels p={contentPadding}> <TabPanels p={contentPadding}>
{tabs.map((tab, idx) => { {tabs.map((tab, idx) => {

View File

@@ -345,12 +345,6 @@ const UnifiedFinancialTableInner: React.FC<UnifiedFinancialTableProps> = ({
return ( return (
<Box className={TABLE_CLASS_NAME}> <Box className={TABLE_CLASS_NAME}>
<style>{extendedTableStyles}</style> <style>{extendedTableStyles}</style>
{/* 右上角显示期数标签 */}
<HStack justify="flex-end" mb={1}>
<Text fontSize="2xs" color="gray.500">
{displayData.length}
</Text>
</HStack>
<ConfigProvider theme={BLACK_GOLD_TABLE_THEME}> <ConfigProvider theme={BLACK_GOLD_TABLE_THEME}>
<Table <Table
columns={columns} columns={columns}

View File

@@ -216,26 +216,26 @@ const BALANCE_SHEET_SECTIONS = [
]; ];
/** 资产负债表 Tab */ /** 资产负债表 Tab */
export const BalanceSheetTab = memo<BalanceSheetTabProps>(({ balanceSheet, loading, showMetricChart }) => ( export const BalanceSheetTab = memo<BalanceSheetTabProps>(({ balanceSheet, loading, loadingTab, showMetricChart }) => (
<UnifiedFinancialTable <UnifiedFinancialTable
type="statement" type="statement"
data={balanceSheet as unknown as FinancialDataItem[]} data={balanceSheet as unknown as FinancialDataItem[]}
sections={BALANCE_SHEET_SECTIONS} sections={BALANCE_SHEET_SECTIONS}
showMetricChart={showMetricChart} showMetricChart={showMetricChart}
loading={loading} loading={loading || loadingTab === 'balance'}
/> />
)); ));
BalanceSheetTab.displayName = 'BalanceSheetTab'; BalanceSheetTab.displayName = 'BalanceSheetTab';
/** 利润表 Tab */ /** 利润表 Tab */
export const IncomeStatementTab = memo<IncomeStatementTabProps>(({ incomeStatement, loading, showMetricChart }) => ( export const IncomeStatementTab = memo<IncomeStatementTabProps>(({ incomeStatement, loading, loadingTab, showMetricChart }) => (
<UnifiedFinancialTable <UnifiedFinancialTable
type="statement" type="statement"
data={incomeStatement as unknown as FinancialDataItem[]} data={incomeStatement as unknown as FinancialDataItem[]}
sections={INCOME_STATEMENT_SECTIONS} sections={INCOME_STATEMENT_SECTIONS}
hideTotalSectionTitle={false} hideTotalSectionTitle={false}
showMetricChart={showMetricChart} showMetricChart={showMetricChart}
loading={loading} loading={loading || loadingTab === 'income'}
/> />
)); ));
IncomeStatementTab.displayName = 'IncomeStatementTab'; IncomeStatementTab.displayName = 'IncomeStatementTab';
@@ -251,14 +251,14 @@ const CASHFLOW_SECTIONS = [{
}]; }];
/** 现金流量表 Tab */ /** 现金流量表 Tab */
export const CashflowTab = memo<CashflowTabProps>(({ cashflow, loading, showMetricChart }) => ( export const CashflowTab = memo<CashflowTabProps>(({ cashflow, loading, loadingTab, showMetricChart }) => (
<UnifiedFinancialTable <UnifiedFinancialTable
type="statement" type="statement"
data={cashflow as unknown as FinancialDataItem[]} data={cashflow as unknown as FinancialDataItem[]}
sections={CASHFLOW_SECTIONS} sections={CASHFLOW_SECTIONS}
hideTotalSectionTitle hideTotalSectionTitle
showMetricChart={showMetricChart} showMetricChart={showMetricChart}
loading={loading} loading={loading || loadingTab === 'cashflow'}
/> />
)); ));
CashflowTab.displayName = 'CashflowTab'; CashflowTab.displayName = 'CashflowTab';

View File

@@ -2,7 +2,7 @@
// K线模块 - 日K线/分时图切换展示(黑金主题 + 专业技术指标 + 商品数据叠加 + 分时盘口) // K线模块 - 日K线/分时图切换展示(黑金主题 + 专业技术指标 + 商品数据叠加 + 分时盘口)
import React, { useState, useMemo, useCallback, memo } from 'react'; import React, { useState, useMemo, useCallback, memo } from 'react';
import { Box } from '@chakra-ui/react'; import { Box, HStack, Skeleton, Card, CardBody } from '@chakra-ui/react';
import { darkGoldTheme } from '../../../constants'; import { darkGoldTheme } from '../../../constants';
import type { IndicatorType, MainIndicatorType, DrawingType } from '../../../utils/chartOptions'; import type { IndicatorType, MainIndicatorType, DrawingType } from '../../../utils/chartOptions';
@@ -30,6 +30,7 @@ const KLineModule: React.FC<KLineModuleProps> = ({
selectedPeriod, selectedPeriod,
onPeriodChange, onPeriodChange,
stockCode, stockCode,
loading = false,
}) => { }) => {
// ========== 状态管理 ========== // ========== 状态管理 ==========
const [mode, setMode] = useState<ChartMode>('daily'); const [mode, setMode] = useState<ChartMode>('daily');
@@ -127,7 +128,19 @@ const KLineModule: React.FC<KLineModuleProps> = ({
{/* 图表内容区域 */} {/* 图表内容区域 */}
<Box pt={4}> <Box pt={4}>
{mode === 'daily' ? ( {/* 加载中骨架屏 */}
{loading && tradeData.length === 0 ? (
<Card bg="gray.900" border="1px solid" borderColor="rgba(212, 175, 55, 0.3)">
<CardBody p={4}>
<Skeleton
height="600px"
borderRadius="md"
startColor="rgba(26, 32, 44, 0.6)"
endColor="rgba(212, 175, 55, 0.2)"
/>
</CardBody>
</Card>
) : mode === 'daily' ? (
// 日K线图 // 日K线图
<DailyKLineChart <DailyKLineChart
tradeData={tradeData} tradeData={tradeData}

View File

@@ -30,7 +30,6 @@ import {
UnusualPanel, UnusualPanel,
PledgePanel, PledgePanel,
} from './components/panels'; } from './components/panels';
import LoadingState from '../LoadingState';
import type { MarketDataViewProps } from './types'; import type { MarketDataViewProps } from './types';
/** /**
@@ -61,7 +60,6 @@ const MarketDataView: React.FC<MarketDataViewProps> = ({ stockCode: propStockCod
minuteData, minuteData,
minuteLoading, minuteLoading,
analysisMap, analysisMap,
refetch,
loadMinuteData, loadMinuteData,
loadDataByType, loadDataByType,
} = useMarketData(stockCode, selectedPeriod); } = useMarketData(stockCode, selectedPeriod);
@@ -133,41 +131,29 @@ const MarketDataView: React.FC<MarketDataViewProps> = ({ stockCode: propStockCod
{summary && <StockSummaryCard summary={summary} theme={theme} />} {summary && <StockSummaryCard summary={summary} theme={theme} />}
{/* 交易数据 - 日K/分钟K线独立显示在 Tab 上方) */} {/* 交易数据 - 日K/分钟K线独立显示在 Tab 上方) */}
{!loading && ( <TradeDataPanel
<TradeDataPanel theme={theme}
theme={theme} tradeData={tradeData}
tradeData={tradeData} minuteData={minuteData}
minuteData={minuteData} minuteLoading={minuteLoading}
minuteLoading={minuteLoading} analysisMap={analysisMap}
analysisMap={analysisMap} onLoadMinuteData={loadMinuteData}
onLoadMinuteData={loadMinuteData} onChartClick={handleChartClick}
onChartClick={handleChartClick} selectedPeriod={selectedPeriod}
selectedPeriod={selectedPeriod} onPeriodChange={setSelectedPeriod}
onPeriodChange={setSelectedPeriod} stockCode={stockCode}
stockCode={stockCode} loading={loading}
/> />
)}
{/* 主要内容区域 - Tab */} {/* 主要内容区域 - Tab */}
{loading ? ( <SubTabContainer
<Box tabs={tabConfigs}
bg="gray.900" componentProps={componentProps}
border="1px solid" themePreset="blackGold"
borderColor="rgba(212, 175, 55, 0.3)" index={activeTab}
borderRadius="xl" onTabChange={handleTabChange}
> isLazy
<LoadingState message="数据加载中..." height="400px" /> />
</Box>
) : (
<SubTabContainer
tabs={tabConfigs}
componentProps={componentProps}
themePreset="blackGold"
index={activeTab}
onTabChange={handleTabChange}
isLazy
/>
)}
</VStack> </VStack>
</Container> </Container>

View File

@@ -300,6 +300,7 @@ export interface KLineModuleProps {
selectedPeriod?: number; selectedPeriod?: number;
onPeriodChange?: (period: number) => void; onPeriodChange?: (period: number) => void;
stockCode?: string; // 股票代码,用于获取实时盘口数据 stockCode?: string; // 股票代码,用于获取实时盘口数据
loading?: boolean; // 日K线数据加载中
} }
/** /**