style(ForecastReport): 详细数据表格 UI 优化

- 斑马纹(奇数行浅色背景)
- 等宽字体(SF Mono/Monaco/Menlo)
- 重要指标行高亮(归母净利润、ROE、EPS、营业总收入)
- 预测列区分样式(斜体+浅金背景+分隔线)
- 负数红色/正增长绿色显示

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

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
zdl
2025-12-17 10:27:38 +08:00
parent e2dd9e2648
commit 21e83ac1bc

View File

@@ -1,5 +1,6 @@
/**
* 详细数据表格 - 黑金主题
* 优化:斑马纹、等宽字体、首列高亮、重要行强调、预测列区分
*/
import React, { useMemo } from 'react';
@@ -8,6 +9,12 @@ import { Table, ConfigProvider, Tag, theme as antTheme } from 'antd';
import type { ColumnsType } from 'antd/es/table';
import type { DetailTableProps, DetailTableRow } from '../types';
// 判断是否为预测年份
const isForecastYear = (year: string) => year.includes('E');
// 重要指标(需要高亮的行)
const IMPORTANT_METRICS = ['归母净利润', 'ROE', 'EPS', '营业总收入'];
// Ant Design 黑金主题配置
const BLACK_GOLD_THEME = {
algorithm: antTheme.darkAlgorithm,
@@ -23,43 +30,103 @@ const BLACK_GOLD_THEME = {
},
components: {
Table: {
headerBg: 'rgba(212, 175, 55, 0.1)',
headerBg: 'rgba(212, 175, 55, 0.12)',
headerColor: '#D4AF37',
rowHoverBg: 'rgba(212, 175, 55, 0.05)',
rowHoverBg: 'rgba(212, 175, 55, 0.08)',
borderColor: 'rgba(212, 175, 55, 0.2)',
cellPaddingBlock: 8,
cellPaddingInline: 12,
cellPaddingBlock: 12, // 增加行高
cellPaddingInline: 14,
},
},
};
// 表格样式
// 表格样式 - 斑马纹、等宽字体、预测列区分
const tableStyles = `
/* 固定列背景 */
.forecast-detail-table .ant-table-cell-fix-left,
.forecast-detail-table .ant-table-cell-fix-right {
background: #1A202C !important;
}
.forecast-detail-table .ant-table-thead .ant-table-cell-fix-left,
.forecast-detail-table .ant-table-thead .ant-table-cell-fix-right {
background: rgba(26, 32, 44, 0.95) !important;
background: rgba(26, 32, 44, 0.98) !important;
}
.forecast-detail-table .ant-table-tbody > tr:hover > td.ant-table-cell-fix-left {
background: #242d3d !important;
}
/* 指标标签样式 */
.forecast-detail-table .metric-tag {
background: rgba(212, 175, 55, 0.15);
border-color: rgba(212, 175, 55, 0.3);
color: #D4AF37;
font-weight: 500;
}
/* 重要指标行高亮 */
.forecast-detail-table .important-row {
background: rgba(212, 175, 55, 0.06) !important;
}
.forecast-detail-table .important-row .metric-tag {
background: rgba(212, 175, 55, 0.25);
color: #FFD700;
font-weight: 600;
}
/* 斑马纹 - 奇数行 */
.forecast-detail-table .ant-table-tbody > tr:nth-child(odd) > td {
background: rgba(255, 255, 255, 0.02);
}
.forecast-detail-table .ant-table-tbody > tr:nth-child(odd):hover > td {
background: rgba(212, 175, 55, 0.08) !important;
}
/* 等宽字体 - 数值列 */
.forecast-detail-table .data-cell {
font-family: 'SF Mono', 'Monaco', 'Menlo', 'Consolas', monospace;
font-variant-numeric: tabular-nums;
letter-spacing: -0.02em;
}
/* 预测列样式 */
.forecast-detail-table .forecast-col {
background: rgba(212, 175, 55, 0.04) !important;
font-style: italic;
}
.forecast-detail-table .ant-table-thead .forecast-col {
color: #FFD700 !important;
font-weight: 600;
}
/* 负数红色显示 */
.forecast-detail-table .negative-value {
color: #FC8181;
}
/* 正增长绿色 */
.forecast-detail-table .positive-growth {
color: #68D391;
}
/* 表头预测/历史分隔线 */
.forecast-detail-table .forecast-divider {
border-left: 2px solid rgba(212, 175, 55, 0.5) !important;
}
`;
interface TableRowData extends DetailTableRow {
key: string;
isImportant?: boolean;
}
const DetailTable: React.FC<DetailTableProps> = ({ data }) => {
const { years, rows } = data;
// 找出预测年份起始索引
const forecastStartIndex = useMemo(() => {
return years.findIndex(isForecastYear);
}, [years]);
// 构建列配置
const columns: ColumnsType<TableRowData> = useMemo(() => {
const cols: ColumnsType<TableRowData> = [
@@ -69,35 +136,65 @@ const DetailTable: React.FC<DetailTableProps> = ({ data }) => {
key: '指标',
fixed: 'left',
width: 160,
render: (value: string) => (
<Tag className="metric-tag">{value}</Tag>
render: (value: string, record: TableRowData) => (
<Tag className={`metric-tag ${record.isImportant ? 'important' : ''}`}>
{value}
</Tag>
),
},
];
// 添加年份列
years.forEach((year) => {
years.forEach((year, idx) => {
const isForecast = isForecastYear(year);
const isFirstForecast = idx === forecastStartIndex;
cols.push({
title: year,
title: isForecast ? `${year}` : year,
dataIndex: year,
key: year,
align: 'right',
width: 100,
render: (value: string | number | null) => value ?? '-',
width: 110,
className: `${isForecast ? 'forecast-col' : ''} ${isFirstForecast ? 'forecast-divider' : ''}`,
render: (value: string | number | null, record: TableRowData) => {
if (value === null || value === undefined) return '-';
// 格式化数值
const numValue = typeof value === 'number' ? value : parseFloat(value);
const isNegative = !isNaN(numValue) && numValue < 0;
const isGrowthMetric = record['指标']?.includes('增长') || record['指标']?.includes('率');
const isPositiveGrowth = isGrowthMetric && !isNaN(numValue) && numValue > 0;
// 数值类添加样式类名
const className = `data-cell ${isNegative ? 'negative-value' : ''} ${isPositiveGrowth ? 'positive-growth' : ''}`;
return <span className={className}>{value}</span>;
},
});
});
return cols;
}, [years]);
}, [years, forecastStartIndex]);
// 构建数据源
const dataSource: TableRowData[] = useMemo(() => {
return rows.map((row, idx) => ({
return rows.map((row, idx) => {
const metric = row['指标'] as string;
const isImportant = IMPORTANT_METRICS.some(m => metric?.includes(m));
return {
...row,
key: `row-${idx}`,
}));
isImportant,
};
});
}, [rows]);
// 行类名
const rowClassName = (record: TableRowData) => {
return record.isImportant ? 'important-row' : '';
};
return (
<Box className="forecast-detail-table">
<style>{tableStyles}</style>
@@ -109,9 +206,10 @@ const DetailTable: React.FC<DetailTableProps> = ({ data }) => {
columns={columns}
dataSource={dataSource}
pagination={false}
size="small"
size="middle"
scroll={{ x: 'max-content' }}
bordered
rowClassName={rowClassName}
/>
</ConfigProvider>
</Box>