diff --git a/src/views/Company/components/FinancialPanorama/components/IndustryRankingView.tsx b/src/views/Company/components/FinancialPanorama/components/IndustryRankingView.tsx index 9c924285..4f3c3e55 100644 --- a/src/views/Company/components/FinancialPanorama/components/IndustryRankingView.tsx +++ b/src/views/Company/components/FinancialPanorama/components/IndustryRankingView.tsx @@ -21,14 +21,23 @@ import type { IndustryRankingViewProps } from '../types'; export const IndustryRankingView: React.FC = ({ industryRank, - bgColor, - borderColor, + bgColor = 'white', + borderColor = 'gray.200', + textColor, + labelColor, }) => { + // 判断是否为深色主题 + const isDarkTheme = bgColor === 'gray.800' || bgColor === 'gray.900'; + const resolvedTextColor = textColor || (isDarkTheme ? 'white' : 'gray.800'); + const resolvedLabelColor = labelColor || (isDarkTheme ? 'gray.400' : 'gray.500'); + const cardBg = isDarkTheme ? 'transparent' : 'white'; + const headingColor = isDarkTheme ? 'yellow.500' : 'gray.800'; + if (!industryRank || !Array.isArray(industryRank) || industryRank.length === 0) { return ( - + - + 暂无行业排名数据 @@ -39,17 +48,32 @@ export const IndustryRankingView: React.FC = ({ return ( {industryRank.map((periodData, periodIdx) => ( - - + + - {periodData.report_type} 行业排名 - {periodData.period} + + {periodData.report_type} 行业排名 + + + {periodData.period} + - + {periodData.rankings?.map((ranking, idx) => ( - + {ranking.industry_name} ({ranking.level_description}) @@ -65,6 +89,15 @@ export const IndustryRankingView: React.FC = ({ metric.key.includes('margin') || metric.key === 'roe'; + // 格式化数值 + const formattedValue = isPercentMetric + ? formatUtils.formatPercent(metricData.value) + : metricData.value?.toFixed(2) ?? '-'; + + const formattedAvg = isPercentMetric + ? formatUtils.formatPercent(metricData.industry_avg) + : metricData.industry_avg?.toFixed(2) ?? '-'; + return ( = ({ borderWidth="1px" borderColor={borderColor} > - + {metric.name} - - - {isPercentMetric - ? formatUtils.formatPercent(metricData.value) - : metricData.value?.toFixed(2) || '-'} + + + {formattedValue} {metricData.rank && ( = ({ )} - - 行业均值:{' '} - {isPercentMetric - ? formatUtils.formatPercent(metricData.industry_avg) - : metricData.industry_avg?.toFixed(2) || '-'} + + 行业均值: {formattedAvg} ); diff --git a/src/views/Company/components/FinancialPanorama/components/PeriodSelector.tsx b/src/views/Company/components/FinancialPanorama/components/PeriodSelector.tsx new file mode 100644 index 00000000..c18fe5fb --- /dev/null +++ b/src/views/Company/components/FinancialPanorama/components/PeriodSelector.tsx @@ -0,0 +1,78 @@ +/** + * 期数选择器组件 + * 用于选择显示的财务报表期数,并提供刷新功能 + */ + +import React, { memo } from 'react'; +import { + Card, + CardBody, + HStack, + Text, + Select, + IconButton, +} from '@chakra-ui/react'; +import { RepeatIcon } from '@chakra-ui/icons'; + +export interface PeriodSelectorProps { + /** 当前选中的期数 */ + selectedPeriods: number; + /** 期数变更回调 */ + onPeriodsChange: (periods: number) => void; + /** 刷新回调 */ + onRefresh: () => void; + /** 是否加载中 */ + isLoading?: boolean; + /** 可选期数列表,默认 [4, 8, 12, 16] */ + periodOptions?: number[]; + /** 标签文本 */ + label?: string; +} + +const PeriodSelector: React.FC = memo(({ + selectedPeriods, + onPeriodsChange, + onRefresh, + isLoading = false, + periodOptions = [4, 8, 12, 16], + label = '显示期数:', +}) => { + return ( + + + + + + {label} + + + + } + onClick={onRefresh} + isLoading={isLoading} + variant="outline" + size="sm" + aria-label="刷新数据" + /> + + + + ); +}); + +PeriodSelector.displayName = 'PeriodSelector'; + +export { PeriodSelector }; +export default PeriodSelector; diff --git a/src/views/Company/components/FinancialPanorama/components/StockInfoHeader.tsx b/src/views/Company/components/FinancialPanorama/components/StockInfoHeader.tsx index 07d634e4..50e93c51 100644 --- a/src/views/Company/components/FinancialPanorama/components/StockInfoHeader.tsx +++ b/src/views/Company/components/FinancialPanorama/components/StockInfoHeader.tsx @@ -1,11 +1,10 @@ /** - * 股票信息头部组件 + * 股票信息头部组件 - 黑金主题 */ import React from 'react'; import { - Card, - CardBody, + Box, Grid, GridItem, VStack, @@ -18,93 +17,153 @@ import { StatNumber, Alert, AlertIcon, - Box, } from '@chakra-ui/react'; import { formatUtils } from '@services/financialService'; import type { StockInfoHeaderProps } from '../types'; +// 黑金主题配置 +const darkGoldTheme = { + bgCard: 'rgba(26, 32, 44, 0.95)', + border: 'rgba(212, 175, 55, 0.3)', + borderHover: 'rgba(212, 175, 55, 0.5)', + gold: '#D4AF37', + goldLight: '#F4D03F', + orange: '#FF9500', + red: '#FF4444', + green: '#00C851', + textPrimary: 'rgba(255, 255, 255, 0.92)', + textSecondary: 'rgba(255, 255, 255, 0.7)', + textMuted: 'rgba(255, 255, 255, 0.5)', + tagBg: 'rgba(212, 175, 55, 0.15)', +}; + export const StockInfoHeader: React.FC = ({ stockInfo, - positiveColor, - negativeColor, }) => { if (!stockInfo) return null; return ( - - - - - - - 股票名称 - - - {stockInfo.stock_name} - {stockInfo.stock_code} - - - - - - 最新EPS - - {stockInfo.key_metrics?.eps?.toFixed(3) || '-'} - - - - - - ROE - - {formatUtils.formatPercent(stockInfo.key_metrics?.roe)} - - - - - - 营收增长 - 0 - ? positiveColor - : negativeColor - : 'gray.500' - } + + + + + + 股票名称 + + + - {formatUtils.formatPercent(stockInfo.growth_rates?.revenue_growth)} - - - - - - 利润增长 - 0 - ? positiveColor - : negativeColor - : 'gray.500' - } + {stockInfo.stock_name} + + - {formatUtils.formatPercent(stockInfo.growth_rates?.profit_growth)} - - - - - {stockInfo.latest_forecast && ( - - - - {stockInfo.latest_forecast.forecast_type} - {stockInfo.latest_forecast.content} - - - )} - - + {stockInfo.stock_code} + + + + + + + + 最新EPS + + + {stockInfo.key_metrics?.eps?.toFixed(3) || '-'} + + + + + + + ROE + + + {formatUtils.formatPercent(stockInfo.key_metrics?.roe)} + + + + + + + 营收增长 + + 0 + ? darkGoldTheme.red + : darkGoldTheme.green + : darkGoldTheme.textMuted + } + > + {formatUtils.formatPercent(stockInfo.growth_rates?.revenue_growth)} + + + + + + + 利润增长 + + 0 + ? darkGoldTheme.red + : darkGoldTheme.green + : darkGoldTheme.textMuted + } + > + {formatUtils.formatPercent(stockInfo.growth_rates?.profit_growth)} + + + + + {stockInfo.latest_forecast && ( + + + + + {stockInfo.latest_forecast.forecast_type} + + + {stockInfo.latest_forecast.content} + + + + )} + ); }; diff --git a/src/views/Company/components/FinancialPanorama/components/index.ts b/src/views/Company/components/FinancialPanorama/components/index.ts index 3463c0ca..1a6b69fc 100644 --- a/src/views/Company/components/FinancialPanorama/components/index.ts +++ b/src/views/Company/components/FinancialPanorama/components/index.ts @@ -2,6 +2,7 @@ * 组件统一导出 */ +export { PeriodSelector } from './PeriodSelector'; export { StockInfoHeader } from './StockInfoHeader'; export { BalanceSheetTable } from './BalanceSheetTable'; export { IncomeStatementTable } from './IncomeStatementTable'; diff --git a/src/views/Company/components/FinancialPanorama/index.tsx b/src/views/Company/components/FinancialPanorama/index.tsx index 269fa643..d0c02ac4 100644 --- a/src/views/Company/components/FinancialPanorama/index.tsx +++ b/src/views/Company/components/FinancialPanorama/index.tsx @@ -1,9 +1,9 @@ /** * 财务全景组件 - * 重构后的主组件,使用模块化结构 + * 重构后的主组件,使用模块化结构和 SubTabContainer 二级导航 */ -import React, { useState, ReactNode } from 'react'; +import React, { useState, useMemo, ReactNode } from 'react'; import { Box, Container, @@ -11,20 +11,12 @@ import { HStack, Card, CardBody, - CardHeader, - Heading, Text, - Badge, Select, IconButton, Alert, AlertIcon, Skeleton, - Tabs, - TabList, - TabPanels, - Tab, - TabPanel, Modal, ModalOverlay, ModalContent, @@ -40,25 +32,25 @@ import { Td, TableContainer, Divider, - Tooltip, } from '@chakra-ui/react'; import { RepeatIcon } from '@chakra-ui/icons'; +import { BarChart3, DollarSign, TrendingUp } from 'lucide-react'; import ReactECharts from 'echarts-for-react'; import { formatUtils } from '@services/financialService'; +// 通用组件 +import SubTabContainer, { type SubTabConfig } from '@components/SubTabContainer'; + // 内部模块导入 import { useFinancialData } from './hooks'; import { COLORS } from './constants'; import { calculateYoYChange, getCellBackground, getMetricChartOption } from './utils'; import { StockInfoHeader, - BalanceSheetTable, - IncomeStatementTable, - CashflowTable, FinancialMetricsTable, MainBusinessAnalysis, - ComparisonAnalysis, } from './components'; +import { BalanceSheetTab, IncomeStatementTab, CashflowTab } from './tabs'; import type { FinancialPanoramaProps } from './types'; /** @@ -73,29 +65,24 @@ const FinancialPanorama: React.FC = ({ stockCode: propSt cashflow, financialMetrics, mainBusiness, - forecast, - industryRank, - comparison, loading, error, refetch, - currentStockCode, selectedPeriods, setSelectedPeriods, } = useFinancialData({ stockCode: propStockCode }); // UI 状态 - const [activeTab, setActiveTab] = useState(0); const { isOpen, onOpen, onClose } = useDisclosure(); const [modalContent, setModalContent] = useState(null); // 颜色配置 - const { bgColor, hoverBg, positiveColor, negativeColor, borderColor } = COLORS; + const { bgColor, hoverBg, positiveColor, negativeColor } = COLORS; // 点击指标行显示图表 const showMetricChart = ( metricName: string, - metricKey: string, + _metricKey: string, data: Array<{ period: string; [key: string]: unknown }>, dataPath: string ) => { @@ -204,6 +191,45 @@ const FinancialPanorama: React.FC = ({ stockCode: propSt hoverBg, }; + // Tab 配置 - 只保留三大财务报表 + const tabConfigs: SubTabConfig[] = useMemo( + () => [ + { key: 'balance', name: '资产负债表', icon: BarChart3, component: BalanceSheetTab }, + { key: 'income', name: '利润表', icon: DollarSign, component: IncomeStatementTab }, + { key: 'cashflow', name: '现金流量表', icon: TrendingUp, component: CashflowTab }, + ], + [] + ); + + // 传递给 Tab 组件的 props + const componentProps = useMemo( + () => ({ + // 数据 + balanceSheet, + incomeStatement, + cashflow, + // 工具函数 + showMetricChart, + calculateYoYChange, + getCellBackground, + // 颜色配置 + positiveColor, + negativeColor, + bgColor, + hoverBg, + }), + [ + balanceSheet, + incomeStatement, + cashflow, + showMetricChart, + positiveColor, + negativeColor, + bgColor, + hoverBg, + ] + ); + return ( @@ -250,124 +276,35 @@ const FinancialPanorama: React.FC = ({ stockCode: propSt /> )} - {/* 主要内容区域 */} + {/* 财务指标速览 */} {!loading && stockInfo && ( - - - 财务概览 - 资产负债表 - 利润表 - 现金流量表 - 财务指标 - 主营业务 - + + )} - - {/* 财务概览 */} - - - - - - + {/* 主营业务 */} + {!loading && stockInfo && ( + + + + 主营业务 + + + + + )} - {/* 资产负债表 */} - - - - - - 资产负债表 - - - 显示最近{Math.min(balanceSheet.length, 8)}期 - - - 红涨绿跌 | 同比变化 - - - - - 提示:表格可横向滚动查看更多数据,点击行查看历史趋势 - - - - - - - - - - {/* 利润表 */} - - - - - - 利润表 - - - 显示最近{Math.min(incomeStatement.length, 8)}期 - - - 红涨绿跌 | 同比变化 - - - - - 提示:Q1、中报、Q3、年报数据为累计值,同比显示与去年同期对比 - - - - - - - - - - {/* 现金流量表 */} - - - - - - 现金流量表 - - - 显示最近{Math.min(cashflow.length, 8)}期 - - - 红涨绿跌 | 同比变化 - - - - - 提示:现金流数据为累计值,正值红色表示现金流入,负值绿色表示现金流出 - - - - - - - - - - {/* 财务指标 */} - - - - - {/* 主营业务 */} - - - - - + {/* 三大财务报表 - 使用 SubTabContainer 二级导航 */} + {!loading && stockInfo && ( + + + + + )} {/* 错误提示 */} diff --git a/src/views/Company/components/FinancialPanorama/tabs/BalanceSheetTab.tsx b/src/views/Company/components/FinancialPanorama/tabs/BalanceSheetTab.tsx new file mode 100644 index 00000000..da25cc02 --- /dev/null +++ b/src/views/Company/components/FinancialPanorama/tabs/BalanceSheetTab.tsx @@ -0,0 +1,77 @@ +/** + * 资产负债表 Tab + */ + +import React from 'react'; +import { + Card, + CardBody, + CardHeader, + VStack, + HStack, + Heading, + Badge, + Text, +} from '@chakra-ui/react'; +import { BalanceSheetTable } from '../components'; +import type { BalanceSheetData } from '../types'; + +export interface BalanceSheetTabProps { + balanceSheet: BalanceSheetData[]; + 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 BalanceSheetTab: React.FC = ({ + balanceSheet, + showMetricChart, + calculateYoYChange, + getCellBackground, + positiveColor, + negativeColor, + bgColor, + hoverBg, +}) => { + const tableProps = { + showMetricChart, + calculateYoYChange, + getCellBackground, + positiveColor, + negativeColor, + bgColor, + hoverBg, + }; + + return ( + + + + + 资产负债表 + + + 显示最近{Math.min(balanceSheet.length, 8)}期 + + + 红涨绿跌 | 同比变化 + + + + + 提示:表格可横向滚动查看更多数据,点击行查看历史趋势 + + + + + + + + ); +}; + +export default BalanceSheetTab; diff --git a/src/views/Company/components/FinancialPanorama/tabs/CashflowTab.tsx b/src/views/Company/components/FinancialPanorama/tabs/CashflowTab.tsx new file mode 100644 index 00000000..a447bd19 --- /dev/null +++ b/src/views/Company/components/FinancialPanorama/tabs/CashflowTab.tsx @@ -0,0 +1,77 @@ +/** + * 现金流量表 Tab + */ + +import React from 'react'; +import { + Card, + CardBody, + CardHeader, + VStack, + HStack, + Heading, + Badge, + Text, +} from '@chakra-ui/react'; +import { CashflowTable } from '../components'; +import type { CashflowData } from '../types'; + +export interface CashflowTabProps { + cashflow: CashflowData[]; + 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 CashflowTab: React.FC = ({ + cashflow, + showMetricChart, + calculateYoYChange, + getCellBackground, + positiveColor, + negativeColor, + bgColor, + hoverBg, +}) => { + const tableProps = { + showMetricChart, + calculateYoYChange, + getCellBackground, + positiveColor, + negativeColor, + bgColor, + hoverBg, + }; + + return ( + + + + + 现金流量表 + + + 显示最近{Math.min(cashflow.length, 8)}期 + + + 红涨绿跌 | 同比变化 + + + + + 提示:现金流数据为累计值,正值红色表示现金流入,负值绿色表示现金流出 + + + + + + + + ); +}; + +export default CashflowTab; diff --git a/src/views/Company/components/FinancialPanorama/tabs/IncomeStatementTab.tsx b/src/views/Company/components/FinancialPanorama/tabs/IncomeStatementTab.tsx new file mode 100644 index 00000000..d574334b --- /dev/null +++ b/src/views/Company/components/FinancialPanorama/tabs/IncomeStatementTab.tsx @@ -0,0 +1,77 @@ +/** + * 利润表 Tab + */ + +import React from 'react'; +import { + Card, + CardBody, + CardHeader, + VStack, + HStack, + Heading, + Badge, + Text, +} from '@chakra-ui/react'; +import { IncomeStatementTable } from '../components'; +import type { IncomeStatementData } from '../types'; + +export interface IncomeStatementTabProps { + incomeStatement: IncomeStatementData[]; + 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 IncomeStatementTab: React.FC = ({ + incomeStatement, + showMetricChart, + calculateYoYChange, + getCellBackground, + positiveColor, + negativeColor, + bgColor, + hoverBg, +}) => { + const tableProps = { + showMetricChart, + calculateYoYChange, + getCellBackground, + positiveColor, + negativeColor, + bgColor, + hoverBg, + }; + + return ( + + + + + 利润表 + + + 显示最近{Math.min(incomeStatement.length, 8)}期 + + + 红涨绿跌 | 同比变化 + + + + + 提示:Q1、中报、Q3、年报数据为累计值,同比显示与去年同期对比 + + + + + + + + ); +}; + +export default IncomeStatementTab; diff --git a/src/views/Company/components/FinancialPanorama/tabs/MainBusinessTab.tsx b/src/views/Company/components/FinancialPanorama/tabs/MainBusinessTab.tsx new file mode 100644 index 00000000..8e422e55 --- /dev/null +++ b/src/views/Company/components/FinancialPanorama/tabs/MainBusinessTab.tsx @@ -0,0 +1,17 @@ +/** + * 主营业务 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/MetricsTab.tsx b/src/views/Company/components/FinancialPanorama/tabs/MetricsTab.tsx new file mode 100644 index 00000000..6168a1d8 --- /dev/null +++ b/src/views/Company/components/FinancialPanorama/tabs/MetricsTab.tsx @@ -0,0 +1,43 @@ +/** + * 财务指标 Tab + */ + +import React from 'react'; +import { FinancialMetricsTable } from '../components'; +import type { FinancialMetricsData } from '../types'; + +export interface MetricsTabProps { + 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 MetricsTab: React.FC = ({ + financialMetrics, + showMetricChart, + calculateYoYChange, + getCellBackground, + positiveColor, + negativeColor, + bgColor, + hoverBg, +}) => { + const tableProps = { + showMetricChart, + calculateYoYChange, + getCellBackground, + positiveColor, + negativeColor, + bgColor, + hoverBg, + }; + + return ; +}; + +export default MetricsTab; diff --git a/src/views/Company/components/FinancialPanorama/tabs/OverviewTab.tsx b/src/views/Company/components/FinancialPanorama/tabs/OverviewTab.tsx new file mode 100644 index 00000000..95717533 --- /dev/null +++ b/src/views/Company/components/FinancialPanorama/tabs/OverviewTab.tsx @@ -0,0 +1,51 @@ +/** + * 财务概览 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 new file mode 100644 index 00000000..253802de --- /dev/null +++ b/src/views/Company/components/FinancialPanorama/tabs/index.ts @@ -0,0 +1,12 @@ +/** + * Tab 组件统一导出 + * 仅保留三大财务报表 Tab + */ + +export { default as BalanceSheetTab } from './BalanceSheetTab'; +export { default as IncomeStatementTab } from './IncomeStatementTab'; +export { default as CashflowTab } from './CashflowTab'; + +export type { BalanceSheetTabProps } from './BalanceSheetTab'; +export type { IncomeStatementTabProps } from './IncomeStatementTab'; +export type { CashflowTabProps } from './CashflowTab'; diff --git a/src/views/Company/components/FinancialPanorama/types.ts b/src/views/Company/components/FinancialPanorama/types.ts index 6bf2d9c8..6ab1c1b8 100644 --- a/src/views/Company/components/FinancialPanorama/types.ts +++ b/src/views/Company/components/FinancialPanorama/types.ts @@ -392,8 +392,10 @@ export interface MainBusinessAnalysisProps { /** 行业排名 Props */ export interface IndustryRankingViewProps { industryRank: IndustryRankData[]; - bgColor: string; - borderColor: string; + bgColor?: string; + borderColor?: string; + textColor?: string; + labelColor?: string; } /** 股票对比 Props */