feat: add LiveTemperatureThresholdChart component with SSE-driven data synchronization and automated polling fallback

This commit is contained in:
2569718930@qq.com
2026-05-26 23:28:43 +08:00
parent b6194b4756
commit b3496bed2d
2 changed files with 29 additions and 12 deletions
@@ -295,31 +295,33 @@ export function LiveTemperatureThresholdChart({
[data, series, viewMode],
);
const visibleRange = zoomRange ?? autoWindowRange;
const visibleRangeKey = visibleRange ? `${visibleRange[0]}:${visibleRange[1]}` : "full";
const zoomedData = useMemo(() => {
if (!visibleRange || data.length === 0) return data;
const [start, end] = visibleRange;
return data.slice(start, end + 1);
}, [data, visibleRange]);
}, [data, visibleRangeKey]);
useEffect(() => {
const nextTargetResolution = useMemo(() => {
if (visibleRange && data.length > 0) {
const zoomedData = data.slice(visibleRange[0], visibleRange[1] + 1);
if (zoomedData.length > 0) {
const startTs = zoomedData[0].ts;
const endTs = zoomedData[zoomedData.length - 1].ts;
if (endTs - startTs <= 2 * 60 * 60 * 1000) {
setTargetResolution("1m");
} else {
setTargetResolution("10m");
return "1m";
}
} else {
setTargetResolution("10m");
}
} else {
setTargetResolution("10m");
}
}, [visibleRange, data]);
return "10m";
}, [data, visibleRangeKey]);
useEffect(() => {
if (targetResolution !== nextTargetResolution) {
setTargetResolution(nextTargetResolution);
}
}, [targetResolution, nextTargetResolution]);
const tzOffset = row?.tz_offset_seconds ?? 0;
const settlementObs = useMemo(() => {
@@ -786,7 +788,7 @@ export function LiveTemperatureThresholdChart({
)}
{/* Chart */}
<div className="relative min-h-0 flex-1 p-2">
<div className="relative min-h-[240px] flex-1 p-2">
{/* Interactive legend */}
<div className="flex flex-wrap items-center gap-x-4 gap-y-1.5 px-3 py-1.5 text-[11px] border-b border-[#e2e8f0] bg-white">
{chartSeries.length > 1 &&
@@ -832,7 +834,13 @@ export function LiveTemperatureThresholdChart({
</label>
)}
</div>
<ResponsiveContainer width="100%" height="100%">
<ResponsiveContainer
width="100%"
height="100%"
minWidth={1}
minHeight={220}
initialDimension={{ width: 1, height: 220 }}
>
<ReComposedChart
data={zoomedData}
margin={{ top: 16, right: compact ? 20 : 44, left: 4, bottom: 8 }}
@@ -106,6 +106,15 @@ export function runTests() {
assert(chart.includes("加载图表") && chart.includes("absolute inset-2"), "temperature chart must render an in-chart loading overlay");
assert(chart.includes("viewMode"), "temperature chart must expose a view mode for DEB-peak auto view versus full-day view");
assert(chart.includes("getDebPeakWindowRange"), "temperature chart must derive its default view from the DEB peak window");
assert(chart.includes("nextTargetResolution"), "temperature chart must derive target resolution without setting state on every render");
assert(
chart.includes("targetResolution !== nextTargetResolution"),
"temperature chart must guard target-resolution state updates to prevent render/update loops",
);
assert(
chart.includes("initialDimension") && chart.includes("minHeight={"),
"temperature chart ResponsiveContainer must have stable initial/min dimensions to avoid zero-size resize loops",
);
assert(!chart.includes("3D"), "temperature chart UI must not expose a 3D/future-forecast mode");
assert(!chart.includes("build3DayChartData"), "temperature chart component must not render future prediction curves");
assert(