|
|
@ -1,233 +1,344 @@ |
|
|
|
import React from 'react'; |
|
|
|
import { Typography, Badge } from 'antd'; |
|
|
|
import ReactECharts from 'echarts-for-react'; |
|
|
|
import React, { useMemo, useEffect, useRef } from "react"; |
|
|
|
import { Typography, Badge } from "antd"; |
|
|
|
import { |
|
|
|
Chart as ChartJS, |
|
|
|
CategoryScale, |
|
|
|
LinearScale, |
|
|
|
PointElement, |
|
|
|
LineElement, |
|
|
|
Title as ChartTitle, |
|
|
|
Tooltip, |
|
|
|
Legend, |
|
|
|
} from "chart.js"; |
|
|
|
import { Line } from "react-chartjs-2"; |
|
|
|
|
|
|
|
// 注册Chart.js组件 |
|
|
|
ChartJS.register( |
|
|
|
CategoryScale, |
|
|
|
LinearScale, |
|
|
|
PointElement, |
|
|
|
LineElement, |
|
|
|
ChartTitle, |
|
|
|
Tooltip, |
|
|
|
Legend |
|
|
|
); |
|
|
|
|
|
|
|
const { Title } = Typography; |
|
|
|
|
|
|
|
const RealtimeCharts = ({ tableData, lastUpdateTime }) => { |
|
|
|
const xChartRef = useRef(null); |
|
|
|
const yChartRef = useRef(null); |
|
|
|
|
|
|
|
// 添加数据验证 |
|
|
|
if (!Array.isArray(tableData)) { |
|
|
|
console.warn('tableData is not an array:', tableData); |
|
|
|
return <div>数据格式错误</div>; |
|
|
|
} |
|
|
|
|
|
|
|
// 固定的设备颜色映射,确保颜色一致性 |
|
|
|
const getDeviceColor = (deviceId) => { |
|
|
|
const colorMap = { |
|
|
|
'DEV000': "#1890ff", // 蓝色 |
|
|
|
'DEV001': "#52c41a", // 绿色 |
|
|
|
'DEV002': "#faad14", // 橙色 |
|
|
|
'DEV003': "#f5222d", // 红色 |
|
|
|
'DEV004': "#722ed1", // 紫色 |
|
|
|
'DEV005': "#fa8c16", // 橙红色 |
|
|
|
'DEV006': "#13c2c2", // 青色 |
|
|
|
'DEV007': "#eb2f96", // 粉色 |
|
|
|
'DEV008': "#2f54eb", // 深蓝色 |
|
|
|
'DEV009': "#fa541c", // 火橙色 |
|
|
|
target1: "#52c41a", // 绿色 |
|
|
|
target2: "#faad14", // 橙色 |
|
|
|
target3: "#f5222d", // 红色 |
|
|
|
target4: "#722ed1", // 紫色 |
|
|
|
target5: "#fa8c16", // 橙红色 |
|
|
|
target6: "#13c2c2", // 青色 |
|
|
|
target7: "#eb2f96", // 粉色 |
|
|
|
target8: "#2f54eb", // 深蓝色 |
|
|
|
target9: "#fa541c", // 火橙色 |
|
|
|
target10: "#1890ff", // 蓝色 |
|
|
|
}; |
|
|
|
|
|
|
|
// 如果设备ID在映射中,使用固定颜色 |
|
|
|
if (colorMap[deviceId]) { |
|
|
|
return colorMap[deviceId]; |
|
|
|
} |
|
|
|
return colorMap[deviceId] || "#1890ff"; |
|
|
|
}; |
|
|
|
|
|
|
|
// 对于未预定义的设备,基于设备编号生成颜色 |
|
|
|
const deviceNumber = deviceId.replace(/\D/g, ''); // 提取数字 |
|
|
|
const colors = [ |
|
|
|
"#1890ff", "#52c41a", "#faad14", "#f5222d", "#722ed1", |
|
|
|
"#fa8c16", "#13c2c2", "#eb2f96", "#2f54eb", "#fa541c" |
|
|
|
]; |
|
|
|
return colors[parseInt(deviceNumber) % colors.length]; |
|
|
|
// 数据采样函数 - 每秒采样一次,最多保留25个数据点 |
|
|
|
const sampleData = (data) => { |
|
|
|
if (!data || data.length === 0) { |
|
|
|
return { |
|
|
|
labels: [], |
|
|
|
deviceIds: [], |
|
|
|
timeGroups: {}, |
|
|
|
sortedTimes: [] |
|
|
|
}; |
|
|
|
} |
|
|
|
|
|
|
|
try { |
|
|
|
// 按时间分组数据 |
|
|
|
const timeGroups = {}; |
|
|
|
data.forEach((item) => { |
|
|
|
if (!item || !item.updateTime) return; |
|
|
|
|
|
|
|
const timeKey = Math.floor(new Date(item.updateTime).getTime() / 1000); // 按秒分组 |
|
|
|
if (!timeGroups[timeKey]) { |
|
|
|
timeGroups[timeKey] = []; |
|
|
|
} |
|
|
|
timeGroups[timeKey].push(item); |
|
|
|
}); |
|
|
|
|
|
|
|
// 取最新的25秒数据 |
|
|
|
const sortedTimes = Object.keys(timeGroups) |
|
|
|
.sort((a, b) => Number(b) - Number(a)) |
|
|
|
.slice(0, 25) |
|
|
|
.reverse(); |
|
|
|
|
|
|
|
// 准备图表数据 |
|
|
|
const prepareChartData = () => { |
|
|
|
// 获取所有唯一的设备ID并排序,确保顺序一致 |
|
|
|
const deviceIds = [...new Set(tableData.map((item) => item.deviceId))].sort(); |
|
|
|
// 获取所有设备ID |
|
|
|
const deviceIds = [...new Set(data.map((item) => item?.deviceId).filter(Boolean))].sort(); |
|
|
|
|
|
|
|
// 获取所有时间点并排序 |
|
|
|
const allTimes = [ |
|
|
|
...new Set(tableData.map((item) => item.updateTime)), |
|
|
|
].sort((a, b) => new Date(a) - new Date(b)); |
|
|
|
const timeLabels = allTimes.map((time) => { |
|
|
|
const date = new Date(time); |
|
|
|
return date.toLocaleTimeString("zh-CN", { |
|
|
|
// 生成时间标签 |
|
|
|
const labels = sortedTimes.map((timeKey) => { |
|
|
|
return new Date(Number(timeKey) * 1000).toLocaleTimeString("zh-CN", { |
|
|
|
hour: "2-digit", |
|
|
|
minute: "2-digit", |
|
|
|
second: "2-digit", |
|
|
|
}); |
|
|
|
}); |
|
|
|
|
|
|
|
// 为每个设备准备数据 |
|
|
|
const deviceData = deviceIds.map((deviceId) => { |
|
|
|
const deviceItems = tableData.filter( |
|
|
|
return { labels, deviceIds, timeGroups, sortedTimes }; |
|
|
|
} catch (error) { |
|
|
|
console.error('数据采样出错:', error); |
|
|
|
return { |
|
|
|
labels: [], |
|
|
|
deviceIds: [], |
|
|
|
timeGroups: {}, |
|
|
|
sortedTimes: [] |
|
|
|
}; |
|
|
|
} |
|
|
|
}; |
|
|
|
|
|
|
|
// 准备X轴图表数据 |
|
|
|
const xChartData = useMemo(() => { |
|
|
|
try { |
|
|
|
const { labels, deviceIds, timeGroups, sortedTimes } = sampleData(tableData); |
|
|
|
|
|
|
|
if (!deviceIds || deviceIds.length === 0) { |
|
|
|
return { labels: [], datasets: [] }; |
|
|
|
} |
|
|
|
|
|
|
|
const datasets = deviceIds.map((deviceId) => { |
|
|
|
const data = sortedTimes.map((timeKey) => { |
|
|
|
const timeData = timeGroups[timeKey] || []; |
|
|
|
const deviceData = timeData.find( |
|
|
|
(item) => item.deviceId === deviceId |
|
|
|
); |
|
|
|
|
|
|
|
// 为每个时间点找到对应的X和Y值,如果没有则为null |
|
|
|
const xData = allTimes.map((time) => { |
|
|
|
const item = deviceItems.find((d) => d.updateTime === time); |
|
|
|
return item ? parseFloat(item.xValue) : null; |
|
|
|
return deviceData && deviceData.xValue !== undefined ? parseFloat(deviceData.xValue) : null; |
|
|
|
}); |
|
|
|
|
|
|
|
const yData = allTimes.map((time) => { |
|
|
|
const item = deviceItems.find((d) => d.updateTime === time); |
|
|
|
return item ? parseFloat(item.yValue) : null; |
|
|
|
return { |
|
|
|
label: deviceId, |
|
|
|
data: data, |
|
|
|
borderColor: getDeviceColor(deviceId), |
|
|
|
backgroundColor: getDeviceColor(deviceId) + "20", |
|
|
|
borderWidth: 2, |
|
|
|
pointRadius: 3, |
|
|
|
pointHoverRadius: 5, |
|
|
|
tension: 0, |
|
|
|
connectNulls: false, |
|
|
|
}; |
|
|
|
}); |
|
|
|
|
|
|
|
// 使用固定的颜色映射 |
|
|
|
const color = getDeviceColor(deviceId); |
|
|
|
return { labels, datasets }; |
|
|
|
} catch (error) { |
|
|
|
console.error('准备X轴图表数据出错:', error); |
|
|
|
return { labels: [], datasets: [] }; |
|
|
|
} |
|
|
|
}, [tableData]); |
|
|
|
|
|
|
|
// 准备Y轴图表数据 |
|
|
|
const yChartData = useMemo(() => { |
|
|
|
try { |
|
|
|
const { labels, deviceIds, timeGroups, sortedTimes } = sampleData(tableData); |
|
|
|
|
|
|
|
if (!deviceIds || deviceIds.length === 0) { |
|
|
|
return { labels: [], datasets: [] }; |
|
|
|
} |
|
|
|
|
|
|
|
const datasets = deviceIds.map((deviceId) => { |
|
|
|
const data = sortedTimes.map((timeKey) => { |
|
|
|
const timeData = timeGroups[timeKey] || []; |
|
|
|
const deviceData = timeData.find( |
|
|
|
(item) => item.deviceId === deviceId |
|
|
|
); |
|
|
|
return deviceData && deviceData.yValue !== undefined ? parseFloat(deviceData.yValue) : null; |
|
|
|
}); |
|
|
|
|
|
|
|
return { |
|
|
|
deviceId, |
|
|
|
xData, |
|
|
|
yData, |
|
|
|
color, |
|
|
|
label: deviceId, |
|
|
|
data: data, |
|
|
|
borderColor: getDeviceColor(deviceId), |
|
|
|
backgroundColor: getDeviceColor(deviceId) + "20", |
|
|
|
borderWidth: 2, |
|
|
|
pointRadius: 3, |
|
|
|
pointHoverRadius: 5, |
|
|
|
tension: 0, |
|
|
|
connectNulls: false, |
|
|
|
}; |
|
|
|
}); |
|
|
|
|
|
|
|
return { timeLabels, deviceData }; |
|
|
|
}; |
|
|
|
return { labels, datasets }; |
|
|
|
} catch (error) { |
|
|
|
console.error('准备Y轴图表数据出错:', error); |
|
|
|
return { labels: [], datasets: [] }; |
|
|
|
} |
|
|
|
}, [tableData]); |
|
|
|
|
|
|
|
const { timeLabels, deviceData } = prepareChartData(); |
|
|
|
// Chart.js配置选项 |
|
|
|
const chartOptions = { |
|
|
|
responsive: true, |
|
|
|
maintainAspectRatio: false, |
|
|
|
interaction: { |
|
|
|
mode: "index", |
|
|
|
intersect: false, |
|
|
|
}, |
|
|
|
plugins: { |
|
|
|
legend: { |
|
|
|
position: "bottom", |
|
|
|
labels: { |
|
|
|
usePointStyle: true, |
|
|
|
padding: 20, |
|
|
|
font: { |
|
|
|
size: 12, |
|
|
|
}, |
|
|
|
}, |
|
|
|
}, |
|
|
|
tooltip: { |
|
|
|
filter: function (tooltipItem) { |
|
|
|
return tooltipItem.parsed.y !== null; |
|
|
|
}, |
|
|
|
}, |
|
|
|
}, |
|
|
|
scales: { |
|
|
|
x: { |
|
|
|
display: true, |
|
|
|
title: { |
|
|
|
display: true, |
|
|
|
text: "时间", |
|
|
|
}, |
|
|
|
ticks: { |
|
|
|
maxRotation: 45, |
|
|
|
minRotation: 0, |
|
|
|
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, |
|
|
|
}, |
|
|
|
}, |
|
|
|
}; |
|
|
|
|
|
|
|
// X值折线图配置 |
|
|
|
const getXChartOption = () => ({ |
|
|
|
// X轴图表配置 |
|
|
|
const xChartOptions = { |
|
|
|
...chartOptions, |
|
|
|
plugins: { |
|
|
|
...chartOptions.plugins, |
|
|
|
title: { |
|
|
|
display: true, |
|
|
|
text: "X轴位移数据", |
|
|
|
left: "center", |
|
|
|
textStyle: { |
|
|
|
fontSize: 16, |
|
|
|
fontWeight: "normal", |
|
|
|
font: { |
|
|
|
size: 16, |
|
|
|
}, |
|
|
|
}, |
|
|
|
tooltip: { |
|
|
|
trigger: "axis", |
|
|
|
formatter: function (params) { |
|
|
|
let result = `时间: ${params[0].axisValue}<br/>`; |
|
|
|
params.forEach((param) => { |
|
|
|
if (param.value !== null) { |
|
|
|
result += `${param.seriesName}: ${param.value} mm<br/>`; |
|
|
|
} |
|
|
|
}); |
|
|
|
return result; |
|
|
|
}, |
|
|
|
scales: { |
|
|
|
...chartOptions.scales, |
|
|
|
y: { |
|
|
|
...chartOptions.scales.y, |
|
|
|
title: { |
|
|
|
...chartOptions.scales.y.title, |
|
|
|
text: "X值(mm)", |
|
|
|
}, |
|
|
|
legend: { |
|
|
|
orient: "horizontal", |
|
|
|
bottom: "5%", |
|
|
|
textStyle: { |
|
|
|
fontSize: 12, |
|
|
|
}, |
|
|
|
}, |
|
|
|
grid: { |
|
|
|
left: "3%", |
|
|
|
right: "4%", |
|
|
|
bottom: "15%", |
|
|
|
top: "15%", |
|
|
|
containLabel: true, |
|
|
|
}, |
|
|
|
xAxis: { |
|
|
|
type: "category", |
|
|
|
data: timeLabels, |
|
|
|
axisLabel: { |
|
|
|
rotate: 45, |
|
|
|
fontSize: 11, |
|
|
|
}, |
|
|
|
}, |
|
|
|
yAxis: { |
|
|
|
type: "value", |
|
|
|
name: "X值(mm)", |
|
|
|
nameTextStyle: { |
|
|
|
fontSize: 13, |
|
|
|
}, |
|
|
|
axisLabel: { |
|
|
|
fontSize: 11, |
|
|
|
}, |
|
|
|
}, |
|
|
|
series: deviceData.map((device) => ({ |
|
|
|
name: device.deviceId, |
|
|
|
type: "line", |
|
|
|
data: device.xData, |
|
|
|
smooth: false, |
|
|
|
connectNulls: false, |
|
|
|
lineStyle: { |
|
|
|
color: device.color, |
|
|
|
width: 2, |
|
|
|
}, |
|
|
|
itemStyle: { |
|
|
|
color: device.color, |
|
|
|
}, |
|
|
|
symbol: "circle", |
|
|
|
symbolSize: 4, |
|
|
|
})), |
|
|
|
}); |
|
|
|
}; |
|
|
|
|
|
|
|
// Y值折线图配置 |
|
|
|
const getYChartOption = () => ({ |
|
|
|
// Y轴图表配置 |
|
|
|
const yChartOptions = { |
|
|
|
...chartOptions, |
|
|
|
plugins: { |
|
|
|
...chartOptions.plugins, |
|
|
|
title: { |
|
|
|
display: true, |
|
|
|
text: "Y轴位移数据", |
|
|
|
left: "center", |
|
|
|
textStyle: { |
|
|
|
fontSize: 16, |
|
|
|
fontWeight: "normal", |
|
|
|
font: { |
|
|
|
size: 16, |
|
|
|
}, |
|
|
|
}, |
|
|
|
tooltip: { |
|
|
|
trigger: "axis", |
|
|
|
formatter: function (params) { |
|
|
|
let result = `时间: ${params[0].axisValue}<br/>`; |
|
|
|
params.forEach((param) => { |
|
|
|
if (param.value !== null) { |
|
|
|
result += `${param.seriesName}: ${param.value} mm<br/>`; |
|
|
|
} |
|
|
|
}); |
|
|
|
return result; |
|
|
|
}, |
|
|
|
scales: { |
|
|
|
...chartOptions.scales, |
|
|
|
y: { |
|
|
|
...chartOptions.scales.y, |
|
|
|
title: { |
|
|
|
...chartOptions.scales.y.title, |
|
|
|
text: "Y值(mm)", |
|
|
|
}, |
|
|
|
legend: { |
|
|
|
orient: "horizontal", |
|
|
|
bottom: "5%", |
|
|
|
textStyle: { |
|
|
|
fontSize: 12, |
|
|
|
}, |
|
|
|
}, |
|
|
|
grid: { |
|
|
|
left: "3%", |
|
|
|
right: "4%", |
|
|
|
bottom: "15%", |
|
|
|
top: "15%", |
|
|
|
containLabel: true, |
|
|
|
}, |
|
|
|
xAxis: { |
|
|
|
type: "category", |
|
|
|
data: timeLabels, |
|
|
|
axisLabel: { |
|
|
|
rotate: 45, |
|
|
|
fontSize: 11, |
|
|
|
}, |
|
|
|
}, |
|
|
|
yAxis: { |
|
|
|
type: "value", |
|
|
|
name: "Y值(mm)", |
|
|
|
nameTextStyle: { |
|
|
|
fontSize: 13, |
|
|
|
}, |
|
|
|
axisLabel: { |
|
|
|
fontSize: 11, |
|
|
|
}, |
|
|
|
}, |
|
|
|
series: deviceData.map((device) => ({ |
|
|
|
name: device.deviceId, |
|
|
|
type: "line", |
|
|
|
data: device.yData, |
|
|
|
smooth: false, |
|
|
|
connectNulls: false, |
|
|
|
lineStyle: { |
|
|
|
color: device.color, |
|
|
|
width: 2, |
|
|
|
}, |
|
|
|
itemStyle: { |
|
|
|
color: device.color, |
|
|
|
}, |
|
|
|
symbol: "circle", |
|
|
|
symbolSize: 4, |
|
|
|
})), |
|
|
|
}); |
|
|
|
}; |
|
|
|
|
|
|
|
// 添加调试信息 |
|
|
|
useEffect(() => { |
|
|
|
console.log('RealtimeCharts - tableData:', tableData); |
|
|
|
console.log('RealtimeCharts - xChartData:', xChartData); |
|
|
|
console.log('RealtimeCharts - yChartData:', yChartData); |
|
|
|
}, [tableData, xChartData, yChartData]); |
|
|
|
|
|
|
|
// 如果没有数据,显示空状态 |
|
|
|
if (!tableData || tableData.length === 0) { |
|
|
|
return ( |
|
|
|
<div |
|
|
|
style={{ |
|
|
|
flex: 2, |
|
|
|
padding: "16px", |
|
|
|
display: "flex", |
|
|
|
flexDirection: "column", |
|
|
|
}} |
|
|
|
> |
|
|
|
<Title level={4} style={{ marginBottom: "16px" }}> |
|
|
|
实时数据图 |
|
|
|
<Badge |
|
|
|
status="default" |
|
|
|
text="等待数据..." |
|
|
|
style={{ marginLeft: "16px", fontSize: "12px" }} |
|
|
|
/> |
|
|
|
</Title> |
|
|
|
<div style={{ |
|
|
|
flex: 1, |
|
|
|
display: "flex", |
|
|
|
alignItems: "center", |
|
|
|
justifyContent: "center", |
|
|
|
minHeight: "500px" |
|
|
|
}}> |
|
|
|
<div style={{ textAlign: "center", color: "#999" }}> |
|
|
|
<p>等待实时数据...</p> |
|
|
|
</div> |
|
|
|
</div> |
|
|
|
</div> |
|
|
|
); |
|
|
|
} |
|
|
|
|
|
|
|
return ( |
|
|
|
<div |
|
|
@ -262,13 +373,14 @@ const RealtimeCharts = ({ tableData, lastUpdateTime }) => { |
|
|
|
backgroundColor: "white", |
|
|
|
borderRadius: "8px", |
|
|
|
border: "1px solid #e8e8e8", |
|
|
|
minHeight: "250px", |
|
|
|
minHeight: "280px", |
|
|
|
padding: "16px", |
|
|
|
}} |
|
|
|
> |
|
|
|
<ReactECharts |
|
|
|
option={getXChartOption()} |
|
|
|
style={{ height: "100%", width: "100%" }} |
|
|
|
opts={{ renderer: "canvas" }} |
|
|
|
<Line |
|
|
|
ref={xChartRef} |
|
|
|
data={xChartData} |
|
|
|
options={xChartOptions} |
|
|
|
/> |
|
|
|
</div> |
|
|
|
|
|
|
@ -279,13 +391,14 @@ const RealtimeCharts = ({ tableData, lastUpdateTime }) => { |
|
|
|
backgroundColor: "white", |
|
|
|
borderRadius: "8px", |
|
|
|
border: "1px solid #e8e8e8", |
|
|
|
minHeight: "250px", |
|
|
|
minHeight: "280px", |
|
|
|
padding: "16px", |
|
|
|
}} |
|
|
|
> |
|
|
|
<ReactECharts |
|
|
|
option={getYChartOption()} |
|
|
|
style={{ height: "100%", width: "100%" }} |
|
|
|
opts={{ renderer: "canvas" }} |
|
|
|
<Line |
|
|
|
ref={yChartRef} |
|
|
|
data={yChartData} |
|
|
|
options={yChartOptions} |
|
|
|
/> |
|
|
|
</div> |
|
|
|
</div> |
|
|
|