diff --git a/src/views/Company/components/FinancialPanorama/utils/calculations.ts b/src/views/Company/components/FinancialPanorama/utils/calculations.ts new file mode 100644 index 00000000..b517f466 --- /dev/null +++ b/src/views/Company/components/FinancialPanorama/utils/calculations.ts @@ -0,0 +1,99 @@ +/** + * 财务计算工具函数 + */ + +/** + * 计算同比变化率 + * @param currentValue 当前值 + * @param currentPeriod 当前期间 + * @param allData 所有数据 + * @param metricPath 指标路径 + * @returns 变化率和强度 + */ +export const calculateYoYChange = ( + currentValue: number | null | undefined, + currentPeriod: string, + allData: Array<{ period: string; [key: string]: unknown }>, + metricPath: string +): { change: number; intensity: number } => { + 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: unknown, key: string) => { + if (obj && typeof obj === 'object') { + return (obj as Record)[key]; + } + return undefined; + }, lastYearSamePeriod) as number | undefined; + + 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 }; +}; + +/** + * 获取单元格背景色(中国市场颜色) + * @param change 变化率 + * @param intensity 强度 + * @returns 背景色 + */ +export const getCellBackground = (change: number, intensity: number): string => { + 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'; +}; + +/** + * 从对象中获取嵌套路径的值 + * @param obj 对象 + * @param path 路径(如 'assets.current_assets.cash') + * @returns 值 + */ +export const getValueByPath = ( + obj: unknown, + path: string +): T | undefined => { + return path.split('.').reduce((current: unknown, key: string) => { + if (current && typeof current === 'object') { + return (current as Record)[key]; + } + return undefined; + }, obj) as T | undefined; +}; + +/** + * 判断是否为成本费用类指标(负向指标) + * @param key 指标 key + * @returns 是否为负向指标 + */ +export const isNegativeIndicator = (key: string): boolean => { + return ( + key.includes('cost') || + key.includes('expense') || + key === 'income_tax' || + key.includes('impairment') || + key.includes('days') || + key.includes('debt_ratio') + ); +}; diff --git a/src/views/Company/components/FinancialPanorama/utils/chartOptions.ts b/src/views/Company/components/FinancialPanorama/utils/chartOptions.ts new file mode 100644 index 00000000..0d9b708b --- /dev/null +++ b/src/views/Company/components/FinancialPanorama/utils/chartOptions.ts @@ -0,0 +1,250 @@ +/** + * ECharts 图表配置生成器 + */ + +import { formatUtils } from '@services/financialService'; + +interface ChartDataItem { + period: string; + date: string; + value: number; +} + +/** + * 生成指标趋势图表配置 + * @param metricName 指标名称 + * @param data 图表数据 + * @returns ECharts 配置 + */ +export const getMetricChartOption = ( + metricName: string, + data: ChartDataItem[] +) => { + return { + title: { + text: metricName, + left: 'center', + }, + tooltip: { + trigger: 'axis', + formatter: (params: Array<{ name: string; value: number }>) => { + 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: data.map((d) => d.period), + axisLabel: { + rotate: 45, + }, + }, + yAxis: { + type: 'value', + axisLabel: { + formatter: (value: number) => { + 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: data.map((d) => d.value), + itemStyle: { + color: (params: { dataIndex: number; value: number }) => { + const idx = params.dataIndex; + if (idx === 0) return '#3182CE'; + const prevValue = data[idx - 1].value; + const currValue = params.value; + // 中国市场颜色:红涨绿跌 + return currValue >= prevValue ? '#EF4444' : '#10B981'; + }, + }, + label: { + show: true, + position: 'top', + formatter: (params: { value: number }) => { + 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); + }, + }, + }, + ], + }; +}; + +/** + * 生成营收与利润趋势图表配置 + * @param revenueData 营收数据 + * @param profitData 利润数据 + * @returns ECharts 配置 + */ +export const getComparisonChartOption = ( + revenueData: { period: string; value: number }[], + profitData: { period: string; value: number }[] +) => { + return { + 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: { dataIndex: number; value: number }) => { + 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 }, + }, + ], + }; +}; + +/** + * 生成主营业务饼图配置 + * @param title 标题 + * @param subtitle 副标题 + * @param data 饼图数据 + * @returns ECharts 配置 + */ +export const getMainBusinessPieOption = ( + title: string, + subtitle: string, + data: { name: string; value: number }[] +) => { + return { + title: { + text: title, + subtext: subtitle, + left: 'center', + }, + tooltip: { + trigger: 'item', + formatter: (params: { name: string; value: number; percent: number }) => { + return `${params.name}
营收: ${formatUtils.formatLargeNumber( + params.value + )}
占比: ${params.percent}%`; + }, + }, + legend: { + orient: 'vertical', + left: 'left', + top: 'center', + }, + series: [ + { + type: 'pie', + radius: '50%', + data: data, + emphasis: { + itemStyle: { + shadowBlur: 10, + shadowOffsetX: 0, + shadowColor: 'rgba(0, 0, 0, 0.5)', + }, + }, + }, + ], + }; +}; + +/** + * 生成对比柱状图配置 + * @param title 标题 + * @param stockName1 股票1名称 + * @param stockName2 股票2名称 + * @param categories X轴分类 + * @param data1 股票1数据 + * @param data2 股票2数据 + * @returns ECharts 配置 + */ +export const getCompareBarChartOption = ( + title: string, + stockName1: string, + stockName2: string, + categories: string[], + data1: (number | undefined)[], + data2: (number | undefined)[] +) => { + return { + tooltip: { trigger: 'axis' }, + legend: { data: [stockName1, stockName2] }, + xAxis: { + type: 'category', + data: categories, + }, + yAxis: { type: 'value', axisLabel: { formatter: '{value}%' } }, + series: [ + { + name: stockName1, + type: 'bar', + data: data1, + }, + { + name: stockName2, + type: 'bar', + data: data2, + }, + ], + }; +}; diff --git a/src/views/Company/components/FinancialPanorama/utils/index.ts b/src/views/Company/components/FinancialPanorama/utils/index.ts new file mode 100644 index 00000000..fd302b02 --- /dev/null +++ b/src/views/Company/components/FinancialPanorama/utils/index.ts @@ -0,0 +1,17 @@ +/** + * 工具函数统一导出 + */ + +export { + calculateYoYChange, + getCellBackground, + getValueByPath, + isNegativeIndicator, +} from './calculations'; + +export { + getMetricChartOption, + getComparisonChartOption, + getMainBusinessPieOption, + getCompareBarChartOption, +} from './chartOptions';