From af6c594aa3764144650771c33d1d7059f8b174b8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=99=BD=E5=87=A4=E5=90=89?= Date: Sat, 8 Mar 2025 13:20:14 +0800 Subject: [PATCH 1/7] =?UTF-8?q?0308=E4=BB=A3=E6=9B=BF=E9=BE=99=E9=BE=99?= =?UTF-8?q?=E6=8F=90=E4=BA=A4=E4=BB=A3=E7=A0=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .env | 2 +- src/App.tsx | 13 +- src/components/Header.tsx | 38 +++-- src/pages/login/Login.tsx | 6 +- src/pages/measure/components/MeasureAction.tsx | 174 +++++++++++++++++++-- src/pages/measure/components/MeasureConfig.tsx | 13 +- src/pages/measure/components/MeasureDetail.tsx | 48 ++++-- src/pages/measure/components/graph/GridLayer.tsx | 2 +- src/pages/measure/components/graph/MarkLayer.tsx | 18 ++- src/pages/measure/components/graph/ResultLayer.tsx | 24 ++- src/services/measure/analysis.ts | 14 +- src/services/measure/type.ts | 13 ++ src/services/wsTypes.ts | 28 +++- src/store/features/contextSlice.ts | 3 +- 14 files changed, 329 insertions(+), 67 deletions(-) diff --git a/.env b/.env index 6e6057d..febab32 100644 --- a/.env +++ b/.env @@ -1 +1 @@ -REACT_APP_WS_URL=192.168.1.200 +REACT_APP_WS_URL=192.168.1.200:80/ws diff --git a/src/App.tsx b/src/App.tsx index 1bfa511..c989531 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -20,14 +20,13 @@ function App() { //连接websocket const wsClient = createWebSocket(sharedWsUrl); const subscription = wsClient.dataOb.subscribe(data => { - console.log("data---", data); if (data.messageType === "DeviceContext") { - if (data.data.loginFlag) { - dispatch(updateUser(data.data)); - navigate("/measure/config"); - } else { - navigate("/login"); - } + // if (data.data.loginFlag) { + // dispatch(updateUser(data.data)); + // navigate("/measure/config"); + // } else { + // navigate("/login"); + // } }else if(data.messageType === 'STATE'){ dispatch(updateDeviceState(data.data)); } diff --git a/src/components/Header.tsx b/src/components/Header.tsx index 72ac097..6b4e1c4 100644 --- a/src/components/Header.tsx +++ b/src/components/Header.tsx @@ -4,23 +4,21 @@ import bluetooth_c from "../assets/bluetooth_c.svg"; import icon_avatar from "../assets/icon_avatar.svg"; import icon_logout from "../assets/icon_logout.svg"; import check_mark from "../assets/check_mark.svg"; -import icon_pwd from "../assets/icon_pwd.svg"; import { useState, useEffect } from "react"; -import {updateDeviceState } from '../store/device/deviceState'; -// import { bluetoothList, bluetoothInfo } from '../mock/recordList' import { Dropdown, MenuProps, message, Button, Popover } from "antd"; import { logout } from "../services/user/user"; -import { getDeviceInfo } from "../services/device/deviceState" +import { useNavigate } from "react-router"; +import { useAppDispatch } from "../utils/hooks"; +import { updateUser } from "../store/features/contextSlice"; import "./bluetooth.scss"; -import { useAppSelector } from "../utils/hooks"; -import { Device } from "../services/measure/type"; +import { loginUser } from '../services/wsTypes'; export default function Header() { - console.log('updateDeviceState---', updateDeviceState) + const navigate = useNavigate(); + const dispatch = useAppDispatch(); let [isConnect, setIsConnect] = useState(false) //@ts-ignoref const deviceState = useSelector(store => store.deviceState); const [bluetoothInfo, setBluetoothInfo] = useState(deviceState) - console.log('deviceState---',deviceState) //获取当前websocet的状态 const showBlueImg = () => { if(!isConnect){ @@ -50,12 +48,21 @@ export default function Header() { // //@ts-ignore // setbluetoothList(res.data.list) // }) + if(context.user.loginUser && context.user.loginUser.nickName){ + setNickname(context.user.loginUser.nickName) + }else{ + const user = localStorage.getItem('user') + if(user){ + let userData = JSON.parse( user || '') + setNickname(userData.nickname) + } + } }) //@ts-ignore const context = useSelector(store => store.context); const [messageApi, contextHolder] = message.useMessage(); - + const [nickname, setNickname] = useState() const items: MenuProps["items"] = [ // { // key: "1", @@ -73,6 +80,13 @@ export default function Header() { logout({}).then(res => { if (res.status !== 0) { messageApi.error(res.data.info) + }else{ + localStorage.setItem('user','') + dispatch(updateUser({ + loginFlag:false, + loginUser:{} + })); + navigate("/login"); } }) } @@ -130,13 +144,13 @@ export default function Header() {
-
+ {/*
{showBlueImg()} -
+
*/}
-

{context.user.loginFlag ? context.user.loginUser.nickname : "未登录"}

+

{ nickname || "未登录"}

diff --git a/src/pages/login/Login.tsx b/src/pages/login/Login.tsx index 933a65c..752344b 100644 --- a/src/pages/login/Login.tsx +++ b/src/pages/login/Login.tsx @@ -5,11 +5,13 @@ import img_bg from "../../assets/img_bg.jpg"; import img_logo from "../../assets/icon_logo.png"; import { useNavigate } from "react-router"; import { login } from "../../services/user/user"; +import { useAppDispatch } from "../../utils/hooks"; +import { updateUser } from "../../store/features/contextSlice"; export default function Login() { const navigate = useNavigate(); const [messageApi, contextHolder] = message.useMessage(); - + const dispatch = useAppDispatch(); const onFinish = (values: any) => { console.log("Received values of form: ", values); login({ account: values["username"], password: values["password"] }).then(res => { @@ -19,6 +21,8 @@ export default function Login() { content: res.data.info, }); } else { + dispatch(updateUser(res.data)); + localStorage.setItem('user',JSON.stringify(res.data)) navigate("/measure/config"); } }); diff --git a/src/pages/measure/components/MeasureAction.tsx b/src/pages/measure/components/MeasureAction.tsx index 1c34ede..08c1e1c 100644 --- a/src/pages/measure/components/MeasureAction.tsx +++ b/src/pages/measure/components/MeasureAction.tsx @@ -1,7 +1,7 @@ import { Button, Checkbox, CheckboxProps, Switch, message } from "antd"; import { useState, useEffect } from "react"; import { useNavigate } from "react-router"; -import { analyzeMeasurement, saveMeasurement, startMeasurement } from "../../../services/measure/analysis"; +import { analyzeMeasurement, saveMeasurement, startMeasurement, analysisReport } from "../../../services/measure/analysis"; import { createWebSocket, sharedWsUrl } from "../../../services/socket"; import GridLayer from "./graph/GridLayer"; import StandardLayer from "./graph/StandardLayer"; @@ -11,7 +11,8 @@ import { switchMeasureAfterSave } from "../../../store/features/contextSlice"; import { 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' const wsClient = createWebSocket(sharedWsUrl); export default function MeasureAction() { @@ -34,21 +35,53 @@ export default function MeasureAction() { }; const onAnalysisBtnClick = () => { // navigate("../detail"); - analyzeMeasurement().then(res => { + // if(taskStatus !== 'FINISHED'){ + // message.error('测量还未结束') + // return; + // } + const params = {//静态数据 TODO + code: 6001 + } + analysisReport(params).then(res => { + console.log('res===', res) if (res.success) { - setAngles(res.data.angles); - } else { - message.error(res.data.info); + 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{ + } - }); + }) + // analyzeMeasurement().then(res => { + // if (res.success) { + // setAngles(res.data.angles); + // } else { + // message.error(res.data.info); + // } + // }); }; 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) } }); }; @@ -66,15 +99,113 @@ export default function MeasureAction() { }); }; + let [taskStatusName, setTaskStatusName] = useState('') useEffect(() => { const subscription = wsClient.dataOb.subscribe(data => { if (data.messageType === "STATE" && data.path === "/measurement-task/get-task-state") { - setTaskStatus(data.data.taskStatus); + 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(); }); + + 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; + } + + const onHandleIcon =(item:StatusCodeData, index:number)=>{ + if(item.color === 'g'){ + return + }else if(item.color === 'b'){//index > 0 && statusList[index-1].isReady + return + }else if(item.color === 'h'){ + return
+ } + } + + 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' + }]) + return (
@@ -159,7 +290,22 @@ export default function MeasureAction() {

测量步骤

-
测量状态: {taskStatusDescMap[taskStatus]}
+ {/*
测量状态: {taskStatusDescMap[taskStatus]}
*/} + {/*
+ {taskStatus !== 'FINISHED' && +
+ +
{taskStatusName}
+
+ } + { + taskStatus === 'FINISHED' && +
+ +
{taskStatusName}
+
+ } +
*/} @@ -173,6 +319,16 @@ export default function MeasureAction() { 保存后自动开始新测量
+
+ {statusList && statusList.map((item,index) => { + return
+
+ {onHandleIcon(item, index)} +
{item.name}
+
+
+ })} +
); diff --git a/src/pages/measure/components/MeasureConfig.tsx b/src/pages/measure/components/MeasureConfig.tsx index a5488cd..046704b 100644 --- a/src/pages/measure/components/MeasureConfig.tsx +++ b/src/pages/measure/components/MeasureConfig.tsx @@ -1,5 +1,5 @@ import { Button, Form, Input, message } from "antd"; -import { useEffect } from "react"; +import { useEffect, useState } from "react"; import { useNavigate } from "react-router"; import { createMeasure } from "../../../services/measure/analysis"; import { useAppSelector } from "../../../utils/hooks"; @@ -24,13 +24,18 @@ export default function MeasureConfig() { } }); }; - + let user = localStorage.getItem('user') || '' + let userInfo = user && JSON.parse(user) || {}; + let [nickName, setNickName] = useState() + if(context.user.loginUser && context.user.loginUser.nickname){ + // setNickName(context.user.loginUser.nickname) + } const [form] = Form.useForm(); useEffect(() => { form.setFieldsValue({ - username: context.user.loginUser.nickname || "", + username: userInfo.nickname || "", }); - }, [context.user.loginUser.nickname, form]); + }, [userInfo.nickname, form]); return ( <> {contextHolder} diff --git a/src/pages/measure/components/MeasureDetail.tsx b/src/pages/measure/components/MeasureDetail.tsx index f5ef333..d3f8ee4 100644 --- a/src/pages/measure/components/MeasureDetail.tsx +++ b/src/pages/measure/components/MeasureDetail.tsx @@ -1,6 +1,6 @@ import {getDetailList, delDetail} from '../../../services/measure/analysis' import { useState, useEffect } from 'react' -import {message, Button, type TableColumnsType, type TableProps, Modal, Table } from 'antd'; +import {message, Button, type TableColumnsType, type TableProps, Modal, Table, Pagination } from 'antd'; import { useNavigate } from 'react-router-dom'; import type { DetailTable } from "../../../services/measure/type"; @@ -13,14 +13,21 @@ export default function MeasureDetail() { } useEffect(()=>{ - getDetailDataList() + const params = { + pageSize, + pageNum, + } + getDetailDataList(params) }, []) const [selectRows, setSelectedRow] = useState([]) - const rowSelection: TableProps['rowSelection'] = { + const [selectedRowKeys, setSelectedRowKeys] = useState([]); + const rowSelection = { + selectedRowKeys, onChange: (selectedRowKeys: React.Key[], selectedRows: DetailTable[]) => { console.log(`selectedRowKeys: ${selectedRowKeys}`, 'selectedRows: ', selectedRows); setSelectedRow(selectedRows) + setSelectedRowKeys(selectedRowKeys) }, getCheckboxProps: (record: DetailTable) => ({ name: record.name, @@ -59,12 +66,15 @@ export default function MeasureDetail() { ]; const [tableData, setTableData] = useState([]) - const getDetailDataList = () => { - getDetailList().then(res => { - if(res.status === 0){ + const [total, setTotal] = useState(0) + const getDetailDataList = (params:{pageSize:number, pageNum:number}) => { + + getDetailList(params).then(res => { + if(res.success){ console.log('res====', res) //@ts-ignore setTableData(res.data.list) + setTotal(res.data.total) }else{ // setTableData(detailList) } @@ -77,8 +87,6 @@ export default function MeasureDetail() { const [selectionType, setSelectionType] = useState<'checkbox'>('checkbox'); const { confirm } = Modal; const onDel = () => { - console.log('selectedRows====', selectRows) - confirm({ title: '提示', icon: , @@ -104,9 +112,14 @@ export default function MeasureDetail() { ids:ids.join(',') } delDetail(params).then(res => { - if(res.status === 0){ - getDetailList() + if(res.success){ message.success('删除成功') + setSelectedRowKeys([]) + const params = { + pageSize, + pageNum, + } + getDetailDataList(params) } }).catch(e=> { //删除mock的数据 @@ -118,6 +131,18 @@ export default function MeasureDetail() { } + const [pageNum, setPageNum] = useState(1) + const [pageSize, setPageSize] = useState(8) + const onPageChange = (pageNum:number) => { + console.log('pageNum---', pageNum) + setPageNum(pageNum) + const params = { + pageSize, + pageNum, + } + getDetailDataList(params) + } + return (
@@ -135,8 +160,11 @@ export default function MeasureDetail() { }} rowSelection={{ type: selectionType, ...rowSelection }} columns={columns} + rowKey="id" dataSource={tableData && tableData.map(item => ({ ...item, key: item.name }))} + pagination={false} /> +
diff --git a/src/pages/measure/components/graph/GridLayer.tsx b/src/pages/measure/components/graph/GridLayer.tsx index 37b44b7..048c8e5 100644 --- a/src/pages/measure/components/graph/GridLayer.tsx +++ b/src/pages/measure/components/graph/GridLayer.tsx @@ -81,7 +81,7 @@ export default function GridLayer(props: { ctx.fillText((index * 10).toString(), xStepPx * index, yMax + 20); } for (let index = -1; index < 5; index++) { - ctx.fillText((-index * 10).toString(), -xOffset - (index > 0 ? 18 : 14), yStepPx * index + 4); + ctx.fillText((index * 10).toString(), -xOffset - (index > 0 ? 18 : 14), yStepPx * index + 4); } }, [props.colCellNum, props.columns, props.height, props.rowCellNum, props.rows, props.width, xEndPx, xStartPx, xStepPx, xUnitPx, yEndPx, yStartPx, yStepPx, yUnitPx] diff --git a/src/pages/measure/components/graph/MarkLayer.tsx b/src/pages/measure/components/graph/MarkLayer.tsx index b04045a..3818337 100644 --- a/src/pages/measure/components/graph/MarkLayer.tsx +++ b/src/pages/measure/components/graph/MarkLayer.tsx @@ -3,10 +3,10 @@ import { calculatePointOnCircle, findSymmetricPoint } from "../../../../utils"; import { AnalyzeAngle } from "../../../../services/measure/type"; const marks = [ - { x: 9.949007022412, y: 0.1650166186941, degree: -80 }, - { x: 25.35, y: 2.184814802617, degree: -60 }, - { x: -9.949007022412, y: 0.1650166186941, degree: -100 }, - { x: -25.35, y: 2.184814802617, degree: -120 }, + { x: 9.949007022412, y: 0.1650166186941, degree: -80, describe:'6°C' }, + { x: 25.35, y: -2.184814802617, degree: -60, describe:'0.6°C' }, + { x: -9.949007022412, y: 0.1650166186941, degree: -100, describe: '0.2°C'}, + { x: -20.35, y: -2.184814802617, degree: -120, describe:'1.6°C' }, ]; export default function MarkLayer(props: { @@ -32,8 +32,13 @@ export default function MarkLayer(props: { const canvasRef = useRef(null); const calcPoints = useCallback(() => { - return props.angles - .map(p => ({ ...p, y: -p.y, degree: -p.degree })) + //props.angles + return marks + .map(p => { + //@ts-ignore + let degree = parseInt(p.degree) + return { ...p, y: -p.y, degree: -degree } + }) .map(p => { const p1 = calculatePointOnCircle(p.x * unitPx, p.y * unitPx, xStepPx / 2, p.degree); const p2 = findSymmetricPoint(p1.x, p1.y, p.x * unitPx, p.y * unitPx); @@ -62,6 +67,7 @@ export default function MarkLayer(props: { ctx.save(); ctx.translate(line[2].x, line[2].y); + //@ts-ignore 这个地方原来是marks, 现在替换为lines, 0307 ctx.rotate(((marks[idx].degree + 90) * Math.PI) / 180); ctx.fillText(props.angles[idx].describe, 0, 0); ctx.restore(); diff --git a/src/pages/measure/components/graph/ResultLayer.tsx b/src/pages/measure/components/graph/ResultLayer.tsx index 0b523b8..12be83e 100644 --- a/src/pages/measure/components/graph/ResultLayer.tsx +++ b/src/pages/measure/components/graph/ResultLayer.tsx @@ -16,6 +16,7 @@ export default function ResultLayer(props: { rows: number; visibility: 'hidden' | 'visible'; }) { + console.log('props---', props) const xStartPx = props.leftPadding; const xEndPx = props.width - props.rightPadding; const xStepPx = (props.width - props.leftPadding - props.rightPadding) / props.columns; @@ -46,14 +47,19 @@ export default function ResultLayer(props: { useEffect(() => { const subscription = wsClient.dataOb.subscribe(data => { - if (data.path === "/measurement-task/get-task-state") { + let isRight = false; + if(data.path === "/measurement-task/measurement-task"){ + + } + if (data.path === "/measurement-task/event") { const canvas = canvasRef.current; if (!canvas) return; const ctx = canvas.getContext("2d"); if (!ctx) return; - - if ('event' in data.data && data.data.event === "START_RECORD_SIG") { + console.log('data.data---', data.data) + //@ts-ignore + if (data.data === "START_RECORD_LEFT") { // setRtPoints([]); pointArr.length = 0; ctx.resetTransform(); @@ -65,12 +71,18 @@ export default function ResultLayer(props: { ctx.translate(xStartPx + xOffset, yStartPx + yOffset); ctx.beginPath(); }, 0); - } else if ('event' in data.data && data.data.event === "END_RECORD_SIG") { - // + //@ts-ignore + } else if (data.data === "START_RECORD_RIGHT") { + // pointArr.length = 0; + isRight = true; } - } else if (data.path === "/measurement-task/profile-record-ctrl-sig") { + } else if (data.path === "/measurement-task/point-report") { // console.log(data.data); // setRtPoints(rtPoints.concat([data.data])); + if(isRight){ + pointArr.length = 0 + } + //@ts-ignore pointArr.push(data.data); const canvas = canvasRef.current; diff --git a/src/services/measure/analysis.ts b/src/services/measure/analysis.ts index 9e793c6..0dcddb0 100644 --- a/src/services/measure/analysis.ts +++ b/src/services/measure/analysis.ts @@ -1,5 +1,5 @@ import httpRequest, { type BaseResponse } from "../httpRequest"; -import type { AnalyzeResult, DetailTable, MeasureRecord } from "../../services/measure/type"; +import type { AnalySisReport, AnalyzeResult, DetailTable, MeasureRecord } from "../../services/measure/type"; export function startMeasurement() { return httpRequest({ @@ -27,9 +27,10 @@ export function saveMeasurement() { }); } -export function getDetailList() { - return httpRequest>({ +export function getDetailList(params:{pageSize:number,pageNum:number}) { + return httpRequest>({ url: "/measurement-data/list", + params, method: "POST", }); } @@ -48,3 +49,10 @@ export function createMeasure(params: MeasureRecord) { method: "POST", }); } + +export function analysisReport(params:{code:number}) { + return httpRequest>({ + url: `/measurement-task/save-analysis-report/${params.code}`, + method: "POST", + }); +} diff --git a/src/services/measure/type.ts b/src/services/measure/type.ts index e8bb58e..0833e4f 100644 --- a/src/services/measure/type.ts +++ b/src/services/measure/type.ts @@ -6,6 +6,7 @@ export type DetailTable = { id:number; operatorName:string; name: string; + trackShapeCode:string; createTime: string; } @@ -34,3 +35,15 @@ export type Device ={ name: string; id:number } +export type AnalySisReport = { + pointA:{ + x: string; + y: string; + }; + pointB:{ + x: string; + y: string; + }; + distance:string; + describe:string; +} \ No newline at end of file diff --git a/src/services/wsTypes.ts b/src/services/wsTypes.ts index 74d2c8e..4790e16 100644 --- a/src/services/wsTypes.ts +++ b/src/services/wsTypes.ts @@ -1,10 +1,11 @@ // 开始、停止绘制 export type TaskState = { messageType: "EVENT"; - data: { - event: "START_RECORD_SIG" | "END_RECORD_SIG"; - }; - path: "/measurement-task/get-task-state"; + data: "START_RECORD_SIG" | "END_RECORD_SIG" | "FINISHED" | "START_RECORD_LEFT" | "FINISH_RECORD_RIGHT" | "FINISH_RECORD" | "FINISH_RECORD_LEFT" | "END_RECORD_SIG" | "END_RECORD" | "START_RECORD_RIGHT"; + // data: { + // event: "START_RECORD_SIG" | "END_RECORD_SIG" | "FINISHED" | "START_RECORD_LEFT" | "FINISH_RECORD_RIGHT" | "FINISH_RECORD" | "FINISH_RECORD_LEFT" | "END_RECORD_SIG" | "END_RECORD" | "START_RECORD_RIGHT"; + // }; + path: "/measurement-task/get-task-state" | "/measurement-task/point-report" | "/measurement-task/event"; }; // 连续上报坐标点 @@ -45,17 +46,32 @@ export type ContextMessage = { path: "/deviceContext"; }; +export type loginUser = Partial<{ + id: 3, //数据主键id + account: "test001", //用户账户 + nickname: "测试账户001", //用户昵称 + userRole: "User", //用户角色,可用值:User,Admin,Dev + isBuiltInUser: false, //是否内置用户(内置用户不可删除) +}>; + export const taskStatusDescMap: { [k in MeasureState["data"]["taskStatus"]]: string } = { IDLE: "空闲", MEASURING: "测量中", WAITING_FOR_MEASURING: "等待测量", FINISHED: "测量完成", + START_RECORD_LEFT: '', + FINISH_RECORD_RIGHT: '', + FINISH_RECORD:'', + FINISH_RECORD_LEFT: '', + END_RECORD_SIG: '', + END_RECORD: '', + START_RECORD_RIGHT : '' }; // 测量任务状态 export type MeasureState = { messageType: "STATE"; data: { - taskStatus: "IDLE" | "MEASURING" | "WAITING_FOR_MEASURING" | "FINISHED"; + taskStatus: "IDLE" | "MEASURING" | "WAITING_FOR_MEASURING" | "FINISHED" | "START_RECORD_LEFT" | "FINISH_RECORD_RIGHT" | "FINISH_RECORD" | "FINISH_RECORD_LEFT" | "END_RECORD_SIG" | "END_RECORD" | "START_RECORD_RIGHT"; measureSideCnt: 0 | 1 | 2; //已测量数量,0,1,2 最多两边(左边和右边) isMeasuringLeftEnd: boolean; //测量左侧完成 isMeasuringRightEnd: boolean; //测量右侧完成 @@ -63,7 +79,7 @@ export type MeasureState = { inStartMeasuringPos: boolean; //是否在允许开始测量的位置 // profileRecordDescription: null; //用户填写的新测量信息 }; - path: "/measurement-task/get-task-state"; + path: "/measurement-task/get-task-state" | "/measurement-task/event" | "/measurement-task/measurement-task"; }; export const defaultMeasureState = { diff --git a/src/store/features/contextSlice.ts b/src/store/features/contextSlice.ts index f2bb972..7573d93 100644 --- a/src/store/features/contextSlice.ts +++ b/src/store/features/contextSlice.ts @@ -1,7 +1,7 @@ // counterSlice.ts 文件 import { createSlice, PayloadAction } from "@reduxjs/toolkit"; -import { ContextMessage } from "../../services/wsTypes"; +import type { ContextMessage } from "../../services/wsTypes"; interface ContextSlice { user: ContextMessage["data"]; @@ -19,6 +19,7 @@ export const contextSlice = createSlice({ reducers: { updateUser: (state, action: PayloadAction) => { state.user.loginFlag = action.payload.loginFlag; + //@ts-ignore state.user.loginUser = action.payload.loginUser; }, switchMeasureAfterSave: (state, action: PayloadAction) => { From 00c3cd077738b83d9d07b3a27a4528a0ba91d4e3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=99=BD=E5=87=A4=E5=90=89?= Date: Sat, 8 Mar 2025 13:41:37 +0800 Subject: [PATCH 2/7] =?UTF-8?q?=E6=96=B0=E6=B5=8B=E9=87=8F=E6=93=8D?= =?UTF-8?q?=E4=BD=9C=E5=91=98=E4=B8=8E=E6=B5=8B=E9=87=8F=E5=90=8D=E7=A7=B0?= =?UTF-8?q?=E5=BA=94=E5=BD=93=E6=98=AF=E5=BF=85=E5=A1=AB=E7=9A=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/pages/measure/components/MeasureConfig.tsx | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/src/pages/measure/components/MeasureConfig.tsx b/src/pages/measure/components/MeasureConfig.tsx index 046704b..e1396a7 100644 --- a/src/pages/measure/components/MeasureConfig.tsx +++ b/src/pages/measure/components/MeasureConfig.tsx @@ -51,8 +51,12 @@ export default function MeasureConfig() { onFinish={onFinish} // onFinishFailed={onFinishFailed} autoComplete="off"> - - + + + {/* @@ -68,21 +72,21 @@ export default function MeasureConfig() { + rules={[{ required: true, message: "请输入测量名称" }]}> + rules={[{ required: false, message: "请输入线路名称" }]}> - + - {/* + { - */} + }
-
- setShowResult(checked)} /> - 对比线 -
+ {/*
*/} + {/* setShowResult(checked)} />*/} + {/* 对比线*/} + {/*
*/} {angles.length > 0 && (
setShowMark(checked)} /> @@ -289,6 +289,16 @@ export default function MeasureAction() {

测量步骤

+
+ {statusList && statusList.map((item,index) => { + return
+
+ {onHandleIcon(item, index)} +
{item.name}
+
+
+ })} +
{/*
测量状态: {taskStatusDescMap[taskStatus]}
*/} {/*
@@ -319,16 +329,7 @@ export default function MeasureAction() { 保存后自动开始新测量
-
- {statusList && statusList.map((item,index) => { - return
-
- {onHandleIcon(item, index)} -
{item.name}
-
-
- })} -
+
); From dcf1e366e30520b3e73e07cbb6d464e4d217bc96 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=99=BD=E5=87=A4=E5=90=89?= Date: Sat, 8 Mar 2025 13:52:33 +0800 Subject: [PATCH 4/7] =?UTF-8?q?=E6=B5=8B=E9=87=8F=E8=8F=9C=E5=8D=95?= =?UTF-8?q?=E9=BB=98=E8=AE=A4=E5=BA=94=E8=AF=A5=E6=98=AF=E5=B1=95=E5=BC=80?= =?UTF-8?q?=E7=9A=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/SideMenu.tsx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/components/SideMenu.tsx b/src/components/SideMenu.tsx index 1c74dcd..7dec168 100644 --- a/src/components/SideMenu.tsx +++ b/src/components/SideMenu.tsx @@ -30,7 +30,7 @@ export default function SideMenu() { const location = useLocation(); const onClick: MenuProps["onClick"] = e => { console.log("click menu----", e); - navigate(e.key) + navigate(e.key); }; return (
@@ -40,12 +40,12 @@ export default function SideMenu() { onClick={onClick} style={{ width: "100%", backgroundColor: "transparent", color: "#fff" }} defaultSelectedKeys={["1"]} - defaultOpenKeys={["sub1"]} + defaultOpenKeys={["measure"]} selectedKeys={[location.pathname]} mode="inline" items={items} /> -

V1.0

+

V1.0

); } From aad3951c69a055012ac296bed0dd7f7f7b9fe8ba Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=99=BD=E5=87=A4=E5=90=89?= Date: Sat, 8 Mar 2025 13:57:44 +0800 Subject: [PATCH 5/7] =?UTF-8?q?=E6=96=B0=E6=B5=8B=E9=87=8F=E9=A1=B5?= =?UTF-8?q?=E9=9D=A2=E5=A1=AB=E5=86=99=E8=A1=A8=E5=8D=95=E5=90=8E=E8=B7=B3?= =?UTF-8?q?=E8=BD=AC=E5=88=B0=E6=B5=8B=E9=87=8F=E9=A1=B5=E9=9D=A2=EF=BC=8C?= =?UTF-8?q?=E5=B7=A6=E4=BE=A7=E8=8F=9C=E5=8D=95=E4=B9=9F=E5=BA=94=E5=BD=93?= =?UTF-8?q?=E6=98=AF=E9=80=89=E4=B8=AD=E7=8A=B6=E6=80=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/SideMenu.tsx | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/src/components/SideMenu.tsx b/src/components/SideMenu.tsx index 7dec168..5050302 100644 --- a/src/components/SideMenu.tsx +++ b/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["items"][number]; @@ -28,8 +28,13 @@ 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); }; return ( @@ -39,9 +44,8 @@ export default function SideMenu() { className="side-menu" onClick={onClick} style={{ width: "100%", backgroundColor: "transparent", color: "#fff" }} - defaultSelectedKeys={["1"]} defaultOpenKeys={["measure"]} - selectedKeys={[location.pathname]} + selectedKeys={[key]} mode="inline" items={items} /> From 7069538342ba92a711e17f6c5072f9cad2352b97 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=99=BD=E5=87=A4=E5=90=89?= Date: Sat, 8 Mar 2025 14:19:35 +0800 Subject: [PATCH 6/7] =?UTF-8?q?=E6=B5=8B=E9=87=8F=E6=AD=A5=E9=AA=A4?= =?UTF-8?q?=E6=94=BE=E5=88=B0=E4=B8=8A=E9=9D=A2=EF=BC=8C=E7=82=B9=E5=87=BB?= =?UTF-8?q?=E5=BC=80=E5=A7=8B=E6=B5=8B=E9=87=8F=E6=8C=89=E9=92=AE=EF=BC=8C?= =?UTF-8?q?=E6=8C=89=E9=92=AE=E5=BA=94=E5=BD=93=E5=8F=98=E4=B8=BA=E9=87=8D?= =?UTF-8?q?=E6=96=B0=E6=B5=8B=E9=87=8F=EF=BC=8C=E7=82=B9=E5=87=BB=E9=87=8D?= =?UTF-8?q?=E6=96=B0=E6=B5=8B=E9=87=8F=E6=B5=8B=E9=87=8F=E6=AD=A5=E9=AA=A4?= =?UTF-8?q?=E5=BA=94=E5=BD=93=E8=BF=98=E5=8E=9F=E5=88=9D=E5=A7=8B=E7=8A=B6?= =?UTF-8?q?=E6=80=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/pages/measure/components/MeasureAction.tsx | 184 +++++++++++-------------- 1 file changed, 82 insertions(+), 102 deletions(-) diff --git a/src/pages/measure/components/MeasureAction.tsx b/src/pages/measure/components/MeasureAction.tsx index 0d00bcc..fa99aad 100644 --- a/src/pages/measure/components/MeasureAction.tsx +++ b/src/pages/measure/components/MeasureAction.tsx @@ -29,24 +29,66 @@ export default function MeasureAction() { const [angles, setAngles] = useState([]); const [taskStatus, setTaskStatus] = useState("IDLE"); + const [startBtnText, setStartBtnText] = 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' + } + ]; + let [statusList, setStatusList] = useState(initialStatusList); const onAfterSaveChange: CheckboxProps["onChange"] = e => { dispatch(switchMeasureAfterSave(e.target.checked)); }; const onAnalysisBtnClick = () => { - // navigate("../detail"); - // if(taskStatus !== 'FINISHED'){ - // message.error('测量还未结束') - // return; - // } - const params = {//静态数据 TODO + const params = { code: 6001 } analysisReport(params).then(res => { console.log('res===', res) if (res.success) { const angleAnalysisList = res.data.angleAnalysisList; - let angles:any = [] + let angles: any = [] angleAnalysisList && angleAnalysisList.map(item => { const pointA = item.pointA; angles.push({ @@ -57,31 +99,28 @@ export default function MeasureAction() { }) }) setAngles(angles); - }else{ - + } else { } }) - // analyzeMeasurement().then(res => { - // if (res.success) { - // setAngles(res.data.angles); - // } else { - // message.error(res.data.info); - // } - // }); }; const onStart = () => { + if (startBtnText === "重新测量") { + setStatusList(initialStatusList); + } 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) + const newStatusList = [...initialStatusList]; + newStatusList[0].color = 'b'; + setStatusList(newStatusList); message.success("已通知设备开始测量"); let name = taskStatusDescMap['IDLE'] setTaskStatusName(name) + setStartBtnText("重新测量") } }); }; @@ -103,36 +142,36 @@ export default function MeasureAction() { 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) return; + if (data.data.isMeasuringLeftEnd && !data.data.isMeasuringRightEnd) { + console.log('这是右侧的状态===', data.data.isMeasuringRightEnd) } - if(data.data.taskStatus === 'IDLE'){ + if (data.data.taskStatus === 'IDLE') { setTaskStatusName('空闲') - }else if(!data.data.isMeasuringLeftEnd){//正在测量左边 + } else if (!data.data.isMeasuringLeftEnd) { setTaskStatusName('左侧正在测量') statusList[0].isReady = true; setStatusList(statusList) - }else if(data.data.isMeasuringLeftEnd && !data.data.isMeasuringRightEnd){//左边为true, 右边是false时表示左边已完成,右边准备 + } else if (data.data.isMeasuringLeftEnd && !data.data.isMeasuringRightEnd) { setTaskStatusName('右侧正在测量') - }else{ + } 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"){ + if (data.data === "START_RECORD_LEFT") { statusList[0].color = 'g'; statusList[1].color = 'b'; - }else if(data.data === "FINISH_RECORD_LEFT"){ + } 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"){ + } else if (data.data === "START_RECORD_RIGHT") { statusList[3].color = 'g'; statusList[4].color = 'b'; - }else if(data.data === "FINISH_RECORD_RIGHT"){ + } else if (data.data === "FINISH_RECORD_RIGHT") { statusList[4].color = 'g'; statusList[5].color = 'g'; } @@ -147,65 +186,27 @@ export default function MeasureAction() { statusCode: string; name: string; background: string; - isReady:boolean; + isReady: boolean; color: string; } - const onHandleChangeStatus = (item:StatusCodeData) => { + const onHandleChangeStatus = (item: StatusCodeData) => { let backgroundColor = '' - if(item.statusCode === 'START_RECORD_LEFT'){ + if (item.statusCode === 'START_RECORD_LEFT') { backgroundColor = item.background } return backgroundColor; } - const onHandleIcon =(item:StatusCodeData, index:number)=>{ - if(item.color === 'g'){ - return - }else if(item.color === 'b'){//index > 0 && statusList[index-1].isReady - return - }else if(item.color === 'h'){ - return
+ const onHandleIcon = (item: StatusCodeData, index: number) => { + if (item.color === 'g') { + return + } else if (item.color === 'b') { + return + } else if (item.color === 'h') { + return
} } - 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' - }]) - return (
@@ -218,10 +219,6 @@ export default function MeasureAction() { setShowStandard(checked)} /> 标准线
- {/*
*/} - {/* setShowResult(checked)} />*/} - {/* 对比线*/} - {/*
*/} {angles.length > 0 && (
setShowMark(checked)} /> @@ -290,9 +287,9 @@ export default function MeasureAction() {

测量步骤

- {statusList && statusList.map((item,index) => { - return
-
+ {statusList && statusList.map((item, index) => { + return
+
{onHandleIcon(item, index)}
{item.name}
@@ -300,24 +297,8 @@ export default function MeasureAction() { })}
- {/*
测量状态: {taskStatusDescMap[taskStatus]}
*/} - {/*
- {taskStatus !== 'FINISHED' && -
- -
{taskStatusName}
-
- } - { - taskStatus === 'FINISHED' && -
- -
{taskStatusName}
-
- } -
*/}
-
); From 031d5d04ef09b28ec1e8c5999a883af1c427df6b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=99=BD=E5=87=A4=E5=90=89?= Date: Sat, 8 Mar 2025 21:38:02 +0800 Subject: [PATCH 7/7] =?UTF-8?q?=E8=B0=83=E6=95=B4=E6=96=B0=E6=B5=8B?= =?UTF-8?q?=E9=87=8F=E9=A1=B5=E9=9D=A2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- package-lock.json | 90 +++ package.json | 4 +- src/index.tsx | 72 +- src/pages/measure/components/MeasureAction.tsx | 728 ++++++++++++--------- .../measure/components/konva/MeasurementCanvas.tsx | 651 ++++++++++++++++++ src/services/measure/analysis.ts | 118 ++-- src/services/measure/type.ts | 62 +- 7 files changed, 1343 insertions(+), 382 deletions(-) create mode 100644 src/pages/measure/components/konva/MeasurementCanvas.tsx diff --git a/package-lock.json b/package-lock.json index 50b7c7c..831d253 100644 --- a/package-lock.json +++ b/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", diff --git a/package.json b/package.json index 2af872c..d787bc8 100644 --- a/package.json +++ b/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", diff --git a/src/index.tsx b/src/index.tsx index 6f781af..0b5f9f5 100644 --- a/src/index.tsx +++ b/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: , - children: [ - { - path: "measure", - element: , - children: [ - { - path: "config", - element: , - }, - { - path: "detail", - element: , - }, - { - path: "action", - element: , - }, - ], - }, - ], - }, - { - path: "/login", - element: , - }, - + { + path: "/", + element: , + children: [ + { + path: "measure", + element: , + children: [ + { + path: "config", + element: , + }, + { + path: "detail", + element: , + }, + { + path: "action", + element: , + } + ], + }, + ], + }, + { + path: "/login", + element: , + }, + ]); const root = ReactDOM.createRoot(document.getElementById("root") as HTMLElement); root.render( - // - - - - // + // + + + + // ); console.log(process.env.REACT_APP_WS_URL); diff --git a/src/pages/measure/components/MeasureAction.tsx b/src/pages/measure/components/MeasureAction.tsx index fa99aad..892bfe9 100644 --- a/src/pages/measure/components/MeasureAction.tsx +++ b/src/pages/measure/components/MeasureAction.tsx @@ -1,316 +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(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 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 [showGrid, setShowGrid] = useState(true); - const [showStandard, setShowStandard] = useState(true); - const [showResult, setShowResult] = useState(true); - const [showMark, setShowMark] = useState(true); + const onAfterSaveChange: CheckboxProps["onChange"] = (e) => { + dispatch(switchMeasureAfterSave(e.target.checked)); + }; - const afterSave = useAppSelector(store => store.context.newMeasureAfterSave); + 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 navigate = useNavigate(); + 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 [angles, setAngles] = useState([]); - const [taskStatus, setTaskStatus] = useState("IDLE"); - const [startBtnText, setStartBtnText] = useState("开始测量"); + 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 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' - } - ]; - let [statusList, setStatusList] = useState(initialStatusList); + 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 onAfterSaveChange: CheckboxProps["onChange"] = e => { - dispatch(switchMeasureAfterSave(e.target.checked)); - }; - const onAnalysisBtnClick = () => { - const params = { - 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 { - } - }) - }; + // 处理状态变化事件 + 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 onStart = () => { - 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("重新测量") - } - }); - }; + 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]); - const onSaveBtnClick = () => { - saveMeasurement().then(res => { - if (res.status !== 0) { - message.error(res.data.info); - } else { - message.success("保存成功"); - if (afterSave) { - navigate("../config"); - } - } - }); - }; + // 页面加载时获取基础图形数据,并传入 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); + } + } + }); + }, []); - 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) { - 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(); - }); + type StatusCodeData = { + statusCode: string; + name: string; + background: string; + isReady: boolean; + color: string; + }; - 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; - } + const onHandleChangeStatus = (item: StatusCodeData) => { + let backgroundColor = ""; + if (item.statusCode === "START_RECORD_LEFT") { + backgroundColor = item.background; + } + return backgroundColor; + }; - const onHandleIcon = (item: StatusCodeData, index: number) => { - if (item.color === 'g') { - return - } else if (item.color === 'b') { - return - } else if (item.color === 'h') { - return
- } - } + const onHandleIcon = (item: StatusCodeData, index: number) => { + if (item.color === "g") { + return ; + } else if (item.color === "b") { + return ; + } else if (item.color === "h") { + return ( +
+ ); + } + }; - return ( -
-
-
-
- setShowGrid(checked)} /> - 参考线 -
-
- setShowStandard(checked)} /> - 标准线 -
- {angles.length > 0 && ( -
- setShowMark(checked)} /> - 角度线 -
- )} -
-
- -
- -
-
- -
- {angles.length > 0 && ( -
- -
- )} -
-
-
-

测量步骤

-
- {statusList && statusList.map((item, index) => { - return
-
- {onHandleIcon(item, index)} -
{item.name}
-
-
- })} -
-
- - - - - 保存后自动开始新测量 - -
-
-
- ); + return ( +
+ {/* 左侧区域:包含开关区域和测量画布 */} +
+
+
+ setShowGrid(checked)} /> + 参考线 +
+
+ { + setShowStandard(checked); + if (!checked) { + // 关闭标准线时,备份当前角度线状态,并关闭角度线 + setAngleMarkBackup(showMark); + setShowMark(false); + } else { + // 打开标准线时,恢复角度线之前的状态 + setShowMark(angleMarkBackup); + } + }} + /> + 标准线 +
+
+ { + 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) => { + return ( +
+
+ {onHandleIcon(item, index)} +
{item.name}
+
+
+ ); + })} +
+
+ + + + + 保存后自动开始新测量 + +
+
+ )} +
+
+ ); } diff --git a/src/pages/measure/components/konva/MeasurementCanvas.tsx b/src/pages/measure/components/konva/MeasurementCanvas.tsx new file mode 100644 index 0000000..b629c2e --- /dev/null +++ b/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( + (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(initialScale); + 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 [measurementData, setMeasurementDataState] = useState([]); + + const refreshInterval = 50; + const refreshTimer = useRef(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(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(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(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( + + ); + } + 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; + }; + + 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( + + ); + } + for (let y = logicalExtent.minY; y <= logicalExtent.maxY; y += dynamicYInterval) { + const pos = transform({ x: 0, y }); + texts.push( + + ); + } + return texts; + }; + + const renderBenchmarkShapes = () => { + return benchmarkData.map((shape, idx) => { + if (shape.type === "line") { + const p1 = transform(shape.start); + const p2 = transform(shape.end); + return ( + + ); + } 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 ( + { + 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 ( + + ); + }; + + const renderMeasurementCurveRight = () => { + if (measurementDataRight.length === 0) return null; + const pts = measurementDataRight + .map((pt) => { + const p = transform(pt); + return [p.x, p.y]; + }) + .flat(); + return ( + + ); + }; + + const renderMeasurementCurve = () => { + if (measurementData.length === 0) return null; + const pts = measurementData + .map((pt) => { + const p = transform(pt); + return [p.x, p.y]; + }) + .flat(); + return ( + + ); + }; + + const renderAnalysis = () => { + return analysisData.map((item, idx) => { + const pA = transform(item.pointA); + const pB = transform(item.pointB); + return ( + + + + + ); + }); + }; + + return ( +
+ + + {showGrid && renderGridAndAxes()} + {showBenchmark && renderBenchmarkShapes()} + {renderMeasurementCurveLeft()} + {renderMeasurementCurveRight()} + {renderMeasurementCurve()} + {showAnalysis && renderAnalysis()} + + {showCoordinates && {renderCoordinates()}} + + {showScale && ( +
+
+
{`${gridStep * scaleInterval} mm`}
+
+ )} +
+ ); + } +); + +export default MeasurementCanvas; diff --git a/src/services/measure/analysis.ts b/src/services/measure/analysis.ts index 0dcddb0..185765c 100644 --- a/src/services/measure/analysis.ts +++ b/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({ - url: "/measurement-task/start-measurement", - method: "POST", - }); + return httpRequest({ + url: "/measurement-task/start-measurement", + method: "POST", + }); } + export function stopMeasurement() { - return httpRequest({ - url: "/measurement-task/stop-measurement", - method: "POST", - }); + return httpRequest({ + url: "/measurement-task/stop-measurement", + method: "POST", + }); } export function analyzeMeasurement() { - return httpRequest>({ - url: "/measurement-task/analyze-measurement", - method: "POST", - }); + return httpRequest>({ + url: "/measurement-task/analyze-measurement", + method: "POST", + }); } + export function saveMeasurement() { - return httpRequest({ - url: "/measurement-task/save-report", - method: "POST", - }); + return httpRequest({ + url: "/measurement-task/save-report", + method: "POST", + }); } -export function getDetailList(params:{pageSize:number,pageNum:number}) { - return httpRequest>({ - url: "/measurement-data/list", - params, - method: "POST", - }); +export function getDetailList(params: { pageSize: number, pageNum: number }) { + return httpRequest>({ + url: "/measurement-data/list", + params, + method: "POST", + }); } export function delDetail(params: { ids: string }) { - return httpRequest({ - url: `/measurement-data/delete/${params.ids}`, - method: "POST", - }); + return httpRequest({ + url: `/measurement-data/delete/${params.ids}`, + method: "POST", + }); } export function createMeasure(params: MeasureRecord) { - return httpRequest({ - url: "/measurement-task/cache-measurement", - params, - method: "POST", - }); + return httpRequest({ + url: "/measurement-task/cache-measurement", + params, + method: "POST", + }); +} + +export function fetchAnalysisReport(code: string) { + return httpRequest>({ + url: `/measurement-task/save-analysis-report/${code}`, + method: "POST", + }); } -export function analysisReport(params:{code:number}) { - return httpRequest>({ - url: `/measurement-task/save-analysis-report/${params.code}`, - method: "POST", - }); +/** + * 根据测量记录 UUID 获取记录点集 + * POST /measurement-analysis/point/{uuid} + */ +export function getRecordPointSetByUUID(uuid: string) { + return httpRequest>({ + url: `/measurement-analysis/point/${uuid}`, + method: "POST", + }); } + +/** + * 根据标准轨 CODE 获取记录点集 + * POST /measurement-analysis/base-point/{code} + */ +export function getBaseRecordPointSetByCode(code: string) { + return httpRequest>({ + 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>({ + url: `/measurement-analysis/report`, + method: "POST", + params: { uuid, code }, + }); +} \ No newline at end of file diff --git a/src/services/measure/type.ts b/src/services/measure/type.ts index 0833e4f..47e522f 100644 --- a/src/services/measure/type.ts +++ b/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; -} \ No newline at end of file +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; + } +}