From 48716f4876817254017d1e722629c0a50aba45f6 Mon Sep 17 00:00:00 2001 From: JIANG Date: Wed, 19 Nov 2025 17:46:16 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BD=BF=E7=94=A8=20echart=20=E7=BB=98?= =?UTF-8?q?=E5=88=B6=E5=9B=BE=E8=A1=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- package-lock.json | 53 +++ package.json | 2 + src/components/olmap/SCADADataPanel.tsx | 417 ++++++++---------------- 3 files changed, 200 insertions(+), 272 deletions(-) diff --git a/package-lock.json b/package-lock.json index 39d83b0..0799e81 100644 --- a/package-lock.json +++ b/package-lock.json @@ -29,6 +29,8 @@ "clsx": "^2.1.1", "date-fns": "^4.1.0", "deck.gl": "^9.1.14", + "echarts": "^6.0.0", + "echarts-for-react": "^3.0.5", "js-cookie": "^3.0.5", "next": "^15.2.4", "next-auth": "^4.24.5", @@ -10691,6 +10693,36 @@ "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", "license": "MIT" }, + "node_modules/echarts": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/echarts/-/echarts-6.0.0.tgz", + "integrity": "sha512-Tte/grDQRiETQP4xz3iZWSvoHrkCQtwqd6hs+mifXcjrCuo2iKWbajFObuLJVBlDIJlOzgQPd1hsaKt/3+OMkQ==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "2.3.0", + "zrender": "6.0.0" + } + }, + "node_modules/echarts-for-react": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/echarts-for-react/-/echarts-for-react-3.0.5.tgz", + "integrity": "sha512-YpEI5Ty7O/2nvCfQ7ybNa+S90DwE8KYZWacGvJW4luUqywP7qStQ+pxDlYOmr4jGDu10mhEkiAuMKcUlT4W5vg==", + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.3", + "size-sensor": "^1.0.1" + }, + "peerDependencies": { + "echarts": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0", + "react": "^15.0.0 || >=16.0.0" + } + }, + "node_modules/echarts/node_modules/tslib": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.3.0.tgz", + "integrity": "sha512-N82ooyxVNm6h1riLCoyS9e3fuJ3AMG2zIZs2Gd1ATcSFjSA23Q0fzjjZeh0jbJvWVDZ0cJT8yaNNaaXHzueNjg==", + "license": "0BSD" + }, "node_modules/ee-first": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", @@ -17374,6 +17406,12 @@ "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", "license": "ISC" }, + "node_modules/size-sensor": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/size-sensor/-/size-sensor-1.0.2.tgz", + "integrity": "sha512-2NCmWxY7A9pYKGXNBfteo4hy14gWu47rg5692peVMst6lQLPKrVjhY+UTEsPI5ceFRJSl3gVgMYaUi/hKuaiKw==", + "license": "ISC" + }, "node_modules/skin-tone": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/skin-tone/-/skin-tone-2.0.0.tgz", @@ -19071,6 +19109,21 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/zrender": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/zrender/-/zrender-6.0.0.tgz", + "integrity": "sha512-41dFXEEXuJpNecuUQq6JlbybmnHaqqpGlbH1yxnA5V9MMP4SbohSVZsJIwz+zdjQXSSlR1Vc34EgH1zxyTDvhg==", + "license": "BSD-3-Clause", + "dependencies": { + "tslib": "2.3.0" + } + }, + "node_modules/zrender/node_modules/tslib": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.3.0.tgz", + "integrity": "sha512-N82ooyxVNm6h1riLCoyS9e3fuJ3AMG2zIZs2Gd1ATcSFjSA23Q0fzjjZeh0jbJvWVDZ0cJT8yaNNaaXHzueNjg==", + "license": "0BSD" + }, "node_modules/zstd-codec": { "version": "0.1.5", "resolved": "https://registry.npmjs.org/zstd-codec/-/zstd-codec-0.1.5.tgz", diff --git a/package.json b/package.json index e36f9e6..1b68913 100644 --- a/package.json +++ b/package.json @@ -34,6 +34,8 @@ "clsx": "^2.1.1", "date-fns": "^4.1.0", "deck.gl": "^9.1.14", + "echarts": "^6.0.0", + "echarts-for-react": "^3.0.5", "js-cookie": "^3.0.5", "next": "^15.2.4", "next-auth": "^4.24.5", diff --git a/src/components/olmap/SCADADataPanel.tsx b/src/components/olmap/SCADADataPanel.tsx index ea6fc06..511015a 100644 --- a/src/components/olmap/SCADADataPanel.tsx +++ b/src/components/olmap/SCADADataPanel.tsx @@ -14,7 +14,6 @@ import { Tooltip, Typography, Drawer, - Slider, } from "@mui/material"; import { Refresh, @@ -25,7 +24,8 @@ import { ChevronRight, } from "@mui/icons-material"; import { DataGrid, GridColDef } from "@mui/x-data-grid"; -import { LineChart } from "@mui/x-charts"; +import ReactECharts from "echarts-for-react"; +import * as echarts from "echarts"; import "dayjs/locale/zh-cn"; // 引入中文包 import dayjs, { Dayjs } from "dayjs"; import utc from "dayjs/plugin/utc"; @@ -347,10 +347,7 @@ const SCADADataPanel: React.FC = ({ const [isCleaning, setIsCleaning] = useState(false); const [selectedSource, setSelectedSource] = useState< "raw" | "clean" | "sim" | "all" - >("all"); - - // 滑块状态:用于图表缩放 - const [zoomRange, setZoomRange] = useState<[number, number]>([0, 100]); + >(() => (deviceIds.length === 1 ? "all" : "clean")); // 获取 SCADA 设备信息,生成 deviceLabels useEffect(() => { @@ -396,21 +393,6 @@ const SCADADataPanel: React.FC = ({ [timeSeries, deviceIds, fractionDigits, showCleaning] ); - // 根据滑块范围过滤数据集 - const filteredDataset = useMemo(() => { - if (dataset.length === 0) return dataset; - - const startIndex = Math.floor((zoomRange[0] / 100) * dataset.length); - const endIndex = Math.ceil((zoomRange[1] / 100) * dataset.length); - - return dataset.slice(startIndex, endIndex); - }, [dataset, zoomRange]); - - // 重置滑块范围当数据变化时 - useEffect(() => { - setZoomRange([0, 100]); - }, [timeSeries]); - const handleFetch = useCallback( async (reason: string) => { if (!hasDevices) { @@ -530,10 +512,7 @@ const SCADADataPanel: React.FC = ({ if (deviceIds.length > 1 && selectedSource === "all") { setSelectedSource("clean"); } - // else if (deviceIds.length === 1 && selectedSource !== "all") { - // setSelectedSource("all"); - // } - }, [deviceIds.length]); + }, [deviceIds.length, selectedSource]); const columns: GridColDef[] = useMemo(() => { const base: GridColDef[] = [ @@ -680,17 +659,119 @@ const SCADADataPanel: React.FC = ({ "#3f51b5", // 靛蓝色 ]; - // 获取当前显示范围的时间边界 - const getTimeRangeLabel = () => { - if (filteredDataset.length === 0) return ""; - const firstTime = filteredDataset[0].time; - const lastTime = filteredDataset[filteredDataset.length - 1].time; - if (firstTime instanceof Date && lastTime instanceof Date) { - return `${dayjs(firstTime).format("MM-DD HH:mm")} ~ ${dayjs( - lastTime - ).format("MM-DD HH:mm")}`; + const xData = dataset.map((item) => item.label); + + const getSeries = () => { + if (showCleaning) { + if (selectedSource === "all") { + return deviceIds.flatMap((id, index) => [ + { + name: `${deviceLabels?.[id] ?? id} (原始)`, + type: "line", + symbol: "none", + sampling: "lttb", + itemStyle: { color: colors[index % colors.length] }, + data: dataset.map((item) => item[`${id}_raw`]), + }, + { + name: `${deviceLabels?.[id] ?? id} (清洗)`, + type: "line", + symbol: "none", + sampling: "lttb", + itemStyle: { color: colors[(index + 3) % colors.length] }, + data: dataset.map((item) => item[`${id}_clean`]), + }, + { + name: `${deviceLabels?.[id] ?? id} (模拟)`, + type: "line", + symbol: "none", + sampling: "lttb", + itemStyle: { color: colors[(index + 6) % colors.length] }, + data: dataset.map((item) => item[`${id}_sim`]), + }, + ]); + } else { + return deviceIds.map((id, index) => ({ + name: deviceLabels?.[id] ?? id, + type: "line", + symbol: "none", + sampling: "lttb", + itemStyle: { color: colors[index % colors.length] }, + data: dataset.map((item) => item[`${id}_${selectedSource}`]), + })); + } + } else { + return deviceIds.map((id, index) => ({ + name: deviceLabels?.[id] ?? id, + type: "line", + symbol: "none", + sampling: "lttb", + itemStyle: { color: colors[index % colors.length] }, + data: dataset.map((item) => item[id]), + areaStyle: { + color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [ + { + offset: 0, + color: colors[index % colors.length], + }, + { + offset: 1, + color: "rgba(255, 255, 255, 0)", + }, + ]), + opacity: 0.3, + }, + })); } - return ""; + }; + + const option = { + tooltip: { + trigger: "axis", + confine: true, + position: function (pt: any[]) { + return [pt[0], "10%"]; + }, + }, + legend: { + top: "top", + }, + grid: { + left: "5%", + right: "5%", + bottom: "11%", + containLabel: true, + }, + toolbox: { + feature: { + dataZoom: { + yAxisIndex: "none", + }, + restore: {}, + saveAsImage: {}, + }, + }, + xAxis: { + type: "category", + boundaryGap: false, + data: xData, + }, + yAxis: { + type: "value", + scale: true, + }, + dataZoom: [ + { + type: "inside", + start: 0, + end: 100, + }, + { + start: 0, + end: 100, + }, + ], + series: getSeries(), }; return ( @@ -700,237 +781,15 @@ const SCADADataPanel: React.FC = ({ height: "100%", display: "flex", flexDirection: "column", + overflow: "hidden", }} > - - - value instanceof Date - ? dayjs(value).format("MM-DD HH:mm") - : String(value), - tickLabelStyle: { - angle: -45, - textAnchor: "end", - fontSize: 11, - fill: "#666", - }, - }, - ]} - yAxis={[ - { - label: "压力/流量值", - labelStyle: { - fontSize: 13, - fill: "#333", - fontWeight: 500, - }, - tickLabelStyle: { - fontSize: 11, - fill: "#666", - }, - }, - ]} - series={(() => { - if (showCleaning) { - if (selectedSource === "all") { - // 全部模式:显示所有设备的三种数据 - return deviceIds.flatMap((id, index) => [ - { - dataKey: `${id}_raw`, - label: `${deviceLabels?.[id] ?? id} (原始)`, - showMark: dataset.length < 50, - curve: "catmullRom", - color: colors[index % colors.length], - valueFormatter: (value: number | null) => - value !== null ? value.toFixed(fractionDigits) : "--", - area: false, - stack: undefined, - }, - { - dataKey: `${id}_clean`, - label: `${deviceLabels?.[id] ?? id} (清洗)`, - showMark: dataset.length < 50, - curve: "catmullRom", - color: colors[(index + 3) % colors.length], - valueFormatter: (value: number | null) => - value !== null ? value.toFixed(fractionDigits) : "--", - area: false, - stack: undefined, - }, - { - dataKey: `${id}_sim`, - label: `${deviceLabels?.[id] ?? id} (模拟)`, - showMark: dataset.length < 50, - curve: "catmullRom", - color: colors[(index + 6) % colors.length], - valueFormatter: (value: number | null) => - value !== null ? value.toFixed(fractionDigits) : "--", - area: false, - stack: undefined, - }, - ]); - } else { - // 单一数据源模式:只显示选中的数据源 - return deviceIds.map((id, index) => ({ - dataKey: `${id}_${selectedSource}`, - label: deviceLabels?.[id] ?? id, - showMark: dataset.length < 50, - curve: "catmullRom", - color: colors[index % colors.length], - valueFormatter: (value: number | null) => - value !== null ? value.toFixed(fractionDigits) : "--", - area: false, - stack: undefined, - })); - } - } else { - return deviceIds.map((id, index) => ({ - dataKey: id, - label: deviceLabels?.[id] ?? id, - showMark: dataset.length < 50, - curve: "catmullRom", - color: colors[index % colors.length], - valueFormatter: (value: number | null) => - value !== null ? value.toFixed(fractionDigits) : "--", - area: false, - stack: undefined, - })); - } - })()} - grid={{ vertical: true, horizontal: true }} - sx={{ - "& .MuiLineElement-root": { - strokeWidth: 2.5, - strokeLinecap: "round", - strokeLinejoin: "round", - }, - "& .MuiMarkElement-root": { - scale: "0.8", - strokeWidth: 2, - }, - "& .MuiChartsAxis-line": { - stroke: "#e0e0e0", - strokeWidth: 1, - }, - "& .MuiChartsAxis-tick": { - stroke: "#e0e0e0", - strokeWidth: 1, - }, - "& .MuiChartsGrid-line": { - stroke: "#d0d0d0", - strokeWidth: 0.8, - strokeDasharray: "4 4", - }, - }} - slotProps={{ - legend: { - direction: "row", - position: { horizontal: "middle", vertical: "bottom" }, - padding: { bottom: 2, left: 0, right: 0 }, - itemMarkWidth: 16, - itemMarkHeight: 3, - markGap: 8, - itemGap: 16, - labelStyle: { - fontSize: 12, - fill: "#333", - fontWeight: 500, - }, - }, - loadingOverlay: { - style: { backgroundColor: "rgba(255, 255, 255, 0.7)" }, - }, - }} - tooltip={{ - trigger: "axis", - }} - /> - - - {/* 时间范围滑块 */} - - - - 时间范围 - - - setZoomRange(newValue as [number, number]) - } - valueLabelDisplay="auto" - valueLabelFormat={(value) => { - const index = Math.floor((value / 100) * dataset.length); - if (dataset[index] && dataset[index].time instanceof Date) { - return dayjs(dataset[index].time).format("MM-DD HH:mm"); - } - return `${value}%`; - }} - marks={[ - { - value: 0, - label: - dataset.length > 0 && dataset[0].time instanceof Date - ? dayjs(dataset[0].time).format("MM-DD HH:mm") - : "起始", - }, - { - value: 100, - label: - dataset.length > 0 && - dataset[dataset.length - 1].time instanceof Date - ? dayjs(dataset[dataset.length - 1].time).format( - "MM-DD HH:mm" - ) - : "结束", - }, - ]} - sx={{ - flex: 1, - "& .MuiSlider-thumb": { - width: 16, - height: 16, - }, - "& .MuiSlider-markLabel": { - fontSize: "0.7rem", - color: "text.secondary", - }, - }} - /> - - - {getTimeRangeLabel() && ( - - 当前显示: {getTimeRangeLabel()} (共 {filteredDataset.length}{" "} - 个数据点) - - )} - + ); }; @@ -1087,11 +946,18 @@ const SCADADataPanel: React.FC = ({ - value && dayjs.isDayjs(value) && setFrom(value) - } + onChange={(value) => { + if (value && dayjs.isDayjs(value) && value.isValid()) { + setFrom(value); + } + }} onAccept={(value) => { - if (value && dayjs.isDayjs(value) && hasDevices) { + if ( + value && + dayjs.isDayjs(value) && + value.isValid() && + hasDevices + ) { handleFetch("date-change"); } }} @@ -1103,11 +969,18 @@ const SCADADataPanel: React.FC = ({ - value && dayjs.isDayjs(value) && setTo(value) - } + onChange={(value) => { + if (value && dayjs.isDayjs(value) && value.isValid()) { + setTo(value); + } + }} onAccept={(value) => { - if (value && dayjs.isDayjs(value) && hasDevices) { + if ( + value && + dayjs.isDayjs(value) && + value.isValid() && + hasDevices + ) { handleFetch("date-change"); } }}