Initial commit
This commit is contained in:
404
src/services/eventService.js
Normal file
404
src/services/eventService.js
Normal file
@@ -0,0 +1,404 @@
|
||||
// src/services/eventService.js
|
||||
|
||||
// 判断当前是否是生产环境
|
||||
const isProduction = process.env.NODE_ENV === 'production';
|
||||
const API_BASE_URL = isProduction ? "" : process.env.REACT_APP_API_URL;
|
||||
//const API_BASE_URL = process.env.REACT_APP_API_URL || "http://49.232.185.254:5001";
|
||||
|
||||
const apiRequest = async (url, options = {}) => {
|
||||
try {
|
||||
console.log(`Making API request to: ${API_BASE_URL}${url}`);
|
||||
|
||||
const response = await fetch(`${API_BASE_URL}${url}`, {
|
||||
...options,
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
...options.headers,
|
||||
},
|
||||
credentials: 'include', // 包含 cookies,以便后端识别登录状态
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const errorText = await response.text();
|
||||
console.error(`API request failed: ${response.status} - ${errorText}`);
|
||||
throw new Error(`HTTP error! status: ${response.status}`);
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
console.log(`API response from ${url}:`, data);
|
||||
|
||||
return data;
|
||||
} catch (error) {
|
||||
console.error(`API request failed for ${url}:`, error);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
export const eventService = {
|
||||
getEvents: (params = {}) => {
|
||||
// Filter out empty params
|
||||
const cleanParams = Object.fromEntries(Object.entries(params).filter(([_, v]) => v != null && v !== ''));
|
||||
const query = new URLSearchParams(cleanParams).toString();
|
||||
return apiRequest(`/api/events/?${query}`);
|
||||
},
|
||||
getHotEvents: (params = {}) => {
|
||||
const query = new URLSearchParams(params).toString();
|
||||
return apiRequest(`/api/events/hot?${query}`);
|
||||
},
|
||||
getPopularKeywords: (limit = 20) => {
|
||||
return apiRequest(`/api/events/keywords/popular?limit=${limit}`);
|
||||
},
|
||||
calendar: {
|
||||
/**
|
||||
* Fetches event counts for a given month, for display on the calendar grid.
|
||||
* @param {number} year - The year to fetch counts for.
|
||||
* @param {number} month - The month to fetch counts for (1-12).
|
||||
* @returns {Promise<object>} A list of objects with date, count, and className.
|
||||
*/
|
||||
getEventCounts: (year, month) => {
|
||||
// Note: The backend route is `/api/v1/calendar/event-counts`, not `/api/calendar-event-counts`
|
||||
return apiRequest(`/api/v1/calendar/event-counts?year=${year}&month=${month}`);
|
||||
},
|
||||
|
||||
/**
|
||||
* Fetches a list of all events for a specific date.
|
||||
* @param {string} date - The date in 'YYYY-MM-DD' format.
|
||||
* @param {string} [type='all'] - The type of events to fetch ('event', 'data', or 'all').
|
||||
* @returns {Promise<object>} A list of event objects for the given date.
|
||||
*/
|
||||
getEventsForDate: (date, type = 'all') => {
|
||||
return apiRequest(`/api/v1/calendar/events?date=${date}&type=${type}`);
|
||||
},
|
||||
|
||||
/**
|
||||
* Fetches the full details of a single calendar event.
|
||||
* @param {number} eventId - The ID of the calendar event.
|
||||
* @returns {Promise<object>} The detailed calendar event object.
|
||||
*/
|
||||
getEventDetail: (eventId) => {
|
||||
return apiRequest(`/api/v1/calendar/events/${eventId}`);
|
||||
},
|
||||
|
||||
/**
|
||||
* 切换未来事件的关注状态
|
||||
* @param {number} eventId - 未来事件ID
|
||||
* @returns {Promise<object>} 返回关注状态
|
||||
*/
|
||||
toggleFollow: async (eventId) => {
|
||||
return await apiRequest(`/api/v1/calendar/events/${eventId}/follow`, {
|
||||
method: 'POST'
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* 获取用户关注的所有未来事件
|
||||
* @returns {Promise<object>} 返回关注的未来事件列表
|
||||
*/
|
||||
getFollowingEvents: async () => {
|
||||
return await apiRequest(`/api/account/future-events/following`);
|
||||
},
|
||||
},
|
||||
getEventDetail: async (eventId) => {
|
||||
return await apiRequest(`/api/events/${eventId}`);
|
||||
},
|
||||
|
||||
getRelatedStocks: async (eventId) => {
|
||||
return await apiRequest(`/api/events/${eventId}/stocks`);
|
||||
},
|
||||
|
||||
addRelatedStock: async (eventId, stockData) => {
|
||||
return await apiRequest(`/api/events/${eventId}/stocks`, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify(stockData),
|
||||
});
|
||||
},
|
||||
|
||||
deleteRelatedStock: async (stockId) => {
|
||||
return await apiRequest(`/api/stocks/${stockId}`, {
|
||||
method: 'DELETE',
|
||||
});
|
||||
},
|
||||
|
||||
getRelatedConcepts: async (eventId) => {
|
||||
return await apiRequest(`/api/events/${eventId}/concepts`);
|
||||
},
|
||||
|
||||
getHistoricalEvents: async (eventId) => {
|
||||
return await apiRequest(`/api/events/${eventId}/historical`);
|
||||
},
|
||||
|
||||
getExpectationScore: async (eventId) => {
|
||||
return await apiRequest(`/api/events/${eventId}/expectation-score`);
|
||||
},
|
||||
|
||||
getTransmissionChainAnalysis: async (eventId) => {
|
||||
// This API is optimized: it filters isolated nodes and provides a clean data structure.
|
||||
return await apiRequest(`/api/events/${eventId}/transmission`);
|
||||
},
|
||||
|
||||
getChainNodeDetail: async (eventId, nodeId) => {
|
||||
return await apiRequest(`/api/events/${eventId}/chain-node/${nodeId}`);
|
||||
},
|
||||
getSankeyData: async (eventId) => {
|
||||
return await apiRequest(`/api/events/${eventId}/sankey-data`);
|
||||
},
|
||||
getSankeyNodeDetail: async (eventId, nodeInfo) => {
|
||||
const params = new URLSearchParams({
|
||||
node_name: nodeInfo.name,
|
||||
node_type: nodeInfo.type,
|
||||
});
|
||||
if (nodeInfo.level !== undefined && nodeInfo.level !== null) {
|
||||
params.append('node_level', nodeInfo.level);
|
||||
}
|
||||
const url = `/api/events/${eventId}/sankey-data?${params.toString()}`;
|
||||
return await apiRequest(url);
|
||||
},
|
||||
toggleFollow: async (eventId, isFollowing) => {
|
||||
return await apiRequest(`/api/events/${eventId}/follow`, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({ is_following: isFollowing }),
|
||||
});
|
||||
},
|
||||
|
||||
// 帖子相关API
|
||||
getPosts: async (eventId, sortType = 'latest', page = 1, perPage = 20) => {
|
||||
try {
|
||||
return await apiRequest(`/api/events/${eventId}/posts?sort=${sortType}&page=${page}&per_page=${perPage}`);
|
||||
} catch (error) {
|
||||
console.error('获取帖子失败:', error);
|
||||
return { success: false, data: [], pagination: {} };
|
||||
}
|
||||
},
|
||||
|
||||
createPost: async (eventId, postData) => {
|
||||
try {
|
||||
return await apiRequest(`/api/events/${eventId}/posts`, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify(postData)
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('创建帖子失败:', error);
|
||||
return { success: false, message: '创建帖子失败' };
|
||||
}
|
||||
},
|
||||
|
||||
deletePost: async (postId) => {
|
||||
try {
|
||||
return await apiRequest(`/api/posts/${postId}`, {
|
||||
method: 'DELETE'
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('删除帖子失败:', error);
|
||||
return { success: false, message: '删除帖子失败' };
|
||||
}
|
||||
},
|
||||
|
||||
likePost: async (postId) => {
|
||||
try {
|
||||
return await apiRequest(`/api/posts/${postId}/like`, {
|
||||
method: 'POST'
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('点赞失败:', error);
|
||||
return { success: false, message: '点赞失败' };
|
||||
}
|
||||
},
|
||||
|
||||
// 评论相关API
|
||||
getPostComments: async (postId, sortType = 'latest') => {
|
||||
try {
|
||||
return await apiRequest(`/api/posts/${postId}/comments?sort=${sortType}`);
|
||||
} catch (error) {
|
||||
console.error('获取评论失败:', error);
|
||||
return { success: false, data: [] };
|
||||
}
|
||||
},
|
||||
|
||||
addPostComment: async (postId, commentData) => {
|
||||
try {
|
||||
return await apiRequest(`/api/posts/${postId}/comments`, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify(commentData)
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('添加评论失败:', error);
|
||||
return { success: false, message: '添加评论失败' };
|
||||
}
|
||||
},
|
||||
|
||||
deleteComment: async (commentId) => {
|
||||
try {
|
||||
return await apiRequest(`/api/comments/${commentId}`, {
|
||||
method: 'DELETE'
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('删除评论失败:', error);
|
||||
return { success: false, message: '删除评论失败' };
|
||||
}
|
||||
},
|
||||
|
||||
likeComment: async (commentId) => {
|
||||
try {
|
||||
return await apiRequest(`/api/comments/${commentId}/like`, {
|
||||
method: 'POST'
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('点赞失败:', error);
|
||||
return { success: false, message: '点赞失败' };
|
||||
}
|
||||
},
|
||||
|
||||
// 兼容旧版本的评论API
|
||||
getComments: async (eventId, sortType = 'latest') => {
|
||||
console.warn('getComments 已废弃,请使用 getPosts');
|
||||
// 直接调用 getPosts 的实现,避免循环引用
|
||||
try {
|
||||
return await apiRequest(`/api/events/${eventId}/posts?sort=${sortType}&page=1&per_page=20`);
|
||||
} catch (error) {
|
||||
console.error('获取帖子失败:', error);
|
||||
return { success: false, data: [], pagination: {} };
|
||||
}
|
||||
},
|
||||
|
||||
addComment: async (eventId, commentData) => {
|
||||
console.warn('addComment 已废弃,请使用 createPost');
|
||||
// 直接调用 createPost 的实现,避免循环引用
|
||||
try {
|
||||
return await apiRequest(`/api/events/${eventId}/posts`, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify(commentData)
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('创建帖子失败:', error);
|
||||
return { success: false, message: '创建帖子失败' };
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
export const stockService = {
|
||||
getQuotes: async (codes, eventTime = null) => {
|
||||
try {
|
||||
const requestBody = {
|
||||
codes: codes
|
||||
};
|
||||
|
||||
if (eventTime) {
|
||||
requestBody.event_time = eventTime;
|
||||
}
|
||||
|
||||
console.log(`获取股票报价,请求体:`, requestBody);
|
||||
|
||||
const response = await apiRequest(`/api/stock/quotes`, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify(requestBody)
|
||||
});
|
||||
|
||||
console.log('股票报价原始响应:', response);
|
||||
console.log('response.success:', response.success);
|
||||
console.log('response.data:', response.data);
|
||||
|
||||
if (response.success && response.data) {
|
||||
console.log('返回股票报价数据:', response.data);
|
||||
return response.data;
|
||||
} else {
|
||||
console.warn('股票报价响应格式异常:', response);
|
||||
return {};
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('获取股票报价失败:', error);
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
getForecastReport: async (stockCode) => {
|
||||
return await apiRequest(`/api/stock/${stockCode}/forecast-report`);
|
||||
},
|
||||
|
||||
getKlineData: async (stockCode, chartType = 'timeline', eventTime = null) => {
|
||||
try {
|
||||
const params = new URLSearchParams();
|
||||
params.append('type', chartType);
|
||||
if (eventTime) {
|
||||
params.append('event_time', eventTime);
|
||||
}
|
||||
|
||||
const url = `/api/stock/${stockCode}/kline?${params.toString()}`;
|
||||
console.log(`获取K线数据: ${API_BASE_URL}${url}`);
|
||||
|
||||
const response = await apiRequest(url);
|
||||
|
||||
if (response.error) {
|
||||
console.warn('K线数据响应包含错误:', response.error);
|
||||
return response;
|
||||
}
|
||||
|
||||
return response;
|
||||
} catch (error) {
|
||||
console.error('获取K线数据失败:', error);
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
getTransmissionChainAnalysis: async (eventId) => {
|
||||
return await apiRequest(`/api/events/${eventId}/transmission`);
|
||||
},
|
||||
async getSankeyNodeDetail(eventId, nodeInfo) {
|
||||
try {
|
||||
const params = new URLSearchParams({
|
||||
node_name: nodeInfo.name,
|
||||
node_type: nodeInfo.type,
|
||||
node_level: nodeInfo.level
|
||||
});
|
||||
|
||||
const response = await fetch(`/api/events/${eventId}/sankey-node-detail?${params}`, {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
}
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP error! status: ${response.status}`);
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
return data;
|
||||
} catch (error) {
|
||||
console.error('Error fetching sankey node detail:', error);
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
|
||||
getChainNodeDetail: async (eventId, nodeId) => {
|
||||
return await apiRequest(`/api/events/${eventId}/transmission/node/${nodeId}`);
|
||||
},
|
||||
|
||||
getHistoricalEventStocks: async (eventId) => {
|
||||
return await apiRequest(`/api/historical-events/${eventId}/stocks`);
|
||||
},
|
||||
|
||||
};
|
||||
|
||||
export const indexService = {
|
||||
getKlineData: async (indexCode, chartType = 'timeline', eventTime = null) => {
|
||||
try {
|
||||
const params = new URLSearchParams();
|
||||
params.append('type', chartType);
|
||||
if (eventTime) {
|
||||
params.append('event_time', eventTime);
|
||||
}
|
||||
|
||||
const url = `/api/index/${indexCode}/kline?${params.toString()}`;
|
||||
console.log(`获取指数K线数据: ${API_BASE_URL}${url}`);
|
||||
const response = await apiRequest(url);
|
||||
return response;
|
||||
} catch (error) {
|
||||
console.error('获取指数K线数据失败:', error);
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
export default {
|
||||
eventService,
|
||||
stockService,
|
||||
indexService,
|
||||
};
|
||||
399
src/services/financialService.js
Normal file
399
src/services/financialService.js
Normal file
@@ -0,0 +1,399 @@
|
||||
// src/services/financialService.js
|
||||
/**
|
||||
* 完整的财务数据服务层
|
||||
* 对应Flask后端的所有财务API接口
|
||||
*/
|
||||
|
||||
const isProduction = process.env.NODE_ENV === 'production';
|
||||
const API_BASE_URL = isProduction ? "" : process.env.REACT_APP_API_URL;
|
||||
|
||||
const apiRequest = async (url, options = {}) => {
|
||||
try {
|
||||
console.log(`Making Financial API request to: ${API_BASE_URL}${url}`);
|
||||
|
||||
const response = await fetch(`${API_BASE_URL}${url}`, {
|
||||
...options,
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
...options.headers,
|
||||
},
|
||||
credentials: 'include', // 包含 cookies,以便后端识别登录状态
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const errorText = await response.text();
|
||||
console.error(`Financial API request failed: ${response.status} - ${errorText}`);
|
||||
throw new Error(`HTTP error! status: ${response.status}`);
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
console.log(`Financial API response from ${url}:`, data);
|
||||
|
||||
return data;
|
||||
} catch (error) {
|
||||
console.error(`Financial API request failed for ${url}:`, error);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
export const financialService = {
|
||||
/**
|
||||
* 获取股票基本信息和最新财务摘要
|
||||
* @param {string} seccode - 股票代码
|
||||
*/
|
||||
getStockInfo: async (seccode) => {
|
||||
return await apiRequest(`/api/financial/stock-info/${seccode}`);
|
||||
},
|
||||
|
||||
/**
|
||||
* 获取完整的资产负债表数据
|
||||
* @param {string} seccode - 股票代码
|
||||
* @param {number} limit - 获取的报告期数量
|
||||
*/
|
||||
getBalanceSheet: async (seccode, limit = 12) => {
|
||||
return await apiRequest(`/api/financial/balance-sheet/${seccode}?limit=${limit}`);
|
||||
},
|
||||
|
||||
/**
|
||||
* 获取完整的利润表数据
|
||||
* @param {string} seccode - 股票代码
|
||||
* @param {number} limit - 获取的报告期数量
|
||||
*/
|
||||
getIncomeStatement: async (seccode, limit = 12) => {
|
||||
return await apiRequest(`/api/financial/income-statement/${seccode}?limit=${limit}`);
|
||||
},
|
||||
|
||||
/**
|
||||
* 获取完整的现金流量表数据
|
||||
* @param {string} seccode - 股票代码
|
||||
* @param {number} limit - 获取的报告期数量
|
||||
*/
|
||||
getCashflow: async (seccode, limit = 12) => {
|
||||
return await apiRequest(`/api/financial/cashflow/${seccode}?limit=${limit}`);
|
||||
},
|
||||
|
||||
/**
|
||||
* 获取完整的财务指标数据
|
||||
* @param {string} seccode - 股票代码
|
||||
* @param {number} limit - 获取的报告期数量
|
||||
*/
|
||||
getFinancialMetrics: async (seccode, limit = 12) => {
|
||||
return await apiRequest(`/api/financial/financial-metrics/${seccode}?limit=${limit}`);
|
||||
},
|
||||
|
||||
/**
|
||||
* 获取主营业务构成数据
|
||||
* @param {string} seccode - 股票代码
|
||||
* @param {number} periods - 获取的报告期数量
|
||||
*/
|
||||
getMainBusiness: async (seccode, periods = 4) => {
|
||||
return await apiRequest(`/api/financial/main-business/${seccode}?periods=${periods}`);
|
||||
},
|
||||
|
||||
/**
|
||||
* 获取业绩预告和预披露时间
|
||||
* @param {string} seccode - 股票代码
|
||||
*/
|
||||
getForecast: async (seccode) => {
|
||||
return await apiRequest(`/api/financial/forecast/${seccode}`);
|
||||
},
|
||||
|
||||
/**
|
||||
* 获取行业排名数据
|
||||
* @param {string} seccode - 股票代码
|
||||
* @param {number} limit - 获取的报告期数量
|
||||
*/
|
||||
getIndustryRank: async (seccode, limit = 4) => {
|
||||
return await apiRequest(`/api/financial/industry-rank/${seccode}?limit=${limit}`);
|
||||
},
|
||||
|
||||
/**
|
||||
* 获取不同报告期的对比数据
|
||||
* @param {string} seccode - 股票代码
|
||||
* @param {number} periods - 对比的报告期数量
|
||||
*/
|
||||
getPeriodComparison: async (seccode, periods = 8) => {
|
||||
return await apiRequest(`/api/financial/comparison/${seccode}?periods=${periods}`);
|
||||
},
|
||||
};
|
||||
|
||||
// 数据格式化工具函数
|
||||
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;
|
||||
28
src/services/industryService.js
Normal file
28
src/services/industryService.js
Normal file
@@ -0,0 +1,28 @@
|
||||
// src/services/industryService.js
|
||||
import axios from 'axios';
|
||||
|
||||
// 判断当前是否是生产环境
|
||||
const isProduction = process.env.NODE_ENV === 'production';
|
||||
|
||||
|
||||
const API_BASE_URL = isProduction ? "" : process.env.REACT_APP_API_URL;
|
||||
|
||||
// 配置 axios 默认包含 credentials
|
||||
axios.defaults.withCredentials = true;
|
||||
|
||||
export const industryService = {
|
||||
// 获取所有行业分类体系
|
||||
async getClassifications() {
|
||||
const res = await axios.get(`${API_BASE_URL}/api/classifications`);
|
||||
return res.data;
|
||||
},
|
||||
// 获取指定体系下的多级行业
|
||||
async getLevels({ classification, level = 1, level1_name, level2_name, level3_name }) {
|
||||
let url = `${API_BASE_URL}/api/levels?classification=${encodeURIComponent(classification)}&level=${level}`;
|
||||
if (level1_name) url += `&level1_name=${encodeURIComponent(level1_name)}`;
|
||||
if (level2_name) url += `&level2_name=${encodeURIComponent(level2_name)}`;
|
||||
if (level3_name) url += `&level3_name=${encodeURIComponent(level3_name)}`;
|
||||
const res = await axios.get(url);
|
||||
return res.data;
|
||||
}
|
||||
};
|
||||
495
src/services/marketService.js
Normal file
495
src/services/marketService.js
Normal file
@@ -0,0 +1,495 @@
|
||||
// src/services/marketService.js
|
||||
/**
|
||||
* 完整的市场行情数据服务层
|
||||
* 对应Flask后端的所有市场API接口
|
||||
*/
|
||||
|
||||
const isProduction = process.env.NODE_ENV === 'production';
|
||||
const API_BASE_URL = isProduction ? "" : process.env.REACT_APP_API_URL;
|
||||
|
||||
const apiRequest = async (url, options = {}) => {
|
||||
try {
|
||||
console.log(`Making Market API request to: ${API_BASE_URL}${url}`);
|
||||
|
||||
const response = await fetch(`${API_BASE_URL}${url}`, {
|
||||
...options,
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
...options.headers,
|
||||
},
|
||||
credentials: 'include', // 包含 cookies,以便后端识别登录状态
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const errorText = await response.text();
|
||||
console.error(`Market API request failed: ${response.status} - ${errorText}`);
|
||||
throw new Error(`HTTP error! status: ${response.status}`);
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
console.log(`Market API response from ${url}:`, data);
|
||||
|
||||
return data;
|
||||
} catch (error) {
|
||||
console.error(`Market API request failed for ${url}:`, error);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
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);
|
||||
},
|
||||
|
||||
/**
|
||||
* 获取融资融券数据
|
||||
* @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) {
|
||||
console.log(`Cache hit for key: ${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()
|
||||
});
|
||||
console.log(`Data cached for key: ${key}`);
|
||||
},
|
||||
|
||||
/**
|
||||
* 清除所有缓存
|
||||
*/
|
||||
clear: function() {
|
||||
this.cache.clear();
|
||||
console.log('All market cache cleared');
|
||||
},
|
||||
|
||||
/**
|
||||
* 清除特定股票的缓存
|
||||
* @param {string} stockCode - 股票代码
|
||||
*/
|
||||
clearStock: function(stockCode) {
|
||||
for (const key of this.cache.keys()) {
|
||||
if (key.includes(`market_${stockCode}_`)) {
|
||||
this.cache.delete(key);
|
||||
}
|
||||
}
|
||||
console.log(`Cache cleared for stock: ${stockCode}`);
|
||||
}
|
||||
};
|
||||
|
||||
export default marketService;
|
||||
Reference in New Issue
Block a user