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:
@@ -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) => ({
|
||||
...row,
|
||||
key: `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>
|
||||
|
||||
Reference in New Issue
Block a user