feat(ForecastReport): 添加盈利预测骨架屏
- 创建 ForecastSkeleton 组件(图表卡片 + 表格) - 初始加载时显示骨架屏 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -19,11 +19,14 @@ import type { MetricConfig, MetricSectionConfig } from '../types';
|
||||
|
||||
export type TableType = 'metrics' | 'statement';
|
||||
|
||||
// 数据类型:必须有 period 字段
|
||||
export type FinancialDataItem = { period: string; [key: string]: unknown };
|
||||
|
||||
export interface UnifiedFinancialTableProps {
|
||||
/** 表格类型: metrics=指标表格, statement=报表表格 */
|
||||
type: TableType;
|
||||
/** 数据数组 */
|
||||
data: Array<{ period: string; [key: string]: unknown }>;
|
||||
data: FinancialDataItem[];
|
||||
/** metrics 类型: 指标分类 key */
|
||||
categoryKey?: string;
|
||||
/** metrics 类型: 分类标题 */
|
||||
@@ -35,7 +38,7 @@ export interface UnifiedFinancialTableProps {
|
||||
/** 是否隐藏汇总行的分组标题 */
|
||||
hideTotalSectionTitle?: boolean;
|
||||
/** 点击行显示图表回调 */
|
||||
showMetricChart: (name: string, key: string, data: unknown[], path: string) => void;
|
||||
showMetricChart: (name: string, key: string, data: FinancialDataItem[], path: string) => void;
|
||||
/** 是否为成长类指标(正负值着色) */
|
||||
isGrowthCategory?: boolean;
|
||||
/** 核心指标 keys(用于 cashflow 表格) */
|
||||
@@ -78,7 +81,6 @@ const extendedTableStyles = getTableStyles(TABLE_CLASS_NAME) + `
|
||||
const UnifiedFinancialTableInner: React.FC<UnifiedFinancialTableProps> = ({
|
||||
type,
|
||||
data,
|
||||
categoryKey,
|
||||
categoryTitle,
|
||||
metrics,
|
||||
sections,
|
||||
@@ -107,7 +109,7 @@ const UnifiedFinancialTableInner: React.FC<UnifiedFinancialTableProps> = ({
|
||||
}
|
||||
|
||||
// 限制显示列数
|
||||
const maxColumns = type === 'metrics' ? 6 : (type === 'statement' ? 8 : 6);
|
||||
const maxColumns = type === 'metrics' ? 6 : 8;
|
||||
const displayData = data.slice(0, Math.min(data.length, maxColumns));
|
||||
|
||||
// 构建表格数据
|
||||
@@ -168,7 +170,7 @@ const UnifiedFinancialTableInner: React.FC<UnifiedFinancialTableProps> = ({
|
||||
}
|
||||
|
||||
return rows;
|
||||
}, [type, metrics, sections, displayData, data, hideTotalSectionTitle, coreMetricKeys]);
|
||||
}, [type, metrics, sections, displayData, hideTotalSectionTitle, coreMetricKeys]);
|
||||
|
||||
// 计算同比变化
|
||||
const calcYoY = (
|
||||
|
||||
@@ -1,264 +0,0 @@
|
||||
/**
|
||||
* 统一的财务 Tab 组件
|
||||
*
|
||||
* 使用 UnifiedFinancialTable 实现所有 10 个财务表格:
|
||||
* - 7 个财务指标分类 Tab
|
||||
* - 3 个财务报表 Tab
|
||||
*/
|
||||
|
||||
import React, { memo } from 'react';
|
||||
import { UnifiedFinancialTable } from '../components/UnifiedFinancialTable';
|
||||
import {
|
||||
FINANCIAL_METRICS_CATEGORIES,
|
||||
CURRENT_ASSETS_METRICS,
|
||||
NON_CURRENT_ASSETS_METRICS,
|
||||
TOTAL_ASSETS_METRICS,
|
||||
CURRENT_LIABILITIES_METRICS,
|
||||
NON_CURRENT_LIABILITIES_METRICS,
|
||||
TOTAL_LIABILITIES_METRICS,
|
||||
EQUITY_METRICS,
|
||||
INCOME_STATEMENT_SECTIONS,
|
||||
CASHFLOW_METRICS,
|
||||
} from '../constants';
|
||||
import type { FinancialMetricsData, BalanceSheetData, IncomeStatementData, CashflowData } from '../types';
|
||||
|
||||
// ==================== 通用 Props 类型 ====================
|
||||
|
||||
/** 财务指标 Tab Props */
|
||||
export interface MetricsTabProps {
|
||||
financialMetrics: FinancialMetricsData[];
|
||||
loading?: boolean;
|
||||
loadingTab?: string | null;
|
||||
showMetricChart: (name: string, key: string, data: unknown[], path: string) => void;
|
||||
calculateYoYChange: (value: number, period: string, data: unknown[], path: string) => { change: number; intensity: number };
|
||||
getCellBackground: (change: number, intensity: number) => string;
|
||||
positiveColor: string;
|
||||
negativeColor: string;
|
||||
bgColor: string;
|
||||
hoverBg: string;
|
||||
}
|
||||
|
||||
/** 资产负债表 Tab Props */
|
||||
export interface BalanceSheetTabProps {
|
||||
balanceSheet: BalanceSheetData[];
|
||||
loading?: boolean;
|
||||
loadingTab?: string | null;
|
||||
showMetricChart: (name: string, key: string, data: unknown[], path: string) => void;
|
||||
calculateYoYChange: (value: number, period: string, data: unknown[], path: string) => { change: number; intensity: number };
|
||||
getCellBackground: (change: number, intensity: number) => string;
|
||||
positiveColor: string;
|
||||
negativeColor: string;
|
||||
bgColor: string;
|
||||
hoverBg: string;
|
||||
}
|
||||
|
||||
/** 利润表 Tab Props */
|
||||
export interface IncomeStatementTabProps {
|
||||
incomeStatement: IncomeStatementData[];
|
||||
loading?: boolean;
|
||||
loadingTab?: string | null;
|
||||
showMetricChart: (name: string, key: string, data: unknown[], path: string) => void;
|
||||
calculateYoYChange: (value: number, period: string, data: unknown[], path: string) => { change: number; intensity: number };
|
||||
getCellBackground: (change: number, intensity: number) => string;
|
||||
positiveColor: string;
|
||||
negativeColor: string;
|
||||
bgColor: string;
|
||||
hoverBg: string;
|
||||
}
|
||||
|
||||
/** 现金流量表 Tab Props */
|
||||
export interface CashflowTabProps {
|
||||
cashflow: CashflowData[];
|
||||
loading?: boolean;
|
||||
loadingTab?: string | null;
|
||||
showMetricChart: (name: string, key: string, data: unknown[], path: string) => void;
|
||||
calculateYoYChange: (value: number, period: string, data: unknown[], path: string) => { change: number; intensity: number };
|
||||
getCellBackground: (change: number, intensity: number) => string;
|
||||
positiveColor: string;
|
||||
negativeColor: string;
|
||||
bgColor: string;
|
||||
hoverBg: string;
|
||||
}
|
||||
|
||||
// ==================== 财务指标 Tab (7个) ====================
|
||||
|
||||
/** 盈利能力 Tab */
|
||||
export const ProfitabilityTab = memo<MetricsTabProps>(({ financialMetrics, loading, showMetricChart }) => {
|
||||
const category = FINANCIAL_METRICS_CATEGORIES.profitability;
|
||||
return (
|
||||
<UnifiedFinancialTable
|
||||
type="metrics"
|
||||
data={financialMetrics}
|
||||
categoryKey="profitability"
|
||||
categoryTitle={category.title}
|
||||
metrics={category.metrics}
|
||||
showMetricChart={showMetricChart}
|
||||
loading={loading}
|
||||
/>
|
||||
);
|
||||
});
|
||||
ProfitabilityTab.displayName = 'ProfitabilityTab';
|
||||
|
||||
/** 每股指标 Tab */
|
||||
export const PerShareTab = memo<MetricsTabProps>(({ financialMetrics, loading, showMetricChart }) => {
|
||||
const category = FINANCIAL_METRICS_CATEGORIES.perShare;
|
||||
return (
|
||||
<UnifiedFinancialTable
|
||||
type="metrics"
|
||||
data={financialMetrics}
|
||||
categoryKey="perShare"
|
||||
categoryTitle={category.title}
|
||||
metrics={category.metrics}
|
||||
showMetricChart={showMetricChart}
|
||||
loading={loading}
|
||||
/>
|
||||
);
|
||||
});
|
||||
PerShareTab.displayName = 'PerShareTab';
|
||||
|
||||
/** 成长能力 Tab */
|
||||
export const GrowthTab = memo<MetricsTabProps>(({ financialMetrics, loading, showMetricChart }) => {
|
||||
const category = FINANCIAL_METRICS_CATEGORIES.growth;
|
||||
return (
|
||||
<UnifiedFinancialTable
|
||||
type="metrics"
|
||||
data={financialMetrics}
|
||||
categoryKey="growth"
|
||||
categoryTitle={category.title}
|
||||
metrics={category.metrics}
|
||||
showMetricChart={showMetricChart}
|
||||
loading={loading}
|
||||
isGrowthCategory
|
||||
/>
|
||||
);
|
||||
});
|
||||
GrowthTab.displayName = 'GrowthTab';
|
||||
|
||||
/** 运营效率 Tab */
|
||||
export const OperationalTab = memo<MetricsTabProps>(({ financialMetrics, loading, showMetricChart }) => {
|
||||
const category = FINANCIAL_METRICS_CATEGORIES.operational;
|
||||
return (
|
||||
<UnifiedFinancialTable
|
||||
type="metrics"
|
||||
data={financialMetrics}
|
||||
categoryKey="operational"
|
||||
categoryTitle={category.title}
|
||||
metrics={category.metrics}
|
||||
showMetricChart={showMetricChart}
|
||||
loading={loading}
|
||||
/>
|
||||
);
|
||||
});
|
||||
OperationalTab.displayName = 'OperationalTab';
|
||||
|
||||
/** 偿债能力 Tab */
|
||||
export const SolvencyTab = memo<MetricsTabProps>(({ financialMetrics, loading, showMetricChart }) => {
|
||||
const category = FINANCIAL_METRICS_CATEGORIES.solvency;
|
||||
return (
|
||||
<UnifiedFinancialTable
|
||||
type="metrics"
|
||||
data={financialMetrics}
|
||||
categoryKey="solvency"
|
||||
categoryTitle={category.title}
|
||||
metrics={category.metrics}
|
||||
showMetricChart={showMetricChart}
|
||||
loading={loading}
|
||||
/>
|
||||
);
|
||||
});
|
||||
SolvencyTab.displayName = 'SolvencyTab';
|
||||
|
||||
/** 费用率 Tab */
|
||||
export const ExpenseTab = memo<MetricsTabProps>(({ financialMetrics, loading, showMetricChart }) => {
|
||||
const category = FINANCIAL_METRICS_CATEGORIES.expense;
|
||||
return (
|
||||
<UnifiedFinancialTable
|
||||
type="metrics"
|
||||
data={financialMetrics}
|
||||
categoryKey="expense"
|
||||
categoryTitle={category.title}
|
||||
metrics={category.metrics}
|
||||
showMetricChart={showMetricChart}
|
||||
loading={loading}
|
||||
/>
|
||||
);
|
||||
});
|
||||
ExpenseTab.displayName = 'ExpenseTab';
|
||||
|
||||
/** 现金流指标 Tab */
|
||||
export const CashflowMetricsTab = memo<MetricsTabProps>(({ financialMetrics, loading, showMetricChart }) => {
|
||||
const category = FINANCIAL_METRICS_CATEGORIES.cashflow;
|
||||
return (
|
||||
<UnifiedFinancialTable
|
||||
type="metrics"
|
||||
data={financialMetrics}
|
||||
categoryKey="cashflow"
|
||||
categoryTitle={category.title}
|
||||
metrics={category.metrics}
|
||||
showMetricChart={showMetricChart}
|
||||
loading={loading}
|
||||
/>
|
||||
);
|
||||
});
|
||||
CashflowMetricsTab.displayName = 'CashflowMetricsTab';
|
||||
|
||||
// ==================== 财务报表 Tab (3个) ====================
|
||||
|
||||
// 资产负债表分组配置
|
||||
const BALANCE_SHEET_SECTIONS = [
|
||||
CURRENT_ASSETS_METRICS,
|
||||
NON_CURRENT_ASSETS_METRICS,
|
||||
TOTAL_ASSETS_METRICS,
|
||||
CURRENT_LIABILITIES_METRICS,
|
||||
NON_CURRENT_LIABILITIES_METRICS,
|
||||
TOTAL_LIABILITIES_METRICS,
|
||||
EQUITY_METRICS,
|
||||
];
|
||||
|
||||
/** 资产负债表 Tab */
|
||||
export const BalanceSheetTab = memo<BalanceSheetTabProps>(({ balanceSheet, loading, showMetricChart }) => (
|
||||
<UnifiedFinancialTable
|
||||
type="statement"
|
||||
data={balanceSheet}
|
||||
sections={BALANCE_SHEET_SECTIONS}
|
||||
showMetricChart={showMetricChart}
|
||||
loading={loading}
|
||||
/>
|
||||
));
|
||||
BalanceSheetTab.displayName = 'BalanceSheetTab';
|
||||
|
||||
/** 利润表 Tab */
|
||||
export const IncomeStatementTab = memo<IncomeStatementTabProps>(({ incomeStatement, loading, showMetricChart }) => (
|
||||
<UnifiedFinancialTable
|
||||
type="statement"
|
||||
data={incomeStatement}
|
||||
sections={INCOME_STATEMENT_SECTIONS}
|
||||
hideTotalSectionTitle={false}
|
||||
showMetricChart={showMetricChart}
|
||||
loading={loading}
|
||||
/>
|
||||
));
|
||||
IncomeStatementTab.displayName = 'IncomeStatementTab';
|
||||
|
||||
// 现金流量表配置(转换为 sections 格式)
|
||||
const CASHFLOW_SECTIONS = [{
|
||||
title: '现金流量',
|
||||
key: 'cashflow',
|
||||
metrics: CASHFLOW_METRICS.map(m => ({
|
||||
...m,
|
||||
isCore: ['operating_net', 'free_cash_flow'].includes(m.key),
|
||||
})),
|
||||
}];
|
||||
|
||||
/** 现金流量表 Tab */
|
||||
export const CashflowTab = memo<CashflowTabProps>(({ cashflow, loading, showMetricChart }) => (
|
||||
<UnifiedFinancialTable
|
||||
type="statement"
|
||||
data={cashflow}
|
||||
sections={CASHFLOW_SECTIONS}
|
||||
hideTotalSectionTitle
|
||||
showMetricChart={showMetricChart}
|
||||
loading={loading}
|
||||
/>
|
||||
));
|
||||
CashflowTab.displayName = 'CashflowTab';
|
||||
@@ -0,0 +1,79 @@
|
||||
/**
|
||||
* 盈利预测骨架屏组件
|
||||
*/
|
||||
|
||||
import React, { memo } from 'react';
|
||||
import {
|
||||
Box,
|
||||
SimpleGrid,
|
||||
Skeleton,
|
||||
SkeletonText,
|
||||
Card,
|
||||
CardBody,
|
||||
VStack,
|
||||
} from '@chakra-ui/react';
|
||||
|
||||
// 黑金主题配色
|
||||
const SKELETON_COLORS = {
|
||||
startColor: 'rgba(26, 32, 44, 0.6)',
|
||||
endColor: 'rgba(212, 175, 55, 0.2)',
|
||||
};
|
||||
|
||||
/**
|
||||
* 图表卡片骨架屏
|
||||
*/
|
||||
const ChartCardSkeleton: React.FC = memo(() => (
|
||||
<Card bg="gray.900" border="1px solid" borderColor="rgba(212, 175, 55, 0.3)">
|
||||
<CardBody>
|
||||
<Skeleton height="20px" width="100px" mb={4} {...SKELETON_COLORS} />
|
||||
<Skeleton height="200px" borderRadius="md" {...SKELETON_COLORS} />
|
||||
</CardBody>
|
||||
</Card>
|
||||
));
|
||||
|
||||
ChartCardSkeleton.displayName = 'ChartCardSkeleton';
|
||||
|
||||
/**
|
||||
* 表格骨架屏
|
||||
*/
|
||||
const TableSkeleton: React.FC = memo(() => (
|
||||
<Card bg="gray.900" border="1px solid" borderColor="rgba(212, 175, 55, 0.3)">
|
||||
<CardBody>
|
||||
<Skeleton height="20px" width="120px" mb={4} {...SKELETON_COLORS} />
|
||||
<VStack align="stretch" spacing={3}>
|
||||
{/* 表头 */}
|
||||
<Skeleton height="40px" {...SKELETON_COLORS} />
|
||||
{/* 表格行 */}
|
||||
{[1, 2, 3, 4, 5].map((i) => (
|
||||
<Skeleton key={i} height="36px" {...SKELETON_COLORS} />
|
||||
))}
|
||||
</VStack>
|
||||
</CardBody>
|
||||
</Card>
|
||||
));
|
||||
|
||||
TableSkeleton.displayName = 'TableSkeleton';
|
||||
|
||||
/**
|
||||
* 盈利预测完整骨架屏
|
||||
*/
|
||||
const ForecastSkeleton: React.FC = memo(() => (
|
||||
<Box>
|
||||
{/* 图表区域 - 3列布局 */}
|
||||
<SimpleGrid columns={{ base: 1, md: 3 }} spacing={4}>
|
||||
<ChartCardSkeleton />
|
||||
<ChartCardSkeleton />
|
||||
<ChartCardSkeleton />
|
||||
</SimpleGrid>
|
||||
|
||||
{/* 详细数据表格 */}
|
||||
<Box mt={4}>
|
||||
<TableSkeleton />
|
||||
</Box>
|
||||
</Box>
|
||||
));
|
||||
|
||||
ForecastSkeleton.displayName = 'ForecastSkeleton';
|
||||
|
||||
export { ForecastSkeleton };
|
||||
export default ForecastSkeleton;
|
||||
@@ -9,3 +9,4 @@ export { default as IncomeProfitGrowthChart } from './IncomeProfitGrowthChart';
|
||||
export { default as EpsChart } from './EpsChart';
|
||||
export { default as PePegChart } from './PePegChart';
|
||||
export { default as DetailTable } from './DetailTable';
|
||||
export { ForecastSkeleton } from './ForecastSkeleton';
|
||||
|
||||
@@ -11,16 +11,16 @@ import {
|
||||
EpsChart,
|
||||
PePegChart,
|
||||
DetailTable,
|
||||
ForecastSkeleton,
|
||||
} from './components';
|
||||
import LoadingState from '../LoadingState';
|
||||
import type { ForecastReportProps } from './types';
|
||||
|
||||
const ForecastReport: React.FC<ForecastReportProps> = ({ stockCode }) => {
|
||||
const { data, isLoading, error, refetch } = useForecastData(stockCode);
|
||||
|
||||
// 加载状态
|
||||
// 加载状态 - 显示骨架屏
|
||||
if (isLoading && !data) {
|
||||
return <LoadingState message="加载盈利预测数据中..." height="300px" />;
|
||||
return <ForecastSkeleton />;
|
||||
}
|
||||
|
||||
// 错误状态
|
||||
|
||||
Reference in New Issue
Block a user