diff --git a/src/views/Company/components/FinancialPanorama/components/FinancialPanoramaSkeleton.tsx b/src/views/Company/components/FinancialPanorama/components/FinancialPanoramaSkeleton.tsx
new file mode 100644
index 00000000..fe22c631
--- /dev/null
+++ b/src/views/Company/components/FinancialPanorama/components/FinancialPanoramaSkeleton.tsx
@@ -0,0 +1,151 @@
+/**
+ * 财务全景骨架屏组件
+ */
+
+import React, { memo } from 'react';
+import {
+ Box,
+ 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 OverviewPanelSkeleton: React.FC = memo(() => (
+
+
+
+ {[1, 2, 3].map((i) => (
+
+
+
+ {[1, 2, 3, 4].map((j) => (
+
+
+
+
+ ))}
+
+
+ ))}
+
+
+
+));
+
+OverviewPanelSkeleton.displayName = 'OverviewPanelSkeleton';
+
+/**
+ * 图表区域骨架屏
+ */
+const ChartSkeleton: React.FC = memo(() => (
+
+
+
+
+
+
+));
+
+ChartSkeleton.displayName = 'ChartSkeleton';
+
+/**
+ * 主营业务骨架屏
+ */
+const MainBusinessSkeleton: React.FC = memo(() => (
+
+
+
+ {[1, 2].map((i) => (
+
+
+
+
+ {[1, 2, 3].map((j) => (
+
+
+
+
+ ))}
+
+
+
+ ))}
+
+
+));
+
+MainBusinessSkeleton.displayName = 'MainBusinessSkeleton';
+
+/**
+ * Tab 区域骨架屏
+ */
+const TabSkeleton: React.FC = memo(() => (
+
+
+ {/* Tab 栏 */}
+
+ {[1, 2, 3, 4, 5, 6, 7].map((i) => (
+
+ ))}
+
+ {/* Tab 内容 */}
+
+
+
+
+
+));
+
+TabSkeleton.displayName = 'TabSkeleton';
+
+/**
+ * 财务全景完整骨架屏
+ */
+const FinancialPanoramaSkeleton: React.FC = memo(() => (
+
+
+
+
+
+
+));
+
+FinancialPanoramaSkeleton.displayName = 'FinancialPanoramaSkeleton';
+
+export { FinancialPanoramaSkeleton };
+export default FinancialPanoramaSkeleton;
diff --git a/src/views/Company/components/FinancialPanorama/components/UnifiedFinancialTable.tsx b/src/views/Company/components/FinancialPanorama/components/UnifiedFinancialTable.tsx
new file mode 100644
index 00000000..ed6a1fcc
--- /dev/null
+++ b/src/views/Company/components/FinancialPanorama/components/UnifiedFinancialTable.tsx
@@ -0,0 +1,374 @@
+/**
+ * 统一财务表格组件 - Ant Design 黑金主题
+ *
+ * 支持两种表格类型:
+ * - metrics: 财务指标表格(7个分类,扁平结构)
+ * - statement: 财务报表表格(3个报表,分组结构)
+ */
+
+import React, { useMemo, memo } from 'react';
+import { Box, Text, HStack, Badge as ChakraBadge, Button, Spinner, Center } from '@chakra-ui/react';
+import { Table, ConfigProvider, Tooltip } from 'antd';
+import type { ColumnsType } from 'antd/es/table';
+import { Eye } from 'lucide-react';
+import { formatUtils } from '@services/financialService';
+import { BLACK_GOLD_TABLE_THEME, getTableStyles, calculateYoY, getValueByPath, isNegativeIndicator } from '../utils';
+import type { MetricConfig, MetricSectionConfig } from '../types';
+
+// ==================== 类型定义 ====================
+
+export type TableType = 'metrics' | 'statement';
+
+export interface UnifiedFinancialTableProps {
+ /** 表格类型: metrics=指标表格, statement=报表表格 */
+ type: TableType;
+ /** 数据数组 */
+ data: Array<{ period: string; [key: string]: unknown }>;
+ /** metrics 类型: 指标分类 key */
+ categoryKey?: string;
+ /** metrics 类型: 分类标题 */
+ categoryTitle?: string;
+ /** metrics 类型: 指标配置数组 */
+ metrics?: MetricConfig[];
+ /** statement 类型: 分组配置数组 */
+ sections?: MetricSectionConfig[];
+ /** 是否隐藏汇总行的分组标题 */
+ hideTotalSectionTitle?: boolean;
+ /** 点击行显示图表回调 */
+ showMetricChart: (name: string, key: string, data: unknown[], path: string) => void;
+ /** 是否为成长类指标(正负值着色) */
+ isGrowthCategory?: boolean;
+ /** 核心指标 keys(用于 cashflow 表格) */
+ coreMetricKeys?: string[];
+ /** 加载中 */
+ loading?: boolean;
+}
+
+// 表格行数据类型
+interface TableRowData {
+ key: string;
+ name: string;
+ path: string;
+ isCore?: boolean;
+ isTotal?: boolean;
+ isSubtotal?: boolean;
+ isSection?: boolean;
+ indent?: number;
+ [period: string]: unknown;
+}
+
+const TABLE_CLASS_NAME = 'unified-financial-table';
+
+// 扩展样式(支持 positive-value, negative-value)
+const extendedTableStyles = getTableStyles(TABLE_CLASS_NAME) + `
+ .${TABLE_CLASS_NAME} .positive-value {
+ color: #E53E3E;
+ }
+ .${TABLE_CLASS_NAME} .negative-value {
+ color: #48BB78;
+ }
+ .${TABLE_CLASS_NAME} .ant-table-tbody > tr.subtotal-row > td {
+ background: rgba(212, 175, 55, 0.1) !important;
+ font-weight: 500;
+ }
+`;
+
+// ==================== 组件实现 ====================
+
+const UnifiedFinancialTableInner: React.FC = ({
+ type,
+ data,
+ categoryKey,
+ categoryTitle,
+ metrics,
+ sections,
+ hideTotalSectionTitle = true,
+ showMetricChart,
+ isGrowthCategory = false,
+ coreMetricKeys = [],
+ loading = false,
+}) => {
+ // 加载中状态
+ if (loading && (!Array.isArray(data) || data.length === 0)) {
+ return (
+
+
+
+ );
+ }
+
+ // 数据安全检查
+ if (!Array.isArray(data) || data.length === 0) {
+ return (
+
+ 暂无数据
+
+ );
+ }
+
+ // 限制显示列数
+ const maxColumns = type === 'metrics' ? 6 : (type === 'statement' ? 8 : 6);
+ const displayData = data.slice(0, Math.min(data.length, maxColumns));
+
+ // 构建表格数据
+ const tableData = useMemo(() => {
+ const rows: TableRowData[] = [];
+
+ if (type === 'metrics' && metrics) {
+ // 财务指标表格: 扁平结构
+ metrics.forEach((metric) => {
+ const row: TableRowData = {
+ key: metric.key,
+ name: metric.name,
+ path: metric.path,
+ isCore: metric.isCore || coreMetricKeys.includes(metric.key),
+ };
+
+ displayData.forEach((item) => {
+ const value = getValueByPath(item, metric.path);
+ row[item.period] = value;
+ });
+
+ rows.push(row);
+ });
+ } else if (type === 'statement' && sections) {
+ // 财务报表表格: 分组结构
+ sections.forEach((section) => {
+ // 添加分组标题行(可配置隐藏汇总行标题)
+ const isTotalSection = section.title.includes('总计') || section.title.includes('合计');
+ if (!isTotalSection || !hideTotalSectionTitle) {
+ rows.push({
+ key: `section-${section.key}`,
+ name: section.title,
+ path: '',
+ isSection: true,
+ });
+ }
+
+ // 添加指标行
+ section.metrics.forEach((metric) => {
+ const row: TableRowData = {
+ key: metric.key,
+ name: metric.name,
+ path: metric.path,
+ isCore: metric.isCore,
+ isTotal: metric.isTotal || isTotalSection,
+ isSubtotal: metric.isSubtotal,
+ indent: metric.isTotal || metric.isSubtotal ? 0 : (metric.name.startsWith(' ') ? 2 : 1),
+ };
+
+ displayData.forEach((item) => {
+ const value = getValueByPath(item, metric.path);
+ row[item.period] = value;
+ });
+
+ rows.push(row);
+ });
+ });
+ }
+
+ return rows;
+ }, [type, metrics, sections, displayData, data, hideTotalSectionTitle, coreMetricKeys]);
+
+ // 计算同比变化
+ const calcYoY = (
+ currentValue: number | undefined,
+ currentPeriod: string,
+ path: string
+ ): number | null => {
+ return calculateYoY(data, currentValue, currentPeriod, path, getValueByPath);
+ };
+
+ // 构建列定义
+ const columns: ColumnsType = useMemo(() => {
+ const cols: ColumnsType = [
+ // 指标名称列
+ {
+ title: type === 'metrics' ? (categoryTitle || '指标') : '项目',
+ dataIndex: 'name',
+ key: 'name',
+ fixed: 'left',
+ width: type === 'metrics' ? 200 : 250,
+ render: (name: string, record: TableRowData) => {
+ if (record.isSection) {
+ return {name};
+ }
+ return (
+
+
+ {name}
+
+ {record.isCore && (
+
+ 核心
+
+ )}
+
+ );
+ },
+ },
+ // 各期数据列
+ ...displayData.map((item) => ({
+ title: (
+
+ {formatUtils.getReportType(item.period)}
+ {item.period.substring(0, 10)}
+
+ ),
+ dataIndex: item.period,
+ key: item.period,
+ width: type === 'metrics' ? 100 : 120,
+ align: 'right' as const,
+ render: (value: number | undefined, record: TableRowData) => {
+ if (record.isSection) return null;
+
+ const yoy = calcYoY(value, item.period, record.path);
+ const isNegative = isNegativeIndicator(record.key);
+
+ // 值格式化
+ let formattedValue: string;
+ let valueColorClass = '';
+
+ if (type === 'metrics') {
+ formattedValue = value?.toFixed(2) || '-';
+ // 成长类指标: 正值红色,负值绿色
+ if (isGrowthCategory) {
+ valueColorClass = value !== undefined && value > 0
+ ? 'positive-value'
+ : value !== undefined && value < 0
+ ? 'negative-value'
+ : '';
+ }
+ } else {
+ // 财务报表:使用大数格式化
+ const isEPS = record.key.includes('eps');
+ formattedValue = isEPS ? (value?.toFixed(3) || '-') : formatUtils.formatLargeNumber(value, 0);
+ // 利润表负值着色
+ if (value !== undefined && value < 0) {
+ valueColorClass = 'negative-value';
+ }
+ }
+
+ // 同比变化颜色(负向指标逻辑反转)
+ const changeColor = isNegative
+ ? (yoy && yoy > 0 ? 'negative-change' : 'positive-change')
+ : (yoy && yoy > 0 ? 'positive-change' : 'negative-change');
+
+ // 显示同比箭头的阈值
+ const yoyThreshold = type === 'metrics' ? 20 : 30;
+ const showYoyArrow = yoy !== null && Math.abs(yoy) > yoyThreshold && !record.isTotal;
+
+ return (
+
+ {record.name}: {type === 'metrics' ? (value?.toFixed(2) || '-') : formatUtils.formatLargeNumber(value)}
+ {yoy !== null && 同比: {yoy.toFixed(2)}%}
+
+ }
+ >
+
+
+ {formattedValue}
+
+ {showYoyArrow && value !== undefined && Math.abs(value) > 0.01 && (
+
+ {yoy > 0 ? '↑' : '↓'}{type === 'statement' && Math.abs(yoy) < 100 ? `${Math.abs(yoy).toFixed(0)}%` : ''}
+
+ )}
+
+
+ );
+ },
+ })),
+ // 操作列
+ {
+ title: '',
+ key: 'action',
+ width: type === 'metrics' ? 40 : 80,
+ fixed: 'right',
+ render: (_: unknown, record: TableRowData) => {
+ if (record.isSection) return null;
+
+ if (type === 'metrics') {
+ return (
+ {
+ e.stopPropagation();
+ showMetricChart(record.name, record.key, data, record.path);
+ }}
+ />
+ );
+ }
+
+ return (
+
+ );
+ },
+ },
+ ];
+
+ return cols;
+ }, [type, displayData, data, showMetricChart, categoryTitle, isGrowthCategory]);
+
+ return (
+
+
+
+ {
+ if (record.isSection) return 'section-header';
+ if (record.isTotal) return 'total-row';
+ if (record.isSubtotal) return 'subtotal-row';
+ return '';
+ }}
+ onRow={(record) => ({
+ onClick: () => {
+ if (!record.isSection) {
+ showMetricChart(record.name, record.key, data, record.path);
+ }
+ },
+ style: { cursor: record.isSection ? 'default' : 'pointer' },
+ })}
+ locale={{ emptyText: '暂无数据' }}
+ />
+
+
+ );
+};
+
+export const UnifiedFinancialTable = memo(UnifiedFinancialTableInner);
+export default UnifiedFinancialTable;
diff --git a/src/views/Company/components/FinancialPanorama/components/index.ts b/src/views/Company/components/FinancialPanorama/components/index.ts
index e2d002c1..95e50c44 100644
--- a/src/views/Company/components/FinancialPanorama/components/index.ts
+++ b/src/views/Company/components/FinancialPanorama/components/index.ts
@@ -17,3 +17,4 @@ export { StockComparison } from './StockComparison';
export { ComparisonAnalysis } from './ComparisonAnalysis';
export { MetricChartModal } from './MetricChartModal';
export type { MetricChartModalProps } from './MetricChartModal';
+export { FinancialPanoramaSkeleton } from './FinancialPanoramaSkeleton';
diff --git a/src/views/Company/components/FinancialPanorama/index.tsx b/src/views/Company/components/FinancialPanorama/index.tsx
index a9a7f349..1ab72aa6 100644
--- a/src/views/Company/components/FinancialPanorama/index.tsx
+++ b/src/views/Company/components/FinancialPanorama/index.tsx
@@ -30,7 +30,6 @@ import {
// 通用组件
import SubTabContainer, { type SubTabConfig } from '@components/SubTabContainer';
-import LoadingState from '../LoadingState';
// 内部模块导入
import { useFinancialData, type DataTypeKey } from './hooks';
@@ -42,6 +41,7 @@ import {
MainBusinessAnalysis,
ComparisonAnalysis,
MetricChartModal,
+ FinancialPanoramaSkeleton,
} from './components';
import {
BalanceSheetTab,
@@ -172,26 +172,31 @@ const FinancialPanorama: React.FC = ({ stockCode: propSt
]
);
+ // 初始加载显示骨架屏
+ if (loading && !stockInfo) {
+ return (
+
+
+
+ );
+ }
+
return (
{/* 财务全景面板(三列布局:成长能力、盈利与回报、风险与运营) */}
- {loading ? (
-
- ) : (
-
- )}
+
{/* 营收与利润趋势 */}
- {!loading && comparison && comparison.length > 0 && (
+ {comparison && comparison.length > 0 && (
)}
{/* 主营业务 */}
- {!loading && stockInfo && (
+ {stockInfo && (
主营业务
diff --git a/src/views/Company/components/FinancialPanorama/tabs/UnifiedTabs.tsx b/src/views/Company/components/FinancialPanorama/tabs/UnifiedTabs.tsx
new file mode 100644
index 00000000..6a046f02
--- /dev/null
+++ b/src/views/Company/components/FinancialPanorama/tabs/UnifiedTabs.tsx
@@ -0,0 +1,264 @@
+/**
+ * 统一的财务 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(({ financialMetrics, loading, showMetricChart }) => {
+ const category = FINANCIAL_METRICS_CATEGORIES.profitability;
+ return (
+
+ );
+});
+ProfitabilityTab.displayName = 'ProfitabilityTab';
+
+/** 每股指标 Tab */
+export const PerShareTab = memo(({ financialMetrics, loading, showMetricChart }) => {
+ const category = FINANCIAL_METRICS_CATEGORIES.perShare;
+ return (
+
+ );
+});
+PerShareTab.displayName = 'PerShareTab';
+
+/** 成长能力 Tab */
+export const GrowthTab = memo(({ financialMetrics, loading, showMetricChart }) => {
+ const category = FINANCIAL_METRICS_CATEGORIES.growth;
+ return (
+
+ );
+});
+GrowthTab.displayName = 'GrowthTab';
+
+/** 运营效率 Tab */
+export const OperationalTab = memo(({ financialMetrics, loading, showMetricChart }) => {
+ const category = FINANCIAL_METRICS_CATEGORIES.operational;
+ return (
+
+ );
+});
+OperationalTab.displayName = 'OperationalTab';
+
+/** 偿债能力 Tab */
+export const SolvencyTab = memo(({ financialMetrics, loading, showMetricChart }) => {
+ const category = FINANCIAL_METRICS_CATEGORIES.solvency;
+ return (
+
+ );
+});
+SolvencyTab.displayName = 'SolvencyTab';
+
+/** 费用率 Tab */
+export const ExpenseTab = memo(({ financialMetrics, loading, showMetricChart }) => {
+ const category = FINANCIAL_METRICS_CATEGORIES.expense;
+ return (
+
+ );
+});
+ExpenseTab.displayName = 'ExpenseTab';
+
+/** 现金流指标 Tab */
+export const CashflowMetricsTab = memo(({ financialMetrics, loading, showMetricChart }) => {
+ const category = FINANCIAL_METRICS_CATEGORIES.cashflow;
+ return (
+
+ );
+});
+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(({ balanceSheet, loading, showMetricChart }) => (
+
+));
+BalanceSheetTab.displayName = 'BalanceSheetTab';
+
+/** 利润表 Tab */
+export const IncomeStatementTab = memo(({ incomeStatement, loading, showMetricChart }) => (
+
+));
+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(({ cashflow, loading, showMetricChart }) => (
+
+));
+CashflowTab.displayName = 'CashflowTab';
diff --git a/src/views/Company/components/StockQuoteCard/hooks/useStockQuoteData.ts b/src/views/Company/components/StockQuoteCard/hooks/useStockQuoteData.ts
index 4de99506..5f6f943e 100644
--- a/src/views/Company/components/StockQuoteCard/hooks/useStockQuoteData.ts
+++ b/src/views/Company/components/StockQuoteCard/hooks/useStockQuoteData.ts
@@ -126,6 +126,10 @@ export const useStockQuoteData = (stockCode?: string): UseStockQuoteDataResult =
return;
}
+ // 立即清空旧数据,触发骨架屏显示
+ setQuoteData(null);
+ setBasicInfo(null);
+
const controller = new AbortController();
let isCancelled = false;