diff --git a/src/views/Company/components/FinancialPanorama/components/BalanceSheetTable.tsx b/src/views/Company/components/FinancialPanorama/components/BalanceSheetTable.tsx index ab76d635..5864e70c 100644 --- a/src/views/Company/components/FinancialPanorama/components/BalanceSheetTable.tsx +++ b/src/views/Company/components/FinancialPanorama/components/BalanceSheetTable.tsx @@ -2,7 +2,7 @@ * 资产负债表组件 - Ant Design 黑金主题 */ -import React, { useMemo } from 'react'; +import React, { useMemo, memo } from 'react'; import { Box, Text, HStack, Badge as ChakraBadge } from '@chakra-ui/react'; import { Table, ConfigProvider, Tooltip } from 'antd'; import type { ColumnsType } from 'antd/es/table'; @@ -17,79 +17,11 @@ import { TOTAL_LIABILITIES_METRICS, EQUITY_METRICS, } from '../constants'; -import { getValueByPath } from '../utils'; +import { getValueByPath, BLACK_GOLD_TABLE_THEME, getTableStyles, calculateYoY } from '../utils'; import type { BalanceSheetTableProps, MetricConfig } from '../types'; -// 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 = ` - .balance-sheet-table .ant-table { - background: transparent !important; - } - .balance-sheet-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; - } - .balance-sheet-table .ant-table-tbody > tr > td { - border-bottom: 1px solid rgba(212, 175, 55, 0.1) !important; - color: #E2E8F0; - font-size: 12px; - } - .balance-sheet-table .ant-table-tbody > tr:hover > td { - background: rgba(212, 175, 55, 0.08) !important; - } - .balance-sheet-table .ant-table-tbody > tr.total-row > td { - background: rgba(212, 175, 55, 0.15) !important; - font-weight: 600; - } - .balance-sheet-table .ant-table-tbody > tr.section-header > td { - background: rgba(212, 175, 55, 0.08) !important; - font-weight: 600; - color: #D4AF37; - } - .balance-sheet-table .ant-table-cell-fix-left, - .balance-sheet-table .ant-table-cell-fix-right { - background: #1A202C !important; - } - .balance-sheet-table .ant-table-tbody > tr:hover .ant-table-cell-fix-left, - .balance-sheet-table .ant-table-tbody > tr:hover .ant-table-cell-fix-right { - background: rgba(26, 32, 44, 0.95) !important; - } - .balance-sheet-table .positive-change { - color: #E53E3E; - } - .balance-sheet-table .negative-change { - color: #48BB78; - } - .balance-sheet-table .ant-table-placeholder { - background: transparent !important; - } - .balance-sheet-table .ant-empty-description { - color: #A0AEC0; - } -`; +const TABLE_CLASS_NAME = 'balance-sheet-table'; +const tableStyles = getTableStyles(TABLE_CLASS_NAME); // 表格行数据类型 interface TableRowData { @@ -103,7 +35,7 @@ interface TableRowData { [period: string]: unknown; } -export const BalanceSheetTable: React.FC = ({ +const BalanceSheetTableInner: React.FC = ({ data, showMetricChart, calculateYoYChange, @@ -172,29 +104,13 @@ export const BalanceSheetTable: React.FC = ({ return rows; }, [data, displayData]); - // 计算同比变化 - const calculateYoY = ( + // 计算同比变化(使用共享函数) + const calcYoY = ( 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; + return calculateYoY(data, currentValue, currentPeriod, path, getValueByPath); }; // 构建列定义 @@ -236,7 +152,7 @@ export const BalanceSheetTable: React.FC = ({ render: (value: number | undefined, record: TableRowData) => { if (record.isSection) return null; - const yoy = calculateYoY(value, item.period, record.path); + const yoy = calcYoY(value, item.period, record.path); const formattedValue = formatUtils.formatLargeNumber(value, 0); return ( @@ -296,7 +212,7 @@ export const BalanceSheetTable: React.FC = ({ return ( - + = ({ ); }; +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 index a30e7199..e6a19a61 100644 --- a/src/views/Company/components/FinancialPanorama/components/CashflowTable.tsx +++ b/src/views/Company/components/FinancialPanorama/components/CashflowTable.tsx @@ -2,82 +2,24 @@ * 现金流量表组件 - Ant Design 黑金主题 */ -import React, { useMemo } from 'react'; +import React, { useMemo, memo } from 'react'; import { Box, Text, HStack, Badge as ChakraBadge } 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 { CASHFLOW_METRICS } from '../constants'; -import { getValueByPath } from '../utils'; +import { getValueByPath, BLACK_GOLD_TABLE_THEME, getTableStyles, calculateYoY } from '../utils'; import type { CashflowTableProps } from '../types'; -// 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 = ` - .cashflow-table .ant-table { - background: transparent !important; - } - .cashflow-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; - } - .cashflow-table .ant-table-tbody > tr > td { - border-bottom: 1px solid rgba(212, 175, 55, 0.1) !important; - color: #E2E8F0; - font-size: 12px; - } - .cashflow-table .ant-table-tbody > tr:hover > td { - background: rgba(212, 175, 55, 0.08) !important; - } - .cashflow-table .ant-table-cell-fix-left, - .cashflow-table .ant-table-cell-fix-right { - background: #1A202C !important; - } - .cashflow-table .ant-table-tbody > tr:hover .ant-table-cell-fix-left, - .cashflow-table .ant-table-tbody > tr:hover .ant-table-cell-fix-right { - background: rgba(26, 32, 44, 0.95) !important; - } - .cashflow-table .positive-value { +const TABLE_CLASS_NAME = 'cashflow-table'; +const tableStyles = getTableStyles(TABLE_CLASS_NAME) + ` + .${TABLE_CLASS_NAME} .positive-value { color: #E53E3E; } - .cashflow-table .negative-value { + .${TABLE_CLASS_NAME} .negative-value { color: #48BB78; } - .cashflow-table .positive-change { - color: #E53E3E; - } - .cashflow-table .negative-change { - color: #48BB78; - } - .cashflow-table .ant-table-placeholder { - background: transparent !important; - } - .cashflow-table .ant-empty-description { - color: #A0AEC0; - } `; // 核心指标 @@ -92,7 +34,7 @@ interface TableRowData { [period: string]: unknown; } -export const CashflowTable: React.FC = ({ +const CashflowTableInner: React.FC = ({ data, showMetricChart, calculateYoYChange, @@ -131,29 +73,13 @@ export const CashflowTable: React.FC = ({ }); }, [data, displayData]); - // 计算同比变化 - const calculateYoY = ( + // 计算同比变化(使用共享函数) + const calcYoY = ( 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; + return calculateYoY(data, currentValue, currentPeriod, path, getValueByPath); }; // 构建列定义 @@ -188,7 +114,7 @@ export const CashflowTable: React.FC = ({ width: 110, align: 'right' as const, render: (value: number | undefined, record: TableRowData) => { - const yoy = calculateYoY(value, item.period, record.path); + const yoy = calcYoY(value, item.period, record.path); const formattedValue = formatUtils.formatLargeNumber(value, 1); const isNegative = value !== undefined && value < 0; @@ -246,7 +172,7 @@ export const CashflowTable: React.FC = ({ return ( - +
= ({ ); }; +export const CashflowTable = memo(CashflowTableInner); export default CashflowTable; diff --git a/src/views/Company/components/FinancialPanorama/components/IncomeStatementTable.tsx b/src/views/Company/components/FinancialPanorama/components/IncomeStatementTable.tsx index 5f593658..606c3e48 100644 --- a/src/views/Company/components/FinancialPanorama/components/IncomeStatementTable.tsx +++ b/src/views/Company/components/FinancialPanorama/components/IncomeStatementTable.tsx @@ -2,92 +2,25 @@ * 利润表组件 - Ant Design 黑金主题 */ -import React, { useMemo } from 'react'; +import React, { useMemo, memo } from 'react'; import { Box, Text, HStack, Badge as ChakraBadge } 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 { INCOME_STATEMENT_SECTIONS } from '../constants'; -import { getValueByPath, isNegativeIndicator } from '../utils'; +import { getValueByPath, isNegativeIndicator, BLACK_GOLD_TABLE_THEME, getTableStyles, calculateYoY } from '../utils'; import type { IncomeStatementTableProps, MetricConfig } from '../types'; -// 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 = ` - .income-statement-table .ant-table { - background: transparent !important; - } - .income-statement-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; - } - .income-statement-table .ant-table-tbody > tr > td { - border-bottom: 1px solid rgba(212, 175, 55, 0.1) !important; - color: #E2E8F0; - font-size: 12px; - } - .income-statement-table .ant-table-tbody > tr:hover > td { - background: rgba(212, 175, 55, 0.08) !important; - } - .income-statement-table .ant-table-tbody > tr.total-row > td { - background: rgba(212, 175, 55, 0.15) !important; - font-weight: 600; - } - .income-statement-table .ant-table-tbody > tr.subtotal-row > td { +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; } - .income-statement-table .ant-table-tbody > tr.section-header > td { - background: rgba(212, 175, 55, 0.08) !important; - font-weight: 600; - color: #D4AF37; - } - .income-statement-table .ant-table-cell-fix-left, - .income-statement-table .ant-table-cell-fix-right { - background: #1A202C !important; - } - .income-statement-table .ant-table-tbody > tr:hover .ant-table-cell-fix-left, - .income-statement-table .ant-table-tbody > tr:hover .ant-table-cell-fix-right { - background: rgba(26, 32, 44, 0.95) !important; - } - .income-statement-table .positive-change { + .${TABLE_CLASS_NAME} .negative-value { color: #E53E3E; } - .income-statement-table .negative-change { - color: #48BB78; - } - .income-statement-table .negative-value { - color: #E53E3E; - } - .income-statement-table .ant-table-placeholder { - background: transparent !important; - } - .income-statement-table .ant-empty-description { - color: #A0AEC0; - } `; // 表格行数据类型 @@ -103,7 +36,7 @@ interface TableRowData { [period: string]: unknown; } -export const IncomeStatementTable: React.FC = ({ +const IncomeStatementTableInner: React.FC = ({ data, showMetricChart, calculateYoYChange, @@ -160,29 +93,13 @@ export const IncomeStatementTable: React.FC = ({ return rows; }, [data, displayData]); - // 计算同比变化 - const calculateYoY = ( + // 计算同比变化(使用共享函数) + const calcYoY = ( 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; + return calculateYoY(data, currentValue, currentPeriod, path, getValueByPath); }; // 构建列定义 @@ -224,7 +141,7 @@ export const IncomeStatementTable: React.FC = ({ render: (value: number | undefined, record: TableRowData) => { if (record.isSection) return null; - const yoy = calculateYoY(value, item.period, record.path); + 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; @@ -295,7 +212,7 @@ export const IncomeStatementTable: React.FC = ({ return ( - +
= ({ ); }; +export const IncomeStatementTable = memo(IncomeStatementTableInner); export default IncomeStatementTable;