diff --git a/src/views/Company/components/FinancialPanorama/components/BalanceSheetTable.tsx b/src/views/Company/components/FinancialPanorama/components/BalanceSheetTable.tsx index 2f70e7b7..ab76d635 100644 --- a/src/views/Company/components/FinancialPanorama/components/BalanceSheetTable.tsx +++ b/src/views/Company/components/FinancialPanorama/components/BalanceSheetTable.tsx @@ -1,27 +1,12 @@ /** - * 资产负债表组件 + * 资产负债表组件 - Ant Design 黑金主题 */ -import React, { useState } from 'react'; -import { - Table, - Thead, - Tbody, - Tr, - Th, - Td, - TableContainer, - Text, - VStack, - HStack, - Box, - Badge, - Tooltip, - IconButton, - Alert, - AlertIcon, -} from '@chakra-ui/react'; -import { ChevronDownIcon, ChevronUpIcon, ViewIcon } from '@chakra-ui/icons'; +import React, { useMemo } 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 { CURRENT_ASSETS_METRICS, @@ -33,221 +18,308 @@ import { EQUITY_METRICS, } from '../constants'; import { getValueByPath } from '../utils'; -import type { BalanceSheetTableProps, MetricSectionConfig } from '../types'; +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; + } +`; + +// 表格行数据类型 +interface TableRowData { + key: string; + name: string; + path: string; + isCore?: boolean; + isTotal?: boolean; + isSection?: boolean; + indent?: number; + [period: string]: unknown; +} export const BalanceSheetTable: React.FC = ({ data, showMetricChart, calculateYoYChange, - getCellBackground, - positiveColor, - negativeColor, - bgColor, - hoverBg, + positiveColor = 'red.500', + negativeColor = 'green.500', }) => { - const [expandedSections, setExpandedSections] = useState>({ - currentAssets: true, - nonCurrentAssets: true, - currentLiabilities: true, - nonCurrentLiabilities: true, - equity: true, - }); - - const toggleSection = (section: string) => { - setExpandedSections((prev) => ({ - ...prev, - [section]: !prev[section], - })); - }; - - // 资产部分配置 - const assetSections: MetricSectionConfig[] = [ - CURRENT_ASSETS_METRICS, - NON_CURRENT_ASSETS_METRICS, - TOTAL_ASSETS_METRICS, - ]; - - // 负债部分配置 - const liabilitySections: MetricSectionConfig[] = [ - CURRENT_LIABILITIES_METRICS, - NON_CURRENT_LIABILITIES_METRICS, - TOTAL_LIABILITIES_METRICS, - ]; - - // 权益部分配置 - const equitySections: MetricSectionConfig[] = [EQUITY_METRICS]; - // 数组安全检查 if (!Array.isArray(data) || data.length === 0) { return ( - - + 暂无资产负债表数据 - + ); } const maxColumns = Math.min(data.length, 6); const displayData = data.slice(0, maxColumns); - const renderSection = (sections: MetricSectionConfig[]) => ( - <> - {sections.map((section) => ( - - {section.title !== '资产总计' && - section.title !== '负债合计' && ( - toggleSection(section.key)} - > - - - {expandedSections[section.key] ? ( - - ) : ( - - )} - {section.title} - - - - )} - {(expandedSections[section.key] || - section.title === '资产总计' || - section.title === '负债合计' || - section.title === '股东权益合计') && - section.metrics.map((metric) => { - const rowData = data.map((item) => - getValueByPath(item, metric.path) - ); + // 所有分类配置 + const allSections = [ + CURRENT_ASSETS_METRICS, + NON_CURRENT_ASSETS_METRICS, + TOTAL_ASSETS_METRICS, + CURRENT_LIABILITIES_METRICS, + NON_CURRENT_LIABILITIES_METRICS, + TOTAL_LIABILITIES_METRICS, + EQUITY_METRICS, + ]; - return ( - - showMetricChart(metric.name, metric.key, data, metric.path) - } - bg={metric.isTotal ? 'blue.50' : 'transparent'} - > - - - {!metric.isTotal && } - - {metric.name} - - {metric.isCore && ( - - 核心 - - )} - - - {displayData.map((item, idx) => { - const value = rowData[idx]; - const { change, intensity } = calculateYoYChange( - value ?? 0, - item.period, - data, - metric.path - ); + // 构建表格数据 + const tableData = useMemo(() => { + const rows: TableRowData[] = []; - return ( - - - - 数值: {formatUtils.formatLargeNumber(value)} - - 同比: {change.toFixed(2)}% - - } - placement="top" - > - - {formatUtils.formatLargeNumber(value, 0)} - - - {Math.abs(change) > 30 && !metric.isTotal && ( - 0 ? positiveColor : negativeColor} - fontWeight="bold" - > - {change > 0 ? '↑' : '↓'} - {Math.abs(change).toFixed(0)}% - - )} - - ); - })} - - } - variant="ghost" - colorScheme="blue" - aria-label="查看图表" - onClick={(e) => { - e.stopPropagation(); - showMetricChart(metric.name, metric.key, data, metric.path); - }} - /> - - - ); - })} - - ))} - - ); + allSections.forEach((section) => { + // 添加分组标题行(汇总行不显示标题) + if (!['资产总计', '负债合计'].includes(section.title)) { + rows.push({ + key: `section-${section.key}`, + name: section.title, + path: '', + isSection: true, + }); + } + + // 添加指标行 + section.metrics.forEach((metric: MetricConfig) => { + const row: TableRowData = { + key: metric.key, + name: metric.name, + path: metric.path, + isCore: metric.isCore, + isTotal: metric.isTotal || ['资产总计', '负债合计'].includes(section.title), + indent: metric.isTotal ? 0 : 1, + }; + + // 添加各期数值 + displayData.forEach((item) => { + const value = getValueByPath(item, metric.path); + row[item.period] = value; + }); + + rows.push(row); + }); + }); + + return rows; + }, [data, displayData]); + + // 计算同比变化 + const calculateYoY = ( + 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(lastYearPeriod, path); + if (lastYearValue === undefined || lastYearValue === 0) return null; + + return ((currentValue - lastYearValue) / Math.abs(lastYearValue)) * 100; + }; + + // 构建列定义 + const columns: ColumnsType = useMemo(() => { + const cols: ColumnsType = [ + { + title: '项目', + dataIndex: 'name', + key: 'name', + fixed: 'left', + width: 200, + render: (name: string, record: TableRowData) => { + if (record.isSection) { + return {name}; + } + return ( + + {name} + {record.isCore && ( + + 核心 + + )} + + ); + }, + }, + ...displayData.map((item) => ({ + title: ( + + {formatUtils.getReportType(item.period)} + {item.period.substring(0, 10)} + + ), + dataIndex: item.period, + key: item.period, + width: 120, + align: 'right' as const, + render: (value: number | undefined, record: TableRowData) => { + if (record.isSection) return null; + + const yoy = calculateYoY(value, item.period, record.path); + const formattedValue = formatUtils.formatLargeNumber(value, 0); + + return ( + + 数值: {formatUtils.formatLargeNumber(value)} + {yoy !== null && 同比: {yoy.toFixed(2)}%} + + } + > + + + {formattedValue} + + {yoy !== null && Math.abs(yoy) > 30 && !record.isTotal && ( + 0 ? 'positive-change' : 'negative-change'} + > + {yoy > 0 ? '↑' : '↓'}{Math.abs(yoy).toFixed(0)}% + + )} + + + ); + }, + })), + { + title: '', + key: 'action', + width: 40, + fixed: 'right', + render: (_: unknown, record: TableRowData) => { + if (record.isSection) return null; + return ( + { + e.stopPropagation(); + showMetricChart(record.name, record.key, data, record.path); + }} + /> + ); + }, + }, + ]; + + return cols; + }, [displayData, data, showMetricChart]); return ( - - - - - - {displayData.map((item) => ( - - ))} - - - - - {renderSection(assetSections)} - - {renderSection(liabilitySections)} - - {renderSection(equitySections)} - -
- 项目 - - - {formatUtils.getReportType(item.period)} - - {item.period.substring(0, 10)} - - - 操作
-
+ + + + { + if (record.isSection) return 'section-header'; + if (record.isTotal) return 'total-row'; + return ''; + }} + onRow={(record) => ({ + onClick: () => { + if (!record.isSection) { + showMetricChart(record.name, record.key, data, record.path); + } + }, + style: { cursor: record.isSection ? 'default' : 'pointer' }, + })} + locale={{ emptyText: '暂无数据' }} + /> + + ); }; diff --git a/src/views/Company/components/FinancialPanorama/components/CashflowTable.tsx b/src/views/Company/components/FinancialPanorama/components/CashflowTable.tsx index ed193273..a30e7199 100644 --- a/src/views/Company/components/FinancialPanorama/components/CashflowTable.tsx +++ b/src/views/Company/components/FinancialPanorama/components/CashflowTable.tsx @@ -1,156 +1,268 @@ /** - * 现金流量表组件 + * 现金流量表组件 - Ant Design 黑金主题 */ -import React from 'react'; -import { - Table, - Thead, - Tbody, - Tr, - Th, - Td, - TableContainer, - Text, - VStack, - HStack, - Badge, - Tooltip, - IconButton, - Alert, - AlertIcon, -} from '@chakra-ui/react'; -import { ViewIcon } from '@chakra-ui/icons'; +import React, { useMemo } 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 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 { + color: #E53E3E; + } + .cashflow-table .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; + } +`; + +// 核心指标 +const CORE_METRICS = ['operating_net', 'free_cash_flow']; + +// 表格行数据类型 +interface TableRowData { + key: string; + name: string; + path: string; + isCore?: boolean; + [period: string]: unknown; +} + export const CashflowTable: React.FC = ({ data, showMetricChart, calculateYoYChange, - getCellBackground, - positiveColor, - negativeColor, - bgColor, - hoverBg, + positiveColor = 'red.500', + negativeColor = 'green.500', }) => { // 数组安全检查 if (!Array.isArray(data) || data.length === 0) { return ( - - + 暂无现金流量表数据 - + ); } const maxColumns = Math.min(data.length, 8); const displayData = data.slice(0, maxColumns); - return ( - -
- - - - {displayData.map((item) => ( - - ))} - - - - - {CASHFLOW_METRICS.map((metric) => { - const rowData = data.map((item) => getValueByPath(item, metric.path)); + )} + + + ); + }, + })), + { + title: '', + key: 'action', + width: 40, + fixed: 'right', + render: (_: unknown, record: TableRowData) => ( + { + e.stopPropagation(); + showMetricChart(record.name, record.key, data, record.path); + }} + /> + ), + }, + ]; - return ( - showMetricChart(metric.name, metric.key, data, metric.path)} - > - - {displayData.map((item, idx) => { - const value = rowData[idx]; - const isNegative = value !== undefined && value < 0; - const { change, intensity } = calculateYoYChange( - value ?? 0, - item.period, - data, - metric.path - ); + return cols; + }, [displayData, data, showMetricChart]); - return ( - - ); - })} - - - ); + return ( + + + +
- 项目 - - - {formatUtils.getReportType(item.period)} - - {item.period.substring(0, 10)} + // 构建表格数据 + const tableData = useMemo(() => { + return CASHFLOW_METRICS.map((metric) => { + const row: TableRowData = { + key: metric.key, + name: metric.name, + path: metric.path, + isCore: CORE_METRICS.includes(metric.key), + }; + + // 添加各期数值 + displayData.forEach((item) => { + const value = getValueByPath(item, metric.path); + row[item.period] = value; + }); + + return row; + }); + }, [data, displayData]); + + // 计算同比变化 + const calculateYoY = ( + 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(lastYearPeriod, path); + if (lastYearValue === undefined || lastYearValue === 0) return null; + + return ((currentValue - lastYearValue) / Math.abs(lastYearValue)) * 100; + }; + + // 构建列定义 + const columns: ColumnsType = useMemo(() => { + const cols: ColumnsType = [ + { + title: '项目', + dataIndex: 'name', + key: 'name', + fixed: 'left', + width: 180, + render: (name: string, record: TableRowData) => ( + + {name} + {record.isCore && ( + + 核心 + + )} + + ), + }, + ...displayData.map((item) => ({ + title: ( + + {formatUtils.getReportType(item.period)} + {item.period.substring(0, 10)} + + ), + dataIndex: item.period, + key: item.period, + width: 110, + align: 'right' as const, + render: (value: number | undefined, record: TableRowData) => { + const yoy = calculateYoY(value, item.period, record.path); + const formattedValue = formatUtils.formatLargeNumber(value, 1); + const isNegative = value !== undefined && value < 0; + + return ( + + 数值: {formatUtils.formatLargeNumber(value)} + {yoy !== null && 同比: {yoy.toFixed(2)}%} + + } + > + + + {formattedValue} + + {yoy !== null && Math.abs(yoy) > 50 && ( + 0 ? 'positive-change' : 'negative-change'} + > + {yoy > 0 ? '↑' : '↓'} - - 趋势
- - {metric.name} - {['operating_net', 'free_cash_flow'].includes(metric.key) && ( - 核心 - )} - - - - 数值: {formatUtils.formatLargeNumber(value)} - 同比: {change.toFixed(2)}% - - } - placement="top" - > - - {formatUtils.formatLargeNumber(value, 1)} - - - {Math.abs(change) > 50 && ( - 0 ? positiveColor : negativeColor} - fontWeight="bold" - > - {change > 0 ? '↑' : '↓'} - - )} - - } - variant="ghost" - colorScheme="blue" - aria-label="查看趋势" - /> -
({ + onClick: () => { + showMetricChart(record.name, record.key, data, record.path); + }, + style: { cursor: 'pointer' }, })} - -
- + locale={{ emptyText: '暂无数据' }} + /> +
+
); }; diff --git a/src/views/Company/components/FinancialPanorama/components/FinancialMetricsTable.tsx b/src/views/Company/components/FinancialPanorama/components/FinancialMetricsTable.tsx index 05ac1d57..ef90a856 100644 --- a/src/views/Company/components/FinancialPanorama/components/FinancialMetricsTable.tsx +++ b/src/views/Company/components/FinancialPanorama/components/FinancialMetricsTable.tsx @@ -1,33 +1,12 @@ /** - * 财务指标表格组件 + * 财务指标表格组件 - Ant Design 黑金主题 */ -import React, { useState } from 'react'; -import { - VStack, - HStack, - Button, - Table, - Thead, - Tbody, - Tr, - Th, - Td, - TableContainer, - Text, - Badge, - Tooltip, - IconButton, - Alert, - AlertIcon, - Card, - CardBody, - CardHeader, - Heading, - SimpleGrid, - Box, -} from '@chakra-ui/react'; -import { ViewIcon } from '@chakra-ui/icons'; +import React, { useState, useMemo } from 'react'; +import { Box, Text, HStack, Badge as ChakraBadge, SimpleGrid, Card, CardBody, CardHeader, Heading, Button } 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 { FINANCIAL_METRICS_CATEGORIES } from '../constants'; import { getValueByPath, isNegativeIndicator } from '../utils'; @@ -35,25 +14,96 @@ import type { FinancialMetricsTableProps } from '../types'; type CategoryKey = keyof typeof FINANCIAL_METRICS_CATEGORIES; +// 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 = ` + .financial-metrics-table .ant-table { + background: transparent !important; + } + .financial-metrics-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; + } + .financial-metrics-table .ant-table-tbody > tr > td { + border-bottom: 1px solid rgba(212, 175, 55, 0.1) !important; + color: #E2E8F0; + font-size: 12px; + } + .financial-metrics-table .ant-table-tbody > tr:hover > td { + background: rgba(212, 175, 55, 0.08) !important; + } + .financial-metrics-table .ant-table-cell-fix-left, + .financial-metrics-table .ant-table-cell-fix-right { + background: #1A202C !important; + } + .financial-metrics-table .ant-table-tbody > tr:hover .ant-table-cell-fix-left, + .financial-metrics-table .ant-table-tbody > tr:hover .ant-table-cell-fix-right { + background: rgba(26, 32, 44, 0.95) !important; + } + .financial-metrics-table .positive-change { + color: #E53E3E; + } + .financial-metrics-table .negative-change { + color: #48BB78; + } + .financial-metrics-table .positive-value { + color: #E53E3E; + } + .financial-metrics-table .negative-value { + color: #48BB78; + } + .financial-metrics-table .ant-table-placeholder { + background: transparent !important; + } + .financial-metrics-table .ant-empty-description { + color: #A0AEC0; + } +`; + +// 表格行数据类型 +interface TableRowData { + key: string; + name: string; + path: string; + isCore?: boolean; + [period: string]: unknown; +} + export const FinancialMetricsTable: React.FC = ({ data, showMetricChart, calculateYoYChange, - getCellBackground, - positiveColor, - negativeColor, - bgColor, - hoverBg, }) => { const [selectedCategory, setSelectedCategory] = useState('profitability'); // 数组安全检查 if (!Array.isArray(data) || data.length === 0) { return ( - - + 暂无财务指标数据 - + ); } @@ -61,172 +111,202 @@ export const FinancialMetricsTable: React.FC = ({ const displayData = data.slice(0, maxColumns); const currentCategory = FINANCIAL_METRICS_CATEGORIES[selectedCategory]; + // 构建表格数据 + const tableData = useMemo(() => { + return currentCategory.metrics.map((metric) => { + const row: TableRowData = { + key: metric.key, + name: metric.name, + path: metric.path, + isCore: metric.isCore, + }; + + // 添加各期数值 + displayData.forEach((item) => { + const value = getValueByPath(item, metric.path); + row[item.period] = value; + }); + + return row; + }); + }, [data, displayData, currentCategory]); + + // 计算同比变化 + const calculateYoY = ( + 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(lastYearPeriod, path); + if (lastYearValue === undefined || lastYearValue === 0) return null; + + return ((currentValue - lastYearValue) / Math.abs(lastYearValue)) * 100; + }; + + // 构建列定义 + const columns: ColumnsType = useMemo(() => { + const cols: ColumnsType = [ + { + title: currentCategory.title, + dataIndex: 'name', + key: 'name', + fixed: 'left', + width: 200, + render: (name: string, record: TableRowData) => ( + + {name} + {record.isCore && ( + + 核心 + + )} + + ), + }, + ...displayData.map((item) => ({ + title: ( + + {formatUtils.getReportType(item.period)} + {item.period.substring(0, 10)} + + ), + dataIndex: item.period, + key: item.period, + width: 100, + align: 'right' as const, + render: (value: number | undefined, record: TableRowData) => { + const yoy = calculateYoY(value, item.period, record.path); + const isNegative = isNegativeIndicator(record.key); + + // 对于负向指标,增加是坏事(绿色),减少是好事(红色) + const changeColor = isNegative + ? (yoy && yoy > 0 ? 'negative-change' : 'positive-change') + : (yoy && yoy > 0 ? 'positive-change' : 'negative-change'); + + // 成长能力指标特殊处理:正值红色,负值绿色 + const valueColor = selectedCategory === 'growth' + ? (value !== undefined && value > 0 ? 'positive-value' : value !== undefined && value < 0 ? 'negative-value' : '') + : ''; + + return ( + + {record.name}: {value?.toFixed(2) || '-'} + {yoy !== null && 同比: {yoy.toFixed(2)}%} + + } + > + + + {value?.toFixed(2) || '-'} + + {yoy !== null && Math.abs(yoy) > 20 && value !== undefined && Math.abs(value) > 0.01 && ( + + {yoy > 0 ? '↑' : '↓'} + + )} + + + ); + }, + })), + { + title: '', + key: 'action', + width: 40, + fixed: 'right', + render: (_: unknown, record: TableRowData) => ( + { + e.stopPropagation(); + showMetricChart(record.name, record.key, data, record.path); + }} + /> + ), + }, + ]; + + return cols; + }, [displayData, data, showMetricChart, currentCategory, selectedCategory]); + return ( - + {/* 分类选择器 */} - + {(Object.entries(FINANCIAL_METRICS_CATEGORIES) as [CategoryKey, typeof currentCategory][]).map( ([key, category]) => ( ) )} {/* 指标表格 */} - - - - - - {displayData.map((item) => ( - - ))} - - - - - {currentCategory.metrics.map((metric) => { - const rowData = data.map((item) => - getValueByPath(item, metric.path) - ); - - return ( - - showMetricChart(metric.name, metric.key, data, metric.path) - } - > - - {displayData.map((item, idx) => { - const value = rowData[idx]; - const { change, intensity } = calculateYoYChange( - value ?? 0, - item.period, - data, - metric.path - ); - - // 判断指标性质 - const isNegative = isNegativeIndicator(metric.key); - - // 对于负向指标,增加是坏事(绿色),减少是好事(红色) - const displayColor = isNegative - ? change > 0 - ? negativeColor - : positiveColor - : change > 0 - ? positiveColor - : negativeColor; - - return ( - - ); - })} - - - ); + + + +
- {currentCategory.title} - - - {formatUtils.getReportType(item.period)} - - {item.period.substring(0, 10)} - - - 趋势
- - - {metric.name} - - {metric.isCore && ( - - 核心 - - )} - - - - - {metric.name}: {value?.toFixed(2) || '-'} - - 同比: {change.toFixed(2)}% - - } - placement="top" - > - 0 - ? positiveColor - : value !== undefined && value < 0 - ? negativeColor - : 'gray.500' - : 'inherit' - } - > - {value?.toFixed(2) || '-'} - - - {Math.abs(change) > 20 && - value !== undefined && - Math.abs(value) > 0.01 && ( - - {change > 0 ? '↑' : '↓'} - - )} - - } - variant="ghost" - colorScheme="blue" - aria-label="查看趋势" - onClick={(e) => { - e.stopPropagation(); - showMetricChart(metric.name, metric.key, data, metric.path); - }} - /> -
({ + onClick: () => { + showMetricChart(record.name, record.key, data, record.path); + }, + style: { cursor: 'pointer' }, })} - -
-
+ locale={{ emptyText: '暂无数据' }} + /> + +
{/* 关键指标快速对比 */} - - - 关键指标速览 - - - - {data[0] && - [ + {data[0] && ( + + + 关键指标速览 + + + + {[ { label: 'ROE', value: getValueByPath(data[0], 'profitability.roe'), @@ -258,21 +338,22 @@ export const FinancialMetricsTable: React.FC = ({ format: 'percent', }, ].map((item, idx) => ( - - + + {item.label} - + {item.format === 'percent' ? formatUtils.formatPercent(item.value) : item.value?.toFixed(2) || '-'} ))} - - - -
+ + + + )} + ); }; diff --git a/src/views/Company/components/FinancialPanorama/components/FinancialTable.tsx b/src/views/Company/components/FinancialPanorama/components/FinancialTable.tsx new file mode 100644 index 00000000..bfc1a937 --- /dev/null +++ b/src/views/Company/components/FinancialPanorama/components/FinancialTable.tsx @@ -0,0 +1,328 @@ +/** + * 通用财务表格组件 - Ant Design 黑金主题 + */ + +import React from 'react'; +import { Box, Text, HStack, Badge as ChakraBadge } from '@chakra-ui/react'; +import { Table, ConfigProvider, Tooltip, Badge } from 'antd'; +import type { ColumnsType } from 'antd/es/table'; +import { Eye } from 'lucide-react'; +import { formatUtils } from '@services/financialService'; + +// Ant Design 表格黑金主题配置 +export const FINANCIAL_TABLE_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, + }, + }, +}; + +// 通用样式 +export const tableStyles = ` + .financial-table .ant-table { + background: transparent !important; + } + .financial-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; + } + .financial-table .ant-table-tbody > tr > td { + border-bottom: 1px solid rgba(212, 175, 55, 0.1) !important; + color: #E2E8F0; + font-size: 12px; + } + .financial-table .ant-table-tbody > tr:hover > td { + background: rgba(212, 175, 55, 0.08) !important; + } + .financial-table .ant-table-tbody > tr.total-row > td { + background: rgba(212, 175, 55, 0.15) !important; + font-weight: 600; + } + .financial-table .ant-table-tbody > tr.section-header > td { + background: rgba(212, 175, 55, 0.08) !important; + font-weight: 600; + color: #D4AF37; + } + .financial-table .ant-table-cell-fix-left, + .financial-table .ant-table-cell-fix-right { + background: #1A202C !important; + } + .financial-table .ant-table-tbody > tr:hover .ant-table-cell-fix-left, + .financial-table .ant-table-tbody > tr:hover .ant-table-cell-fix-right { + background: rgba(26, 32, 44, 0.95) !important; + } + .financial-table .positive-change { + color: #E53E3E; + } + .financial-table .negative-change { + color: #48BB78; + } + .financial-table .ant-table-placeholder { + background: transparent !important; + } + .financial-table .ant-empty-description { + color: #A0AEC0; + } +`; + +// 指标类型 +export interface MetricConfig { + name: string; + key: string; + path: string; + isCore?: boolean; + isTotal?: boolean; + isSubtotal?: boolean; +} + +export interface MetricSectionConfig { + title: string; + key: string; + metrics: MetricConfig[]; +} + +// 表格行数据类型 +export interface FinancialTableRow { + key: string; + name: string; + path: string; + isCore?: boolean; + isTotal?: boolean; + isSection?: boolean; + indent?: number; + [period: string]: unknown; +} + +// 组件 Props +export interface FinancialTableProps { + data: Array<{ period: string; [key: string]: unknown }>; + sections: MetricSectionConfig[]; + onRowClick?: (name: string, key: string, path: string) => void; + loading?: boolean; + maxColumns?: number; +} + +// 获取嵌套路径的值 +const getValueByPath = (obj: Record, path: string): number | undefined => { + const keys = path.split('.'); + let value: unknown = obj; + for (const key of keys) { + if (value && typeof value === 'object') { + value = (value as Record)[key]; + } else { + return undefined; + } + } + return typeof value === 'number' ? value : undefined; +}; + +// 计算同比变化 +const calculateYoY = ( + currentValue: number | undefined, + currentPeriod: string, + data: Array<{ period: string; [key: string]: unknown }>, + 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(lastYearPeriod as Record, path); + if (lastYearValue === undefined || lastYearValue === 0) return null; + + return ((currentValue - lastYearValue) / Math.abs(lastYearValue)) * 100; +}; + +const FinancialTable: React.FC = ({ + data, + sections, + onRowClick, + loading = false, + maxColumns = 6, +}) => { + // 限制显示列数 + const displayData = data.slice(0, maxColumns); + + // 构建表格数据 + const tableData: FinancialTableRow[] = []; + + sections.forEach((section) => { + // 添加分组标题行(除了汇总行) + if (!section.title.includes('总计') && !section.title.includes('合计')) { + tableData.push({ + key: `section-${section.key}`, + name: section.title, + path: '', + isSection: true, + }); + } + + // 添加指标行 + section.metrics.forEach((metric) => { + const row: FinancialTableRow = { + key: metric.key, + name: metric.name, + path: metric.path, + isCore: metric.isCore, + isTotal: metric.isTotal || section.title.includes('总计') || section.title.includes('合计'), + indent: metric.isTotal ? 0 : 1, + }; + + // 添加各期数值 + displayData.forEach((item) => { + const value = getValueByPath(item as Record, metric.path); + row[item.period] = value; + }); + + tableData.push(row); + }); + }); + + // 构建列定义 + const columns: ColumnsType = [ + { + title: '项目', + dataIndex: 'name', + key: 'name', + fixed: 'left', + width: 180, + render: (name: string, record: FinancialTableRow) => { + if (record.isSection) { + return {name}; + } + return ( + + {name} + {record.isCore && ( + + 核心 + + )} + + ); + }, + }, + ...displayData.map((item) => ({ + title: ( + + {formatUtils.getReportType(item.period)} + {item.period.substring(0, 10)} + + ), + dataIndex: item.period, + key: item.period, + width: 110, + align: 'right' as const, + render: (value: number | undefined, record: FinancialTableRow) => { + if (record.isSection) return null; + + const yoy = calculateYoY(value, item.period, data, record.path); + const formattedValue = formatUtils.formatLargeNumber(value, 0); + + return ( + + 数值: {formatUtils.formatLargeNumber(value)} + {yoy !== null && 同比: {yoy.toFixed(2)}%} + + } + > + + + {formattedValue} + + {yoy !== null && Math.abs(yoy) > 30 && !record.isTotal && ( + 0 ? 'positive-change' : 'negative-change'} + > + {yoy > 0 ? '↑' : '↓'}{Math.abs(yoy).toFixed(0)}% + + )} + + + ); + }, + })), + { + title: '', + key: 'action', + width: 40, + fixed: 'right', + render: (_: unknown, record: FinancialTableRow) => { + if (record.isSection) return null; + return ( + { + e.stopPropagation(); + onRowClick?.(record.name, record.key, record.path); + }} + /> + ); + }, + }, + ]; + + return ( + + + + { + if (record.isSection) return 'section-header'; + if (record.isTotal) return 'total-row'; + return ''; + }} + onRow={(record) => ({ + onClick: () => { + if (!record.isSection && onRowClick) { + onRowClick(record.name, record.key, record.path); + } + }, + style: { cursor: record.isSection ? 'default' : 'pointer' }, + })} + locale={{ emptyText: '暂无数据' }} + /> + + + ); +}; + +export default FinancialTable; diff --git a/src/views/Company/components/FinancialPanorama/components/IncomeStatementTable.tsx b/src/views/Company/components/FinancialPanorama/components/IncomeStatementTable.tsx index a9d1dbee..5f593658 100644 --- a/src/views/Company/components/FinancialPanorama/components/IncomeStatementTable.tsx +++ b/src/views/Company/components/FinancialPanorama/components/IncomeStatementTable.tsx @@ -1,228 +1,325 @@ /** - * 利润表组件 + * 利润表组件 - Ant Design 黑金主题 */ -import React, { useState } from 'react'; -import { - Table, - Thead, - Tbody, - Tr, - Th, - Td, - TableContainer, - Text, - VStack, - HStack, - Box, - Badge, - Tooltip, - IconButton, - Alert, - AlertIcon, -} from '@chakra-ui/react'; -import { ChevronDownIcon, ChevronUpIcon, ViewIcon } from '@chakra-ui/icons'; +import React, { useMemo } 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 type { IncomeStatementTableProps } from '../types'; +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 { + 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 { + 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; + } +`; + +// 表格行数据类型 +interface TableRowData { + key: string; + name: string; + path: string; + isCore?: boolean; + isTotal?: boolean; + isSubtotal?: boolean; + isSection?: boolean; + indent?: number; + [period: string]: unknown; +} export const IncomeStatementTable: React.FC = ({ data, showMetricChart, calculateYoYChange, - getCellBackground, - positiveColor, - negativeColor, - bgColor, - hoverBg, + positiveColor = 'red.500', + negativeColor = 'green.500', }) => { - const [expandedSections, setExpandedSections] = useState>({ - revenue: true, - costs: true, - otherGains: true, - profits: true, - eps: true, - comprehensive: true, - }); - - const toggleSection = (section: string) => { - setExpandedSections((prev) => ({ - ...prev, - [section]: !prev[section], - })); - }; - // 数组安全检查 if (!Array.isArray(data) || data.length === 0) { return ( - - + 暂无利润表数据 - + ); } const maxColumns = Math.min(data.length, 6); const displayData = data.slice(0, maxColumns); - const renderSection = (section: (typeof INCOME_STATEMENT_SECTIONS)[0]) => ( - - toggleSection(section.key)} - > - - - {expandedSections[section.key] && - section.metrics.map((metric) => { - const rowData = data.map((item) => getValueByPath(item, metric.path)); + // 构建表格数据 + const tableData = useMemo(() => { + const rows: TableRowData[] = []; + + INCOME_STATEMENT_SECTIONS.forEach((section) => { + // 添加分组标题行 + rows.push({ + key: `section-${section.key}`, + name: section.title, + path: '', + isSection: true, + }); + + // 添加指标行 + section.metrics.forEach((metric: MetricConfig) => { + const row: TableRowData = { + key: metric.key, + name: metric.name, + path: metric.path, + isCore: metric.isCore, + isTotal: metric.isTotal, + isSubtotal: metric.isSubtotal, + indent: metric.isTotal || metric.isSubtotal ? 0 : (metric.name.startsWith(' ') ? 2 : 1), + }; + + // 添加各期数值 + displayData.forEach((item) => { + const value = getValueByPath(item, metric.path); + row[item.period] = value; + }); + + rows.push(row); + }); + }); + + return rows; + }, [data, displayData]); + + // 计算同比变化 + const calculateYoY = ( + 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(lastYearPeriod, path); + if (lastYearValue === undefined || lastYearValue === 0) return null; + + return ((currentValue - lastYearValue) / Math.abs(lastYearValue)) * 100; + }; + + // 构建列定义 + const columns: ColumnsType = useMemo(() => { + const cols: ColumnsType = [ + { + title: '项目', + dataIndex: 'name', + key: 'name', + fixed: 'left', + width: 250, + render: (name: string, record: TableRowData) => { + if (record.isSection) { + return {name}; + } + return ( + + {name} + {record.isCore && ( + + 核心 + + )} + + ); + }, + }, + ...displayData.map((item) => ({ + title: ( + + {formatUtils.getReportType(item.period)} + {item.period.substring(0, 10)} + + ), + dataIndex: item.period, + key: item.period, + width: 120, + align: 'right' as const, + render: (value: number | undefined, record: TableRowData) => { + if (record.isSection) return null; + + const yoy = calculateYoY(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; + + // 成本费用类负向指标,增长用绿色,减少用红色 + const isCostItem = isNegativeIndicator(record.key); + const changeColor = isCostItem + ? (yoy && yoy > 0 ? 'negative-change' : 'positive-change') + : (yoy && yoy > 0 ? 'positive-change' : 'negative-change'); return ( - showMetricChart(metric.name, metric.key, data, metric.path)} - bg={ - metric.isTotal - ? 'blue.50' - : metric.isSubtotal - ? 'orange.50' - : 'transparent' + + 数值: {isEPS ? value?.toFixed(3) : formatUtils.formatLargeNumber(value)} + {yoy !== null && 同比: {yoy.toFixed(2)}%} + } > - - {displayData.map((item, idx) => { - const value = rowData[idx]; - const { change, intensity } = calculateYoYChange( - value ?? 0, - item.period, - data, - metric.path - ); - - // 特殊处理:成本费用类负向指标,增长用绿色,减少用红色 - const isCostItem = isNegativeIndicator(metric.key); - const displayColor = isCostItem - ? change > 0 - ? negativeColor - : positiveColor - : change > 0 - ? positiveColor - : negativeColor; - - return ( - - ); - })} - - + )} + + ); - })} - - ); + }, + })), + { + title: '', + key: 'action', + width: 40, + fixed: 'right', + render: (_: unknown, record: TableRowData) => { + if (record.isSection) return null; + return ( + { + e.stopPropagation(); + showMetricChart(record.name, record.key, data, record.path); + }} + /> + ); + }, + }, + ]; + + return cols; + }, [displayData, data, showMetricChart]); return ( - -
- - {expandedSections[section.key] ? : } - {section.title} - -
- - {!metric.isTotal && - !metric.isSubtotal && ( - - )} + + + {formattedValue} + + {yoy !== null && Math.abs(yoy) > 30 && !record.isTotal && ( - {metric.name} + {yoy > 0 ? '↑' : '↓'}{Math.abs(yoy).toFixed(0)}% - {metric.isCore && ( - - 核心 - - )} - - - - - 数值:{' '} - {metric.key.includes('eps') - ? value?.toFixed(3) - : formatUtils.formatLargeNumber(value)} - - 同比: {change.toFixed(2)}% - - } - placement="top" - > - - {metric.key.includes('eps') - ? value?.toFixed(3) - : formatUtils.formatLargeNumber(value, 0)} - - - {Math.abs(change) > 30 && !metric.isTotal && ( - - {change > 0 ? '↑' : '↓'} - {Math.abs(change).toFixed(0)}% - - )} - - } - variant="ghost" - colorScheme="blue" - aria-label="查看图表" - /> -
- - - - {displayData.map((item) => ( - - ))} - - - - - {INCOME_STATEMENT_SECTIONS.map((section) => renderSection(section))} - -
- 项目 - - - {formatUtils.getReportType(item.period)} - - {item.period.substring(0, 10)} - - - 操作
- + + + + { + if (record.isSection) return 'section-header'; + if (record.isTotal) return 'total-row'; + if (record.isSubtotal) return 'subtotal-row'; + return ''; + }} + onRow={(record) => ({ + onClick: () => { + if (!record.isSection) { + showMetricChart(record.name, record.key, data, record.path); + } + }, + style: { cursor: record.isSection ? 'default' : 'pointer' }, + })} + locale={{ emptyText: '暂无数据' }} + /> + + ); }; diff --git a/src/views/Company/components/FinancialPanorama/components/KeyMetricsOverview.tsx b/src/views/Company/components/FinancialPanorama/components/KeyMetricsOverview.tsx new file mode 100644 index 00000000..cff4e47c --- /dev/null +++ b/src/views/Company/components/FinancialPanorama/components/KeyMetricsOverview.tsx @@ -0,0 +1,138 @@ +/** + * 关键指标速览组件 - 黑金主题 + * 展示核心财务指标的快速概览 + */ + +import React, { memo } from 'react'; +import { Box, Heading, SimpleGrid, Text, HStack, Icon } from '@chakra-ui/react'; +import { TrendingUp, TrendingDown, Minus } from 'lucide-react'; +import { formatUtils } from '@services/financialService'; +import type { FinancialMetricsData } from '../types'; + +// 黑金主题样式 +const THEME = { + cardBg: 'transparent', + border: 'rgba(212, 175, 55, 0.2)', + headingColor: '#D4AF37', + itemBg: 'rgba(212, 175, 55, 0.05)', + itemBorder: 'rgba(212, 175, 55, 0.15)', + labelColor: 'gray.400', + valueColor: 'white', + positiveColor: '#22c55e', + negativeColor: '#ef4444', +}; + +// 指标配置 +const KEY_METRICS = [ + { label: 'ROE', path: 'profitability.roe', format: 'percent', higherBetter: true }, + { label: '毛利率', path: 'profitability.gross_margin', format: 'percent', higherBetter: true }, + { label: '净利率', path: 'profitability.net_profit_margin', format: 'percent', higherBetter: true }, + { label: '流动比率', path: 'solvency.current_ratio', format: 'decimal', higherBetter: true }, + { label: '资产负债率', path: 'solvency.asset_liability_ratio', format: 'percent', higherBetter: false }, + { label: '研发费用率', path: 'expense_ratios.rd_expense_ratio', format: 'percent', higherBetter: true }, +]; + +// 通过路径获取值 +const getValueByPath = (obj: FinancialMetricsData, path: string): T | undefined => { + return path.split('.').reduce((acc: unknown, key: string) => { + if (acc && typeof acc === 'object') { + return (acc as Record)[key]; + } + return undefined; + }, obj as unknown) as T | undefined; +}; + +export interface KeyMetricsOverviewProps { + financialMetrics: FinancialMetricsData[]; +} + +export const KeyMetricsOverview: React.FC = memo(({ + financialMetrics, +}) => { + if (!financialMetrics || financialMetrics.length === 0) { + return null; + } + + const currentPeriod = financialMetrics[0]; + const previousPeriod = financialMetrics[1]; + + return ( + + + + 关键指标速览 + + + + + {KEY_METRICS.map((metric, idx) => { + const currentValue = getValueByPath(currentPeriod, metric.path); + const previousValue = previousPeriod + ? getValueByPath(previousPeriod, metric.path) + : undefined; + + // 计算变化 + let change: number | null = null; + let trend: 'up' | 'down' | 'flat' = 'flat'; + if (currentValue !== undefined && previousValue !== undefined && previousValue !== 0) { + change = currentValue - previousValue; + if (Math.abs(change) > 0.01) { + trend = change > 0 ? 'up' : 'down'; + } + } + + // 判断趋势是好是坏 + const isPositiveTrend = metric.higherBetter ? trend === 'up' : trend === 'down'; + const trendColor = trend === 'flat' + ? 'gray.500' + : isPositiveTrend + ? THEME.positiveColor + : THEME.negativeColor; + + return ( + + + {metric.label} + + + + {metric.format === 'percent' + ? formatUtils.formatPercent(currentValue) + : currentValue?.toFixed(2) ?? '-'} + + {trend !== 'flat' && ( + + )} + {trend === 'flat' && ( + + )} + + + ); + })} + + + + ); +}); + +KeyMetricsOverview.displayName = 'KeyMetricsOverview'; + +export default KeyMetricsOverview; diff --git a/src/views/Company/components/FinancialPanorama/components/MainBusinessAnalysis.tsx b/src/views/Company/components/FinancialPanorama/components/MainBusinessAnalysis.tsx index 3bb8067b..f9538f0a 100644 --- a/src/views/Company/components/FinancialPanorama/components/MainBusinessAnalysis.tsx +++ b/src/views/Company/components/FinancialPanorama/components/MainBusinessAnalysis.tsx @@ -1,15 +1,13 @@ /** - * 主营业务分析组件 + * 主营业务分析组件 - 黑金主题 */ -import React from 'react'; +import React, { useMemo } from 'react'; import { VStack, Grid, GridItem, - Card, - CardBody, - CardHeader, + Box, Heading, Table, Thead, @@ -21,6 +19,8 @@ import { Alert, AlertIcon, } from '@chakra-ui/react'; +import { Table as AntTable, ConfigProvider, theme as antTheme } from 'antd'; +import type { ColumnsType } from 'antd/es/table'; import ReactECharts from 'echarts-for-react'; import { formatUtils } from '@services/financialService'; import { getMainBusinessPieOption } from '../utils'; @@ -31,6 +31,142 @@ import type { IndustryClassification, } from '../types'; +// 黑金主题样式 +const THEME = { + cardBg: 'transparent', + border: 'rgba(212, 175, 55, 0.2)', + headingColor: '#D4AF37', + textColor: 'gray.300', + thColor: 'gray.400', +}; + +// Ant Design 黑金主题配置 +const BLACK_GOLD_THEME = { + algorithm: antTheme.darkAlgorithm, + token: { + colorPrimary: '#D4AF37', + colorBgContainer: 'transparent', + colorBgElevated: '#1a1a2e', + colorBorder: 'rgba(212, 175, 55, 0.3)', + colorText: '#e0e0e0', + colorTextSecondary: '#a0a0a0', + borderRadius: 4, + fontSize: 13, + }, + components: { + Table: { + headerBg: 'rgba(212, 175, 55, 0.1)', + headerColor: '#D4AF37', + rowHoverBg: 'rgba(212, 175, 55, 0.05)', + borderColor: 'rgba(212, 175, 55, 0.2)', + cellPaddingBlock: 8, + cellPaddingInline: 12, + }, + }, +}; + +// 历史对比表格数据行类型 +interface HistoricalRowData { + key: string; + business: string; + [period: string]: string | number | undefined; +} + +// 历史对比表格组件 +interface HistoricalComparisonTableProps { + historicalData: (ProductClassification | IndustryClassification)[]; + businessItems: BusinessItem[]; + hasProductData: boolean; +} + +const HistoricalComparisonTable: React.FC = ({ + historicalData, + businessItems, + hasProductData, +}) => { + // 动态生成列配置 + const columns: ColumnsType = useMemo(() => { + const cols: ColumnsType = [ + { + title: '业务/期间', + dataIndex: 'business', + key: 'business', + fixed: 'left', + width: 150, + }, + ]; + + // 添加各期间列 + historicalData.slice(0, 4).forEach((period) => { + cols.push({ + title: period.report_type, + dataIndex: period.period, + key: period.period, + align: 'right', + width: 120, + render: (value: number | string | undefined) => + value !== undefined && value !== '-' + ? formatUtils.formatLargeNumber(value as number) + : '-', + }); + }); + + return cols; + }, [historicalData]); + + // 生成表格数据 + const dataSource: HistoricalRowData[] = useMemo(() => { + return businessItems + .filter((item: BusinessItem) => item.content !== '合计') + .map((item: BusinessItem, idx: number) => { + const row: HistoricalRowData = { + key: `${idx}`, + business: item.content, + }; + + historicalData.slice(0, 4).forEach((period) => { + const periodItems: BusinessItem[] = hasProductData + ? (period as ProductClassification).products + : (period as IndustryClassification).industries; + const matchItem = periodItems.find( + (p: BusinessItem) => p.content === item.content + ); + row[period.period] = matchItem?.revenue ?? '-'; + }); + + return row; + }); + }, [businessItems, historicalData, hasProductData]); + + return ( + + + + 主营业务历史对比 + + + + + + columns={columns} + dataSource={dataSource} + pagination={false} + size="small" + scroll={{ x: 'max-content' }} + bordered + /> + + + + ); +}; + export const MainBusinessAnalysis: React.FC = ({ mainBusiness, }) => { @@ -42,8 +178,8 @@ export const MainBusinessAnalysis: React.FC = ({ if (!hasProductData && !hasIndustryData) { return ( - - + + 暂无主营业务数据 ); @@ -85,26 +221,38 @@ export const MainBusinessAnalysis: React.FC = ({ - - - - - + + + - - - 业务明细 - {latestPeriod.report_type} - - + + + + 业务明细 - {latestPeriod.report_type} + + +
- - - - + + + + @@ -112,69 +260,33 @@ export const MainBusinessAnalysis: React.FC = ({ .filter((item: BusinessItem) => item.content !== '合计') .map((item: BusinessItem, idx: number) => ( - - - + + - + ))}
业务营收毛利率(%)利润业务营收毛利率(%)利润
{item.content}{formatUtils.formatLargeNumber(item.revenue)} + {item.content} + {formatUtils.formatLargeNumber(item.revenue)} + {formatUtils.formatPercent(item.gross_margin || item.profit_margin)} {formatUtils.formatLargeNumber(item.profit)} + {formatUtils.formatLargeNumber(item.profit)} +
- - +
+
- {/* 历史对比 */} + {/* 历史对比 - Ant Design Table 黑金主题 */} {historicalData.length > 1 && ( - - - 主营业务历史对比 - - - - - - - - {historicalData.slice(0, 3).map((period) => ( - - ))} - - - - {businessItems - .filter((item: BusinessItem) => item.content !== '合计') - .map((item: BusinessItem, idx: number) => ( - - - {historicalData.slice(0, 3).map((period) => { - const periodItems: BusinessItem[] = hasProductData - ? (period as ProductClassification).products - : (period as IndustryClassification).industries; - const matchItem = periodItems.find( - (p: BusinessItem) => p.content === item.content - ); - return ( - - ); - })} - - ))} - -
业务/期间 - {period.report_type} -
{item.content} - {matchItem - ? formatUtils.formatLargeNumber(matchItem.revenue) - : '-'} -
-
-
-
+ )} ); diff --git a/src/views/Company/components/FinancialPanorama/components/PeriodSelector.tsx b/src/views/Company/components/FinancialPanorama/components/PeriodSelector.tsx index c18fe5fb..e223c559 100644 --- a/src/views/Company/components/FinancialPanorama/components/PeriodSelector.tsx +++ b/src/views/Company/components/FinancialPanorama/components/PeriodSelector.tsx @@ -1,18 +1,12 @@ /** - * 期数选择器组件 + * 期数选择器组件 - 黑金主题 * 用于选择显示的财务报表期数,并提供刷新功能 */ import React, { memo } from 'react'; -import { - Card, - CardBody, - HStack, - Text, - Select, - IconButton, -} from '@chakra-ui/react'; -import { RepeatIcon } from '@chakra-ui/icons'; +import { HStack, Text, IconButton } from '@chakra-ui/react'; +import { Select } from 'antd'; +import { RefreshCw } from 'lucide-react'; export interface PeriodSelectorProps { /** 当前选中的期数 */ @@ -38,37 +32,62 @@ const PeriodSelector: React.FC = memo(({ label = '显示期数:', }) => { return ( - - - - - - {label} - - - - } - onClick={onRefresh} - isLoading={isLoading} - variant="outline" - size="sm" - aria-label="刷新数据" - /> - - - + + + {label} + + setSelectedPeriods(Number(e.target.value))} - w="150px" - size="sm" - > - - - - - - - } - onClick={refetch} - isLoading={loading} - variant="outline" - size="sm" - aria-label="刷新数据" - /> - - - - {/* 股票信息头部 */} {loading ? ( @@ -276,16 +286,16 @@ const FinancialPanorama: React.FC = ({ stockCode: propSt /> )} - {/* 财务指标速览 */} - {!loading && stockInfo && ( - + {/* 关键指标速览 */} + {!loading && stockInfo && financialMetrics.length > 0 && ( + )} {/* 主营业务 */} {!loading && stockInfo && ( - + - + 主营业务 @@ -302,6 +312,15 @@ const FinancialPanorama: React.FC = ({ stockCode: propSt componentProps={componentProps} themePreset="blackGold" isLazy + onTabChange={handleTabChange} + rightElement={ + + } /> diff --git a/src/views/Company/components/FinancialPanorama/tabs/BalanceSheetTab.tsx b/src/views/Company/components/FinancialPanorama/tabs/BalanceSheetTab.tsx index da25cc02..9257f1a9 100644 --- a/src/views/Company/components/FinancialPanorama/tabs/BalanceSheetTab.tsx +++ b/src/views/Company/components/FinancialPanorama/tabs/BalanceSheetTab.tsx @@ -3,16 +3,7 @@ */ import React from 'react'; -import { - Card, - CardBody, - CardHeader, - VStack, - HStack, - Heading, - Badge, - Text, -} from '@chakra-ui/react'; +import { Box, VStack, HStack, Heading, Badge, Text } from '@chakra-ui/react'; import { BalanceSheetTable } from '../components'; import type { BalanceSheetData } from '../types'; @@ -48,29 +39,25 @@ const BalanceSheetTab: React.FC = ({ }; return ( - - - - - 资产负债表 - - - 显示最近{Math.min(balanceSheet.length, 8)}期 - - - 红涨绿跌 | 同比变化 - - + + + + 资产负债表 + + + 显示最近{Math.min(balanceSheet.length, 8)}期 + + + 红涨绿跌 | 同比变化 + - - 提示:表格可横向滚动查看更多数据,点击行查看历史趋势 - - - - - - - + + + 提示:表格可横向滚动查看更多数据,点击行查看历史趋势 + + + + ); }; diff --git a/src/views/Company/components/FinancialPanorama/tabs/CashflowTab.tsx b/src/views/Company/components/FinancialPanorama/tabs/CashflowTab.tsx index a447bd19..b03d7c9e 100644 --- a/src/views/Company/components/FinancialPanorama/tabs/CashflowTab.tsx +++ b/src/views/Company/components/FinancialPanorama/tabs/CashflowTab.tsx @@ -3,16 +3,7 @@ */ import React from 'react'; -import { - Card, - CardBody, - CardHeader, - VStack, - HStack, - Heading, - Badge, - Text, -} from '@chakra-ui/react'; +import { Box, VStack, HStack, Heading, Badge, Text } from '@chakra-ui/react'; import { CashflowTable } from '../components'; import type { CashflowData } from '../types'; @@ -48,29 +39,25 @@ const CashflowTab: React.FC = ({ }; return ( - - - - - 现金流量表 - - - 显示最近{Math.min(cashflow.length, 8)}期 - - - 红涨绿跌 | 同比变化 - - + + + + 现金流量表 + + + 显示最近{Math.min(cashflow.length, 8)}期 + + + 红涨绿跌 | 同比变化 + - - 提示:现金流数据为累计值,正值红色表示现金流入,负值绿色表示现金流出 - - - - - - - + + + 提示:现金流数据为累计值,正值红色表示现金流入,负值绿色表示现金流出 + + + + ); }; diff --git a/src/views/Company/components/FinancialPanorama/tabs/MetricsTab.tsx b/src/views/Company/components/FinancialPanorama/tabs/FinancialMetricsTab.tsx similarity index 79% rename from src/views/Company/components/FinancialPanorama/tabs/MetricsTab.tsx rename to src/views/Company/components/FinancialPanorama/tabs/FinancialMetricsTab.tsx index 6168a1d8..c8f209e1 100644 --- a/src/views/Company/components/FinancialPanorama/tabs/MetricsTab.tsx +++ b/src/views/Company/components/FinancialPanorama/tabs/FinancialMetricsTab.tsx @@ -6,7 +6,7 @@ import React from 'react'; import { FinancialMetricsTable } from '../components'; import type { FinancialMetricsData } from '../types'; -export interface MetricsTabProps { +export interface FinancialMetricsTabProps { financialMetrics: FinancialMetricsData[]; showMetricChart: (name: string, key: string, data: unknown[], path: string) => void; calculateYoYChange: (value: number, period: string, data: unknown[], path: string) => { change: number; intensity: number }; @@ -17,7 +17,7 @@ export interface MetricsTabProps { hoverBg: string; } -const MetricsTab: React.FC = ({ +const FinancialMetricsTab: React.FC = ({ financialMetrics, showMetricChart, calculateYoYChange, @@ -37,7 +37,9 @@ const MetricsTab: React.FC = ({ hoverBg, }; - return ; + return ( + + ); }; -export default MetricsTab; +export default FinancialMetricsTab; diff --git a/src/views/Company/components/FinancialPanorama/tabs/IncomeStatementTab.tsx b/src/views/Company/components/FinancialPanorama/tabs/IncomeStatementTab.tsx index d574334b..52223625 100644 --- a/src/views/Company/components/FinancialPanorama/tabs/IncomeStatementTab.tsx +++ b/src/views/Company/components/FinancialPanorama/tabs/IncomeStatementTab.tsx @@ -3,16 +3,7 @@ */ import React from 'react'; -import { - Card, - CardBody, - CardHeader, - VStack, - HStack, - Heading, - Badge, - Text, -} from '@chakra-ui/react'; +import { Box, VStack, HStack, Heading, Badge, Text } from '@chakra-ui/react'; import { IncomeStatementTable } from '../components'; import type { IncomeStatementData } from '../types'; @@ -48,29 +39,25 @@ const IncomeStatementTab: React.FC = ({ }; return ( - - - - - 利润表 - - - 显示最近{Math.min(incomeStatement.length, 8)}期 - - - 红涨绿跌 | 同比变化 - - + + + + 利润表 + + + 显示最近{Math.min(incomeStatement.length, 8)}期 + + + 红涨绿跌 | 同比变化 + - - 提示:Q1、中报、Q3、年报数据为累计值,同比显示与去年同期对比 - - - - - - - + + + 提示:Q1、中报、Q3、年报数据为累计值,同比显示与去年同期对比 + + + + ); }; diff --git a/src/views/Company/components/FinancialPanorama/tabs/MainBusinessTab.tsx b/src/views/Company/components/FinancialPanorama/tabs/MainBusinessTab.tsx deleted file mode 100644 index 8e422e55..00000000 --- a/src/views/Company/components/FinancialPanorama/tabs/MainBusinessTab.tsx +++ /dev/null @@ -1,17 +0,0 @@ -/** - * 主营业务 Tab - */ - -import React from 'react'; -import { MainBusinessAnalysis } from '../components'; -import type { MainBusinessData } from '../types'; - -export interface MainBusinessTabProps { - mainBusiness: MainBusinessData | null; -} - -const MainBusinessTab: React.FC = ({ mainBusiness }) => { - return ; -}; - -export default MainBusinessTab; diff --git a/src/views/Company/components/FinancialPanorama/tabs/MetricsCategoryTab.tsx b/src/views/Company/components/FinancialPanorama/tabs/MetricsCategoryTab.tsx new file mode 100644 index 00000000..b224e1d0 --- /dev/null +++ b/src/views/Company/components/FinancialPanorama/tabs/MetricsCategoryTab.tsx @@ -0,0 +1,330 @@ +/** + * 财务指标分类 Tab - Ant Design 黑金主题 + * 接受 categoryKey 显示单个分类的指标表格 + */ + +import React, { useMemo } 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 { FINANCIAL_METRICS_CATEGORIES } from '../constants'; +import { getValueByPath, isNegativeIndicator } from '../utils'; +import type { FinancialMetricsData } from '../types'; + +type CategoryKey = keyof typeof FINANCIAL_METRICS_CATEGORIES; + +// 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 = ` + .metrics-category-table .ant-table { + background: transparent !important; + } + .metrics-category-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; + } + .metrics-category-table .ant-table-tbody > tr > td { + border-bottom: 1px solid rgba(212, 175, 55, 0.1) !important; + color: #E2E8F0; + font-size: 12px; + } + .metrics-category-table .ant-table-tbody > tr:hover > td { + background: rgba(212, 175, 55, 0.08) !important; + } + .metrics-category-table .ant-table-cell-fix-left, + .metrics-category-table .ant-table-cell-fix-right { + background: #1A202C !important; + } + .metrics-category-table .ant-table-tbody > tr:hover .ant-table-cell-fix-left, + .metrics-category-table .ant-table-tbody > tr:hover .ant-table-cell-fix-right { + background: rgba(26, 32, 44, 0.95) !important; + } + .metrics-category-table .positive-change { + color: #E53E3E; + } + .metrics-category-table .negative-change { + color: #48BB78; + } + .metrics-category-table .positive-value { + color: #E53E3E; + } + .metrics-category-table .negative-value { + color: #48BB78; + } + .metrics-category-table .ant-table-placeholder { + background: transparent !important; + } + .metrics-category-table .ant-empty-description { + color: #A0AEC0; + } +`; + +export interface MetricsCategoryTabProps { + categoryKey: CategoryKey; + financialMetrics: FinancialMetricsData[]; + showMetricChart: (name: string, key: string, data: unknown[], path: string) => void; + calculateYoYChange: (value: number, period: string, data: unknown[], path: string) => { change: number; intensity: number }; + getCellBackground: (change: number, intensity: number) => string; + positiveColor: string; + negativeColor: string; + bgColor: string; + hoverBg: string; +} + +// 表格行数据类型 +interface TableRowData { + key: string; + name: string; + path: string; + isCore?: boolean; + [period: string]: unknown; +} + +const MetricsCategoryTab: React.FC = ({ + categoryKey, + financialMetrics, + showMetricChart, + calculateYoYChange, +}) => { + // 数组安全检查 + if (!Array.isArray(financialMetrics) || financialMetrics.length === 0) { + return ( + + 暂无财务指标数据 + + ); + } + + const maxColumns = Math.min(financialMetrics.length, 6); + const displayData = financialMetrics.slice(0, maxColumns); + const category = FINANCIAL_METRICS_CATEGORIES[categoryKey]; + + if (!category) { + return ( + + 未找到指标分类配置 + + ); + } + + // 构建表格数据 + const tableData = useMemo(() => { + return category.metrics.map((metric) => { + const row: TableRowData = { + key: metric.key, + name: metric.name, + path: metric.path, + isCore: metric.isCore, + }; + + // 添加各期数值 + displayData.forEach((item) => { + const value = getValueByPath(item, metric.path); + row[item.period] = value; + }); + + return row; + }); + }, [financialMetrics, displayData, category]); + + // 计算同比变化 + const calculateYoY = ( + currentValue: number | undefined, + currentPeriod: string, + path: string + ): number | null => { + if (currentValue === undefined || currentValue === null) return null; + + const currentDate = new Date(currentPeriod); + const lastYearPeriod = financialMetrics.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(lastYearPeriod, path); + if (lastYearValue === undefined || lastYearValue === 0) return null; + + return ((currentValue - lastYearValue) / Math.abs(lastYearValue)) * 100; + }; + + // 构建列定义 + const columns: ColumnsType = useMemo(() => { + const cols: ColumnsType = [ + { + title: category.title, + dataIndex: 'name', + key: 'name', + fixed: 'left', + width: 200, + render: (name: string, record: TableRowData) => ( + + {name} + {record.isCore && ( + + 核心 + + )} + + ), + }, + ...displayData.map((item) => ({ + title: ( + + {formatUtils.getReportType(item.period)} + {item.period.substring(0, 10)} + + ), + dataIndex: item.period, + key: item.period, + width: 100, + align: 'right' as const, + render: (value: number | undefined, record: TableRowData) => { + const yoy = calculateYoY(value, item.period, record.path); + const isNegative = isNegativeIndicator(record.key); + + // 对于负向指标,增加是坏事(绿色),减少是好事(红色) + const changeColor = isNegative + ? (yoy && yoy > 0 ? 'negative-change' : 'positive-change') + : (yoy && yoy > 0 ? 'positive-change' : 'negative-change'); + + // 成长能力指标特殊处理:正值红色,负值绿色 + const valueColor = categoryKey === 'growth' + ? (value !== undefined && value > 0 ? 'positive-value' : value !== undefined && value < 0 ? 'negative-value' : '') + : ''; + + return ( + + {record.name}: {value?.toFixed(2) || '-'} + {yoy !== null && 同比: {yoy.toFixed(2)}%} + + } + > + + + {value?.toFixed(2) || '-'} + + {yoy !== null && Math.abs(yoy) > 20 && value !== undefined && Math.abs(value) > 0.01 && ( + + {yoy > 0 ? '↑' : '↓'} + + )} + + + ); + }, + })), + { + title: '', + key: 'action', + width: 40, + fixed: 'right', + render: (_: unknown, record: TableRowData) => ( + { + e.stopPropagation(); + showMetricChart(record.name, record.key, financialMetrics, record.path); + }} + /> + ), + }, + ]; + + return cols; + }, [displayData, financialMetrics, showMetricChart, category, categoryKey]); + + return ( + + + + + ({ + onClick: () => { + showMetricChart(record.name, record.key, financialMetrics, record.path); + }, + style: { cursor: 'pointer' }, + })} + locale={{ emptyText: '暂无数据' }} + /> + + + + + ); +}; + +// 为每个分类创建预配置的组件 +export const ProfitabilityTab: React.FC> = (props) => ( + +); + +export const PerShareTab: React.FC> = (props) => ( + +); + +export const GrowthTab: React.FC> = (props) => ( + +); + +export const OperationalTab: React.FC> = (props) => ( + +); + +export const SolvencyTab: React.FC> = (props) => ( + +); + +export const ExpenseTab: React.FC> = (props) => ( + +); + +export const CashflowMetricsTab: React.FC> = (props) => ( + +); + +export default MetricsCategoryTab; diff --git a/src/views/Company/components/FinancialPanorama/tabs/OverviewTab.tsx b/src/views/Company/components/FinancialPanorama/tabs/OverviewTab.tsx deleted file mode 100644 index 95717533..00000000 --- a/src/views/Company/components/FinancialPanorama/tabs/OverviewTab.tsx +++ /dev/null @@ -1,51 +0,0 @@ -/** - * 财务概览 Tab - */ - -import React from 'react'; -import { VStack } from '@chakra-ui/react'; -import { ComparisonAnalysis, FinancialMetricsTable } from '../components'; -import type { FinancialMetricsData, ComparisonData } from '../types'; - -export interface OverviewTabProps { - comparison: ComparisonData[]; - financialMetrics: FinancialMetricsData[]; - showMetricChart: (name: string, key: string, data: unknown[], path: string) => void; - calculateYoYChange: (value: number, period: string, data: unknown[], path: string) => { change: number; intensity: number }; - getCellBackground: (change: number, intensity: number) => string; - positiveColor: string; - negativeColor: string; - bgColor: string; - hoverBg: string; -} - -const OverviewTab: React.FC = ({ - comparison, - financialMetrics, - showMetricChart, - calculateYoYChange, - getCellBackground, - positiveColor, - negativeColor, - bgColor, - hoverBg, -}) => { - const tableProps = { - showMetricChart, - calculateYoYChange, - getCellBackground, - positiveColor, - negativeColor, - bgColor, - hoverBg, - }; - - return ( - - - - - ); -}; - -export default OverviewTab; diff --git a/src/views/Company/components/FinancialPanorama/tabs/index.ts b/src/views/Company/components/FinancialPanorama/tabs/index.ts index 253802de..3d39d62e 100644 --- a/src/views/Company/components/FinancialPanorama/tabs/index.ts +++ b/src/views/Company/components/FinancialPanorama/tabs/index.ts @@ -1,12 +1,28 @@ /** * Tab 组件统一导出 - * 仅保留三大财务报表 Tab */ +// 三大财务报表 export { default as BalanceSheetTab } from './BalanceSheetTab'; export { default as IncomeStatementTab } from './IncomeStatementTab'; export { default as CashflowTab } from './CashflowTab'; +// 财务指标分类 tabs +export { + ProfitabilityTab, + PerShareTab, + GrowthTab, + OperationalTab, + SolvencyTab, + ExpenseTab, + CashflowMetricsTab, +} from './MetricsCategoryTab'; + +// 旧的综合财务指标 tab(保留兼容) +export { default as FinancialMetricsTab } from './FinancialMetricsTab'; + export type { BalanceSheetTabProps } from './BalanceSheetTab'; export type { IncomeStatementTabProps } from './IncomeStatementTab'; export type { CashflowTabProps } from './CashflowTab'; +export type { FinancialMetricsTabProps } from './FinancialMetricsTab'; +export type { MetricsCategoryTabProps } from './MetricsCategoryTab';