refactor: 重构 Community 目录,将公共组件迁移到 src/components/

- 迁移 klineDataCache.js 到 src/utils/stock/(被 StockChart 使用)
- 迁移 InvestmentCalendar 到 src/components/InvestmentCalendar/(被 Navbar、Dashboard 使用)
- 迁移 DynamicNewsDetail 到 src/components/EventDetailPanel/(被 EventDetail 使用)
- 更新所有相关导入路径,使用路径别名
- 保持 Community 目录其余结构不变

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

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
zdl
2025-12-08 12:09:24 +08:00
parent b4ddccfb92
commit ee33f7ffd7
30 changed files with 56 additions and 43 deletions

View File

@@ -0,0 +1,288 @@
// src/views/Community/components/StockDetailPanel/utils/klineDataCache.js
import dayjs from 'dayjs';
import { stockService } from '../../../../../services/eventService';
import { logger } from '../../../../../utils/logger';
// ================= 全局缓存和请求管理 =================
export const klineDataCache = new Map(); // 缓存K线数据: key = `${code}|${date}|${chartType}` -> data
export const pendingRequests = new Map(); // 正在进行的请求: key = `${code}|${date}|${chartType}` -> Promise
export const lastRequestTime = new Map(); // 最后请求时间: key = `${code}|${date}|${chartType}` -> timestamp
export const batchPendingRequests = new Map(); // 批量请求的 Promise: key = `${eventTime}|${chartType}` -> Promise
// 请求间隔限制(毫秒)
const REQUEST_INTERVAL = 30000; // 30秒内不重复请求同一只股票的数据
/**
* 获取缓存键
* @param {string} stockCode - 股票代码
* @param {string} eventTime - 事件时间
* @param {string} chartType - 图表类型timeline/daily
* @returns {string} 缓存键
*/
export const getCacheKey = (stockCode, eventTime, chartType = 'timeline') => {
const date = eventTime ? dayjs(eventTime).format('YYYY-MM-DD') : dayjs().format('YYYY-MM-DD');
return `${stockCode}|${date}|${chartType}`;
};
/**
* 检查是否需要刷新数据
* @param {string} cacheKey - 缓存键
* @returns {boolean} 是否需要刷新
*/
export const shouldRefreshData = (cacheKey) => {
const lastTime = lastRequestTime.get(cacheKey);
if (!lastTime) return true;
const now = Date.now();
const elapsed = now - lastTime;
// 如果是今天的数据且交易时间内,允许更频繁的更新
const today = dayjs().format('YYYY-MM-DD');
const isToday = cacheKey.includes(today);
const currentHour = new Date().getHours();
const isTradingHours = currentHour >= 9 && currentHour < 16;
if (isToday && isTradingHours) {
return elapsed > REQUEST_INTERVAL;
}
// 历史数据不需要频繁更新
return elapsed > 3600000; // 1小时
};
/**
* 获取K线数据带缓存和防重复请求
* @param {string} stockCode - 股票代码
* @param {string} eventTime - 事件时间
* @param {string} chartType - 图表类型timeline/daily
* @returns {Promise<Array>} K线数据
*/
export const fetchKlineData = async (stockCode, eventTime, chartType = 'timeline') => {
const cacheKey = getCacheKey(stockCode, eventTime, chartType);
// 1. 检查缓存
if (klineDataCache.has(cacheKey)) {
// 检查是否需要刷新
if (!shouldRefreshData(cacheKey)) {
logger.debug('klineDataCache', '使用缓存数据', { cacheKey });
return klineDataCache.get(cacheKey);
}
}
// 2. 检查是否有正在进行的请求
if (pendingRequests.has(cacheKey)) {
logger.debug('klineDataCache', '等待进行中的请求', { cacheKey });
return pendingRequests.get(cacheKey);
}
// 3. 发起新请求
logger.debug('klineDataCache', '发起新K线数据请求', { cacheKey, chartType });
const normalizedEventTime = eventTime ? dayjs(eventTime).format('YYYY-MM-DD HH:mm') : undefined;
const requestPromise = stockService
.getKlineData(stockCode, chartType, normalizedEventTime)
.then((res) => {
const data = Array.isArray(res?.data) ? res.data : [];
// 更新缓存
klineDataCache.set(cacheKey, data);
lastRequestTime.set(cacheKey, Date.now());
// 清除pending状态
pendingRequests.delete(cacheKey);
logger.debug('klineDataCache', 'K线数据请求完成并缓存', {
cacheKey,
chartType,
dataPoints: data.length
});
return data;
})
.catch((error) => {
logger.error('klineDataCache', 'fetchKlineData', error, { stockCode, chartType, cacheKey });
// 清除pending状态
pendingRequests.delete(cacheKey);
// 如果有旧缓存,返回旧数据
if (klineDataCache.has(cacheKey)) {
return klineDataCache.get(cacheKey);
}
return [];
});
// 保存pending请求
pendingRequests.set(cacheKey, requestPromise);
return requestPromise;
};
/**
* 清除指定股票的缓存
* @param {string} stockCode - 股票代码
* @param {string} eventTime - 事件时间(可选)
*/
export const clearCache = (stockCode, eventTime = null) => {
if (eventTime) {
const cacheKey = getCacheKey(stockCode, eventTime);
klineDataCache.delete(cacheKey);
lastRequestTime.delete(cacheKey);
pendingRequests.delete(cacheKey);
logger.debug('klineDataCache', '清除缓存', { cacheKey });
} else {
// 清除该股票的所有缓存
const prefix = `${stockCode}|`;
for (const key of klineDataCache.keys()) {
if (key.startsWith(prefix)) {
klineDataCache.delete(key);
lastRequestTime.delete(key);
pendingRequests.delete(key);
}
}
logger.debug('klineDataCache', '清除股票所有缓存', { stockCode });
}
};
/**
* 清除所有缓存
*/
export const clearAllCache = () => {
klineDataCache.clear();
lastRequestTime.clear();
pendingRequests.clear();
logger.debug('klineDataCache', '清除所有缓存');
};
/**
* 获取缓存统计信息
* @returns {Object} 缓存统计
*/
export const getCacheStats = () => {
return {
totalCached: klineDataCache.size,
pendingRequests: pendingRequests.size,
cacheKeys: Array.from(klineDataCache.keys())
};
};
/**
* 批量获取多只股票的K线数据一次API请求
* @param {string[]} stockCodes - 股票代码数组
* @param {string} eventTime - 事件时间
* @param {string} chartType - 图表类型timeline/daily
* @returns {Promise<Object>} 股票代码到K线数据的映射 { [stockCode]: data[] }
*/
export const fetchBatchKlineData = async (stockCodes, eventTime, chartType = 'timeline') => {
if (!stockCodes || stockCodes.length === 0) {
return {};
}
const normalizedEventTime = eventTime ? dayjs(eventTime).format('YYYY-MM-DD HH:mm') : undefined;
const batchKey = `${normalizedEventTime || 'today'}|${chartType}`;
// 过滤出未缓存的股票
const uncachedCodes = stockCodes.filter(code => {
const cacheKey = getCacheKey(code, eventTime, chartType);
return !klineDataCache.has(cacheKey) || shouldRefreshData(cacheKey);
});
logger.debug('klineDataCache', '批量请求分析', {
totalCodes: stockCodes.length,
uncachedCodes: uncachedCodes.length,
cachedCodes: stockCodes.length - uncachedCodes.length
});
// 如果所有股票都有缓存,直接返回缓存数据
if (uncachedCodes.length === 0) {
const result = {};
stockCodes.forEach(code => {
const cacheKey = getCacheKey(code, eventTime, chartType);
result[code] = klineDataCache.get(cacheKey) || [];
});
logger.debug('klineDataCache', '所有股票数据来自缓存', { stockCount: stockCodes.length });
return result;
}
// 检查是否有正在进行的批量请求
if (batchPendingRequests.has(batchKey)) {
logger.debug('klineDataCache', '等待进行中的批量请求', { batchKey });
return batchPendingRequests.get(batchKey);
}
// 发起批量请求
logger.debug('klineDataCache', '发起批量K线数据请求', {
batchKey,
stockCount: uncachedCodes.length,
chartType
});
const requestPromise = stockService
.getBatchKlineData(uncachedCodes, chartType, normalizedEventTime)
.then((response) => {
const batchData = response?.data || {};
const now = Date.now();
// 将批量数据存入缓存
Object.entries(batchData).forEach(([code, stockData]) => {
const data = Array.isArray(stockData?.data) ? stockData.data : [];
const cacheKey = getCacheKey(code, eventTime, chartType);
klineDataCache.set(cacheKey, data);
lastRequestTime.set(cacheKey, now);
});
// 对于请求中没有返回数据的股票,设置空数组
uncachedCodes.forEach(code => {
if (!batchData[code]) {
const cacheKey = getCacheKey(code, eventTime, chartType);
if (!klineDataCache.has(cacheKey)) {
klineDataCache.set(cacheKey, []);
lastRequestTime.set(cacheKey, now);
}
}
});
// 清除批量请求状态
batchPendingRequests.delete(batchKey);
logger.debug('klineDataCache', '批量K线数据请求完成', {
batchKey,
stockCount: Object.keys(batchData).length
});
// 返回所有请求股票的数据(包括之前缓存的)
const result = {};
stockCodes.forEach(code => {
const cacheKey = getCacheKey(code, eventTime, chartType);
result[code] = klineDataCache.get(cacheKey) || [];
});
return result;
})
.catch((error) => {
logger.error('klineDataCache', 'fetchBatchKlineData', error, {
stockCount: uncachedCodes.length,
chartType
});
// 清除批量请求状态
batchPendingRequests.delete(batchKey);
// 返回已缓存的数据
const result = {};
stockCodes.forEach(code => {
const cacheKey = getCacheKey(code, eventTime, chartType);
result[code] = klineDataCache.get(cacheKey) || [];
});
return result;
});
// 保存批量请求
batchPendingRequests.set(batchKey, requestPromise);
return requestPromise;
};
/**
* 预加载多只股票的K线数据后台执行不阻塞UI
* @param {string[]} stockCodes - 股票代码数组
* @param {string} eventTime - 事件时间
* @param {string} chartType - 图表类型timeline/daily
*/
export const preloadBatchKlineData = (stockCodes, eventTime, chartType = 'timeline') => {
// 异步执行不返回Promise不阻塞调用方
fetchBatchKlineData(stockCodes, eventTime, chartType).catch(() => {
// 静默处理错误,预加载失败不影响用户体验
});
};