- 使用 useReducer 整合 7 个折叠状态为统一的 sectionState - 提取自选股逻辑到 useWatchlist Hook,移除 70 行重复代码 - 扩展 useWatchlist 添加 handleAddToWatchlist、isInWatchlist 方法 - 清理未使用的导入(HStack、useColorModeValue) - 移除调试 console.log 日志 - RelatedStocksSection 改用 isInWatchlist 函数替代 watchlistSet 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
183 lines
6.1 KiB
JavaScript
183 lines
6.1 KiB
JavaScript
// src/views/Community/components/DynamicNewsDetail/RelatedStocksSection.js
|
||
// 相关股票列表区组件(纯内容,不含标题)
|
||
|
||
import React, { useState, useEffect, useMemo } from 'react';
|
||
import { VStack } from '@chakra-ui/react';
|
||
import dayjs from 'dayjs';
|
||
import StockListItem from './StockListItem';
|
||
import { fetchBatchKlineData, klineDataCache, getCacheKey } from '../StockDetailPanel/utils/klineDataCache';
|
||
import { logger } from '../../../../utils/logger';
|
||
|
||
/**
|
||
* 相关股票列表区组件(纯内容部分)
|
||
* 只负责渲染详细的股票列表,精简模式由外层 CollapsibleSection 的 simpleContent 提供
|
||
* @param {Object} props
|
||
* @param {Array<Object>} props.stocks - 股票数组
|
||
* @param {Object} props.quotes - 股票行情字典 { [stockCode]: { change: number } }
|
||
* @param {string} props.eventTime - 事件时间
|
||
* @param {Function} props.isInWatchlist - 检查股票是否在自选股中的函数
|
||
* @param {Function} props.onWatchlistToggle - 切换自选股回调
|
||
*/
|
||
const RelatedStocksSection = ({
|
||
stocks,
|
||
quotes = {},
|
||
eventTime = null,
|
||
isInWatchlist = () => false,
|
||
onWatchlistToggle
|
||
}) => {
|
||
// 分时图数据状态:{ [stockCode]: data[] }
|
||
const [timelineDataMap, setTimelineDataMap] = useState({});
|
||
const [timelineLoading, setTimelineLoading] = useState(false);
|
||
|
||
// 日K线数据状态:{ [stockCode]: data[] }
|
||
const [dailyDataMap, setDailyDataMap] = useState({});
|
||
const [dailyLoading, setDailyLoading] = useState(false);
|
||
|
||
// 稳定的事件时间
|
||
const stableEventTime = useMemo(() => {
|
||
return eventTime ? dayjs(eventTime).format('YYYY-MM-DD HH:mm') : '';
|
||
}, [eventTime]);
|
||
|
||
// 稳定的股票列表 key
|
||
const stocksKey = useMemo(() => {
|
||
if (!stocks || stocks.length === 0) return '';
|
||
return stocks.map(s => s.stock_code).sort().join(',');
|
||
}, [stocks]);
|
||
|
||
// 计算分时图是否应该显示 loading
|
||
const shouldShowTimelineLoading = useMemo(() => {
|
||
if (!stocks || stocks.length === 0) return false;
|
||
const currentDataKeys = Object.keys(timelineDataMap).sort().join(',');
|
||
if (stocksKey !== currentDataKeys) {
|
||
return true;
|
||
}
|
||
return timelineLoading;
|
||
}, [stocks, stocksKey, timelineDataMap, timelineLoading]);
|
||
|
||
// 计算日K线是否应该显示 loading
|
||
const shouldShowDailyLoading = useMemo(() => {
|
||
if (!stocks || stocks.length === 0) return false;
|
||
const currentDataKeys = Object.keys(dailyDataMap).sort().join(',');
|
||
if (stocksKey !== currentDataKeys) {
|
||
return true;
|
||
}
|
||
return dailyLoading;
|
||
}, [stocks, stocksKey, dailyDataMap, dailyLoading]);
|
||
|
||
// 批量加载分时图数据
|
||
useEffect(() => {
|
||
if (!stocks || stocks.length === 0) {
|
||
setTimelineDataMap({});
|
||
setTimelineLoading(false);
|
||
return;
|
||
}
|
||
|
||
setTimelineLoading(true);
|
||
const stockCodes = stocks.map(s => s.stock_code);
|
||
|
||
// 检查缓存
|
||
const cachedData = {};
|
||
stockCodes.forEach(code => {
|
||
const cacheKey = getCacheKey(code, stableEventTime, 'timeline');
|
||
const cached = klineDataCache.get(cacheKey);
|
||
if (cached !== undefined) {
|
||
cachedData[code] = cached;
|
||
}
|
||
});
|
||
|
||
if (Object.keys(cachedData).length === stockCodes.length) {
|
||
setTimelineDataMap(cachedData);
|
||
setTimelineLoading(false);
|
||
logger.debug('RelatedStocksSection', '分时图数据全部来自缓存', { stockCount: stockCodes.length });
|
||
return;
|
||
}
|
||
|
||
logger.debug('RelatedStocksSection', '批量加载分时图数据', {
|
||
totalCount: stockCodes.length,
|
||
eventTime: stableEventTime
|
||
});
|
||
|
||
fetchBatchKlineData(stockCodes, stableEventTime, 'timeline')
|
||
.then((batchData) => {
|
||
setTimelineDataMap({ ...cachedData, ...batchData });
|
||
setTimelineLoading(false);
|
||
})
|
||
.catch((error) => {
|
||
logger.error('RelatedStocksSection', '批量加载分时图数据失败', error);
|
||
setTimelineDataMap(cachedData);
|
||
setTimelineLoading(false);
|
||
});
|
||
}, [stocksKey, stableEventTime]);
|
||
|
||
// 批量加载日K线数据
|
||
useEffect(() => {
|
||
if (!stocks || stocks.length === 0) {
|
||
setDailyDataMap({});
|
||
setDailyLoading(false);
|
||
return;
|
||
}
|
||
|
||
setDailyLoading(true);
|
||
const stockCodes = stocks.map(s => s.stock_code);
|
||
|
||
// 检查缓存
|
||
const cachedData = {};
|
||
stockCodes.forEach(code => {
|
||
const cacheKey = getCacheKey(code, stableEventTime, 'daily');
|
||
const cached = klineDataCache.get(cacheKey);
|
||
if (cached !== undefined) {
|
||
cachedData[code] = cached;
|
||
}
|
||
});
|
||
|
||
if (Object.keys(cachedData).length === stockCodes.length) {
|
||
setDailyDataMap(cachedData);
|
||
setDailyLoading(false);
|
||
logger.debug('RelatedStocksSection', '日K线数据全部来自缓存', { stockCount: stockCodes.length });
|
||
return;
|
||
}
|
||
|
||
logger.debug('RelatedStocksSection', '批量加载日K线数据', {
|
||
totalCount: stockCodes.length,
|
||
eventTime: stableEventTime
|
||
});
|
||
|
||
fetchBatchKlineData(stockCodes, stableEventTime, 'daily')
|
||
.then((batchData) => {
|
||
setDailyDataMap({ ...cachedData, ...batchData });
|
||
setDailyLoading(false);
|
||
})
|
||
.catch((error) => {
|
||
logger.error('RelatedStocksSection', '批量加载日K线数据失败', error);
|
||
setDailyDataMap(cachedData);
|
||
setDailyLoading(false);
|
||
});
|
||
}, [stocksKey, stableEventTime]);
|
||
|
||
// 如果没有股票数据,不渲染
|
||
if (!stocks || stocks.length === 0) {
|
||
return null;
|
||
}
|
||
|
||
return (
|
||
<VStack align="stretch" spacing={3}>
|
||
{stocks.map((stock, index) => (
|
||
<StockListItem
|
||
key={index}
|
||
stock={stock}
|
||
quote={quotes[stock.stock_code]}
|
||
eventTime={eventTime}
|
||
isInWatchlist={isInWatchlist(stock.stock_code)}
|
||
onWatchlistToggle={onWatchlistToggle}
|
||
timelineData={timelineDataMap[stock.stock_code]}
|
||
timelineLoading={shouldShowTimelineLoading && !timelineDataMap[stock.stock_code]}
|
||
dailyData={dailyDataMap[stock.stock_code]}
|
||
dailyLoading={shouldShowDailyLoading && !dailyDataMap[stock.stock_code]}
|
||
/>
|
||
))}
|
||
</VStack>
|
||
);
|
||
};
|
||
|
||
export default RelatedStocksSection;
|