diff --git a/src/components/SubTabContainer/index.tsx b/src/components/SubTabContainer/index.tsx index d48cfb24..e077358d 100644 --- a/src/components/SubTabContainer/index.tsx +++ b/src/components/SubTabContainer/index.tsx @@ -45,6 +45,8 @@ export interface SubTabConfig { name: string; icon?: IconType | ComponentType; component?: ComponentType; + /** 自定义 Suspense fallback(如骨架屏) */ + fallback?: React.ReactNode; } /** @@ -314,14 +316,16 @@ const SubTabContainer: React.FC = memo(({ {shouldRender && Component ? ( - - + tab.fallback || ( +
+ +
+ ) } > diff --git a/src/views/Company/components/FinancialPanorama/components/BalanceSheetTable.tsx b/src/views/Company/components/FinancialPanorama/components/BalanceSheetTable.tsx deleted file mode 100644 index f9242506..00000000 --- a/src/views/Company/components/FinancialPanorama/components/BalanceSheetTable.tsx +++ /dev/null @@ -1,246 +0,0 @@ -/** - * 资产负债表组件 - Ant Design 黑金主题 - */ - -import React, { useMemo, memo } from 'react'; -import { Box, Text, HStack, Badge as ChakraBadge, Button } from '@chakra-ui/react'; -import { Table, ConfigProvider, Tooltip } from 'antd'; -import type { ColumnsType } from 'antd/es/table'; -import { formatUtils } from '@services/financialService'; -import { - CURRENT_ASSETS_METRICS, - NON_CURRENT_ASSETS_METRICS, - TOTAL_ASSETS_METRICS, - CURRENT_LIABILITIES_METRICS, - NON_CURRENT_LIABILITIES_METRICS, - TOTAL_LIABILITIES_METRICS, - EQUITY_METRICS, -} from '../constants'; -import { getValueByPath, BLACK_GOLD_TABLE_THEME, getTableStyles, calculateYoY } from '../utils'; -import type { BalanceSheetTableProps, MetricConfig } from '../types'; - -const TABLE_CLASS_NAME = 'balance-sheet-table'; -const tableStyles = getTableStyles(TABLE_CLASS_NAME); - -// 表格行数据类型 -interface TableRowData { - key: string; - name: string; - path: string; - isCore?: boolean; - isTotal?: boolean; - isSection?: boolean; - indent?: number; - [period: string]: unknown; -} - -const BalanceSheetTableInner: React.FC = ({ - data, - showMetricChart, - calculateYoYChange, - positiveColor = 'red.500', - negativeColor = 'green.500', -}) => { - // 数组安全检查 - if (!Array.isArray(data) || data.length === 0) { - return ( - - 暂无资产负债表数据 - - ); - } - - const maxColumns = Math.min(data.length, 6); - const displayData = data.slice(0, maxColumns); - - // 所有分类配置 - const allSections = [ - CURRENT_ASSETS_METRICS, - NON_CURRENT_ASSETS_METRICS, - TOTAL_ASSETS_METRICS, - CURRENT_LIABILITIES_METRICS, - NON_CURRENT_LIABILITIES_METRICS, - TOTAL_LIABILITIES_METRICS, - EQUITY_METRICS, - ]; - - // 构建表格数据 - const tableData = useMemo(() => { - const rows: TableRowData[] = []; - - allSections.forEach((section) => { - // 添加分组标题行(汇总行不显示标题) - if (!['资产总计', '负债合计'].includes(section.title)) { - rows.push({ - key: `section-${section.key}`, - name: section.title, - path: '', - isSection: true, - }); - } - - // 添加指标行 - section.metrics.forEach((metric: MetricConfig) => { - const row: TableRowData = { - key: metric.key, - name: metric.name, - path: metric.path, - isCore: metric.isCore, - isTotal: metric.isTotal || ['资产总计', '负债合计'].includes(section.title), - indent: metric.isTotal ? 0 : 1, - }; - - // 添加各期数值 - displayData.forEach((item) => { - const value = getValueByPath(item, metric.path); - row[item.period] = value; - }); - - rows.push(row); - }); - }); - - return rows; - }, [data, displayData]); - - // 计算同比变化(使用共享函数) - 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: '项目', - dataIndex: 'name', - key: 'name', - fixed: 'left', - width: 200, - 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: 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 formattedValue = formatUtils.formatLargeNumber(value, 0); - - return ( - - 数值: {formatUtils.formatLargeNumber(value)} - {yoy !== null && 同比: {yoy.toFixed(2)}%} - - } - > - - - {formattedValue} - - {yoy !== null && Math.abs(yoy) > 30 && !record.isTotal && ( - 0 ? 'positive-change' : 'negative-change'} - > - {yoy > 0 ? '↑' : '↓'}{Math.abs(yoy).toFixed(0)}% - - )} - - - ); - }, - })), - { - title: '', - key: 'action', - width: 80, - fixed: 'right', - render: (_: unknown, record: TableRowData) => { - if (record.isSection) return null; - return ( - - ); - }, - }, - ]; - - return cols; - }, [displayData, data, showMetricChart]); - - return ( - - - - { - if (record.isSection) return 'section-header'; - if (record.isTotal) return 'total-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 BalanceSheetTable = memo(BalanceSheetTableInner); -export default BalanceSheetTable; diff --git a/src/views/Company/components/FinancialPanorama/components/CashflowTable.tsx b/src/views/Company/components/FinancialPanorama/components/CashflowTable.tsx deleted file mode 100644 index 72240315..00000000 --- a/src/views/Company/components/FinancialPanorama/components/CashflowTable.tsx +++ /dev/null @@ -1,199 +0,0 @@ -/** - * 现金流量表组件 - Ant Design 黑金主题 - */ - -import React, { useMemo, memo } from 'react'; -import { Box, Text, HStack, Badge as ChakraBadge, Button } from '@chakra-ui/react'; -import { Table, ConfigProvider, Tooltip } from 'antd'; -import type { ColumnsType } from 'antd/es/table'; -import { formatUtils } from '@services/financialService'; -import { CASHFLOW_METRICS } from '../constants'; -import { getValueByPath, BLACK_GOLD_TABLE_THEME, getTableStyles, calculateYoY } from '../utils'; -import type { CashflowTableProps } from '../types'; - -const TABLE_CLASS_NAME = 'cashflow-table'; -const tableStyles = getTableStyles(TABLE_CLASS_NAME) + ` - .${TABLE_CLASS_NAME} .positive-value { - color: #E53E3E; - } - .${TABLE_CLASS_NAME} .negative-value { - color: #48BB78; - } -`; - -// 核心指标 -const CORE_METRICS = ['operating_net', 'free_cash_flow']; - -// 表格行数据类型 -interface TableRowData { - key: string; - name: string; - path: string; - isCore?: boolean; - [period: string]: unknown; -} - -const CashflowTableInner: React.FC = ({ - data, - showMetricChart, - calculateYoYChange, - positiveColor = 'red.500', - negativeColor = 'green.500', -}) => { - // 数组安全检查 - if (!Array.isArray(data) || data.length === 0) { - return ( - - 暂无现金流量表数据 - - ); - } - - const maxColumns = Math.min(data.length, 8); - const displayData = data.slice(0, maxColumns); - - // 构建表格数据 - const tableData = useMemo(() => { - return CASHFLOW_METRICS.map((metric) => { - const row: TableRowData = { - key: metric.key, - name: metric.name, - path: metric.path, - isCore: CORE_METRICS.includes(metric.key), - }; - - // 添加各期数值 - displayData.forEach((item) => { - const value = getValueByPath(item, metric.path); - row[item.period] = value; - }); - - return row; - }); - }, [data, displayData]); - - // 计算同比变化(使用共享函数) - 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: '项目', - dataIndex: 'name', - key: 'name', - fixed: 'left', - width: 180, - render: (name: string, record: TableRowData) => ( - - {name} - {record.isCore && ( - - 核心 - - )} - - ), - }, - ...displayData.map((item) => ({ - title: ( - - {formatUtils.getReportType(item.period)} - {item.period.substring(0, 10)} - - ), - dataIndex: item.period, - key: item.period, - width: 110, - align: 'right' as const, - render: (value: number | undefined, record: TableRowData) => { - const yoy = calcYoY(value, item.period, record.path); - const formattedValue = formatUtils.formatLargeNumber(value, 1); - const isNegative = value !== undefined && value < 0; - - return ( - - 数值: {formatUtils.formatLargeNumber(value)} - {yoy !== null && 同比: {yoy.toFixed(2)}%} - - } - > - - - {formattedValue} - - {yoy !== null && Math.abs(yoy) > 50 && ( - 0 ? 'positive-change' : 'negative-change'} - > - {yoy > 0 ? '↑' : '↓'} - - )} - - - ); - }, - })), - { - title: '', - key: 'action', - width: 80, - fixed: 'right', - render: (_: unknown, record: TableRowData) => ( - - ), - }, - ]; - - return cols; - }, [displayData, data, showMetricChart]); - - return ( - - - -
({ - onClick: () => { - showMetricChart(record.name, record.key, data, record.path); - }, - style: { cursor: 'pointer' }, - })} - locale={{ emptyText: '暂无数据' }} - /> - - - ); -}; - -export const CashflowTable = memo(CashflowTableInner); -export default CashflowTable; diff --git a/src/views/Company/components/FinancialPanorama/components/FinancialMetricsTable.tsx b/src/views/Company/components/FinancialPanorama/components/FinancialMetricsTable.tsx deleted file mode 100644 index ef90a856..00000000 --- a/src/views/Company/components/FinancialPanorama/components/FinancialMetricsTable.tsx +++ /dev/null @@ -1,360 +0,0 @@ -/** - * 财务指标表格组件 - Ant Design 黑金主题 - */ - -import React, { useState, useMemo } from 'react'; -import { Box, Text, HStack, Badge as ChakraBadge, SimpleGrid, Card, CardBody, CardHeader, Heading, Button } 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 { FINANCIAL_METRICS_CATEGORIES } from '../constants'; -import { getValueByPath, isNegativeIndicator } from '../utils'; -import type { FinancialMetricsTableProps } from '../types'; - -type CategoryKey = keyof typeof FINANCIAL_METRICS_CATEGORIES; - -// Ant Design 黑金主题配置 -const BLACK_GOLD_THEME = { - token: { - colorBgContainer: 'transparent', - colorText: '#E2E8F0', - colorTextHeading: '#D4AF37', - colorBorderSecondary: 'rgba(212, 175, 55, 0.2)', - }, - components: { - Table: { - headerBg: 'rgba(26, 32, 44, 0.8)', - headerColor: '#D4AF37', - rowHoverBg: 'rgba(212, 175, 55, 0.1)', - borderColor: 'rgba(212, 175, 55, 0.15)', - cellPaddingBlock: 8, - cellPaddingInline: 12, - }, - }, -}; - -// 黑金主题CSS -const tableStyles = ` - .financial-metrics-table .ant-table { - background: transparent !important; - } - .financial-metrics-table .ant-table-thead > tr > th { - background: rgba(26, 32, 44, 0.8) !important; - color: #D4AF37 !important; - border-bottom: 1px solid rgba(212, 175, 55, 0.3) !important; - font-weight: 600; - font-size: 13px; - } - .financial-metrics-table .ant-table-tbody > tr > td { - border-bottom: 1px solid rgba(212, 175, 55, 0.1) !important; - color: #E2E8F0; - font-size: 12px; - } - .financial-metrics-table .ant-table-tbody > tr:hover > td { - background: rgba(212, 175, 55, 0.08) !important; - } - .financial-metrics-table .ant-table-cell-fix-left, - .financial-metrics-table .ant-table-cell-fix-right { - background: #1A202C !important; - } - .financial-metrics-table .ant-table-tbody > tr:hover .ant-table-cell-fix-left, - .financial-metrics-table .ant-table-tbody > tr:hover .ant-table-cell-fix-right { - background: rgba(26, 32, 44, 0.95) !important; - } - .financial-metrics-table .positive-change { - color: #E53E3E; - } - .financial-metrics-table .negative-change { - color: #48BB78; - } - .financial-metrics-table .positive-value { - color: #E53E3E; - } - .financial-metrics-table .negative-value { - color: #48BB78; - } - .financial-metrics-table .ant-table-placeholder { - background: transparent !important; - } - .financial-metrics-table .ant-empty-description { - color: #A0AEC0; - } -`; - -// 表格行数据类型 -interface TableRowData { - key: string; - name: string; - path: string; - isCore?: boolean; - [period: string]: unknown; -} - -export const FinancialMetricsTable: React.FC = ({ - data, - showMetricChart, - calculateYoYChange, -}) => { - const [selectedCategory, setSelectedCategory] = useState('profitability'); - - // 数组安全检查 - if (!Array.isArray(data) || data.length === 0) { - return ( - - 暂无财务指标数据 - - ); - } - - const maxColumns = Math.min(data.length, 6); - const displayData = data.slice(0, maxColumns); - const currentCategory = FINANCIAL_METRICS_CATEGORIES[selectedCategory]; - - // 构建表格数据 - const tableData = useMemo(() => { - return currentCategory.metrics.map((metric) => { - const row: TableRowData = { - key: metric.key, - name: metric.name, - path: metric.path, - isCore: metric.isCore, - }; - - // 添加各期数值 - displayData.forEach((item) => { - const value = getValueByPath(item, metric.path); - row[item.period] = value; - }); - - return row; - }); - }, [data, displayData, currentCategory]); - - // 计算同比变化 - const calculateYoY = ( - currentValue: number | undefined, - currentPeriod: string, - path: string - ): number | null => { - if (currentValue === undefined || currentValue === null) return null; - - const currentDate = new Date(currentPeriod); - const lastYearPeriod = data.find((item) => { - const date = new Date(item.period); - return ( - date.getFullYear() === currentDate.getFullYear() - 1 && - date.getMonth() === currentDate.getMonth() - ); - }); - - if (!lastYearPeriod) return null; - - const lastYearValue = getValueByPath(lastYearPeriod, path); - if (lastYearValue === undefined || lastYearValue === 0) return null; - - return ((currentValue - lastYearValue) / Math.abs(lastYearValue)) * 100; - }; - - // 构建列定义 - const columns: ColumnsType = useMemo(() => { - const cols: ColumnsType = [ - { - title: currentCategory.title, - dataIndex: 'name', - key: 'name', - fixed: 'left', - width: 200, - render: (name: string, record: TableRowData) => ( - - {name} - {record.isCore && ( - - 核心 - - )} - - ), - }, - ...displayData.map((item) => ({ - title: ( - - {formatUtils.getReportType(item.period)} - {item.period.substring(0, 10)} - - ), - dataIndex: item.period, - key: item.period, - width: 100, - align: 'right' as const, - render: (value: number | undefined, record: TableRowData) => { - const yoy = calculateYoY(value, item.period, record.path); - const isNegative = isNegativeIndicator(record.key); - - // 对于负向指标,增加是坏事(绿色),减少是好事(红色) - const changeColor = isNegative - ? (yoy && yoy > 0 ? 'negative-change' : 'positive-change') - : (yoy && yoy > 0 ? 'positive-change' : 'negative-change'); - - // 成长能力指标特殊处理:正值红色,负值绿色 - const valueColor = selectedCategory === 'growth' - ? (value !== undefined && value > 0 ? 'positive-value' : value !== undefined && value < 0 ? 'negative-value' : '') - : ''; - - return ( - - {record.name}: {value?.toFixed(2) || '-'} - {yoy !== null && 同比: {yoy.toFixed(2)}%} - - } - > - - - {value?.toFixed(2) || '-'} - - {yoy !== null && Math.abs(yoy) > 20 && value !== undefined && Math.abs(value) > 0.01 && ( - - {yoy > 0 ? '↑' : '↓'} - - )} - - - ); - }, - })), - { - title: '', - key: 'action', - width: 40, - fixed: 'right', - render: (_: unknown, record: TableRowData) => ( - { - e.stopPropagation(); - showMetricChart(record.name, record.key, data, record.path); - }} - /> - ), - }, - ]; - - return cols; - }, [displayData, data, showMetricChart, currentCategory, selectedCategory]); - - return ( - - {/* 分类选择器 */} - - {(Object.entries(FINANCIAL_METRICS_CATEGORIES) as [CategoryKey, typeof currentCategory][]).map( - ([key, category]) => ( - - ) - )} - - - {/* 指标表格 */} - - - -
({ - onClick: () => { - showMetricChart(record.name, record.key, data, record.path); - }, - style: { cursor: 'pointer' }, - })} - locale={{ emptyText: '暂无数据' }} - /> - - - - {/* 关键指标快速对比 */} - {data[0] && ( - - - 关键指标速览 - - - - {[ - { - label: 'ROE', - value: getValueByPath(data[0], 'profitability.roe'), - format: 'percent', - }, - { - label: '毛利率', - value: getValueByPath(data[0], 'profitability.gross_margin'), - format: 'percent', - }, - { - label: '净利率', - value: getValueByPath(data[0], 'profitability.net_profit_margin'), - format: 'percent', - }, - { - label: '流动比率', - value: getValueByPath(data[0], 'solvency.current_ratio'), - format: 'decimal', - }, - { - label: '资产负债率', - value: getValueByPath(data[0], 'solvency.asset_liability_ratio'), - format: 'percent', - }, - { - label: '研发费用率', - value: getValueByPath(data[0], 'expense_ratios.rd_expense_ratio'), - format: 'percent', - }, - ].map((item, idx) => ( - - - {item.label} - - - {item.format === 'percent' - ? formatUtils.formatPercent(item.value) - : item.value?.toFixed(2) || '-'} - - - ))} - - - - )} - - ); -}; - -export default FinancialMetricsTable; diff --git a/src/views/Company/components/FinancialPanorama/components/FinancialTable.tsx b/src/views/Company/components/FinancialPanorama/components/FinancialTable.tsx deleted file mode 100644 index bfc1a937..00000000 --- a/src/views/Company/components/FinancialPanorama/components/FinancialTable.tsx +++ /dev/null @@ -1,328 +0,0 @@ -/** - * 通用财务表格组件 - Ant Design 黑金主题 - */ - -import React from 'react'; -import { Box, Text, HStack, Badge as ChakraBadge } from '@chakra-ui/react'; -import { Table, ConfigProvider, Tooltip, Badge } from 'antd'; -import type { ColumnsType } from 'antd/es/table'; -import { Eye } from 'lucide-react'; -import { formatUtils } from '@services/financialService'; - -// Ant Design 表格黑金主题配置 -export const FINANCIAL_TABLE_THEME = { - token: { - colorBgContainer: 'transparent', - colorText: '#E2E8F0', - colorTextHeading: '#D4AF37', - colorBorderSecondary: 'rgba(212, 175, 55, 0.2)', - }, - components: { - Table: { - headerBg: 'rgba(26, 32, 44, 0.8)', - headerColor: '#D4AF37', - rowHoverBg: 'rgba(212, 175, 55, 0.1)', - borderColor: 'rgba(212, 175, 55, 0.15)', - cellPaddingBlock: 8, - cellPaddingInline: 12, - }, - }, -}; - -// 通用样式 -export const tableStyles = ` - .financial-table .ant-table { - background: transparent !important; - } - .financial-table .ant-table-thead > tr > th { - background: rgba(26, 32, 44, 0.8) !important; - color: #D4AF37 !important; - border-bottom: 1px solid rgba(212, 175, 55, 0.3) !important; - font-weight: 600; - font-size: 13px; - } - .financial-table .ant-table-tbody > tr > td { - border-bottom: 1px solid rgba(212, 175, 55, 0.1) !important; - color: #E2E8F0; - font-size: 12px; - } - .financial-table .ant-table-tbody > tr:hover > td { - background: rgba(212, 175, 55, 0.08) !important; - } - .financial-table .ant-table-tbody > tr.total-row > td { - background: rgba(212, 175, 55, 0.15) !important; - font-weight: 600; - } - .financial-table .ant-table-tbody > tr.section-header > td { - background: rgba(212, 175, 55, 0.08) !important; - font-weight: 600; - color: #D4AF37; - } - .financial-table .ant-table-cell-fix-left, - .financial-table .ant-table-cell-fix-right { - background: #1A202C !important; - } - .financial-table .ant-table-tbody > tr:hover .ant-table-cell-fix-left, - .financial-table .ant-table-tbody > tr:hover .ant-table-cell-fix-right { - background: rgba(26, 32, 44, 0.95) !important; - } - .financial-table .positive-change { - color: #E53E3E; - } - .financial-table .negative-change { - color: #48BB78; - } - .financial-table .ant-table-placeholder { - background: transparent !important; - } - .financial-table .ant-empty-description { - color: #A0AEC0; - } -`; - -// 指标类型 -export interface MetricConfig { - name: string; - key: string; - path: string; - isCore?: boolean; - isTotal?: boolean; - isSubtotal?: boolean; -} - -export interface MetricSectionConfig { - title: string; - key: string; - metrics: MetricConfig[]; -} - -// 表格行数据类型 -export interface FinancialTableRow { - key: string; - name: string; - path: string; - isCore?: boolean; - isTotal?: boolean; - isSection?: boolean; - indent?: number; - [period: string]: unknown; -} - -// 组件 Props -export interface FinancialTableProps { - data: Array<{ period: string; [key: string]: unknown }>; - sections: MetricSectionConfig[]; - onRowClick?: (name: string, key: string, path: string) => void; - loading?: boolean; - maxColumns?: number; -} - -// 获取嵌套路径的值 -const getValueByPath = (obj: Record, path: string): number | undefined => { - const keys = path.split('.'); - let value: unknown = obj; - for (const key of keys) { - if (value && typeof value === 'object') { - value = (value as Record)[key]; - } else { - return undefined; - } - } - return typeof value === 'number' ? value : undefined; -}; - -// 计算同比变化 -const calculateYoY = ( - currentValue: number | undefined, - currentPeriod: string, - data: Array<{ period: string; [key: string]: unknown }>, - path: string -): number | null => { - if (currentValue === undefined || currentValue === null) return null; - - const currentDate = new Date(currentPeriod); - const lastYearPeriod = data.find((item) => { - const date = new Date(item.period); - return ( - date.getFullYear() === currentDate.getFullYear() - 1 && - date.getMonth() === currentDate.getMonth() - ); - }); - - if (!lastYearPeriod) return null; - - const lastYearValue = getValueByPath(lastYearPeriod as Record, path); - if (lastYearValue === undefined || lastYearValue === 0) return null; - - return ((currentValue - lastYearValue) / Math.abs(lastYearValue)) * 100; -}; - -const FinancialTable: React.FC = ({ - data, - sections, - onRowClick, - loading = false, - maxColumns = 6, -}) => { - // 限制显示列数 - const displayData = data.slice(0, maxColumns); - - // 构建表格数据 - const tableData: FinancialTableRow[] = []; - - sections.forEach((section) => { - // 添加分组标题行(除了汇总行) - if (!section.title.includes('总计') && !section.title.includes('合计')) { - tableData.push({ - key: `section-${section.key}`, - name: section.title, - path: '', - isSection: true, - }); - } - - // 添加指标行 - section.metrics.forEach((metric) => { - const row: FinancialTableRow = { - key: metric.key, - name: metric.name, - path: metric.path, - isCore: metric.isCore, - isTotal: metric.isTotal || section.title.includes('总计') || section.title.includes('合计'), - indent: metric.isTotal ? 0 : 1, - }; - - // 添加各期数值 - displayData.forEach((item) => { - const value = getValueByPath(item as Record, metric.path); - row[item.period] = value; - }); - - tableData.push(row); - }); - }); - - // 构建列定义 - const columns: ColumnsType = [ - { - title: '项目', - dataIndex: 'name', - key: 'name', - fixed: 'left', - width: 180, - render: (name: string, record: FinancialTableRow) => { - 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: 110, - align: 'right' as const, - render: (value: number | undefined, record: FinancialTableRow) => { - if (record.isSection) return null; - - const yoy = calculateYoY(value, item.period, data, record.path); - const formattedValue = formatUtils.formatLargeNumber(value, 0); - - return ( - - 数值: {formatUtils.formatLargeNumber(value)} - {yoy !== null && 同比: {yoy.toFixed(2)}%} - - } - > - - - {formattedValue} - - {yoy !== null && Math.abs(yoy) > 30 && !record.isTotal && ( - 0 ? 'positive-change' : 'negative-change'} - > - {yoy > 0 ? '↑' : '↓'}{Math.abs(yoy).toFixed(0)}% - - )} - - - ); - }, - })), - { - title: '', - key: 'action', - width: 40, - fixed: 'right', - render: (_: unknown, record: FinancialTableRow) => { - if (record.isSection) return null; - return ( - { - e.stopPropagation(); - onRowClick?.(record.name, record.key, record.path); - }} - /> - ); - }, - }, - ]; - - return ( - - - -
{ - if (record.isSection) return 'section-header'; - if (record.isTotal) return 'total-row'; - return ''; - }} - onRow={(record) => ({ - onClick: () => { - if (!record.isSection && onRowClick) { - onRowClick(record.name, record.key, record.path); - } - }, - style: { cursor: record.isSection ? 'default' : 'pointer' }, - })} - locale={{ emptyText: '暂无数据' }} - /> - - - ); -}; - -export default FinancialTable; diff --git a/src/views/Company/components/FinancialPanorama/components/IncomeStatementTable.tsx b/src/views/Company/components/FinancialPanorama/components/IncomeStatementTable.tsx deleted file mode 100644 index 3ddd406d..00000000 --- a/src/views/Company/components/FinancialPanorama/components/IncomeStatementTable.tsx +++ /dev/null @@ -1,247 +0,0 @@ -/** - * 利润表组件 - Ant Design 黑金主题 - */ - -import React, { useMemo, memo } from 'react'; -import { Box, Text, HStack, Badge as ChakraBadge, Button } from '@chakra-ui/react'; -import { Table, ConfigProvider, Tooltip } from 'antd'; -import type { ColumnsType } from 'antd/es/table'; -import { formatUtils } from '@services/financialService'; -import { INCOME_STATEMENT_SECTIONS } from '../constants'; -import { getValueByPath, isNegativeIndicator, BLACK_GOLD_TABLE_THEME, getTableStyles, calculateYoY } from '../utils'; -import type { IncomeStatementTableProps, MetricConfig } from '../types'; - -const TABLE_CLASS_NAME = 'income-statement-table'; -const tableStyles = getTableStyles(TABLE_CLASS_NAME) + ` - .${TABLE_CLASS_NAME} .ant-table-tbody > tr.subtotal-row > td { - background: rgba(212, 175, 55, 0.1) !important; - font-weight: 500; - } - .${TABLE_CLASS_NAME} .negative-value { - color: #E53E3E; - } -`; - -// 表格行数据类型 -interface TableRowData { - key: string; - name: string; - path: string; - isCore?: boolean; - isTotal?: boolean; - isSubtotal?: boolean; - isSection?: boolean; - indent?: number; - [period: string]: unknown; -} - -const IncomeStatementTableInner: React.FC = ({ - data, - showMetricChart, - calculateYoYChange, - positiveColor = 'red.500', - negativeColor = 'green.500', -}) => { - // 数组安全检查 - if (!Array.isArray(data) || data.length === 0) { - return ( - - 暂无利润表数据 - - ); - } - - const maxColumns = Math.min(data.length, 6); - const displayData = data.slice(0, maxColumns); - - // 构建表格数据 - const tableData = useMemo(() => { - const rows: TableRowData[] = []; - - INCOME_STATEMENT_SECTIONS.forEach((section) => { - // 添加分组标题行 - rows.push({ - key: `section-${section.key}`, - name: section.title, - path: '', - isSection: true, - }); - - // 添加指标行 - section.metrics.forEach((metric: MetricConfig) => { - const row: TableRowData = { - key: metric.key, - name: metric.name, - path: metric.path, - isCore: metric.isCore, - isTotal: metric.isTotal, - 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; - }, [data, displayData]); - - // 计算同比变化(使用共享函数) - 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: '项目', - dataIndex: 'name', - key: 'name', - fixed: 'left', - width: 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: 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 isEPS = record.key.includes('eps'); - const formattedValue = isEPS ? value?.toFixed(3) : formatUtils.formatLargeNumber(value, 0); - const isNegative = value !== undefined && value < 0; - - // 成本费用类负向指标,增长用绿色,减少用红色 - const isCostItem = isNegativeIndicator(record.key); - const changeColor = isCostItem - ? (yoy && yoy > 0 ? 'negative-change' : 'positive-change') - : (yoy && yoy > 0 ? 'positive-change' : 'negative-change'); - - return ( - - 数值: {isEPS ? value?.toFixed(3) : formatUtils.formatLargeNumber(value)} - {yoy !== null && 同比: {yoy.toFixed(2)}%} - - } - > - - - {formattedValue} - - {yoy !== null && Math.abs(yoy) > 30 && !record.isTotal && ( - - {yoy > 0 ? '↑' : '↓'}{Math.abs(yoy).toFixed(0)}% - - )} - - - ); - }, - })), - { - title: '', - key: 'action', - width: 80, - fixed: 'right', - render: (_: unknown, record: TableRowData) => { - if (record.isSection) return null; - return ( - - ); - }, - }, - ]; - - return cols; - }, [displayData, data, showMetricChart]); - - 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 IncomeStatementTable = memo(IncomeStatementTableInner); -export default IncomeStatementTable; diff --git a/src/views/Company/components/FinancialPanorama/components/index.ts b/src/views/Company/components/FinancialPanorama/components/index.ts index 95e50c44..6f2270f5 100644 --- a/src/views/Company/components/FinancialPanorama/components/index.ts +++ b/src/views/Company/components/FinancialPanorama/components/index.ts @@ -4,13 +4,8 @@ export { PeriodSelector } from './PeriodSelector'; export { FinancialOverviewPanel } from './FinancialOverviewPanel'; -// 保留旧组件导出(向后兼容) export { KeyMetricsOverview } from './KeyMetricsOverview'; export { StockInfoHeader } from './StockInfoHeader'; -export { BalanceSheetTable } from './BalanceSheetTable'; -export { IncomeStatementTable } from './IncomeStatementTable'; -export { CashflowTable } from './CashflowTable'; -export { FinancialMetricsTable } from './FinancialMetricsTable'; export { MainBusinessAnalysis } from './MainBusinessAnalysis'; export { IndustryRankingView } from './IndustryRankingView'; export { StockComparison } from './StockComparison'; @@ -18,3 +13,6 @@ export { ComparisonAnalysis } from './ComparisonAnalysis'; export { MetricChartModal } from './MetricChartModal'; export type { MetricChartModalProps } from './MetricChartModal'; export { FinancialPanoramaSkeleton } from './FinancialPanoramaSkeleton'; +// 统一财务表格组件 +export { UnifiedFinancialTable } from './UnifiedFinancialTable'; +export type { UnifiedFinancialTableProps, TableType, FinancialDataItem } from './UnifiedFinancialTable'; diff --git a/src/views/Company/components/FinancialPanorama/tabs/BalanceSheetTab.tsx b/src/views/Company/components/FinancialPanorama/tabs/BalanceSheetTab.tsx deleted file mode 100644 index 07a9c9ac..00000000 --- a/src/views/Company/components/FinancialPanorama/tabs/BalanceSheetTab.tsx +++ /dev/null @@ -1,60 +0,0 @@ -/** - * 资产负债表 Tab - */ - -import React, { memo } from 'react'; -import { Box, Spinner, Center } from '@chakra-ui/react'; -import { BalanceSheetTable } from '../components'; -import type { BalanceSheetData } from '../types'; - -export interface BalanceSheetTabProps { - balanceSheet: BalanceSheetData[]; - loading?: boolean; - 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; -} - -const BalanceSheetTabInner: React.FC = ({ - balanceSheet, - loading, - showMetricChart, - calculateYoYChange, - getCellBackground, - positiveColor, - negativeColor, - bgColor, - hoverBg, -}) => { - // 加载中状态 - if (loading && (!Array.isArray(balanceSheet) || balanceSheet.length === 0)) { - return ( -
- -
- ); - } - - const tableProps = { - showMetricChart, - calculateYoYChange, - getCellBackground, - positiveColor, - negativeColor, - bgColor, - hoverBg, - }; - - return ( - - - - ); -}; - -const BalanceSheetTab = memo(BalanceSheetTabInner); -export default BalanceSheetTab; diff --git a/src/views/Company/components/FinancialPanorama/tabs/CashflowTab.tsx b/src/views/Company/components/FinancialPanorama/tabs/CashflowTab.tsx deleted file mode 100644 index 3895e311..00000000 --- a/src/views/Company/components/FinancialPanorama/tabs/CashflowTab.tsx +++ /dev/null @@ -1,60 +0,0 @@ -/** - * 现金流量表 Tab - */ - -import React, { memo } from 'react'; -import { Box, Spinner, Center } from '@chakra-ui/react'; -import { CashflowTable } from '../components'; -import type { CashflowData } from '../types'; - -export interface CashflowTabProps { - cashflow: CashflowData[]; - loading?: boolean; - 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; -} - -const CashflowTabInner: React.FC = ({ - cashflow, - loading, - showMetricChart, - calculateYoYChange, - getCellBackground, - positiveColor, - negativeColor, - bgColor, - hoverBg, -}) => { - // 加载中状态 - if (loading && (!Array.isArray(cashflow) || cashflow.length === 0)) { - return ( -
- -
- ); - } - - const tableProps = { - showMetricChart, - calculateYoYChange, - getCellBackground, - positiveColor, - negativeColor, - bgColor, - hoverBg, - }; - - return ( - - - - ); -}; - -const CashflowTab = memo(CashflowTabInner); -export default CashflowTab; diff --git a/src/views/Company/components/FinancialPanorama/tabs/FinancialMetricsTab.tsx b/src/views/Company/components/FinancialPanorama/tabs/FinancialMetricsTab.tsx deleted file mode 100644 index bfb5e393..00000000 --- a/src/views/Company/components/FinancialPanorama/tabs/FinancialMetricsTab.tsx +++ /dev/null @@ -1,58 +0,0 @@ -/** - * 财务指标 Tab - */ - -import React, { memo } from 'react'; -import { Spinner, Center } from '@chakra-ui/react'; -import { FinancialMetricsTable } from '../components'; -import type { FinancialMetricsData } from '../types'; - -export interface FinancialMetricsTabProps { - financialMetrics: FinancialMetricsData[]; - loading?: boolean; - 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; -} - -const FinancialMetricsTabInner: React.FC = ({ - financialMetrics, - loading, - showMetricChart, - calculateYoYChange, - getCellBackground, - positiveColor, - negativeColor, - bgColor, - hoverBg, -}) => { - // 加载中状态 - if (loading && (!Array.isArray(financialMetrics) || financialMetrics.length === 0)) { - return ( -
- -
- ); - } - - const tableProps = { - showMetricChart, - calculateYoYChange, - getCellBackground, - positiveColor, - negativeColor, - bgColor, - hoverBg, - }; - - return ( - - ); -}; - -const FinancialMetricsTab = memo(FinancialMetricsTabInner); -export default FinancialMetricsTab; diff --git a/src/views/Company/components/FinancialPanorama/tabs/IncomeStatementTab.tsx b/src/views/Company/components/FinancialPanorama/tabs/IncomeStatementTab.tsx deleted file mode 100644 index 45b8f3a0..00000000 --- a/src/views/Company/components/FinancialPanorama/tabs/IncomeStatementTab.tsx +++ /dev/null @@ -1,60 +0,0 @@ -/** - * 利润表 Tab - */ - -import React, { memo } from 'react'; -import { Box, Spinner, Center } from '@chakra-ui/react'; -import { IncomeStatementTable } from '../components'; -import type { IncomeStatementData } from '../types'; - -export interface IncomeStatementTabProps { - incomeStatement: IncomeStatementData[]; - loading?: boolean; - 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; -} - -const IncomeStatementTabInner: React.FC = ({ - incomeStatement, - loading, - showMetricChart, - calculateYoYChange, - getCellBackground, - positiveColor, - negativeColor, - bgColor, - hoverBg, -}) => { - // 加载中状态 - if (loading && (!Array.isArray(incomeStatement) || incomeStatement.length === 0)) { - return ( -
- -
- ); - } - - const tableProps = { - showMetricChart, - calculateYoYChange, - getCellBackground, - positiveColor, - negativeColor, - bgColor, - hoverBg, - }; - - return ( - - - - ); -}; - -const IncomeStatementTab = memo(IncomeStatementTabInner); -export default IncomeStatementTab; diff --git a/src/views/Company/components/FinancialPanorama/tabs/MetricsCategoryTab.tsx b/src/views/Company/components/FinancialPanorama/tabs/MetricsCategoryTab.tsx deleted file mode 100644 index 76754789..00000000 --- a/src/views/Company/components/FinancialPanorama/tabs/MetricsCategoryTab.tsx +++ /dev/null @@ -1,269 +0,0 @@ -/** - * 财务指标分类 Tab - Ant Design 黑金主题 - * 接受 categoryKey 显示单个分类的指标表格 - */ - -import React, { useMemo, memo } from 'react'; -import { Box, Text, HStack, Badge as ChakraBadge, 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 { FINANCIAL_METRICS_CATEGORIES } from '../constants'; -import { getValueByPath, isNegativeIndicator, BLACK_GOLD_TABLE_THEME, getTableStyles, calculateYoY } from '../utils'; -import type { FinancialMetricsData } from '../types'; - -type CategoryKey = keyof typeof FINANCIAL_METRICS_CATEGORIES; - -const TABLE_CLASS_NAME = 'metrics-category-table'; -const tableStyles = getTableStyles(TABLE_CLASS_NAME) + ` - .${TABLE_CLASS_NAME} .positive-value { - color: #E53E3E; - } - .${TABLE_CLASS_NAME} .negative-value { - color: #48BB78; - } -`; - -export interface MetricsCategoryTabProps { - categoryKey: CategoryKey; - financialMetrics: FinancialMetricsData[]; - loading?: boolean; - 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; -} - -// 表格行数据类型 -interface TableRowData { - key: string; - name: string; - path: string; - isCore?: boolean; - [period: string]: unknown; -} - -const MetricsCategoryTabInner: React.FC = ({ - categoryKey, - financialMetrics, - loading, - showMetricChart, - calculateYoYChange, -}) => { - // 加载中状态 - if (loading && (!Array.isArray(financialMetrics) || financialMetrics.length === 0)) { - return ( -
- -
- ); - } - - // 数组安全检查 - if (!Array.isArray(financialMetrics) || financialMetrics.length === 0) { - return ( - - 暂无财务指标数据 - - ); - } - - const maxColumns = Math.min(financialMetrics.length, 6); - const displayData = financialMetrics.slice(0, maxColumns); - const category = FINANCIAL_METRICS_CATEGORIES[categoryKey]; - - if (!category) { - return ( - - 未找到指标分类配置 - - ); - } - - // 构建表格数据 - const tableData = useMemo(() => { - return category.metrics.map((metric) => { - const row: TableRowData = { - key: metric.key, - name: metric.name, - path: metric.path, - isCore: metric.isCore, - }; - - // 添加各期数值 - displayData.forEach((item) => { - const value = getValueByPath(item, metric.path); - row[item.period] = value; - }); - - return row; - }); - }, [financialMetrics, displayData, category]); - - // 计算同比变化(使用共享函数) - const calcYoY = ( - currentValue: number | undefined, - currentPeriod: string, - path: string - ): number | null => { - return calculateYoY(financialMetrics, currentValue, currentPeriod, path, getValueByPath); - }; - - // 构建列定义 - const columns: ColumnsType = useMemo(() => { - const cols: ColumnsType = [ - { - title: category.title, - dataIndex: 'name', - key: 'name', - fixed: 'left', - width: 200, - render: (name: string, record: TableRowData) => ( - - {name} - {record.isCore && ( - - 核心 - - )} - - ), - }, - ...displayData.map((item) => ({ - title: ( - - {formatUtils.getReportType(item.period)} - {item.period.substring(0, 10)} - - ), - dataIndex: item.period, - key: item.period, - width: 100, - align: 'right' as const, - render: (value: number | undefined, record: TableRowData) => { - const yoy = calcYoY(value, item.period, record.path); - const isNegative = isNegativeIndicator(record.key); - - // 对于负向指标,增加是坏事(绿色),减少是好事(红色) - const changeColor = isNegative - ? (yoy && yoy > 0 ? 'negative-change' : 'positive-change') - : (yoy && yoy > 0 ? 'positive-change' : 'negative-change'); - - // 成长能力指标特殊处理:正值红色,负值绿色 - const valueColor = categoryKey === 'growth' - ? (value !== undefined && value > 0 ? 'positive-value' : value !== undefined && value < 0 ? 'negative-value' : '') - : ''; - - return ( - - {record.name}: {value?.toFixed(2) || '-'} - {yoy !== null && 同比: {yoy.toFixed(2)}%} - - } - > - - - {value?.toFixed(2) || '-'} - - {yoy !== null && Math.abs(yoy) > 20 && value !== undefined && Math.abs(value) > 0.01 && ( - - {yoy > 0 ? '↑' : '↓'} - - )} - - - ); - }, - })), - { - title: '', - key: 'action', - width: 40, - fixed: 'right', - render: (_: unknown, record: TableRowData) => ( - { - e.stopPropagation(); - showMetricChart(record.name, record.key, financialMetrics, record.path); - }} - /> - ), - }, - ]; - - return cols; - }, [displayData, financialMetrics, showMetricChart, category, categoryKey]); - - return ( - - - - -
({ - onClick: () => { - showMetricChart(record.name, record.key, financialMetrics, record.path); - }, - style: { cursor: 'pointer' }, - })} - locale={{ emptyText: '暂无数据' }} - /> - - - - - ); -}; - -const MetricsCategoryTab = memo(MetricsCategoryTabInner); - -// 为每个分类创建预配置的组件(使用 memo) -export const ProfitabilityTab = memo>((props) => ( - -)); - -export const PerShareTab = memo>((props) => ( - -)); - -export const GrowthTab = memo>((props) => ( - -)); - -export const OperationalTab = memo>((props) => ( - -)); - -export const SolvencyTab = memo>((props) => ( - -)); - -export const ExpenseTab = memo>((props) => ( - -)); - -export const CashflowMetricsTab = memo>((props) => ( - -)); - -export default MetricsCategoryTab; 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..664ddeb7 --- /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, type FinancialDataItem } 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: FinancialDataItem[], 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: FinancialDataItem[], 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: FinancialDataItem[], 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: FinancialDataItem[], 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/FinancialPanorama/tabs/index.ts b/src/views/Company/components/FinancialPanorama/tabs/index.ts index 3d39d62e..fd3171e8 100644 --- a/src/views/Company/components/FinancialPanorama/tabs/index.ts +++ b/src/views/Company/components/FinancialPanorama/tabs/index.ts @@ -1,14 +1,12 @@ /** * Tab 组件统一导出 + * + * 使用 UnifiedTabs 实现的 10 个财务表格 Tab */ -// 三大财务报表 -export { default as BalanceSheetTab } from './BalanceSheetTab'; -export { default as IncomeStatementTab } from './IncomeStatementTab'; -export { default as CashflowTab } from './CashflowTab'; - -// 财务指标分类 tabs +// 统一 Tab 组件导出 export { + // 7 个财务指标 Tab ProfitabilityTab, PerShareTab, GrowthTab, @@ -16,13 +14,20 @@ export { SolvencyTab, ExpenseTab, CashflowMetricsTab, -} from './MetricsCategoryTab'; + // 3 个财务报表 Tab + BalanceSheetTab, + IncomeStatementTab, + CashflowTab, +} from './UnifiedTabs'; -// 旧的综合财务指标 tab(保留兼容) -export { default as FinancialMetricsTab } from './FinancialMetricsTab'; +// 类型导出 +export type { + MetricsTabProps, + BalanceSheetTabProps, + IncomeStatementTabProps, + CashflowTabProps, +} from './UnifiedTabs'; -export type { BalanceSheetTabProps } from './BalanceSheetTab'; -export type { IncomeStatementTabProps } from './IncomeStatementTab'; -export type { CashflowTabProps } from './CashflowTab'; -export type { FinancialMetricsTabProps } from './FinancialMetricsTab'; -export type { MetricsCategoryTabProps } from './MetricsCategoryTab'; +// 兼容旧的类型别名 +export type { MetricsTabProps as MetricsCategoryTabProps } from './UnifiedTabs'; +export type { MetricsTabProps as FinancialMetricsTabProps } from './UnifiedTabs'; diff --git a/src/views/Company/config.ts b/src/views/Company/config.ts index c8153319..3f9c75a2 100644 --- a/src/views/Company/config.ts +++ b/src/views/Company/config.ts @@ -4,10 +4,14 @@ * - Tab 配置 */ -import { lazy } from 'react'; +import React, { lazy } from 'react'; import { Building2, Brain, TrendingUp, Wallet, FileBarChart, Newspaper } from 'lucide-react'; import type { CompanyTheme, TabConfig } from './types'; +// 骨架屏组件(同步导入,用于 Suspense fallback) +import { FinancialPanoramaSkeleton } from './components/FinancialPanorama/components'; +import { ForecastSkeleton } from './components/ForecastReport/components'; + // ============================================ // 黑金主题配置 // ============================================ @@ -89,12 +93,14 @@ export const TAB_CONFIG: TabConfig[] = [ name: '财务全景', icon: Wallet, component: FinancialPanorama, + fallback: React.createElement(FinancialPanoramaSkeleton), }, { key: 'forecast', name: '盈利预测', icon: FileBarChart, component: ForecastReport, + fallback: React.createElement(ForecastSkeleton), }, { key: 'tracking', diff --git a/src/views/Company/types.ts b/src/views/Company/types.ts index 19d36e86..3334f0e9 100644 --- a/src/views/Company/types.ts +++ b/src/views/Company/types.ts @@ -2,7 +2,7 @@ * Company 页面类型定义 */ -import type { ComponentType } from 'react'; +import type { ComponentType, ReactNode } from 'react'; import type { IconType } from 'react-icons'; import type { LucideIcon } from 'lucide-react'; @@ -36,6 +36,8 @@ export interface TabConfig { name: string; icon: LucideIcon | IconType | ComponentType; component: ComponentType; + /** 自定义 Suspense fallback(如骨架屏) */ + fallback?: ReactNode; } export interface TabComponentProps {