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 黑金主题
*/
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<BalanceSheetTableProps> = ({
const BalanceSheetTableInner: React.FC<BalanceSheetTableProps> = ({
data,
showMetricChart,
calculateYoYChange,
@@ -172,29 +104,13 @@ export const BalanceSheetTable: React.FC<BalanceSheetTableProps> = ({
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<number>(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<BalanceSheetTableProps> = ({
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<BalanceSheetTableProps> = ({
return (
<Box className="balance-sheet-table">
<style>{tableStyles}</style>
<ConfigProvider theme={BLACK_GOLD_THEME}>
<ConfigProvider theme={BLACK_GOLD_TABLE_THEME}>
<Table
columns={columns}
dataSource={tableData}
@@ -323,4 +239,5 @@ export const BalanceSheetTable: React.FC<BalanceSheetTableProps> = ({
);
};
export const BalanceSheetTable = memo(BalanceSheetTableInner);
export default BalanceSheetTable;

View File

@@ -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<CashflowTableProps> = ({
const CashflowTableInner: React.FC<CashflowTableProps> = ({
data,
showMetricChart,
calculateYoYChange,
@@ -131,29 +73,13 @@ export const CashflowTable: React.FC<CashflowTableProps> = ({
});
}, [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<number>(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<CashflowTableProps> = ({
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<CashflowTableProps> = ({
return (
<Box className="cashflow-table">
<style>{tableStyles}</style>
<ConfigProvider theme={BLACK_GOLD_THEME}>
<ConfigProvider theme={BLACK_GOLD_TABLE_THEME}>
<Table
columns={columns}
dataSource={tableData}
@@ -266,4 +192,5 @@ export const CashflowTable: React.FC<CashflowTableProps> = ({
);
};
export const CashflowTable = memo(CashflowTableInner);
export default CashflowTable;

View File

@@ -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<IncomeStatementTableProps> = ({
const IncomeStatementTableInner: React.FC<IncomeStatementTableProps> = ({
data,
showMetricChart,
calculateYoYChange,
@@ -160,29 +93,13 @@ export const IncomeStatementTable: React.FC<IncomeStatementTableProps> = ({
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<number>(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<IncomeStatementTableProps> = ({
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<IncomeStatementTableProps> = ({
return (
<Box className="income-statement-table">
<style>{tableStyles}</style>
<ConfigProvider theme={BLACK_GOLD_THEME}>
<ConfigProvider theme={BLACK_GOLD_TABLE_THEME}>
<Table
columns={columns}
dataSource={tableData}
@@ -323,4 +240,5 @@ export const IncomeStatementTable: React.FC<IncomeStatementTableProps> = ({
);
};
export const IncomeStatementTable = memo(IncomeStatementTableInner);
export default IncomeStatementTable;