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.

454 lines
20 KiB

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