From 35e3b666841c30777bd200fb917862fdf1e5c96d Mon Sep 17 00:00:00 2001 From: zdl <3489966805@qq.com> Date: Fri, 12 Dec 2025 15:02:05 +0800 Subject: [PATCH] =?UTF-8?q?refactor(FinancialPanorama):=20=E9=87=8D?= =?UTF-8?q?=E6=9E=84=E4=B8=BB=E7=BB=84=E4=BB=B6=E4=B8=BA=E6=A8=A1=E5=9D=97?= =?UTF-8?q?=E5=8C=96=E7=BB=93=E6=9E=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 从 2,150 行单文件重构为模块化 TypeScript 组件: - 使用 useFinancialData Hook 管理数据加载 - 组合9个子组件渲染9个Tab面板 - 保留指标图表弹窗功能 - 保留期数选择器功能 - 删除旧的 index.js(2,150行) - 新增 index.tsx(454行,精简79%) 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- .../components/FinancialPanorama/index.js | 2151 ----------------- .../components/FinancialPanorama/index.tsx | 453 ++++ 2 files changed, 453 insertions(+), 2151 deletions(-) delete mode 100644 src/views/Company/components/FinancialPanorama/index.js create mode 100644 src/views/Company/components/FinancialPanorama/index.tsx diff --git a/src/views/Company/components/FinancialPanorama/index.js b/src/views/Company/components/FinancialPanorama/index.js deleted file mode 100644 index 9e85945a..00000000 --- a/src/views/Company/components/FinancialPanorama/index.js +++ /dev/null @@ -1,2151 +0,0 @@ -// src/views/Company/FinancialPanorama.jsx -import React, { useState, useEffect, useMemo } from 'react'; -import { logger } from '@utils/logger'; -import { - Box, - Container, - Tabs, - TabList, - TabPanels, - Tab, - TabPanel, - Heading, - Text, - Table, - Thead, - Tbody, - Tr, - Th, - Td, - TableContainer, - Stat, - StatLabel, - StatNumber, - StatHelpText, - StatArrow, - SimpleGrid, - Card, - CardBody, - CardHeader, - Spinner, - Center, - Alert, - AlertIcon, - Badge, - VStack, - HStack, - Divider, - Select, - Button, - Tooltip, - Progress, - Modal, - ModalOverlay, - ModalContent, - ModalHeader, - ModalBody, - ModalCloseButton, - useDisclosure, - Input, - Flex, - Tag, - TagLabel, - IconButton, - useToast, - Skeleton, - SkeletonText, - Grid, - GridItem, - ButtonGroup, - Stack, - Collapse, -} from '@chakra-ui/react'; -import { - ChevronDownIcon, - ChevronUpIcon, - InfoIcon, - DownloadIcon, - RepeatIcon, - SearchIcon, - ViewIcon, - TimeIcon, - ArrowUpIcon, - ArrowDownIcon, -} from '@chakra-ui/icons'; -import ReactECharts from 'echarts-for-react'; -import { financialService, formatUtils, chartUtils } from '@services/financialService'; - -const FinancialPanorama = ({ stockCode: propStockCode }) => { - // 状态管理 - const [stockCode, setStockCode] = useState(propStockCode || '600000'); - const [loading, setLoading] = useState(false); - const [error, setError] = useState(null); - const [selectedPeriods, setSelectedPeriods] = useState(8); - const [activeTab, setActiveTab] = useState(0); - - // 财务数据状态 - const [stockInfo, setStockInfo] = useState(null); - const [balanceSheet, setBalanceSheet] = useState([]); - const [incomeStatement, setIncomeStatement] = useState([]); - const [cashflow, setCashflow] = useState([]); - const [financialMetrics, setFinancialMetrics] = useState([]); - const [mainBusiness, setMainBusiness] = useState(null); - const [forecast, setForecast] = useState(null); - const [industryRank, setIndustryRank] = useState([]); - const [comparison, setComparison] = useState([]); - - // UI状态 - const { isOpen, onOpen, onClose } = useDisclosure(); - const [modalContent, setModalContent] = useState(null); - const [expandedRows, setExpandedRows] = useState({}); - const toast = useToast(); - - // 颜色配置(中国市场:红涨绿跌) - const bgColor = 'white'; - const borderColor = 'gray.200'; - const hoverBg = 'gray.50'; - const positiveColor = 'red.500'; // 红涨 - const negativeColor = 'green.500'; // 绿跌 - - // 加载所有财务数据 - const loadFinancialData = async () => { - if (!stockCode || stockCode.length !== 6) { - logger.warn('FinancialPanorama', 'loadFinancialData', '无效的股票代码', { stockCode }); - toast({ - title: '请输入有效的6位股票代码', - status: 'warning', - duration: 3000, - }); - return; - } - - logger.debug('FinancialPanorama', '开始加载财务数据', { stockCode, selectedPeriods }); - setLoading(true); - setError(null); - - try { - // 并行加载所有数据 - const [ - stockInfoRes, - balanceRes, - incomeRes, - cashflowRes, - metricsRes, - businessRes, - forecastRes, - rankRes, - comparisonRes - ] = await Promise.all([ - financialService.getStockInfo(stockCode), - financialService.getBalanceSheet(stockCode, selectedPeriods), - financialService.getIncomeStatement(stockCode, selectedPeriods), - financialService.getCashflow(stockCode, selectedPeriods), - financialService.getFinancialMetrics(stockCode, selectedPeriods), - financialService.getMainBusiness(stockCode, 4), - financialService.getForecast(stockCode), - financialService.getIndustryRank(stockCode, 4), - financialService.getPeriodComparison(stockCode, selectedPeriods) - ]); - - // 设置数据 - if (stockInfoRes.success) setStockInfo(stockInfoRes.data); - if (balanceRes.success) setBalanceSheet(balanceRes.data); - if (incomeRes.success) setIncomeStatement(incomeRes.data); - if (cashflowRes.success) setCashflow(cashflowRes.data); - if (metricsRes.success) setFinancialMetrics(metricsRes.data); - if (businessRes.success) setMainBusiness(businessRes.data); - if (forecastRes.success) setForecast(forecastRes.data); - if (rankRes.success) setIndustryRank(rankRes.data); - if (comparisonRes.success) setComparison(comparisonRes.data); - - // ❌ 移除数据加载成功toast - logger.info('FinancialPanorama', '财务数据加载成功', { stockCode }); - } catch (err) { - setError(err.message); - logger.error('FinancialPanorama', 'loadFinancialData', err, { stockCode, selectedPeriods }); - - // ❌ 移除数据加载失败toast - // toast({ title: '数据加载失败', description: err.message, status: 'error', duration: 5000 }); - } finally { - setLoading(false); - } - }; - - // 监听props中的stockCode变化 - useEffect(() => { - if (propStockCode && propStockCode !== stockCode) { - setStockCode(propStockCode); - } - }, [propStockCode, stockCode]); - - // 初始加载 - useEffect(() => { - if (stockCode) { - loadFinancialData(); - } - }, [stockCode, selectedPeriods]); - - // 计算同比变化率 - const calculateYoYChange = (currentValue, currentPeriod, allData, metricPath) => { - if (!currentValue || !currentPeriod) return { change: 0, intensity: 0 }; - - // 找到去年同期的数据 - const currentDate = new Date(currentPeriod); - const currentYear = currentDate.getFullYear(); - const currentMonth = currentDate.getMonth() + 1; - - // 查找去年同期 - const lastYearSamePeriod = allData.find(item => { - const itemDate = new Date(item.period); - const itemYear = itemDate.getFullYear(); - const itemMonth = itemDate.getMonth() + 1; - return itemYear === currentYear - 1 && itemMonth === currentMonth; - }); - - if (!lastYearSamePeriod) return { change: 0, intensity: 0 }; - - const previousValue = metricPath.split('.').reduce((obj, key) => obj?.[key], lastYearSamePeriod); - - if (!previousValue || previousValue === 0) return { change: 0, intensity: 0 }; - - const change = ((currentValue - previousValue) / Math.abs(previousValue)) * 100; - const intensity = Math.min(Math.abs(change) / 50, 1); // 50%变化达到最大强度 - return { change, intensity }; - }; - - // 获取单元格背景色(中国市场颜色) - const getCellBackground = (change, intensity) => { - if (change > 0) { - return `rgba(239, 68, 68, ${intensity * 0.15})`; // 红色背景,涨 - } else if (change < 0) { - return `rgba(34, 197, 94, ${intensity * 0.15})`; // 绿色背景,跌 - } - return 'transparent'; - }; - - // 点击指标行显示图表 - const showMetricChart = (metricName, metricKey, data, dataPath) => { - const chartData = data.map(item => { - const value = dataPath.split('.').reduce((obj, key) => obj?.[key], item); - return { - period: formatUtils.getReportType(item.period), - date: item.period, - value: value - }; - }).reverse(); - - const option = { - title: { - text: metricName, - left: 'center' - }, - tooltip: { - trigger: 'axis', - formatter: (params) => { - const value = params[0].value; - const formattedValue = value > 10000 ? - formatUtils.formatLargeNumber(value) : - value?.toFixed(2); - return `${params[0].name}
${metricName}: ${formattedValue}`; - } - }, - xAxis: { - type: 'category', - data: chartData.map(d => d.period), - axisLabel: { - rotate: 45 - } - }, - yAxis: { - type: 'value', - axisLabel: { - formatter: (value) => { - if (Math.abs(value) >= 100000000) { - return (value / 100000000).toFixed(0) + '亿'; - } else if (Math.abs(value) >= 10000) { - return (value / 10000).toFixed(0) + '万'; - } - return value.toFixed(0); - } - } - }, - series: [{ - type: 'bar', - data: chartData.map(d => d.value), - itemStyle: { - color: (params) => { - const idx = params.dataIndex; - if (idx === 0) return '#3182CE'; - const prevValue = chartData[idx - 1].value; - const currValue = params.value; - // 中国市场颜色:红涨绿跌 - return currValue >= prevValue ? '#EF4444' : '#10B981'; - } - }, - label: { - show: true, - position: 'top', - formatter: (params) => { - const value = params.value; - if (Math.abs(value) >= 100000000) { - return (value / 100000000).toFixed(1) + '亿'; - } else if (Math.abs(value) >= 10000) { - return (value / 10000).toFixed(1) + '万'; - } else if (Math.abs(value) >= 1) { - return value.toFixed(1); - } - return value.toFixed(2); - } - } - }] - }; - - setModalContent( - - - - - - - - - - - - - - - {chartData.map((item, idx) => { - // 计算环比 - const qoq = idx > 0 ? - ((item.value - chartData[idx - 1].value) / Math.abs(chartData[idx - 1].value) * 100) : null; - - // 计算同比 - const currentDate = new Date(item.date); - const lastYearItem = chartData.find(d => { - const date = new Date(d.date); - return date.getFullYear() === currentDate.getFullYear() - 1 && - date.getMonth() === currentDate.getMonth(); - }); - const yoy = lastYearItem ? - ((item.value - lastYearItem.value) / Math.abs(lastYearItem.value) * 100) : null; - - return ( - - - - - - - ); - })} - -
报告期数值同比环比
{item.period}{formatUtils.formatLargeNumber(item.value)} 0 ? positiveColor : yoy < 0 ? negativeColor : 'gray.500'}> - {yoy ? `${yoy.toFixed(2)}%` : '-'} - 0 ? positiveColor : qoq < 0 ? negativeColor : 'gray.500'}> - {qoq ? `${qoq.toFixed(2)}%` : '-'} -
-
-
- ); - onOpen(); - }; - - // 资产负债表组件 - 完整版 - const BalanceSheetTable = () => { - const [expandedSections, setExpandedSections] = useState({ - currentAssets: true, - nonCurrentAssets: true, - currentLiabilities: true, - nonCurrentLiabilities: true, - equity: true - }); - - const toggleSection = (section) => { - setExpandedSections(prev => ({ - ...prev, - [section]: !prev[section] - })); - }; - - // 完整的资产负债表指标 - const assetSections = [ - { - title: '流动资产', - key: 'currentAssets', - metrics: [ - { name: '货币资金', key: 'cash', path: 'assets.current_assets.cash', isCore: true }, - { name: '交易性金融资产', key: 'trading_financial_assets', path: 'assets.current_assets.trading_financial_assets' }, - { name: '应收票据', key: 'notes_receivable', path: 'assets.current_assets.notes_receivable' }, - { name: '应收账款', key: 'accounts_receivable', path: 'assets.current_assets.accounts_receivable', isCore: true }, - { name: '预付款项', key: 'prepayments', path: 'assets.current_assets.prepayments' }, - { name: '其他应收款', key: 'other_receivables', path: 'assets.current_assets.other_receivables' }, - { name: '存货', key: 'inventory', path: 'assets.current_assets.inventory', isCore: true }, - { name: '合同资产', key: 'contract_assets', path: 'assets.current_assets.contract_assets' }, - { name: '其他流动资产', key: 'other_current_assets', path: 'assets.current_assets.other_current_assets' }, - { name: '流动资产合计', key: 'total_current_assets', path: 'assets.current_assets.total', isTotal: true }, - ] - }, - { - title: '非流动资产', - key: 'nonCurrentAssets', - metrics: [ - { name: '长期股权投资', key: 'long_term_equity_investments', path: 'assets.non_current_assets.long_term_equity_investments' }, - { name: '投资性房地产', key: 'investment_property', path: 'assets.non_current_assets.investment_property' }, - { name: '固定资产', key: 'fixed_assets', path: 'assets.non_current_assets.fixed_assets', isCore: true }, - { name: '在建工程', key: 'construction_in_progress', path: 'assets.non_current_assets.construction_in_progress' }, - { name: '使用权资产', key: 'right_of_use_assets', path: 'assets.non_current_assets.right_of_use_assets' }, - { name: '无形资产', key: 'intangible_assets', path: 'assets.non_current_assets.intangible_assets', isCore: true }, - { name: '商誉', key: 'goodwill', path: 'assets.non_current_assets.goodwill', isCore: true }, - { name: '递延所得税资产', key: 'deferred_tax_assets', path: 'assets.non_current_assets.deferred_tax_assets' }, - { name: '其他非流动资产', key: 'other_non_current_assets', path: 'assets.non_current_assets.other_non_current_assets' }, - { name: '非流动资产合计', key: 'total_non_current_assets', path: 'assets.non_current_assets.total', isTotal: true }, - ] - }, - { - title: '资产总计', - key: 'totalAssets', - metrics: [ - { name: '资产总计', key: 'total_assets', path: 'assets.total', isTotal: true, isCore: true }, - ] - } - ]; - - const liabilitySections = [ - { - title: '流动负债', - key: 'currentLiabilities', - metrics: [ - { name: '短期借款', key: 'short_term_borrowings', path: 'liabilities.current_liabilities.short_term_borrowings', isCore: true }, - { name: '应付票据', key: 'notes_payable', path: 'liabilities.current_liabilities.notes_payable' }, - { name: '应付账款', key: 'accounts_payable', path: 'liabilities.current_liabilities.accounts_payable', isCore: true }, - { name: '预收款项', key: 'advance_receipts', path: 'liabilities.current_liabilities.advance_receipts' }, - { name: '合同负债', key: 'contract_liabilities', path: 'liabilities.current_liabilities.contract_liabilities' }, - { name: '应付职工薪酬', key: 'employee_compensation_payable', path: 'liabilities.current_liabilities.employee_compensation_payable' }, - { name: '应交税费', key: 'taxes_payable', path: 'liabilities.current_liabilities.taxes_payable' }, - { name: '其他应付款', key: 'other_payables', path: 'liabilities.current_liabilities.other_payables' }, - { name: '一年内到期的非流动负债', key: 'non_current_due_within_one_year', path: 'liabilities.current_liabilities.non_current_liabilities_due_within_one_year' }, - { name: '流动负债合计', key: 'total_current_liabilities', path: 'liabilities.current_liabilities.total', isTotal: true }, - ] - }, - { - title: '非流动负债', - key: 'nonCurrentLiabilities', - metrics: [ - { name: '长期借款', key: 'long_term_borrowings', path: 'liabilities.non_current_liabilities.long_term_borrowings', isCore: true }, - { name: '应付债券', key: 'bonds_payable', path: 'liabilities.non_current_liabilities.bonds_payable' }, - { name: '租赁负债', key: 'lease_liabilities', path: 'liabilities.non_current_liabilities.lease_liabilities' }, - { name: '递延所得税负债', key: 'deferred_tax_liabilities', path: 'liabilities.non_current_liabilities.deferred_tax_liabilities' }, - { name: '其他非流动负债', key: 'other_non_current_liabilities', path: 'liabilities.non_current_liabilities.other_non_current_liabilities' }, - { name: '非流动负债合计', key: 'total_non_current_liabilities', path: 'liabilities.non_current_liabilities.total', isTotal: true }, - ] - }, - { - title: '负债合计', - key: 'totalLiabilities', - metrics: [ - { name: '负债合计', key: 'total_liabilities', path: 'liabilities.total', isTotal: true, isCore: true }, - ] - } - ]; - - const equitySections = [ - { - title: '股东权益', - key: 'equity', - metrics: [ - { name: '股本', key: 'share_capital', path: 'equity.share_capital', isCore: true }, - { name: '资本公积', key: 'capital_reserve', path: 'equity.capital_reserve' }, - { name: '盈余公积', key: 'surplus_reserve', path: 'equity.surplus_reserve' }, - { name: '未分配利润', key: 'undistributed_profit', path: 'equity.undistributed_profit', isCore: true }, - { name: '库存股', key: 'treasury_stock', path: 'equity.treasury_stock' }, - { name: '其他综合收益', key: 'other_comprehensive_income', path: 'equity.other_comprehensive_income' }, - { name: '归属母公司股东权益', key: 'parent_company_equity', path: 'equity.parent_company_equity', isCore: true }, - { name: '少数股东权益', key: 'minority_interests', path: 'equity.minority_interests' }, - { name: '股东权益合计', key: 'total_equity', path: 'equity.total', isTotal: true, isCore: true }, - ] - } - ]; - - // 数组安全检查 - if (!Array.isArray(balanceSheet) || balanceSheet.length === 0) { - return ( - - - 暂无资产负债表数据 - - ); - } - - const maxColumns = Math.min(balanceSheet.length, 6); - const displayData = balanceSheet.slice(0, maxColumns); - - const renderSection = (sections, sectionType) => ( - <> - {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 = balanceSheet.map(item => { - const value = metric.path.split('.').reduce((obj, key) => obj?.[key], item); - return value; - }); - - return ( - showMetricChart(metric.name, metric.key, balanceSheet, 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, - item.period, - balanceSheet, - metric.path - ); - - 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" - onClick={(e) => { - e.stopPropagation(); - showMetricChart(metric.name, metric.key, balanceSheet, metric.path); - }} - /> - - - ); - })} - - ))} - - ); - - return ( - - - - - - {displayData.map(item => ( - - ))} - - - - - {renderSection(assetSections, 'assets')} - - {renderSection(liabilitySections, 'liabilities')} - - {renderSection(equitySections, 'equity')} - -
项目 - - {formatUtils.getReportType(item.period)} - - {item.period.substring(0, 10)} - - - 操作
-
- ); - }; - - // 利润表组件 - 完整版 - const IncomeStatementTable = () => { - const [expandedSections, setExpandedSections] = useState({ - revenue: true, - costs: true, - profits: true, - eps: true - }); - - const toggleSection = (section) => { - setExpandedSections(prev => ({ - ...prev, - [section]: !prev[section] - })); - }; - - const sections = [ - { - title: '营业收入', - key: 'revenue', - metrics: [ - { name: '营业总收入', key: 'total_revenue', path: 'revenue.total_operating_revenue', isCore: true }, - { name: '营业收入', key: 'revenue', path: 'revenue.operating_revenue', isCore: true }, - { name: '其他业务收入', key: 'other_income', path: 'revenue.other_income' }, - ] - }, - { - title: '营业成本与费用', - key: 'costs', - metrics: [ - { name: '营业总成本', key: 'total_cost', path: 'costs.total_operating_cost', isTotal: true }, - { name: '营业成本', key: 'cost', path: 'costs.operating_cost', isCore: true }, - { name: '税金及附加', key: 'taxes_and_surcharges', path: 'costs.taxes_and_surcharges' }, - { name: '销售费用', key: 'selling_expenses', path: 'costs.selling_expenses', isCore: true }, - { name: '管理费用', key: 'admin_expenses', path: 'costs.admin_expenses', isCore: true }, - { name: '研发费用', key: 'rd_expenses', path: 'costs.rd_expenses', isCore: true }, - { name: '财务费用', key: 'financial_expenses', path: 'costs.financial_expenses' }, - { name: ' 其中:利息费用', key: 'interest_expense', path: 'costs.interest_expense' }, - { name: '    利息收入', key: 'interest_income', path: 'costs.interest_income' }, - { name: '三费合计', key: 'three_expenses', path: 'costs.three_expenses_total', isSubtotal: true }, - { name: '四费合计(含研发)', key: 'four_expenses', path: 'costs.four_expenses_total', isSubtotal: true }, - { name: '资产减值损失', key: 'asset_impairment', path: 'costs.asset_impairment_loss' }, - { name: '信用减值损失', key: 'credit_impairment', path: 'costs.credit_impairment_loss' }, - ] - }, - { - title: '其他收益', - key: 'otherGains', - metrics: [ - { name: '公允价值变动收益', key: 'fair_value_change', path: 'other_gains.fair_value_change' }, - { name: '投资收益', key: 'investment_income', path: 'other_gains.investment_income', isCore: true }, - { name: ' 其中:对联营企业和合营企业的投资收益', key: 'investment_income_associates', path: 'other_gains.investment_income_from_associates' }, - { name: '汇兑收益', key: 'exchange_income', path: 'other_gains.exchange_income' }, - { name: '资产处置收益', key: 'asset_disposal_income', path: 'other_gains.asset_disposal_income' }, - ] - }, - { - title: '利润', - key: 'profits', - metrics: [ - { name: '营业利润', key: 'operating_profit', path: 'profit.operating_profit', isCore: true, isTotal: true }, - { name: '加:营业外收入', key: 'non_operating_income', path: 'non_operating.non_operating_income' }, - { name: '减:营业外支出', key: 'non_operating_expenses', path: 'non_operating.non_operating_expenses' }, - { name: '利润总额', key: 'total_profit', path: 'profit.total_profit', isCore: true, isTotal: true }, - { name: '减:所得税费用', key: 'income_tax', path: 'profit.income_tax_expense' }, - { name: '净利润', key: 'net_profit', path: 'profit.net_profit', isCore: true, isTotal: true }, - { name: ' 归属母公司所有者的净利润', key: 'parent_net_profit', path: 'profit.parent_net_profit', isCore: true }, - { name: ' 少数股东损益', key: 'minority_profit', path: 'profit.minority_profit' }, - { name: '持续经营净利润', key: 'continuing_net_profit', path: 'profit.continuing_operations_net_profit' }, - { name: '终止经营净利润', key: 'discontinued_net_profit', path: 'profit.discontinued_operations_net_profit' }, - ] - }, - { - title: '每股收益', - key: 'eps', - metrics: [ - { name: '基本每股收益(元)', key: 'basic_eps', path: 'per_share.basic_eps', isCore: true }, - { name: '稀释每股收益(元)', key: 'diluted_eps', path: 'per_share.diluted_eps' }, - ] - }, - { - title: '综合收益', - key: 'comprehensive', - metrics: [ - { name: '其他综合收益(税后)', key: 'other_comprehensive_income', path: 'comprehensive_income.other_comprehensive_income' }, - { name: '综合收益总额', key: 'total_comprehensive_income', path: 'comprehensive_income.total_comprehensive_income', isTotal: true }, - { name: ' 归属母公司', key: 'parent_comprehensive_income', path: 'comprehensive_income.parent_comprehensive_income' }, - { name: ' 归属少数股东', key: 'minority_comprehensive_income', path: 'comprehensive_income.minority_comprehensive_income' }, - ] - } - ]; - - // 数组安全检查 - if (!Array.isArray(incomeStatement) || incomeStatement.length === 0) { - return ( - - - 暂无利润表数据 - - ); - } - - const maxColumns = Math.min(incomeStatement.length, 6); - const displayData = incomeStatement.slice(0, maxColumns); - - const renderSection = (section) => ( - - toggleSection(section.key)} - > - - - {expandedSections[section.key] ? : } - {section.title} - - - - {expandedSections[section.key] && section.metrics.map(metric => { - const rowData = incomeStatement.map(item => { - const value = metric.path.split('.').reduce((obj, key) => obj?.[key], item); - return value; - }); - - return ( - showMetricChart(metric.name, metric.key, incomeStatement, metric.path)} - bg={metric.isTotal ? 'blue.50' : - metric.isSubtotal ? 'orange.50' : 'transparent'} - > - - - {!metric.isTotal && !metric.isSubtotal && } - - {metric.name} - - {metric.isCore && 核心} - - - {displayData.map((item, idx) => { - const value = rowData[idx]; - const { change, intensity } = calculateYoYChange( - value, - item.period, - incomeStatement, - metric.path - ); - - // 特殊处理:成本费用类负向指标,增长用绿色,减少用红色 - const isCostItem = metric.key.includes('cost') || metric.key.includes('expense') || - metric.key === 'income_tax' || metric.key.includes('impairment'); - const displayColor = isCostItem ? - (change > 0 ? negativeColor : positiveColor) : - (change > 0 ? positiveColor : negativeColor); - - return ( - - - - 数值: {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" - /> - - - ); - })} - - ); - - return ( - - - - - - {displayData.map(item => ( - - ))} - - - - - {sections.map(section => renderSection(section))} - -
项目 - - {formatUtils.getReportType(item.period)} - - {item.period.substring(0, 10)} - - - 操作
-
- ); - }; - - // 现金流量表组件 - const CashflowTable = () => { - const metrics = [ - { name: '经营现金流净额', key: 'operating_net', path: 'operating_activities.net_flow' }, - { name: '销售收现', key: 'cash_from_sales', path: 'operating_activities.inflow.cash_from_sales' }, - { name: '购买支付现金', key: 'cash_for_goods', path: 'operating_activities.outflow.cash_for_goods' }, - { name: '投资现金流净额', key: 'investment_net', path: 'investment_activities.net_flow' }, - { name: '筹资现金流净额', key: 'financing_net', path: 'financing_activities.net_flow' }, - { name: '现金净增加额', key: 'net_increase', path: 'cash_changes.net_increase' }, - { name: '期末现金余额', key: 'ending_balance', path: 'cash_changes.ending_balance' }, - { name: '自由现金流', key: 'free_cash_flow', path: 'key_metrics.free_cash_flow' }, - ]; - - // 数组安全检查 - if (!Array.isArray(cashflow) || cashflow.length === 0) { - return ( - - - 暂无现金流量表数据 - - ); - } - - const maxColumns = Math.min(cashflow.length, 8); - const displayData = cashflow.slice(0, maxColumns); - - return ( - - - - - - {displayData.map(item => ( - - ))} - - - - - {metrics.map(metric => { - const rowData = cashflow.map(item => { - const value = metric.path.split('.').reduce((obj, key) => obj?.[key], item); - return value; - }); - - return ( - showMetricChart(metric.name, metric.key, cashflow, metric.path)} - > - - {displayData.map((item, idx) => { - const value = rowData[idx]; - const isNegative = value < 0; - const { change, intensity } = calculateYoYChange( - value, - item.period, - cashflow, - metric.path - ); - - return ( - - ); - })} - - - ); - })} - -
项目 - - {formatUtils.getReportType(item.period)} - - {item.period.substring(0, 10)} - - - 趋势
- - {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" - /> -
-
- ); - }; - - // 财务指标表格组件 - 时间序列版 - const FinancialMetricsTable = () => { - const [selectedCategory, setSelectedCategory] = useState('profitability'); - - const metricsCategories = { - profitability: { - title: '盈利能力指标', - metrics: [ - { name: '净资产收益率(ROE)%', key: 'roe', path: 'profitability.roe', isCore: true }, - { name: '净资产收益率(扣非)%', key: 'roe_deducted', path: 'profitability.roe_deducted' }, - { name: '净资产收益率(加权)%', key: 'roe_weighted', path: 'profitability.roe_weighted', isCore: true }, - { name: '总资产报酬率(ROA)%', key: 'roa', path: 'profitability.roa', isCore: true }, - { name: '毛利率%', key: 'gross_margin', path: 'profitability.gross_margin', isCore: true }, - { name: '净利率%', key: 'net_margin', path: 'profitability.net_profit_margin', isCore: true }, - { name: '营业利润率%', key: 'operating_margin', path: 'profitability.operating_profit_margin' }, - { name: '成本费用利润率%', key: 'cost_profit_ratio', path: 'profitability.cost_profit_ratio' }, - { name: 'EBIT', key: 'ebit', path: 'profitability.ebit' }, - ] - }, - perShare: { - title: '每股指标', - metrics: [ - { name: '每股收益(EPS)', key: 'eps', path: 'per_share_metrics.eps', isCore: true }, - { name: '基本每股收益', key: 'basic_eps', path: 'per_share_metrics.basic_eps', isCore: true }, - { name: '稀释每股收益', key: 'diluted_eps', path: 'per_share_metrics.diluted_eps' }, - { name: '扣非每股收益', key: 'deducted_eps', path: 'per_share_metrics.deducted_eps', isCore: true }, - { name: '每股净资产', key: 'bvps', path: 'per_share_metrics.bvps', isCore: true }, - { name: '每股经营现金流', key: 'operating_cash_flow_ps', path: 'per_share_metrics.operating_cash_flow_ps' }, - { name: '每股资本公积', key: 'capital_reserve_ps', path: 'per_share_metrics.capital_reserve_ps' }, - { name: '每股未分配利润', key: 'undistributed_profit_ps', path: 'per_share_metrics.undistributed_profit_ps' }, - ] - }, - growth: { - title: '成长能力指标', - metrics: [ - { name: '营收增长率%', key: 'revenue_growth', path: 'growth.revenue_growth', isCore: true }, - { name: '净利润增长率%', key: 'profit_growth', path: 'growth.net_profit_growth', isCore: true }, - { name: '扣非净利润增长率%', key: 'deducted_profit_growth', path: 'growth.deducted_profit_growth', isCore: true }, - { name: '归母净利润增长率%', key: 'parent_profit_growth', path: 'growth.parent_profit_growth' }, - { name: '经营现金流增长率%', key: 'operating_cash_flow_growth', path: 'growth.operating_cash_flow_growth' }, - { name: '总资产增长率%', key: 'asset_growth', path: 'growth.total_asset_growth' }, - { name: '净资产增长率%', key: 'equity_growth', path: 'growth.equity_growth' }, - { name: '固定资产增长率%', key: 'fixed_asset_growth', path: 'growth.fixed_asset_growth' }, - ] - }, - operational: { - title: '运营效率指标', - metrics: [ - { name: '总资产周转率', key: 'asset_turnover', path: 'operational_efficiency.total_asset_turnover', isCore: true }, - { name: '固定资产周转率', key: 'fixed_asset_turnover', path: 'operational_efficiency.fixed_asset_turnover' }, - { name: '流动资产周转率', key: 'current_asset_turnover', path: 'operational_efficiency.current_asset_turnover' }, - { name: '应收账款周转率', key: 'receivable_turnover', path: 'operational_efficiency.receivable_turnover', isCore: true }, - { name: '应收账款周转天数', key: 'receivable_days', path: 'operational_efficiency.receivable_days', isCore: true }, - { name: '存货周转率', key: 'inventory_turnover', path: 'operational_efficiency.inventory_turnover', isCore: true }, - { name: '存货周转天数', key: 'inventory_days', path: 'operational_efficiency.inventory_days' }, - { name: '营运资金周转率', key: 'working_capital_turnover', path: 'operational_efficiency.working_capital_turnover' }, - ] - }, - solvency: { - title: '偿债能力指标', - metrics: [ - { name: '流动比率', key: 'current_ratio', path: 'solvency.current_ratio', isCore: true }, - { name: '速动比率', key: 'quick_ratio', path: 'solvency.quick_ratio', isCore: true }, - { name: '现金比率', key: 'cash_ratio', path: 'solvency.cash_ratio' }, - { name: '保守速动比率', key: 'conservative_quick_ratio', path: 'solvency.conservative_quick_ratio' }, - { name: '资产负债率%', key: 'debt_ratio', path: 'solvency.asset_liability_ratio', isCore: true }, - { name: '利息保障倍数', key: 'interest_coverage', path: 'solvency.interest_coverage' }, - { name: '现金到期债务比', key: 'cash_to_maturity_debt', path: 'solvency.cash_to_maturity_debt_ratio' }, - { name: '有形资产净值债务率%', key: 'tangible_asset_debt_ratio', path: 'solvency.tangible_asset_debt_ratio' }, - ] - }, - expense: { - title: '费用率指标', - metrics: [ - { name: '销售费用率%', key: 'selling_expense_ratio', path: 'expense_ratios.selling_expense_ratio', isCore: true }, - { name: '管理费用率%', key: 'admin_expense_ratio', path: 'expense_ratios.admin_expense_ratio', isCore: true }, - { name: '财务费用率%', key: 'financial_expense_ratio', path: 'expense_ratios.financial_expense_ratio' }, - { name: '研发费用率%', key: 'rd_expense_ratio', path: 'expense_ratios.rd_expense_ratio', isCore: true }, - { name: '三费费用率%', key: 'three_expense_ratio', path: 'expense_ratios.three_expense_ratio' }, - { name: '四费费用率%', key: 'four_expense_ratio', path: 'expense_ratios.four_expense_ratio' }, - { name: '营业成本率%', key: 'cost_ratio', path: 'expense_ratios.cost_ratio' }, - ] - }, - cashflow: { - title: '现金流量指标', - metrics: [ - { name: '经营现金流/净利润', key: 'cash_to_profit', path: 'cash_flow_quality.operating_cash_to_profit_ratio', isCore: true }, - { name: '净利含金量', key: 'profit_cash_content', path: 'cash_flow_quality.cash_to_profit_ratio', isCore: true }, - { name: '营收现金含量', key: 'revenue_cash_content', path: 'cash_flow_quality.cash_revenue_ratio' }, - { name: '全部资产现金回收率%', key: 'cash_recovery_rate', path: 'cash_flow_quality.cash_recovery_rate' }, - { name: '经营现金流/短期债务', key: 'cash_to_short_debt', path: 'cash_flow_quality.operating_cash_to_short_debt' }, - { name: '经营现金流/总债务', key: 'cash_to_total_debt', path: 'cash_flow_quality.operating_cash_to_total_debt' }, - ] - } - }; - - // 数组安全检查 - if (!Array.isArray(financialMetrics) || financialMetrics.length === 0) { - return ( - - - 暂无财务指标数据 - - ); - } - - const maxColumns = Math.min(financialMetrics.length, 6); - const displayData = financialMetrics.slice(0, maxColumns); - const currentCategory = metricsCategories[selectedCategory]; - - return ( - - {/* 分类选择器 */} - - {Object.entries(metricsCategories).map(([key, category]) => ( - - ))} - - - {/* 指标表格 */} - - - - - - {displayData.map(item => ( - - ))} - - - - - {currentCategory.metrics.map(metric => { - const rowData = financialMetrics.map(item => { - const value = metric.path.split('.').reduce((obj, key) => obj?.[key], item); - return value; - }); - - return ( - showMetricChart(metric.name, metric.key, financialMetrics, metric.path)} - > - - {displayData.map((item, idx) => { - const value = rowData[idx]; - const { change, intensity } = calculateYoYChange( - value, - item.period, - financialMetrics, - metric.path - ); - - // 判断指标性质 - const isNegativeIndicator = metric.key.includes('days') || - metric.key.includes('expense_ratio') || - metric.key.includes('debt_ratio') || - metric.key.includes('cost_ratio'); - - // 对于负向指标,增加是坏事(绿色),减少是好事(红色) - const displayColor = isNegativeIndicator ? - (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 < 0 ? negativeColor : 'gray.500') : - 'inherit' - } - > - {value?.toFixed(2) || '-'} - - - {Math.abs(change) > 20 && Math.abs(value) > 0.01 && ( - - {change > 0 ? '↑' : '↓'} - - )} - - } - variant="ghost" - colorScheme="blue" - onClick={(e) => { - e.stopPropagation(); - showMetricChart(metric.name, metric.key, financialMetrics, metric.path); - }} - /> -
-
- - {/* 关键指标快速对比 */} - - - 关键指标速览 - - - - {financialMetrics[0] && [ - { label: 'ROE', value: financialMetrics[0].profitability?.roe, format: 'percent' }, - { label: '毛利率', value: financialMetrics[0].profitability?.gross_margin, format: 'percent' }, - { label: '净利率', value: financialMetrics[0].profitability?.net_profit_margin, format: 'percent' }, - { label: '流动比率', value: financialMetrics[0].solvency?.current_ratio, format: 'decimal' }, - { label: '资产负债率', value: financialMetrics[0].solvency?.asset_liability_ratio, format: 'percent' }, - { label: '研发费用率', value: financialMetrics[0].expense_ratios?.rd_expense_ratio, format: 'percent' }, - ].map((item, idx) => ( - - {item.label} - - {item.format === 'percent' ? - formatUtils.formatPercent(item.value) : - item.value?.toFixed(2) || '-'} - - - ))} - - - -
- ); - }; - - // 主营业务分析组件 - 修复bug,支持product和industry两种分类 - const MainBusinessAnalysis = () => { - // 优先使用product_classification,如果为空则使用industry_classification - const hasProductData = mainBusiness?.product_classification?.length > 0; - const hasIndustryData = mainBusiness?.industry_classification?.length > 0; - - if (!hasProductData && !hasIndustryData) { - return ( - - - 暂无主营业务数据 - - ); - } - - // 选择数据源 - const dataSource = hasProductData ? 'product' : 'industry'; - const latestPeriod = hasProductData ? - mainBusiness.product_classification[0] : - mainBusiness.industry_classification[0]; - - const businessItems = hasProductData ? latestPeriod.products : latestPeriod.industries; - - // 过滤掉"合计"项,准备饼图数据 - const pieData = businessItems - .filter(item => item.content !== '合计') - .map(item => ({ - name: item.content, - value: item.revenue || 0 - })); - - const pieOption = { - title: { - text: `主营业务构成 - ${latestPeriod.report_type}`, - subtext: dataSource === 'industry' ? '按行业分类' : '按产品分类', - left: 'center' - }, - tooltip: { - trigger: 'item', - formatter: (params) => { - return `${params.name}
营收: ${formatUtils.formatLargeNumber(params.value)}
占比: ${params.percent}%`; - } - }, - legend: { - orient: 'vertical', - left: 'left', - top: 'center' - }, - series: [{ - type: 'pie', - radius: '50%', - data: pieData, - emphasis: { - itemStyle: { - shadowBlur: 10, - shadowOffsetX: 0, - shadowColor: 'rgba(0, 0, 0, 0.5)' - } - } - }] - }; - - // 历史对比数据 - const historicalData = hasProductData ? - mainBusiness.product_classification : - mainBusiness.industry_classification; - - return ( - - - - - - - - - - - - - 业务明细 - {latestPeriod.report_type} - - - - - - - - - - - - - - {businessItems - .filter(item => item.content !== '合计') - .map((item, idx) => ( - - - - - - - ))} - -
业务营收毛利率(%)利润
{item.content}{formatUtils.formatLargeNumber(item.revenue)}{formatUtils.formatPercent(item.gross_margin || item.profit_margin)}{formatUtils.formatLargeNumber(item.profit)}
-
-
-
-
-
- - {/* 历史对比 */} - {historicalData.length > 1 && ( - - - 主营业务历史对比 - - - - - - - - {historicalData.slice(0, 3).map(period => ( - - ))} - - - - {businessItems - .filter(item => item.content !== '合计') - .map((item, idx) => ( - - - {historicalData.slice(0, 3).map(period => { - const periodItems = hasProductData ? period.products : period.industries; - const matchItem = periodItems.find(p => p.content === item.content); - return ( - - ); - })} - - ))} - -
业务/期间{period.report_type}
{item.content} - {matchItem ? formatUtils.formatLargeNumber(matchItem.revenue) : '-'} -
-
-
-
- )} -
- ); - }; - - // 行业排名组件 - const IndustryRankingView = () => { - if (!industryRank || industryRank.length === 0) { - return ( - - - 暂无行业排名数据 - - ); - } - - const latestRanking = industryRank[0]; - - const rankingMetrics = [ - { name: 'EPS', key: 'eps' }, - { name: '每股净资产', key: 'bvps' }, - { name: 'ROE', key: 'roe' }, - { name: '营收增长率', key: 'revenue_growth' }, - { name: '利润增长率', key: 'profit_growth' }, - { name: '营业利润率', key: 'operating_margin' }, - { name: '资产负债率', key: 'debt_ratio' }, - { name: '应收账款周转率', key: 'receivable_turnover' } - ]; - - return ( - - {Array.isArray(industryRank) && industryRank.length > 0 ? ( - industryRank.map((periodData, periodIdx) => ( - - - - {periodData.report_type} 行业排名 - {periodData.period} - - - - {periodData.rankings?.map((ranking, idx) => ( - - - {ranking.industry_name} ({ranking.level_description}) - - - {rankingMetrics.map(metric => { - const metricData = ranking.metrics?.[metric.key]; - if (!metricData) return null; - - const isGood = metricData.rank && metricData.rank <= 10; - const isBad = metricData.rank && metricData.rank > 30; - - return ( - - {metric.name} - - - {metric.key.includes('growth') || metric.key.includes('margin') || metric.key === 'roe' ? - formatUtils.formatPercent(metricData.value) : - metricData.value?.toFixed(2) || '-'} - - {metricData.rank && ( - - #{metricData.rank} - - )} - - - 行业均值: {metric.key.includes('growth') || metric.key.includes('margin') || metric.key === 'roe' ? - formatUtils.formatPercent(metricData.industry_avg) : - metricData.industry_avg?.toFixed(2) || '-'} - - - ); - })} - - - ))} - - - )) - ) : ( - - - - 暂无行业排名数据 - - - - )} - - ); - }; - - // 股票对比组件 - const StockComparison = ({ currentStock }) => { - const [compareStock, setCompareStock] = useState(''); - const [compareData, setCompareData] = useState(null); - const [compareLoading, setCompareLoading] = useState(false); - - const loadCompareData = async () => { - if (!compareStock || compareStock.length !== 6) { - logger.warn('FinancialPanorama', 'loadCompareData', '无效的对比股票代码', { compareStock }); - toast({ - title: '请输入有效的6位股票代码', - status: 'warning', - duration: 3000, - }); - return; - } - - logger.debug('FinancialPanorama', '开始加载对比数据', { currentStock, compareStock }); - setCompareLoading(true); - try { - const [stockInfoRes, metricsRes, comparisonRes] = await Promise.all([ - financialService.getStockInfo(compareStock), - financialService.getFinancialMetrics(compareStock, 4), - financialService.getPeriodComparison(compareStock, 4) - ]); - - setCompareData({ - stockInfo: stockInfoRes.data, - metrics: metricsRes.data, - comparison: comparisonRes.data - }); - - // ❌ 移除对比数据加载成功toast - logger.info('FinancialPanorama', '对比数据加载成功', { currentStock, compareStock }); - } catch (error) { - logger.error('FinancialPanorama', 'loadCompareData', error, { currentStock, compareStock }); - - // ❌ 移除对比数据加载失败toast - // toast({ title: '加载对比数据失败', description: error.message, status: 'error', duration: 3000 }); - } finally { - setCompareLoading(false); - } - }; - - const compareMetrics = [ - { label: '营业收入', key: 'revenue', path: 'financial_summary.revenue' }, - { label: '净利润', key: 'net_profit', path: 'financial_summary.net_profit' }, - { label: 'ROE', key: 'roe', path: 'key_metrics.roe', format: 'percent' }, - { label: 'ROA', key: 'roa', path: 'key_metrics.roa', format: 'percent' }, - { label: '毛利率', key: 'gross_margin', path: 'key_metrics.gross_margin', format: 'percent' }, - { label: '净利率', key: 'net_margin', path: 'key_metrics.net_margin', format: 'percent' }, - { label: '营收增长率', key: 'revenue_growth', path: 'growth_rates.revenue_growth', format: 'percent' }, - { label: '利润增长率', key: 'profit_growth', path: 'growth_rates.profit_growth', format: 'percent' }, - { label: '资产总额', key: 'total_assets', path: 'financial_summary.total_assets' }, - { label: '负债总额', key: 'total_liabilities', path: 'financial_summary.total_liabilities' }, - ]; - - return ( - - - - - setCompareStock(e.target.value)} - maxLength={6} - /> - - - - - - {compareData && ( - - - - {stockInfo?.stock_name} ({currentStock}) VS {compareData.stockInfo?.stock_name} ({compareStock}) - - - - - - - - - - - - - - - {compareMetrics.map(metric => { - const value1 = metric.path.split('.').reduce((obj, key) => obj?.[key], stockInfo); - const value2 = metric.path.split('.').reduce((obj, key) => obj?.[key], compareData.stockInfo); - - let diff = null; - let diffColor = 'gray.500'; - - if (value1 && value2) { - if (metric.format === 'percent') { - diff = value1 - value2; - diffColor = diff > 0 ? positiveColor : negativeColor; - } else { - diff = ((value1 - value2) / value2) * 100; - diffColor = diff > 0 ? positiveColor : negativeColor; - } - } - - return ( - - - - - - - ); - })} - -
指标{stockInfo?.stock_name}{compareData.stockInfo?.stock_name}差异
{metric.label} - {metric.format === 'percent' ? - formatUtils.formatPercent(value1) : - formatUtils.formatLargeNumber(value1)} - - {metric.format === 'percent' ? - formatUtils.formatPercent(value2) : - formatUtils.formatLargeNumber(value2)} - - {diff !== null ? ( - - {diff > 0 && } - {diff < 0 && } - - {metric.format === 'percent' ? - `${Math.abs(diff).toFixed(2)}pp` : - `${Math.abs(diff).toFixed(2)}%`} - - - ) : '-'} -
-
- - {/* 对比图表 */} - - - - - 盈利能力对比 - - - - - - - - - - - 成长能力对比 - - - - - - - -
-
- )} -
- ); - }; - - // 综合对比分析 - const ComparisonAnalysis = () => { - if (!Array.isArray(comparison) || comparison.length === 0) return null; - - const revenueData = comparison.map(item => ({ - period: formatUtils.getReportType(item.period), - value: item.performance.revenue / 100000000 // 转换为亿 - })).reverse(); - - const profitData = comparison.map(item => ({ - period: formatUtils.getReportType(item.period), - value: item.performance.net_profit / 100000000 // 转换为亿 - })).reverse(); - - const combinedOption = { - title: { - text: '营收与利润趋势', - left: 'center' - }, - tooltip: { - trigger: 'axis', - axisPointer: { - type: 'cross' - } - }, - legend: { - data: ['营业收入', '净利润'], - bottom: 0 - }, - xAxis: { - type: 'category', - data: revenueData.map(d => d.period) - }, - yAxis: [ - { - type: 'value', - name: '营收(亿)', - position: 'left' - }, - { - type: 'value', - name: '利润(亿)', - position: 'right' - } - ], - series: [ - { - name: '营业收入', - type: 'bar', - data: revenueData.map(d => d.value?.toFixed(2)), - itemStyle: { - color: (params) => { - const idx = params.dataIndex; - if (idx === 0) return '#3182CE'; - const prevValue = revenueData[idx - 1].value; - const currValue = params.value; - // 中国市场颜色 - return currValue >= prevValue ? '#EF4444' : '#10B981'; - } - } - }, - { - name: '净利润', - type: 'line', - yAxisIndex: 1, - data: profitData.map(d => d.value?.toFixed(2)), - smooth: true, - itemStyle: { color: '#F59E0B' }, - lineStyle: { width: 2 } - } - ] - }; - - return ( - - - - - - ); - }; - - // 页面头部信息 - const StockInfoHeader = () => { - 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 : stockInfo.growth_rates?.revenue_growth < 0 ? negativeColor : 'gray.500'}> - {formatUtils.formatPercent(stockInfo.growth_rates?.revenue_growth)} - - - - - - 利润增长 - 0 ? positiveColor : stockInfo.growth_rates?.profit_growth < 0 ? negativeColor : 'gray.500'}> - {formatUtils.formatPercent(stockInfo.growth_rates?.profit_growth)} - - - - - {stockInfo.latest_forecast && ( - - - - {stockInfo.latest_forecast.forecast_type} - {stockInfo.latest_forecast.content} - - - )} - - - ); - }; - - return ( - - - {/* 时间选择器 */} - - - - - 显示期数: - - - } - onClick={loadFinancialData} - isLoading={loading} - variant="outline" - size="sm" - aria-label="刷新数据" - /> - - - - - {/* 股票信息头部 */} - {loading ? ( - - ) : ( - - )} - - {/* 主要内容区域 */} - {!loading && stockInfo && ( - - - 财务概览 - 资产负债表 - 利润表 - 现金流量表 - 财务指标 - 主营业务 - 行业排名 - 业绩预告 - 股票对比 - - - - {/* 财务概览 */} - - - - - - - - {/* 资产负债表 */} - - - - - - 资产负债表 - - 显示最近{Math.min(balanceSheet.length, 8)}期 - - 红涨绿跌 | 同比变化 - - - - - 提示:表格可横向滚动查看更多数据,点击行查看历史趋势 - - - - - - - - - - {/* 利润表 */} - - - - - - 利润表 - - 显示最近{Math.min(incomeStatement.length, 8)}期 - - 红涨绿跌 | 同比变化 - - - - - 提示:Q1、中报、Q3、年报数据为累计值,同比显示与去年同期对比 - - - - - - - - - - {/* 现金流量表 */} - - - - - - 现金流量表 - - 显示最近{Math.min(cashflow.length, 8)}期 - - 红涨绿跌 | 同比变化 - - - - - 提示:现金流数据为累计值,正值红色表示现金流入,负值绿色表示现金流出 - - - - - - - - - - {/* 财务指标 */} - - - - - {/* 主营业务 */} - - - - - {/* 行业排名 */} - - - - - {/* 业绩预告 */} - - {forecast && ( - - {forecast.forecasts?.map((item, idx) => ( - - - - {item.forecast_type} - - 报告期: {item.report_date} - - - {item.content} - {item.reason && ( - {item.reason} - )} - {item.change_range?.lower && ( - - 预计变动范围: - - {item.change_range.lower}% ~ {item.change_range.upper}% - - - )} - - - ))} - - )} - - - {/* 股票对比 */} - - - - - - )} - - {/* 错误提示 */} - {error && ( - - - {error} - - )} - - {/* 弹出模态框 */} - - - - 指标详情 - - - {modalContent} - - - - - - ); -}; - -export default FinancialPanorama; \ No newline at end of file diff --git a/src/views/Company/components/FinancialPanorama/index.tsx b/src/views/Company/components/FinancialPanorama/index.tsx new file mode 100644 index 00000000..cc37fdde --- /dev/null +++ b/src/views/Company/components/FinancialPanorama/index.tsx @@ -0,0 +1,453 @@ +/** + * 财务全景组件 + * 重构后的主组件,使用模块化结构 + */ + +import React, { useState, ReactNode } from 'react'; +import { + Box, + Container, + VStack, + HStack, + Card, + CardBody, + CardHeader, + Heading, + Text, + Badge, + Select, + IconButton, + Alert, + AlertIcon, + Skeleton, + Tabs, + TabList, + TabPanels, + Tab, + TabPanel, + Modal, + ModalOverlay, + ModalContent, + ModalHeader, + ModalBody, + ModalCloseButton, + useDisclosure, + Table, + Thead, + Tbody, + Tr, + Th, + Td, + TableContainer, + Divider, + Tooltip, +} from '@chakra-ui/react'; +import { RepeatIcon } from '@chakra-ui/icons'; +import ReactECharts from 'echarts-for-react'; +import { formatUtils } from '@services/financialService'; + +// 内部模块导入 +import { useFinancialData } from './hooks'; +import { COLORS } from './constants'; +import { calculateYoYChange, getCellBackground, getMetricChartOption } from './utils'; +import { + StockInfoHeader, + BalanceSheetTable, + IncomeStatementTable, + CashflowTable, + FinancialMetricsTable, + MainBusinessAnalysis, + IndustryRankingView, + StockComparison, + ComparisonAnalysis, +} from './components'; +import type { FinancialPanoramaProps } from './types'; + +/** + * 财务全景主组件 + */ +const FinancialPanorama: React.FC = ({ stockCode: propStockCode }) => { + // 使用数据加载 Hook + const { + stockInfo, + balanceSheet, + incomeStatement, + 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 showMetricChart = ( + metricName: string, + metricKey: string, + data: Array<{ period: string; [key: string]: unknown }>, + dataPath: string + ) => { + const chartData = data + .map((item) => { + const value = dataPath.split('.').reduce((obj: unknown, key: string) => { + if (obj && typeof obj === 'object') { + return (obj as Record)[key]; + } + return undefined; + }, item) as number | undefined; + return { + period: formatUtils.getReportType(item.period), + date: item.period, + value: value ?? 0, + }; + }) + .reverse(); + + const option = getMetricChartOption(metricName, chartData); + + setModalContent( + + + + + + + + + + + + + + + {chartData.map((item, idx) => { + // 计算环比 + const qoq = + idx > 0 + ? ((item.value - chartData[idx - 1].value) / + Math.abs(chartData[idx - 1].value)) * + 100 + : null; + + // 计算同比 + const currentDate = new Date(item.date); + const lastYearItem = chartData.find((d) => { + const date = new Date(d.date); + return ( + date.getFullYear() === currentDate.getFullYear() - 1 && + date.getMonth() === currentDate.getMonth() + ); + }); + const yoy = lastYearItem + ? ((item.value - lastYearItem.value) / Math.abs(lastYearItem.value)) * 100 + : null; + + return ( + + + + + + + ); + })} + +
报告期数值同比环比
{item.period}{formatUtils.formatLargeNumber(item.value)} 0 + ? positiveColor + : yoy !== null && yoy < 0 + ? negativeColor + : 'gray.500' + } + > + {yoy !== null ? `${yoy.toFixed(2)}%` : '-'} + 0 + ? positiveColor + : qoq !== null && qoq < 0 + ? negativeColor + : 'gray.500' + } + > + {qoq !== null ? `${qoq.toFixed(2)}%` : '-'} +
+
+
+ ); + onOpen(); + }; + + // 通用表格属性 + const tableProps = { + showMetricChart, + calculateYoYChange, + getCellBackground, + positiveColor, + negativeColor, + bgColor, + hoverBg, + }; + + return ( + + + {/* 时间选择器 */} + + + + + + 显示期数: + + + + } + onClick={refetch} + isLoading={loading} + variant="outline" + size="sm" + aria-label="刷新数据" + /> + + + + + {/* 股票信息头部 */} + {loading ? ( + + ) : ( + + )} + + {/* 主要内容区域 */} + {!loading && stockInfo && ( + + + 财务概览 + 资产负债表 + 利润表 + 现金流量表 + 财务指标 + 主营业务 + 行业排名 + 业绩预告 + 股票对比 + + + + {/* 财务概览 */} + + + + + + + + {/* 资产负债表 */} + + + + + + 资产负债表 + + + 显示最近{Math.min(balanceSheet.length, 8)}期 + + + 红涨绿跌 | 同比变化 + + + + + 提示:表格可横向滚动查看更多数据,点击行查看历史趋势 + + + + + + + + + + {/* 利润表 */} + + + + + + 利润表 + + + 显示最近{Math.min(incomeStatement.length, 8)}期 + + + 红涨绿跌 | 同比变化 + + + + + 提示:Q1、中报、Q3、年报数据为累计值,同比显示与去年同期对比 + + + + + + + + + + {/* 现金流量表 */} + + + + + + 现金流量表 + + + 显示最近{Math.min(cashflow.length, 8)}期 + + + 红涨绿跌 | 同比变化 + + + + + 提示:现金流数据为累计值,正值红色表示现金流入,负值绿色表示现金流出 + + + + + + + + + + {/* 财务指标 */} + + + + + {/* 主营业务 */} + + + + + {/* 行业排名 */} + + + + + {/* 业绩预告 */} + + {forecast && ( + + {forecast.forecasts?.map((item, idx) => ( + + + + {item.forecast_type} + + 报告期: {item.report_date} + + + {item.content} + {item.reason && ( + + {item.reason} + + )} + {item.change_range?.lower && ( + + 预计变动范围: + + {item.change_range.lower}% ~ {item.change_range.upper}% + + + )} + + + ))} + + )} + + + {/* 股票对比 */} + + + + + + )} + + {/* 错误提示 */} + {error && ( + + + {error} + + )} + + {/* 弹出模态框 */} + + + + 指标详情 + + {modalContent} + + + + + ); +}; + +export default FinancialPanorama;