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';
|
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 { ColumnsType } from 'antd/es/table';
|
||||||
import type { DetailTableProps, DetailTableRow } from '../types';
|
import type { DetailTableProps, DetailTableRow } from '../types';
|
||||||
|
|
||||||
|
// 判断是否为预测年份
|
||||||
|
const isForecastYear = (year: string) => year.includes('E');
|
||||||
|
|
||||||
|
// 重要指标(需要高亮的行)
|
||||||
|
const IMPORTANT_METRICS = ['归母净利润', 'ROE', 'EPS', '营业总收入'];
|
||||||
|
|
||||||
// Ant Design 黑金主题配置
|
// Ant Design 黑金主题配置
|
||||||
const BLACK_GOLD_THEME = {
|
const BLACK_GOLD_THEME = {
|
||||||
algorithm: antTheme.darkAlgorithm,
|
algorithm: antTheme.darkAlgorithm,
|
||||||
@@ -23,43 +30,103 @@ const BLACK_GOLD_THEME = {
|
|||||||
},
|
},
|
||||||
components: {
|
components: {
|
||||||
Table: {
|
Table: {
|
||||||
headerBg: 'rgba(212, 175, 55, 0.1)',
|
headerBg: 'rgba(212, 175, 55, 0.12)',
|
||||||
headerColor: '#D4AF37',
|
headerColor: '#D4AF37',
|
||||||
rowHoverBg: 'rgba(212, 175, 55, 0.05)',
|
rowHoverBg: 'rgba(212, 175, 55, 0.08)',
|
||||||
borderColor: 'rgba(212, 175, 55, 0.2)',
|
borderColor: 'rgba(212, 175, 55, 0.2)',
|
||||||
cellPaddingBlock: 8,
|
cellPaddingBlock: 12, // 增加行高
|
||||||
cellPaddingInline: 12,
|
cellPaddingInline: 14,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
// 表格样式
|
// 表格样式 - 斑马纹、等宽字体、预测列区分
|
||||||
const tableStyles = `
|
const tableStyles = `
|
||||||
|
/* 固定列背景 */
|
||||||
.forecast-detail-table .ant-table-cell-fix-left,
|
.forecast-detail-table .ant-table-cell-fix-left,
|
||||||
.forecast-detail-table .ant-table-cell-fix-right {
|
.forecast-detail-table .ant-table-cell-fix-right {
|
||||||
background: #1A202C !important;
|
background: #1A202C !important;
|
||||||
}
|
}
|
||||||
.forecast-detail-table .ant-table-thead .ant-table-cell-fix-left,
|
.forecast-detail-table .ant-table-thead .ant-table-cell-fix-left,
|
||||||
.forecast-detail-table .ant-table-thead .ant-table-cell-fix-right {
|
.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 {
|
.forecast-detail-table .ant-table-tbody > tr:hover > td.ant-table-cell-fix-left {
|
||||||
background: #242d3d !important;
|
background: #242d3d !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* 指标标签样式 */
|
||||||
.forecast-detail-table .metric-tag {
|
.forecast-detail-table .metric-tag {
|
||||||
background: rgba(212, 175, 55, 0.15);
|
background: rgba(212, 175, 55, 0.15);
|
||||||
border-color: rgba(212, 175, 55, 0.3);
|
border-color: rgba(212, 175, 55, 0.3);
|
||||||
color: #D4AF37;
|
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 {
|
interface TableRowData extends DetailTableRow {
|
||||||
key: string;
|
key: string;
|
||||||
|
isImportant?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
const DetailTable: React.FC<DetailTableProps> = ({ data }) => {
|
const DetailTable: React.FC<DetailTableProps> = ({ data }) => {
|
||||||
const { years, rows } = data;
|
const { years, rows } = data;
|
||||||
|
|
||||||
|
// 找出预测年份起始索引
|
||||||
|
const forecastStartIndex = useMemo(() => {
|
||||||
|
return years.findIndex(isForecastYear);
|
||||||
|
}, [years]);
|
||||||
|
|
||||||
// 构建列配置
|
// 构建列配置
|
||||||
const columns: ColumnsType<TableRowData> = useMemo(() => {
|
const columns: ColumnsType<TableRowData> = useMemo(() => {
|
||||||
const cols: ColumnsType<TableRowData> = [
|
const cols: ColumnsType<TableRowData> = [
|
||||||
@@ -69,35 +136,65 @@ const DetailTable: React.FC<DetailTableProps> = ({ data }) => {
|
|||||||
key: '指标',
|
key: '指标',
|
||||||
fixed: 'left',
|
fixed: 'left',
|
||||||
width: 160,
|
width: 160,
|
||||||
render: (value: string) => (
|
render: (value: string, record: TableRowData) => (
|
||||||
<Tag className="metric-tag">{value}</Tag>
|
<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({
|
cols.push({
|
||||||
title: year,
|
title: isForecast ? `${year}` : year,
|
||||||
dataIndex: year,
|
dataIndex: year,
|
||||||
key: year,
|
key: year,
|
||||||
align: 'right',
|
align: 'right',
|
||||||
width: 100,
|
width: 110,
|
||||||
render: (value: string | number | null) => value ?? '-',
|
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;
|
return cols;
|
||||||
}, [years]);
|
}, [years, forecastStartIndex]);
|
||||||
|
|
||||||
// 构建数据源
|
// 构建数据源
|
||||||
const dataSource: TableRowData[] = useMemo(() => {
|
const dataSource: TableRowData[] = useMemo(() => {
|
||||||
return rows.map((row, idx) => ({
|
return rows.map((row, idx) => {
|
||||||
...row,
|
const metric = row['指标'] as string;
|
||||||
key: `row-${idx}`,
|
const isImportant = IMPORTANT_METRICS.some(m => metric?.includes(m));
|
||||||
}));
|
|
||||||
|
return {
|
||||||
|
...row,
|
||||||
|
key: `row-${idx}`,
|
||||||
|
isImportant,
|
||||||
|
};
|
||||||
|
});
|
||||||
}, [rows]);
|
}, [rows]);
|
||||||
|
|
||||||
|
// 行类名
|
||||||
|
const rowClassName = (record: TableRowData) => {
|
||||||
|
return record.isImportant ? 'important-row' : '';
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box className="forecast-detail-table">
|
<Box className="forecast-detail-table">
|
||||||
<style>{tableStyles}</style>
|
<style>{tableStyles}</style>
|
||||||
@@ -109,9 +206,10 @@ const DetailTable: React.FC<DetailTableProps> = ({ data }) => {
|
|||||||
columns={columns}
|
columns={columns}
|
||||||
dataSource={dataSource}
|
dataSource={dataSource}
|
||||||
pagination={false}
|
pagination={false}
|
||||||
size="small"
|
size="middle"
|
||||||
scroll={{ x: 'max-content' }}
|
scroll={{ x: 'max-content' }}
|
||||||
bordered
|
bordered
|
||||||
|
rowClassName={rowClassName}
|
||||||
/>
|
/>
|
||||||
</ConfigProvider>
|
</ConfigProvider>
|
||||||
</Box>
|
</Box>
|
||||||
|
|||||||
Reference in New Issue
Block a user