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

5 months ago
5 months ago
5 months ago
5 months ago
5 months ago
5 months ago
5 months ago
5 months ago
5 months ago
5 months ago
5 months ago
5 months ago
5 months ago
5 months ago
5 months ago
5 months ago
5 months ago
5 months ago
5 months ago
5 months ago
5 months ago
5 months ago
5 months ago
5 months ago
5 months ago
5 months ago
5 months ago
5 months ago
5 months ago
5 months ago
5 months ago
5 months ago
5 months ago
5 months ago
5 months ago
5 months ago
5 months ago
  1. import React, { useState, useEffect, useRef } from "react";
  2. import { Button, Checkbox, CheckboxProps, message, Switch } from "antd";
  3. import { useNavigate } from "react-router";
  4. import {
  5. fetchAnalysisReport,
  6. getBaseRecordPointSetByCode,
  7. saveMeasurement,
  8. startMeasurement,
  9. } from "../../../services/measure/analysis";
  10. import { createWebSocket, sharedWsUrl } from "../../../services/socket";
  11. import { switchMeasureAfterSave } from "../../../store/features/contextSlice";
  12. import { AnalysisReport, AnalyzeAngle } from "../../../services/measure/type";
  13. import { MeasureState, TaskState, taskStatusDescMap, TrackRecordSig } from "../../../services/wsTypes";
  14. import { useAppDispatch, useAppSelector } from "../../../utils/hooks";
  15. import Gr_round from "../../../assets/green_round.svg";
  16. import Bl_round from "../../../assets/blue_round.svg";
  17. import MeasurementCanvas, { AnalysisData, BenchmarkShape, MeasurementCanvasRef } from "./konva/MeasurementCanvas";
  18. import "./MeasureAction.scss";
  19. // 创建 websocket 客户端
  20. const wsClient = createWebSocket(sharedWsUrl);
  21. export default function MeasureAction() {
  22. const dispatch = useAppDispatch();
  23. const navigate = useNavigate();
  24. /** ----------------------- 引用 ----------------------- **/
  25. const canvasRef = useRef<MeasurementCanvasRef>(null);
  26. const leftPoints = useRef<{ x: number; y: number }[]>([]);
  27. const rightPoints = useRef<{ x: number; y: number }[]>([]);
  28. const isLeftFinished = useRef(false);
  29. /** ----------------------- 状态 ----------------------- **/
  30. const [showGrid, setShowGrid] = useState(true);
  31. const [showStandard, setShowStandard] = useState(true);
  32. const [showMark, setShowMark] = useState(true);
  33. const [angleMarkBackup, setAngleMarkBackup] = useState(true);
  34. const afterSave = useAppSelector(store => store.context.newMeasureAfterSave);
  35. // const [angles, setAngles] = useState<AnalyzeAngle[]>([]);
  36. // const [taskStatus, setTaskStatus] = useState<MeasureState["data"]["taskStatus"]>("IDLE");
  37. const [startBtnText, setStartBtnText] = useState("开始测量");
  38. const [measurementFinished, setMeasurementFinished] = useState(false);
  39. const [analysisClicked, setAnalysisClicked] = useState(false);
  40. const [saveClicked, setSaveClicked] = useState(false);
  41. const [analysisReport, setAnalysisReport] = useState<AnalysisReport | null>(null);
  42. const [showAnalysisTable, setShowAnalysisTable] = useState(false);
  43. // const [taskStatusName, setTaskStatusName] = useState("");
  44. // 初始状态列表
  45. const initialStatusList = [
  46. { statusCode: "START_RECORD_LEFT", name: "请移动到顶部,停顿2秒", background: "#ececec", isReady: false, color: "h" },
  47. { statusCode: "START_RECORD_LEFT", name: "开始测量左侧", background: "#ececec", isReady: false, color: "h" },
  48. { statusCode: "START_RECORD_LEFT", name: "左侧测量完成", background: "#ececec", isReady: false, color: "h" },
  49. { statusCode: "START_RECORD_LEFT", name: "请移动到顶部,停顿2秒", background: "#ececec", isReady: false, color: "h" },
  50. { statusCode: "START_RECORD_LEFT", name: "开始测量右侧", background: "#ececec", isReady: false, color: "h" },
  51. { statusCode: "START_RECORD_LEFT", name: "右侧测量完成", background: "#ececec", isReady: false, color: "h" },
  52. ];
  53. const [statusList, setStatusList] = useState(initialStatusList);
  54. /** ----------------------- 事件处理函数 ----------------------- */
  55. // 切换保存后自动开始新测量
  56. const onAfterSaveChange: CheckboxProps["onChange"] = e => {
  57. dispatch(switchMeasureAfterSave(e.target.checked));
  58. };
  59. // 分析按钮点击事件
  60. const onAnalysisBtnClick = () => {
  61. setAnalysisClicked(true);
  62. fetchAnalysisReport("6001").then(res => {
  63. if (res.success) {
  64. const report: AnalysisReport = res.data;
  65. console.log(report);
  66. // 更新 canvas 的分析数据
  67. if (report && report.angleAnalysisList) {
  68. // 先过滤掉 distance 为 null 的数据
  69. const validItems = report.angleAnalysisList.filter(item => item.distance !== null);
  70. const analysisData: AnalysisData[] = validItems.map(item => ({
  71. pointA: { x: parseFloat(item.pointA.x), y: parseFloat(item.pointA.y) },
  72. pointB: { x: parseFloat(item.pointB.x), y: parseFloat(item.pointB.y) },
  73. base: { x: parseFloat(item.pointA.x), y: parseFloat(item.pointA.y) },
  74. measure: { x: parseFloat(item.pointB.x), y: parseFloat(item.pointB.y) },
  75. distance: parseFloat(item.distance),
  76. describe: item.describe,
  77. }));
  78. canvasRef.current?.setAnalysisData(analysisData);
  79. }
  80. setAnalysisReport(report);
  81. setShowAnalysisTable(true);
  82. } else {
  83. message.error("分析报告请求失败: " + res.data.info);
  84. }
  85. });
  86. };
  87. // 开始/重新测量按钮点击事件
  88. const onStart = () => {
  89. if (startBtnText === "新测量") {
  90. navigate("../newMeasure");
  91. return;
  92. }
  93. // 重置测量相关状态
  94. setShowAnalysisTable(false);
  95. setMeasurementFinished(false);
  96. setAnalysisClicked(false);
  97. setSaveClicked(false);
  98. isLeftFinished.current = false;
  99. leftPoints.current = [];
  100. rightPoints.current = [];
  101. canvasRef.current?.clearShapes();
  102. canvasRef.current?.resetCanvas();
  103. if (startBtnText === "重新测量") {
  104. setStatusList(initialStatusList);
  105. }
  106. startMeasurement().then(res => {
  107. if (res.status !== 0) {
  108. message.error(res.data.info);
  109. // setTaskStatusName(taskStatusDescMap["IDLE"]);
  110. } else {
  111. const newStatusList = [...initialStatusList];
  112. newStatusList[0].color = "b";
  113. setStatusList(newStatusList);
  114. message.success("已通知设备开始测量");
  115. // setTaskStatusName(taskStatusDescMap["IDLE"]);
  116. setStartBtnText("重新测量");
  117. }
  118. });
  119. };
  120. // 保存按钮点击事件
  121. const onSaveBtnClick = () => {
  122. setSaveClicked(true);
  123. saveMeasurement().then(res => {
  124. if (res.status !== 0) {
  125. message.error(res.data.info);
  126. } else {
  127. message.success("保存成功");
  128. if (afterSave) {
  129. navigate("../config");
  130. } else {
  131. setStartBtnText("新测量");
  132. }
  133. }
  134. });
  135. };
  136. // 辅助函数:渲染状态项的图标
  137. const renderStatusIcon = (item: (typeof initialStatusList)[0]) => {
  138. if (item.color === "g") {
  139. return <img src={Gr_round} alt="green" />;
  140. } else if (item.color === "b") {
  141. return <img src={Bl_round} alt="blue" />;
  142. } else {
  143. return (
  144. <div
  145. style={{
  146. width: "22px",
  147. height: "22px",
  148. background: "#c0c0c0",
  149. borderRadius: "50%",
  150. marginTop: "10px",
  151. }}
  152. />
  153. );
  154. }
  155. };
  156. /** ----------------------- WebSocket 消息处理 ----------------------- **/
  157. useEffect(() => {
  158. // 处理任务状态消息
  159. const handleStateMessage = (state: MeasureState["data"]) => {
  160. };
  161. // 处理事件消息
  162. const handleEventMessage = (type: TaskState["data"]) => {
  163. setStatusList(prev => {
  164. const updated = [...prev];
  165. switch (type) {
  166. case "START_RECORD_LEFT":
  167. updated[0].color = "g";
  168. updated[1].color = "b";
  169. const audio1 = new Audio("/audio/begin_left.mp3");
  170. // 播放音频
  171. audio1
  172. .play()
  173. .then(() => {
  174. console.log("音频开始播放");
  175. })
  176. .catch(err => {
  177. console.error("播放音频时出错:", err);
  178. });
  179. break;
  180. case "FINISH_RECORD_LEFT":
  181. updated[1].color = "g";
  182. updated[2].color = "g";
  183. updated[3].color = "b";
  184. isLeftFinished.current = true;
  185. const audio2 = new Audio("/audio/end_left.mp3");
  186. // 播放音频
  187. audio2
  188. .play()
  189. .then(() => {
  190. console.log("音频开始播放");
  191. })
  192. .catch(err => {
  193. console.error("播放音频时出错:", err);
  194. });
  195. break;
  196. case "START_RECORD_RIGHT":
  197. updated[3].color = "g";
  198. updated[4].color = "b";
  199. const audio3 = new Audio("/audio/begin_right.mp3");
  200. // 播放音频
  201. audio3
  202. .play()
  203. .then(() => {
  204. console.log("音频开始播放");
  205. })
  206. .catch(err => {
  207. console.error("播放音频时出错:", err);
  208. });
  209. break;
  210. case "FINISH_RECORD_RIGHT":
  211. updated[4].color = "g";
  212. updated[5].color = "g";
  213. setMeasurementFinished(true);
  214. const audio4 = new Audio("/audio/end_right.mp3");
  215. // 播放音频
  216. audio4
  217. .play()
  218. .then(() => {
  219. console.log("音频开始播放");
  220. })
  221. .catch(err => {
  222. console.error("播放音频时出错:", err);
  223. });
  224. break;
  225. default:
  226. break;
  227. }
  228. return updated;
  229. });
  230. };
  231. // 处理点数据消息
  232. const handlePointReport = (pointData: TrackRecordSig["data"]) => {
  233. console.log(`pointData === ${pointData.x},${pointData.y}`);
  234. if (!isLeftFinished.current) {
  235. leftPoints.current.push(pointData);
  236. canvasRef.current?.setMeasurementDataLeft([...leftPoints.current]);
  237. } else {
  238. rightPoints.current.push(pointData);
  239. canvasRef.current?.setMeasurementDataRight([...rightPoints.current]);
  240. }
  241. };
  242. const subscription = wsClient.dataOb.subscribe((data) => {
  243. if ( data.path === "/measurement-task/get-task-state") {
  244. handleStateMessage(data.data);
  245. } else if (data.path === "/measurement-task/event") {
  246. handleEventMessage(data.data);
  247. } else if (data.path === "/measurement-task/point-report") {
  248. handlePointReport(data.data);
  249. }
  250. });
  251. wsClient.connect();
  252. return () => subscription.unsubscribe();
  253. }, []);
  254. /** ----------------------- 页面加载获取基础图形数据 ----------------------- **/
  255. useEffect(() => {
  256. getBaseRecordPointSetByCode("6001").then(res => {
  257. if (res.success) {
  258. const benchmarkShapes = JSON.parse(res.data.points) as BenchmarkShape[];
  259. if (canvasRef.current) {
  260. console.log("解析后的基础图形数据:", benchmarkShapes);
  261. canvasRef.current.setBenchmarkData(benchmarkShapes);
  262. }
  263. }
  264. });
  265. }, []);
  266. /** ----------------------- 渲染 ----------------------- **/
  267. return (
  268. <div className="flex h-full px-6">
  269. {/* 左侧区域:包含开关区域和测量画布 */}
  270. <div className="">
  271. <div className="flex gap-4 items-center px-6 pt-5">
  272. {/* 参考线开关 */}
  273. <div className="flex gap-2 items-center">
  274. <Switch defaultChecked onChange={checked => setShowGrid(checked)} />
  275. <span>线</span>
  276. </div>
  277. {/* 标准线开关 */}
  278. <div className="flex gap-2 items-center">
  279. <Switch
  280. checked={showStandard}
  281. onChange={checked => {
  282. setShowStandard(checked);
  283. if (!checked) {
  284. setAngleMarkBackup(showMark);
  285. setShowMark(false);
  286. } else {
  287. setShowMark(angleMarkBackup);
  288. }
  289. }}
  290. />
  291. <span>线</span>
  292. </div>
  293. {/* 角度线开关,仅在点击分析按钮后显示 */}
  294. {analysisClicked && (
  295. <div className="flex gap-2 items-center">
  296. <Switch
  297. checked={showMark}
  298. disabled={!showStandard}
  299. onChange={checked => {
  300. setShowMark(checked);
  301. setAngleMarkBackup(checked);
  302. }}
  303. />
  304. <span>线</span>
  305. </div>
  306. )}
  307. </div>
  308. <div className="relative m-2">
  309. <MeasurementCanvas
  310. width={800}
  311. height={600}
  312. logicalExtent={{ minX: -50, maxX: 50, minY: -20, maxY: 60 }}
  313. gridStep={1}
  314. origin={{ x: 0, y: 20 }}
  315. pixelPerMm={8}
  316. maxZoom={10}
  317. showGrid={showGrid}
  318. showBenchmark={showStandard}
  319. showAnalysis={showMark}
  320. showScale={false}
  321. scaleInterval={1}
  322. showCoordinates={showGrid}
  323. ref={canvasRef}
  324. />
  325. </div>
  326. </div>
  327. {/* 右侧区域:根据 showAnalysisTable 状态显示测量步骤或分析表格 */}
  328. <div className="min-w-[300px] flex-auto py-6 flex flex-col items-center">
  329. {showAnalysisTable && analysisReport ? (
  330. <>
  331. <header className="bg-[#e8f0ff] w-[300px] text-center text-lg font-medium py-2 text-primary border border-[#c1c6d4]">
  332. </header>
  333. <div className="analysis-table">
  334. <table
  335. style={{
  336. width: "100%",
  337. borderCollapse: "collapse",
  338. border: "1px solid #ccc",
  339. textAlign: "center",
  340. }}>
  341. <tbody>
  342. <tr style={{ height: "40px", fontSize: "18px", color: "#9E9E9E" }}>
  343. <td style={{ padding: "8px", border: "1px solid #ccc" }}>W1垂直磨耗</td>
  344. <td style={{ padding: "8px", border: "1px solid #ccc" }}>{analysisReport.w1}</td>
  345. </tr>
  346. <tr style={{ height: "40px", fontSize: "18px", color: "#9E9E9E" }}>
  347. <td style={{ padding: "8px", border: "1px solid #ccc" }}></td>
  348. <td style={{ padding: "8px", border: "1px solid #ccc" }}>
  349. {analysisReport.railHeadWidth}
  350. </td>
  351. </tr>
  352. {analysisReport.angleAnalysisList.map((item, index) => (
  353. <tr key={index} style={{ height: "40px", fontSize: "18px", color: "#9E9E9E" }}>
  354. <td style={{ padding: "8px", border: "1px solid #ccc" }}>{item.describe}</td>
  355. <td style={{ padding: "8px", border: "1px solid #ccc" }}>{item.distance}</td>
  356. </tr>
  357. ))}
  358. </tbody>
  359. </table>
  360. </div>
  361. <Button
  362. style={{ width: 200, marginTop: 18 }}
  363. size="large"
  364. type="primary"
  365. onClick={() => navigate("../config")}>
  366. </Button>
  367. </>
  368. ) : (
  369. <div>
  370. <h1 className="font-medium text-xl text-center"></h1>
  371. <div className="w-[13rem] mt-5">
  372. {statusList.map((item, index) => (
  373. <div
  374. key={index}
  375. style={{ background: item.background, borderRadius: "20px" }}
  376. className="mt-[10px] h-[40px]">
  377. <div style={{ display: "flex", lineHeight: "40px" }} className="pl-[1rem]">
  378. {renderStatusIcon(item)}
  379. <div className="pl-[5px]">{item.name}</div>
  380. </div>
  381. </div>
  382. ))}
  383. </div>
  384. <section className="flex flex-col items-center gap-4 mt-6 border-t border-[#D8D8D8] py-4">
  385. <Button style={{ width: 200 }} size="large" type="primary" onClick={onStart}>
  386. {startBtnText}
  387. </Button>
  388. <Button
  389. style={{ width: 200 }}
  390. size="large"
  391. type="primary"
  392. onClick={onAnalysisBtnClick}
  393. disabled={!measurementFinished || analysisClicked}>
  394. </Button>
  395. <Button
  396. style={{ width: 200 }}
  397. size="large"
  398. type="primary"
  399. onClick={onSaveBtnClick}
  400. disabled={!measurementFinished || saveClicked}>
  401. </Button>
  402. <Checkbox checked={afterSave} onChange={onAfterSaveChange}>
  403. </Checkbox>
  404. </section>
  405. </div>
  406. )}
  407. </div>
  408. </div>
  409. );
  410. }