diff --git a/src/App.tsx b/src/App.tsx index a21d1f3..6fbf69c 100644 --- a/src/App.tsx +++ b/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, diff --git a/src/index.tsx b/src/index.tsx index 0ca3e2c..72915e6 100644 --- a/src/index.tsx +++ b/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( // + + // ); diff --git a/src/pages/measure/components/MeasureAction.scss b/src/pages/measure/components/MeasureAction.scss new file mode 100644 index 0000000..307ab80 --- /dev/null +++ b/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; +} \ No newline at end of file diff --git a/src/pages/measure/components/MeasureAction.tsx b/src/pages/measure/components/MeasureAction.tsx index 24598ff..d1802b5 100644 --- a/src/pages/measure/components/MeasureAction.tsx +++ b/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(null); - const leftPoints = useRef<{ x: number; y: number }[]>([]); - const rightPoints = useRef<{ x: number; y: number }[]>([]); - const isLeftFinished = useRef(false); + /** ----------------------- 引用 ----------------------- **/ + const canvasRef = useRef(null); + const leftPoints = useRef<{ x: number; y: number }[]>([]); + const rightPoints = useRef<{ x: number; y: number }[]>([]); + const isLeftFinished = useRef(false); - /** ----------------------- 状态 ----------------------- **/ - const [showGrid, setShowGrid] = useState(true); - const [showStandard, setShowStandard] = useState(true); - const [showMark, setShowMark] = useState(true); - const [angleMarkBackup, setAngleMarkBackup] = useState(true); - const afterSave = useAppSelector((store) => store.context.newMeasureAfterSave); - const [angles, setAngles] = useState([]); - const [taskStatus, setTaskStatus] = useState("IDLE"); - const [startBtnText, setStartBtnText] = useState("开始测量"); - const [measurementFinished, setMeasurementFinished] = useState(false); - const [analysisClicked, setAnalysisClicked] = useState(false); - const [saveClicked, setSaveClicked] = useState(false); - const [analysisReport, setAnalysisReport] = useState(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([]); + const [taskStatus, setTaskStatus] = useState("IDLE"); + const [startBtnText, setStartBtnText] = useState("开始测量"); + const [measurementFinished, setMeasurementFinished] = useState(false); + const [analysisClicked, setAnalysisClicked] = useState(false); + const [saveClicked, setSaveClicked] = useState(false); + const [analysisReport, setAnalysisReport] = useState(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 green; - } else if (item.color === "b") { - return blue; - } else { - return ( -
- ); - } - }; + // 辅助函数:渲染状态项的图标 + const renderStatusIcon = (item: (typeof initialStatusList)[0]) => { + if (item.color === "g") { + return green; + } else if (item.color === "b") { + return blue; + } else { + return ( +
+ ); + } + }; - /** ----------------------- 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 ( +
+ {/* 左侧区域:包含开关区域和测量画布 */} +
+
+ {/* 参考线开关 */} +
+ setShowGrid(checked)} /> + 参考线 +
+ {/* 标准线开关 */} +
+ { + setShowStandard(checked); + if (!checked) { + setAngleMarkBackup(showMark); + setShowMark(false); + } else { + setShowMark(angleMarkBackup); + } + }} + /> + 标准线 +
+ {/* 角度线开关,仅在点击分析按钮后显示 */} + {analysisClicked && ( +
+ { + setShowMark(checked); + setAngleMarkBackup(checked); + }} + /> + 角度线 +
+ )} +
+
+ +
+
+ {/* 右侧区域:根据 showAnalysisTable 状态显示测量步骤或分析表格 */} +
+ {showAnalysisTable && analysisReport ? ( + <> +
+ + + + + + + + + + + {analysisReport.angleAnalysisList.map((item, index) => ( + + + + + ))} + +
W1垂直磨耗{analysisReport.w1}
轨头宽度 + {analysisReport.railHeadWidth} +
{item.describe}{item.distance}
+
- /** ----------------------- 渲染 ----------------------- **/ - return ( -
- {/* 左侧区域:包含开关区域和测量画布 */} -
-
- {/* 参考线开关 */} -
- setShowGrid(checked)} /> - 参考线 -
- {/* 标准线开关 */} -
- { - setShowStandard(checked); - if (!checked) { - setAngleMarkBackup(showMark); - setShowMark(false); - } else { - setShowMark(angleMarkBackup); - } - }} - /> - 标准线 -
- {/* 角度线开关,仅在点击分析按钮后显示 */} - {analysisClicked && ( -
- { - setShowMark(checked); - setAngleMarkBackup(checked); - }} - /> - 角度线 -
- )} -
-
- -
-
- {/* 右侧区域:根据 showAnalysisTable 状态显示测量步骤或分析表格 */} -
- {showAnalysisTable && analysisReport ? ( -
- - - - - - - - - - - {analysisReport.angleAnalysisList.map((item, index) => ( - - - - - ))} - - - - -
W1垂直磨耗{analysisReport.w1}
轨头宽度{analysisReport.railHeadWidth}
{item.describe}{item.distance}
- -
-
- ) : ( -
-

测量步骤

-
- {statusList.map((item, index) => ( -
-
- {renderStatusIcon(item)} -
{item.name}
-
-
- ))} -
-
- - - - - 保存后自动开始新测量 - -
-
- )} -
-
- ); -} \ No newline at end of file + + + ) : ( +
+

测量步骤

+
+ {statusList.map((item, index) => ( +
+
+ {renderStatusIcon(item)} +
{item.name}
+
+
+ ))} +
+
+ + + + + 保存后自动开始新测量 + +
+
+ )} +
+
+ ); +} diff --git a/src/pages/measure/components/konva/MeasurementCanvas.tsx b/src/pages/measure/components/konva/MeasurementCanvas.tsx index b629c2e..0ed2249 100644 --- a/src/pages/measure/components/konva/MeasurementCanvas.tsx +++ b/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( (props, ref) => { const { @@ -100,19 +107,18 @@ const MeasurementCanvas = forwardRef pixelPerMm ? computedScale : pixelPerMm; - const logicalCenter = { x: (logicalExtent.minX + logicalExtent.maxX) / 2, y: (logicalExtent.minY + logicalExtent.maxY) / 2, @@ -127,28 +133,17 @@ const MeasurementCanvas = forwardRef(initialOffset); const [scale, setScale] = useState(initialScale); - const [benchmarkData, setBenchmarkData] = - useState(initialBenchmarkData); - const [analysisData, setAnalysisData] = - useState(initialAnalysisData); + const [benchmarkData, setBenchmarkData] = useState(initialBenchmarkData); + const [analysisData, setAnalysisData] = useState(initialAnalysisData); - // 左右测量数据使用定时器更新 + // 定时更新测量数据(左右两侧) const leftPointsRef = useRef([...initialMeasurementDataLeft]); const rightPointsRef = useRef([...initialMeasurementDataRight]); - - const [measurementDataLeft, setMeasurementDataLeftState] = useState( - initialMeasurementDataLeft - ); - const [measurementDataRight, setMeasurementDataRightState] = useState( - initialMeasurementDataRight - ); - - // 新增直接绘制的测量数据状态,不使用定时器 + const [measurementDataLeft, setMeasurementDataLeftState] = useState(initialMeasurementDataLeft); + const [measurementDataRight, setMeasurementDataRightState] = useState(initialMeasurementDataRight); const [measurementData, setMeasurementDataState] = useState([]); - const refreshInterval = 50; const refreshTimer = useRef(null); - useEffect(() => { if (!refreshTimer.current) { refreshTimer.current = window.setInterval(() => { @@ -199,80 +194,122 @@ const MeasurementCanvas = forwardRef(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>(new Map()); + // 单指拖拽的上一次位置 + const lastDragPosRef = useRef(null); + // 针对鼠标,记录是否按下 + const isDraggingRef = useRef(false); + // 记录双指缩放的初始数据(只在两指刚开始时记录一次) + const pinchDataRef = useRef(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(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(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 + ); } 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( - + ); } const xAxisStart = transform({ x: logicalExtent.minX, y: 0 }); const xAxisEnd = transform({ x: logicalExtent.maxX, y: 0 }); lines.push( - + ); const yAxisStart = transform({ x: 0, y: logicalExtent.minY }); const yAxisEnd = transform({ x: 0, y: logicalExtent.maxY }); lines.push( - + ); return lines; }; @@ -400,31 +375,16 @@ const MeasurementCanvas = forwardRef + ); } for (let y = logicalExtent.minY; y <= logicalExtent.maxY; y += dynamicYInterval) { const pos = transform({ x: 0, y }); texts.push( - + ); } return texts; @@ -474,9 +434,7 @@ const MeasurementCanvas = forwardRef - Math.PI, + (normalize(endAngle) - normalize(startAngle) + 2 * Math.PI) % (2 * Math.PI) > Math.PI, }; return ( - - + + ); }); @@ -600,14 +548,11 @@ const MeasurementCanvas = forwardRef