Browse Source

merge 代码

feature/rail
LiLongLong 5 months ago
parent
commit
683fc42be5
  1. 11
      src/App.tsx
  2. 5
      src/index.tsx
  3. 5
      src/pages/measure/components/MeasureAction.scss
  4. 759
      src/pages/measure/components/MeasureAction.tsx
  5. 341
      src/pages/measure/components/konva/MeasurementCanvas.tsx

11
src/App.tsx

@ -14,6 +14,7 @@ const { Header, Footer, Sider, Content } = Layout;
function App() {
const dispatch = useAppDispatch();
const navigate = useNavigate();
useEffect(() => {
//连接websocket
@ -26,7 +27,7 @@ function App() {
// } else {
// navigate("/login");
// }
}else if(data.messageType === 'STATE'){
} else if (data.messageType === "STATE") {
dispatch(updateDeviceState(data.data));
}
});
@ -34,6 +35,14 @@ function App() {
return () => subscription.unsubscribe();
});
useEffect(() => {
if (localStorage.getItem("user")) {
navigate("/measure/config", { replace: true });
} else {
navigate("/login");
}
}, [navigate]);
const headerStyle: React.CSSProperties = {
height: 64,
padding: 0,

5
src/index.tsx

@ -17,6 +17,9 @@ import reportWebVitals from "./reportWebVitals";
import {Provider} from "react-redux";
import store from "./store/index";
import { ConfigProvider } from 'antd';
import zhCN from 'antd/locale/zh_CN';
const router = createBrowserRouter([
{
path: "/",
@ -61,7 +64,9 @@ const root = ReactDOM.createRoot(document.getElementById("root") as HTMLElement)
root.render(
// <React.StrictMode>
<Provider store={store}>
<ConfigProvider locale={zhCN}>
<RouterProvider router={router}/>
</ConfigProvider>
</Provider>
// </React.StrictMode>
);

5
src/pages/measure/components/MeasureAction.scss

@ -0,0 +1,5 @@
.analysis-table {
width: 300px;
height: calc(100% - 60px) ;// calc(100vh - var(--headerHeight) - var(--footerHeight) - 3rem - 60px);
overflow: auto;
}

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

@ -1,411 +1,408 @@
import React, { useState, useEffect, useRef } from 'react';
import React, { useState, useEffect, useRef } from "react";
import { Button, Checkbox, CheckboxProps, message, Switch } from "antd";
import { useNavigate } from "react-router";
import {
fetchAnalysisReport,
getBaseRecordPointSetByCode,
saveMeasurement,
startMeasurement,
fetchAnalysisReport,
getBaseRecordPointSetByCode,
saveMeasurement,
startMeasurement,
} from "../../../services/measure/analysis";
import { createWebSocket, sharedWsUrl } from "../../../services/socket";
import { switchMeasureAfterSave } from "../../../store/features/contextSlice";
import { AnalysisReport, AnalyzeAngle } from "../../../services/measure/type";
import { MeasureState, 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 MeasurementCanvas, {
AnalysisData,
BenchmarkShape,
MeasurementCanvasRef,
} from "./konva/MeasurementCanvas";
import Gr_round from "../../../assets/green_round.svg";
import Bl_round from "../../../assets/blue_round.svg";
import MeasurementCanvas, { AnalysisData, BenchmarkShape, MeasurementCanvasRef } from "./konva/MeasurementCanvas";
import "./MeasureAction.scss";
// 创建 websocket 客户端
const wsClient = createWebSocket(sharedWsUrl);
export default function MeasureAction() {
const dispatch = useAppDispatch();
const navigate = useNavigate();
const dispatch = useAppDispatch();
const navigate = useNavigate();
/** ----------------------- 引用 ----------------------- **/
const canvasRef = useRef<MeasurementCanvasRef>(null);
const leftPoints = useRef<{ x: number; y: number }[]>([]);
const rightPoints = useRef<{ x: number; y: number }[]>([]);
const isLeftFinished = useRef(false);
/** ----------------------- 引用 ----------------------- **/
const canvasRef = useRef<MeasurementCanvasRef>(null);
const leftPoints = useRef<{ x: number; y: number }[]>([]);
const rightPoints = useRef<{ x: number; y: number }[]>([]);
const isLeftFinished = useRef(false);
/** ----------------------- 状态 ----------------------- **/
const [showGrid, setShowGrid] = useState(true);
const [showStandard, setShowStandard] = useState(true);
const [showMark, setShowMark] = useState(true);
const [angleMarkBackup, setAngleMarkBackup] = useState(true);
const afterSave = useAppSelector((store) => store.context.newMeasureAfterSave);
const [angles, setAngles] = useState<AnalyzeAngle[]>([]);
const [taskStatus, setTaskStatus] = useState<MeasureState["data"]["taskStatus"]>("IDLE");
const [startBtnText, setStartBtnText] = useState("开始测量");
const [measurementFinished, setMeasurementFinished] = useState(false);
const [analysisClicked, setAnalysisClicked] = useState(false);
const [saveClicked, setSaveClicked] = useState(false);
const [analysisReport, setAnalysisReport] = useState<AnalysisReport | null>(null);
const [showAnalysisTable, setShowAnalysisTable] = useState(false);
const [taskStatusName, setTaskStatusName] = useState("");
/** ----------------------- 状态 ----------------------- **/
const [showGrid, setShowGrid] = useState(true);
const [showStandard, setShowStandard] = useState(true);
const [showMark, setShowMark] = useState(true);
const [angleMarkBackup, setAngleMarkBackup] = useState(true);
const afterSave = useAppSelector(store => store.context.newMeasureAfterSave);
const [angles, setAngles] = useState<AnalyzeAngle[]>([]);
const [taskStatus, setTaskStatus] = useState<MeasureState["data"]["taskStatus"]>("IDLE");
const [startBtnText, setStartBtnText] = useState("开始测量");
const [measurementFinished, setMeasurementFinished] = useState(false);
const [analysisClicked, setAnalysisClicked] = useState(false);
const [saveClicked, setSaveClicked] = useState(false);
const [analysisReport, setAnalysisReport] = useState<AnalysisReport | null>(null);
const [showAnalysisTable, setShowAnalysisTable] = useState(false);
const [taskStatusName, setTaskStatusName] = useState("");
// 初始状态列表
const initialStatusList = [
{ statusCode: "START_RECORD_LEFT", name: "请移动到顶部,停顿2秒", background: "#ececec", isReady: false, color: "h" },
{ statusCode: "START_RECORD_LEFT", name: "开始测量左侧", background: "#ececec", isReady: false, color: "h" },
{ statusCode: "START_RECORD_LEFT", name: "左侧测量完成", background: "#ececec", isReady: false, color: "h" },
{ statusCode: "START_RECORD_LEFT", name: "请移动到顶部,停顿2秒", background: "#ececec", isReady: false, color: "h" },
{ statusCode: "START_RECORD_LEFT", name: "开始测量右侧", background: "#ececec", isReady: false, color: "h" },
{ statusCode: "START_RECORD_LEFT", name: "右侧测量完成", background: "#ececec", isReady: false, color: "h" },
];
const [statusList, setStatusList] = useState(initialStatusList);
// 初始状态列表
const initialStatusList = [
{ statusCode: "START_RECORD_LEFT", name: "请移动到顶部,停顿2秒", background: "#ececec", isReady: false, color: "h" },
{ statusCode: "START_RECORD_LEFT", name: "开始测量左侧", background: "#ececec", isReady: false, color: "h" },
{ statusCode: "START_RECORD_LEFT", name: "左侧测量完成", background: "#ececec", isReady: false, color: "h" },
{ statusCode: "START_RECORD_LEFT", name: "请移动到顶部,停顿2秒", background: "#ececec", isReady: false, color: "h" },
{ statusCode: "START_RECORD_LEFT", name: "开始测量右侧", background: "#ececec", isReady: false, color: "h" },
{ statusCode: "START_RECORD_LEFT", name: "右侧测量完成", background: "#ececec", isReady: false, color: "h" },
];
const [statusList, setStatusList] = useState(initialStatusList);
/** ----------------------- 事件处理函数 ----------------------- **/
// 切换保存后自动开始新测量
const onAfterSaveChange: CheckboxProps["onChange"] = (e) => {
dispatch(switchMeasureAfterSave(e.target.checked));
};
/** ----------------------- 事件处理函数 ----------------------- **/
// 切换保存后自动开始新测量
const onAfterSaveChange: CheckboxProps["onChange"] = e => {
dispatch(switchMeasureAfterSave(e.target.checked));
};
// 分析按钮点击事件
const onAnalysisBtnClick = () => {
setAnalysisClicked(true);
fetchAnalysisReport("6001").then((res) => {
if (res.success) {
const report: AnalysisReport = res.data;
console.log(report);
// 更新 canvas 的分析数据
if (report && report.angleAnalysisList) {
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: { 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 onAnalysisBtnClick = () => {
setAnalysisClicked(true);
fetchAnalysisReport("6001").then(res => {
if (res.success) {
const report: AnalysisReport = res.data;
console.log(report);
// 更新 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: { x: parseFloat(item.pointA.x), y: parseFloat(item.pointA.y) },
measure: { x: parseFloat(item.pointB.x), y: parseFloat(item.pointB.y) },
distance: parseFloat(item.distance),
describe: item.describe,
}));
canvasRef.current?.setAnalysisData(analysisData);
}
setAnalysisReport(report);
setShowAnalysisTable(true);
} else {
message.error("分析报告请求失败: " + res.data.info);
}
});
};
// 开始/重新测量按钮点击事件
const onStart = () => {
if (startBtnText === "新测量") {
navigate("../newMeasure");
return;
}
// 重置测量相关状态
setShowAnalysisTable(false);
setMeasurementFinished(false);
setAnalysisClicked(false);
setSaveClicked(false);
isLeftFinished.current = false;
leftPoints.current = [];
rightPoints.current = [];
canvasRef.current?.clearShapes();
canvasRef.current?.resetCanvas();
if (startBtnText === "重新测量") {
setStatusList(initialStatusList);
}
startMeasurement().then((res) => {
if (res.status !== 0) {
message.error(res.data.info);
setTaskStatusName(taskStatusDescMap["IDLE"]);
} else {
const newStatusList = [...initialStatusList];
newStatusList[0].color = "b";
setStatusList(newStatusList);
message.success("已通知设备开始测量");
setTaskStatusName(taskStatusDescMap["IDLE"]);
setStartBtnText("重新测量");
}
});
};
// 开始/重新测量按钮点击事件
const onStart = () => {
if (startBtnText === "新测量") {
navigate("../newMeasure");
return;
}
// 重置测量相关状态
setShowAnalysisTable(false);
setMeasurementFinished(false);
setAnalysisClicked(false);
setSaveClicked(false);
isLeftFinished.current = false;
leftPoints.current = [];
rightPoints.current = [];
canvasRef.current?.clearShapes();
canvasRef.current?.resetCanvas();
if (startBtnText === "重新测量") {
setStatusList(initialStatusList);
}
startMeasurement().then(res => {
if (res.status !== 0) {
message.error(res.data.info);
setTaskStatusName(taskStatusDescMap["IDLE"]);
} else {
const newStatusList = [...initialStatusList];
newStatusList[0].color = "b";
setStatusList(newStatusList);
message.success("已通知设备开始测量");
setTaskStatusName(taskStatusDescMap["IDLE"]);
setStartBtnText("重新测量");
}
});
};
// 保存按钮点击事件
const onSaveBtnClick = () => {
setSaveClicked(true);
saveMeasurement().then((res) => {
if (res.status !== 0) {
message.error(res.data.info);
} else {
message.success("保存成功");
if (afterSave) {
navigate("../config");
} else {
setStartBtnText("新测量");
}
}
});
};
// 保存按钮点击事件
const onSaveBtnClick = () => {
setSaveClicked(true);
saveMeasurement().then(res => {
if (res.status !== 0) {
message.error(res.data.info);
} else {
message.success("保存成功");
if (afterSave) {
navigate("../config");
} else {
setStartBtnText("新测量");
}
}
});
};
// 辅助函数:渲染状态项的背景颜色
const renderStatusBackground = (item: typeof initialStatusList[0]) =>
item.statusCode === "START_RECORD_LEFT" ? item.background : "";
// 辅助函数:渲染状态项的背景颜色
const renderStatusBackground = (item: (typeof initialStatusList)[0]) =>
item.statusCode === "START_RECORD_LEFT" ? item.background : "";
// 辅助函数:渲染状态项的图标
const renderStatusIcon = (item: typeof initialStatusList[0]) => {
if (item.color === "g") {
return <img src={Gr_round} alt="green" />;
} else if (item.color === "b") {
return <img src={Bl_round} alt="blue" />;
} else {
return (
<div
style={{
width: "22px",
height: "22px",
background: "#c0c0c0",
borderRadius: "50%",
marginTop: "10px",
}}
/>
);
}
};
// 辅助函数:渲染状态项的图标
const renderStatusIcon = (item: (typeof initialStatusList)[0]) => {
if (item.color === "g") {
return <img src={Gr_round} alt="green" />;
} else if (item.color === "b") {
return <img src={Bl_round} alt="blue" />;
} else {
return (
<div
style={{
width: "22px",
height: "22px",
background: "#c0c0c0",
borderRadius: "50%",
marginTop: "10px",
}}
/>
);
}
};
/** ----------------------- WebSocket 消息处理 ----------------------- **/
useEffect(() => {
// 处理任务状态消息
const handleStateMessage = (data: any) => {
if (!data.data) return;
if (data.data.taskStatus === "IDLE") {
setTaskStatusName("空闲");
} else if (!data.data.isMeasuringLeftEnd) {
setTaskStatusName("左侧正在测量");
setStatusList((prev) => {
const updated = [...prev];
updated[0].isReady = true;
return updated;
});
} else if (data.data.isMeasuringLeftEnd && !data.data.isMeasuringRightEnd) {
setTaskStatusName("右侧正在测量");
} else {
setTaskStatusName(taskStatusDescMap[data.data.taskStatus as keyof typeof taskStatusDescMap]);
/** ----------------------- WebSocket 消息处理 ----------------------- **/
useEffect(() => {
// 处理任务状态消息
const handleStateMessage = (data: any) => {
if (!data.data) return;
if (data.data.taskStatus === "IDLE") {
setTaskStatusName("空闲");
} else if (!data.data.isMeasuringLeftEnd) {
setTaskStatusName("左侧正在测量");
setStatusList(prev => {
const updated = [...prev];
updated[0].isReady = true;
return updated;
});
} else if (data.data.isMeasuringLeftEnd && !data.data.isMeasuringRightEnd) {
setTaskStatusName("右侧正在测量");
} else {
setTaskStatusName(taskStatusDescMap[data.data.taskStatus as keyof typeof taskStatusDescMap]);
}
setTaskStatus(data.data.taskStatus);
};
}
setTaskStatus(data.data.taskStatus);
};
// 处理事件消息
const handleEventMessage = (data: any) => {
setStatusList(prev => {
const updated = [...prev];
switch (data.data) {
case "START_RECORD_LEFT":
updated[0].color = "g";
updated[1].color = "b";
break;
case "FINISH_RECORD_LEFT":
updated[1].color = "g";
updated[2].color = "g";
updated[3].color = "b";
isLeftFinished.current = true;
break;
case "START_RECORD_RIGHT":
updated[3].color = "g";
updated[4].color = "b";
break;
case "FINISH_RECORD_RIGHT":
updated[4].color = "g";
updated[5].color = "g";
setMeasurementFinished(true);
break;
default:
break;
}
return updated;
});
};
// 处理事件消息
const handleEventMessage = (data: any) => {
setStatusList((prev) => {
const updated = [...prev];
switch (data.data) {
case "START_RECORD_LEFT":
updated[0].color = "g";
updated[1].color = "b";
break;
case "FINISH_RECORD_LEFT":
updated[1].color = "g";
updated[2].color = "g";
updated[3].color = "b";
isLeftFinished.current = true;
break;
case "START_RECORD_RIGHT":
updated[3].color = "g";
updated[4].color = "b";
break;
case "FINISH_RECORD_RIGHT":
updated[4].color = "g";
updated[5].color = "g";
setMeasurementFinished(true);
break;
default:
break;
}
return updated;
});
};
// 处理点数据消息
const handlePointReport = (data: any) => {
const pointData = data.data as { x: number; y: number };
console.log(`pointData === ${pointData.x},${pointData.y}`);
if (!isLeftFinished.current) {
leftPoints.current.push(pointData);
canvasRef.current?.setMeasurementDataLeft([...leftPoints.current]);
} else {
rightPoints.current.push(pointData);
canvasRef.current?.setMeasurementDataRight([...rightPoints.current]);
}
};
// 处理点数据消息
const handlePointReport = (data: any) => {
const pointData = data.data as { x: number; y: number };
console.log(`pointData === ${pointData.x},${pointData.y}`);
if (!isLeftFinished.current) {
leftPoints.current.push(pointData);
canvasRef.current?.setMeasurementDataLeft([...leftPoints.current]);
} else {
rightPoints.current.push(pointData);
canvasRef.current?.setMeasurementDataRight([...rightPoints.current]);
}
};
const subscription = wsClient.dataOb.subscribe((data: any) => {
if (data.messageType === "STATE" && data.path === "/measurement-task/get-task-state") {
handleStateMessage(data);
} else if (data.messageType === "EVENT" && data.path === "/measurement-task/event") {
handleEventMessage(data);
} else if (data.messageType === "STATE" && data.path === "/measurement-task/point-report") {
handlePointReport(data);
}
});
wsClient.connect();
return () => subscription.unsubscribe();
}, []);
const subscription = wsClient.dataOb.subscribe((data: any) => {
if (data.messageType === "STATE" && data.path === "/measurement-task/get-task-state") {
handleStateMessage(data);
} else if (data.messageType === "EVENT" && data.path === "/measurement-task/event") {
handleEventMessage(data);
} else if (data.messageType === "STATE" && data.path === "/measurement-task/point-report") {
handlePointReport(data);
}
});
wsClient.connect();
return () => subscription.unsubscribe();
}, []);
/** ----------------------- 页面加载获取基础图形数据 ----------------------- **/
useEffect(() => {
getBaseRecordPointSetByCode("6001").then(res => {
if (res.success) {
const benchmarkShapes = JSON.parse(res.data.points) as BenchmarkShape[];
if (canvasRef.current) {
console.log("解析后的基础图形数据:", benchmarkShapes);
canvasRef.current.setBenchmarkData(benchmarkShapes);
}
}
});
}, []);
/** ----------------------- 页面加载获取基础图形数据 ----------------------- **/
useEffect(() => {
getBaseRecordPointSetByCode("6001").then((res) => {
if (res.success) {
const benchmarkShapes = JSON.parse(res.data.points) as BenchmarkShape[];
if (canvasRef.current) {
console.log("解析后的基础图形数据:", benchmarkShapes);
canvasRef.current.setBenchmarkData(benchmarkShapes);
}
}
});
}, []);
/** ----------------------- 渲染 ----------------------- **/
return (
<div className="flex h-full px-6">
{/* 左侧区域:包含开关区域和测量画布 */}
<div className="">
<div className="flex gap-4 items-center px-6 pt-5">
{/* 参考线开关 */}
<div className="flex gap-2 items-center">
<Switch defaultChecked onChange={checked => setShowGrid(checked)} />
<span>线</span>
</div>
{/* 标准线开关 */}
<div className="flex gap-2 items-center">
<Switch
checked={showStandard}
onChange={checked => {
setShowStandard(checked);
if (!checked) {
setAngleMarkBackup(showMark);
setShowMark(false);
} else {
setShowMark(angleMarkBackup);
}
}}
/>
<span>线</span>
</div>
{/* 角度线开关,仅在点击分析按钮后显示 */}
{analysisClicked && (
<div className="flex gap-2 items-center">
<Switch
checked={showMark}
disabled={!showStandard}
onChange={checked => {
setShowMark(checked);
setAngleMarkBackup(checked);
}}
/>
<span>线</span>
</div>
)}
</div>
<div className="relative m-2">
<MeasurementCanvas
width={800}
height={600}
logicalExtent={{ minX: -50, maxX: 50, minY: -20, maxY: 60 }}
gridStep={1}
origin={{ x: 0, y: 20 }}
pixelPerMm={8}
maxZoom={10}
showGrid={showGrid}
showBenchmark={showStandard}
showAnalysis={showMark}
showScale={false}
scaleInterval={1}
showCoordinates={true}
ref={canvasRef}
/>
</div>
</div>
{/* 右侧区域:根据 showAnalysisTable 状态显示测量步骤或分析表格 */}
<div className="min-w-[300px] flex-auto py-6 flex flex-col items-center">
{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>
))}
</tbody>
</table>
</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>
{/* 角度线开关,仅在点击分析按钮后显示 */}
{analysisClicked && (
<div className="flex gap-2 items-center">
<Switch
checked={showMark}
disabled={!showStandard}
onChange={(checked) => {
setShowMark(checked);
setAngleMarkBackup(checked);
}}
/>
<span>线</span>
</div>
)}
</div>
<div className="relative m-2">
<MeasurementCanvas
width={800}
height={600}
logicalExtent={{ minX: -50, maxX: 50, minY: -20, maxY: 60 }}
gridStep={1}
origin={{ x: 0, y: 20 }}
pixelPerMm={8}
maxZoom={10}
showGrid={showGrid}
showBenchmark={showStandard}
showAnalysis={showMark}
showScale={false}
scaleInterval={1}
showCoordinates={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) => (
<div
key={index}
style={{ background: renderStatusBackground(item), borderRadius: "20px" }}
className="mt-[10px] h-[40px]"
>
<div style={{ display: "flex", lineHeight: "40px" }} className="pl-[1rem]">
{renderStatusIcon(item)}
<div className="pl-[5px]">{item.name}</div>
</div>
</div>
))}
</div>
<section className="flex flex-col items-center gap-4 mt-6 border-t border-[#D8D8D8] py-4">
<Button style={{ width: 200 }} size="large" type="primary" onClick={onStart}>
{startBtnText}
</Button>
<Button
style={{ width: 200 }}
size="large"
type="primary"
onClick={onAnalysisBtnClick}
disabled={!measurementFinished || analysisClicked}
>
</Button>
<Button
style={{ width: 200 }}
size="large"
type="primary"
onClick={onSaveBtnClick}
disabled={!measurementFinished || saveClicked}
>
</Button>
<Checkbox checked={afterSave} onChange={onAfterSaveChange}>
</Checkbox>
</section>
</div>
)}
</div>
</div>
);
}
<Button
style={{ width: 200, marginTop: 18 }}
size="large"
type="primary"
onClick={() => navigate("../config")}>
</Button>
</>
) : (
<div>
<h1 className="font-medium text-xl text-center"></h1>
<div className="w-[13rem] mt-5">
{statusList.map((item, index) => (
<div
key={index}
style={{ background: renderStatusBackground(item), borderRadius: "20px" }}
className="mt-[10px] h-[40px]">
<div style={{ display: "flex", lineHeight: "40px" }} className="pl-[1rem]">
{renderStatusIcon(item)}
<div className="pl-[5px]">{item.name}</div>
</div>
</div>
))}
</div>
<section className="flex flex-col items-center gap-4 mt-6 border-t border-[#D8D8D8] py-4">
<Button style={{ width: 200 }} size="large" type="primary" onClick={onStart}>
{startBtnText}
</Button>
<Button
style={{ width: 200 }}
size="large"
type="primary"
onClick={onAnalysisBtnClick}
disabled={!measurementFinished || analysisClicked}>
</Button>
<Button
style={{ width: 200 }}
size="large"
type="primary"
onClick={onSaveBtnClick}
disabled={!measurementFinished || saveClicked}>
</Button>
<Checkbox checked={afterSave} onChange={onAfterSaveChange}>
</Checkbox>
</section>
</div>
)}
</div>
</div>
);
}

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

