// 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, useColorModeValue, Select, Button, Tooltip, Progress, Modal, ModalOverlay, ModalContent, ModalHeader, ModalBody, ModalCloseButton, useDisclosure, Input, Flex, Tag, TagLabel, IconButton, useToast, Skeleton, SkeletonText, Grid, GridItem, ButtonGroup, Stack, Collapse, useColorMode, } 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 { colorMode } = useColorMode(); // 颜色配置(中国市场:红涨绿跌) const bgColor = useColorModeValue('white', 'gray.800'); const borderColor = useColorModeValue('gray.200', 'gray.600'); const hoverBg = useColorModeValue('gray.50', 'gray.700'); const positiveColor = useColorModeValue('red.500', 'red.400'); // 红涨 const negativeColor = useColorModeValue('green.500', 'green.400'); // 绿跌 // 加载所有财务数据 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]); // 初始加载 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 ? useColorModeValue('blue.50', 'blue.900') : '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 ? useColorModeValue('blue.50', 'blue.900') : metric.isSubtotal ? useColorModeValue('orange.50', 'orange.900') : '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;