|
|
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<string>(GX_CODE) const STEP_COLOR_GREEN = "green"; const STEP_COLOR_BLUE = "blue"; const STEP_COLOR_GREY = "grey"; /** ----------------------- 引用 ----------------------- **/ 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); // 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<AnalysisReport | null>(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 <img src={Gr_round} alt="green" />; } else if (item.color === STEP_COLOR_BLUE) { 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 = 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<trackItem[]>([]) const [defaultTrackValue, setDefaultTrackValue] = useState<string>() 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<string>(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<number>(0) let [newMeasureData, setNewMeasureData] = useState<Point[]>() let [angle, setAngle] = useState<number>(1);//角度单位 分
let [distance, setDistance] = useState<number>(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 ( <> <div className="flex h-full px-6"> {/* 左侧区域:包含开关区域和测量画布 */} <div className=""> <div className="relative flex gap-4 items-center py-3"> <Select className="w-[150px]" placeholder="请选择轨型" key={defaultTrackValue} defaultValue={defaultTrackValue} onChange={onTrackChange} options={trackList.map((item) => ({ label: item.name, value: item.code, }))} ></Select> {/* <div className="absolute text-primary border border-primary rounded px-4 py-[2px] font-medium cursor-pointer hover:text-primary/[0.8]" onClick={() => navigate("../config", { replace: true })}> 返回 </div> */} <section className="ml-auto flex gap-4 items-center"> {/* 参考线开关 */} <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> {/* 角度线开关,仅在点击分析按钮后显示 */} {analysisReport && ( <div className="flex gap-2 items-center"> <Switch checked={showMark} disabled={!showStandard} onChange={checked => { setShowMark(checked); setAngleMarkBackup(checked); }} /> <span>角度线</span> </div> )} </section> </div> <Spin spinning={caloading} tip="正在校准..."> <div className="relative"> <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} showCalibration={showCalibration} ref={canvasRef} /> </div> {showCalibration && <div className="flex justify-center h-[50px]"> <div className="mt-[12px]"> 移动距离:<InputNumber defaultValue={distance} placeholder="微米" onChange={(value) => value !== null && setDistance(Number(value))}></InputNumber> <span>微米</span> </div> <img width={40} src={icon_left} onClick={()=>(onMoveLine("left"))} className="text-[20px] ml-[20px]" alt="左移"/> <img width={40} src={icon_right} onClick={()=>(onMoveLine("right"))} className="text-[20px] ml-[20px]" alt="右移"/> <img width={40} src={icon_up} onClick={()=>(onMoveLine("up"))} className="text-[20px] ml-[20px]" alt="上移"/> <img width={40} src={icon_down} onClick={()=>(onMoveLine("down"))} className="text-[20px] ml-[20px]" alt="下移"/> <img width={40} src={icon_leftR} onClick={()=>(onRotationLine("left"))} className="text-[20px] ml-[20px]" alt="逆时针旋转"/> <img width={40} src={icon_rightR} onClick={()=>(onRotationLine("right"))} className="text-[20px] ml-[20px]" alt="顺时针旋转"/> <div className="mt-[12px] ml-[20px]"> 旋转角度:<InputNumber placeholder="请输入角度" defaultValue={angle} onChange={(value) => value !== null && setAngle(Number(value))}></InputNumber> <span>分</span> </div> </div> } </Spin> </div> {/* 右侧区域:根据 showAnalysisTable 状态显示测量步骤或分析表格 */} <div className="min-w-[300px] flex-auto py-6 flex flex-col items-center"> <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}> 分析 </Button> <Button style={{ width: 200 }} size="large" type="primary" onClick={onSaveBtnClick} disabled={!measurementFinished} > 保存 </Button> <Button style={{ width: 200 }} size="large" type="primary" onClick={onCalibrationBtnClick} disabled={!measurementFinished} > 校准 </Button> <Checkbox checked={afterSave} onChange={onAfterSaveChange}> 保存后自动开始新测量 </Checkbox> </section> </div> </div> </div>
<Drawer title="分析结果" onClose={() => setOpenDrawer(false)} open={openDrawer}> {analysisReport && ( <> <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> {/* <div className="mt-5 flex justify-center"> <Button style={{ minWidth: 200 }} size="large" type="primary" onClick={onExport}> 导出 </Button> </div> */} </> )} </Drawer> </> ); }
|