Files
vf_react/src/services/marketService.js
zdl eaa65b2328 fix(SubTabContainer): 移除外层 Suspense,Tab 内容直接展示
- SubTabContainer 内部为每个 Tab 添加 Suspense fallback={null}
- 移除 Company/index.tsx 外层 Suspense 和 TabLoadingFallback
- 切换一级 Tab 时不再显示整体 loading,直接展示内容

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

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

516 lines
16 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/marketService.js
/**
* 完整的市场行情数据服务层
* 对应Flask后端的所有市场API接口
*/
import axios from '@utils/axiosConfig';
import { logger } from '../utils/logger';
/**
* 统一的 API 请求函数
* axios 拦截器已自动处理日志记录
*/
const apiRequest = async (url, options = {}) => {
const { method = 'GET', body, ...rest } = options;
const config = {
method,
url,
...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 marketService = {
/**
* 获取股票交易数据日K线
* @param {string} seccode - 股票代码
* @param {number} days - 获取天数
* @param {string} end_date - 截止日期
*/
getTradeData: async (seccode, days = 60, end_date = null) => {
let url = `/api/market/trade/${seccode}?days=${days}`;
if (end_date) {
url += `&end_date=${end_date}`;
}
return await apiRequest(url);
},
/**
* 批量获取多只股票的交易数据日K线
* @param {string[]} codes - 股票代码数组6位代码
* @param {number} days - 获取天数默认1
* @param {string} end_date - 截止日期
* @returns {Promise<{success: boolean, data: Object}>}
*/
getBatchTradeData: async (codes, days = 1, end_date = null) => {
const body = { codes, days };
if (end_date) {
body.end_date = end_date;
}
return await apiRequest('/api/market/trade/batch', {
method: 'POST',
body: JSON.stringify(body)
});
},
/**
* 获取融资融券数据
* @param {string} seccode - 股票代码
* @param {number} days - 获取天数
*/
getFundingData: async (seccode, days = 30) => {
return await apiRequest(`/api/market/funding/${seccode}?days=${days}`);
},
/**
* 获取大宗交易数据
* @param {string} seccode - 股票代码
* @param {number} days - 获取天数
*/
getBigDealData: async (seccode, days = 30) => {
return await apiRequest(`/api/market/bigdeal/${seccode}?days=${days}`);
},
/**
* 获取龙虎榜数据
* @param {string} seccode - 股票代码
* @param {number} days - 获取天数
*/
getUnusualData: async (seccode, days = 30) => {
return await apiRequest(`/api/market/unusual/${seccode}?days=${days}`);
},
/**
* 获取股权质押数据
* @param {string} seccode - 股票代码
*/
getPledgeData: async (seccode) => {
return await apiRequest(`/api/market/pledge/${seccode}`);
},
/**
* 获取市场汇总数据
* @param {string} seccode - 股票代码
*/
getMarketSummary: async (seccode) => {
return await apiRequest(`/api/market/summary/${seccode}`);
},
/**
* 批量获取多只股票数据
* @param {array} seccodes - 股票代码数组
*/
getBatchMarketData: async (seccodes) => {
const promises = seccodes.map(code => marketService.getMarketSummary(code));
const results = await Promise.allSettled(promises);
return results.map((result, index) => {
if (result.status === 'fulfilled' && result.value.success) {
return result.value.data;
}
return {
stock_code: seccodes[index],
error: true,
message: result.reason?.message || 'Failed to fetch data'
};
});
},
};
// 数据格式化工具函数与financialService保持一致的风格
export const marketFormatUtils = {
/**
* 格式化大数字(亿/万)
* @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);
}
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 {number} volume - 成交量
*/
formatVolume: (volume) => {
if (!volume) return '-';
if (volume >= 100000000) {
return (volume / 100000000).toFixed(2) + '亿股';
} else if (volume >= 10000) {
return (volume / 10000).toFixed(2) + '万股';
}
return volume.toFixed(0) + '股';
},
/**
* 格式化成交额
* @param {number} amount - 成交额
*/
formatAmount: (amount) => {
if (!amount) return '-';
return '¥' + marketFormatUtils.formatLargeNumber(amount);
},
/**
* 获取涨跌颜色(中国市场:红涨绿跌)
* @param {number} value - 涨跌值
*/
getPriceColor: (value) => {
if (value === null || value === undefined) return 'gray.500';
if (value > 0) return 'red.500';
if (value < 0) return 'green.500';
return 'gray.500';
},
/**
* 格式化涨跌幅
* @param {number} change - 涨跌幅
*/
formatChange: (change) => {
if (change === null || change === undefined) return '-';
const formatted = change.toFixed(2) + '%';
return change >= 0 ? '+' + formatted : formatted;
},
/**
* 格式化换手率
* @param {number} turnover - 换手率
*/
formatTurnover: (turnover) => {
if (!turnover) return '-';
return turnover.toFixed(2) + '%';
},
/**
* 获取融资融券状态
* @param {object} funding - 融资融券数据
*/
getFundingStatus: (funding) => {
if (!funding) return { status: 'unknown', label: '未知' };
const netFinancing = funding.financing.buy - funding.financing.repay;
const netSecurities = funding.securities.sell - funding.securities.repay;
if (netFinancing > 0 && netSecurities <= 0) {
return { status: 'bullish', label: '看多', color: 'red.500' };
} else if (netFinancing <= 0 && netSecurities > 0) {
return { status: 'bearish', label: '看空', color: 'green.500' };
} else if (netFinancing > 0 && netSecurities > 0) {
return { status: 'divergent', label: '分歧', color: 'yellow.500' };
} else {
return { status: 'neutral', label: '中性', color: 'gray.500' };
}
},
/**
* 计算技术指标
* @param {array} data - K线数据
*/
calculateTechnicalIndicators: (data) => {
// 计算MA均线
const calculateMA = (period) => {
return data.map((item, index) => {
if (index < period - 1) return null;
const sum = data.slice(index - period + 1, index + 1)
.reduce((acc, cur) => acc + cur.close, 0);
return sum / period;
});
};
// 计算RSI
const calculateRSI = (period = 14) => {
const changes = data.map((item, index) => {
if (index === 0) return null;
return item.close - data[index - 1].close;
});
let avgGain = 0;
let avgLoss = 0;
for (let i = 1; i <= period; i++) {
if (changes[i] > 0) avgGain += changes[i];
else avgLoss += Math.abs(changes[i]);
}
avgGain /= period;
avgLoss /= period;
const rsi = [null];
for (let i = 1; i < period; i++) {
rsi.push(null);
}
for (let i = period; i < data.length; i++) {
const change = changes[i];
if (change > 0) {
avgGain = (avgGain * (period - 1) + change) / period;
avgLoss = (avgLoss * (period - 1)) / period;
} else {
avgGain = (avgGain * (period - 1)) / period;
avgLoss = (avgLoss * (period - 1) + Math.abs(change)) / period;
}
const rs = avgGain / avgLoss;
rsi.push(100 - (100 / (1 + rs)));
}
return rsi;
};
return {
ma5: calculateMA(5),
ma10: calculateMA(10),
ma20: calculateMA(20),
ma60: calculateMA(60),
rsi: calculateRSI(14)
};
},
/**
* 获取大宗交易评级
* @param {number} amount - 交易金额
* @param {number} avgAmount - 平均交易金额
*/
getBigDealRating: (amount, avgAmount) => {
if (!amount || !avgAmount) return { level: 'normal', label: '普通' };
const ratio = amount / avgAmount;
if (ratio >= 3) return { level: 'huge', label: '超大', color: 'red.600' };
if (ratio >= 2) return { level: 'large', label: '大额', color: 'orange.500' };
if (ratio >= 1.5) return { level: 'medium', label: '中等', color: 'yellow.500' };
return { level: 'normal', label: '普通', color: 'gray.500' };
},
};
// 图表数据处理工具
export const marketChartUtils = {
/**
* 准备K线图数据
* @param {array} data - 原始交易数据
*/
prepareKLineData: (data) => {
return data.map(item => ({
date: item.date.substring(5, 10), // MM-DD格式
fullDate: item.date,
open: item.open,
high: item.high,
low: item.low,
close: item.close,
volume: item.volume,
amount: item.amount,
change: item.change_percent,
k: [item.open, item.close, item.low, item.high], // K线数据格式
color: item.change_percent >= 0 ? '#ef4444' : '#10b981' // 红涨绿跌
}));
},
/**
* 准备融资融券趋势图数据
* @param {array} data - 原始融资融券数据
*/
prepareFundingTrendData: (data) => {
return data.map(item => ({
date: item.date.substring(5, 10),
fullDate: item.date,
financing: item.financing.balance / 100000000, // 转换为亿
securities: item.securities.balance_amount / 100000000,
total: item.total_balance / 100000000,
netBuy: (item.financing.buy - item.financing.repay) / 10000, // 转换为万
netSell: (item.securities.sell - item.securities.repay) / 10000
}));
},
/**
* 准备龙虎榜资金流向图数据
* @param {array} data - 龙虎榜数据
*/
prepareUnusualFlowData: (data) => {
const flowData = {};
data.grouped_data.forEach(dayData => {
flowData[dayData.date] = {
date: dayData.date,
totalBuy: dayData.total_buy / 10000, // 转换为万
totalSell: dayData.total_sell / 10000,
netFlow: dayData.net_amount / 10000,
buyersCount: dayData.buyers.length,
sellersCount: dayData.sellers.length
};
});
return Object.values(flowData).sort((a, b) =>
new Date(a.date) - new Date(b.date)
);
},
/**
* 准备股权质押趋势图数据
* @param {array} data - 原始质押数据
*/
preparePledgeTrendData: (data) => {
return data.map(item => ({
date: item.end_date.substring(0, 10),
pledgeRatio: item.pledge_ratio,
pledgeCount: item.pledge_count,
totalPledge: item.total_pledge / 10000, // 转换为万股
unrestricted: item.unrestricted_pledge / 10000,
restricted: item.restricted_pledge / 10000
})).reverse(); // 按时间正序排列
},
/**
* 获取图表配色方案
* @param {string} theme - 主题名称
*/
getChartTheme: (theme = 'default') => {
const themes = {
default: {
upColor: '#ef4444', // 红色上涨
downColor: '#10b981', // 绿色下跌
volumeUpColor: 'rgba(239, 68, 68, 0.7)',
volumeDownColor: 'rgba(16, 185, 129, 0.7)',
ma5Color: '#3b82f6',
ma10Color: '#8b5cf6',
ma20Color: '#f59e0b',
ma60Color: '#ec4899',
gridColor: '#e5e7eb'
},
dark: {
upColor: '#ef4444',
downColor: '#10b981',
volumeUpColor: 'rgba(239, 68, 68, 0.5)',
volumeDownColor: 'rgba(16, 185, 129, 0.5)',
ma5Color: '#60a5fa',
ma10Color: '#a78bfa',
ma20Color: '#fbbf24',
ma60Color: '#f472b6',
gridColor: '#374151'
}
};
return themes[theme] || themes.default;
},
};
// 数据缓存管理
export const marketCacheManager = {
cache: new Map(),
/**
* 生成缓存键
* @param {string} stockCode - 股票代码
* @param {string} dataType - 数据类型
* @param {object} params - 查询参数
*/
generateKey: (stockCode, dataType, params = {}) => {
return `market_${stockCode}_${dataType}_${JSON.stringify(params)}`;
},
/**
* 获取缓存数据
* @param {string} key - 缓存键
* @param {number} maxAge - 最大缓存时间(毫秒)
*/
get: function(key, maxAge = 5 * 60 * 1000) {
const cached = this.cache.get(key);
if (cached) {
const { data, timestamp } = cached;
if (Date.now() - timestamp < maxAge) {
logger.debug('marketCacheManager', '缓存命中', { key });
return data;
}
this.cache.delete(key);
}
return null;
},
/**
* 设置缓存数据
* @param {string} key - 缓存键
* @param {any} data - 要缓存的数据
*/
set: function(key, data) {
this.cache.set(key, {
data,
timestamp: Date.now()
});
logger.debug('marketCacheManager', '数据已缓存', { key });
},
/**
* 清除所有缓存
*/
clear: function() {
const cacheSize = this.cache.size;
this.cache.clear();
logger.debug('marketCacheManager', '清除所有缓存', { clearedCount: cacheSize });
},
/**
* 清除特定股票的缓存
* @param {string} stockCode - 股票代码
*/
clearStock: function(stockCode) {
let clearedCount = 0;
for (const key of this.cache.keys()) {
if (key.includes(`market_${stockCode}_`)) {
this.cache.delete(key);
clearedCount++;
}
}
logger.debug('marketCacheManager', '清除股票缓存', {
stockCode,
clearedCount
});
}
};
export default marketService;