Browse Source

重构测量页面,使用konva库

feature/rail
LiLongLong 5 months ago
parent
commit
c85cc441ef
  1. 90
      package-lock.json
  2. 4
      package.json
  3. 18
      src/components/SideMenu.tsx
  4. 72
      src/index.tsx
  5. 745
      src/pages/measure/components/MeasureAction.tsx
  6. 18
      src/pages/measure/components/MeasureConfig.tsx
  7. 651
      src/pages/measure/components/konva/MeasurementCanvas.tsx
  8. 122
      src/services/measure/analysis.ts
  9. 62
      src/services/measure/type.ts

90
package-lock.json

@ -47,6 +47,7 @@
"jest": "^27.4.3",
"jest-resolve": "^27.4.2",
"jest-watch-typeahead": "^1.0.0",
"konva": "^8.3.5",
"mini-css-extract-plugin": "^2.4.5",
"postcss-flexbugs-fixes": "^5.0.2",
"postcss-loader": "^6.2.1",
@ -58,6 +59,7 @@
"react-app-polyfill": "^3.0.0",
"react-dev-utils": "^12.0.1",
"react-dom": "^18.3.1",
"react-konva": "^18.0.0",
"react-redux": "^9.2.0",
"react-refresh": "^0.11.0",
"react-router": "^6.30.0",
@ -4557,6 +4559,15 @@
"@types/react": "^19.0.0"
}
},
"node_modules/@types/react-reconciler": {
"version": "0.28.9",
"resolved": "https://registry.npmjs.org/@types/react-reconciler/-/react-reconciler-0.28.9.tgz",
"integrity": "sha512-HHM3nxyUZ3zAylX8ZEyrDNd2XZOnQ0D5XfunJF5FLQnZbHHYq4UWvW1QfelQNXv1ICNkwYhfxjwfnqivYB6bFg==",
"license": "MIT",
"peerDependencies": {
"@types/react": "*"
}
},
"node_modules/@types/resolve": {
"version": "1.17.1",
"resolved": "https://registry.npmmirror.com/@types/resolve/-/resolve-1.17.1.tgz",
@ -10874,6 +10885,18 @@
"node": ">= 0.4"
}
},
"node_modules/its-fine": {
"version": "1.2.5",
"resolved": "https://registry.npmjs.org/its-fine/-/its-fine-1.2.5.tgz",
"integrity": "sha512-fXtDA0X0t0eBYAGLVM5YsgJGsJ5jEmqZEPrGbzdf5awjv0xE7nqv3TVnvtUF060Tkes15DbDAKW/I48vsb6SyA==",
"license": "MIT",
"dependencies": {
"@types/react-reconciler": "^0.28.0"
},
"peerDependencies": {
"react": ">=18.0"
}
},
"node_modules/jackspeak": {
"version": "3.4.3",
"resolved": "https://registry.npmmirror.com/jackspeak/-/jackspeak-3.4.3.tgz",
@ -12036,6 +12059,26 @@
"node": ">= 8"
}
},
"node_modules/konva": {
"version": "8.4.3",
"resolved": "https://registry.npmjs.org/konva/-/konva-8.4.3.tgz",
"integrity": "sha512-ARqdgAbdNIougRlOKvkQwHlGhXPRBV4KvhCP+qoPpGoVQwwiJe4Hkdu4HHdRPb9rGUp04jDTAxBzEwBsE272pg==",
"funding": [
{
"type": "patreon",
"url": "https://www.patreon.com/lavrton"
},
{
"type": "opencollective",
"url": "https://opencollective.com/konva"
},
{
"type": "github",
"url": "https://github.com/sponsors/lavrton"
}
],
"license": "MIT"
},
"node_modules/language-subtag-registry": {
"version": "0.3.23",
"resolved": "https://registry.npmmirror.com/language-subtag-registry/-/language-subtag-registry-0.3.23.tgz",
@ -15532,6 +15575,53 @@
"integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==",
"license": "MIT"
},
"node_modules/react-konva": {
"version": "18.2.10",
"resolved": "https://registry.npmjs.org/react-konva/-/react-konva-18.2.10.tgz",
"integrity": "sha512-ohcX1BJINL43m4ynjZ24MxFI1syjBdrXhqVxYVDw2rKgr3yuS0x/6m1Y2Z4sl4T/gKhfreBx8KHisd0XC6OT1g==",
"funding": [
{
"type": "patreon",
"url": "https://www.patreon.com/lavrton"
},
{
"type": "opencollective",
"url": "https://opencollective.com/konva"
},
{
"type": "github",
"url": "https://github.com/sponsors/lavrton"
}
],
"license": "MIT",
"dependencies": {
"@types/react-reconciler": "^0.28.2",
"its-fine": "^1.1.1",
"react-reconciler": "~0.29.0",
"scheduler": "^0.23.0"
},
"peerDependencies": {
"konva": "^8.0.1 || ^7.2.5 || ^9.0.0",
"react": ">=18.0.0",
"react-dom": ">=18.0.0"
}
},
"node_modules/react-reconciler": {
"version": "0.29.2",
"resolved": "https://registry.npmjs.org/react-reconciler/-/react-reconciler-0.29.2.tgz",
"integrity": "sha512-zZQqIiYgDCTP/f1N/mAR10nJGrPD2ZR+jDSEsKWJHYC7Cm2wodlwbR3upZRdC3cjIjSlTLNVyO7Iu0Yy7t2AYg==",
"license": "MIT",
"dependencies": {
"loose-envify": "^1.1.0",
"scheduler": "^0.23.2"
},
"engines": {
"node": ">=0.10.0"
},
"peerDependencies": {
"react": "^18.3.1"
}
},
"node_modules/react-redux": {
"version": "9.2.0",
"resolved": "https://registry.npmmirror.com/react-redux/-/react-redux-9.2.0.tgz",

4
package.json

@ -2,7 +2,7 @@
"name": "outline",
"version": "0.1.0",
"private": true,
"proxy":"http://192.168.1.200:80",
"proxy": "http://192.168.1.200:80",
"dependencies": {
"@babel/core": "^7.16.0",
"@pmmmwh/react-refresh-webpack-plugin": "^0.5.3",
@ -54,6 +54,8 @@
"react-app-polyfill": "^3.0.0",
"react-dev-utils": "^12.0.1",
"react-dom": "^18.3.1",
"konva": "^8.3.5",
"react-konva": "^18.0.0",
"react-redux": "^9.2.0",
"react-refresh": "^0.11.0",
"react-router": "^6.30.0",

18
src/components/SideMenu.tsx

@ -2,7 +2,7 @@ import type { MenuProps } from "antd";
import { Menu } from "antd";
import icon_logo from "../assets/icon_logo.svg";
import icon_measure from "../assets/menu/icon_measure.svg";
import { useNavigate, useLocation } from 'react-router-dom';
import { useNavigate, useLocation } from "react-router-dom";
import "./SideMenu.scss";
type MenuItem = Required<MenuProps>["items"][number];
@ -28,9 +28,14 @@ const items: MenuItem[] = [
export default function SideMenu() {
const navigate = useNavigate();
const location = useLocation();
const key =
location.pathname === "/measure/detail"
? "/measure/detail"
: location.pathname.startsWith("/measure")
? "/measure/config"
: location.pathname;
const onClick: MenuProps["onClick"] = e => {
console.log("click menu----", e);
navigate(e.key)
navigate(e.key);
};
return (
<div className="h-[100vh] bg-primary flex flex-col">
@ -39,13 +44,12 @@ export default function SideMenu() {
className="side-menu"
onClick={onClick}
style={{ width: "100%", backgroundColor: "transparent", color: "#fff" }}
defaultSelectedKeys={["1"]}
defaultOpenKeys={["sub1"]}
selectedKeys={[location.pathname]}
defaultOpenKeys={["measure"]}
selectedKeys={[key]}
mode="inline"
items={items}
/>
<p className="text-white/[0.7] text-center mt-auto mb-4 text-lg">V1.0</p>
<p className="text-white/[0.7] text-center mt-auto mb-4 text-lg">V1.0</p>
</div>
);
}

72
src/index.tsx

@ -1,7 +1,7 @@
import React from "react";
import ReactDOM from "react-dom/client";
import "./index.css";
import { createBrowserRouter, RouterProvider } from "react-router-dom";
import {createBrowserRouter, RouterProvider} from "react-router-dom";
import Login from "./pages/login/Login";
import Measure from "./pages/measure/Measure";
import MeasureConfig from "./pages/measure/components/MeasureConfig";
@ -11,48 +11,48 @@ import App from "./App";
import reportWebVitals from "./reportWebVitals";
// redux toolkit
import { Provider } from "react-redux";
import {Provider} from "react-redux";
import store from "./store/index";
const router = createBrowserRouter([
{
path: "/",
element: <App />,
children: [
{
path: "measure",
element: <Measure />,
children: [
{
path: "config",
element: <MeasureConfig />,
},
{
path: "detail",
element: <MeasureDetail />,
},
{
path: "action",
element: <MeasureAction />,
},
],
},
],
},
{
path: "/login",
element: <Login />,
},
{
path: "/",
element: <App/>,
children: [
{
path: "measure",
element: <Measure/>,
children: [
{
path: "config",
element: <MeasureConfig/>,
},
{
path: "detail",
element: <MeasureDetail/>,
},
{
path: "action",
element: <MeasureAction/>,
}
],
},
],
},
{
path: "/login",
element: <Login/>,
},
]);
const root = ReactDOM.createRoot(document.getElementById("root") as HTMLElement);
root.render(
// <React.StrictMode>
<Provider store={store}>
<RouterProvider router={router} />
</Provider>
// </React.StrictMode>
// <React.StrictMode>
<Provider store={store}>
<RouterProvider router={router}/>
</Provider>
// </React.StrictMode>
);
console.log(process.env.REACT_APP_WS_URL);

745
src/pages/measure/components/MeasureAction.tsx

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

18
src/pages/measure/components/MeasureConfig.tsx

@ -51,8 +51,12 @@ export default function MeasureConfig() {
onFinish={onFinish}
// onFinishFailed={onFinishFailed}
autoComplete="off">
<Form.Item label="操作员" name="username">
<Input disabled />
<Form.Item
label="操作员"
name="username"
rules={[{ required: true, message: "请输入操作员姓名" }]}>
<Input />
</Form.Item>
{/* <Form.Item label="" name="trackType">
@ -68,21 +72,21 @@ export default function MeasureConfig() {
<Form.Item
label="测量名称"
name="measureName"
rules={[{ required: true, message: "Please input your username!" }]}>
rules={[{ required: true, message: "请输入测量名称" }]}>
<Input />
</Form.Item>
<Form.Item
label="线路名称"
name="lineName"
rules={[{ required: true, message: "Please input your username!" }]}>
rules={[{ required: false, message: "请输入线路名称" }]}>
<Input />
</Form.Item>
<Form.Item label="位置" name="position" rules={[{ required: true, message: "Please input your username!" }]}>
<Form.Item label="位置" name="position" rules={[{ required: false, message: "请输入位置" }]}>
<Input />
</Form.Item>
{/* <Form.Item label="" name="direction" rules={[{ required: true, message: "Please input your username!" }]}>
{ <Form.Item label="方向" name="direction" rules={[{ required: false, message: "请输入方向" }]}>
<Input />
</Form.Item> */}
</Form.Item> }
<Form.Item label={null}>
<Button type="primary" size="large" style={{ width: 220 }} htmlType="submit">

651
src/pages/measure/components/konva/MeasurementCanvas.tsx

@ -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;

122
src/services/measure/analysis.ts

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

62
src/services/measure/type.ts

@ -35,15 +35,53 @@ export type Device ={
name: string;
id:number
}
export type AnalySisReport = {
pointA:{
x: string;
y: string;
};
pointB:{
x: string;
y: string;
};
distance:string;
describe:string;
}
export type AnalysisReport = {
railHeadWidth: number;
w1: number;
angleAnalysisList: {
pointA: {
x: string;
y: string;
};
pointB: {
x: string;
y: string;
};
distance: string;
describe: string;
}[];
};
export interface ProfileRecordPointSet {
id: number;
createTime: string;
updateTime: string;
profileRecordUuid: string;
points: {
x: number;
y: number;
}[];
}
export interface BaseProfileRecordPointSet {
id: number;
createTime: string;
updateTime: string;
name: string;
code: string;
points: string;
}
export interface AnalysisResults {
RailHeadWidth: number;
w1: number;
angleAnalysisList: {
base: { x: string; y: string };
measure: { x: string; y: string };
pointA: { x: string; y: string };
pointB: { x: string; y: string };
distance: string;
describe: string;
}
}
Loading…
Cancel
Save