@ -63,9 +63,9 @@ export interface MeasurementCanvasProps {
initialMeasurementDataLeft?: Point[];
initialMeasurementDataRight?: Point[];
initialAnalysisData?: AnalysisData[];
// 新增属性:控制是否显示标准线(benchmark shapes)
// 控制是否显示标准线(benchmark shapes)
showBenchmark?: boolean;
// 新增属性:控制是否显示分析线
// 控制是否显示分析线
showAnalysis?: boolean;
}
@ -80,6 +80,13 @@ export interface MeasurementCanvasRef {
redraw: () => void;
}
interface PinchData {
initialDistance: number;
initialScale: number;
initialOffset: { x: number; y: number };
initialCenter: Point; // 固定的缩放中心
}
const MeasurementCanvas = forwardRef<MeasurementCanvasRef, MeasurementCanvasProps>(
(props, ref) => {
const {
@ -100,19 +107,18 @@ const MeasurementCanvas = forwardRef<MeasurementCanvasRef, MeasurementCanvasProp
initialMeasurementDataLeft = [],
initialMeasurementDataRight = [],
initialAnalysisData = [],
showBenchmark = true, // 默认显示标准线
showAnalysis = true, // 默认显示分析线
showBenchmark = true,
showAnalysis = true,
} = props;
// Stage 物理中心(像素)
const canvasCenter = { x: width / 2, y: height / 2 };
// 当 logicalExtent 范围较小时,自动计算一个 scale 使其铺满整个 Stage
// 计算初始 scale 与 offset
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,
@ -127,28 +133,17 @@ const MeasurementCanvas = forwardRef<MeasurementCanvasRef, MeasurementCanvasProp
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 [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 [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(() => {
@ -199,80 +194,122 @@ const MeasurementCanvas = forwardRef<MeasurementCanvasRef, MeasurementCanvasProp
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 };
// 记录当前活跃 pointer(pointerId -> Point)
const pointersRef = useRef<Map<number, Point>>(new Map());
// 单指拖拽的上一次位置
const lastDragPosRef = useRef<Point | null>(null);
// 针对鼠标,记录是否按下
const isDraggingRef = useRef<boolean>(false);
// 记录双指缩放的初始数据(只在两指刚开始时记录一次)
const pinchDataRef = useRef<PinchData | null>(null);
// 辅助函数:从事件中获取正确的指针位置
const getPointerPosition = (e: any): Point => {
const pos = stageRef.current.getPointerPosition();
return pos ? pos : { x: e.evt.clientX, y: e.evt.clientY };
};
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 handlePointerDown = (e: any) => {
const point = getPointerPosition(e);
const pointerId = e.evt.pointerId;
pointersRef.current.set(pointerId, point);
// 判断是否为触摸事件
const isTouch = e.evt.touches && e.evt.touches.length > 0;
if (!isTouch) {
isDraggingRef.current = true;
}
if (pointersRef.current.size === 1) {
lastDragPosRef.current = point;
}
// 当检测到两个手指时,记录初始数据——固定缩放中心
if (pointersRef.current.size === 2) {
const pts = Array.from(pointersRef.current.values());
const initialCenter = { x: (pts[0].x + pts[1].x) / 2, y: (pts[0].y + pts[1].y) / 2 };
const initialDistance = Math.hypot(pts[1].x - pts[0].x, pts[1].y - pts[0].y);
pinchDataRef.current = {
initialDistance,
initialScale: scale,
initialOffset: offset,
initialCenter, // 固定的缩放中心
};
}
};
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 handlePointerMove = (e: any) => {
const isTouch = e.evt.touches && e.evt.touches.length > 0;
if (!isTouch && !isDraggingRef.current) {
return;
}
const pointerId = e.evt.pointerId;
const point = getPointerPosition(e);
pointersRef.current.set(pointerId, point);
if (pointersRef.current.size === 1) {
// 单指拖拽
if (lastDragPosRef.current) {
const dx = point.x - lastDragPosRef.current.x;
const dy = point.y - lastDragPosRef.current.y;
setOffset((prev) => clampOffset({ x: prev.x + dx, y: prev.y + dy }, scale));
}
lastDragPosRef.current = point;
} else if (pointersRef.current.size >= 2 && pinchDataRef.current) {
// 双指缩放:固定缩放中心,与鼠标滚轮缩放逻辑一致
const pts = Array.from(pointersRef.current.values());
const currentDistance = Math.hypot(pts[1].x - pts[0].x, pts[1].y - pts[0].y);
const { initialDistance, initialScale, initialOffset, initialCenter } = pinchDataRef.current;
const scaleFactor = currentDistance / initialDistance;
let newScale = initialScale * scaleFactor;
newScale = Math.max(minZoom * pixelPerMm, Math.min(newScale, maxZoom * pixelPerMm));
// 计算逻辑坐标 L:与鼠标滚轮缩放公式一致,固定 pointer 为初始记录的 initialCenter
const L = {
x: origin.x + (initialCenter.x - (canvasCenter.x + initialOffset.x)) / initialScale,
y: origin.y + (initialCenter.y - (canvasCenter.y + initialOffset.y)) / initialScale,
};
// 计算新的 offset
const newOffset = {
x: initialCenter.x - canvasCenter.x - (L.x - origin.x) * newScale,
y: initialCenter.y - canvasCenter.y - (L.y - origin.y) * newScale,
};
setScale(newScale);
// 此处调用 clampOffset 确保缩放过程中不会超出边界
setOffset(clampOffset(newOffset, newScale));
}
};
const handleMouseUp = () => {
isDragging.current = false;
if (dragFrame.current) {
cancelAnimationFrame(dragFrame.current);
dragFrame.current = null;
const handlePointerUp = (e: any) => {
const pointerId = e.evt.pointerId;
pointersRef.current.delete(pointerId);
const isTouch = e.evt.touches && e.evt.touches.length > 0;
if (!isTouch) {
isDraggingRef.current = false;
}
// 当手指数不足2时重置双指缩放初始数据
if (pointersRef.current.size < 2) {
pinchDataRef.current = null;
}
if (pointersRef.current.size === 1) {
const remaining = Array.from(pointersRef.current.values())[0];
lastDragPosRef.current = remaining;
} else if (pointersRef.current.size === 0) {
lastDragPosRef.current = null;
}
};
const handleMouseLeave = () => {
isDragging.current = false;
if (dragFrame.current) {
cancelAnimationFrame(dragFrame.current);
dragFrame.current = null;
}
const handlePointerCancel = (e: any) => {
handlePointerUp(e);
};
// 鼠标滚轮缩放(仅适用于 PC)
const handleWheel = (e: any) => {
e.evt.preventDefault();
const oldScale = scale;
const pointer = stageRef.current.getPointerPosition();
if (!pointer) return;
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;
newScale = Math.max(minZoom * pixelPerMm, Math.min(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,
@ -281,69 +318,27 @@ const MeasurementCanvas = forwardRef<MeasurementCanvasRef, MeasurementCanvasProp
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 transform = (pt: Point | null): { x: number; y: number } => {
if (!pt) return { x: 0, y: 0 };
return {
x: canvasCenter.x + offset.x + (pt.x - origin.x) * scale,
y: canvasCenter.y + offset.y + (pt.y - origin.y) * scale,
};
};
const handleTouchEnd = () => {
isDragging.current = false;
lastTouchDistance.current = null;
lastPos.current = null;
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 renderGridAndAxes = () => {
@ -352,45 +347,25 @@ const MeasurementCanvas = forwardRef<MeasurementCanvasRef, MeasurementCanvasProp
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}
/>
<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}
/>
<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}
/>
<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}
/>
<Line key="y-axis" points={[yAxisStart.x, yAxisStart.y, yAxisEnd.x, yAxisEnd.y]} stroke="gray" strokeWidth={2} />
);
return lines;
};
@ -400,31 +375,16 @@ const MeasurementCanvas = forwardRef<MeasurementCanvasRef, MeasurementCanvasProp
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"
/>
<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"
/>
<Text key={`coord-y-${y}`} x={5} y={pos.y - 6} text={y.toFixed(0)} fontSize={12} fill="black" />
);
}
return texts;
@ -474,9 +434,7 @@ const MeasurementCanvas = forwardRef<MeasurementCanvasRef, MeasurementCanvasProp
startAngle: normalize(startAngle),
endAngle: normalize(endAngle),
anticlockwise:
(normalize(endAngle) - normalize(startAngle) + 2 * Math.PI) %
(2 * Math.PI) >
Math.PI,
(normalize(endAngle) - normalize(startAngle) + 2 * Math.PI) % (2 * Math.PI) > Math.PI,
};
return (
<Shape
@ -569,18 +527,8 @@ const MeasurementCanvas = forwardRef<MeasurementCanvasRef, MeasurementCanvasProp
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"
/>
<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>
);
});
@ -600,14 +548,11 @@ const MeasurementCanvas = forwardRef<MeasurementCanvasRef, MeasurementCanvasProp
ref={stageRef}
width={width}
height={height}
onMouseDown={handleMouseDown}
onMouseMove={handleMouseMove}
onMouseUp={handleMouseUp}
onMouseLeave={handleMouseLeave}
onPointerDown={handlePointerDown}
onPointerMove={handlePointerMove}
onPointerUp={handlePointerUp}
onPointerCancel={handlePointerCancel}
onWheel={handleWheel}
onTouchStart={handleTouchStart}
onTouchMove={handleTouchMove}
onTouchEnd={handleTouchEnd}
style={{ background: "#fff" }}
>
<Layer>

Loading…
Cancel
Save