Browse Source

feat:添加数据导出功能

master
qinjian 2 weeks ago
parent
commit
ca160814fd
  1. 2
      client/src/sections/wuyuanbiaoba/components/CameraView.jsx
  2. 344
      client/src/sections/wuyuanbiaoba/components/RealtimeCharts.jsx
  3. 167
      client/src/sections/wuyuanbiaoba/container/index.jsx
  4. 932
      package-lock.json
  5. 1
      package.json

2
client/src/sections/wuyuanbiaoba/components/CameraView.jsx

@ -575,7 +575,7 @@ const CameraView = ({
//
if ((!rectangles || rectangles.length === 0) && !currentDrawingRect) {
console.log("redrawAllRectangles: 没有矩形框数据且没有预览矩形");
// console.log("redrawAllRectangles: ");
return;
}

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

@ -1,5 +1,12 @@
import React, { useMemo, useEffect, useRef, useState, useCallback } from "react";
import { Typography, Badge, Button, Space } from "antd";
import React, {
useMemo,
useEffect,
useRef,
useState,
useCallback,
} from "react";
import { Typography, Badge, Button, Space,InputNumber } from "antd";
import { DownloadOutlined } from "@ant-design/icons";
import {
Chart as ChartJS,
CategoryScale,
@ -25,7 +32,7 @@ ChartJS.register(
const { Title } = Typography;
const RealtimeCharts = ({ tableData, lastUpdateTime }) => {
const RealtimeCharts = ({ tableData, lastUpdateTime, onDataExport,exportCount = 200, setExportCount }) => {
const xChartRef = useRef(null);
const yChartRef = useRef(null);
@ -119,7 +126,7 @@ const RealtimeCharts = ({ tableData, lastUpdateTime }) => {
// deviceId desc
const deviceDescMap = useMemo(() => {
const map = {};
tableData.forEach(item => {
tableData.forEach((item) => {
if (item.deviceId && item.desc) {
map[item.deviceId] = item.desc;
}
@ -130,18 +137,18 @@ const RealtimeCharts = ({ tableData, lastUpdateTime }) => {
useEffect(() => {
if (deviceIds.length > 0) {
const initialVisible = {};
deviceIds.forEach(deviceId => {
deviceIds.forEach((deviceId) => {
initialVisible[deviceId] = true; //
});
setVisibleTargets(prev => ({ ...initialVisible, ...prev }));
setVisibleTargets((prev) => ({ ...initialVisible, ...prev }));
}
}, [deviceIds.join(',')]);
}, [deviceIds.join(",")]);
//
const toggleTargetVisibility = useCallback((deviceId) => {
setVisibleTargets(prev => ({
setVisibleTargets((prev) => ({
...prev,
[deviceId]: !prev[deviceId]
[deviceId]: !prev[deviceId],
}));
//
@ -149,8 +156,12 @@ const RealtimeCharts = ({ tableData, lastUpdateTime }) => {
const yChart = yChartRef.current;
if (xChart && yChart) {
const xDatasetIndex = xChart.data.datasets.findIndex(dataset => dataset.label === deviceId);
const yDatasetIndex = yChart.data.datasets.findIndex(dataset => dataset.label === deviceId);
const xDatasetIndex = xChart.data.datasets.findIndex(
(dataset) => dataset.label === deviceId
);
const yDatasetIndex = yChart.data.datasets.findIndex(
(dataset) => dataset.label === deviceId
);
if (xDatasetIndex !== -1 && yDatasetIndex !== -1) {
const isVisible = xChart.isDatasetVisible(xDatasetIndex);
@ -167,29 +178,32 @@ const RealtimeCharts = ({ tableData, lastUpdateTime }) => {
}, []);
// /
const toggleAllTargets = useCallback((visible) => {
const newVisibleTargets = {};
deviceIds.forEach(deviceId => {
newVisibleTargets[deviceId] = visible;
});
setVisibleTargets(newVisibleTargets);
const toggleAllTargets = useCallback(
(visible) => {
const newVisibleTargets = {};
deviceIds.forEach((deviceId) => {
newVisibleTargets[deviceId] = visible;
});
setVisibleTargets(newVisibleTargets);
//
const xChart = xChartRef.current;
const yChart = yChartRef.current;
//
const xChart = xChartRef.current;
const yChart = yChartRef.current;
if (xChart && yChart) {
xChart.data.datasets.forEach((_, index) => {
xChart.setDatasetVisibility(index, visible);
});
yChart.data.datasets.forEach((_, index) => {
yChart.setDatasetVisibility(index, visible);
});
if (xChart && yChart) {
xChart.data.datasets.forEach((_, index) => {
xChart.setDatasetVisibility(index, visible);
});
yChart.data.datasets.forEach((_, index) => {
yChart.setDatasetVisibility(index, visible);
});
xChart.update();
yChart.update();
}
}, [deviceIds]);
xChart.update();
yChart.update();
}
},
[deviceIds]
);
// X
const xChartData = useMemo(() => {
@ -229,7 +243,10 @@ const RealtimeCharts = ({ tableData, lastUpdateTime }) => {
return { labels, datasets };
} catch (error) {
console.error(`${new Date().toLocaleString()} - 准备X轴图表数据出错:`, error);
console.error(
`${new Date().toLocaleString()} - 准备X轴图表数据出错:`,
error
);
return { labels: [], datasets: [] };
}
}, [tableData, visibleTargets]);
@ -272,130 +289,142 @@ const RealtimeCharts = ({ tableData, lastUpdateTime }) => {
return { labels, datasets };
} catch (error) {
console.error(`${new Date().toLocaleString()} - 准备Y轴图表数据出错:`, error);
console.error(
`${new Date().toLocaleString()} - 准备Y轴图表数据出错:`,
error
);
return { labels: [], datasets: [] };
}
}, [tableData, visibleTargets]);
// Chart.js - 访 deviceDescMap
const chartOptions = useMemo(() => ({
responsive: true,
maintainAspectRatio: false,
interaction: {
mode: "index",
intersect: false,
},
plugins: {
legend: {
display: false, // 使
const chartOptions = useMemo(
() => ({
responsive: true,
maintainAspectRatio: false,
interaction: {
mode: "index",
intersect: false,
},
tooltip: {
filter: function (tooltipItem) {
return tooltipItem.parsed.y !== null;
plugins: {
legend: {
display: false, // 使
},
callbacks: {
title: function(context) {
return context[0].label; //
tooltip: {
filter: function (tooltipItem) {
return tooltipItem.parsed.y !== null;
},
label: function(context) {
const deviceId = context.dataset.label;
const deviceDesc = deviceDescMap[deviceId] || deviceId;
const value = context.parsed.y;
return `${deviceDesc}: ${value}`;
}
}
},
},
scales: {
x: {
display: true,
title: {
display: true,
text: "时间",
},
ticks: {
maxRotation: 45,
minRotation: 0,
font: {
size: 10,
callbacks: {
title: function (context) {
return context[0].label; //
},
label: function (context) {
const deviceId = context.dataset.label;
const deviceDesc = deviceDescMap[deviceId] || deviceId;
const value = context.parsed.y;
return `${deviceDesc}: ${value}`;
},
},
},
},
y: {
display: true,
title: {
scales: {
x: {
display: true,
font: {
size: 12,
title: {
display: true,
text: "时间",
},
ticks: {
maxRotation: 45,
minRotation: 0,
font: {
size: 10,
},
},
},
ticks: {
font: {
size: 10,
y: {
display: true,
title: {
display: true,
font: {
size: 12,
},
},
ticks: {
font: {
size: 10,
},
},
},
},
},
elements: {
point: {
radius: 3,
hoverRadius: 5,
},
line: {
tension: 0,
elements: {
point: {
radius: 3,
hoverRadius: 5,
},
line: {
tension: 0,
},
},
},
}), [deviceDescMap]);
}),
[deviceDescMap]
);
// X
const xChartOptions = useMemo(() => ({
...chartOptions,
plugins: {
...chartOptions.plugins,
title: {
display: true,
text: "X轴位移数据",
font: {
size: 16,
const xChartOptions = useMemo(
() => ({
...chartOptions,
plugins: {
...chartOptions.plugins,
title: {
display: true,
text: "X轴位移数据",
font: {
size: 16,
},
},
},
},
scales: {
...chartOptions.scales,
y: {
...chartOptions.scales.y,
title: {
...chartOptions.scales.y.title,
text: "X值(mm)",
scales: {
...chartOptions.scales,
y: {
...chartOptions.scales.y,
title: {
...chartOptions.scales.y.title,
text: "X值(mm)",
},
},
},
},
}), [chartOptions]);
}),
[chartOptions]
);
// Y
const yChartOptions = useMemo(() => ({
...chartOptions,
plugins: {
...chartOptions.plugins,
title: {
display: true,
text: "Y轴位移数据",
font: {
size: 16,
const yChartOptions = useMemo(
() => ({
...chartOptions,
plugins: {
...chartOptions.plugins,
title: {
display: true,
text: "Y轴位移数据",
font: {
size: 16,
},
},
},
},
scales: {
...chartOptions.scales,
y: {
...chartOptions.scales.y,
title: {
...chartOptions.scales.y.title,
text: "Y值(mm)",
scales: {
...chartOptions.scales,
y: {
...chartOptions.scales.y,
title: {
...chartOptions.scales.y.title,
text: "Y值(mm)",
},
},
},
},
}), [chartOptions]);
}),
[chartOptions]
);
//
if (!tableData || tableData.length === 0) {
@ -442,7 +471,14 @@ const RealtimeCharts = ({ tableData, lastUpdateTime }) => {
flexDirection: "column",
}}
>
<Title level={4} style={{ marginBottom: "16px" }}>
<Title
level={4}
style={{
marginBottom: "16px",
display: "flex",
alignItems: "center",
}}
>
实时数据图
<Badge
status="processing"
@ -450,15 +486,50 @@ const RealtimeCharts = ({ tableData, lastUpdateTime }) => {
style={{ marginLeft: "16px", fontSize: "12px" }}
/>
<Badge
status="default"
status="success"
text={`图表数据采样频率:1Hz`}
style={{ marginLeft: "16px", fontSize: "12px" }}
/>
<Badge
status="default"
text={`导出数据条数配置`}
style={{ marginLeft: "16px", fontSize: "12px" }}
/>
<InputNumber
min={1}
max={1000}
value={exportCount}
onChange={setExportCount}
style={{ marginLeft: 16, width: 80 }}
step={1}
/>
<Button
style={{ marginLeft: 8 }}
icon={<DownloadOutlined />}
onClick={() => onDataExport(exportCount)}
>
导出
</Button>
</Title>
{/* 统一的图例控制器 */}
<div style={{ marginBottom: "16px", padding: "12px", backgroundColor: "#fafafa", borderRadius: "6px" }}>
<div style={{ marginBottom: "8px", fontSize: "14px", fontWeight: "500" }}>显示控制</div>
<div
style={{
marginBottom: "16px",
padding: "12px",
backgroundColor: "#fafafa",
borderRadius: "6px",
}}
>
<div
style={{
marginBottom: "8px",
fontSize: "14px",
fontWeight: "500",
}}
>
显示控制
</div>
<Space wrap>
<Button
size="small"
@ -467,10 +538,7 @@ const RealtimeCharts = ({ tableData, lastUpdateTime }) => {
>
全部显示
</Button>
<Button
size="small"
onClick={() => toggleAllTargets(false)}
>
<Button size="small" onClick={() => toggleAllTargets(false)}>
全部隐藏
</Button>
{deviceIds.map((deviceId, index) => (
@ -480,8 +548,12 @@ const RealtimeCharts = ({ tableData, lastUpdateTime }) => {
type={visibleTargets[deviceId] ? "primary" : "default"}
style={{
borderColor: getDeviceColor(index),
color: visibleTargets[deviceId] ? "#fff" : getDeviceColor(index),
backgroundColor: visibleTargets[deviceId] ? getDeviceColor(index) : "#fff"
color: visibleTargets[deviceId]
? "#fff"
: getDeviceColor(index),
backgroundColor: visibleTargets[deviceId]
? getDeviceColor(index)
: "#fff",
}}
onClick={() => toggleTargetVisibility(deviceId)}
>

167
client/src/sections/wuyuanbiaoba/container/index.jsx

@ -1,6 +1,7 @@
import React, { useState, useEffect } from "react";
import { Tabs, Typography } from "antd";
import { EyeOutlined } from "@ant-design/icons";
import ExcelJS from "exceljs";
import {
CameraView,
TargetList,
@ -17,7 +18,7 @@ import {
} from "../actions/websocket.jsx";
import { useTemplateStorage } from "../hooks/useTemplateStorage.js";
import { useTargetStorage } from "../hooks/useTargetStorage.js";
import { useRef } from "react";
const { Title } = Typography;
// 使WebSocket hook
@ -26,7 +27,29 @@ const WuyuanbiaobaContent = () => {
//
const realtimeDataSubscription = useWebSocketSubscription("dev", "data");
const realtimeBufferRef = useRef([]);
const exportCountRef = useRef(200);
//
const setExportCount = (count) => {
exportCountRef.current = count;
};
//
const pushToRealtimeBuffer = (newDataGroup) => {
// newDataGroup: { time: "2025-09-29 01:40:57.091", data: [...] }
const now = Date.now();
const group = {
...newDataGroup,
_bufferTimestamp: now,
};
//
realtimeBufferRef.current = [...realtimeBufferRef.current, group];
// 200
if (realtimeBufferRef.current.length > exportCountRef.current) {
realtimeBufferRef.current = realtimeBufferRef.current.slice(-exportCountRef.current);
}
};
const {
templates: tempListData,
loading: templatesLoading,
@ -78,7 +101,7 @@ const WuyuanbiaobaContent = () => {
desc: item.desc, // desc
xValue: item.x,
yValue: item.y,
updateTime: data.time || new Date().toLocaleString(),
updateTime: data.time,
}));
};
@ -87,7 +110,7 @@ const WuyuanbiaobaContent = () => {
//
setRealtimeData([]);
setTableData([]);
// console.log("...", import.meta.env.MODE);
// console.log("...", import.meta.env.MODE);
}, []);
//
@ -119,7 +142,7 @@ const WuyuanbiaobaContent = () => {
//
useEffect(() => {
// console.log(':', {
// console.log(':', {
// hasData: !!realtimeDataSubscription.latest,
// dataCount: realtimeDataSubscription.data?.length || 0,
// latestTimestamp: realtimeDataSubscription.latest?.timestamp,
@ -156,46 +179,143 @@ const WuyuanbiaobaContent = () => {
})),
];
// 2525
// 25
return updatedData.slice(-75); // 3 * 25
});
// 使
setRealtimeData(newRealtimeData.map((point) => ({
...point,
key: `realtime_${point.key}`,
updateTime: new Date(currentTime).toLocaleString(),
})));
setRealtimeData(
newRealtimeData.map((point) => ({
...point,
key: `realtime_${point.key}`,
updateTime: new Date(currentTime).toLocaleString(),
}))
);
}
//
pushToRealtimeBuffer(realtimeDataSubscription.latest.values);
setLastUpdateTime(new Date());
}
}
}, [realtimeDataSubscription.latest, lastSampleTime]);
}, [realtimeDataSubscription.latest]);
const dataExport = async () => {
const dataToExport = realtimeBufferRef.current;
if (dataToExport.length === 0) {
console.warn("没有数据可导出");
return;
}
// console.log(':', dataToExport);
// desc
const descSet = [];
dataToExport.forEach((group) => {
if (Array.isArray(group.data)) {
group.data.forEach((point) => {
if (point && !descSet.includes(point.desc)) {
descSet.push(point.desc);
}
});
}
});
//
const columns = [{ header: "采集时间", key: "time", width: 24 }];
descSet.forEach((desc) => {
columns.push(
{ header: `标靶${desc}X`, key: `x_${desc}`, width: 12 },
{ header: `标靶${desc}Y`, key: `y_${desc}`, width: 12 }
);
});
// 簿
const workbook = new ExcelJS.Workbook();
const worksheet = workbook.addWorksheet("两分钟内数据");
worksheet.columns = columns;
//
dataToExport.forEach((group) => {
const row = { time: group.time };
// desc
descSet.forEach((desc) => {
row[`x_${desc}`] = "";
row[`y_${desc}`] = "";
});
//
group.data.forEach((point) => {
if (point && descSet.includes(point.desc)) {
row[`x_${point.desc}`] = point.x;
row[`y_${point.desc}`] = point.y;
}
});
worksheet.addRow(row);
});
//
worksheet.getRow(1).font = { bold: true, color: { argb: "FF1F497D" } };
worksheet.getRow(1).alignment = {
vertical: "middle",
horizontal: "center",
};
//
worksheet.eachRow((row, rowNumber) => {
row.eachCell((cell) => {
cell.border = {
top: { style: "thin" },
left: { style: "thin" },
bottom: { style: "thin" },
right: { style: "thin" },
};
cell.alignment = { vertical: "middle", horizontal: "center" };
});
});
//
const now = new Date();
const pad = (n) => n.toString().padStart(2, "0");
const localFileName = `${now.getFullYear()}-${pad(
now.getMonth() + 1
)}-${pad(now.getDate())}-${pad(now.getHours())}-${pad(
now.getMinutes()
)}-${pad(now.getSeconds())}.xlsx`;
const buffer = await workbook.xlsx.writeBuffer();
const blob = new Blob([buffer], {
type: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
});
const url = window.URL.createObjectURL(blob);
const a = document.createElement("a");
a.href = url;
a.download = localFileName;
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
window.URL.revokeObjectURL(url);
};
//
const handleEditTarget = (target) => {
setCurrentEditTarget(target);
setTargetDetailModalMode("edit");
setTargetDetailModalVisible(true);
// console.log(":", target);
// console.log(":", target);
};
//
const handleSelectTarget = (target) => {
setSelectedTargetId(target.id);
// console.log(":", target);
// console.log(":", target);
};
//
const handleClearSelection = () => {
setSelectedTargetId(null);
// console.log("");
// console.log("");
};
//
const handleRectangleClick = (targetData) => {
// console.log(":", targetData);
// console.log(":", targetData);
setCurrentEditTarget(targetData);
setTargetDetailModalMode("edit");
setTargetDetailModalVisible(true);
@ -204,7 +324,7 @@ const WuyuanbiaobaContent = () => {
//
const handleTemplateSelect = (templateKey) => {
setSelectedTemplate(templateKey);
// console.log(":", templateKey);
// console.log(":", templateKey);
};
//
@ -223,7 +343,7 @@ const WuyuanbiaobaContent = () => {
//
const handleTemplateModalOk = (templateInfo) => {
// console.log(templateInfo, "templateInfo");
// console.log(templateInfo, "templateInfo");
let success = false;
if (templateModalMode === "add") {
@ -285,7 +405,7 @@ const WuyuanbiaobaContent = () => {
//
const handleDeleteTarget = (targetKey) => {
// console.log(":", targetKey);
// console.log(":", targetKey);
const success = deleteTarget(targetKey);
if (success) {
@ -308,7 +428,7 @@ const WuyuanbiaobaContent = () => {
};
// -
const onClickClearAll = () => {
// console.log("");
// console.log("");
sendMessage(
JSON.stringify({
_from: "setup",
@ -431,6 +551,9 @@ const WuyuanbiaobaContent = () => {
<RealtimeCharts
tableData={tableData}
lastUpdateTime={lastUpdateTime}
onDataExport={dataExport}
exportCount={exportCountRef.current}
setExportCount={setExportCount}
/>
{/* Table 区域 - 使用采样数据显示 */}

932
package-lock.json

File diff suppressed because it is too large

1
package.json

@ -27,6 +27,7 @@
"dependencies": {
"@koa/multer": "^3.0.2",
"@peace/react_client": "^5.0.1",
"exceljs": "^4.4.0",
"fs-extra": "^11.2.0",
"ws": "^8.18.3"
}

Loading…
Cancel
Save