Browse Source

feat: 集成 ZuStand 进行矩形管理并增强图像收集组件

- 添加了用于管理矩形数据的 zustand 存储。
- 更新了 ImageCollection 组件以利用 react-konva 进行图像渲染和矩形绘制。
- 增强的 MeasurementPointSetting 组件用于管理传感器数据并与矩形存储集成。
- 添加了使用确认模式添加、删除和清除传感器的功能。
- 改进了处理图像和矩形交互的 UI 响应能力和用户体验。
master
cles 2 weeks ago
parent
commit
8a2588f1cf
  1. 204
      package-lock.json
  2. 6
      package.json
  3. 2
      src/renderer/src/common/ipcEvents.js
  4. 324
      src/renderer/src/components/ImageCollection/ImageCollection.jsx
  5. 338
      src/renderer/src/components/MeasurementPointSetting/MeasurementPointSetting.jsx
  6. 9
      src/renderer/src/stores/rectangleStore.js

204
package-lock.json

@ -12,12 +12,16 @@
"@ant-design/icons": "^5.6.1", "@ant-design/icons": "^5.6.1",
"@electron-toolkit/preload": "^3.0.2", "@electron-toolkit/preload": "^3.0.2",
"@electron-toolkit/utils": "^4.0.0", "@electron-toolkit/utils": "^4.0.0",
"ahooks": "^3.9.5",
"antd": "^5.27.1", "antd": "^5.27.1",
"chart.js": "^4.5.0", "chart.js": "^4.5.0",
"electron-updater": "^6.3.9", "electron-updater": "^6.3.9",
"konva": "^9.3.22",
"react": "^18.0.0", "react": "^18.0.0",
"react-chartjs-2": "^5.3.0", "react-chartjs-2": "^5.3.0",
"react-dom": "^18.0.0" "react-dom": "^18.0.0",
"react-konva": "^18.2.12",
"zustand": "^5.0.8"
}, },
"devDependencies": { "devDependencies": {
"@electron-toolkit/eslint-config": "^2.0.0", "@electron-toolkit/eslint-config": "^2.0.0",
@ -2455,6 +2459,12 @@
"integrity": "sha512-1m0bIFVc7eJWyve9S0RnuRgcQqF/Xd5QsUZAZeQFr1Q3/p9JWoQQEqmVy+DPTNpGXwhgIetAoYF8JSc33q29QA==", "integrity": "sha512-1m0bIFVc7eJWyve9S0RnuRgcQqF/Xd5QsUZAZeQFr1Q3/p9JWoQQEqmVy+DPTNpGXwhgIetAoYF8JSc33q29QA==",
"license": "MIT" "license": "MIT"
}, },
"node_modules/@types/js-cookie": {
"version": "3.0.6",
"resolved": "https://registry.npmmirror.com/@types/js-cookie/-/js-cookie-3.0.6.tgz",
"integrity": "sha512-wkw9yd1kEXOPnvEeEV1Go1MmxtBJL0RR79aOTAApecWFVu7w0NNXNqhcWgvw2YgZDYadliXkl14pa3WXw5jlCQ==",
"license": "MIT"
},
"node_modules/@types/json-schema": { "node_modules/@types/json-schema": {
"version": "7.0.15", "version": "7.0.15",
"resolved": "https://registry.npmmirror.com/@types/json-schema/-/json-schema-7.0.15.tgz", "resolved": "https://registry.npmmirror.com/@types/json-schema/-/json-schema-7.0.15.tgz",
@ -2499,6 +2509,25 @@
"xmlbuilder": ">=11.0.1" "xmlbuilder": ">=11.0.1"
} }
}, },
"node_modules/@types/react": {
"version": "19.1.12",
"resolved": "https://registry.npmmirror.com/@types/react/-/react-19.1.12.tgz",
"integrity": "sha512-cMoR+FoAf/Jyq6+Df2/Z41jISvGZZ2eTlnsaJRptmZ76Caldwy1odD4xTr/gNV9VLj0AWgg/nmkevIyUfIIq5w==",
"license": "MIT",
"peer": true,
"dependencies": {
"csstype": "^3.0.2"
}
},
"node_modules/@types/react-reconciler": {
"version": "0.28.9",
"resolved": "https://registry.npmmirror.com/@types/react-reconciler/-/react-reconciler-0.28.9.tgz",
"integrity": "sha512-HHM3nxyUZ3zAylX8ZEyrDNd2XZOnQ0D5XfunJF5FLQnZbHHYq4UWvW1QfelQNXv1ICNkwYhfxjwfnqivYB6bFg==",
"license": "MIT",
"peerDependencies": {
"@types/react": "*"
}
},
"node_modules/@types/responselike": { "node_modules/@types/responselike": {
"version": "1.0.3", "version": "1.0.3",
"resolved": "https://registry.npmmirror.com/@types/responselike/-/responselike-1.0.3.tgz", "resolved": "https://registry.npmmirror.com/@types/responselike/-/responselike-1.0.3.tgz",
@ -2631,6 +2660,31 @@
"node": ">=8" "node": ">=8"
} }
}, },
"node_modules/ahooks": {
"version": "3.9.5",
"resolved": "https://registry.npmmirror.com/ahooks/-/ahooks-3.9.5.tgz",
"integrity": "sha512-TrjXie49Q8HuHKTa84Fm9A+famMDAG1+7a9S9Gq6RQ0h90Jgqmiq3CkObuRjWT/C4d6nRZCw35Y2k2fmybb5eA==",
"license": "MIT",
"dependencies": {
"@babel/runtime": "^7.21.0",
"@types/js-cookie": "^3.0.6",
"dayjs": "^1.9.1",
"intersection-observer": "^0.12.0",
"js-cookie": "^3.0.5",
"lodash": "^4.17.21",
"react-fast-compare": "^3.2.2",
"resize-observer-polyfill": "^1.5.1",
"screenfull": "^5.0.0",
"tslib": "^2.4.1"
},
"engines": {
"node": ">=18"
},
"peerDependencies": {
"react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0",
"react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
}
},
"node_modules/ajv": { "node_modules/ajv": {
"version": "6.12.6", "version": "6.12.6",
"resolved": "https://registry.npmmirror.com/ajv/-/ajv-6.12.6.tgz", "resolved": "https://registry.npmmirror.com/ajv/-/ajv-6.12.6.tgz",
@ -6235,6 +6289,12 @@
"node": ">= 0.4" "node": ">= 0.4"
} }
}, },
"node_modules/intersection-observer": {
"version": "0.12.2",
"resolved": "https://registry.npmmirror.com/intersection-observer/-/intersection-observer-0.12.2.tgz",
"integrity": "sha512-7m1vEcPCxXYI8HqnL8CKI6siDyD+eIWSwgB3DZA+ZTogxk9I4CDnj4wilt9x/+/QbHI4YG5YZNmC6458/e9Ktg==",
"license": "Apache-2.0"
},
"node_modules/ip-address": { "node_modules/ip-address": {
"version": "10.0.1", "version": "10.0.1",
"resolved": "https://registry.npmmirror.com/ip-address/-/ip-address-10.0.1.tgz", "resolved": "https://registry.npmmirror.com/ip-address/-/ip-address-10.0.1.tgz",
@ -6724,6 +6784,18 @@
"node": ">= 0.4" "node": ">= 0.4"
} }
}, },
"node_modules/its-fine": {
"version": "1.2.5",
"resolved": "https://registry.npmmirror.com/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": { "node_modules/jackspeak": {
"version": "3.4.3", "version": "3.4.3",
"resolved": "https://registry.npmmirror.com/jackspeak/-/jackspeak-3.4.3.tgz", "resolved": "https://registry.npmmirror.com/jackspeak/-/jackspeak-3.4.3.tgz",
@ -6758,6 +6830,15 @@
"node": ">=10" "node": ">=10"
} }
}, },
"node_modules/js-cookie": {
"version": "3.0.5",
"resolved": "https://registry.npmmirror.com/js-cookie/-/js-cookie-3.0.5.tgz",
"integrity": "sha512-cEiJEAEoIbWfCZYKWhVwFuvPX1gETRYPw6LlaTKoxD3s2AkXzkCjnp6h0V77ozyqj0jakteJ4YqDJT830+lVGw==",
"license": "MIT",
"engines": {
"node": ">=14"
}
},
"node_modules/js-tokens": { "node_modules/js-tokens": {
"version": "4.0.0", "version": "4.0.0",
"resolved": "https://registry.npmmirror.com/js-tokens/-/js-tokens-4.0.0.tgz", "resolved": "https://registry.npmmirror.com/js-tokens/-/js-tokens-4.0.0.tgz",
@ -6872,6 +6953,26 @@
"json-buffer": "3.0.1" "json-buffer": "3.0.1"
} }
}, },
"node_modules/konva": {
"version": "9.3.22",
"resolved": "https://registry.npmmirror.com/konva/-/konva-9.3.22.tgz",
"integrity": "sha512-yQI5d1bmELlD/fowuyfOp9ff+oamg26WOCkyqUyc+nczD/lhRa3EvD2MZOoc4c1293TAubW9n34fSQLgSeEgSw==",
"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/lazy-val": { "node_modules/lazy-val": {
"version": "1.0.5", "version": "1.0.5",
"resolved": "https://registry.npmmirror.com/lazy-val/-/lazy-val-1.0.5.tgz", "resolved": "https://registry.npmmirror.com/lazy-val/-/lazy-val-1.0.5.tgz",
@ -6970,7 +7071,6 @@
"version": "4.17.21", "version": "4.17.21",
"resolved": "https://registry.npmmirror.com/lodash/-/lodash-4.17.21.tgz", "resolved": "https://registry.npmmirror.com/lodash/-/lodash-4.17.21.tgz",
"integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==",
"dev": true,
"license": "MIT" "license": "MIT"
}, },
"node_modules/lodash.defaults": { "node_modules/lodash.defaults": {
@ -8804,6 +8904,12 @@
"react": "^18.3.1" "react": "^18.3.1"
} }
}, },
"node_modules/react-fast-compare": {
"version": "3.2.2",
"resolved": "https://registry.npmmirror.com/react-fast-compare/-/react-fast-compare-3.2.2.tgz",
"integrity": "sha512-nsO+KSNgo1SbJqJEYRE9ERzo7YtYbou/OqjSQKxV7jcKox7+usiUVZOAC+XnDOABXggQTno0Y1CpVnuWEc1boQ==",
"license": "MIT"
},
"node_modules/react-is": { "node_modules/react-is": {
"version": "16.13.1", "version": "16.13.1",
"resolved": "https://registry.npmmirror.com/react-is/-/react-is-16.13.1.tgz", "resolved": "https://registry.npmmirror.com/react-is/-/react-is-16.13.1.tgz",
@ -8811,6 +8917,53 @@
"dev": true, "dev": true,
"license": "MIT" "license": "MIT"
}, },
"node_modules/react-konva": {
"version": "18.2.12",
"resolved": "https://registry.npmmirror.com/react-konva/-/react-konva-18.2.12.tgz",
"integrity": "sha512-tszrM/emkX1u2reJTn3M9nMG9kuFv09s974dUEXK7luIN3z0VRD8PUjwyaLWi8Ba52ntQceZ0nfYWC6VlPa3vA==",
"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.npmmirror.com/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-refresh": { "node_modules/react-refresh": {
"version": "0.17.0", "version": "0.17.0",
"resolved": "https://registry.npmmirror.com/react-refresh/-/react-refresh-0.17.0.tgz", "resolved": "https://registry.npmmirror.com/react-refresh/-/react-refresh-0.17.0.tgz",
@ -9217,6 +9370,18 @@
"loose-envify": "^1.1.0" "loose-envify": "^1.1.0"
} }
}, },
"node_modules/screenfull": {
"version": "5.2.0",
"resolved": "https://registry.npmmirror.com/screenfull/-/screenfull-5.2.0.tgz",
"integrity": "sha512-9BakfsO2aUQN2K9Fdbj87RJIEZ82Q9IGim7FqM5OsebfoFC6ZHXgDq/KvniuLTPdeM8wY2o6Dj3WQ7KeQCj3cA==",
"license": "MIT",
"engines": {
"node": ">=0.10.0"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/scroll-into-view-if-needed": { "node_modules/scroll-into-view-if-needed": {
"version": "3.1.0", "version": "3.1.0",
"resolved": "https://registry.npmmirror.com/scroll-into-view-if-needed/-/scroll-into-view-if-needed-3.1.0.tgz", "resolved": "https://registry.npmmirror.com/scroll-into-view-if-needed/-/scroll-into-view-if-needed-3.1.0.tgz",
@ -10006,6 +10171,12 @@
"utf8-byte-length": "^1.0.1" "utf8-byte-length": "^1.0.1"
} }
}, },
"node_modules/tslib": {
"version": "2.8.1",
"resolved": "https://registry.npmmirror.com/tslib/-/tslib-2.8.1.tgz",
"integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==",
"license": "0BSD"
},
"node_modules/type-check": { "node_modules/type-check": {
"version": "0.4.0", "version": "0.4.0",
"resolved": "https://registry.npmmirror.com/type-check/-/type-check-0.4.0.tgz", "resolved": "https://registry.npmmirror.com/type-check/-/type-check-0.4.0.tgz",
@ -10625,6 +10796,35 @@
"engines": { "engines": {
"node": ">= 10" "node": ">= 10"
} }
},
"node_modules/zustand": {
"version": "5.0.8",
"resolved": "https://registry.npmmirror.com/zustand/-/zustand-5.0.8.tgz",
"integrity": "sha512-gyPKpIaxY9XcO2vSMrLbiER7QMAMGOQZVRdJ6Zi782jkbzZygq5GI9nG8g+sMgitRtndwaBSl7uiqC49o1SSiw==",
"license": "MIT",
"engines": {
"node": ">=12.20.0"
},
"peerDependencies": {
"@types/react": ">=18.0.0",
"immer": ">=9.0.6",
"react": ">=18.0.0",
"use-sync-external-store": ">=1.2.0"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
},
"immer": {
"optional": true
},
"react": {
"optional": true
},
"use-sync-external-store": {
"optional": true
}
}
} }
} }
} }

