refactor(FinancialPanorama): 添加工具函数模块

计算工具 (calculations.ts):
- calculateYoYChange: 同比变化率计算
- getCellBackground: 单元格背景色(红涨绿跌)
- getValueByPath: 嵌套路径取值
- isNegativeIndicator: 负向指标判断

图表配置 (chartOptions.ts):
- getMetricChartOption: 指标趋势柱状图
- getComparisonChartOption: 营收利润双轴图
- getMainBusinessPieOption: 主营业务饼图
- getCompareBarChartOption: 股票对比柱状图

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
zdl
2025-12-12 15:01:09 +08:00
parent a424b3338d
commit fb42ef566b
3 changed files with 366 additions and 0 deletions

View File

@@ -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<string, unknown>)[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 = <T = unknown>(
obj: unknown,
path: string
): T | undefined => {
return path.split('.').reduce((current: unknown, key: string) => {
if (current && typeof current === 'object') {
return (current as Record<string, unknown>)[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')
);
};

View File

@@ -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}<br/>${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}<br/>营收: ${formatUtils.formatLargeNumber(
params.value
)}<br/>占比: ${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,
},
],
};
};

View File

@@ -0,0 +1,17 @@
/**
* 工具函数统一导出
*/
export {
calculateYoYChange,
getCellBackground,
getValueByPath,
isNegativeIndicator,
} from './calculations';
export {
getMetricChartOption,
getComparisonChartOption,
getMainBusinessPieOption,
getCompareBarChartOption,
} from './chartOptions';