测距功能开发

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_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'
# 文件上传类型server - 后端上传, client - 前端直连上传,仅支持 S3 服务

30
package-lock.json generated
View File

@ -36,6 +36,7 @@
"fast-xml-parser": "^4.3.2",
"highlight.js": "^11.9.0",
"jsencrypt": "^3.3.2",
"json-bigint": "^1.0.0",
"lodash-es": "^4.17.21",
"markdown-it": "^14.1.0",
"markmap-common": "^0.16.0",
@ -6691,6 +6692,14 @@
"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": {
"version": "2.3.0",
"resolved": "https://registry.npmmirror.com/binary-extensions/-/binary-extensions-2.3.0.tgz",
@ -11700,6 +11709,14 @@
"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": {
"version": "3.0.1",
"resolved": "https://registry.npmmirror.com/json-buffer/-/json-buffer-3.0.1.tgz",
@ -23149,6 +23166,11 @@
"integrity": "sha512-vyL2OymJxmarO8gxMr0mhChsO9QGwhynfuu4+MHTAW6czfq9humCB7rKpUjDd9YUiDPU4mzpyupFSvOClAwbmQ==",
"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": {
"version": "2.3.0",
"resolved": "https://registry.npmmirror.com/binary-extensions/-/binary-extensions-2.3.0.tgz",
@ -26748,6 +26770,14 @@
"integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==",
"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": {
"version": "3.0.1",
"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",
"highlight.js": "^11.9.0",
"jsencrypt": "^3.3.2",
"json-bigint": "^1.0.0",
"lodash-es": "^4.17.21",
"markdown-it": "^14.1.0",
"markmap-common": "^0.16.0",

View File

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

View File

@ -2,10 +2,10 @@
<Dialog v-model="dialogFormVisible" title="节点属性" width="540" class="node-form-dialog">
<el-form :model="form" label-width="auto" ref="ruleFormRef">
<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 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 label="类型" prop="type" required>
<el-select v-model="form.type" placeholder="请选择类型" @change="typeChange">
@ -72,13 +72,13 @@
</el-form-item>
<el-form-item label="库位长度" prop="locationDeep">
<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>
</div>
</el-form-item>
<el-form-item label="库位宽度" prop="locationWide">
<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>
</div>
</el-form-item>
@ -188,7 +188,7 @@ const submit = async (formEl) => {
form.value.dataList[index] &&
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.mapItemId = form.value.dataList[index].mapItemId
item.laneId = form.value.dataList[index].laneId

View File

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

View File

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

View File

@ -240,38 +240,72 @@
{{ item.text }}
</div>
</VueDragResizeRotate>
<!-- 文档 https://github.com/a7650/vue3-draggable-resizable/blob/main/docs/document_zh.md#resizable -->
</div>
<!-- 框选区域 -->
<div
v-for="(box, index) in state.allDrawSelectionAreaBox"
:key="index"
:style="{
position: 'absolute',
left: `${box.x}px`,
top: `${box.y}px`,
width: `${box.width}px`,
height: `${box.height}px`,
border: '2px dashed #007bff',
backgroundColor: 'rgba(0, 123, 255, 0.1)',
zIndex: 999
}"
></div>
<!-- 当前框选区域 -->
<div
v-if="state.drawSelectionAreaShow"
:style="{
position: 'absolute',
left: `${state.drawSelectionAreaBox.x}px`,
top: `${state.drawSelectionAreaBox.y}px`,
width: `${state.drawSelectionAreaBox.width}px`,
height: `${state.drawSelectionAreaBox.height}px`,
border: '2px dashed #007bff',
backgroundColor: 'rgba(0, 123, 255, 0.1)',
zIndex: 999
}"
></div>
<template>
<div
v-for="(box, index) in state.allDrawSelectionAreaBox"
:key="index"
:style="{
position: 'absolute',
left: `${box.x}px`,
top: `${box.y}px`,
width: `${box.width}px`,
height: `${box.height}px`,
border: '2px dashed #007bff',
backgroundColor: 'rgba(0, 123, 255, 0.1)',
zIndex: 999
}"
></div>
<!-- 当前框选区域 -->
<div
v-if="state.drawSelectionAreaShow"
:style="{
position: 'absolute',
left: `${state.drawSelectionAreaBox.x}px`,
top: `${state.drawSelectionAreaBox.y}px`,
width: `${state.drawSelectionAreaBox.width}px`,
height: `${state.drawSelectionAreaBox.height}px`,
border: '2px dashed #007bff',
backgroundColor: 'rgba(0, 123, 255, 0.1)',
zIndex: 999
}"
></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
@ -323,6 +357,7 @@
</template>
<script setup>
import JSONBigInt from 'json-bigint'
import { ref, defineComponent, reactive, nextTick, onMounted } from 'vue'
import editNodeProperties from './components-tool/editNodeProperties.vue'
import textFormToolDialog from './components-tool/textFormToolDialog.vue'
@ -465,6 +500,10 @@ const mapClick = (e) => {
inputBoxRef.value.focus()
}, 0)
}
//
if (toolbarSwitchType.value === 'ranging') {
measureDistancesClick(e)
}
}
//
const textFormSuccess = (form) => {
@ -697,7 +736,9 @@ const state = reactive({
locationY: 0
}, //
inputBoxValue: '', //
isShowLayer: false //
isShowLayer: false, //
measureDistancesPoints: [], //
measureDistancesNum: 0 //
})
const toolbarClick = (item) => {
let type = item.switchType
@ -736,6 +777,11 @@ const toolbarClick = (item) => {
state.allDrawSelectionAreaBox = []
state.drawSelectionPointList = []
}
//
if (toolbarSwitchType.value !== 'ranging') {
state.measureDistancesPoints = [] //
state.measureDistancesNum = 0 //
}
switch (type) {
case 'open':
@ -968,9 +1014,13 @@ const clickDrawSelectionArea = () => {
//线
if (binLocation.length < 2) {
message.warning('至少选择两个库位')
} else {
lineLibrarySettingDialogRef.value.open(binLocation)
return
}
if (!isStraightLine(binLocation)) {
message.warning('您选择的库位不在一条直线上')
return
}
lineLibrarySettingDialogRef.value.open(binLocation)
}
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({
@ -1046,7 +1179,7 @@ const getAllNodeList = async () => {
item.lockAspectRatio = true
} 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.locationWide = item.dataList[0].locationWide
item.draggable = true
@ -1054,7 +1187,7 @@ const getAllNodeList = async () => {
item.rotatable = false
item.lockAspectRatio = true
} 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.locationDeep = item.dataObj.locationDeep
item.locationWide = item.dataObj.locationWide
@ -1063,7 +1196,7 @@ const getAllNodeList = async () => {
item.rotatable = false
item.lockAspectRatio = true
} 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.fontColor = item.dataObj.fontColor
item.fontFamily = item.dataObj.fontFamily
@ -1163,7 +1296,7 @@ onMounted(() => {
margin-top: -15px;
margin-left: -20px;
margin-right: -20px;
padding: 0 14px;
padding: 0 12px;
margin-bottom: 20px;
box-shadow: rgba(0, 0, 0, 0.05) 0px 0px 0px 1px;
height: 70px;
@ -1174,7 +1307,7 @@ onMounted(() => {
.tool-item {
cursor: pointer;
width: 52px;
width: 49px;
height: 70px;
display: flex;
flex-direction: column;
@ -1195,7 +1328,7 @@ onMounted(() => {
}
}
.line {
margin: 0 14px;
margin: 0 12px;
width: 1px;
height: 47px;
border: 1px solid #cccccc;
@ -1248,6 +1381,6 @@ onMounted(() => {
.selection-area-btn {
width: 80px;
margin-left: 6px;
margin-left: 4px;
}
</style>

View File

@ -1,125 +1,105 @@
<template>
<div
@mousedown="startSelection"
@mousemove="updateSelection"
@mouseup="endSelection"
style="position: relative; height: 100vh; border: 1px solid #ccc"
>
<!-- 框选区域 -->
<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>
<div @click="handleClick" style="position: relative; width: 100vw; height: 100vh">
<!-- 显示测量结果 -->
<div v-if="distance !== null" style="position: absolute; top: 20px; left: 20px">
距离{{ distance.toFixed(2) }} 像素
</div>
<!-- 图片 -->
<img
v-for="(img, index) in images"
:key="index"
:src="img.src"
:style="{
position: 'absolute',
left: `${img.x}px`,
top: `${img.y}px`,
width: '100px',
height: '100px',
userSelect: isSelecting ? 'none' : 'auto', //
pointerEvents: isSelecting ? 'none' : 'auto' //
}"
/>
<!-- 绘制点和连线 -->
<template v-if="points.length > 0">
<div
v-for="(point, index) in points"
: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="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>
</template>
<script>
import { ref } from 'vue'
import { ref, computed } from 'vue'
export default {
setup() {
//
const images = ref([
{ 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 points = ref([]) //
const distance = ref(null) //
//
const selectionBox = ref({
x: 0,
y: 0,
width: 0,
height: 0
//
const calculateDistance = (point1, point2) => {
const dx = point2.x - point1.x
const dy = point2.y - point1.y
return Math.sqrt(dx * dx + dy * dy)
}
// 线
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 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
// 线
const lineWidth = computed(() => {
if (points.value.length === 2) {
return calculateDistance(points.value[0], points.value[1])
}
return 0
})
//
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)
//
const handleClick = (event) => {
if (points.value.length === 2) {
//
points.value = []
distance.value = null
} else {
//
points.value.push({ x: event.offsetX, y: event.offsetY })
if (points.value.length === 2) {
//
distance.value = calculateDistance(points.value[0], points.value[1])
}
}
//
event.preventDefault()
}
//
const endSelection = () => {
isSelecting.value = false
console.log('框选区域:', selectionBox.value)
}
return {
images,
selectionBox,
isSelecting,
startSelection,
updateSelection,
endSelection
points,
distance,
lineAngle,
lineWidth,
handleClick
}
}
}
</script>
<style scoped>
/* 禁用用户选择 */
img {
user-select: none;
-webkit-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
}
/* 样式可以根据需要调整 */
</style>

View File

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