perf(FinancialPanorama): 表格组件使用共享配置 + memo

- BalanceSheetTable: 使用共享主题,添加 memo
- IncomeStatementTable: 使用共享主题,添加 memo
- CashflowTable: 使用共享主题,添加 memo
- 移除内联主题定义,减少重复代码

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
zdl
2025-12-19 14:44:26 +08:00
parent 0e29f1aff4
commit 54cce55c29
3 changed files with 37 additions and 275 deletions

View File

@@ -2,7 +2,7 @@
* 资产负债表组件 - Ant Design 黑金主题 * 资产负债表组件 - 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 { Box, Text, HStack, Badge as ChakraBadge } from '@chakra-ui/react';
import { Table, ConfigProvider, Tooltip } from 'antd'; import { Table, ConfigProvider, Tooltip } from 'antd';
import type { ColumnsType } from 'antd/es/table'; import type { ColumnsType } from 'antd/es/table';
@@ -17,79 +17,11 @@ import {
TOTAL_LIABILITIES_METRICS, TOTAL_LIABILITIES_METRICS,
EQUITY_METRICS, EQUITY_METRICS,
} from '../constants'; } from '../constants';
import { getValueByPath } from '../utils'; import { getValueByPath, BLACK_GOLD_TABLE_THEME, getTableStyles, calculateYoY } from '../utils';
import type { BalanceSheetTableProps, MetricConfig } from '../types'; import type { BalanceSheetTableProps, MetricConfig } from '../types';
// Ant Design 黑金主题配置 const TABLE_CLASS_NAME = 'balance-sheet-table';
const BLACK_GOLD_THEME = { const tableStyles = getTableStyles(TABLE_CLASS_NAME);
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;
}
`;
// 表格行数据类型 // 表格行数据类型
interface TableRowData { interface TableRowData {
@@ -103,7 +35,7 @@ interface TableRowData {
[period: string]: unknown; [period: string]: unknown;
} }
export const BalanceSheetTable: React.FC<BalanceSheetTableProps> = ({ const BalanceSheetTableInner: React.FC<BalanceSheetTableProps> = ({
data, data,
showMetricChart, showMetricChart,
calculateYoYChange, calculateYoYChange,
@@ -172,29 +104,13 @@ export const BalanceSheetTable: React.FC<BalanceSheetTableProps> = ({
return rows; return rows;
}, [data, displayData]); }, [data, displayData]);
// 计算同比变化 // 计算同比变化(使用共享函数)
const calculateYoY = ( const calcYoY = (
currentValue: number | undefined, currentValue: number | undefined,
currentPeriod: string, currentPeriod: string,
path: string path: string
): number | null => { ): number | null => {
if (currentValue === undefined || currentValue === null) return null; return calculateYoY(data, currentValue, currentPeriod, path, getValueByPath);
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<number>(lastYearPeriod, path);
if (lastYearValue === undefined || lastYearValue === 0) return null;
return ((currentValue - lastYearValue) / Math.abs(lastYearValue)) * 100;
}; };
// 构建列定义 // 构建列定义
@@ -236,7 +152,7 @@ export const BalanceSheetTable: React.FC<BalanceSheetTableProps> = ({
render: (value: number | undefined, record: TableRowData) => { render: (value: number | undefined, record: TableRowData) => {
if (record.isSection) return null; 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); const formattedValue = formatUtils.formatLargeNumber(value, 0);
return ( return (
@@ -296,7 +212,7 @@ export const BalanceSheetTable: React.FC<BalanceSheetTableProps> = ({
return ( return (
<Box className="balance-sheet-table"> <Box className="balance-sheet-table">
<style>{tableStyles}</style> <style>{tableStyles}</style>
<ConfigProvider theme={BLACK_GOLD_THEME}> <ConfigProvider theme={BLACK_GOLD_TABLE_THEME}>
<Table <Table
columns={columns} columns={columns}
dataSource={tableData} dataSource={tableData}
@@ -323,4 +239,5 @@ export const BalanceSheetTable: React.FC<BalanceSheetTableProps> = ({
); );
}; };
export const BalanceSheetTable = memo(BalanceSheetTableInner);
export default BalanceSheetTable; export default BalanceSheetTable;

View File

@@ -2,82 +2,24 @@
* 现金流量表组件 - Ant Design 黑金主题 * 现金流量表组件 - 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 { Box, Text, HStack, Badge as ChakraBadge } from '@chakra-ui/react';
import { Table, ConfigProvider, Tooltip } from 'antd'; import { Table, ConfigProvider, Tooltip } from 'antd';
import type { ColumnsType } from 'antd/es/table'; import type { ColumnsType } from 'antd/es/table';
import { Eye } from 'lucide-react'; import { Eye } from 'lucide-react';
import { formatUtils } from '@services/financialService'; import { formatUtils } from '@services/financialService';
import { CASHFLOW_METRICS } from '../constants'; import { CASHFLOW_METRICS } from '../constants';
import { getValueByPath } from '../utils'; import { getValueByPath, BLACK_GOLD_TABLE_THEME, getTableStyles, calculateYoY } from '../utils';
import type { CashflowTableProps } from '../types'; import type { CashflowTableProps } from '../types';
// Ant Design 黑金主题配置 const TABLE_CLASS_NAME = 'cashflow-table';
const BLACK_GOLD_THEME = { const tableStyles = getTableStyles(TABLE_CLASS_NAME) + `
token: { .${TABLE_CLASS_NAME} .positive-value {
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 {
color: #E53E3E; color: #E53E3E;
} }
.cashflow-table .negative-value { .${TABLE_CLASS_NAME} .negative-value {
color: #48BB78; 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; [period: string]: unknown;
} }
export const CashflowTable: React.FC<CashflowTableProps> = ({ const CashflowTableInner: React.FC<CashflowTableProps> = ({
data, data,
showMetricChart, showMetricChart,
calculateYoYChange, calculateYoYChange,
@@ -131,29 +73,13 @@ export const CashflowTable: React.FC<CashflowTableProps> = ({
}); });
}, [data, displayData]); }, [data, displayData]);
// 计算同比变化 // 计算同比变化(使用共享函数)
const calculateYoY = ( const calcYoY = (
currentValue: number | undefined, currentValue: number | undefined,
currentPeriod: string, currentPeriod: string,
path: string path: string
): number | null => { ): number | null => {
if (currentValue === undefined || currentValue === null) return null; return calculateYoY(data, currentValue, currentPeriod, path, getValueByPath);
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<number>(lastYearPeriod, path);
if (lastYearValue === undefined || lastYearValue === 0) return null;
return ((currentValue - lastYearValue) / Math.abs(lastYearValue)) * 100;
}; };
// 构建列定义 // 构建列定义
@@ -188,7 +114,7 @@ export const CashflowTable: React.FC<CashflowTableProps> = ({
width: 110, width: 110,
align: 'right' as const, align: 'right' as const,
render: (value: number | undefined, record: TableRowData) => { 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 formattedValue = formatUtils.formatLargeNumber(value, 1);
const isNegative = value !== undefined && value < 0; const isNegative = value !== undefined && value < 0;
@@ -246,7 +172,7 @@ export const CashflowTable: React.FC<CashflowTableProps> = ({
return ( return (
<Box className="cashflow-table"> <Box className="cashflow-table">
<style>{tableStyles}</style> <style>{tableStyles}</style>
<ConfigProvider theme={BLACK_GOLD_THEME}> <ConfigProvider theme={BLACK_GOLD_TABLE_THEME}>
<Table <Table
columns={columns} columns={columns}
dataSource={tableData} dataSource={tableData}
@@ -266,4 +192,5 @@ export const CashflowTable: React.FC<CashflowTableProps> = ({
); );
}; };
export const CashflowTable = memo(CashflowTableInner);
export default CashflowTable; export default CashflowTable;

View File

@@ -2,92 +2,25 @@
* 利润表组件 - Ant Design 黑金主题 * 利润表组件 - 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 { Box, Text, HStack, Badge as ChakraBadge } from '@chakra-ui/react';
import { Table, ConfigProvider, Tooltip } from 'antd'; import { Table, ConfigProvider, Tooltip } from 'antd';
import type { ColumnsType } from 'antd/es/table'; import type { ColumnsType } from 'antd/es/table';
import { Eye } from 'lucide-react'; import { Eye } from 'lucide-react';
import { formatUtils } from '@services/financialService'; import { formatUtils } from '@services/financialService';
import { INCOME_STATEMENT_SECTIONS } from '../constants'; 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'; import type { IncomeStatementTableProps, MetricConfig } from '../types';
// Ant Design 黑金主题配置 const TABLE_CLASS_NAME = 'income-statement-table';
const BLACK_GOLD_THEME = { const tableStyles = getTableStyles(TABLE_CLASS_NAME) + `
token: { .${TABLE_CLASS_NAME} .ant-table-tbody > tr.subtotal-row > td {
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 {
background: rgba(212, 175, 55, 0.1) !important; background: rgba(212, 175, 55, 0.1) !important;
font-weight: 500; font-weight: 500;
} }
.income-statement-table .ant-table-tbody > tr.section-header > td { .${TABLE_CLASS_NAME} .negative-value {
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 {
color: #E53E3E; 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; [period: string]: unknown;
} }
export const IncomeStatementTable: React.FC<IncomeStatementTableProps> = ({ const IncomeStatementTableInner: React.FC<IncomeStatementTableProps> = ({
data, data,
showMetricChart, showMetricChart,
calculateYoYChange, calculateYoYChange,
@@ -160,29 +93,13 @@ export const IncomeStatementTable: React.FC<IncomeStatementTableProps> = ({
return rows; return rows;
}, [data, displayData]); }, [data, displayData]);
// 计算同比变化 // 计算同比变化(使用共享函数)
const calculateYoY = ( const calcYoY = (
currentValue: number | undefined, currentValue: number | undefined,
currentPeriod: string, currentPeriod: string,
path: string path: string
): number | null => { ): number | null => {
if (currentValue === undefined || currentValue === null) return null; return calculateYoY(data, currentValue, currentPeriod, path, getValueByPath);
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<number>(lastYearPeriod, path);
if (lastYearValue === undefined || lastYearValue === 0) return null;
return ((currentValue - lastYearValue) / Math.abs(lastYearValue)) * 100;
}; };
// 构建列定义 // 构建列定义
@@ -224,7 +141,7 @@ export const IncomeStatementTable: React.FC<IncomeStatementTableProps> = ({
render: (value: number | undefined, record: TableRowData) => { render: (value: number | undefined, record: TableRowData) => {
if (record.isSection) return null; 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 isEPS = record.key.includes('eps');
const formattedValue = isEPS ? value?.toFixed(3) : formatUtils.formatLargeNumber(value, 0); const formattedValue = isEPS ? value?.toFixed(3) : formatUtils.formatLargeNumber(value, 0);
const isNegative = value !== undefined && value < 0; const isNegative = value !== undefined && value < 0;
@@ -295,7 +212,7 @@ export const IncomeStatementTable: React.FC<IncomeStatementTableProps> = ({
return ( return (
<Box className="income-statement-table"> <Box className="income-statement-table">
<style>{tableStyles}</style> <style>{tableStyles}</style>
<ConfigProvider theme={BLACK_GOLD_THEME}> <ConfigProvider theme={BLACK_GOLD_TABLE_THEME}>
<Table <Table
columns={columns} columns={columns}
dataSource={tableData} dataSource={tableData}
@@ -323,4 +240,5 @@ export const IncomeStatementTable: React.FC<IncomeStatementTableProps> = ({
); );
}; };
export const IncomeStatementTable = memo(IncomeStatementTableInner);
export default IncomeStatementTable; export default IncomeStatementTable;