import React, { useState, useEffect, useRef, useCallback, useMemo } from "react"; import { Button, Checkbox, CheckboxProps, Drawer, Input, InputNumber, message, Select, Spin, Switch } from "antd"; import { DownOutlined, UpOutlined, LeftOutlined, RightOutlined, UndoOutlined, RedoOutlined } from '@ant-design/icons'; import { useNavigate } from "react-router"; import { fetchAnalysisReport, getAlignPointsByRailSize, startMeasurement, stopMeasurement, } from "../../../services/measure/analysis"; import { getBaseRecordPointSetByCode, gx_list } from "../../../services/track/trackShape" import { createWebSocket, sharedWsUrl } from "../../../services/socket"; import { switchMeasureAfterSave } from "../../../store/features/contextSlice"; import measureState, { updateGxState, updateMeasureData } from "../../../store/measure/measureState"; import { AnalysisReport, trackItem } from "../../../services/measure/type"; import { MeasureState, TaskState, 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 icon_left from "../../../assets/icon_left.svg"; import icon_right from "../../../assets/icon_right.svg"; import icon_up from "../../../assets/icon_up.svg"; import icon_down from "../../../assets/icon_down.svg"; import icon_leftR from "../../../assets/icon_leftR.svg"; import icon_rightR from "../../../assets/icon_rightR.svg"; import MeasurementCanvas, { AnalysisData, BenchmarkShape, MeasurementCanvasRef, Point } from "./konva/MeasurementCanvas"; import "./MeasureAction.scss"; import { GX_CODE } from "../../../constant"; // 创建 websocket 客户端 const wsClient = createWebSocket(sharedWsUrl); export default function MeasureAction() { const dispatch = useAppDispatch(); const navigate = useNavigate(); const deviceInfo = useAppSelector(store => store.context.device); const measureState = useAppSelector((store) => store.measureState); const [gxCode, setGxCode] = useState(GX_CODE) const STEP_COLOR_GREEN = "green"; const STEP_COLOR_BLUE = "blue"; const STEP_COLOR_GREY = "grey"; /** ----------------------- 引用 ----------------------- **/ const canvasRef = useRef(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); // showMark的备份,记录showMark最近一次的值 const [angleMarkBackup, setAngleMarkBackup] = useState(true); const afterSave = useAppSelector(store => store.context.newMeasureAfterSave); const [startBtnText, setStartBtnText] = useState("开始测量"); const [measurementFinished, setMeasurementFinished] = useState(false); // 【分析】之后,会得到分析报告 const [analysisReport, setAnalysisReport] = useState(null); // 初始状态列表 const initialStatusList = useMemo( () => [ { name: "请移动到顶部,停顿2秒", background: "#ececec", isReady: false, color: STEP_COLOR_GREY }, { name: "开始测量左侧", background: "#ececec", isReady: false, color: STEP_COLOR_GREY }, { name: "左侧测量完成", background: "#ececec", isReady: false, color: STEP_COLOR_GREY }, { name: "请移动到顶部,停顿2秒", background: "#ececec", isReady: false, color: STEP_COLOR_GREY }, { name: "开始测量右侧", background: "#ececec", isReady: false, color: STEP_COLOR_GREY }, { name: "右侧测量完成", background: "#ececec", isReady: false, color: STEP_COLOR_GREY }, ], [] ); const [statusList, setStatusList] = useState(initialStatusList); // 打开抽屉(显示分析结果) const [openDrawer, setOpenDrawer] = useState(false); /** ----------------------- 事件处理函数 ----------------------- */ // 切换保存后自动开始新测量 const onAfterSaveChange: CheckboxProps["onChange"] = e => { dispatch(switchMeasureAfterSave(e.target.checked)); }; useEffect(()=>{ setGxCode(measureState.gxCode) }, [measureState]) // 分析按钮点击事件 const onAnalysisBtnClick = () => { if (analysisReport) { setOpenDrawer(true); return; } fetchAnalysisReport(gxCode).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); setOpenDrawer(true); } else { message.error("分析报告请求失败: " + res.data.info); } }); }; // 开始/重新测量按钮点击事件 const onStart = useCallback(() => { if (!deviceInfo.isConnected) { message.error("请先连接设备"); return; } // if(deviceInfo.power < 20){ // message.error('电量低于20%,请充电后再测量!') // return // } // 重置测量相关状态 setMeasurementFinished(false); setAnalysisReport(null); setshowCalibration(false)//校准线 dispatch(updateMeasureData([])) isLeftFinished.current = false; leftPoints.current = []; rightPoints.current = []; canvasRef.current?.clearShapes(); canvasRef.current?.resetCanvas(); startMeasurement().then(res => { if (res.status !== 0) { message.error(res.data.info); } else { let list = [...initialStatusList] list.forEach((item, index) => { if(index === 0){ item.color = STEP_COLOR_BLUE; }else{ item.color = STEP_COLOR_GREY; } }) setStatusList(list); message.success("已通知设备开始测量"); setStartBtnText("重新测量"); } }); }, [initialStatusList, startBtnText, deviceInfo]); //停止测量 const onStop = () => { stopMeasurement().then(res=>{ if (res.status !== 0) { }else{ message.error('已停止测量') } }).catch(e=>{ message.error('调用出错') }) } // 保存按钮点击事件 const onSaveBtnClick = () => { dispatch(updateMeasureData(newMeasureData)) navigate('/measure/config') //将校准的数据存入store }; //校准 const [showCalibration, setshowCalibration] = useState(false) const [caloading, setCaLoading] = useState(false) const onCalibrationBtnClick = () => { setCaLoading(true) //获取校准数据 getAlignPointsByRailSize({railSize:railSize}).then(res => { if(res.success){ setshowCalibration(true) canvasRef.current?.setMeasurementCalibrationData(res.data) }else{ message.error('校准失败!') } setCaLoading(false) }).catch(e=>{ setCaLoading(false) message.error('校准失败!') }) } // 辅助函数:渲染状态项的图标 const renderStatusIcon = (item: (typeof initialStatusList)[0]) => { if (item.color === STEP_COLOR_GREEN) { return green; } else if (item.color === STEP_COLOR_BLUE) { return blue; } else { return (
); } }; /** ----------------------- 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 = STEP_COLOR_GREEN; updated[1].color = STEP_COLOR_BLUE; 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 = STEP_COLOR_GREEN; updated[2].color = STEP_COLOR_GREEN; updated[3].color = STEP_COLOR_BLUE; 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 = STEP_COLOR_GREEN; updated[4].color = STEP_COLOR_BLUE; 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 = STEP_COLOR_GREEN; updated[5].color = STEP_COLOR_GREEN; setMeasurementFinished(true); const audio4 = new Audio("/audio/end_right.mp3"); // 播放音频 audio4 .play() .then(() => { console.log("音频开始播放"); }) .catch(err => { console.error("播放音频时出错:", err); }); break; case "WRONG_SIDE": const audio5 = new Audio("/audio/alert_left.mp3"); // 播放音频 audio5 .play() .then(() => { console.log("音频开始播放"); }) .catch(err => { console.error("播放音频时出错:", err); }); // 把状态全部置灰 updated.forEach(u => (u.color = STEP_COLOR_GREY)); // 调用停止测量 onStop(); break; default: break; } return updated; }); }; // 处理点数据消息 const handlePointReport = (pointData: TrackRecordSig["data"]) => { 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 === "/api/measurement-task/get-task-state") { handleStateMessage(data.data); } else if (data.path === "/api/measurement-task/event") { handleEventMessage(data.data); } else if (data.path === "/api/measurement-task/point-report") { handlePointReport(data.data); } }); wsClient.connect(); return () => subscription.unsubscribe(); }, [onStart]); /** ----------------------- 页面加载获取基础图形数据 -------基线---------------- **/ useEffect(() => { queryBasePoints(gxCode) //获取轨型 getTrackDataList() }, []); //获取测量基线 const queryBasePoints = (gxCode:string) => { getBaseRecordPointSetByCode(gxCode).then(res => { if (res.success) { const benchmarkShapes = JSON.parse(res.data.points) as BenchmarkShape[]; if (canvasRef.current) { console.log("解析后的基础图形数据:", benchmarkShapes); canvasRef.current.setBenchmarkData(benchmarkShapes); } } }); } /********************************轨型数据************************************* */ const [trackList, setTrackList] = useState([]) const [defaultTrackValue, setDefaultTrackValue] = useState() const getTrackDataList = () => { gx_list().then(res => { if(res.data && res.data.length){ let resData:trackItem[] = res.data; let list:trackItem[] = [] resData.map(item => { if(item.points){ list.push(item) } }) setTrackList(list) } }) } useEffect(()=>{ if(trackList && trackList.length){ setDefaultTrackValue(trackList[0].code) } },[trackList]) //当前选择的轨型 默认"GX-60" const [railSize, setRailSize] = useState(GX_CODE) const onTrackChange = (value: string) => { setRailSize(value) queryBasePoints(value) //缓存至STORE dispatch(updateGxState(value)) } //上下移动 const onMoveLine = (type:string) => { let list = canvasRef.current?.getMeasurementCalibrationData() if(list && list.length){ list.forEach(item => { if(type === 'up'){//向上移动,原数据减y X轴不动 item.y = item.y - distance/1000; } if(type === 'down'){//向上移动,原数据加y X轴不动 item.y = item.y + distance/1000; } if(type === 'left'){//向左移动,原数据减x Y轴不动 item.x = item.x - distance/1000; } if(type === 'right'){//向右移动,原数据加x Y轴不动 item.x = item.x + distance/1000; } }) canvasRef.current?.setMeasurementCalibrationData(list) setNewMeasureData(list) } } //旋转 let [measurementRotation, setMeasurementRotation] = useState(0) let [newMeasureData, setNewMeasureData] = useState() let [angle, setAngle] = useState(1);//角度单位 分 let [distance, setDistance] = useState(10) const onRotationLine = (type:string) => { let mrValue = 0 if(type === 'left'){//逆时针 mrValue = measurementRotation - (angle/60) * Math.PI / 180; } if(type === 'right'){//顺时针 mrValue = measurementRotation + (angle/60) * Math.PI / 180; } let list = canvasRef.current?.getMeasurementCalibrationData() if(list && list.length){ list.forEach((item, index) => { let cloneItem = rotatePoint(item, mrValue) item.x = cloneItem.x item.y = cloneItem.y }) canvasRef.current?.setMeasurementCalibrationData(list) setNewMeasureData(list) } } const rotatePoint = (pt:{x:number;y:number}, angle:number) => { const item = { x: pt.x * Math.cos(angle) - pt.y * Math.sin(angle), y: pt.x * Math.sin(angle) + pt.y * Math.cos(angle) }; return item } /** ----------------------- 渲染 ----------------------- **/ return ( <>
{/* 左侧区域:包含开关区域和测量画布 */}
{/*
navigate("../config", { replace: true })}> 返回
*/}
{/* 参考线开关 */}
setShowGrid(checked)} /> 参考线
{/* 标准线开关 */}
{ setShowStandard(checked); if (!checked) { setAngleMarkBackup(showMark); setShowMark(false); } else { setShowMark(angleMarkBackup); } }} /> 标准线
{/* 角度线开关,仅在点击分析按钮后显示 */} {analysisReport && (
{ setShowMark(checked); setAngleMarkBackup(checked); }} /> 角度线
)}
{showCalibration &&
移动距离: value !== null && setDistance(Number(value))}> 微米
(onMoveLine("left"))} className="text-[20px] ml-[20px]" alt="左移"/> (onMoveLine("right"))} className="text-[20px] ml-[20px]" alt="右移"/> (onMoveLine("up"))} className="text-[20px] ml-[20px]" alt="上移"/> (onMoveLine("down"))} className="text-[20px] ml-[20px]" alt="下移"/> (onRotationLine("left"))} className="text-[20px] ml-[20px]" alt="逆时针旋转"/> (onRotationLine("right"))} className="text-[20px] ml-[20px]" alt="顺时针旋转"/>
旋转角度: value !== null && setAngle(Number(value))}>
}
{/* 右侧区域:根据 showAnalysisTable 状态显示测量步骤或分析表格 */}

测量步骤

{statusList.map((item, index) => (
{renderStatusIcon(item)}
{item.name}
))}
保存后自动开始新测量
setOpenDrawer(false)} open={openDrawer}> {analysisReport && ( <>
{analysisReport.angleAnalysisList.map((item, index) => ( ))}
W1垂直磨耗 {analysisReport.w1}
轨头宽度 {analysisReport.railHeadWidth}
{item.describe} {item.distance}
{/*
*/} )}
); }