6
package.json

@ -21,12 +21,16 @@
"@ant-design/icons": "^5.6.1", "@ant-design/icons": "^5.6.1",
"@electron-toolkit/preload": "^3.0.2", "@electron-toolkit/preload": "^3.0.2",
"@electron-toolkit/utils": "^4.0.0", "@electron-toolkit/utils": "^4.0.0",
"ahooks": "^3.9.5",
"antd": "^5.27.1", "antd": "^5.27.1",
"chart.js": "^4.5.0", "chart.js": "^4.5.0",
"electron-updater": "^6.3.9", "electron-updater": "^6.3.9",
"konva": "^9.3.22",
"react": "^18.0.0", "react": "^18.0.0",
"react-chartjs-2": "^5.3.0", "react-chartjs-2": "^5.3.0",
"react-dom": "^18.0.0" "react-dom": "^18.0.0",
"react-konva": "^18.2.12",
"zustand": "^5.0.8"
}, },
"devDependencies": { "devDependencies": {
"@electron-toolkit/eslint-config": "^2.0.0", "@electron-toolkit/eslint-config": "^2.0.0",

2
src/renderer/src/common/ipcEvents.js

@ -9,5 +9,5 @@ export const IPC_EVENT = {
SENSOR_DATA: 'sensor:data', // 传感器数据 SENSOR_DATA: 'sensor:data', // 传感器数据
IMAGE_DATA: 'image:data', // 图像数据 IMAGE_DATA: 'image:data', // 图像数据
HEARTBEAT: 'heartbeat' // 心跳包(如需推送) HEARTBEAT: 'heartbeat' // 心跳包
} }

324
src/renderer/src/components/ImageCollection/ImageCollection.jsx

@ -1,17 +1,254 @@
import { useEffect, useState } from 'react' import { useEffect, useState, useRef, useCallback } from 'react'
import { IPC_EVENT } from '../../common/ipcEvents' import { IPC_EVENT } from '../../common/ipcEvents'
import { Stage, Layer, Image as KonvaImage, Rect } from 'react-konva'
import useRectangleStore from '../../stores/rectangleStore'
function ImagePreview() { function ImagePreview() {
const [imgSrc, setImgSrc] = useState(null) const [imgSrc, setImgSrc] = useState(null)
const [timestamp, setTimestamp] = useState(null) const [image, setImage] = useState(null)
const [stageSize, setStageSize] = useState({ width: 800, height: 600 })
const [imageScale, setImageScale] = useState(1)
const [imagePos, setImagePos] = useState({ x: 0, y: 0 })
const [rectangle, setRectangle] = useState(null) //
const [isDrawing, setIsDrawing] = useState(false)
const [startPos, setStartPos] = useState({ x: 0, y: 0 })
const [currentMousePos, setCurrentMousePos] = useState({ x: 0, y: 0 })
const stageRef = useRef(null)
const containerRef = useRef(null)
// 使Zustand store
const rectangleData = useRectangleStore((state) => state.rectangleData)
const setRectangleData = useRectangleStore((state) => state.setRectangleData)
// store
useEffect(() => {
if (rectangleData && image && imageScale) {
//
const displayRect = {
x: rectangleData.x * imageScale + imagePos.x,
y: rectangleData.y * imageScale + imagePos.y,
width: rectangleData.width * imageScale,
height: rectangleData.height * imageScale
}
setRectangle(displayRect)
console.log('从store更新矩形显示:', {
原始数据: rectangleData,
显示坐标: displayRect
})
} else if (!rectangleData) {
setRectangle(null)
}
}, [rectangleData, image, imageScale, imagePos.x, imagePos.y])
//
const updateSizes = useCallback(() => {
if (!containerRef.current || !image) return
const containerRect = containerRef.current.getBoundingClientRect()
const containerWidth = containerRect.width - 32 // padding
const containerHeight = window.innerHeight - containerRect.top - 100 //
//
const scaleX = containerWidth / image.width
const scaleY = containerHeight / image.height
const scale = Math.min(scaleX, scaleY, 1) //
const scaledWidth = image.width * scale
const scaledHeight = image.height * scale
//
const x = (containerWidth - scaledWidth) / 2
const y = (containerHeight - scaledHeight) / 2
setStageSize({ width: containerWidth, height: containerHeight })
setImageScale(scale)
setImagePos({ x, y })
}, [image])
//
useEffect(() => {
const handleResize = () => {
updateSizes()
}
updateSizes()
window.addEventListener('resize', handleResize)
return () => window.removeEventListener('resize', handleResize)
}, [image, updateSizes])
// Canvas
useEffect(() => { useEffect(() => {
// console.log('test') if (imgSrc) {
const handler = (event, data) => { const img = new window.Image()
// data.values.image base64 img.onload = () => {
console.log(data) setImage(img)
setImgSrc(`data:image/png;base64,${data.values.image}`) }
setTimestamp(data.values.timestamp) img.src = imgSrc
}
}, [imgSrc])
//
const handleMouseMove = (e) => {
const stage = e.target.getStage()
const point = clampPointToImage(stage.getPointerPosition())
setCurrentMousePos(point)
}
//
const onRectangleDrawComplete = (rectData) => {
//
const relativeRect = {
x: rectData.x - imagePos.x,
y: rectData.y - imagePos.y,
width: rectData.width,
height: rectData.height
}
//
const originalImageRect = {
x: relativeRect.x / imageScale,
y: relativeRect.y / imageScale,
width: relativeRect.width / imageScale,
height: relativeRect.height / imageScale
}
console.log('矩形绘制完成:')
console.log(' Stage坐标:', rectData)
console.log(' 相对于显示图片的坐标:', relativeRect)
console.log(' 相对于原始图片的坐标:', originalImageRect)
console.log(
`最终坐标: x=${Math.round(originalImageRect.x)}, y=${Math.round(originalImageRect.y)}`
)
console.log(
`最终尺寸: width=${Math.round(originalImageRect.width)}, height=${Math.round(originalImageRect.height)}`
)
// Zustand store
const rectInfo = {
x: Math.round(originalImageRect.x),
y: Math.round(originalImageRect.y),
width: Math.round(originalImageRect.width),
height: Math.round(originalImageRect.height)
}
// store使
setRectangleData(rectInfo)
}
//
const isPointInImage = (point) => {
if (!image) return false
const imageLeft = imagePos.x
const imageTop = imagePos.y
const imageRight = imagePos.x + image.width * imageScale
const imageBottom = imagePos.y + image.height * imageScale
return (
point.x >= imageLeft && point.x <= imageRight && point.y >= imageTop && point.y <= imageBottom
)
}
//
const clampPointToImage = (point) => {
if (!image) return point
const imageLeft = imagePos.x
const imageTop = imagePos.y
const imageRight = imagePos.x + image.width * imageScale
const imageBottom = imagePos.y + image.height * imageScale
return {
x: Math.max(imageLeft, Math.min(imageRight, point.x)),
y: Math.max(imageTop, Math.min(imageBottom, point.y))
}
}
//
const handleMouseDown = (e) => {
if (e.evt.button !== 0) return //
const stage = e.target.getStage()
const point = stage.getPointerPosition()
//
if (isPointInImage(point)) {
//
setRectangle(null)
// store
setRectangleData(null)
setStartPos(clampPointToImage(point))
setIsDrawing(true)
} else {
//
setRectangle(null)
setRectangleData(null)
}
}
//
const handleMouseUp = (e) => {
if (!isDrawing) return
const stage = e.target.getStage()
const point = clampPointToImage(stage.getPointerPosition())
const width = point.x - startPos.x
const height = point.y - startPos.y
//
if (Math.abs(width) > 5 && Math.abs(height) > 5) {
const newRect = {
x: Math.min(startPos.x, point.x),
y: Math.min(startPos.y, point.y),
width: Math.abs(width),
height: Math.abs(height)
}
setRectangle(newRect) //
//
onRectangleDrawComplete(newRect)
}
setIsDrawing(false)
}
//
const handleStageClick = (e) => {
//
if (isDrawing) return
const stage = e.target.getStage()
const point = stage.getPointerPosition()
//
if (!isPointInImage(point)) {
setRectangle(null)
setRectangleData(null)
}
}
//
const getCurrentRect = () => {
if (!isDrawing) return null
const width = currentMousePos.x - startPos.x
const height = currentMousePos.y - startPos.y
return {
x: Math.min(startPos.x, currentMousePos.x),
y: Math.min(startPos.y, currentMousePos.y),
width: Math.abs(width),
height: Math.abs(height)
}
}
useEffect(() => {
const handler = (_, data) => {
const base64 = data.values.image
const src = `data:image/png;base64,${base64}`
setImgSrc(src)
} }
window.electron.ipcRenderer.on(IPC_EVENT.IMAGE_DATA, handler) window.electron.ipcRenderer.on(IPC_EVENT.IMAGE_DATA, handler)
return () => { return () => {
@ -20,18 +257,69 @@ function ImagePreview() {
}, []) }, [])
return ( return (
<div style={{ textAlign: 'center', padding: 16 }}> <div
ref={containerRef}
style={{
width: '100%',
height: '100%',
display: 'flex',
justifyContent: 'center',
alignItems: 'flex-start',
padding: 16
}}
>
{imgSrc ? ( {imgSrc ? (
<> <Stage
<img width={stageSize.width}
src={imgSrc} height={stageSize.height}
alt="设备图像" onMouseMove={handleMouseMove}
style={{ maxWidth: '100%', maxHeight: 400, border: '1px solid #eee', borderRadius: 4 }} onMouseDown={handleMouseDown}
/> onMouseUp={handleMouseUp}
<div style={{ marginTop: 8, color: '#888', fontSize: 12 }}>{timestamp}</div> onClick={handleStageClick}
</> ref={stageRef}
>
<Layer>
{/* 图片 */}
{image && (
<KonvaImage
image={image}
x={imagePos.x}
y={imagePos.y}
width={image.width * imageScale}
height={image.height * imageScale}
/>
)}
{/* 当前矩形 */}
{rectangle && (
<Rect
x={rectangle.x}
y={rectangle.y}
width={rectangle.width}
height={rectangle.height}
stroke="red"
strokeWidth={2}
fill="transparent"
/>
)}
{/* 正在绘制的矩形 */}
{isDrawing && getCurrentRect() && (
<Rect
x={getCurrentRect().x}
y={getCurrentRect().y}
width={getCurrentRect().width}
height={getCurrentRect().height}
stroke="yellow"
strokeWidth={2}
fill="transparent"
dash={[5, 5]}
/>
)}
</Layer>
</Stage>
) : ( ) : (
<div style={{ color: '#aaa' }}>暂无图像数据</div> <div style={{ color: '#aaa', textAlign: 'center' }}>暂无图像数据</div>
)} )}
</div> </div>
) )

338
src/renderer/src/components/MeasurementPointSetting/MeasurementPointSetting.jsx

@ -1,5 +1,5 @@
import styles from './MeasurementPointSetting.module.css' import styles from './MeasurementPointSetting.module.css'
import { Flex, Input, Select, InputNumber, Button, Table } from 'antd' import { Flex, Input, Select, InputNumber, Button, Table, Tooltip, Modal } from 'antd'
import { import {
PlusOutlined, PlusOutlined,
MinusOutlined, MinusOutlined,
@ -8,83 +8,137 @@ import {
SendOutlined, SendOutlined,
BoxPlotFilled BoxPlotFilled
} from '@ant-design/icons' } from '@ant-design/icons'
import useRectangleStore from '../../stores/rectangleStore'
import { useState } from 'react'
function MeasurementPointSetting() { function MeasurementPointSetting() {
// // Zustand store
const dataSource = [ const rectangleData = useRectangleStore((state) => state.rectangleData)
{ const setRectangleData = useRectangleStore((state) => state.setRectangleData)
key: '1',
项目: '传感器', //
数值: '', const [formData, setFormData] = useState({
location: 1, //
description: '', //
coefficient: 0, //
baseTarget: 'n' //
})
//
const [sensorList, setSensorList] = useState([])
//
const [selectedSensorKey, setSelectedSensorKey] = useState(null)
//
const updateRectangleFromSensor = (sensorKey) => {
if (!sensorKey) {
setRectangleData(null)
return
}
//
const selectedSensor = sensorList.find((sensor) => sensor.key === sensorKey)
if (!selectedSensor) {
setRectangleData(null)
return
}
//
const baseTargetInfo = selectedSensor.children?.find((child) => child.name === '基准标靶')
if (!baseTargetInfo || !baseTargetInfo.children) {
setRectangleData(null)
return
}
//
const xData = baseTargetInfo.children.find((coord) => coord.name === 'x')
const yData = baseTargetInfo.children.find((coord) => coord.name === 'y')
const wData = baseTargetInfo.children.find((coord) => coord.name === 'w')
const hData = baseTargetInfo.children.find((coord) => coord.name === 'h')
if (xData && yData && wData && hData) {
const rectangleInfo = {
x: Number(xData.value) || 0,
y: Number(yData.value) || 0,
width: Number(wData.value) || 0,
height: Number(hData.value) || 0
}
console.log('从传感器数据更新矩形:', rectangleInfo)
setRectangleData(rectangleInfo)
} else {
setRectangleData(null)
}
}
//
const handleSensorSelect = (sensorKey) => {
setSelectedSensorKey(sensorKey)
updateRectangleFromSensor(sensorKey)
}
//
const handleFormChange = (field, value) => {
setFormData((prev) => ({
...prev,
[field]: value
}))
}
//
const handleAddSensor = () => {
//
if (!rectangleData || !rectangleData.x) {
alert('请先在图片上绘制矩形区域!')
return
}
//
const newSensorKey = String(sensorList.length + 1)
const newSensor = {
key: newSensorKey,
name: '传感器',
value: '',
children: [ children: [
{ key: '1-1', 项目: '测点位置', 数值: 1 }, { key: `${newSensorKey}-1`, name: '测点位置', value: formData.location },
{ { key: `${newSensorKey}-2`, name: '测点描述', value: formData.description },
key: '1-2', { key: `${newSensorKey}-3`, name: '计算系数', value: formData.coefficient },
项目: '测点描述',
数值: '我去年买了个表'
},
{ key: '1-3', 项目: '计算系数', 数值: 0.448 },
{ {
key: '1-4', key: `${newSensorKey}-4`,
项目: '基准标靶', name: '基准标靶',
数值: 'n', value: formData.baseTarget,
children: [ children: [
{ key: '1-4-1', 项目: 'x', 数值: 349 }, { key: `${newSensorKey}-4-1`, name: 'x', value: rectangleData.x },
{ key: '1-4-2', 项目: 'y', 数值: 1108 }, { key: `${newSensorKey}-4-2`, name: 'y', value: rectangleData.y },
{ key: '1-4-3', 项目: 'w', 数值: 125 }, { key: `${newSensorKey}-4-3`, name: 'w', value: rectangleData.width },
{ key: '1-4-4', 项目: 'h', 数值: 115 } { key: `${newSensorKey}-4-4`, name: 'h', value: rectangleData.height }
] ]
} }
] ]
},
{
key: '2',
项目: '传感器',
数值: ''
},
{
key: '3',
项目: '传感器',
数值: ''
},
{
key: '4',
项目: '传感器',
数值: ''
},
{
key: '5',
项目: '传感器',
数值: ''
},
{
key: '6',
项目: '传感器',
数值: ''
},
{
key: '7',
项目: '传感器',
数值: ''
},
{
key: '8',
项目: '传感器',
数值: ''
},
{
key: '9',
项目: '传感器',
数值: ''
} }
]
//
setSensorList((prev) => [...prev, newSensor])
//
setFormData((prev) => ({
location: prev.location + 1, //
description: '',
coefficient: 0,
baseTarget: 'n'
}))
}
// 使
const dataSource = sensorList
// //
const columns = [ const columns = [
{ {
title: '项目', title: '项目',
dataIndex: '项目', dataIndex: 'name',
key: '项目', key: 'name',
width: 120, width: 120,
render: (text, record) => { render: (text, record) => {
// //
@ -107,8 +161,8 @@ function MeasurementPointSetting() {
}, },
{ {
title: '数值', title: '数值',
dataIndex: '数值', dataIndex: 'value',
key: '数值', key: 'value',
render: (value, record) => { render: (value, record) => {
// //
const level = record.key.split('-').length - 1 const level = record.key.split('-').length - 1
@ -121,13 +175,75 @@ function MeasurementPointSetting() {
color: level === 0 ? '#333' : level === 1 ? '#666' : '#999' color: level === 0 ? '#333' : level === 1 ? '#666' : '#999'
}} }}
> >
{value || ''} {value !== null && value !== undefined ? value : ''}
</span> </span>
) )
} }
} }
] ]
//
const handleDel = () => {
if (!selectedSensorKey) {
Modal.info({
title: '提示',
content: '请先选择要删除的传感器!',
centered: true
})
return
}
Modal.confirm({
title: '删除确认',
content: '是否要删除该传感器?',
okText: '确定',
cancelText: '取消',
centered: true,
onOk: () => {
//
setSensorList((prev) => prev.filter((sensor) => sensor.key !== selectedSensorKey))
setSelectedSensorKey(null)
console.log('删除传感器:', selectedSensorKey)
},
onCancel: () => {
console.log('取消删除')
}
})
}
//
const handleClear = () => {
if (sensorList.length === 0) {
Modal.info({
title: '提示',
content: '传感器列表已经为空!',
centered: true
})
return
}
Modal.confirm({
title: '清空确认',
content: '是否要清空所有传感器?',
okText: '确定',
cancelText: '取消',
centered: true,
onOk: () => {
//
setSensorList([])
setSelectedSensorKey(null)
console.log('清空传感器列表')
},
onCancel: () => {
console.log('取消清空')
}
})
}
const handleLoad = () => {}
const handleSet = () => {}
return ( return (
<Flex vertical className={styles.container}> <Flex vertical className={styles.container}>
{/* 标题 */} {/* 标题 */}
@ -144,19 +260,30 @@ function MeasurementPointSetting() {
<Flex className={styles.formRow}> <Flex className={styles.formRow}>
<span className={styles.label}>测点位置:</span> <span className={styles.label}>测点位置:</span>
<Select className={styles.select} defaultValue="1" style={{ flex: 1 }} options={[]} /> <InputNumber
className={styles.numberInput}
value={formData.location}
onChange={(value) => handleFormChange('location', value)}
style={{ flex: 1 }}
/>
</Flex> </Flex>
<Flex className={styles.formRow}> <Flex className={styles.formRow}>
<span className={styles.label}>测点描述:</span> <span className={styles.label}>测点描述:</span>
<Input className={styles.input} style={{ flex: 1 }} /> <Input
className={styles.input}
value={formData.description}
onChange={(e) => handleFormChange('description', e.target.value)}
style={{ flex: 1 }}
/>
</Flex> </Flex>
<Flex className={styles.formRow}> <Flex className={styles.formRow}>
<span className={styles.label}>计算系数:</span> <span className={styles.label}>计算系数:</span>
<InputNumber <InputNumber
className={styles.numberInput} className={styles.numberInput}
defaultValue={0} value={formData.coefficient}
onChange={(value) => handleFormChange('coefficient', value)}
precision={4} precision={4}
style={{ flex: 1 }} style={{ flex: 1 }}
/> />
@ -166,7 +293,8 @@ function MeasurementPointSetting() {
<span className={styles.label}>基准标靶:</span> <span className={styles.label}>基准标靶:</span>
<Select <Select
className={styles.select} className={styles.select}
defaultValue="n" value={formData.baseTarget}
onChange={(value) => handleFormChange('baseTarget', value)}
style={{ flex: 1 }} style={{ flex: 1 }}
options={[ options={[
{ value: 'y', label: 'y' }, { value: 'y', label: 'y' },
@ -184,7 +312,7 @@ function MeasurementPointSetting() {
<InputNumber <InputNumber
addonBefore={'X'} addonBefore={'X'}
className={styles.numberInput} className={styles.numberInput}
defaultValue={0} value={rectangleData?.x || 0}
style={{ flex: 1 }} style={{ flex: 1 }}
/> />
</Flex> </Flex>
@ -192,7 +320,7 @@ function MeasurementPointSetting() {
<InputNumber <InputNumber
addonBefore={'W'} addonBefore={'W'}
className={styles.numberInput} className={styles.numberInput}
defaultValue={0} value={rectangleData?.width || 0}
style={{ flex: 1 }} style={{ flex: 1 }}
/> />
</Flex> </Flex>
@ -203,7 +331,7 @@ function MeasurementPointSetting() {
<InputNumber <InputNumber
addonBefore={'Y'} addonBefore={'Y'}
className={styles.numberInput} className={styles.numberInput}
defaultValue={0} value={rectangleData?.y || 0}
style={{ flex: 1 }} style={{ flex: 1 }}
/> />
</Flex> </Flex>
@ -211,7 +339,7 @@ function MeasurementPointSetting() {
<InputNumber <InputNumber
addonBefore={'H'} addonBefore={'H'}
className={styles.numberInput} className={styles.numberInput}
defaultValue={0} value={rectangleData?.height || 0}
style={{ flex: 1 }} style={{ flex: 1 }}
/> />
</Flex> </Flex>
@ -229,10 +357,9 @@ function MeasurementPointSetting() {
defaultValue="50" defaultValue="50"
style={{ flex: 1 }} style={{ flex: 1 }}
options={[ options={[
{ value: '25', label: '25' },
{ value: '50', label: '50' }, { value: '50', label: '50' },
{ value: '75', label: '75' }, { value: '125', label: '125' },
{ value: '100', label: '100' } { value: '200', label: '200' }
]} ]}
/> />
</Flex> </Flex>
@ -262,15 +389,60 @@ function MeasurementPointSetting() {
indentSize: '2em' indentSize: '2em'
}} }}
scroll={{ y: 220 }} scroll={{ y: 220 }}
rowSelection={{
type: 'radio',
selectedRowKeys: selectedSensorKey ? [selectedSensorKey] : [],
onChange: (selectedRowKeys) => {
//
const selectedKey = selectedRowKeys[0]
if (selectedKey && !selectedKey.includes('-')) {
handleSensorSelect(selectedKey)
}
},
getCheckboxProps: (record) => ({
//
disabled: record.key.includes('-')
}),
hideSelectAll: true,
renderCell: (checked, record, index, originNode) => {
//
if (record.key.includes('-')) {
return null
}
return originNode
}
}}
onRow={(record) => ({
onClick: () => {
//
if (!record.key.includes('-')) {
handleSensorSelect(record.key)
}
},
style: {
cursor: record.key.includes('-') ? 'default' : 'pointer',
backgroundColor: selectedSensorKey === record.key ? '#e6f7ff' : 'transparent'
}
})}
/> />
{/* 操作按钮 */} {/* 操作按钮 */}
<Flex className={styles.actionButtons} justify="center"> <Flex className={styles.actionButtons} justify="center">
<Button icon={<PlusOutlined />} shape="circle" /> <Tooltip title="添加传感器">
<Button icon={<MinusOutlined />} shape="circle" /> <Button icon={<PlusOutlined />} shape="circle" onClick={handleAddSensor} />
<Button icon={<DeleteOutlined />} shape="circle" /> </Tooltip>
<Button icon={<ReloadOutlined />} shape="circle" /> <Tooltip title="删除传感器">
<Button icon={<SendOutlined />} shape="circle" /> <Button icon={<MinusOutlined />} shape="circle" onClick={handleDel} />
</Tooltip>
<Tooltip title="清空列表">
<Button icon={<DeleteOutlined />} shape="circle" onClick={handleClear} />
</Tooltip>
<Tooltip title="加载传感器">
<Button icon={<ReloadOutlined />} shape="circle" onClick={handleLoad} />
</Tooltip>
<Tooltip title="设置传感器">
<Button icon={<SendOutlined />} shape="circle" onClick={handleSet} />
</Tooltip>
</Flex> </Flex>
</div> </div>
</Flex> </Flex>

9
src/renderer/src/stores/rectangleStore.js

@ -0,0 +1,9 @@
import { create } from 'zustand'
const useRectangleStore = create((set) => ({
rectangleData: null,
setRectangleData: (data) => set({ rectangleData: data }),
clearRectangleData: () => set({ rectangleData: null })
}))
export default useRectangleStore
Loading…
Cancel
Save