无源标靶上位机
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 

606 lines
19 KiB

import React, { useState, useEffect } from "react";
import { Tabs, Typography } from "antd";
import ExcelJS from "exceljs";
import {
CameraView,
TargetList,
TemplateList,
RealtimeCharts,
RealtimeDataTable,
TemplateModal,
TargetDetailModal,
} from "../components";
import {
WebSocketProvider,
useWebSocket,
useWebSocketSubscription,
} 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
const WuyuanbiaobaContent = () => {
const { isConnected, sendMessage } = useWebSocket();
// 订阅实时数据
const realtimeDataSubscription = useWebSocketSubscription("dev", "data");
const realtimeBufferRef = useRef([]);
const exportCountRef = useRef(200);
const MAX_BUFFER_SIZE = 1000;
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];
// 只保留最大MAX_BUFFER_SIZE条数据
if (realtimeBufferRef.current.length > MAX_BUFFER_SIZE) {
realtimeBufferRef.current = realtimeBufferRef.current.slice(
-MAX_BUFFER_SIZE
);
}
};
const {
templates: tempListData,
loading: templatesLoading,
addTemplate,
updateTemplate,
deleteTemplate,
getTemplateByKey,
} = useTemplateStorage();
const {
targets: targetListData,
loading: targetsLoading,
addTarget,
updateTarget,
deleteTarget,
refreshTargets,
} = useTargetStorage();
const [selectedTemplate, setSelectedTemplate] = useState(null);
const [tableData, setTableData] = useState([]);
const [realtimeData, setRealtimeData] = useState([]);
const [lastUpdateTime, setLastUpdateTime] = useState(new Date());
// 数据采样相关状态
const [lastSampleTime, setLastSampleTime] = useState(0);
// 模板相关状态
const [templateModalVisible, setTemplateModalVisible] = useState(false);
const [templateModalMode, setTemplateModalMode] = useState("add"); // 'add' | 'edit'
const [currentEditTemplate, setCurrentEditTemplate] = useState(null);
// 标靶详情模态框相关状态
const [targetDetailModalVisible, setTargetDetailModalVisible] =
useState(false);
const [targetDetailModalMode, setTargetDetailModalMode] = useState("edit"); // 'edit'
const [currentEditTarget, setCurrentEditTarget] = useState(null);
// 添加选中标靶的状态
const [selectedTargetId, setSelectedTargetId] = useState(null);
// 处理实时数据并转换为表格格式
const processRealtimeData = (data) => {
if (!data || !data.data || !Array.isArray(data.data)) {
return [];
}
return data.data.map((item) => ({
key: item.pos,
deviceId: item.pos, // 使用 pos 作为 deviceId
desc: item.desc, // 添加 desc 字段
xValue: item.x,
yValue: item.y,
updateTime: data.time,
}));
};
// 初始化数据
useEffect(() => {
// 初始化空的实时数据表格
setRealtimeData([]);
setTableData([]);
// console.log("数据已初始化,等待实时数据...", import.meta.env.MODE);
}, []);
// 模板数据加载完成后,默认选中内置模板
useEffect(() => {
if (!templatesLoading && tempListData.length > 0 && !selectedTemplate) {
// 查找内置模板
const builtinTemplate = tempListData.find(
(template) => template.key === "builtin_1"
);
if (builtinTemplate) {
setSelectedTemplate("builtin_1");
// console.log("默认选中内置模板:", builtinTemplate.name);
} else {
// 如果没有内置模板,选择第一个模板
setSelectedTemplate(tempListData[0].key);
// console.log("默认选中第一个模板:", tempListData[0].name);
}
}
}, [templatesLoading, tempListData, selectedTemplate]);
// WebSocket连接成功后的处理
useEffect(() => {
if (isConnected) {
// console.log("WebSocket已连接,等待实时数据...");
} else {
// console.log("WebSocket未连接");
}
}, [isConnected]);
// 调试实时数据订阅状态
useEffect(() => {
// console.log('实时数据订阅状态:', {
// hasData: !!realtimeDataSubscription.latest,
// dataCount: realtimeDataSubscription.data?.length || 0,
// latestTimestamp: realtimeDataSubscription.latest?.timestamp,
// });
}, [realtimeDataSubscription]);
// 处理实时数据更新
useEffect(() => {
if (
realtimeDataSubscription.latest &&
realtimeDataSubscription.latest.values
) {
const newRealtimeData = processRealtimeData(
realtimeDataSubscription.latest.values
);
if (newRealtimeData.length > 0) {
const currentTime = Date.now();
const currentSecond = Math.floor(currentTime / 1000);
// 每秒采样一次数据用于图表和表格显示
if (currentSecond > lastSampleTime) {
setLastSampleTime(currentSecond);
// 更新采样后的历史数据(用于图表显示)
setTableData((prevData) => {
const updatedData = [
...prevData,
...newRealtimeData.map((point) => ({
...point,
key: `${currentTime}_${point.key}`,
timestamp: currentTime,
updateTime: new Date(currentTime).toLocaleString(),
})),
];
// 只保留最近25个数据点
return updatedData.slice(-75); // 3个设备 * 25个时间点
});
// 更新实时数据表格(使用最新的采样数据)
setRealtimeData(
newRealtimeData.map((point) => ({
...point,
key: `realtime_${point.key}`,
updateTime: new Date(currentTime).toLocaleString(),
}))
);
}
//维护两分钟的缓冲区
pushToRealtimeBuffer(realtimeDataSubscription.latest.values);
setLastUpdateTime(new Date());
}
}
}, [realtimeDataSubscription.latest]);
const dataExport = async () => {
const count = exportCountRef.current || 200;
const buf = realtimeBufferRef.current;
if (buf.length === 0) {
console.warn("没有数据可导出");
return;
}
// 只取最新的 count 条
const dataToExport = buf.slice(-count);
// 收集所有出现过的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);
};
// 选中标靶的处理函数
const handleSelectTarget = (target) => {
setSelectedTargetId(target.id);
// console.log("选中标靶:", target);
};
// 清除选中标靶的处理函数
const handleClearSelection = () => {
setSelectedTargetId(null);
// console.log("清除标靶选中状态");
};
// 处理矩形框点击事件
const handleRectangleClick = (targetData) => {
// console.log("矩形框被点击,打开标靶详情:", targetData);
setCurrentEditTarget(targetData);
setTargetDetailModalMode("edit");
setTargetDetailModalVisible(true);
};
// 选择模板的处理函数
const handleTemplateSelect = (templateKey) => {
setSelectedTemplate(templateKey);
// console.log("选中模板:", templateKey);
};
// 添加新模板的处理函数
const handleAddTemplate = () => {
setTemplateModalMode("add");
setCurrentEditTemplate(null);
setTemplateModalVisible(true);
};
// 编辑模板的处理函数
const handleEditTemplate = (template) => {
setTemplateModalMode("edit");
setCurrentEditTemplate(template);
setTemplateModalVisible(true);
};
// 模态框确认处理函数
const handleTemplateModalOk = (templateInfo) => {
// console.log(templateInfo, "templateInfo");
let success = false;
if (templateModalMode === "add") {
success = addTemplate(templateInfo);
} else {
success = updateTemplate(templateInfo);
}
if (success) {
setTemplateModalVisible(false);
} else {
console.error("模板操作失败");
// 可以添加用户提示
}
};
// 模态框取消处理函数
const handleTemplateModalCancel = () => {
setTemplateModalVisible(false);
setCurrentEditTemplate(null);
};
// 删除模板处理函数
const handleDeleteTemplate = (template) => {
const success = deleteTemplate(template.key);
if (success) {
setTemplateModalVisible(false);
// 如果删除的是当前选中的模板,则清空选中状态
if (selectedTemplate === template.key) {
setSelectedTemplate(null);
}
} else {
console.error("删除模板失败");
// 可以添加用户提示
}
};
// 标靶详情模态框确认处理函数
const handleTargetDetailModalOk = (targetInfo) => {
const success = updateTarget(targetInfo);
if (success) {
setTargetDetailModalVisible(false);
setCurrentEditTarget(null);
// console.log("更新标靶信息:", targetInfo);
} else {
console.error("更新标靶失败");
// 可以添加用户提示
}
};
// 标靶详情模态框取消处理函数
const handleTargetDetailModalCancel = () => {
setTargetDetailModalVisible(false);
setCurrentEditTarget(null);
};
// 删除标靶处理函数
const handleDeleteTarget = (targetKey) => {
// console.log("开始删除标靶:", targetKey);
const success = deleteTarget(targetKey);
if (success) {
setTargetDetailModalVisible(false);
setCurrentEditTarget(null);
// 如果删除的标靶是当前选中的,清除选中状态
if (selectedTargetId === targetKey) {
setSelectedTargetId(null);
}
// console.log("删除标靶成功:", targetKey);
// 强制刷新标靶数据,确保UI立即更新
setTimeout(() => {
refreshTargets();
}, 500);
} else {
console.error("删除标靶失败");
// 可以添加用户提示
}
};
// 标靶-一键清零
const onClickClearAll = () => {
// console.log("一键清零操作");
sendMessage(
JSON.stringify({
_from: "setup",
cmd: "clearZero",
values: {},
})
);
};
// Tabs items 配置
const tabItems = [
{
key: "target-list",
label: "标靶列表",
children: (
<TargetList
targetListData={targetListData}
selectedTargetId={selectedTargetId}
onEditTarget={handleEditTarget}
onSelectTarget={handleSelectTarget}
onClickClearAll={onClickClearAll}
/>
),
},
{
key: "temp-list",
label: "模板列表",
children: (
<TemplateList
tempListData={tempListData}
selectedTemplate={selectedTemplate}
onTemplateSelect={handleTemplateSelect}
onAddTemplate={handleAddTemplate}
onEditTemplate={handleEditTemplate}
/>
),
},
];
return (
<div
style={{
minHeight: "100vh",
display: "flex",
flexDirection: "column",
backgroundColor: "#f0f2f5",
}}
>
{/* Header 区域 */}
<div
style={{
height: "60px",
backgroundColor: "white",
color: "#333",
display: "flex",
alignItems: "center",
padding: "0 24px",
flexShrink: 0,
borderBottom: "1px solid #d9d9d9",
}}
>
<Title level={3} style={{ color: "#333", margin: 0 }}>
视觉位移计配置工具
</Title>
</div>
{/* 中间区域 - 固定视口剩余高度 */}
<div
style={{
height: "calc(100vh - 60px)",
display: "flex",
margin: "16px",
}}
>
{/* Camera 区域 */}
<CameraView
selectedTargetId={selectedTargetId}
onClearSelection={handleClearSelection}
onRectangleClick={handleRectangleClick}
selectedTemplate={
selectedTemplate ? getTemplateByKey(selectedTemplate) : null
}
// 传递标靶数据,避免重复调用 Hook
targets={targetListData}
targetsLoading={targetsLoading}
onRefreshTargets={refreshTargets}
/>
{/* 右侧 Target List / Temp List 区域 */}
<div
style={{
flex: 1,
backgroundColor: "white",
display: "flex",
flexDirection: "column",
}}
>
<Tabs
defaultActiveKey="target-list"
items={tabItems}
style={{
height: "100%",
display: "flex",
flexDirection: "column",
}}
centered
size="large"
tabBarGutter={64}
/>
</div>
</div>
{/* 底部区域 - 在视口下方,需要滚动查看 */}
<div
style={{
margin: "16px",
marginTop: 0,
minHeight: "600px",
display: "flex",
backgroundColor: "white",
}}
>
{/* Charts 区域 */}
<RealtimeCharts
tableData={tableData}
lastUpdateTime={lastUpdateTime}
onDataExport={dataExport}
exportCount={exportCountRef.current}
setExportCount={setExportCount}
/>
{/* Table 区域 - 使用采样数据显示 */}
<RealtimeDataTable realtimeData={realtimeData} />
</div>
{/* 模板编辑模态框 */}
<TemplateModal
visible={templateModalVisible}
mode={templateModalMode}
templateData={currentEditTemplate}
tempListData={tempListData}
onOk={handleTemplateModalOk}
onCancel={handleTemplateModalCancel}
onDelete={handleDeleteTemplate}
/>
{/* 标靶详情模态框 */}
<TargetDetailModal
visible={targetDetailModalVisible}
mode={targetDetailModalMode}
targetData={currentEditTarget}
onOk={handleTargetDetailModalOk}
onCancel={handleTargetDetailModalCancel}
onDelete={handleDeleteTarget}
/>
</div>
);
};
// 主组件,使用WebSocket Provider包装
const Wuyuanbiaoba = () => {
return (
<WebSocketProvider>
<WuyuanbiaobaContent />
</WebSocketProvider>
);
};
export default Wuyuanbiaoba;