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.
 
 
 
 
 

434 lines
14 KiB

import React, { useState, useEffect, useRef } from "react";
import { Button, Checkbox, CheckboxProps, message, Switch } from "antd";
import { useNavigate } from "react-router";
import {
fetchAnalysisReport,
getBaseRecordPointSetByCode,
saveMeasurement,
startMeasurement,
} from "../../../services/measure/analysis";
import { createWebSocket, sharedWsUrl } from "../../../services/socket";
import { switchMeasureAfterSave } from "../../../store/features/contextSlice";
import { AnalysisReport, AnalyzeAngle } from "../../../services/measure/type";
import { MeasureState, TaskState, taskStatusDescMap, TrackRecordSig } from "../../../services/wsTypes";
import { useAppDispatch, useAppSelector } from "../../../utils/hooks";
import Gr_round from "../../../assets/green_round.svg";
import Bl_round from "../../../assets/blue_round.svg";
import MeasurementCanvas, { AnalysisData, BenchmarkShape, MeasurementCanvasRef } from "./konva/MeasurementCanvas";
import "./MeasureAction.scss";
// 创建 websocket 客户端
const wsClient = createWebSocket(sharedWsUrl);
export default function MeasureAction() {
const dispatch = useAppDispatch();
const navigate = useNavigate();
/** ----------------------- 引用 ----------------------- **/
const canvasRef = useRef<MeasurementCanvasRef>(null);
const leftPoints = useRef<{ x: number; y: number }[]>([]);
const rightPoints = useRef<{ x: number; y: number }[]>([]);
const isLeftFinished = useRef(false);
/** ----------------------- 状态 ----------------------- **/
const [showGrid, setShowGrid] = useState(true);
const [showStandard, setShowStandard] = useState(true);
const [showMark, setShowMark] = useState(true);
const [angleMarkBackup, setAngleMarkBackup] = useState(true);
const afterSave = useAppSelector(store => store.context.newMeasureAfterSave);
// const [angles, setAngles] = useState<AnalyzeAngle[]>([]);
// const [taskStatus, setTaskStatus] = useState<MeasureState["data"]["taskStatus"]>("IDLE");
const [startBtnText, setStartBtnText] = useState("开始测量");
const [measurementFinished, setMeasurementFinished] = useState(false);
const [analysisClicked, setAnalysisClicked] = useState(false);
const [saveClicked, setSaveClicked] = useState(false);
const [analysisReport, setAnalysisReport] = useState<AnalysisReport | null>(null);
const [showAnalysisTable, setShowAnalysisTable] = useState(false);
// const [taskStatusName, setTaskStatusName] = useState("");
// 初始状态列表
const initialStatusList = [
{ statusCode: "START_RECORD_LEFT", name: "请移动到顶部,停顿2秒", background: "#ececec", isReady: false, color: "h" },
{ statusCode: "START_RECORD_LEFT", name: "开始测量左侧", background: "#ececec", isReady: false, color: "h" },
{ statusCode: "START_RECORD_LEFT", name: "左侧测量完成", background: "#ececec", isReady: false, color: "h" },
{ statusCode: "START_RECORD_LEFT", name: "请移动到顶部,停顿2秒", background: "#ececec", isReady: false, color: "h" },
{ statusCode: "START_RECORD_LEFT", name: "开始测量右侧", background: "#ececec", isReady: false, color: "h" },
{ statusCode: "START_RECORD_LEFT", name: "右侧测量完成", background: "#ececec", isReady: false, color: "h" },
];
const [statusList, setStatusList] = useState(initialStatusList);
/** ----------------------- 事件处理函数 ----------------------- */
// 切换保存后自动开始新测量
const onAfterSaveChange: CheckboxProps["onChange"] = e => {
dispatch(switchMeasureAfterSave(e.target.checked));
};
// 分析按钮点击事件
const onAnalysisBtnClick = () => {
setAnalysisClicked(true);
fetchAnalysisReport("6001").then(res => {
if (res.success) {
const report: AnalysisReport = res.data;
console.log(report);
// 更新 canvas 的分析数据
if (report && report.angleAnalysisList) {
// 先过滤掉 distance 为 null 的数据
const validItems = report.angleAnalysisList.filter(item => item.distance !== null);
const analysisData: AnalysisData[] = validItems.map(item => ({
pointA: { x: parseFloat(item.pointA.x), y: parseFloat(item.pointA.y) },
pointB: { x: parseFloat(item.pointB.x), y: parseFloat(item.pointB.y) },
base: { x: parseFloat(item.pointA.x), y: parseFloat(item.pointA.y) },
measure: { x: parseFloat(item.pointB.x), y: parseFloat(item.pointB.y) },
distance: parseFloat(item.distance),
describe: item.describe,
}));
canvasRef.current?.setAnalysisData(analysisData);
}
setAnalysisReport(report);
setShowAnalysisTable(true);
} else {
message.error("分析报告请求失败: " + res.data.info);
}
});
};
// 开始/重新测量按钮点击事件
const onStart = () => {
if (startBtnText === "新测量") {
navigate("../newMeasure");
return;
}
// 重置测量相关状态
setShowAnalysisTable(false);
setMeasurementFinished(false);
setAnalysisClicked(false);
setSaveClicked(false);
isLeftFinished.current = false;
leftPoints.current = [];
rightPoints.current = [];
canvasRef.current?.clearShapes();
canvasRef.current?.resetCanvas();
if (startBtnText === "重新测量") {
setStatusList(initialStatusList);
}
startMeasurement().then(res => {
if (res.status !== 0) {
message.error(res.data.info);
// setTaskStatusName(taskStatusDescMap["IDLE"]);
} else {
const newStatusList = [...initialStatusList];
newStatusList[0].color = "b";
setStatusList(newStatusList);
message.success("已通知设备开始测量");
// setTaskStatusName(taskStatusDescMap["IDLE"]);
setStartBtnText("重新测量");
}
});
};
// 保存按钮点击事件
const onSaveBtnClick = () => {
setSaveClicked(true);
saveMeasurement().then(res => {
if (res.status !== 0) {
message.error(res.data.info);
} else {
message.success("保存成功");
if (afterSave) {
navigate("../config");
} else {
setStartBtnText("新测量");
}
}
});
};
// 辅助函数:渲染状态项的图标
const renderStatusIcon = (item: (typeof initialStatusList)[0]) => {
if (item.color === "g") {
return <img src={Gr_round} alt="green" />;
} else if (item.color === "b") {
return <img src={Bl_round} alt="blue" />;
} else {
return (
<div
style={{
width: "22px",
height: "22px",
background: "#c0c0c0",
borderRadius: "50%",
marginTop: "10px",
}}
/>
);
}
};
/** ----------------------- WebSocket 消息处理 ----------------------- **/
useEffect(() => {
// 处理任务状态消息
const handleStateMessage = (state: MeasureState["data"]) => {
};
// 处理事件消息
const handleEventMessage = (type: TaskState["data"]) => {
setStatusList(prev => {
const updated = [...prev];
switch (type) {
case "START_RECORD_LEFT":
updated[0].color = "g";
updated[1].color = "b";
const audio1 = new Audio("/audio/begin_left.mp3");
// 播放音频
audio1
.play()
.then(() => {
console.log("音频开始播放");
})
.catch(err => {
console.error("播放音频时出错:", err);
});
break;
case "FINISH_RECORD_LEFT":
updated[1].color = "g";
updated[2].color = "g";
updated[3].color = "b";
isLeftFinished.current = true;
const audio2 = new Audio("/audio/end_left.mp3");
// 播放音频
audio2
.play()
.then(() => {
console.log("音频开始播放");
})
.catch(err => {
console.error("播放音频时出错:", err);
});
break;
case "START_RECORD_RIGHT":
updated[3].color = "g";
updated[4].color = "b";
const audio3 = new Audio("/audio/begin_right.mp3");
// 播放音频
audio3
.play()
.then(() => {
console.log("音频开始播放");
})
.catch(err => {
console.error("播放音频时出错:", err);
});
break;
case "FINISH_RECORD_RIGHT":
updated[4].color = "g";
updated[5].color = "g";
setMeasurementFinished(true);
const audio4 = new Audio("/audio/end_right.mp3");
// 播放音频
audio4
.play()
.then(() => {
console.log("音频开始播放");
})
.catch(err => {
console.error("播放音频时出错:", err);
});
break;
default:
break;
}
return updated;
});
};
// 处理点数据消息
const handlePointReport = (pointData: TrackRecordSig["data"]) => {
console.log(`pointData === ${pointData.x},${pointData.y}`);
if (!isLeftFinished.current) {
leftPoints.current.push(pointData);
canvasRef.current?.setMeasurementDataLeft([...leftPoints.current]);
} else {
rightPoints.current.push(pointData);
canvasRef.current?.setMeasurementDataRight([...rightPoints.current]);
}
};
const subscription = wsClient.dataOb.subscribe((data) => {
if ( data.path === "/measurement-task/get-task-state") {
handleStateMessage(data.data);
} else if (data.path === "/measurement-task/event") {
handleEventMessage(data.data);
} else if (data.path === "/measurement-task/point-report") {
handlePointReport(data.data);
}
});
wsClient.connect();
return () => subscription.unsubscribe();
}, []);
/** ----------------------- 页面加载获取基础图形数据 ----------------------- **/
useEffect(() => {
getBaseRecordPointSetByCode("6001").then(res => {
if (res.success) {
const benchmarkShapes = JSON.parse(res.data.points) as BenchmarkShape[];
if (canvasRef.current) {
console.log("解析后的基础图形数据:", benchmarkShapes);
canvasRef.current.setBenchmarkData(benchmarkShapes);
}
}
});
}, []);
/** ----------------------- 渲染 ----------------------- **/
return (
<div className="flex h-full px-6">
{/* 左侧区域:包含开关区域和测量画布 */}
<div className="">
<div className="flex gap-4 items-center px-6 pt-5">
{/* 参考线开关 */}
<div className="flex gap-2 items-center">
<Switch defaultChecked onChange={checked => setShowGrid(checked)} />
<span>线</span>
</div>
{/* 标准线开关 */}
<div className="flex gap-2 items-center">
<Switch
checked={showStandard}
onChange={checked => {
setShowStandard(checked);
if (!checked) {
setAngleMarkBackup(showMark);
setShowMark(false);
} else {
setShowMark(angleMarkBackup);
}
}}
/>
<span>线</span>
</div>
{/* 角度线开关,仅在点击分析按钮后显示 */}
{analysisClicked && (
<div className="flex gap-2 items-center">
<Switch
checked={showMark}
disabled={!showStandard}
onChange={checked => {
setShowMark(checked);
setAngleMarkBackup(checked);
}}
/>
<span>线</span>
</div>
)}
</div>
<div className="relative m-2">
<MeasurementCanvas
width={800}
height={600}
logicalExtent={{ minX: -50, maxX: 50, minY: -20, maxY: 60 }}
gridStep={1}
origin={{ x: 0, y: 20 }}
pixelPerMm={8}
maxZoom={10}
showGrid={showGrid}
showBenchmark={showStandard}
showAnalysis={showMark}
showScale={false}
scaleInterval={1}
showCoordinates={showGrid}
ref={canvasRef}
/>
</div>
</div>
{/* 右侧区域:根据 showAnalysisTable 状态显示测量步骤或分析表格 */}
<div className="min-w-[300px] flex-auto py-6 flex flex-col items-center">
{showAnalysisTable && analysisReport ? (
<>
<header className="bg-[#e8f0ff] w-[300px] text-center text-lg font-medium py-2 text-primary border border-[#c1c6d4]">
</header>
<div className="analysis-table">
<table
style={{
width: "100%",
borderCollapse: "collapse",
border: "1px solid #ccc",
textAlign: "center",
}}>
<tbody>
<tr style={{ height: "40px", fontSize: "18px", color: "#9E9E9E" }}>
<td style={{ padding: "8px", border: "1px solid #ccc" }}>W1垂直磨耗</td>
<td style={{ padding: "8px", border: "1px solid #ccc" }}>{analysisReport.w1}</td>
</tr>
<tr style={{ height: "40px", fontSize: "18px", color: "#9E9E9E" }}>
<td style={{ padding: "8px", border: "1px solid #ccc" }}></td>
<td style={{ padding: "8px", border: "1px solid #ccc" }}>
{analysisReport.railHeadWidth}
</td>
</tr>
{analysisReport.angleAnalysisList.map((item, index) => (
<tr key={index} style={{ height: "40px", fontSize: "18px", color: "#9E9E9E" }}>
<td style={{ padding: "8px", border: "1px solid #ccc" }}>{item.describe}</td>
<td style={{ padding: "8px", border: "1px solid #ccc" }}>{item.distance}</td>
</tr>
))}
</tbody>
</table>
</div>
<Button
style={{ width: 200, marginTop: 18 }}
size="large"
type="primary"
onClick={() => navigate("../config")}>
</Button>
</>
) : (
<div>
<h1 className="font-medium text-xl text-center"></h1>
<div className="w-[13rem] mt-5">
{statusList.map((item, index) => (
<div
key={index}
style={{ background: item.background, borderRadius: "20px" }}
className="mt-[10px] h-[40px]">
<div style={{ display: "flex", lineHeight: "40px" }} className="pl-[1rem]">
{renderStatusIcon(item)}
<div className="pl-[5px]">{item.name}</div>
</div>
</div>
))}
</div>
<section className="flex flex-col items-center gap-4 mt-6 border-t border-[#D8D8D8] py-4">
<Button style={{ width: 200 }} size="large" type="primary" onClick={onStart}>
{startBtnText}
</Button>
<Button
style={{ width: 200 }}
size="large"
type="primary"
onClick={onAnalysisBtnClick}
disabled={!measurementFinished || analysisClicked}>
</Button>
<Button
style={{ width: 200 }}
size="large"
type="primary"
onClick={onSaveBtnClick}
disabled={!measurementFinished || saveClicked}>
</Button>
<Checkbox checked={afterSave} onChange={onAfterSaveChange}>
</Checkbox>
</section>
</div>
)}
</div>
</div>
);
}