liujiangyong 1 week ago
parent
commit
94acda8938
  1. 164
      client/src/sections/wuyuanbiaoba/components/RealtimeCharts.jsx
  2. 4
      client/src/sections/wuyuanbiaoba/components/RealtimeDataTable.jsx

164
client/src/sections/wuyuanbiaoba/components/RealtimeCharts.jsx

@ -32,6 +32,152 @@ ChartJS.register(
const { Title } = Typography;
// 使用 HTML Tooltip,避免 Chart.js Canvas 将多标靶内容裁剪掉。
const getOrCreateExternalTooltip = (chart) => {
const canvas = chart.canvas;
let tooltipEl = canvas._externalTooltipEl;
if (!tooltipEl) {
tooltipEl = document.createElement("div");
tooltipEl.className = "chartjs-external-tooltip";
Object.assign(tooltipEl.style, {
position: "fixed",
maxWidth: "260px",
maxHeight: "min(280px, calc(100vh - 20px))",
overflowY: "auto",
padding: "10px 12px",
background: "#ffffff",
border: "1px solid #d9d9d9",
borderRadius: "4px",
boxShadow: "0 3px 12px rgba(0, 0, 0, 0.18)",
color: "#333333",
fontSize: "12px",
lineHeight: "1.6",
whiteSpace: "nowrap",
pointerEvents: "none",
opacity: "0",
zIndex: "20",
transform: "translate(-50%, -100%)",
transition: "opacity 0.08s ease",
});
tooltipEl.addEventListener("mouseenter", () => {
// Tooltip 已经离开图表后,不允许仅靠再次移入旧位置重新显示。
if (tooltipEl.dataset.active !== "true") {
return;
}
if (tooltipEl._hideTimer) {
clearTimeout(tooltipEl._hideTimer);
tooltipEl._hideTimer = null;
}
tooltipEl.dataset.hovered = "true";
tooltipEl.style.opacity = "1";
tooltipEl.style.pointerEvents = "auto";
});
tooltipEl.addEventListener("mouseleave", () => {
tooltipEl.dataset.hovered = "false";
tooltipEl._hideTimer = setTimeout(() => {
if (tooltipEl.dataset.hovered !== "true") {
tooltipEl.style.opacity = "0";
tooltipEl.style.pointerEvents = "none";
tooltipEl.dataset.active = "false";
}
}, 150);
});
document.body.appendChild(tooltipEl);
canvas._externalTooltipEl = tooltipEl;
}
return tooltipEl;
};
const externalTooltipHandler = ({ chart, tooltip }) => {
const tooltipEl = getOrCreateExternalTooltip(chart);
if (tooltip.opacity === 0) {
if (tooltipEl.dataset.hovered !== "true") {
if (tooltipEl._hideTimer) {
clearTimeout(tooltipEl._hideTimer);
}
tooltipEl._hideTimer = setTimeout(() => {
if (tooltipEl.dataset.hovered !== "true") {
tooltipEl.style.opacity = "0";
tooltipEl.style.pointerEvents = "none";
tooltipEl.dataset.active = "false";
}
}, 150);
}
return;
}
if (tooltipEl._hideTimer) {
clearTimeout(tooltipEl._hideTimer);
tooltipEl._hideTimer = null;
}
tooltipEl.replaceChildren();
(tooltip.title || []).forEach((title) => {
const titleEl = document.createElement("div");
titleEl.style.fontWeight = "600";
titleEl.style.marginBottom = "4px";
titleEl.textContent = title;
tooltipEl.appendChild(titleEl);
});
(tooltip.body || []).forEach((bodyItem) => {
(bodyItem.lines || []).forEach((line, index) => {
const lineEl = document.createElement("div");
lineEl.style.display = "flex";
lineEl.style.alignItems = "center";
const color = tooltip.labelColors?.[tooltip.body.indexOf(bodyItem)];
if (color) {
const markerEl = document.createElement("span");
Object.assign(markerEl.style, {
width: "8px",
height: "8px",
marginRight: "6px",
borderRadius: "50%",
// 与折线保持一致,优先使用不透明的 borderColor。
background: color.borderColor || color.backgroundColor,
flex: "0 0 auto",
});
lineEl.appendChild(markerEl);
}
const textEl = document.createElement("span");
textEl.textContent = line;
lineEl.appendChild(textEl);
tooltipEl.appendChild(lineEl);
});
});
const canvasRect = chart.canvas.getBoundingClientRect();
const positionX = canvasRect.left + tooltip.caretX;
const positionY = canvasRect.top + tooltip.caretY;
const tooltipHeight = tooltipEl.offsetHeight;
const tooltipWidth = tooltipEl.offsetWidth;
const spaceAbove = positionY;
const spaceBelow = window.innerHeight - positionY;
const canPlaceAbove =
spaceAbove >= tooltipHeight + 8 || spaceBelow < tooltipHeight + 8;
const clampedX = Math.max(
tooltipWidth / 2 + 8,
Math.min(positionX, window.innerWidth - tooltipWidth / 2 - 8)
);
// 使用 fixed 定位,避免 Tooltip 超出图表后撑高整个页面。
// 上方空间不足时放到数据点下方,避免鼠标向上移动时离开可交互区域。
tooltipEl.style.transform = canPlaceAbove
? "translate(-50%, calc(-100% - 8px))"
: "translate(-50%, 8px)";
tooltipEl.style.opacity = "1";
tooltipEl.style.pointerEvents = "auto";
tooltipEl.dataset.active = "true";
tooltipEl.style.left = `${clampedX}px`;
tooltipEl.style.top = `${positionY}px`;
};
const RealtimeCharts = ({ tableData, lastUpdateTime, onDataExport,exportCount = 200, setExportCount }) => {
const xChartRef = useRef(null);
const yChartRef = useRef(null);
@ -39,6 +185,18 @@ const RealtimeCharts = ({ tableData, lastUpdateTime, onDataExport,exportCount =
// 添加标靶显示状态控制
const [visibleTargets, setVisibleTargets] = useState({});
// 外部 Tooltip 挂载在 body 上,组件卸载时主动清理,避免遗留 DOM。
useEffect(() => {
return () => {
[xChartRef.current, yChartRef.current].forEach((chart) => {
const tooltipEl = chart?.canvas?._externalTooltipEl;
if (tooltipEl?.parentNode) {
tooltipEl.parentNode.removeChild(tooltipEl);
}
});
};
}, []);
// 添加数据验证
if (!Array.isArray(tableData)) {
return <div>数据格式错误</div>;
@ -314,6 +472,8 @@ const RealtimeCharts = ({ tableData, lastUpdateTime, onDataExport,exportCount =
display: false, // 隐藏默认图例,使用自定义控制器
},
tooltip: {
enabled: false,
external: externalTooltipHandler,
filter: function (tooltipItem) {
return tooltipItem.parsed.y !== null;
},
@ -584,6 +744,8 @@ const RealtimeCharts = ({ tableData, lastUpdateTime, onDataExport,exportCount =
border: "1px solid #e8e8e8",
minHeight: "280px",
padding: "16px",
position: "relative",
overflow: "visible",
}}
>
<Line
@ -602,6 +764,8 @@ const RealtimeCharts = ({ tableData, lastUpdateTime, onDataExport,exportCount =
border: "1px solid #e8e8e8",
minHeight: "280px",
padding: "16px",
position: "relative",
overflow: "visible",
}}
>
<Line

4
client/src/sections/wuyuanbiaoba/components/RealtimeDataTable.jsx

@ -49,8 +49,8 @@ const RealtimeDataTable = ({ realtimeData }) => {
</Title>
<div
style={{
height: "calc(600px - 60px - 32px)",
overflow: "hidden",
height: "calc(800px - 60px - 32px)",
overflow: "auto",
}}
>
<Table

Loading…
Cancel
Save