测距功能开发

This commit is contained in:
yyy 2025-02-14 17:48:28 +08:00
parent 77d34e3971
commit a769b54e7a
10 changed files with 398 additions and 201 deletions

View File

@ -4,7 +4,7 @@ NODE_ENV=development
VITE_DEV=true VITE_DEV=true
# 请求路径 # 请求路径
VITE_BASE_URL='http://192.168.0.74:48080' VITE_BASE_URL='http://192.168.0.66:48080'
# VITE_BASE_URL='http://192.168.0.189:48080' # VITE_BASE_URL='http://192.168.0.189:48080'
# 文件上传类型server - 后端上传, client - 前端直连上传,仅支持 S3 服务 # 文件上传类型server - 后端上传, client - 前端直连上传,仅支持 S3 服务

30
package-lock.json generated
View File

@ -36,6 +36,7 @@
"fast-xml-parser": "^4.3.2", "fast-xml-parser": "^4.3.2",
"highlight.js": "^11.9.0", "highlight.js": "^11.9.0",
"jsencrypt": "^3.3.2", "jsencrypt": "^3.3.2",
"json-bigint": "^1.0.0",
"lodash-es": "^4.17.21", "lodash-es": "^4.17.21",
"markdown-it": "^14.1.0", "markdown-it": "^14.1.0",
"markmap-common": "^0.16.0", "markmap-common": "^0.16.0",
@ -6691,6 +6692,14 @@
"node": "*" "node": "*"
} }
}, },
"node_modules/bignumber.js": {
"version": "9.1.2",
"resolved": "https://registry.npmmirror.com/bignumber.js/-/bignumber.js-9.1.2.tgz",
"integrity": "sha512-2/mKyZH9K85bzOEfhXDBFZTGd1CTs+5IHpeFQo9luiBG7hghdC851Pj2WAhb6E3R6b9tZj/XKhbg4fum+Kepug==",
"engines": {
"node": "*"
}
},
"node_modules/binary-extensions": { "node_modules/binary-extensions": {
"version": "2.3.0", "version": "2.3.0",
"resolved": "https://registry.npmmirror.com/binary-extensions/-/binary-extensions-2.3.0.tgz", "resolved": "https://registry.npmmirror.com/binary-extensions/-/binary-extensions-2.3.0.tgz",
@ -11700,6 +11709,14 @@
"node": ">=6" "node": ">=6"
} }
}, },
"node_modules/json-bigint": {
"version": "1.0.0",
"resolved": "https://registry.npmmirror.com/json-bigint/-/json-bigint-1.0.0.tgz",
"integrity": "sha512-SiPv/8VpZuWbvLSMtTDU8hEfrZWg/mH/nV/b4o0CYbSxu1UIQPLdwKOCIyLQX+VIPO5vrLX3i8qtqFyhdPSUSQ==",
"dependencies": {
"bignumber.js": "^9.0.0"
}
},
"node_modules/json-buffer": { "node_modules/json-buffer": {
"version": "3.0.1", "version": "3.0.1",
"resolved": "https://registry.npmmirror.com/json-buffer/-/json-buffer-3.0.1.tgz", "resolved": "https://registry.npmmirror.com/json-buffer/-/json-buffer-3.0.1.tgz",
@ -23149,6 +23166,11 @@
"integrity": "sha512-vyL2OymJxmarO8gxMr0mhChsO9QGwhynfuu4+MHTAW6czfq9humCB7rKpUjDd9YUiDPU4mzpyupFSvOClAwbmQ==", "integrity": "sha512-vyL2OymJxmarO8gxMr0mhChsO9QGwhynfuu4+MHTAW6czfq9humCB7rKpUjDd9YUiDPU4mzpyupFSvOClAwbmQ==",
"dev": true "dev": true
}, },
"bignumber.js": {
"version": "9.1.2",
"resolved": "https://registry.npmmirror.com/bignumber.js/-/bignumber.js-9.1.2.tgz",
"integrity": "sha512-2/mKyZH9K85bzOEfhXDBFZTGd1CTs+5IHpeFQo9luiBG7hghdC851Pj2WAhb6E3R6b9tZj/XKhbg4fum+Kepug=="
},
"binary-extensions": { "binary-extensions": {
"version": "2.3.0", "version": "2.3.0",
"resolved": "https://registry.npmmirror.com/binary-extensions/-/binary-extensions-2.3.0.tgz", "resolved": "https://registry.npmmirror.com/binary-extensions/-/binary-extensions-2.3.0.tgz",
@ -26748,6 +26770,14 @@
"integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==",
"dev": true "dev": true
}, },
"json-bigint": {
"version": "1.0.0",
"resolved": "https://registry.npmmirror.com/json-bigint/-/json-bigint-1.0.0.tgz",
"integrity": "sha512-SiPv/8VpZuWbvLSMtTDU8hEfrZWg/mH/nV/b4o0CYbSxu1UIQPLdwKOCIyLQX+VIPO5vrLX3i8qtqFyhdPSUSQ==",
"requires": {
"bignumber.js": "^9.0.0"
}
},
"json-buffer": { "json-buffer": {
"version": "3.0.1", "version": "3.0.1",
"resolved": "https://registry.npmmirror.com/json-buffer/-/json-buffer-3.0.1.tgz", "resolved": "https://registry.npmmirror.com/json-buffer/-/json-buffer-3.0.1.tgz",

View File

@ -52,6 +52,7 @@
"fast-xml-parser": "^4.3.2", "fast-xml-parser": "^4.3.2",
"highlight.js": "^11.9.0", "highlight.js": "^11.9.0",
"jsencrypt": "^3.3.2", "jsencrypt": "^3.3.2",
"json-bigint": "^1.0.0",
"lodash-es": "^4.17.21", "lodash-es": "^4.17.21",
"markdown-it": "^14.1.0", "markdown-it": "^14.1.0",
"markmap-common": "^0.16.0", "markmap-common": "^0.16.0",

View File

@ -220,4 +220,5 @@ const handleAuthorized = () => {
} }
return Promise.reject(t('sys.api.timeoutMessage')) return Promise.reject(t('sys.api.timeoutMessage'))
} }
export { service } export { service }

