Merge remote-tracking branch 'origin/main'
This commit is contained in:
@@ -1,6 +1,6 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { useMemo } from 'react';
|
import { useMemo, useRef, useEffect } from 'react';
|
||||||
import {
|
import {
|
||||||
Chart as ChartJS,
|
Chart as ChartJS,
|
||||||
CategoryScale,
|
CategoryScale,
|
||||||
@@ -27,6 +27,9 @@ ChartJS.register(
|
|||||||
* referenceNav: 参考净值(最新单位净值),用于计算涨跌幅;未传则用当日第一个估值作为参考。
|
* referenceNav: 参考净值(最新单位净值),用于计算涨跌幅;未传则用当日第一个估值作为参考。
|
||||||
*/
|
*/
|
||||||
export default function FundIntradayChart({ series = [], referenceNav }) {
|
export default function FundIntradayChart({ series = [], referenceNav }) {
|
||||||
|
const chartRef = useRef(null);
|
||||||
|
const hoverTimeoutRef = useRef(null);
|
||||||
|
|
||||||
const chartData = useMemo(() => {
|
const chartData = useMemo(() => {
|
||||||
if (!series.length) return { labels: [], datasets: [] };
|
if (!series.length) return { labels: [], datasets: [] };
|
||||||
const labels = series.map((d) => d.time);
|
const labels = series.map((d) => d.time);
|
||||||
@@ -91,7 +94,7 @@ export default function FundIntradayChart({ series = [], referenceNav }) {
|
|||||||
},
|
},
|
||||||
y: {
|
y: {
|
||||||
display: true,
|
display: true,
|
||||||
position: 'right',
|
position: 'left',
|
||||||
grid: { color: '#1f2937', drawBorder: false },
|
grid: { color: '#1f2937', drawBorder: false },
|
||||||
ticks: {
|
ticks: {
|
||||||
color: '#9ca3af',
|
color: '#9ca3af',
|
||||||
@@ -100,11 +103,54 @@ export default function FundIntradayChart({ series = [], referenceNav }) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
onHover: (event, chartElement) => {
|
onHover: (event, chartElement, chart) => {
|
||||||
event.native.target.style.cursor = chartElement[0] ? 'crosshair' : 'default';
|
const target = event?.native?.target;
|
||||||
|
const currentChart = chart || chartRef.current;
|
||||||
|
if (!currentChart) return;
|
||||||
|
|
||||||
|
const tooltipActive = currentChart.tooltip?._active ?? [];
|
||||||
|
const activeElements = currentChart.getActiveElements
|
||||||
|
? currentChart.getActiveElements()
|
||||||
|
: [];
|
||||||
|
const hasActive =
|
||||||
|
(chartElement && chartElement.length > 0) ||
|
||||||
|
(tooltipActive && tooltipActive.length > 0) ||
|
||||||
|
(activeElements && activeElements.length > 0);
|
||||||
|
|
||||||
|
if (target) {
|
||||||
|
target.style.cursor = hasActive ? 'crosshair' : 'default';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (hoverTimeoutRef.current) {
|
||||||
|
clearTimeout(hoverTimeoutRef.current);
|
||||||
|
hoverTimeoutRef.current = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (hasActive) {
|
||||||
|
hoverTimeoutRef.current = setTimeout(() => {
|
||||||
|
const c = chartRef.current || currentChart;
|
||||||
|
if (!c) return;
|
||||||
|
c.setActiveElements([]);
|
||||||
|
if (c.tooltip) {
|
||||||
|
c.tooltip.setActiveElements([], { x: 0, y: 0 });
|
||||||
|
}
|
||||||
|
c.update();
|
||||||
|
if (target) {
|
||||||
|
target.style.cursor = 'default';
|
||||||
|
}
|
||||||
|
}, 2000);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}), []);
|
}), []);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
return () => {
|
||||||
|
if (hoverTimeoutRef.current) {
|
||||||
|
clearTimeout(hoverTimeoutRef.current);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
const plugins = useMemo(() => [{
|
const plugins = useMemo(() => [{
|
||||||
id: 'crosshair',
|
id: 'crosshair',
|
||||||
afterDraw: (chart) => {
|
afterDraw: (chart) => {
|
||||||
@@ -157,9 +203,9 @@ export default function FundIntradayChart({ series = [], referenceNav }) {
|
|||||||
const valueStr = typeof val === 'number' ? `${val >= 0 ? '+' : ''}${val.toFixed(2)}%` : String(val);
|
const valueStr = typeof val === 'number' ? `${val >= 0 ? '+' : ''}${val.toFixed(2)}%` : String(val);
|
||||||
const vw = ctx.measureText(valueStr).width + 8;
|
const vw = ctx.measureText(valueStr).width + 8;
|
||||||
ctx.fillStyle = prim;
|
ctx.fillStyle = prim;
|
||||||
ctx.fillRect(rightX - vw, y - 8, vw, 16);
|
ctx.fillRect(leftX, y - 8, vw, 16);
|
||||||
ctx.fillStyle = bgText;
|
ctx.fillStyle = bgText;
|
||||||
ctx.fillText(valueStr, rightX - vw / 2, y);
|
ctx.fillText(valueStr, leftX + vw / 2, y);
|
||||||
}
|
}
|
||||||
ctx.restore();
|
ctx.restore();
|
||||||
}
|
}
|
||||||
@@ -191,7 +237,7 @@ export default function FundIntradayChart({ series = [], referenceNav }) {
|
|||||||
{displayDate && <span style={{ fontSize: 11 }}>估值日期 {displayDate}</span>}
|
{displayDate && <span style={{ fontSize: 11 }}>估值日期 {displayDate}</span>}
|
||||||
</div>
|
</div>
|
||||||
<div style={{ position: 'relative', height: 100, width: '100%' }}>
|
<div style={{ position: 'relative', height: 100, width: '100%' }}>
|
||||||
<Line data={chartData} options={options} plugins={plugins} />
|
<Line ref={chartRef} data={chartData} options={options} plugins={plugins} />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -35,6 +35,7 @@ export default function FundTrendChart({ code, isExpanded, onToggleExpand, trans
|
|||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
const [error, setError] = useState(null);
|
const [error, setError] = useState(null);
|
||||||
const chartRef = useRef(null);
|
const chartRef = useRef(null);
|
||||||
|
const hoverTimeoutRef = useRef(null);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
// If collapsed, don't fetch data unless we have no data yet
|
// If collapsed, don't fetch data unless we have no data yet
|
||||||
@@ -198,7 +199,7 @@ export default function FundTrendChart({ code, isExpanded, onToggleExpand, trans
|
|||||||
},
|
},
|
||||||
y: {
|
y: {
|
||||||
display: true,
|
display: true,
|
||||||
position: 'right',
|
position: 'left',
|
||||||
grid: {
|
grid: {
|
||||||
color: '#1f2937',
|
color: '#1f2937',
|
||||||
drawBorder: false,
|
drawBorder: false,
|
||||||
@@ -217,14 +218,61 @@ export default function FundTrendChart({ code, isExpanded, onToggleExpand, trans
|
|||||||
mode: 'index',
|
mode: 'index',
|
||||||
intersect: false,
|
intersect: false,
|
||||||
},
|
},
|
||||||
onHover: (event, chartElement) => {
|
onHover: (event, chartElement, chart) => {
|
||||||
event.native.target.style.cursor = chartElement[0] ? 'crosshair' : 'default';
|
const target = event?.native?.target;
|
||||||
|
const currentChart = chart || chartRef.current;
|
||||||
|
if (!currentChart) return;
|
||||||
|
|
||||||
|
const tooltipActive = currentChart.tooltip?._active ?? [];
|
||||||
|
const activeElements = currentChart.getActiveElements
|
||||||
|
? currentChart.getActiveElements()
|
||||||
|
: [];
|
||||||
|
const hasActive =
|
||||||
|
(chartElement && chartElement.length > 0) ||
|
||||||
|
(tooltipActive && tooltipActive.length > 0) ||
|
||||||
|
(activeElements && activeElements.length > 0);
|
||||||
|
|
||||||
|
if (target) {
|
||||||
|
target.style.cursor = hasActive ? 'crosshair' : 'default';
|
||||||
|
}
|
||||||
|
|
||||||
|
// 仅用于桌面端 hover 改变光标,不在这里做 2 秒清除,避免移动端 hover 事件不稳定
|
||||||
|
},
|
||||||
|
onClick: () => {}
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
return () => {
|
||||||
|
if (hoverTimeoutRef.current) {
|
||||||
|
clearTimeout(hoverTimeoutRef.current);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const plugins = useMemo(() => [{
|
const plugins = useMemo(() => [{
|
||||||
id: 'crosshair',
|
id: 'crosshair',
|
||||||
|
afterEvent: (chart, args) => {
|
||||||
|
const { event, replay } = args || {};
|
||||||
|
if (!event || replay) return; // 忽略动画重放
|
||||||
|
|
||||||
|
const type = event.type;
|
||||||
|
if (type === 'mousemove' || type === 'click') {
|
||||||
|
if (hoverTimeoutRef.current) {
|
||||||
|
clearTimeout(hoverTimeoutRef.current);
|
||||||
|
hoverTimeoutRef.current = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
hoverTimeoutRef.current = setTimeout(() => {
|
||||||
|
if (!chart) return;
|
||||||
|
chart.setActiveElements([]);
|
||||||
|
if (chart.tooltip) {
|
||||||
|
chart.tooltip.setActiveElements([], { x: 0, y: 0 });
|
||||||
|
}
|
||||||
|
chart.update();
|
||||||
|
}, 2000);
|
||||||
|
}
|
||||||
|
},
|
||||||
afterDraw: (chart) => {
|
afterDraw: (chart) => {
|
||||||
const ctx = chart.ctx;
|
const ctx = chart.ctx;
|
||||||
const datasets = chart.data.datasets;
|
const datasets = chart.data.datasets;
|
||||||
@@ -360,10 +408,10 @@ export default function FundTrendChart({ code, isExpanded, onToggleExpand, trans
|
|||||||
const valueStr = (typeof value === 'number' ? value.toFixed(2) : value) + '%';
|
const valueStr = (typeof value === 'number' ? value.toFixed(2) : value) + '%';
|
||||||
const valWidth = ctx.measureText(valueStr).width + 8;
|
const valWidth = ctx.measureText(valueStr).width + 8;
|
||||||
ctx.fillStyle = primaryColor;
|
ctx.fillStyle = primaryColor;
|
||||||
ctx.fillRect(rightX - valWidth, y - 8, valWidth, 16);
|
ctx.fillRect(leftX, y - 8, valWidth, 16);
|
||||||
ctx.fillStyle = '#0f172a'; // --background
|
ctx.fillStyle = '#0f172a'; // --background
|
||||||
ctx.textAlign = 'center';
|
ctx.textAlign = 'center';
|
||||||
ctx.fillText(valueStr, rightX - valWidth / 2, y);
|
ctx.fillText(valueStr, leftX + valWidth / 2, y);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user