Files
vf_react/src/services/financialService.js
zdl 2720946ccf fix(types): 修复 ECharts 类型导出和组件类型冲突
- echarts.ts: 将 EChartsOption 改为 EChartsCoreOption 的类型别名
- FuiCorners: 移除 extends BoxProps,position 重命名为 corner
- KLineChartModal/TimelineChartModal/ConcentrationCard: 使用导入的 EChartsOption
- LoadingState: 新增骨架屏 variant 支持
- FinancialPanorama: 使用骨架屏加载状态
- useFinancialData/financialService: 优化数据获取逻辑
- Company/index: 简化组件结构

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-12-18 18:42:19 +08:00

417 lines
15 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// src/services/financialService.js
/**
* 完整的财务数据服务层
* 对应Flask后端的所有财务API接口
*/
import axios from '@utils/axiosConfig';
/**
* 统一的 API 请求函数
* axios 拦截器已自动处理日志记录
* @param {string} url - 请求 URL
* @param {object} options - 请求选项
* @param {AbortSignal} options.signal - AbortController 信号,用于取消请求
*/
const apiRequest = async (url, options = {}) => {
const { method = 'GET', body, signal, ...rest } = options;
const config = {
method,
url,
signal,
...rest,
};
// 如果有 body根据方法设置 data 或 params
if (body) {
if (method === 'GET') {
config.params = typeof body === 'string' ? JSON.parse(body) : body;
} else {
config.data = typeof body === 'string' ? JSON.parse(body) : body;
}
}
const response = await axios(config);
return response.data;
};
export const financialService = {
/**
* 获取股票基本信息和最新财务摘要
* @param {string} seccode - 股票代码
* @param {object} options - 请求选项
* @param {AbortSignal} options.signal - AbortController 信号
*/
getStockInfo: async (seccode, options = {}) => {
return await apiRequest(`/api/financial/stock-info/${seccode}`, options);
},
/**
* 获取完整的资产负债表数据
* @param {string} seccode - 股票代码
* @param {number} limit - 获取的报告期数量
* @param {object} options - 请求选项
* @param {AbortSignal} options.signal - AbortController 信号
*/
getBalanceSheet: async (seccode, limit = 12, options = {}) => {
return await apiRequest(`/api/financial/balance-sheet/${seccode}?limit=${limit}`, options);
},
/**
* 获取完整的利润表数据
* @param {string} seccode - 股票代码
* @param {number} limit - 获取的报告期数量
* @param {object} options - 请求选项
* @param {AbortSignal} options.signal - AbortController 信号
*/
getIncomeStatement: async (seccode, limit = 12, options = {}) => {
return await apiRequest(`/api/financial/income-statement/${seccode}?limit=${limit}`, options);
},
/**
* 获取完整的现金流量表数据
* @param {string} seccode - 股票代码
* @param {number} limit - 获取的报告期数量
* @param {object} options - 请求选项
* @param {AbortSignal} options.signal - AbortController 信号
*/
getCashflow: async (seccode, limit = 12, options = {}) => {
return await apiRequest(`/api/financial/cashflow/${seccode}?limit=${limit}`, options);
},
/**
* 获取完整的财务指标数据
* @param {string} seccode - 股票代码
* @param {number} limit - 获取的报告期数量
* @param {object} options - 请求选项
* @param {AbortSignal} options.signal - AbortController 信号
*/
getFinancialMetrics: async (seccode, limit = 12, options = {}) => {
return await apiRequest(`/api/financial/financial-metrics/${seccode}?limit=${limit}`, options);
},
/**
* 获取主营业务构成数据
* @param {string} seccode - 股票代码
* @param {number} periods - 获取的报告期数量
* @param {object} options - 请求选项
* @param {AbortSignal} options.signal - AbortController 信号
*/
getMainBusiness: async (seccode, periods = 4, options = {}) => {
return await apiRequest(`/api/financial/main-business/${seccode}?periods=${periods}`, options);
},
/**
* 获取业绩预告和预披露时间
* @param {string} seccode - 股票代码
* @param {object} options - 请求选项
* @param {AbortSignal} options.signal - AbortController 信号
*/
getForecast: async (seccode, options = {}) => {
return await apiRequest(`/api/financial/forecast/${seccode}`, options);
},
/**
* 获取行业排名数据
* @param {string} seccode - 股票代码
* @param {number} limit - 获取的报告期数量
* @param {object} options - 请求选项
* @param {AbortSignal} options.signal - AbortController 信号
*/
getIndustryRank: async (seccode, limit = 4, options = {}) => {
return await apiRequest(`/api/financial/industry-rank/${seccode}?limit=${limit}`, options);
},
/**
* 获取不同报告期的对比数据
* @param {string} seccode - 股票代码
* @param {number} periods - 对比的报告期数量
* @param {object} options - 请求选项
* @param {AbortSignal} options.signal - AbortController 信号
*/
getPeriodComparison: async (seccode, periods = 8, options = {}) => {
return await apiRequest(`/api/financial/comparison/${seccode}?periods=${periods}`, options);
},
};
// 数据格式化工具函数
export const formatUtils = {
/**
* 格式化大数字(亿/万)
* @param {number} num - 数字
* @param {number} decimal - 小数位数
*/
formatLargeNumber: (num, decimal = 2) => {
if (num === null || num === undefined) return '-';
const absNum = Math.abs(num);
let result = '';
if (absNum >= 100000000) {
result = (num / 100000000).toFixed(decimal) + '亿';
} else if (absNum >= 10000) {
result = (num / 10000).toFixed(decimal) + '万';
} else if (absNum >= 1) {
result = num.toFixed(decimal);
} else {
result = num.toFixed(4); // 小于1的数字显示更多小数位
}
return result;
},
/**
* 格式化百分比
* @param {number} num - 数字
* @param {number} decimal - 小数位数
*/
formatPercent: (num, decimal = 2) => {
if (num === null || num === undefined) return '-';
return num.toFixed(decimal) + '%';
},
/**
* 格式化日期
* @param {string} dateStr - 日期字符串
*/
formatDate: (dateStr) => {
if (!dateStr) return '-';
const date = new Date(dateStr);
return `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, '0')}-${String(date.getDate()).padStart(2, '0')}`;
},
/**
* 获取报告期类型
* @param {string} dateStr - 日期字符串
*/
getReportType: (dateStr) => {
if (!dateStr) return '';
const date = new Date(dateStr);
const month = date.getMonth() + 1;
const year = date.getFullYear();
switch(month) {
case 3:
return `${year}Q1`;
case 6:
return `${year}中报`;
case 9:
return `${year}Q3`;
case 12:
return `${year}年报`;
default:
return dateStr;
}
},
/**
* 格式化增长率颜色用于Chakra UI
* @param {number} value - 增长率数值
*/
getGrowthColor: (value) => {
if (value === null || value === undefined) return 'gray.500';
if (value > 0) return 'green.500';
if (value < 0) return 'red.500';
return 'gray.500';
},
/**
* 获取指标变化趋势图标
* @param {number} current - 当前值
* @param {number} previous - 之前的值
*/
getTrendIcon: (current, previous) => {
if (!current || !previous) return 'stable';
if (current > previous) return 'up';
if (current < previous) return 'down';
return 'stable';
},
/**
* 计算同比增长率
* @param {number} current - 当前值
* @param {number} yearAgo - 去年同期值
*/
calculateYoY: (current, yearAgo) => {
if (!current || !yearAgo) return null;
return ((current - yearAgo) / Math.abs(yearAgo)) * 100;
},
/**
* 计算环比增长率
* @param {number} current - 当前值
* @param {number} previous - 上期值
*/
calculateQoQ: (current, previous) => {
if (!current || !previous) return null;
return ((current - previous) / Math.abs(previous)) * 100;
},
/**
* 获取财务健康度评分
* @param {object} metrics - 财务指标对象
*/
getFinancialHealthScore: (metrics) => {
let score = 0;
let factors = 0;
// ROE评分
if (metrics.roe !== null && metrics.roe !== undefined) {
factors++;
if (metrics.roe > 20) score += 5;
else if (metrics.roe > 15) score += 4;
else if (metrics.roe > 10) score += 3;
else if (metrics.roe > 5) score += 2;
else if (metrics.roe > 0) score += 1;
}
// 流动比率评分
if (metrics.current_ratio !== null && metrics.current_ratio !== undefined) {
factors++;
if (metrics.current_ratio > 2) score += 5;
else if (metrics.current_ratio > 1.5) score += 4;
else if (metrics.current_ratio > 1) score += 3;
else if (metrics.current_ratio > 0.75) score += 2;
else score += 1;
}
// 资产负债率评分
if (metrics.debt_ratio !== null && metrics.debt_ratio !== undefined) {
factors++;
if (metrics.debt_ratio < 30) score += 5;
else if (metrics.debt_ratio < 40) score += 4;
else if (metrics.debt_ratio < 50) score += 3;
else if (metrics.debt_ratio < 60) score += 2;
else if (metrics.debt_ratio < 70) score += 1;
}
// 营收增长率评分
if (metrics.revenue_growth !== null && metrics.revenue_growth !== undefined) {
factors++;
if (metrics.revenue_growth > 30) score += 5;
else if (metrics.revenue_growth > 20) score += 4;
else if (metrics.revenue_growth > 10) score += 3;
else if (metrics.revenue_growth > 5) score += 2;
else if (metrics.revenue_growth > 0) score += 1;
}
if (factors === 0) return null;
const avgScore = (score / factors) * 20; // 转换为100分制
return {
score: avgScore,
level: avgScore >= 80 ? '优秀' : avgScore >= 60 ? '良好' : avgScore >= 40 ? '一般' : '较差',
color: avgScore >= 80 ? 'green' : avgScore >= 60 ? 'blue' : avgScore >= 40 ? 'yellow' : 'red'
};
},
/**
* 格式化数据表格列配置
* @param {string} type - 表格类型
*/
getTableColumns: (type) => {
const columnConfigs = {
balanceSheet: [
{ key: 'period', label: '报告期', align: 'left' },
{ key: 'total_assets', label: '总资产', align: 'right', format: 'largeNumber' },
{ key: 'total_liabilities', label: '总负债', align: 'right', format: 'largeNumber' },
{ key: 'total_equity', label: '股东权益', align: 'right', format: 'largeNumber' },
{ key: 'current_ratio', label: '流动比率', align: 'right', format: 'decimal' },
{ key: 'debt_ratio', label: '资产负债率', align: 'right', format: 'percent' },
],
incomeStatement: [
{ key: 'period', label: '报告期', align: 'left' },
{ key: 'revenue', label: '营业收入', align: 'right', format: 'largeNumber' },
{ key: 'operating_profit', label: '营业利润', align: 'right', format: 'largeNumber' },
{ key: 'net_profit', label: '净利润', align: 'right', format: 'largeNumber' },
{ key: 'gross_margin', label: '毛利率', align: 'right', format: 'percent' },
{ key: 'net_margin', label: '净利率', align: 'right', format: 'percent' },
],
cashflow: [
{ key: 'period', label: '报告期', align: 'left' },
{ key: 'operating_cashflow', label: '经营现金流', align: 'right', format: 'largeNumber' },
{ key: 'investing_cashflow', label: '投资现金流', align: 'right', format: 'largeNumber' },
{ key: 'financing_cashflow', label: '筹资现金流', align: 'right', format: 'largeNumber' },
{ key: 'free_cashflow', label: '自由现金流', align: 'right', format: 'largeNumber' },
],
metrics: [
{ key: 'period', label: '报告期', align: 'left' },
{ key: 'roe', label: 'ROE', align: 'right', format: 'percent' },
{ key: 'roa', label: 'ROA', align: 'right', format: 'percent' },
{ key: 'eps', label: 'EPS', align: 'right', format: 'decimal' },
{ key: 'pe', label: 'PE', align: 'right', format: 'decimal' },
{ key: 'pb', label: 'PB', align: 'right', format: 'decimal' },
]
};
return columnConfigs[type] || [];
}
};
// 图表数据处理工具
export const chartUtils = {
/**
* 准备趋势图数据
* @param {array} data - 原始数据
* @param {array} metrics - 要显示的指标
*/
prepareTrendData: (data, metrics) => {
return data.map(item => {
const point = {
period: formatUtils.getReportType(item.period)
};
metrics.forEach(metric => {
point[metric.key] = item[metric.dataPath];
});
return point;
}).reverse(); // 按时间顺序排列
},
/**
* 准备饼图数据
* @param {array} data - 原始数据
* @param {string} valueKey - 值字段
* @param {string} nameKey - 名称字段
*/
preparePieData: (data, valueKey, nameKey) => {
const total = data.reduce((sum, item) => sum + (item[valueKey] || 0), 0);
return data.map(item => ({
name: item[nameKey],
value: item[valueKey],
percentage: total > 0 ? (item[valueKey] / total * 100).toFixed(2) : 0
}));
},
/**
* 准备对比柱状图数据
* @param {array} data - 原始数据
* @param {array} periods - 要对比的期间
* @param {array} metrics - 要对比的指标
*/
prepareComparisonData: (data, periods, metrics) => {
return metrics.map(metric => ({
metric: metric.label,
...periods.reduce((acc, period, index) => {
const periodData = data.find(d => d.period === period);
acc[`period${index + 1}`] = periodData ? periodData[metric.key] : 0;
return acc;
}, {})
}));
},
/**
* 获取图表颜色配置
* @param {string} theme - 主题类型
*/
getChartColors: (theme = 'default') => {
const themes = {
default: ['#3182CE', '#38A169', '#DD6B20', '#805AD5', '#D69E2E', '#E53E3E'],
blue: ['#0088FE', '#00C49F', '#FFBB28', '#FF8042', '#8884D8', '#82CA9D'],
green: ['#38A169', '#48BB78', '#68D391', '#9AE6B4', '#C6F6D5', '#F0FFF4']
};
return themes[theme] || themes.default;
}
};
export default financialService;