|
|
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> ); }
|