feat(MarketDataView): 添加股票行情骨架屏
- 创建 MarketDataSkeleton 组件(摘要卡片 + K线图表 + Tab) - 配置 Suspense fallback,点击时直接显示骨架屏 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -3,7 +3,21 @@
|
|||||||
|
|
||||||
// 生成财务数据
|
// 生成财务数据
|
||||||
export const generateFinancialData = (stockCode) => {
|
export const generateFinancialData = (stockCode) => {
|
||||||
const periods = ['2024-09-30', '2024-06-30', '2024-03-31', '2023-12-31'];
|
// 12 期数据 - 用于财务指标表格(7个指标Tab)
|
||||||
|
const metricsPeriods = [
|
||||||
|
'2024-09-30', '2024-06-30', '2024-03-31', '2023-12-31',
|
||||||
|
'2023-09-30', '2023-06-30', '2023-03-31', '2022-12-31',
|
||||||
|
'2022-09-30', '2022-06-30', '2022-03-31', '2021-12-31',
|
||||||
|
];
|
||||||
|
|
||||||
|
// 8 期数据 - 用于财务报表(3个报表Tab)
|
||||||
|
const statementPeriods = [
|
||||||
|
'2024-09-30', '2024-06-30', '2024-03-31', '2023-12-31',
|
||||||
|
'2023-09-30', '2023-06-30', '2023-03-31', '2022-12-31',
|
||||||
|
];
|
||||||
|
|
||||||
|
// 兼容旧代码
|
||||||
|
const periods = statementPeriods.slice(0, 4);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
stockCode,
|
stockCode,
|
||||||
@@ -44,8 +58,8 @@ export const generateFinancialData = (stockCode) => {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
// 资产负债表 - 嵌套结构
|
// 资产负债表 - 嵌套结构(8期数据)
|
||||||
balanceSheet: periods.map((period, i) => ({
|
balanceSheet: statementPeriods.map((period, i) => ({
|
||||||
period,
|
period,
|
||||||
assets: {
|
assets: {
|
||||||
current_assets: {
|
current_assets: {
|
||||||
@@ -110,8 +124,8 @@ export const generateFinancialData = (stockCode) => {
|
|||||||
}
|
}
|
||||||
})),
|
})),
|
||||||
|
|
||||||
// 利润表 - 嵌套结构
|
// 利润表 - 嵌套结构(8期数据)
|
||||||
incomeStatement: periods.map((period, i) => ({
|
incomeStatement: statementPeriods.map((period, i) => ({
|
||||||
period,
|
period,
|
||||||
revenue: {
|
revenue: {
|
||||||
total_operating_revenue: 162350 - i * 4000,
|
total_operating_revenue: 162350 - i * 4000,
|
||||||
@@ -166,8 +180,8 @@ export const generateFinancialData = (stockCode) => {
|
|||||||
}
|
}
|
||||||
})),
|
})),
|
||||||
|
|
||||||
// 现金流量表 - 嵌套结构
|
// 现金流量表 - 嵌套结构(8期数据)
|
||||||
cashflow: periods.map((period, i) => ({
|
cashflow: statementPeriods.map((period, i) => ({
|
||||||
period,
|
period,
|
||||||
operating_activities: {
|
operating_activities: {
|
||||||
inflow: {
|
inflow: {
|
||||||
@@ -193,8 +207,8 @@ export const generateFinancialData = (stockCode) => {
|
|||||||
}
|
}
|
||||||
})),
|
})),
|
||||||
|
|
||||||
// 财务指标 - 嵌套结构
|
// 财务指标 - 嵌套结构(12期数据)
|
||||||
financialMetrics: periods.map((period, i) => ({
|
financialMetrics: metricsPeriods.map((period, i) => ({
|
||||||
period,
|
period,
|
||||||
profitability: {
|
profitability: {
|
||||||
roe: 16.23 - i * 0.3,
|
roe: 16.23 - i * 0.3,
|
||||||
|
|||||||
@@ -108,8 +108,9 @@ const UnifiedFinancialTableInner: React.FC<UnifiedFinancialTableProps> = ({
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 限制显示列数
|
// 固定显示期数: 财务指标 6 期, 财务报表 8 期
|
||||||
const maxColumns = type === 'metrics' ? 6 : 8;
|
const FIXED_PERIODS = { metrics: 6, statement: 8 };
|
||||||
|
const maxColumns = FIXED_PERIODS[type];
|
||||||
const displayData = data.slice(0, Math.min(data.length, maxColumns));
|
const displayData = data.slice(0, Math.min(data.length, maxColumns));
|
||||||
|
|
||||||
// 构建表格数据
|
// 构建表格数据
|
||||||
|
|||||||
@@ -106,6 +106,14 @@ export const useFinancialData = (
|
|||||||
const coreDataControllerRef = useRef<AbortController | null>(null);
|
const coreDataControllerRef = useRef<AbortController | null>(null);
|
||||||
const tabDataControllerRef = useRef<AbortController | null>(null);
|
const tabDataControllerRef = useRef<AbortController | null>(null);
|
||||||
|
|
||||||
|
// 记录每种数据类型加载时使用的期数(用于 Tab 切换时判断是否需要重新加载)
|
||||||
|
const dataPeriodsRef = useRef<Record<string, number>>({
|
||||||
|
balance: 0,
|
||||||
|
income: 0,
|
||||||
|
cashflow: 0,
|
||||||
|
metrics: 0,
|
||||||
|
});
|
||||||
|
|
||||||
// 判断 Tab key 对应的数据类型
|
// 判断 Tab key 对应的数据类型
|
||||||
const getDataTypeForTab = (tabKey: DataTypeKey): 'balance' | 'income' | 'cashflow' | 'metrics' => {
|
const getDataTypeForTab = (tabKey: DataTypeKey): 'balance' | 'income' | 'cashflow' | 'metrics' => {
|
||||||
switch (tabKey) {
|
switch (tabKey) {
|
||||||
|
|||||||
@@ -20,8 +20,10 @@ export const BLACK_GOLD_TABLE_THEME: ThemeConfig = {
|
|||||||
headerColor: '#D4AF37',
|
headerColor: '#D4AF37',
|
||||||
rowHoverBg: 'rgba(212, 175, 55, 0.1)',
|
rowHoverBg: 'rgba(212, 175, 55, 0.1)',
|
||||||
borderColor: 'rgba(212, 175, 55, 0.15)',
|
borderColor: 'rgba(212, 175, 55, 0.15)',
|
||||||
cellPaddingBlock: 8,
|
cellPaddingBlock: 6,
|
||||||
cellPaddingInline: 12,
|
cellPaddingInline: 8,
|
||||||
|
// 表头紧凑样式
|
||||||
|
headerSplitColor: 'transparent',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
@@ -40,6 +42,15 @@ export const getTableStyles = (className: string): string => `
|
|||||||
border-bottom: 1px solid rgba(212, 175, 55, 0.3) !important;
|
border-bottom: 1px solid rgba(212, 175, 55, 0.3) !important;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
font-size: 13px;
|
font-size: 13px;
|
||||||
|
padding: 4px 8px !important;
|
||||||
|
margin: 0 !important;
|
||||||
|
}
|
||||||
|
.${className} .ant-table-thead > tr {
|
||||||
|
margin: 0 !important;
|
||||||
|
}
|
||||||
|
.${className} .ant-table-header {
|
||||||
|
margin: 0 !important;
|
||||||
|
padding: 0 !important;
|
||||||
}
|
}
|
||||||
.${className} .ant-table-tbody > tr > td {
|
.${className} .ant-table-tbody > tr > td {
|
||||||
border-bottom: 1px solid rgba(212, 175, 55, 0.1) !important;
|
border-bottom: 1px solid rgba(212, 175, 55, 0.1) !important;
|
||||||
|
|||||||
@@ -0,0 +1,140 @@
|
|||||||
|
/**
|
||||||
|
* 股票行情骨架屏组件
|
||||||
|
*/
|
||||||
|
|
||||||
|
import React, { memo } from 'react';
|
||||||
|
import {
|
||||||
|
Box,
|
||||||
|
Container,
|
||||||
|
VStack,
|
||||||
|
HStack,
|
||||||
|
Skeleton,
|
||||||
|
SkeletonText,
|
||||||
|
SimpleGrid,
|
||||||
|
Card,
|
||||||
|
CardBody,
|
||||||
|
} from '@chakra-ui/react';
|
||||||
|
|
||||||
|
// 黑金主题配色
|
||||||
|
const SKELETON_COLORS = {
|
||||||
|
startColor: 'rgba(26, 32, 44, 0.6)',
|
||||||
|
endColor: 'rgba(212, 175, 55, 0.2)',
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 股票摘要卡片骨架屏
|
||||||
|
*/
|
||||||
|
const SummaryCardSkeleton: React.FC = memo(() => (
|
||||||
|
<Card bg="gray.900" border="1px solid" borderColor="rgba(212, 175, 55, 0.3)">
|
||||||
|
<CardBody>
|
||||||
|
<HStack spacing={8} align="flex-start">
|
||||||
|
{/* 左侧:股票名称和价格 */}
|
||||||
|
<VStack align="flex-start" spacing={2} minW="200px">
|
||||||
|
<Skeleton height="28px" width="150px" {...SKELETON_COLORS} />
|
||||||
|
<Skeleton height="40px" width="120px" {...SKELETON_COLORS} />
|
||||||
|
<HStack spacing={4}>
|
||||||
|
<Skeleton height="20px" width="80px" {...SKELETON_COLORS} />
|
||||||
|
<Skeleton height="20px" width="60px" {...SKELETON_COLORS} />
|
||||||
|
</HStack>
|
||||||
|
</VStack>
|
||||||
|
|
||||||
|
{/* 右侧:指标网格 */}
|
||||||
|
<SimpleGrid columns={4} spacing={4} flex={1}>
|
||||||
|
{[1, 2, 3, 4, 5, 6, 7, 8].map((i) => (
|
||||||
|
<VStack key={i} align="flex-start" spacing={1}>
|
||||||
|
<Skeleton height="14px" width="50px" {...SKELETON_COLORS} />
|
||||||
|
<Skeleton height="20px" width="70px" {...SKELETON_COLORS} />
|
||||||
|
</VStack>
|
||||||
|
))}
|
||||||
|
</SimpleGrid>
|
||||||
|
</HStack>
|
||||||
|
</CardBody>
|
||||||
|
</Card>
|
||||||
|
));
|
||||||
|
|
||||||
|
SummaryCardSkeleton.displayName = 'SummaryCardSkeleton';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* K线图表骨架屏
|
||||||
|
*/
|
||||||
|
const ChartSkeleton: React.FC = memo(() => (
|
||||||
|
<Card bg="gray.900" border="1px solid" borderColor="rgba(212, 175, 55, 0.3)">
|
||||||
|
<CardBody>
|
||||||
|
{/* 工具栏 */}
|
||||||
|
<HStack justify="space-between" mb={4}>
|
||||||
|
<HStack spacing={2}>
|
||||||
|
{[1, 2, 3, 4].map((i) => (
|
||||||
|
<Skeleton key={i} height="32px" width="60px" borderRadius="md" {...SKELETON_COLORS} />
|
||||||
|
))}
|
||||||
|
</HStack>
|
||||||
|
<HStack spacing={2}>
|
||||||
|
<Skeleton height="32px" width="100px" borderRadius="md" {...SKELETON_COLORS} />
|
||||||
|
<Skeleton height="32px" width="32px" borderRadius="md" {...SKELETON_COLORS} />
|
||||||
|
</HStack>
|
||||||
|
</HStack>
|
||||||
|
|
||||||
|
{/* 图表区域 */}
|
||||||
|
<Skeleton height="400px" borderRadius="md" {...SKELETON_COLORS} />
|
||||||
|
</CardBody>
|
||||||
|
</Card>
|
||||||
|
));
|
||||||
|
|
||||||
|
ChartSkeleton.displayName = 'ChartSkeleton';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Tab 区域骨架屏
|
||||||
|
*/
|
||||||
|
const TabSkeleton: React.FC = memo(() => (
|
||||||
|
<Card bg="gray.900" border="1px solid" borderColor="rgba(212, 175, 55, 0.3)">
|
||||||
|
<CardBody p={0}>
|
||||||
|
{/* Tab 栏 */}
|
||||||
|
<HStack
|
||||||
|
spacing={2}
|
||||||
|
p={3}
|
||||||
|
borderBottom="1px solid"
|
||||||
|
borderColor="rgba(212, 175, 55, 0.2)"
|
||||||
|
>
|
||||||
|
{[1, 2, 3, 4].map((i) => (
|
||||||
|
<Skeleton
|
||||||
|
key={i}
|
||||||
|
height="36px"
|
||||||
|
width="100px"
|
||||||
|
borderRadius="md"
|
||||||
|
{...SKELETON_COLORS}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</HStack>
|
||||||
|
{/* Tab 内容 */}
|
||||||
|
<Box p={4}>
|
||||||
|
<SkeletonText
|
||||||
|
noOfLines={6}
|
||||||
|
spacing={4}
|
||||||
|
skeletonHeight={4}
|
||||||
|
{...SKELETON_COLORS}
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
</CardBody>
|
||||||
|
</Card>
|
||||||
|
));
|
||||||
|
|
||||||
|
TabSkeleton.displayName = 'TabSkeleton';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 股票行情完整骨架屏
|
||||||
|
*/
|
||||||
|
const MarketDataSkeleton: React.FC = memo(() => (
|
||||||
|
<Box bg="#1A202C" minH="100vh">
|
||||||
|
<Container maxW="container.xl" py={6}>
|
||||||
|
<VStack align="stretch" spacing={6}>
|
||||||
|
<SummaryCardSkeleton />
|
||||||
|
<ChartSkeleton />
|
||||||
|
<TabSkeleton />
|
||||||
|
</VStack>
|
||||||
|
</Container>
|
||||||
|
</Box>
|
||||||
|
));
|
||||||
|
|
||||||
|
MarketDataSkeleton.displayName = 'MarketDataSkeleton';
|
||||||
|
|
||||||
|
export { MarketDataSkeleton };
|
||||||
|
export default MarketDataSkeleton;
|
||||||
@@ -5,3 +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';
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import type { CompanyTheme, TabConfig } from './types';
|
|||||||
// 骨架屏组件(同步导入,用于 Suspense fallback)
|
// 骨架屏组件(同步导入,用于 Suspense fallback)
|
||||||
import { FinancialPanoramaSkeleton } from './components/FinancialPanorama/components';
|
import { FinancialPanoramaSkeleton } from './components/FinancialPanorama/components';
|
||||||
import { ForecastSkeleton } from './components/ForecastReport/components';
|
import { ForecastSkeleton } from './components/ForecastReport/components';
|
||||||
|
import { MarketDataSkeleton } from './components/MarketDataView/components';
|
||||||
|
|
||||||
// ============================================
|
// ============================================
|
||||||
// 黑金主题配置
|
// 黑金主题配置
|
||||||
@@ -87,6 +88,7 @@ export const TAB_CONFIG: TabConfig[] = [
|
|||||||
name: '股票行情',
|
name: '股票行情',
|
||||||
icon: TrendingUp,
|
icon: TrendingUp,
|
||||||
component: MarketDataView,
|
component: MarketDataView,
|
||||||
|
fallback: React.createElement(MarketDataSkeleton),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: 'financial',
|
key: 'financial',
|
||||||
|
|||||||
Reference in New Issue
Block a user