## 改动内容
- 替换所有 Moment.js 引用为 Day.js (29 个文件)
- 更新 Webpack 配置,调整 calendar-lib chunk
- 添加 Day.js 插件支持 (isSameOrBefore, isSameOrAfter)
- 移除 Moment.js 依赖
## 性能提升
- JavaScript 打包体积减少: ~50 KB (未压缩)
- gzip 后减少: ~15-18 KB
- 预计首屏加载时间提升: 15-20%
## 影响范围
- Dashboard 组件: 5 个文件
- Community 组件: 19 个文件
- 工具函数: tradingTimeUtils.js (添加插件)
- 其他组件: 5 个文件
## 测试状态
- ✅ 构建成功 (npm run build)
186 lines
5.8 KiB
JavaScript
186 lines
5.8 KiB
JavaScript
// src/views/Community/components/DynamicNewsDetail/MiniKLineChart.js
|
||
import React, { useState, useEffect, useMemo, useRef } from 'react';
|
||
import ReactECharts from 'echarts-for-react';
|
||
import dayjs from 'dayjs';
|
||
import {
|
||
fetchKlineData,
|
||
getCacheKey,
|
||
klineDataCache
|
||
} from '../StockDetailPanel/utils/klineDataCache';
|
||
|
||
/**
|
||
* 迷你K线图组件
|
||
* 显示股票的K线走势(蜡烛图),支持事件时间标记
|
||
*
|
||
* @param {string} stockCode - 股票代码
|
||
* @param {string} eventTime - 事件时间(可选)
|
||
* @param {Function} onClick - 点击回调(可选)
|
||
* @returns {JSX.Element}
|
||
*/
|
||
const MiniKLineChart = React.memo(function MiniKLineChart({ stockCode, eventTime, onClick }) {
|
||
const [data, setData] = useState([]);
|
||
const [loading, setLoading] = useState(false);
|
||
const mountedRef = useRef(true);
|
||
const loadedRef = useRef(false);
|
||
const dataFetchedRef = useRef(false);
|
||
|
||
// 稳定的事件时间
|
||
const stableEventTime = useMemo(() => {
|
||
return eventTime ? dayjs(eventTime).format('YYYY-MM-DD HH:mm') : '';
|
||
}, [eventTime]);
|
||
|
||
useEffect(() => {
|
||
mountedRef.current = true;
|
||
return () => {
|
||
mountedRef.current = false;
|
||
};
|
||
}, []);
|
||
|
||
useEffect(() => {
|
||
if (!stockCode) {
|
||
setData([]);
|
||
loadedRef.current = false;
|
||
dataFetchedRef.current = false;
|
||
return;
|
||
}
|
||
|
||
if (dataFetchedRef.current) {
|
||
return;
|
||
}
|
||
|
||
// 检查缓存(K线图使用 'daily' 类型)
|
||
const cacheKey = getCacheKey(stockCode, stableEventTime, 'daily');
|
||
const cachedData = klineDataCache.get(cacheKey);
|
||
|
||
if (cachedData && cachedData.length > 0) {
|
||
setData(cachedData);
|
||
loadedRef.current = true;
|
||
dataFetchedRef.current = true;
|
||
return;
|
||
}
|
||
|
||
dataFetchedRef.current = true;
|
||
setLoading(true);
|
||
|
||
// 获取日K线数据
|
||
fetchKlineData(stockCode, stableEventTime, 'daily')
|
||
.then((result) => {
|
||
if (mountedRef.current) {
|
||
setData(result);
|
||
setLoading(false);
|
||
loadedRef.current = true;
|
||
}
|
||
})
|
||
.catch(() => {
|
||
if (mountedRef.current) {
|
||
setData([]);
|
||
setLoading(false);
|
||
loadedRef.current = true;
|
||
}
|
||
});
|
||
}, [stockCode, stableEventTime]);
|
||
|
||
const chartOption = useMemo(() => {
|
||
// 提取K线数据 [open, close, low, high]
|
||
const klineData = data
|
||
.filter(item => item.open && item.close && item.low && item.high)
|
||
.map(item => [item.open, item.close, item.low, item.high]);
|
||
|
||
// 日K线使用 date 字段
|
||
const dates = data.map(item => item.date || item.time);
|
||
const hasData = klineData.length > 0;
|
||
|
||
if (!hasData) {
|
||
return {
|
||
title: {
|
||
text: loading ? '加载中...' : '无数据',
|
||
left: 'center',
|
||
top: 'middle',
|
||
textStyle: { color: '#999', fontSize: 10 }
|
||
}
|
||
};
|
||
}
|
||
|
||
// 计算事件时间标记
|
||
let eventMarkLineData = [];
|
||
if (stableEventTime && Array.isArray(dates) && dates.length > 0) {
|
||
try {
|
||
const eventDate = dayjs(stableEventTime).format('YYYY-MM-DD');
|
||
const eventIdx = dates.findIndex(d => {
|
||
const dateStr = typeof d === 'object' ? dayjs(d).format('YYYY-MM-DD') : String(d);
|
||
return dateStr.includes(eventDate);
|
||
});
|
||
|
||
if (eventIdx >= 0) {
|
||
eventMarkLineData.push({
|
||
xAxis: eventIdx,
|
||
lineStyle: { color: '#FFD700', type: 'solid', width: 1.5 },
|
||
label: { show: false }
|
||
});
|
||
}
|
||
} catch (e) {
|
||
// 忽略异常
|
||
}
|
||
}
|
||
|
||
return {
|
||
grid: { left: 2, right: 2, top: 2, bottom: 2, containLabel: false },
|
||
xAxis: {
|
||
type: 'category',
|
||
data: dates,
|
||
show: false,
|
||
boundaryGap: true
|
||
},
|
||
yAxis: {
|
||
type: 'value',
|
||
show: false,
|
||
scale: true
|
||
},
|
||
series: [{
|
||
type: 'candlestick',
|
||
data: klineData,
|
||
itemStyle: {
|
||
color: '#ef5350', // 涨(阳线)
|
||
color0: '#26a69a', // 跌(阴线)
|
||
borderColor: '#ef5350', // 涨(边框)
|
||
borderColor0: '#26a69a' // 跌(边框)
|
||
},
|
||
barWidth: '60%',
|
||
markLine: {
|
||
silent: true,
|
||
symbol: 'none',
|
||
label: { show: false },
|
||
data: eventMarkLineData
|
||
}
|
||
}],
|
||
tooltip: { show: false },
|
||
animation: false
|
||
};
|
||
}, [data, loading, stableEventTime]);
|
||
|
||
return (
|
||
<div
|
||
style={{
|
||
width: '100%',
|
||
height: '100%',
|
||
minHeight: '35px',
|
||
cursor: onClick ? 'pointer' : 'default'
|
||
}}
|
||
onClick={onClick}
|
||
>
|
||
<ReactECharts
|
||
option={chartOption}
|
||
style={{ width: '100%', height: '100%' }}
|
||
notMerge={true}
|
||
lazyUpdate={true}
|
||
/>
|
||
</div>
|
||
);
|
||
}, (prevProps, nextProps) => {
|
||
return prevProps.stockCode === nextProps.stockCode &&
|
||
prevProps.eventTime === nextProps.eventTime &&
|
||
prevProps.onClick === nextProps.onClick;
|
||
});
|
||
|
||
export default MiniKLineChart;
|