"use client" import { useEffect, useState } from "react" import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card" import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table" import { Loader2, DollarSign, RefreshCw } from "lucide-react" import { Button } from "@/components/ui/button" import { getFinancialData } from "@/lib/api" import { formatNumber, formatLargeNumber, formatDate } from "@/lib/formatters" import type { FinancialDataResponse } from "@/lib/types" interface BloombergViewProps { companyId: number onBack?: () => void selectedCurrency?: string userMarket?: string } export function BloombergView({ companyId, onBack, selectedCurrency = "Auto", userMarket }: BloombergViewProps) { const [data, setData] = useState(null) const [loading, setLoading] = useState(true) const [error, setError] = useState("") const loadData = async () => { setLoading(true) setError("") try { console.log("Fetching Bloomberg data for company:", companyId) const result = await getFinancialData(companyId, "Bloomberg") console.log("Bloomberg data result:", result) setData(result) } catch (err: any) { console.error("Bloomberg fetch error:", err) setError(err.message || "加载 Bloomberg 数据失败") } finally { setLoading(false) } } useEffect(() => { loadData() }, [companyId]) if (loading) { return (

正在加载 Bloomberg 原始数据...

) } if (error) { return (

{error}

) } if (!data) return null // 如果后端提供了统一数据字段,直接使用 const mergedData = data.unified_data || [] return (

Bloomberg 财务数据总览

) } function RawDataTable({ data, title, selectedCurrency = "Auto", userMarket }: { data: any[], title: string, selectedCurrency?: string, userMarket?: string }) { if (!data || data.length === 0) { return ( {title}
暂无数据
) } // --------------------------------------------------------------------------- // 1. Filter Data Rows (Handle Multi-Currency) - DO THIS FIRST! // --------------------------------------------------------------------------- // Determine Target Currency Strategy let targetCurrency = selectedCurrency if (targetCurrency === "Auto" && userMarket) { const market = userMarket.toUpperCase() if (market.includes("JP")) targetCurrency = "JPY" else if (market.includes("VN")) targetCurrency = "VND" else if (market.includes("CN")) targetCurrency = "CNY" else if (market.includes("HK")) targetCurrency = "HKD" else targetCurrency = "USD" } const filteredRows = data.filter(row => { // Basic Validity Check if (!row || row.revenue === null || row.revenue === undefined) return false // Currency Filtering // Case 1: Specific Currency Selected (or resolved from Auto) if (targetCurrency !== "Auto") { if (row.currency && row.currency !== targetCurrency) { return false } } // Case 2: Pure Auto (Fallback if no userMarket) else if (selectedCurrency === "Auto") { if (data.length > 0 && data[0].currency && row.currency && row.currency !== data[0].currency) { return false } } return true }) // --------------------------------------------------------------------------- // 2. Extract Indicators (From Filtered Data ONLY) // --------------------------------------------------------------------------- // 提取所有指标键值 (排除元数据) const excludeKeys = ['id', 'company_code', 'code', 'symbol', 'market', 'update_date', 'create_time', 'end_date', 'ts_code'] // 从合并后的数据中收集所有可能的指标 const allIndicators = new Set() filteredRows.forEach(row => { Object.keys(row).forEach(k => { if (!excludeKeys.includes(k)) { allIndicators.add(k) } }) }) // 对指标进行逻辑排序/分组 const priorityOrder = [ 'currency', '__SECTION_MAIN__', 'roe', 'roce', 'roa', 'gross_margin', 'net_profit_margin', 'ebitda_margin', 'revenue', 'revenue_growth', 'net_income', 'netincome_growth', 'cash_from_operating', 'capital_expenditure', 'free_cash_flow', 'dividends_paid', 'dividend_payout_ratio', 'repurchase', 'total_assets', 'equity', 'goodwill', '__SECTION_EXPENSE__', 'selling&marketing', 'general&admin', 'sg&a', 'r&d', 'depreciation', 'tax_rate', '__SECTION_ASSET__', 'cash', 'inventory', 'accounts¬es_receivable', 'prepaid', 'property_plant&equipment', 'lt_investment', 'accounts_payable', 'st_defer_rev', 'st_debt', 'lt_debt', 'total_debt_ratio', '__SECTION_TURNOVER__', 'inventory_days', 'days_sales_outstanding', 'payables_days', 'net_fixed_asset_turnover', 'asset_turnover', '__SECTION_EFFICIENCY__', 'employee', 'num_of_employees', '__SECTION_MARKET__', 'last_price', 'market_cap', 'pe', 'pb', 'dividend_yield', 'shareholders' ] // Ensure section header is in the list to be sorted const sectionKeys = [ '__SECTION_MAIN__', '__SECTION_EXPENSE__', '__SECTION_ASSET__', '__SECTION_TURNOVER__', '__SECTION_EFFICIENCY__', '__SECTION_MARKET__' ] sectionKeys.forEach(key => { if (!allIndicators.has(key)) { allIndicators.add(key) } }) const sortedIndicators = Array.from(allIndicators).sort((a, b) => { const idxA = priorityOrder.indexOf(a) const idxB = priorityOrder.indexOf(b) // Items in priority list come first, sorted by their order in the list if (idxA !== -1 && idxB !== -1) return idxA - idxB // Items in priority list come before items not in list if (idxA !== -1) return -1 if (idxB !== -1) return 1 // Remaining items sorted alphabetically return a.localeCompare(b) }) // --------------------------------------------------------------------------- // 1. Filter Data Rows (Handle Multi-Currency) // --------------------------------------------------------------------------- // We must filter BEFORE creating the Map, because multiple rows might exist // for the same date (e.g. 2023 JPY and 2023 USD). // The Map key is 'end_date', so it can only hold one currency's data per date. // --------------------------------------------------------------------------- // 2. Build Data Structures from Filtered Rows // --------------------------------------------------------------------------- // 构建查找表: Map const dataMap = new Map() filteredRows.forEach(row => { dataMap.set(row.end_date, row) }) // 提取日期列表 const dates = Array.from(new Set(filteredRows.map(row => row.end_date))) .sort((a, b) => new Date(b).getTime() - new Date(a).getTime()) // Tooltip State const [tooltip, setTooltip] = useState<{ show: boolean, x: number, y: number, text: string }>({ show: false, x: 0, y: 0, text: '' }) const handleMouseMove = (e: React.MouseEvent, text: string) => { setTooltip({ show: true, x: e.clientX, y: e.clientY, text }) } const handleMouseLeave = () => { setTooltip(prev => ({ ...prev, show: false })) } // 获取第一行数据的货币单位 (假设一致) const currency = data[0]?.currency || 'CNY' return (
{tooltip.show && (
{tooltip.text}
)}
指标 / 报告期 (单位: 亿)
{dates.map(date => ( {formatDate(date)} ))}
{sortedIndicators.map(indicator => { if (indicator === '__SECTION_MAIN__') { return ( 主要指标 ) } if (indicator === '__SECTION_EXPENSE__') { return ( 费用指标 ) } if (indicator === '__SECTION_ASSET__') { return ( 资产结构 ) } if (indicator === '__SECTION_TURNOVER__') { return ( 周转能力 ) } if (indicator === '__SECTION_EFFICIENCY__') { return ( 人均效率 ) } if (indicator === '__SECTION_MARKET__') { return ( 市场表现 ) } return ( handleMouseMove(e, indicator)} onMouseLeave={handleMouseLeave} > {formatColumnName(indicator)} {dates.map(date => { const row = dataMap.get(date) const value = row ? row[indicator] : null let highlightClass = '' let textStyle = '' if (value !== null && value !== undefined) { const numVal = typeof value === 'string' ? parseFloat(value) : value if (typeof numVal === 'number' && !isNaN(numVal)) { if (indicator === 'roe' && numVal > 15) highlightClass = 'bg-green-100 dark:bg-green-900/40' else if (indicator === 'gross_margin' && numVal > 40) highlightClass = 'bg-green-100 dark:bg-green-900/40' else if (indicator === 'net_profit_margin' && numVal > 15) highlightClass = 'bg-green-100 dark:bg-green-900/40' else if (indicator === 'roce' && numVal > 15) highlightClass = 'bg-green-100 dark:bg-green-900/40' else if (indicator === 'roa' && numVal > 10) highlightClass = 'bg-green-100 dark:bg-green-900/40' else if (indicator === 'revenue_growth') { if (numVal > 15) highlightClass = 'bg-green-100 dark:bg-green-900/40' else if (numVal < 0) highlightClass = 'bg-red-100 dark:bg-red-900/40' } else if (indicator === 'netincome_growth') { if (numVal > 20) highlightClass = 'bg-green-100 dark:bg-green-900/40' else if (numVal < 0) highlightClass = 'bg-red-100 dark:bg-red-900/40' } // Dynamic text style based on value for growth rates if (['revenue_growth', 'netincome_growth'].includes(indicator)) { if (numVal < 0) { textStyle = 'text-red-600 italic font-bold dark:text-red-400' } else { textStyle = 'text-blue-600 italic dark:text-blue-400' } } } } // Fallback for text style if value is missing/invalid but indicator is growth rate if (textStyle === '' && ['revenue_growth', 'netincome_growth'].includes(indicator)) { textStyle = 'text-blue-600 italic dark:text-blue-400' } return ( {formatCellValue(indicator, value)} ) })} ) })}
) } // 复用之前的 formatting 逻辑 function formatColumnName(column: string): string { const nameMap: Record = { // --- 核心利润表 --- 'revenue': '营业收入', 'net_income': '净利润', 'operating_profit': '营业利润', 'gross_margin': '毛利率', 'net_profit_margin': '净利率', 'ebitda_margin': 'EBITDA利润率', 'sg&a': '销售管理费用(SG&A)', 'selling&marketing': '销售费用', 'general&admin': '管理费用', 'ga_exp': '管理费用', // Alias 'rd_exp': '研发费用', 'r&d': '研发费用', 'depreciation': '折旧', 'tax_rate': '有效税率', 'dividends': '分红', 'dividend_payout_ratio': '分红支付率', 'netincome_growth': '净利润增长率', 'revenue_growth': '营收增长率', // --- 资产负债表 --- 'total_assets': '总资产', 'total_liabilities': '总负债', 'shareholders_equity': '净资产', // Alias 'total_equity': '净资产', // Alias 'equity': '净资产', // Real Bloomberg Key 'cash': '现金', 'inventory': '存货', 'accounts¬es_receivable': '应收账款及票据', 'receivables': '应收账款', 'accounts_payable': '应付账款', 'prepayment': '预付款项', 'prepaid': '预付款项', 'fixed_assets': '固定资产', 'property_plant&equipment': '固定资产(PP&E)', 'long_term_investments': '长期投资', 'lt_investment': '长期投资', 'short_term_debt': '短期借款', 'st_debt': '短期借款', 'long_term_debt': '长期借款', 'lt_debt': '长期借款', 'deferred_revenue': '递延收入', 'st_defer_rev': '短期递延收入', 'goodwill': '商誉', 'total_debt_ratio': '总债务比率', // --- 现金流量表 --- 'cash_from_operating': '经营现金流', 'ocf': '经营现金流', // Alias 'capital_expenditure': '资本支出', 'capex': '资本支出', // Alias 'free_cash_flow': '自由现金流', 'fcf': '自由现金流', // Alias 'dividends_paid': '支付股息', 'repurchase': '股份回购', // --- 运营效率/周转 --- 'asset_turnover': '总资产周转率', 'net_fixed_asset_turnover': '固定资产周转率', // Correct Bloomberg Key 'net_fixed_asset_turn': '固定资产周转率', // Alias just in case 'inventory_days': '存货周转天数', 'invent_days': '存货周转天数', 'days_sales_outstanding': '应收账款周转天数', 'payables_days': '应付账款周转天数', 'employee': '员工人数', 'num_of_employees': '员工人数', // --- 估值与市场 --- 'pe': '市盈率(PE)', 'pe_ratio': '市盈率(PE)', 'pb': '市净率(PB)', 'pb_ratio': '市净率(PB)', 'market_cap': '市值', 'price': '最新股价', 'last_price': '最新股价', 'roe': 'ROE', 'roa': 'ROA', 'roce': 'ROCE', 'dividend_yield': '股息率', 'shareholders': '股东人数', 'company_name': '公司名称', 'ipo_date': '上市日期', 'rev_abroad': '海外收入占比', 'currency': '货币', } return nameMap[column.toLowerCase()] || column } function formatCellValue(column: string, value: any): string { if (value === null || value === undefined) return '-' // 尝试解析文本数字 let numVal = value if (typeof value === 'string') { const parsed = parseFloat(value) if (!isNaN(parsed)) { numVal = parsed } } if (typeof numVal === 'number') { const lowerCol = column.toLowerCase() // Special scaling for tax_rate if (lowerCol === 'tax_rate') { numVal = numVal * 100 } const isRatio = ['roe', 'roce', 'roa', 'gross_margin', 'net_profit_margin', 'ebitda_margin', 'dividend_yield', 'rev_abroad', 'tax_rate', 'revenue_growth', 'netincome_growth'].includes(lowerCol) // Scale huge monetary values to "Yi" (100 Million) // Bloomberg raw data is in Millions. // Millions -> Yi (100 Millions) = divide by 100. const isMoney = [ 'revenue', 'net_income', 'operating_profit', 'total_assets', 'total_liabilities', 'equity', 'shareholders_equity', 'total_equity', 'cash', 'inventory', 'accounts¬es_receivable', 'receivables', 'accounts_payable', 'prepaid', 'prepayment', 'property_plant&equipment', 'fixed_assets', 'lt_investment', 'long_term_investments', 'st_debt', 'short_term_debt', 'lt_debt', 'long_term_debt', 'st_defer_rev', 'deferred_revenue', 'goodwill', 'cash_from_operating', 'capital_expenditure', 'capex', 'free_cash_flow', 'fcf', 'dividends_paid', 'repurchase', 'sg&a', 'selling&marketing', 'general&admin', 'r&d', 'depreciation', 'market_cap' ].includes(lowerCol) if (isMoney) { numVal = numVal / 100 } if (isRatio) { return numVal.toLocaleString('en-US', { minimumFractionDigits: 2, maximumFractionDigits: 2 }) + '%' } // 如果大于 1000,不保留小数;否则保留两位小数 const isLarge = Math.abs(numVal) > 1000 return numVal.toLocaleString('en-US', { minimumFractionDigits: isLarge ? 0 : 2, maximumFractionDigits: isLarge ? 0 : 2 }) } return String(value) }