View File

@ -2,10 +2,10 @@
<Dialog v-model="dialogFormVisible" title="节点属性" width="540" class="node-form-dialog"> <Dialog v-model="dialogFormVisible" title="节点属性" width="540" class="node-form-dialog">
<el-form :model="form" label-width="auto" ref="ruleFormRef"> <el-form :model="form" label-width="auto" ref="ruleFormRef">
<el-form-item label="X" prop="locationX" required> <el-form-item label="X" prop="locationX" required>
<el-input v-model="form.locationX" placeholder="请输入" /> <el-input type="number" v-model="form.locationX" placeholder="请输入" />
</el-form-item> </el-form-item>
<el-form-item label="Y" prop="locationY" required> <el-form-item label="Y" prop="locationY" required>
<el-input v-model="form.locationY" placeholder="请输入" /> <el-input type="number" v-model="form.locationY" placeholder="请输入" />
</el-form-item> </el-form-item>
<el-form-item label="类型" prop="type" required> <el-form-item label="类型" prop="type" required>
<el-select v-model="form.type" placeholder="请选择类型" @change="typeChange"> <el-select v-model="form.type" placeholder="请选择类型" @change="typeChange">
@ -72,13 +72,13 @@
</el-form-item> </el-form-item>
<el-form-item label="库位长度" prop="locationDeep"> <el-form-item label="库位长度" prop="locationDeep">
<div style="display: flex"> <div style="display: flex">
<el-input v-model="form.locationDeep" placeholder="请输入" /> <el-input type="number" v-model="form.locationDeep" placeholder="请输入" />
<span class="ml-2">cm</span> <span class="ml-2">cm</span>
</div> </div>
</el-form-item> </el-form-item>
<el-form-item label="库位宽度" prop="locationWide"> <el-form-item label="库位宽度" prop="locationWide">
<div style="display: flex"> <div style="display: flex">
<el-input v-model="form.locationWide" placeholder="请输入" /> <el-input type="number" v-model="form.locationWide" placeholder="请输入" />
<span class="ml-2">cm</span> <span class="ml-2">cm</span>
</div> </div>
</el-form-item> </el-form-item>
@ -188,7 +188,7 @@ const submit = async (formEl) => {
form.value.dataList[index] && form.value.dataList[index] &&
form.value.dataList[index].id form.value.dataList[index].id
) { ) {
item.id = form.value.dataList[index].id item.id = String(form.value.dataList[index].id)
item.locationNo = form.value.dataList[index].locationNo item.locationNo = form.value.dataList[index].locationNo
item.mapItemId = form.value.dataList[index].mapItemId item.mapItemId = form.value.dataList[index].mapItemId
item.laneId = form.value.dataList[index].laneId item.laneId = form.value.dataList[index].laneId

View File

@ -6,7 +6,7 @@
width="600" width="600"
class="equipment-form-dialog" class="equipment-form-dialog"
> >
<el-form :model="form" label-width="110"> <el-form :model="form" label-width="110" ref="ruleFormRef">
<el-form-item label="物料区域名称" prop="skuInfo" required> <el-form-item label="物料区域名称" prop="skuInfo" required>
<el-input v-model="form.skuInfo" placeholder="请输入物料区域名称" /> <el-input v-model="form.skuInfo" placeholder="请输入物料区域名称" />
</el-form-item> </el-form-item>
@ -20,7 +20,7 @@
<template #footer> <template #footer>
<div class="dialog-footer"> <div class="dialog-footer">
<el-button @click="dialogFormVisible = false">取消</el-button> <el-button @click="dialogFormVisible = false">取消</el-button>
<el-button type="primary" @click="submitForm"> 确定 </el-button> <el-button type="primary" @click="submitForm(ruleFormRef)"> 确定 </el-button>
</div> </div>
</template> </template>
</Dialog> </Dialog>
@ -39,8 +39,14 @@ const props = defineProps({
} }
}) })
const ruleFormRef = ref()
const dialogFormVisible = ref(false) // const dialogFormVisible = ref(false) //
const rules = reactive({
skuInfo: [{ required: true, message: '请输入物料区域名称', trigger: 'blur' }],
areaName: [{ required: true, message: '请输入物料名称', trigger: 'blur' }]
})
// //
const form = ref({ const form = ref({
positionMapId: '', positionMapId: '',
@ -56,11 +62,16 @@ const open = (list) => {
form.value.areaNumber = form.value.mapItemIds.length form.value.areaNumber = form.value.mapItemIds.length
} }
const submitForm = async () => { const submitForm = async (formEl) => {
form.value.positionMapId = props.positionMapId if (!formEl) return
await MapApi.createOrEditOrDelHouseArea(form.value) await formEl.validate(async (valid, fields) => {
dialogFormVisible.value = false if (valid) {
message.success('设置成功') form.value.positionMapId = props.positionMapId
await MapApi.createOrEditOrDelHouseArea(form.value)
dialogFormVisible.value = false
message.success('设置成功')
}
})
} }
defineExpose({ open }) // open defineExpose({ open }) // open

View File

@ -6,7 +6,7 @@
width="600" width="600"
class="equipment-form-dialog" class="equipment-form-dialog"
> >
<el-form :model="form" label-width="110"> <el-form :model="form" label-width="110" ref="lineFormRef" :rules="rules">
<el-form-item label="线库名称" prop="laneName" required> <el-form-item label="线库名称" prop="laneName" required>
<el-input v-model="form.laneName" placeholder="请输入线库名称" /> <el-input v-model="form.laneName" placeholder="请输入线库名称" />
</el-form-item> </el-form-item>
@ -17,7 +17,7 @@
<template #footer> <template #footer>
<div class="dialog-footer"> <div class="dialog-footer">
<el-button @click="dialogFormVisible = false">取消</el-button> <el-button @click="dialogFormVisible = false">取消</el-button>
<el-button type="primary" @click="submitForm"> 确定 </el-button> <el-button type="primary" @click="submitLineLibraryForm()"> 确定 </el-button>
</div> </div>
</template> </template>
</Dialog> </Dialog>
@ -36,8 +36,13 @@ const props = defineProps({
} }
}) })
const lineFormRef = ref()
const dialogFormVisible = ref(false) // const dialogFormVisible = ref(false) //
const rules = reactive({
laneName: [{ required: true, message: '请输入线库名称', trigger: 'blur' }]
})
// //
const form = ref({ const form = ref({
positionMapId: '', positionMapId: '',
@ -52,11 +57,15 @@ const open = (list) => {
form.value.areaNumber = form.value.mapItemIds.length form.value.areaNumber = form.value.mapItemIds.length
} }
const submitForm = async () => { const submitLineLibraryForm = async () => {
form.value.positionMapId = props.positionMapId await lineFormRef.value.validate(async (valid, fields) => {
await MapApi.createOrEditOrDelHouseLane(form.value) if (valid) {
dialogFormVisible.value = false form.value.positionMapId = props.positionMapId
message.success('设置成功') await MapApi.createOrEditOrDelHouseLane(form.value)
dialogFormVisible.value = false
message.success('设置成功')
}
})
} }
defineExpose({ open }) // open defineExpose({ open }) // open

View File

@ -240,38 +240,72 @@
{{ item.text }} {{ item.text }}
</div> </div>
</VueDragResizeRotate> </VueDragResizeRotate>
<!-- 文档 https://github.com/a7650/vue3-draggable-resizable/blob/main/docs/document_zh.md#resizable -->
</div> </div>
<!-- 框选区域 --> <!-- 框选区域 -->
<div <template>
v-for="(box, index) in state.allDrawSelectionAreaBox" <div
:key="index" v-for="(box, index) in state.allDrawSelectionAreaBox"
:style="{ :key="index"
position: 'absolute', :style="{
left: `${box.x}px`, position: 'absolute',
top: `${box.y}px`, left: `${box.x}px`,
width: `${box.width}px`, top: `${box.y}px`,
height: `${box.height}px`, width: `${box.width}px`,
border: '2px dashed #007bff', height: `${box.height}px`,
backgroundColor: 'rgba(0, 123, 255, 0.1)', border: '2px dashed #007bff',
zIndex: 999 backgroundColor: 'rgba(0, 123, 255, 0.1)',
}" zIndex: 999
></div> }"
<!-- 当前框选区域 --> ></div>
<div <!-- 当前框选区域 -->
v-if="state.drawSelectionAreaShow" <div
:style="{ v-if="state.drawSelectionAreaShow"
position: 'absolute', :style="{
left: `${state.drawSelectionAreaBox.x}px`, position: 'absolute',
top: `${state.drawSelectionAreaBox.y}px`, left: `${state.drawSelectionAreaBox.x}px`,
width: `${state.drawSelectionAreaBox.width}px`, top: `${state.drawSelectionAreaBox.y}px`,
height: `${state.drawSelectionAreaBox.height}px`, width: `${state.drawSelectionAreaBox.width}px`,
border: '2px dashed #007bff', height: `${state.drawSelectionAreaBox.height}px`,
backgroundColor: 'rgba(0, 123, 255, 0.1)', border: '2px dashed #007bff',
zIndex: 999 backgroundColor: 'rgba(0, 123, 255, 0.1)',
}" zIndex: 999
></div> }"
></div>
</template>
<!-- 绘制点和连线 -->
<template v-if="state.measureDistancesPoints.length > 0">
<div
v-for="(point, index) in state.measureDistancesPoints"
:key="index"
:style="{
position: 'absolute',
left: `${point.x}px`,
top: `${point.y}px`,
width: '10px',
height: '10px',
backgroundColor: '#00329F',
borderRadius: '50%'
}"
></div>
<div
v-if="state.measureDistancesPoints.length === 2"
:style="{
position: 'absolute',
left: `${state.measureDistancesPoints[0].x + 5}px`,
top: `${state.measureDistancesPoints[0].y + 5}px`,
width: `${computedLineWidth}px`,
height: '2px',
backgroundColor: '#00329F',
transform: `rotate(${computedLineAngle}deg)`,
transformOrigin: '0 0',
textAlign: 'center'
}"
>
距离{{ state.measureDistancesNum.toFixed(2) }}</div
>
</template>
<!-- 文字输入区域 --> <!-- 文字输入区域 -->
<input <input
@ -323,6 +357,7 @@
</template> </template>
<script setup> <script setup>
import JSONBigInt from 'json-bigint'
import { ref, defineComponent, reactive, nextTick, onMounted } from 'vue' import { ref, defineComponent, reactive, nextTick, onMounted } from 'vue'
import editNodeProperties from './components-tool/editNodeProperties.vue' import editNodeProperties from './components-tool/editNodeProperties.vue'
import textFormToolDialog from './components-tool/textFormToolDialog.vue' import textFormToolDialog from './components-tool/textFormToolDialog.vue'
@ -465,6 +500,10 @@ const mapClick = (e) => {
inputBoxRef.value.focus() inputBoxRef.value.focus()
}, 0) }, 0)
} }
//
if (toolbarSwitchType.value === 'ranging') {
measureDistancesClick(e)
}
} }
// //
const textFormSuccess = (form) => { const textFormSuccess = (form) => {
@ -697,7 +736,9 @@ const state = reactive({
locationY: 0 locationY: 0
}, // }, //
inputBoxValue: '', // inputBoxValue: '', //
isShowLayer: false // isShowLayer: false, //
measureDistancesPoints: [], //
measureDistancesNum: 0 //
}) })
const toolbarClick = (item) => { const toolbarClick = (item) => {
let type = item.switchType let type = item.switchType
@ -736,6 +777,11 @@ const toolbarClick = (item) => {
state.allDrawSelectionAreaBox = [] state.allDrawSelectionAreaBox = []
state.drawSelectionPointList = [] state.drawSelectionPointList = []
} }
//
if (toolbarSwitchType.value !== 'ranging') {
state.measureDistancesPoints = [] //
state.measureDistancesNum = 0 //
}
switch (type) { switch (type) {
case 'open': case 'open':
@ -968,9 +1014,13 @@ const clickDrawSelectionArea = () => {
//线 //线
if (binLocation.length < 2) { if (binLocation.length < 2) {
message.warning('至少选择两个库位') message.warning('至少选择两个库位')
} else { return
lineLibrarySettingDialogRef.value.open(binLocation)
} }
if (!isStraightLine(binLocation)) {
message.warning('您选择的库位不在一条直线上')
return
}
lineLibrarySettingDialogRef.value.open(binLocation)
} }
if (toolbarSwitchType.value === 'region') { if (toolbarSwitchType.value === 'region') {
// //
@ -981,6 +1031,89 @@ const clickDrawSelectionArea = () => {
} }
} }
} }
//线
const isStraightLine = (binLocation) => {
if (binLocation.length <= 2) {
return true // 线
}
const firstPoint = binLocation[0]
const secondPoint = binLocation[1]
// 线 x
const isVertical = binLocation.every(
(point) => Number(point.locationX) === Number(firstPoint.locationX)
)
if (isVertical) {
return true
}
// 线 y
const isHorizontal = binLocation.every(
(point) => Number(point.locationY) === Number(firstPoint.locationY)
)
if (isHorizontal) {
return true
}
//
const slope =
Number(secondPoint.locationY) -
Number(firstPoint.locationY) / (Number(secondPoint.locationX) - Number(firstPoint.locationX))
// 线
return binLocation.every((point) => {
const currentSlope =
(Number(point.locationY) - Number(firstPoint.locationY)) /
(Number(point.locationX) - Number(firstPoint.locationX))
return Math.abs(currentSlope - slope) < Number.EPSILON //
})
}
//
//
const calculateDistance = (point1, point2) => {
const dx = point2.x - point1.x
const dy = point2.y - point1.y
return Math.sqrt(dx * dx + dy * dy)
}
// 线
const computedLineAngle = computed(() => {
if (state.measureDistancesPoints.length === 2) {
const dx = state.measureDistancesPoints[1].x - state.measureDistancesPoints[0].x
const dy = state.measureDistancesPoints[1].y - state.measureDistancesPoints[0].y
return Math.atan2(dy, dx) * (180 / Math.PI)
}
return 0
})
// 线
const computedLineWidth = computed(() => {
if (state.measureDistancesPoints.length === 2) {
return calculateDistance(state.measureDistancesPoints[0], state.measureDistancesPoints[1])
}
return 0
})
//
const measureDistancesClick = (event) => {
if (state.measureDistancesPoints.length === 2) {
//
state.measureDistancesPoints = []
state.measureDistancesNum = null
} else {
//
state.measureDistancesPoints.push({ x: event.offsetX, y: event.offsetY })
if (state.measureDistancesPoints.length === 2) {
//
state.measureDistancesNum = calculateDistance(
state.measureDistancesPoints[0],
state.measureDistancesPoints[1]
)
}
}
}
// //
const imgBgObj = reactive({ const imgBgObj = reactive({
@ -1046,7 +1179,7 @@ const getAllNodeList = async () => {
item.lockAspectRatio = true item.lockAspectRatio = true
} else if (item.type === 2) { } else if (item.type === 2) {
// //
item.dataList = JSON.parse(item.dataJson) item.dataList = JSONBigInt({ storeAsString: true }).parse(item.dataJson)
item.locationDeep = item.dataList[0].locationDeep item.locationDeep = item.dataList[0].locationDeep
item.locationWide = item.dataList[0].locationWide item.locationWide = item.dataList[0].locationWide
item.draggable = true item.draggable = true
@ -1054,7 +1187,7 @@ const getAllNodeList = async () => {
item.rotatable = false item.rotatable = false
item.lockAspectRatio = true item.lockAspectRatio = true
} else if (item.type === 3 || item.type === 4) { } else if (item.type === 3 || item.type === 4) {
item.dataObj = JSON.parse(item.dataJson) item.dataObj = JSONBigInt({ storeAsString: true }).parse(item.dataJson)
item.dataList = [] item.dataList = []
item.locationDeep = item.dataObj.locationDeep item.locationDeep = item.dataObj.locationDeep
item.locationWide = item.dataObj.locationWide item.locationWide = item.dataObj.locationWide
@ -1063,7 +1196,7 @@ const getAllNodeList = async () => {
item.rotatable = false item.rotatable = false
item.lockAspectRatio = true item.lockAspectRatio = true
} else if (item.type === 7) { } else if (item.type === 7) {
item.dataObj = JSON.parse(item.dataJson) item.dataObj = JSONBigInt({ storeAsString: true }).parse(item.dataJson)
item.text = item.dataObj.text item.text = item.dataObj.text
item.fontColor = item.dataObj.fontColor item.fontColor = item.dataObj.fontColor
item.fontFamily = item.dataObj.fontFamily item.fontFamily = item.dataObj.fontFamily
@ -1163,7 +1296,7 @@ onMounted(() => {
margin-top: -15px; margin-top: -15px;
margin-left: -20px; margin-left: -20px;
margin-right: -20px; margin-right: -20px;
padding: 0 14px; padding: 0 12px;
margin-bottom: 20px; margin-bottom: 20px;
box-shadow: rgba(0, 0, 0, 0.05) 0px 0px 0px 1px; box-shadow: rgba(0, 0, 0, 0.05) 0px 0px 0px 1px;
height: 70px; height: 70px;
@ -1174,7 +1307,7 @@ onMounted(() => {
.tool-item { .tool-item {
cursor: pointer; cursor: pointer;
width: 52px; width: 49px;
height: 70px; height: 70px;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
@ -1195,7 +1328,7 @@ onMounted(() => {
} }
} }
.line { .line {
margin: 0 14px; margin: 0 12px;
width: 1px; width: 1px;
height: 47px; height: 47px;
border: 1px solid #cccccc; border: 1px solid #cccccc;
@ -1248,6 +1381,6 @@ onMounted(() => {
.selection-area-btn { .selection-area-btn {
width: 80px; width: 80px;
margin-left: 6px; margin-left: 4px;
} }
</style> </style>

View File

@ -1,125 +1,105 @@
<template> <template>
<div <div @click="handleClick" style="position: relative; width: 100vw; height: 100vh">
@mousedown="startSelection" <!-- 显示测量结果 -->
@mousemove="updateSelection" <div v-if="distance !== null" style="position: absolute; top: 20px; left: 20px">
@mouseup="endSelection" 距离{{ distance.toFixed(2) }} 像素
style="position: relative; height: 100vh; border: 1px solid #ccc" </div>
>
<!-- 框选区域 -->
<div
v-if="isSelecting || selectionBox.width > 0"
:style="{
position: 'absolute',
left: `${selectionBox.x}px`,
top: `${selectionBox.y}px`,
width: `${selectionBox.width}px`,
height: `${selectionBox.height}px`,
border: '2px dashed #007bff',
backgroundColor: 'rgba(0, 123, 255, 0.1)'
}"
></div>
<!-- 图片 --> <!-- 绘制点和连线 -->
<img <template v-if="points.length > 0">
v-for="(img, index) in images" <div
:key="index" v-for="(point, index) in points"
:src="img.src" :key="index"
:style="{ :style="{
position: 'absolute', position: 'absolute',
left: `${img.x}px`, left: `${point.x}px`,
top: `${img.y}px`, top: `${point.y}px`,
width: '100px', width: '10px',
height: '100px', height: '10px',
userSelect: isSelecting ? 'none' : 'auto', // backgroundColor: '#00329F',
pointerEvents: isSelecting ? 'none' : 'auto' // borderRadius: '50%'
}" }"
/> ></div>
<div
v-if="points.length === 2"
:style="{
position: 'absolute',
left: `${points[0].x + 5}px`,
top: `${points[0].y + 5}px`,
width: `${lineWidth}px`,
height: '2px',
backgroundColor: '#00329F',
transform: `rotate(${lineAngle}deg)`,
transformOrigin: '0 0',
textAlign: 'center'
}"
>
距离{{ distance.toFixed(2) }} 像素</div
>
</template>
</div> </div>
</template> </template>
<script> <script>
import { ref } from 'vue' import { ref, computed } from 'vue'
export default { export default {
setup() { setup() {
// const points = ref([]) //
const images = ref([ const distance = ref(null) //
{ src: 'https://via.placeholder.com/100', x: 100, y: 100 },
{ src: 'https://via.placeholder.com/100', x: 300, y: 200 },
{ src: 'https://via.placeholder.com/100', x: 500, y: 300 }
])
// //
const selectionBox = ref({ const calculateDistance = (point1, point2) => {
x: 0, const dx = point2.x - point1.x
y: 0, const dy = point2.y - point1.y
width: 0, return Math.sqrt(dx * dx + dy * dy)
height: 0 }
// 线
const lineAngle = computed(() => {
if (points.value.length === 2) {
const dx = points.value[1].x - points.value[0].x
const dy = points.value[1].y - points.value[0].y
return Math.atan2(dy, dx) * (180 / Math.PI)
}
return 0
}) })
// // 线
const isSelecting = ref(false) const lineWidth = computed(() => {
if (points.value.length === 2) {
// return calculateDistance(points.value[0], points.value[1])
const startPos = ref({ x: 0, y: 0 })
//
const startSelection = (event) => {
isSelecting.value = true
startPos.value = { x: event.clientX, y: event.clientY }
selectionBox.value = {
x: event.clientX,
y: event.clientY,
width: 0,
height: 0
} }
return 0
})
// //
event.preventDefault() const handleClick = (event) => {
} if (points.value.length === 2) {
//
// points.value = []
const updateSelection = (event) => { distance.value = null
if (!isSelecting.value) return } else {
//
const currentX = event.clientX points.value.push({ x: event.offsetX, y: event.offsetY })
const currentY = event.clientY if (points.value.length === 2) {
//
selectionBox.value = { distance.value = calculateDistance(points.value[0], points.value[1])
x: Math.min(startPos.value.x, currentX), }
y: Math.min(startPos.value.y, currentY),
width: Math.abs(currentX - startPos.value.x),
height: Math.abs(currentY - startPos.value.y)
} }
//
event.preventDefault()
}
//
const endSelection = () => {
isSelecting.value = false
console.log('框选区域:', selectionBox.value)
} }
return { return {
images, points,
selectionBox, distance,
isSelecting, lineAngle,
startSelection, lineWidth,
updateSelection, handleClick
endSelection
} }
} }
} }
</script> </script>
<style scoped> <style scoped>
/* 禁用用户选择 */ /* 样式可以根据需要调整 */
img {
user-select: none;
-webkit-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
}
</style> </style>

View File

@ -3,36 +3,37 @@
@mousedown="startSelection" @mousedown="startSelection"
@mousemove="updateSelection" @mousemove="updateSelection"
@mouseup="endSelection" @mouseup="endSelection"
style="position: relative; width: 100%; height: 900px" style="position: relative; height: 100vh; border: 1px solid #ccc"
> >
<!-- 绘制框选区域 --> <!-- 框选区域 -->
<div <div
v-if="isSelecting" v-if="isSelecting || selectionBox.width > 0"
:style="{ :style="{
position: 'absolute', position: 'absolute',
left: `${selectionBox.x}px`, left: `${selectionBox.x}px`,
top: `${selectionBox.y}px`, top: `${selectionBox.y}px`,
width: `${selectionBox.width}px`, width: `${selectionBox.width}px`,
height: `${selectionBox.height}px`, height: `${selectionBox.height}px`,
border: '1px solid blue', border: '2px dashed #007bff',
backgroundColor: 'rgba(0, 0, 255, 0.1)' backgroundColor: 'rgba(0, 123, 255, 0.1)'
}" }"
></div> ></div>
<!-- 绘制点位 --> <!-- 图片 -->
<div <img
v-for="(point, index) in points" v-for="(img, index) in images"
:key="index" :key="index"
:src="img.src"
:style="{ :style="{
position: 'absolute', position: 'absolute',
left: `${point.x}px`, left: `${img.x}px`,
top: `${point.y}px`, top: `${img.y}px`,
width: '100px', width: '100px',
height: '100px', height: '100px',
backgroundColor: selectedPoints.includes(point) ? 'red' : '#fff' userSelect: isSelecting ? 'none' : 'auto', //
pointerEvents: isSelecting ? 'none' : 'auto' //
}" }"
>设备{{ index }}</div />
>
</div> </div>
</template> </template>
@ -41,49 +42,70 @@ import { ref } from 'vue'
export default { export default {
setup() { setup() {
const points = ref([ //
{ x: 210, y: 210 }, const images = ref([
{ x: 330, y: 500 }, { src: 'https://via.placeholder.com/100', x: 100, y: 100 },
{ x: 840, y: 440 }, { src: 'https://via.placeholder.com/100', x: 300, y: 200 },
{ x: 230, y: 400 }, { src: 'https://via.placeholder.com/100', x: 500, y: 300 }
{ x: 750, y: 640 }
]) ])
const isSelecting = ref(false) //
const selectionBox = ref({ x: 0, y: 0, width: 0, height: 0 }) const selectionBox = ref({
const startPos = ref({ x: 0, y: 0 }) x: 0,
const selectedPoints = ref([]) y: 0,
width: 0,
height: 0
})
//
const isSelecting = ref(false)
//
const startPos = ref({ x: 0, y: 0 })
//
const startSelection = (event) => { const startSelection = (event) => {
isSelecting.value = true isSelecting.value = true
startPos.value = { x: event.offsetX, y: event.offsetY } startPos.value = { x: event.clientX, y: event.clientY }
selectionBox.value = { x: event.offsetX, y: event.offsetY, width: 0, height: 0 } selectionBox.value = {
} x: event.clientX,
y: event.clientY,
const updateSelection = (event) => { width: 0,
if (isSelecting.value) { height: 0
selectionBox.value.width = event.offsetX - startPos.value.x
selectionBox.value.height = event.offsetY - startPos.value.y
} }
//
event.preventDefault()
} }
//
const updateSelection = (event) => {
if (!isSelecting.value) return
const currentX = event.clientX
const currentY = event.clientY
selectionBox.value = {
x: Math.min(startPos.value.x, currentX),
y: Math.min(startPos.value.y, currentY),
width: Math.abs(currentX - startPos.value.x),
height: Math.abs(currentY - startPos.value.y)
}
//
event.preventDefault()
}
//
const endSelection = () => { const endSelection = () => {
isSelecting.value = false isSelecting.value = false
selectedPoints.value = points.value.filter((point) => { console.log('框选区域:', selectionBox.value)
return (
point.x >= Math.min(startPos.value.x, startPos.value.x + selectionBox.value.width) &&
point.x <= Math.max(startPos.value.x, startPos.value.x + selectionBox.value.width) &&
point.y >= Math.min(startPos.value.y, startPos.value.y + selectionBox.value.height) &&
point.y <= Math.max(startPos.value.y, startPos.value.y + selectionBox.value.height)
)
})
} }
return { return {
points, images,
isSelecting,
selectionBox, selectionBox,
selectedPoints, isSelecting,
startSelection, startSelection,
updateSelection, updateSelection,
endSelection endSelection
@ -91,3 +113,13 @@ export default {
} }
} }
</script> </script>
<style scoped>
/* 禁用用户选择 */
img {
user-select: none;
-webkit-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
}
</style>