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.

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