update pay ui
This commit is contained in:
309
src/services/ztStaticService.js
Normal file
309
src/services/ztStaticService.js
Normal file
@@ -0,0 +1,309 @@
|
||||
/**
|
||||
* 涨停分析静态数据服务
|
||||
* 从 /data/zt/ 目录读取预生成的 JSON 文件
|
||||
* 不依赖后端 API,适合静态部署
|
||||
*/
|
||||
|
||||
// 数据基础路径
|
||||
const DATA_BASE_URL = '/data/zt';
|
||||
|
||||
// 内存缓存
|
||||
const cache = {
|
||||
dates: null,
|
||||
daily: new Map(),
|
||||
};
|
||||
|
||||
/**
|
||||
* 获取可用日期列表
|
||||
*/
|
||||
export const fetchAvailableDates = async () => {
|
||||
try {
|
||||
// 使用缓存
|
||||
if (cache.dates) {
|
||||
return { success: true, events: cache.dates };
|
||||
}
|
||||
|
||||
const response = await fetch(`${DATA_BASE_URL}/dates.json`);
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP ${response.status}`);
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
// 转换为日历事件格式
|
||||
const events = (data.dates || []).map(d => ({
|
||||
title: `${d.count}只`,
|
||||
start: d.formatted_date,
|
||||
end: d.formatted_date,
|
||||
className: 'bg-gradient-primary',
|
||||
allDay: true,
|
||||
date: d.date,
|
||||
count: d.count,
|
||||
}));
|
||||
|
||||
// 缓存结果
|
||||
cache.dates = events;
|
||||
|
||||
return { success: true, events, total: events.length };
|
||||
} catch (error) {
|
||||
console.error('[ztStaticService] fetchAvailableDates error:', error);
|
||||
return { success: false, error: error.message, events: [] };
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 获取指定日期的分析数据
|
||||
*/
|
||||
export const fetchDailyAnalysis = async (date) => {
|
||||
try {
|
||||
// 使用缓存
|
||||
if (cache.daily.has(date)) {
|
||||
return { success: true, data: cache.daily.get(date), from_cache: true };
|
||||
}
|
||||
|
||||
const response = await fetch(`${DATA_BASE_URL}/daily/${date}.json`);
|
||||
if (!response.ok) {
|
||||
if (response.status === 404) {
|
||||
return { success: false, error: `日期 ${date} 的数据不存在` };
|
||||
}
|
||||
throw new Error(`HTTP ${response.status}`);
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
// 缓存结果
|
||||
cache.daily.set(date, data);
|
||||
|
||||
return { success: true, data, from_cache: false };
|
||||
} catch (error) {
|
||||
console.error('[ztStaticService] fetchDailyAnalysis error:', error);
|
||||
return { success: false, error: error.message };
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 获取词云数据
|
||||
* 从每日分析数据中提取
|
||||
*/
|
||||
export const fetchWordCloudData = async (date) => {
|
||||
try {
|
||||
const result = await fetchDailyAnalysis(date);
|
||||
if (!result.success) {
|
||||
return result;
|
||||
}
|
||||
|
||||
const wordFreqData = result.data.word_freq_data || [];
|
||||
return { success: true, data: wordFreqData };
|
||||
} catch (error) {
|
||||
console.error('[ztStaticService] fetchWordCloudData error:', error);
|
||||
return { success: false, error: error.message, data: [] };
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 获取高位股统计
|
||||
* 从每日分析数据中的 stocks 计算
|
||||
*/
|
||||
export const fetchHighPositionStocks = async (date) => {
|
||||
try {
|
||||
const result = await fetchDailyAnalysis(date);
|
||||
if (!result.success) {
|
||||
return result;
|
||||
}
|
||||
|
||||
const stocks = result.data.stocks || [];
|
||||
|
||||
// 筛选连板股(continuous_days 包含数字 >= 2)
|
||||
const highPositionStocks = stocks
|
||||
.filter(stock => {
|
||||
const days = parseContinuousDays(stock.continuous_days);
|
||||
return days >= 2;
|
||||
})
|
||||
.map(stock => {
|
||||
const days = parseContinuousDays(stock.continuous_days);
|
||||
return {
|
||||
stock_code: stock.scode,
|
||||
stock_name: stock.sname,
|
||||
price: '-', // 静态数据中没有实时价格
|
||||
increase_rate: 10.0, // 涨停固定 10%
|
||||
continuous_limit_up: days,
|
||||
industry: (stock.core_sectors || [])[0] || '未知',
|
||||
turnover_rate: '-', // 静态数据中没有换手率
|
||||
brief: stock.brief || '',
|
||||
};
|
||||
})
|
||||
.sort((a, b) => b.continuous_limit_up - a.continuous_limit_up);
|
||||
|
||||
// 计算统计数据
|
||||
const totalCount = highPositionStocks.length;
|
||||
const maxDays = highPositionStocks.length > 0
|
||||
? Math.max(...highPositionStocks.map(s => s.continuous_limit_up))
|
||||
: 0;
|
||||
const avgDays = highPositionStocks.length > 0
|
||||
? (highPositionStocks.reduce((sum, s) => sum + s.continuous_limit_up, 0) / totalCount).toFixed(1)
|
||||
: 0;
|
||||
|
||||
return {
|
||||
success: true,
|
||||
data: {
|
||||
stocks: highPositionStocks,
|
||||
statistics: {
|
||||
total_count: totalCount,
|
||||
max_continuous_days: maxDays,
|
||||
avg_continuous_days: avgDays,
|
||||
},
|
||||
},
|
||||
};
|
||||
} catch (error) {
|
||||
console.error('[ztStaticService] fetchHighPositionStocks error:', error);
|
||||
return { success: false, error: error.message };
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 解析连板天数
|
||||
* 例如 "2连板" -> 2, "首板" -> 1
|
||||
*/
|
||||
const parseContinuousDays = (str) => {
|
||||
if (!str) return 1;
|
||||
const match = str.match(/(\d+)/);
|
||||
if (match) {
|
||||
return parseInt(match[1], 10);
|
||||
}
|
||||
if (str.includes('首板')) return 1;
|
||||
return 1;
|
||||
};
|
||||
|
||||
/**
|
||||
* 关键词搜索股票
|
||||
* 从缓存的数据中搜索
|
||||
*/
|
||||
export const searchStocks = async (searchParams) => {
|
||||
try {
|
||||
const { query, date, date_range, page = 1, page_size = 20 } = searchParams;
|
||||
|
||||
if (!query || query.trim() === '') {
|
||||
return { success: false, error: '搜索关键词不能为空' };
|
||||
}
|
||||
|
||||
const queryLower = query.toLowerCase().trim();
|
||||
let allStocks = [];
|
||||
|
||||
// 确定要搜索的日期范围
|
||||
let datesToSearch = [];
|
||||
|
||||
if (date) {
|
||||
datesToSearch = [date];
|
||||
} else if (date_range?.start && date_range?.end) {
|
||||
// 从缓存的日期中筛选
|
||||
const datesResult = await fetchAvailableDates();
|
||||
if (datesResult.success) {
|
||||
datesToSearch = datesResult.events
|
||||
.filter(d => d.date >= date_range.start && d.date <= date_range.end)
|
||||
.map(d => d.date);
|
||||
}
|
||||
} else {
|
||||
// 默认搜索最近 30 天
|
||||
const datesResult = await fetchAvailableDates();
|
||||
if (datesResult.success) {
|
||||
datesToSearch = datesResult.events.slice(0, 30).map(d => d.date);
|
||||
}
|
||||
}
|
||||
|
||||
// 从每个日期的数据中搜索
|
||||
for (const d of datesToSearch) {
|
||||
const result = await fetchDailyAnalysis(d);
|
||||
if (result.success && result.data.stocks) {
|
||||
const stocks = result.data.stocks.map(s => ({ ...s, date: d }));
|
||||
allStocks = allStocks.concat(stocks);
|
||||
}
|
||||
}
|
||||
|
||||
// 关键词匹配
|
||||
const results = allStocks
|
||||
.map(stock => {
|
||||
let score = 0;
|
||||
|
||||
// 精确匹配股票代码
|
||||
if (queryLower === (stock.scode || '').toLowerCase()) {
|
||||
score = 100;
|
||||
}
|
||||
// 精确匹配股票名称
|
||||
else if (queryLower === (stock.sname || '').toLowerCase()) {
|
||||
score = 90;
|
||||
}
|
||||
// 部分匹配股票名称
|
||||
else if ((stock.sname || '').toLowerCase().includes(queryLower)) {
|
||||
score = 80;
|
||||
}
|
||||
// 匹配板块
|
||||
else if ((stock.core_sectors || []).some(s => s.toLowerCase().includes(queryLower))) {
|
||||
score = 70;
|
||||
}
|
||||
// 匹配涨停原因
|
||||
else if ((stock.brief || '').toLowerCase().includes(queryLower)) {
|
||||
score = 60;
|
||||
}
|
||||
|
||||
return { ...stock, _score: score };
|
||||
})
|
||||
.filter(s => s._score > 0)
|
||||
.sort((a, b) => b._score - a._score || b.date.localeCompare(a.date));
|
||||
|
||||
// 分页
|
||||
const total = results.length;
|
||||
const start = (page - 1) * page_size;
|
||||
const pageResults = results.slice(start, start + page_size);
|
||||
|
||||
return {
|
||||
success: true,
|
||||
data: {
|
||||
stocks: pageResults,
|
||||
total,
|
||||
page,
|
||||
page_size,
|
||||
total_pages: Math.ceil(total / page_size),
|
||||
search_mode: 'keyword',
|
||||
},
|
||||
};
|
||||
} catch (error) {
|
||||
console.error('[ztStaticService] searchStocks error:', error);
|
||||
return { success: false, error: error.message };
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 批量获取股票详情
|
||||
*/
|
||||
export const fetchStocksBatchDetail = async (codes, date) => {
|
||||
try {
|
||||
const result = await fetchDailyAnalysis(date);
|
||||
if (!result.success) {
|
||||
return result;
|
||||
}
|
||||
|
||||
const stocks = (result.data.stocks || []).filter(s => codes.includes(s.scode));
|
||||
return { success: true, data: stocks };
|
||||
} catch (error) {
|
||||
console.error('[ztStaticService] fetchStocksBatchDetail error:', error);
|
||||
return { success: false, error: error.message };
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 清除缓存
|
||||
*/
|
||||
export const clearCache = () => {
|
||||
cache.dates = null;
|
||||
cache.daily.clear();
|
||||
};
|
||||
|
||||
export default {
|
||||
fetchAvailableDates,
|
||||
fetchDailyAnalysis,
|
||||
fetchWordCloudData,
|
||||
fetchHighPositionStocks,
|
||||
searchStocks,
|
||||
fetchStocksBatchDetail,
|
||||
clearCache,
|
||||
};
|
||||
@@ -28,36 +28,36 @@ import {
|
||||
} from '@chakra-ui/react';
|
||||
import { StarIcon, TriangleUpIcon } from '@chakra-ui/icons';
|
||||
import { logger } from '../../../utils/logger';
|
||||
import { getApiBase } from '../../../utils/apiConfig';
|
||||
import ztStaticService from '../../../services/ztStaticService';
|
||||
|
||||
const HighPositionStocks = ({ dateStr }) => {
|
||||
const [highPositionData, setHighPositionData] = useState(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
|
||||
|
||||
const cardBg = useColorModeValue('white', 'gray.800');
|
||||
const accentColor = useColorModeValue('blue.500', 'blue.300');
|
||||
|
||||
useEffect(() => {
|
||||
fetchHighPositionStocks();
|
||||
if (dateStr) {
|
||||
fetchHighPositionStocks();
|
||||
}
|
||||
}, [dateStr]);
|
||||
|
||||
const fetchHighPositionStocks = async () => {
|
||||
setLoading(true);
|
||||
try {
|
||||
const API_URL = process.env.NODE_ENV === 'production' ? `${getApiBase()}/report-api` : 'http://111.198.58.126:5001';
|
||||
const response = await fetch(`${API_URL}/api/limit-analyse/high-position-stocks?date=${dateStr}`);
|
||||
const data = await response.json();
|
||||
const data = await ztStaticService.fetchHighPositionStocks(dateStr);
|
||||
|
||||
logger.debug('HighPositionStocks', 'API响应', {
|
||||
logger.debug('HighPositionStocks', '静态数据响应', {
|
||||
date: dateStr,
|
||||
success: data.success,
|
||||
dataLength: data.data?.length
|
||||
stockCount: data.data?.stocks?.length
|
||||
});
|
||||
|
||||
if (data.success) {
|
||||
setHighPositionData(data.data);
|
||||
} else {
|
||||
logger.warn('HighPositionStocks', 'API返回失败', {
|
||||
logger.warn('HighPositionStocks', '数据获取失败', {
|
||||
date: dateStr,
|
||||
error: data.error
|
||||
});
|
||||
|
||||
@@ -33,10 +33,8 @@ import {
|
||||
// 注意:在实际使用中,这些组件应该被拆分到独立的文件中
|
||||
// 这里为了演示,我们假设它们已经被正确导出
|
||||
|
||||
import { getApiBase } from '../../utils/apiConfig';
|
||||
|
||||
// API配置
|
||||
const API_URL = process.env.NODE_ENV === 'production' ? `${getApiBase()}/report-api` : 'http://111.198.58.126:5001';
|
||||
// 使用静态数据服务(从 /data/zt/ 读取 JSON 文件)
|
||||
import ztStaticService from '../../services/ztStaticService';
|
||||
|
||||
// 导入的组件(实际使用时应该从独立文件导入)
|
||||
// 恢复使用本页自带的轻量日历
|
||||
@@ -112,14 +110,13 @@ export default function LimitAnalyse() {
|
||||
}
|
||||
}, [availableDates]);
|
||||
|
||||
// API调用函数
|
||||
// 使用静态数据服务获取数据
|
||||
const fetchAvailableDates = async () => {
|
||||
try {
|
||||
const response = await fetch(`${API_URL}/api/v1/dates/available`);
|
||||
const data = await response.json();
|
||||
const data = await ztStaticService.fetchAvailableDates();
|
||||
if (data.success) {
|
||||
setAvailableDates(data.events);
|
||||
logger.debug('LimitAnalyse', '可用日期加载成功', {
|
||||
logger.debug('LimitAnalyse', '可用日期加载成功(静态文件)', {
|
||||
count: data.events?.length || 0
|
||||
});
|
||||
}
|
||||
@@ -131,47 +128,29 @@ export default function LimitAnalyse() {
|
||||
const fetchDailyAnalysis = async (date) => {
|
||||
setLoading(true);
|
||||
try {
|
||||
const response = await fetch(`${API_URL}/api/v1/analysis/daily/${date}`);
|
||||
const data = await response.json();
|
||||
const data = await ztStaticService.fetchDailyAnalysis(date);
|
||||
if (data.success) {
|
||||
setDailyData(data.data);
|
||||
|
||||
// 🎯 追踪每日统计数据查看
|
||||
trackDailyStatsViewed(data.data, date);
|
||||
|
||||
// 获取词云数据
|
||||
fetchWordCloudData(date);
|
||||
// 词云数据已包含在分析数据中
|
||||
setWordCloudData(data.data.word_freq_data || []);
|
||||
|
||||
logger.debug('LimitAnalyse', '每日分析数据加载成功', {
|
||||
logger.debug('LimitAnalyse', '每日分析数据加载成功(静态文件)', {
|
||||
date,
|
||||
totalStocks: data.data?.total_stocks || 0
|
||||
totalStocks: data.data?.total_stocks || 0,
|
||||
fromCache: data.from_cache
|
||||
});
|
||||
// ❌ 移除数据加载成功 toast(非关键操作)
|
||||
}
|
||||
} catch (error) {
|
||||
logger.error('LimitAnalyse', 'fetchDailyAnalysis', error, { date });
|
||||
// ❌ 移除数据加载失败 toast(非关键操作)
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const fetchWordCloudData = async (date) => {
|
||||
try {
|
||||
const response = await fetch(`${API_URL}/api/v1/analysis/wordcloud/${date}`);
|
||||
const data = await response.json();
|
||||
if (data.success) {
|
||||
setWordCloudData(data.data);
|
||||
logger.debug('LimitAnalyse', '词云数据加载成功', {
|
||||
date,
|
||||
count: data.data?.length || 0
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
logger.error('LimitAnalyse', 'fetchWordCloudData', error, { date });
|
||||
}
|
||||
};
|
||||
|
||||
// 格式化日期
|
||||
const formatDateStr = (date) => {
|
||||
const year = date.getFullYear();
|
||||
@@ -193,27 +172,22 @@ export default function LimitAnalyse() {
|
||||
fetchDailyAnalysis(dateString);
|
||||
};
|
||||
|
||||
// 处理搜索
|
||||
// 处理搜索(使用静态数据关键词搜索)
|
||||
const handleSearch = async (searchParams) => {
|
||||
// 🎯 追踪搜索开始
|
||||
trackSearchInitiated(
|
||||
searchParams.query,
|
||||
searchParams.type || 'all',
|
||||
searchParams.mode || 'hybrid'
|
||||
searchParams.mode || 'keyword' // 静态模式只支持关键词搜索
|
||||
);
|
||||
|
||||
setLoading(true);
|
||||
try {
|
||||
const response = await fetch(`${API_URL}/api/v1/stocks/search/hybrid`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(searchParams),
|
||||
});
|
||||
const data = await response.json();
|
||||
const data = await ztStaticService.searchStocks(searchParams);
|
||||
if (data.success) {
|
||||
setSearchResults(data.data);
|
||||
setIsSearchOpen(true);
|
||||
logger.info('LimitAnalyse', '搜索完成', {
|
||||
logger.info('LimitAnalyse', '搜索完成(静态文件)', {
|
||||
resultCount: data.data?.total || 0,
|
||||
searchParams
|
||||
});
|
||||
@@ -223,6 +197,13 @@ export default function LimitAnalyse() {
|
||||
status: 'success',
|
||||
duration: 3000,
|
||||
});
|
||||
} else {
|
||||
toast({
|
||||
title: '搜索失败',
|
||||
description: data.error || '请稍后重试',
|
||||
status: 'error',
|
||||
duration: 3000,
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
logger.error('LimitAnalyse', 'handleSearch', error, { searchParams });
|
||||
|
||||
Reference in New Issue
Block a user