9 changed files with 1366 additions and 416 deletions
-
90package-lock.json
-
4package.json
-
18src/components/SideMenu.tsx
-
72src/index.tsx
-
745src/pages/measure/components/MeasureAction.tsx
-
18src/pages/measure/components/MeasureConfig.tsx
-
651src/pages/measure/components/konva/MeasurementCanvas.tsx
-
122src/services/measure/analysis.ts
-
62src/services/measure/type.ts
@ -1,335 +1,454 @@ |
|||
import { Button, Checkbox, CheckboxProps, Switch, message } from "antd"; |
|||
import { useState, useEffect } from "react"; |
|||
import React from 'react'; |
|||
import { Button, Checkbox, CheckboxProps, message, Switch } from "antd"; |
|||
import { useEffect, useRef, useState } from "react"; |
|||
import { useNavigate } from "react-router"; |
|||
import { analyzeMeasurement, saveMeasurement, startMeasurement, analysisReport } from "../../../services/measure/analysis"; |
|||
import { |
|||
fetchAnalysisReport, |
|||
getBaseRecordPointSetByCode, |
|||
saveMeasurement, |
|||
startMeasurement, |
|||
} from "../../../services/measure/analysis"; |
|||
import { createWebSocket, sharedWsUrl } from "../../../services/socket"; |
|||
import GridLayer from "./graph/GridLayer"; |
|||
import StandardLayer from "./graph/StandardLayer"; |
|||
import ResultLayer from "./graph/ResultLayer"; |
|||
import MarkLayer from "./graph/MarkLayer"; |
|||
import { switchMeasureAfterSave } from "../../../store/features/contextSlice"; |
|||
import { AnalyzeAngle } from "../../../services/measure/type"; |
|||
import { AnalysisReport, AnalyzeAngle } from "../../../services/measure/type"; |
|||
import { MeasureState, taskStatusDescMap } 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 Gr_round from '../../../assets/green_round.svg'; |
|||
import Bl_round from '../../../assets/blue_round.svg'; |
|||
import MeasurementCanvas, { |
|||
AnalysisData, |
|||
BenchmarkShape, |
|||
MeasurementCanvasRef, |
|||
} from "./konva/MeasurementCanvas"; |
|||
|
|||
const wsClient = createWebSocket(sharedWsUrl); |
|||
|
|||
export default function MeasureAction() { |
|||
const dispatch = useAppDispatch(); |
|||
const dispatch = useAppDispatch(); |
|||
const navigate = useNavigate(); |
|||
|
|||
// MeasurementCanvas 的 ref
|
|||
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 [showGrid, setShowGrid] = useState(true); |
|||
const [showStandard, setShowStandard] = useState(true); |
|||
const [showResult, setShowResult] = useState(true); |
|||
const [showMark, setShowMark] = useState(true); |
|||
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 afterSave = useAppSelector(store => store.context.newMeasureAfterSave); |
|||
const onAfterSaveChange: CheckboxProps["onChange"] = (e) => { |
|||
dispatch(switchMeasureAfterSave(e.target.checked)); |
|||
}; |
|||
|
|||
const navigate = useNavigate(); |
|||
const onAnalysisBtnClick = () => { |
|||
// 分析按钮只允许点击一次
|
|||
setAnalysisClicked(true); |
|||
fetchAnalysisReport("6001").then((res) => { |
|||
if (res.success) { |
|||
const report: AnalysisReport = res.data; |
|||
console.log(res.data); |
|||
// 更新 canvas 分析数据(如有需要)
|
|||
if (report && report.angleAnalysisList) { |
|||
const analysisData: AnalysisData[] = report.angleAnalysisList.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 与 measure 分别设置为 pointA 与 pointB
|
|||
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 [angles, setAngles] = useState<AnalyzeAngle[]>([]); |
|||
const [taskStatus, setTaskStatus] = useState<MeasureState["data"]["taskStatus"]>("IDLE"); |
|||
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); |
|||
let name = taskStatusDescMap["IDLE"]; |
|||
setTaskStatusName(name); |
|||
} else { |
|||
const newStatusList = [...initialStatusList]; |
|||
newStatusList[0].color = "b"; |
|||
setStatusList(newStatusList); |
|||
message.success("已通知设备开始测量"); |
|||
let name = taskStatusDescMap["IDLE"]; |
|||
setTaskStatusName(name); |
|||
// 测量启动成功后,按钮文本变为“重新测量”
|
|||
setStartBtnText("重新测量"); |
|||
} |
|||
}); |
|||
}; |
|||
|
|||
const onAfterSaveChange: CheckboxProps["onChange"] = e => { |
|||
dispatch(switchMeasureAfterSave(e.target.checked)); |
|||
}; |
|||
const onAnalysisBtnClick = () => { |
|||
// navigate("../detail");
|
|||
// if(taskStatus !== 'FINISHED'){
|
|||
// message.error('测量还未结束')
|
|||
// return;
|
|||
// }
|
|||
const params = {//静态数据 TODO
|
|||
code: 6001 |
|||
} |
|||
analysisReport(params).then(res => { |
|||
console.log('res===', res) |
|||
if (res.success) { |
|||
const angleAnalysisList = res.data.angleAnalysisList; |
|||
let angles:any = [] |
|||
angleAnalysisList && angleAnalysisList.map(item => { |
|||
const pointA = item.pointA; |
|||
angles.push({ |
|||
x: pointA.x, |
|||
y: pointA.y, |
|||
degree: item.describe, |
|||
describe: item.describe |
|||
}) |
|||
}) |
|||
setAngles(angles); |
|||
}else{ |
|||
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("新测量"); |
|||
} |
|||
} |
|||
}); |
|||
}; |
|||
|
|||
} |
|||
}) |
|||
// analyzeMeasurement().then(res => {
|
|||
// if (res.success) {
|
|||
// setAngles(res.data.angles);
|
|||
// } else {
|
|||
// message.error(res.data.info);
|
|||
// }
|
|||
// });
|
|||
}; |
|||
const [taskStatusName, setTaskStatusName] = useState(""); |
|||
useEffect(() => { |
|||
const subscription = wsClient.dataOb.subscribe((data) => { |
|||
// 处理任务状态消息
|
|||
if (data.messageType === "STATE" && data.path === "/measurement-task/get-task-state") { |
|||
if (!data.data) return; |
|||
if (data.data.taskStatus === "IDLE") { |
|||
setTaskStatusName("空闲"); |
|||
} else if (!data.data.isMeasuringLeftEnd) { |
|||
setTaskStatusName("左侧正在测量"); |
|||
statusList[0].isReady = true; |
|||
setStatusList([...statusList]); |
|||
} else if (data.data.isMeasuringLeftEnd && !data.data.isMeasuringRightEnd) { |
|||
setTaskStatusName("右侧正在测量"); |
|||
} else { |
|||
let name = taskStatusDescMap[data.data.taskStatus]; |
|||
setTaskStatusName(name); |
|||
} |
|||
setTaskStatus(data.data.taskStatus); |
|||
} |
|||
|
|||
const onStart = () => { |
|||
startMeasurement().then(res => { |
|||
if (res.status !== 0) { |
|||
message.error(res.data.info); |
|||
let name = taskStatusDescMap['IDLE'] |
|||
setTaskStatusName(name) |
|||
} else { |
|||
statusList[0].color = 'b'; |
|||
setStatusList(statusList) |
|||
message.success("已通知设备开始测量"); |
|||
let name = taskStatusDescMap['IDLE'] |
|||
setTaskStatusName(name) |
|||
} |
|||
}); |
|||
}; |
|||
// 处理状态变化事件
|
|||
if (data.messageType === "EVENT" && data.path === "/measurement-task/event") { |
|||
if (data.data === "START_RECORD_LEFT") { |
|||
statusList[0].color = "g"; |
|||
statusList[1].color = "b"; |
|||
} else if (data.data === "FINISH_RECORD_LEFT") { |
|||
statusList[1].color = "g"; |
|||
statusList[2].color = "g"; |
|||
statusList[3].color = "b"; |
|||
// 左侧测量结束后,切换到右侧数据累计
|
|||
isLeftFinished.current = true; |
|||
} else if (data.data === "START_RECORD_RIGHT") { |
|||
statusList[3].color = "g"; |
|||
statusList[4].color = "b"; |
|||
} else if (data.data === "FINISH_RECORD_RIGHT") { |
|||
statusList[4].color = "g"; |
|||
statusList[5].color = "g"; |
|||
// 接收到 FINISH_RECORD_RIGHT 后认为测量完成
|
|||
setMeasurementFinished(true); |
|||
} |
|||
setStatusList([...statusList]); |
|||
} |
|||
|
|||
const onSaveBtnClick = () => { |
|||
saveMeasurement().then(res => { |
|||
if (res.status !== 0) { |
|||
message.error(res.data.info); |
|||
} else { |
|||
message.success("保存成功"); |
|||
if (afterSave) { |
|||
navigate("../config"); |
|||
} |
|||
} |
|||
}); |
|||
}; |
|||
if (data.messageType === "STATE" && (data as any).path === "/measurement-task/point-report") { |
|||
const pointData = ((data as unknown) as { data: { x: number; y: number } }).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]); |
|||
} |
|||
} |
|||
}); |
|||
wsClient.connect(); |
|||
return () => subscription.unsubscribe(); |
|||
}, [statusList]); |
|||
|
|||
let [taskStatusName, setTaskStatusName] = useState('') |
|||
useEffect(() => { |
|||
const subscription = wsClient.dataOb.subscribe(data => { |
|||
if (data.messageType === "STATE" && data.path === "/measurement-task/get-task-state") { |
|||
if(!data.data)return; |
|||
if(data.data.isMeasuringLeftEnd && !data.data.isMeasuringRightEnd){ |
|||
console.log('这是右侧的状态===', data.data.isMeasuringRightEnd) |
|||
} |
|||
if(data.data.taskStatus === 'IDLE'){ |
|||
setTaskStatusName('空闲') |
|||
}else if(!data.data.isMeasuringLeftEnd){//正在测量左边
|
|||
setTaskStatusName('左侧正在测量') |
|||
statusList[0].isReady = true; |
|||
setStatusList(statusList) |
|||
}else if(data.data.isMeasuringLeftEnd && !data.data.isMeasuringRightEnd){//左边为true, 右边是false时表示左边已完成,右边准备
|
|||
setTaskStatusName('右侧正在测量') |
|||
}else{ |
|||
let name = taskStatusDescMap[data.data.taskStatus] |
|||
setTaskStatusName(name) |
|||
} |
|||
setTaskStatus(data.data.taskStatus) |
|||
} |
|||
if (data.messageType === "EVENT" && data.path === "/measurement-task/event") { |
|||
if(data.data === "START_RECORD_LEFT"){ |
|||
statusList[0].color = 'g'; |
|||
statusList[1].color = 'b'; |
|||
}else if(data.data === "FINISH_RECORD_LEFT"){ |
|||
statusList[1].color = 'g'; |
|||
statusList[2].color = 'g'; |
|||
statusList[3].color = 'b'; |
|||
}else if(data.data === "START_RECORD_RIGHT"){ |
|||
statusList[3].color = 'g'; |
|||
statusList[4].color = 'b'; |
|||
}else if(data.data === "FINISH_RECORD_RIGHT"){ |
|||
statusList[4].color = 'g'; |
|||
statusList[5].color = 'g'; |
|||
} |
|||
setStatusList(statusList) |
|||
} |
|||
}); |
|||
wsClient.connect(); |
|||
return () => subscription.unsubscribe(); |
|||
}); |
|||
// 页面加载时获取基础图形数据,并传入 MeasurementCanvas
|
|||
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); |
|||
} |
|||
} |
|||
}); |
|||
}, []); |
|||
|
|||
type StatusCodeData = { |
|||
statusCode: string; |
|||
name: string; |
|||
background: string; |
|||
isReady:boolean; |
|||
color: string; |
|||
} |
|||
const onHandleChangeStatus = (item:StatusCodeData) => { |
|||
let backgroundColor = '' |
|||
if(item.statusCode === 'START_RECORD_LEFT'){ |
|||
backgroundColor = item.background |
|||
} |
|||
return backgroundColor; |
|||
} |
|||
type StatusCodeData = { |
|||
statusCode: string; |
|||
name: string; |
|||
background: string; |
|||
isReady: boolean; |
|||
color: string; |
|||
}; |
|||
|
|||
const onHandleIcon =(item:StatusCodeData, index:number)=>{ |
|||
if(item.color === 'g'){ |
|||
return <img src={Gr_round} alt=''/> |
|||
}else if(item.color === 'b'){//index > 0 && statusList[index-1].isReady
|
|||
return <img src={Bl_round} alt=''/> |
|||
}else if(item.color === 'h'){ |
|||
return <div style={{width:'22px',height:'22px',background:'#c0c0c0',borderRadius:"50%",marginTop:'10px'}}></div> |
|||
} |
|||
} |
|||
const onHandleChangeStatus = (item: StatusCodeData) => { |
|||
let backgroundColor = ""; |
|||
if (item.statusCode === "START_RECORD_LEFT") { |
|||
backgroundColor = item.background; |
|||
} |
|||
return backgroundColor; |
|||
}; |
|||
|
|||
let [statusList,setStatusList] = useState([{ |
|||
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 onHandleIcon = (item: StatusCodeData, index: number) => { |
|||
if (item.color === "g") { |
|||
return <img src={Gr_round} alt="" />; |
|||
} else if (item.color === "b") { |
|||
return <img src={Bl_round} alt="" />; |
|||
} else if (item.color === "h") { |
|||
return ( |
|||
<div |
|||
style={{ |
|||
width: "22px", |
|||
height: "22px", |
|||
background: "#c0c0c0", |
|||
borderRadius: "50%", |
|||
marginTop: "10px", |
|||
}} |
|||
></div> |
|||
); |
|||
} |
|||
}; |
|||
|
|||
return ( |
|||
<div className="flex h-full "> |
|||
<div className="flex-none"> |
|||
<div className="flex gap-4 items-center px-6 pt-2"> |
|||
<div className="flex gap-2 items-center"> |
|||
<Switch defaultChecked onChange={checked => setShowGrid(checked)} /> |
|||
<span>参考线</span> |
|||
</div> |
|||
<div className="flex gap-2 items-center"> |
|||
<Switch defaultChecked onChange={checked => setShowStandard(checked)} /> |
|||
<span>标准线</span> |
|||
</div> |
|||
<div className="flex gap-2 items-center"> |
|||
<Switch defaultChecked onChange={checked => setShowResult(checked)} /> |
|||
<span>对比线</span> |
|||
</div> |
|||
{angles.length > 0 && ( |
|||
<div className="flex gap-2 items-center"> |
|||
<Switch defaultChecked onChange={checked => setShowMark(checked)} /> |
|||
<span>角度线</span> |
|||
</div> |
|||
)} |
|||
</div> |
|||
<div className="relative"> |
|||
<GridLayer |
|||
width={840} |
|||
height={600} |
|||
leftPadding={30} |
|||
rightPadding={10} |
|||
topPadding={10} |
|||
bottomPadding={30} |
|||
columns={10} |
|||
rows={7} |
|||
colCellNum={1} |
|||
rowCellNum={2} |
|||
visibility={showGrid ? "visible" : "hidden"} |
|||
/> |
|||
<div className="absolute top-0"> |
|||
<StandardLayer |
|||
width={840} |
|||
height={600} |
|||
leftPadding={30} |
|||
rightPadding={10} |
|||
topPadding={10} |
|||
bottomPadding={30} |
|||
columns={10} |
|||
rows={7} |
|||
visibility={showStandard ? "visible" : "hidden"} |
|||
/> |
|||
</div> |
|||
<div className="absolute top-0"> |
|||
<ResultLayer |
|||
width={840} |
|||
height={600} |
|||
leftPadding={30} |
|||
rightPadding={10} |
|||
topPadding={10} |
|||
bottomPadding={30} |
|||
columns={10} |
|||
rows={7} |
|||
visibility={showResult ? "visible" : "hidden"} |
|||
/> |
|||
</div> |
|||
{angles.length > 0 && ( |
|||
<div className="absolute top-0"> |
|||
<MarkLayer |
|||
width={840} |
|||
height={600} |
|||
leftPadding={30} |
|||
rightPadding={10} |
|||
topPadding={10} |
|||
bottomPadding={30} |
|||
columns={10} |
|||
rows={7} |
|||
visibility={showMark ? "visible" : "hidden"} |
|||
angles={angles} |
|||
/> |
|||
</div> |
|||
)} |
|||
</div> |
|||
</div> |
|||
<div className="w-[300px] flex-none py-6"> |
|||
<h1 className="font-medium text-xl text-center">测量步骤</h1> |
|||
<section className="flex flex-col items-center gap-4 mt-6 border-t border-[#D8D8D8] py-4"> |
|||
{/* <div>测量状态: {taskStatusDescMap[taskStatus]}</div> */} |
|||
{/* <div className="w-[200px]"> |
|||
{taskStatus !== 'FINISHED' && |
|||
<div className="flex"> |
|||
<img src={Gr_round} alt=''/> |
|||
<div className="ml-[2rem]">{taskStatusName}</div> |
|||
</div> |
|||
} |
|||
{ |
|||
taskStatus === 'FINISHED' && |
|||
<div className="flex mt-[10px]"> |
|||
<img src={Bl_round} alt=''/> |
|||
<div className="ml-[2rem]">{taskStatusName}</div> |
|||
</div> |
|||
} |
|||
</div> */} |
|||
<Button style={{ width: 200 }} size="large" type="primary" onClick={onStart}> |
|||
开始测量 |
|||
</Button> |
|||
<Button style={{ width: 200 }} size="large" type="primary" onClick={onAnalysisBtnClick}> |
|||
分析 |
|||
</Button> |
|||
<Button style={{ width: 200 }} size="large" type="primary" onClick={onSaveBtnClick}> |
|||
保存 |
|||
</Button> |
|||
<Checkbox checked={afterSave} onChange={onAfterSaveChange}> |
|||
保存后自动开始新测量 |
|||
</Checkbox> |
|||
</section> |
|||
<div className="ml-[45px] w-[13rem]"> |
|||
{statusList && statusList.map((item,index) => { |
|||
return <div style={{background:onHandleChangeStatus(item),borderRadius:"20px"}} className="mt-[5px] h-[40px]"> |
|||
<div style={{display:'flex',lineHeight:'40px'}} className="pl-[1rem]"> |
|||
{onHandleIcon(item, index)} |
|||
<div className="pl-[5px]">{item.name}</div> |
|||
</div> |
|||
</div> |
|||
})} |
|||
</div> |
|||
</div> |
|||
</div> |
|||
); |
|||
return ( |
|||
<div className="flex h-full"> |
|||
{/* 左侧区域:包含开关区域和测量画布 */} |
|||
<div className="flex-none"> |
|||
<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> |
|||
<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={true} |
|||
ref={canvasRef} |
|||
/> |
|||
</div> |
|||
</div> |
|||
{/* 右侧区域:根据 showAnalysisTable 状态决定显示测量步骤区域还是分析表格 */} |
|||
<div className="w-[300px] flex-none py-6"> |
|||
{showAnalysisTable && 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> |
|||
))} |
|||
<tr style={{ height: "40px", fontSize: "18px", color: "#9E9E9E" }}> |
|||
<td |
|||
colSpan={2} |
|||
style={{ textAlign: "center", padding: "8px", border: "1px solid #ccc" }} |
|||
> |
|||
<Button style={{ width: 200 }} size="large" type="primary" onClick={() => navigate("../config")}> |
|||
返回 |
|||
</Button> |
|||
</td> |
|||
</tr> |
|||
</tbody> |
|||
</table> |
|||
</div> |
|||
) : ( |
|||
<div> |
|||
<h1 className="font-medium text-xl text-center">测量步骤</h1> |
|||
<div className="ml-[45px] w-[13rem] mt-5"> |
|||
{statusList.map((item, index) => { |
|||
return ( |
|||
<div |
|||
key={index} |
|||
style={{ background: onHandleChangeStatus(item), borderRadius: "20px" }} |
|||
className="mt-[10px] h-[40px]" |
|||
> |
|||
<div style={{ display: "flex", lineHeight: "40px" }} className="pl-[1rem]"> |
|||
{onHandleIcon(item, index)} |
|||
<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> |
|||
); |
|||
} |
@ -0,0 +1,651 @@ |
|||
import React, { |
|||
useState, |
|||
useRef, |
|||
useImperativeHandle, |
|||
forwardRef, |
|||
useEffect, |
|||
} from "react"; |
|||
import { Stage, Layer, Line, Shape, Text } from "react-konva"; |
|||
|
|||
// 数据类型定义
|
|||
export interface Point { |
|||
x: number; |
|||
y: number; |
|||
} |
|||
|
|||
export interface BenchmarkArc { |
|||
type: "arc"; |
|||
start: Point; |
|||
end: Point; |
|||
radius: number; |
|||
color: string; |
|||
side: "right" | "left" | "up" | "down"; |
|||
} |
|||
|
|||
export interface BenchmarkLine { |
|||
type: "line"; |
|||
start: Point; |
|||
end: Point; |
|||
color: string; |
|||
} |
|||
|
|||
export type BenchmarkShape = BenchmarkArc | BenchmarkLine; |
|||
|
|||
export interface AnalysisData { |
|||
pointA: Point; |
|||
pointB: Point; |
|||
describe: string; |
|||
} |
|||
|
|||
// 逻辑坐标范围(单位:毫米)
|
|||
export interface LogicalExtent { |
|||
minX: number; |
|||
maxX: number; |
|||
minY: number; |
|||
maxY: number; |
|||
} |
|||
|
|||
export interface MeasurementCanvasProps { |
|||
width: number; |
|||
height: number; |
|||
logicalExtent?: LogicalExtent; |
|||
gridStep?: number; |
|||
showGrid?: boolean; |
|||
showScale?: boolean; |
|||
scaleInterval?: number; |
|||
showCoordinates?: boolean; |
|||
coordinateInterval?: number; |
|||
pixelPerMm?: number; |
|||
origin?: Point; |
|||
minZoom?: number; |
|||
maxZoom?: number; |
|||
initialBenchmarkData?: BenchmarkShape[]; |
|||
initialMeasurementDataLeft?: Point[]; |
|||
initialMeasurementDataRight?: Point[]; |
|||
initialAnalysisData?: AnalysisData[]; |
|||
// 新增属性:控制是否显示标准线(benchmark shapes)
|
|||
showBenchmark?: boolean; |
|||
// 新增属性:控制是否显示分析线
|
|||
showAnalysis?: boolean; |
|||
} |
|||
|
|||
export interface MeasurementCanvasRef { |
|||
resetCanvas: () => void; |
|||
clearShapes: () => void; |
|||
setBenchmarkData: (data: BenchmarkShape[]) => void; |
|||
setMeasurementDataLeft: (data: Point[]) => void; |
|||
setMeasurementDataRight: (data: Point[]) => void; |
|||
setMeasurementData: (data: Point[]) => void; |
|||
setAnalysisData: (data: AnalysisData[]) => void; |
|||
redraw: () => void; |
|||
} |
|||
|
|||
const MeasurementCanvas = forwardRef<MeasurementCanvasRef, MeasurementCanvasProps>( |
|||
(props, ref) => { |
|||
const { |
|||
width, |
|||
height, |
|||
logicalExtent = { minX: -100, maxX: 100, minY: -100, maxY: 100 }, |
|||
gridStep = 1, |
|||
showGrid = true, |
|||
showScale = false, |
|||
scaleInterval = 10, |
|||
showCoordinates = false, |
|||
coordinateInterval = 1, |
|||
pixelPerMm = 10, |
|||
origin = { x: 0, y: 0 }, |
|||
minZoom = 1, |
|||
maxZoom = 10, |
|||
initialBenchmarkData = [], |
|||
initialMeasurementDataLeft = [], |
|||
initialMeasurementDataRight = [], |
|||
initialAnalysisData = [], |
|||
showBenchmark = true, // 默认显示标准线
|
|||
showAnalysis = true, // 默认显示分析线
|
|||
} = props; |
|||
|
|||
// Stage 物理中心(像素)
|
|||
const canvasCenter = { x: width / 2, y: height / 2 }; |
|||
|
|||
// 当 logicalExtent 范围较小时,自动计算一个 scale 使其铺满整个 Stage
|
|||
const logicalWidth = logicalExtent.maxX - logicalExtent.minX; |
|||
const logicalHeight = logicalExtent.maxY - logicalExtent.minY; |
|||
const computedScale = Math.min(width / logicalWidth, height / logicalHeight); |
|||
const initialScale = computedScale > pixelPerMm ? computedScale : pixelPerMm; |
|||
|
|||
const logicalCenter = { |
|||
x: (logicalExtent.minX + logicalExtent.maxX) / 2, |
|||
y: (logicalExtent.minY + logicalExtent.maxY) / 2, |
|||
}; |
|||
const initialOffset = |
|||
computedScale > pixelPerMm |
|||
? { |
|||
x: -(logicalCenter.x - origin.x) * initialScale, |
|||
y: -(logicalCenter.y - origin.y) * initialScale, |
|||
} |
|||
: { x: 0, y: 0 }; |
|||
|
|||
const [offset, setOffset] = useState<{ x: number; y: number }>(initialOffset); |
|||
const [scale, setScale] = useState<number>(initialScale); |
|||
const [benchmarkData, setBenchmarkData] = |
|||
useState<BenchmarkShape[]>(initialBenchmarkData); |
|||
const [analysisData, setAnalysisData] = |
|||
useState<AnalysisData[]>(initialAnalysisData); |
|||
|
|||
// 左右测量数据使用定时器更新
|
|||
const leftPointsRef = useRef<Point[]>([...initialMeasurementDataLeft]); |
|||
const rightPointsRef = useRef<Point[]>([...initialMeasurementDataRight]); |
|||
|
|||
const [measurementDataLeft, setMeasurementDataLeftState] = useState<Point[]>( |
|||
initialMeasurementDataLeft |
|||
); |
|||
const [measurementDataRight, setMeasurementDataRightState] = useState<Point[]>( |
|||
initialMeasurementDataRight |
|||
); |
|||
|
|||
// 新增直接绘制的测量数据状态,不使用定时器
|
|||
const [measurementData, setMeasurementDataState] = useState<Point[]>([]); |
|||
|
|||
const refreshInterval = 50; |
|||
const refreshTimer = useRef<number | null>(null); |
|||
|
|||
useEffect(() => { |
|||
if (!refreshTimer.current) { |
|||
refreshTimer.current = window.setInterval(() => { |
|||
setMeasurementDataLeftState([...leftPointsRef.current]); |
|||
setMeasurementDataRightState([...rightPointsRef.current]); |
|||
}, refreshInterval); |
|||
} |
|||
return () => { |
|||
if (refreshTimer.current) { |
|||
clearInterval(refreshTimer.current); |
|||
refreshTimer.current = null; |
|||
} |
|||
}; |
|||
}, []); |
|||
|
|||
useImperativeHandle(ref, () => ({ |
|||
resetCanvas: () => { |
|||
setScale(pixelPerMm); |
|||
setOffset({ x: 0, y: 0 }); |
|||
}, |
|||
clearShapes: () => { |
|||
leftPointsRef.current = []; |
|||
rightPointsRef.current = []; |
|||
setMeasurementDataLeftState([]); |
|||
setMeasurementDataRightState([]); |
|||
setAnalysisData([]); |
|||
setMeasurementDataState([]); |
|||
}, |
|||
setBenchmarkData: (data: BenchmarkShape[]) => { |
|||
setBenchmarkData(data); |
|||
}, |
|||
setMeasurementDataLeft: (data: Point[]) => { |
|||
leftPointsRef.current = data; |
|||
}, |
|||
setMeasurementDataRight: (data: Point[]) => { |
|||
rightPointsRef.current = data; |
|||
}, |
|||
setMeasurementData: (data: Point[]) => { |
|||
setMeasurementDataState(data); |
|||
}, |
|||
setAnalysisData: (data: AnalysisData[]) => { |
|||
setAnalysisData(data); |
|||
}, |
|||
redraw: () => { |
|||
setScale((prev) => prev); |
|||
}, |
|||
})); |
|||
|
|||
const stageRef = useRef<any>(null); |
|||
|
|||
const transform = (pt: Point) => ({ |
|||
x: canvasCenter.x + offset.x + (pt.x - origin.x) * scale, |
|||
y: canvasCenter.y + offset.y + (pt.y - origin.y) * scale, |
|||
}); |
|||
|
|||
const clampOffset = (newOffset: { x: number; y: number }, currentScale: number) => { |
|||
const left = canvasCenter.x + newOffset.x + (logicalExtent.minX - origin.x) * currentScale; |
|||
const right = canvasCenter.x + newOffset.x + (logicalExtent.maxX - origin.x) * currentScale; |
|||
const top = canvasCenter.y + newOffset.y + (logicalExtent.minY - origin.y) * currentScale; |
|||
const bottom = canvasCenter.y + newOffset.y + (logicalExtent.maxY - origin.y) * currentScale; |
|||
let clampedX = newOffset.x; |
|||
let clampedY = newOffset.y; |
|||
if (left > 0) clampedX -= left; |
|||
if (right < width) clampedX += width - right; |
|||
if (top > 0) clampedY -= top; |
|||
if (bottom < height) clampedY += height - bottom; |
|||
return { x: clampedX, y: clampedY }; |
|||
}; |
|||
|
|||
const isDragging = useRef(false); |
|||
const lastPos = useRef<{ x: number; y: number } | null>(null); |
|||
const dragFrame = useRef<number | null>(null); |
|||
|
|||
const handleMouseDown = () => { |
|||
isDragging.current = true; |
|||
lastPos.current = stageRef.current.getPointerPosition(); |
|||
}; |
|||
|
|||
const handleMouseMove = () => { |
|||
if (!isDragging.current) return; |
|||
const currentPos = stageRef.current.getPointerPosition(); |
|||
if (lastPos.current && currentPos) { |
|||
if (!dragFrame.current) { |
|||
dragFrame.current = requestAnimationFrame(() => { |
|||
const last = lastPos.current!; |
|||
const newOff = { |
|||
x: offset.x + currentPos.x - last.x, |
|||
y: offset.y + currentPos.y - last.y, |
|||
}; |
|||
setOffset(clampOffset(newOff, scale)); |
|||
lastPos.current = currentPos; |
|||
dragFrame.current = null; |
|||
}); |
|||
} |
|||
} |
|||
}; |
|||
|
|||
const handleMouseUp = () => { |
|||
isDragging.current = false; |
|||
if (dragFrame.current) { |
|||
cancelAnimationFrame(dragFrame.current); |
|||
dragFrame.current = null; |
|||
} |
|||
}; |
|||
|
|||
const handleMouseLeave = () => { |
|||
isDragging.current = false; |
|||
if (dragFrame.current) { |
|||
cancelAnimationFrame(dragFrame.current); |
|||
dragFrame.current = null; |
|||
} |
|||
}; |
|||
|
|||
const handleWheel = (e: any) => { |
|||
e.evt.preventDefault(); |
|||
const oldScale = scale; |
|||
const pointer = stageRef.current.getPointerPosition(); |
|||
const L = { |
|||
x: origin.x + (pointer.x - (canvasCenter.x + offset.x)) / oldScale, |
|||
y: origin.y + (pointer.y - (canvasCenter.y + offset.y)) / oldScale, |
|||
}; |
|||
let newScale = e.evt.deltaY < 0 ? oldScale * 1.1 : oldScale / 1.1; |
|||
if (newScale < minZoom * pixelPerMm) newScale = minZoom * pixelPerMm; |
|||
if (newScale > maxZoom * pixelPerMm) newScale = maxZoom * pixelPerMm; |
|||
const newOffset = { |
|||
x: pointer.x - canvasCenter.x - (L.x - origin.x) * newScale, |
|||
y: pointer.y - canvasCenter.y - (L.y - origin.y) * newScale, |
|||
}; |
|||
setScale(newScale); |
|||
setOffset(clampOffset(newOffset, newScale)); |
|||
}; |
|||
|
|||
const lastTouchDistance = useRef<number | null>(null); |
|||
const handleTouchStart = (e: any) => { |
|||
const touches = e.evt.touches; |
|||
if (touches && touches.length === 2) { |
|||
e.evt.preventDefault(); |
|||
const [t1, t2] = touches; |
|||
const dist = Math.hypot(t2.clientX - t1.clientX, t2.clientY - t1.clientY); |
|||
lastTouchDistance.current = dist; |
|||
} else if (touches && touches.length === 1) { |
|||
isDragging.current = true; |
|||
lastPos.current = { x: touches[0].clientX, y: touches[0].clientY }; |
|||
} |
|||
}; |
|||
|
|||
const handleTouchMove = (e: any) => { |
|||
const touches = e.evt.touches; |
|||
if (touches && touches.length === 2) { |
|||
e.evt.preventDefault(); |
|||
const [t1, t2] = touches; |
|||
const dist = Math.hypot(t2.clientX - t1.clientX, t2.clientY - t1.clientY); |
|||
if (lastTouchDistance.current) { |
|||
const delta = dist / lastTouchDistance.current; |
|||
let newScale = scale * delta; |
|||
if (newScale < minZoom * pixelPerMm) newScale = minZoom * pixelPerMm; |
|||
if (newScale > maxZoom * pixelPerMm) newScale = maxZoom * pixelPerMm; |
|||
const center = { |
|||
x: (t1.clientX + t2.clientX) / 2, |
|||
y: (t1.clientY + t2.clientY) / 2, |
|||
}; |
|||
const L = { |
|||
x: origin.x + (center.x - (canvasCenter.x + offset.x)) / scale, |
|||
y: origin.y + (center.y - (canvasCenter.y + offset.y)) / scale, |
|||
}; |
|||
const newOffset = { |
|||
x: center.x - canvasCenter.x - (L.x - origin.x) * newScale, |
|||
y: center.y - canvasCenter.y - (L.y - origin.y) * newScale, |
|||
}; |
|||
setScale(newScale); |
|||
setOffset(clampOffset(newOffset, newScale)); |
|||
lastTouchDistance.current = dist; |
|||
} |
|||
} else if (touches && touches.length === 1 && isDragging.current) { |
|||
const t = touches[0]; |
|||
const pos = { x: t.clientX, y: t.clientY }; |
|||
if (lastPos.current) { |
|||
setOffset((prev) => |
|||
clampOffset( |
|||
{ |
|||
x: prev.x + pos.x - lastPos.current!.x, |
|||
y: prev.y + pos.y - lastPos.current!.y, |
|||
}, |
|||
scale |
|||
) |
|||
); |
|||
} |
|||
lastPos.current = pos; |
|||
} |
|||
}; |
|||
|
|||
const handleTouchEnd = () => { |
|||
isDragging.current = false; |
|||
lastTouchDistance.current = null; |
|||
lastPos.current = null; |
|||
}; |
|||
|
|||
const renderGridAndAxes = () => { |
|||
const lines = []; |
|||
for (let x = logicalExtent.minX; x <= logicalExtent.maxX; x += gridStep) { |
|||
const p1 = transform({ x, y: logicalExtent.minY }); |
|||
const p2 = transform({ x, y: logicalExtent.maxY }); |
|||
lines.push( |
|||
<Line |
|||
key={`v-${x}`} |
|||
points={[p1.x, p1.y, p2.x, p2.y]} |
|||
stroke="#eee" |
|||
strokeWidth={1} |
|||
/> |
|||
); |
|||
} |
|||
for (let y = logicalExtent.minY; y <= logicalExtent.maxY; y += gridStep) { |
|||
const p1 = transform({ x: logicalExtent.minX, y }); |
|||
const p2 = transform({ x: logicalExtent.maxX, y }); |
|||
lines.push( |
|||
<Line |
|||
key={`h-${y}`} |
|||
points={[p1.x, p1.y, p2.x, p2.y]} |
|||
stroke="#eee" |
|||
strokeWidth={1} |
|||
/> |
|||
); |
|||
} |
|||
const xAxisStart = transform({ x: logicalExtent.minX, y: 0 }); |
|||
const xAxisEnd = transform({ x: logicalExtent.maxX, y: 0 }); |
|||
lines.push( |
|||
<Line |
|||
key="x-axis" |
|||
points={[xAxisStart.x, xAxisStart.y, xAxisEnd.x, xAxisEnd.y]} |
|||
stroke="gray" |
|||
strokeWidth={2} |
|||
/> |
|||
); |
|||
const yAxisStart = transform({ x: 0, y: logicalExtent.minY }); |
|||
const yAxisEnd = transform({ x: 0, y: logicalExtent.maxY }); |
|||
lines.push( |
|||
<Line |
|||
key="y-axis" |
|||
points={[yAxisStart.x, yAxisStart.y, yAxisEnd.x, yAxisEnd.y]} |
|||
stroke="gray" |
|||
strokeWidth={2} |
|||
/> |
|||
); |
|||
return lines; |
|||
}; |
|||
|
|||
const renderCoordinates = () => { |
|||
const texts = []; |
|||
const minSpacing = 30; |
|||
const dynamicXInterval = Math.max(coordinateInterval, Math.ceil(minSpacing / scale)); |
|||
const dynamicYInterval = Math.max(coordinateInterval, Math.ceil(minSpacing / scale)); |
|||
|
|||
for (let x = logicalExtent.minX; x <= logicalExtent.maxX; x += dynamicXInterval) { |
|||
const pos = transform({ x, y: 0 }); |
|||
texts.push( |
|||
<Text |
|||
key={`coord-x-${x}`} |
|||
x={pos.x - 10} |
|||
y={height - 20} |
|||
text={x.toFixed(0)} |
|||
fontSize={12} |
|||
fill="black" |
|||
/> |
|||
); |
|||
} |
|||
for (let y = logicalExtent.minY; y <= logicalExtent.maxY; y += dynamicYInterval) { |
|||
const pos = transform({ x: 0, y }); |
|||
texts.push( |
|||
<Text |
|||
key={`coord-y-${y}`} |
|||
x={5} |
|||
y={pos.y - 6} |
|||
text={y.toFixed(0)} |
|||
fontSize={12} |
|||
fill="black" |
|||
/> |
|||
); |
|||
} |
|||
return texts; |
|||
}; |
|||
|
|||
const renderBenchmarkShapes = () => { |
|||
return benchmarkData.map((shape, idx) => { |
|||
if (shape.type === "line") { |
|||
const p1 = transform(shape.start); |
|||
const p2 = transform(shape.end); |
|||
return ( |
|||
<Line |
|||
key={`benchmark-line-${idx}`} |
|||
points={[p1.x, p1.y, p2.x, p2.y]} |
|||
stroke={shape.color} |
|||
strokeWidth={1} |
|||
/> |
|||
); |
|||
} else if (shape.type === "arc") { |
|||
const dx = shape.end.x - shape.start.x; |
|||
const dy = shape.end.y - shape.start.y; |
|||
const d = Math.hypot(dx, dy); |
|||
const Mx = (shape.start.x + shape.end.x) / 2; |
|||
const My = (shape.start.y + shape.end.y) / 2; |
|||
const halfChord = d / 2; |
|||
const h = Math.sqrt(shape.radius * shape.radius - halfChord * halfChord); |
|||
const candidate1 = { |
|||
cx: Mx - (dy / d) * h, |
|||
cy: My + (dx / d) * h, |
|||
}; |
|||
const candidate2 = { |
|||
cx: Mx + (dy / d) * h, |
|||
cy: My - (dx / d) * h, |
|||
}; |
|||
const chosen = shape.side === "right" ? candidate2 : candidate1; |
|||
const startAngle = Math.atan2(shape.start.y - chosen.cy, shape.start.x - chosen.cx); |
|||
const endAngle = Math.atan2(shape.end.y - chosen.cy, shape.end.x - chosen.cx); |
|||
const normalize = (angle: number): number => { |
|||
while (angle < 0) angle += 2 * Math.PI; |
|||
while (angle >= 2 * Math.PI) angle -= 2 * Math.PI; |
|||
return angle; |
|||
}; |
|||
const params = { |
|||
cx: chosen.cx, |
|||
cy: chosen.cy, |
|||
r: shape.radius, |
|||
startAngle: normalize(startAngle), |
|||
endAngle: normalize(endAngle), |
|||
anticlockwise: |
|||
(normalize(endAngle) - normalize(startAngle) + 2 * Math.PI) % |
|||
(2 * Math.PI) > |
|||
Math.PI, |
|||
}; |
|||
return ( |
|||
<Shape |
|||
key={`benchmark-arc-${idx}`} |
|||
sceneFunc={(ctx, shapeObj) => { |
|||
ctx.beginPath(); |
|||
ctx.arc( |
|||
canvasCenter.x + offset.x + (params.cx - origin.x) * scale, |
|||
canvasCenter.y + offset.y + (params.cy - origin.y) * scale, |
|||
params.r * scale, |
|||
params.startAngle, |
|||
params.endAngle, |
|||
params.anticlockwise |
|||
); |
|||
ctx.strokeStyle = shape.color; |
|||
ctx.lineWidth = 1; |
|||
ctx.stroke(); |
|||
ctx.fillStrokeShape(shapeObj); |
|||
}} |
|||
/> |
|||
); |
|||
} |
|||
return null; |
|||
}); |
|||
}; |
|||
|
|||
const renderMeasurementCurveLeft = () => { |
|||
if (measurementDataLeft.length === 0) return null; |
|||
const pts = measurementDataLeft |
|||
.map((pt) => { |
|||
const p = transform(pt); |
|||
return [p.x, p.y]; |
|||
}) |
|||
.flat(); |
|||
return ( |
|||
<Line |
|||
points={pts} |
|||
stroke="green" |
|||
strokeWidth={2} |
|||
tension={1} |
|||
lineCap="round" |
|||
lineJoin="round" |
|||
/> |
|||
); |
|||
}; |
|||
|
|||
const renderMeasurementCurveRight = () => { |
|||
if (measurementDataRight.length === 0) return null; |
|||
const pts = measurementDataRight |
|||
.map((pt) => { |
|||
const p = transform(pt); |
|||
return [p.x, p.y]; |
|||
}) |
|||
.flat(); |
|||
return ( |
|||
<Line |
|||
points={pts} |
|||
stroke="blue" |
|||
strokeWidth={2} |
|||
tension={1} |
|||
lineCap="round" |
|||
lineJoin="round" |
|||
/> |
|||
); |
|||
}; |
|||
|
|||
const renderMeasurementCurve = () => { |
|||
if (measurementData.length === 0) return null; |
|||
const pts = measurementData |
|||
.map((pt) => { |
|||
const p = transform(pt); |
|||
return [p.x, p.y]; |
|||
}) |
|||
.flat(); |
|||
return ( |
|||
<Line |
|||
points={pts} |
|||
stroke="purple" |
|||
strokeWidth={2} |
|||
tension={1} |
|||
lineCap="round" |
|||
lineJoin="round" |
|||
/> |
|||
); |
|||
}; |
|||
|
|||
const renderAnalysis = () => { |
|||
return analysisData.map((item, idx) => { |
|||
const pA = transform(item.pointA); |
|||
const pB = transform(item.pointB); |
|||
return ( |
|||
<React.Fragment key={`analysis-${idx}`}> |
|||
<Line |
|||
points={[pA.x, pA.y, pB.x, pB.y]} |
|||
stroke="red" |
|||
strokeWidth={1} |
|||
/> |
|||
<Text |
|||
x={pA.x - 15} |
|||
y={pA.y - 15} |
|||
text={item.describe} |
|||
fontSize={14} |
|||
fill="black" |
|||
/> |
|||
</React.Fragment> |
|||
); |
|||
}); |
|||
}; |
|||
|
|||
return ( |
|||
<div |
|||
style={{ |
|||
width, |
|||
height, |
|||
border: "1px solid #ccc", |
|||
position: "relative", |
|||
touchAction: "none", |
|||
}} |
|||
> |
|||
<Stage |
|||
ref={stageRef} |
|||
width={width} |
|||
height={height} |
|||
onMouseDown={handleMouseDown} |
|||
onMouseMove={handleMouseMove} |
|||
onMouseUp={handleMouseUp} |
|||
onMouseLeave={handleMouseLeave} |
|||
onWheel={handleWheel} |
|||
onTouchStart={handleTouchStart} |
|||
onTouchMove={handleTouchMove} |
|||
onTouchEnd={handleTouchEnd} |
|||
style={{ background: "#fff" }} |
|||
> |
|||
<Layer> |
|||
{showGrid && renderGridAndAxes()} |
|||
{showBenchmark && renderBenchmarkShapes()} |
|||
{renderMeasurementCurveLeft()} |
|||
{renderMeasurementCurveRight()} |
|||
{renderMeasurementCurve()} |
|||
{showAnalysis && renderAnalysis()} |
|||
</Layer> |
|||
{showCoordinates && <Layer>{renderCoordinates()}</Layer>} |
|||
</Stage> |
|||
{showScale && ( |
|||
<div |
|||
style={{ |
|||
position: "absolute", |
|||
bottom: 10, |
|||
left: 10, |
|||
background: "rgba(255,255,255,0.8)", |
|||
padding: "2px 4px", |
|||
border: "1px solid #ccc", |
|||
fontSize: 12, |
|||
pointerEvents: "none", |
|||
}} |
|||
> |
|||
<div |
|||
style={{ |
|||
width: gridStep * scaleInterval * scale, |
|||
borderBottom: "2px solid black", |
|||
marginBottom: 2, |
|||
}} |
|||
></div> |
|||
<div>{`${gridStep * scaleInterval} mm`}</div> |
|||
</div> |
|||
)} |
|||
</div> |
|||
); |
|||
} |
|||
); |
|||
|
|||
export default MeasurementCanvas; |
@ -1,58 +1,100 @@ |
|||
import httpRequest, { type BaseResponse } from "../httpRequest"; |
|||
import type { AnalySisReport, AnalyzeResult, DetailTable, MeasureRecord } from "../../services/measure/type"; |
|||
import httpRequest, {type BaseResponse} from "../httpRequest"; |
|||
import type { |
|||
AnalyzeResult, DetailTable, MeasureRecord, |
|||
ProfileRecordPointSet, BaseProfileRecordPointSet, AnalysisResults, AnalysisReport |
|||
} from "../../services/measure/type"; |
|||
|
|||
export function startMeasurement() { |
|||
return httpRequest<BaseResponse>({ |
|||
url: "/measurement-task/start-measurement", |
|||
method: "POST", |
|||
}); |
|||
return httpRequest<BaseResponse>({ |
|||
url: "/measurement-task/start-measurement", |
|||
method: "POST", |
|||
}); |
|||
} |
|||
|
|||
export function stopMeasurement() { |
|||
return httpRequest<BaseResponse>({ |
|||
url: "/measurement-task/stop-measurement", |
|||
method: "POST", |
|||
}); |
|||
return httpRequest<BaseResponse>({ |
|||
url: "/measurement-task/stop-measurement", |
|||
method: "POST", |
|||
}); |
|||
} |
|||
|
|||
export function analyzeMeasurement() { |
|||
return httpRequest<BaseResponse<AnalyzeResult>>({ |
|||
url: "/measurement-task/analyze-measurement", |
|||
method: "POST", |
|||
}); |
|||
return httpRequest<BaseResponse<AnalyzeResult>>({ |
|||
url: "/measurement-task/analyze-measurement", |
|||
method: "POST", |
|||
}); |
|||
} |
|||
|
|||
export function saveMeasurement() { |
|||
return httpRequest<BaseResponse>({ |
|||
url: "/measurement-task/save-report", |
|||
method: "POST", |
|||
}); |
|||
return httpRequest<BaseResponse>({ |
|||
url: "/measurement-task/save-report", |
|||
method: "POST", |
|||
}); |
|||
} |
|||
|
|||
export function getDetailList(params:{pageSize:number,pageNum:number}) { |
|||
return httpRequest<BaseResponse<{ list: DetailTable[], total:number }>>({ |
|||
url: "/measurement-data/list", |
|||
params, |
|||
method: "POST", |
|||
}); |
|||
export function getDetailList(params: { pageSize: number, pageNum: number }) { |
|||
return httpRequest<BaseResponse<{ list: DetailTable[], total: number }>>({ |
|||
url: "/measurement-data/list", |
|||
params, |
|||
method: "POST", |
|||
}); |
|||
} |
|||
|
|||
export function delDetail(params: { ids: string }) { |
|||
return httpRequest<BaseResponse>({ |
|||
url: `/measurement-data/delete/${params.ids}`, |
|||
method: "POST", |
|||
}); |
|||
return httpRequest<BaseResponse>({ |
|||
url: `/measurement-data/delete/${params.ids}`, |
|||
method: "POST", |
|||
}); |
|||
} |
|||
|
|||
export function createMeasure(params: MeasureRecord) { |
|||
return httpRequest<BaseResponse>({ |
|||
url: "/measurement-task/cache-measurement", |
|||
params, |
|||
method: "POST", |
|||
}); |
|||
} |
|||
|
|||
export function analysisReport(params:{code:number}) { |
|||
return httpRequest<BaseResponse<{angleAnalysisList:AnalySisReport[]}>>({ |
|||
url: `/measurement-task/save-analysis-report/${params.code}`, |
|||
method: "POST", |
|||
}); |
|||
return httpRequest<BaseResponse>({ |
|||
url: "/measurement-task/cache-measurement", |
|||
params, |
|||
method: "POST", |
|||
}); |
|||
} |
|||
|
|||
export function fetchAnalysisReport(code: string) { |
|||
return httpRequest<BaseResponse<AnalysisReport>>({ |
|||
url: `/measurement-task/save-analysis-report/${code}`, |
|||
method: "POST", |
|||
}); |
|||
} |
|||
|
|||
/** |
|||
* 根据测量记录 UUID 获取记录点集 |
|||
* POST /measurement-analysis/point/{uuid} |
|||
*/ |
|||
export function getRecordPointSetByUUID(uuid: string) { |
|||
return httpRequest<BaseResponse<ProfileRecordPointSet>>({ |
|||
url: `/measurement-analysis/point/${uuid}`, |
|||
method: "POST", |
|||
}); |
|||
} |
|||
|
|||
/** |
|||
* 根据标准轨 CODE 获取记录点集 |
|||
* POST /measurement-analysis/base-point/{code} |
|||
*/ |
|||
export function getBaseRecordPointSetByCode(code: string) { |
|||
return httpRequest<BaseResponse<BaseProfileRecordPointSet>>({ |
|||
url: `/measurement-analysis/base-point/${code}`, |
|||
method: "POST", |
|||
}); |
|||
} |
|||
|
|||
/** |
|||
* 根据测量记录 UUID 与标准轨 CODE 获取测量报告 |
|||
* POST /measurement-analysis/report |
|||
* |
|||
* @param uuid 测量记录 uuid |
|||
* @param code 标准轨 code |
|||
*/ |
|||
export function getReport(uuid: string, code: string) { |
|||
return httpRequest<BaseResponse<AnalysisResults>>({ |
|||
url: `/measurement-analysis/report`, |
|||
method: "POST", |
|||
params: { uuid, code }, |
|||
}); |
|||
} |
Write
Preview
Loading…
Cancel
Save
Reference in new issue