等距复制

This commit is contained in:
yyy 2025-07-03 10:04:25 +08:00
parent 709df94ff3
commit b26c03ba25
3 changed files with 450 additions and 38 deletions

View File

@ -3,7 +3,7 @@
<Dialog
v-model="dialogFormVisible"
title="批量复制"
width="500"
width="600"
class="batch-copying-dialog-form"
@close="dialogClose"
>
@ -30,18 +30,53 @@
precision="4"
/>
</el-form-item>
<!-- 常用方向快捷按钮 -->
<el-form-item label="常用方向">
<div class="quick-angles">
<el-button
size="small"
@click="setQuickAngle(0)"
:type="batchCopyingForm.radian === 0 ? 'primary' : 'default'"
>向右 (0)</el-button
>
<el-button
size="small"
@click="setQuickAngle(Math.PI / 2)"
:type="batchCopyingForm.radian === Math.PI / 2 ? 'primary' : 'default'"
>向上 (π/2)</el-button
>
<el-button
size="small"
@click="setQuickAngle(Math.PI)"
:type="batchCopyingForm.radian === Math.PI ? 'primary' : 'default'"
>向左 (π)</el-button
>
<el-button
size="small"
@click="setQuickAngle(-Math.PI / 2)"
:type="batchCopyingForm.radian === -Math.PI / 2 ? 'primary' : 'default'"
>向下 (-π/2)</el-button
>
</div>
</el-form-item>
<div class="tips">
<div class="mb-1">
<el-text>向上弧度 1.57079</el-text>
<div class="tips-title">
<el-icon><InfoFilled /></el-icon>
<span>角度说明</span>
</div>
<div class="mb-1">
<el-text>向左弧度 3.141592</el-text>
</div>
<div class="mb-1">
<el-text>向下弧度 -1.57079</el-text>
</div>
<div class="mb-1">
<el-text>向右弧度 0</el-text>
<div class="tips-content">
<div class="tip-item">
<el-text>向上弧度 1.57079 (90°)</el-text>
</div>
<div class="tip-item">
<el-text>向左弧度 3.141592 (180°)</el-text>
</div>
<div class="tip-item">
<el-text>向下弧度 -1.57079 (-90°)</el-text>
</div>
<div class="tip-item">
<el-text>向右弧度 0 (0°)</el-text>
</div>
</div>
</div>
</el-form>
@ -130,6 +165,11 @@ const submitLineLibraryForm = async () => {
const safeNumber = (value) => Number(value || 0)
//
const setQuickAngle = (angle) => {
batchCopyingForm.value.radian = angle
}
defineExpose({ open }) // open
</script>
@ -142,8 +182,56 @@ defineExpose({ open }) // 提供 open 方法,用于打开弹窗
border-top: none !important;
}
.quick-angles {
display: flex;
gap: 8px;
flex-wrap: wrap;
.el-button {
min-width: 80px;
font-size: 12px;
}
}
.tips {
user-select: text;
padding-left: 110px;
margin-top: 16px;
.tips-title {
display: flex;
align-items: center;
gap: 6px;
margin-bottom: 8px;
color: #606266;
font-size: 14px;
font-weight: 500;
.el-icon {
color: #409eff;
}
}
.tips-content {
background: #f0f9ff;
border: 1px solid #b3d8ff;
border-radius: 4px;
padding: 8px 12px;
.tip-item {
margin-bottom: 6px;
font-size: 13px;
color: #606266;
&:last-child {
margin-bottom: 0;
}
.el-text {
color: #606266;
}
}
}
}
}
</style>

View File

@ -3,7 +3,7 @@
<Dialog
v-model="dialogFormVisible"
title="等距复制"
width="540"
width="600"
class="isometric-replication-dialog"
@close="dialogClose"
>
@ -13,40 +13,99 @@
v-model="form.distance"
:min="0"
:precision="2"
placeholder="请输入"
placeholder="请输入间隔距离"
style="width: 100%"
/>
</el-form-item>
<el-form-item label="复制数量" prop="number" required>
<el-input-number
v-model="form.number"
:min="0"
:precision="2"
placeholder="请输入"
:min="1"
:max="50"
:precision="0"
placeholder="请输入复制数量"
style="width: 100%"
/>
</el-form-item>
<el-form-item label="复制度" prop="angle" required>
<el-form-item label="复制度" prop="angle" required>
<el-input-number
v-model="form.angle"
:min="0"
:precision="2"
placeholder="请输入"
:min="-Math.PI"
:max="Math.PI"
:precision="4"
placeholder="请输入弧度"
style="width: 100%"
/>
</el-form-item>
<!-- 常用角度快捷按钮 -->
<el-form-item label="常用方向">
<div class="quick-angles">
<el-button
size="small"
@click="setQuickAngle(0)"
:type="form.angle === 0 ? 'primary' : 'default'"
>
向右 (0)
</el-button>
<el-button
size="small"
@click="setQuickAngle(Math.PI / 2)"
:type="form.angle === Math.PI / 2 ? 'primary' : 'default'"
>
向上 (π/2)
</el-button>
<el-button
size="small"
@click="setQuickAngle(Math.PI)"
:type="form.angle === Math.PI ? 'primary' : 'default'"
>
向左 (π)
</el-button>
<el-button
size="small"
@click="setQuickAngle(-Math.PI / 2)"
:type="form.angle === -Math.PI / 2 ? 'primary' : 'default'"
>
向下 (-π/2)
</el-button>
</div>
</el-form-item>
<div class="tips">
<div class="tips-title">
<el-icon><InfoFilled /></el-icon>
<span>角度说明</span>
</div>
<div class="tips-content">
<div class="tip-item">
<el-text>向上弧度 1.57079 (90°)</el-text>
</div>
<div class="tip-item">
<el-text>向左弧度 3.141592 (180°)</el-text>
</div>
<div class="tip-item">
<el-text>向下弧度 -1.57079 (-90°)</el-text>
</div>
<div class="tip-item">
<el-text>向右弧度 0 (0°)</el-text>
</div>
</div>
</div>
</el-form>
<template #footer>
<div class="dialog-footer">
<el-button @click="dialogFormVisible = false">取消</el-button>
<el-button type="primary" @click="submitForm(ruleFormRef)"> 确定 </el-button>
<el-button type="primary" @click="submitForm(ruleFormRef)" :loading="submitting">
确定
</el-button>
</div>
</template>
</Dialog>
</template>
<script setup>
import { reactive, ref } from 'vue'
import { reactive, ref, nextTick } from 'vue'
import * as MapApi from '@/api/map/map'
import { DICT_TYPE, getIntDictOptions } from '@/utils/dict'
@ -54,27 +113,51 @@ const message = useMessage() // 消息弹窗
const ruleFormRef = ref()
const dialogFormVisible = ref(false) //
const submitting = ref(false) //
const props = defineProps({
imgBgObj: {
type: Object,
default: () => {}
}
})
const rules = reactive({
distance: [{ required: true, message: '请输入间隔距离', trigger: 'blur' }],
number: [{ required: true, message: '请输入复制数量', trigger: 'blur' }],
angle: [{ required: true, message: '请输入复制角度', trigger: 'blur' }]
distance: [
{ required: true, message: '请输入间隔距离', trigger: 'blur' },
{ type: 'number', min: 0.01, message: '间隔距离必须大于0', trigger: 'blur' }
],
number: [
{ required: true, message: '请输入复制数量', trigger: 'blur' },
{ type: 'number', min: 1, max: 50, message: '复制数量必须在1-50之间', trigger: 'blur' }
],
angle: [
{ required: true, message: '请输入复制弧度', trigger: 'blur' },
{ type: 'number', min: -Math.PI, max: Math.PI, message: '弧度必须在-π到π之间', trigger: 'blur' }
]
})
//
const form = ref({
positionMapId: '',
skuInfo: '', //
areaName: '', //
areaNumber: 0,
mapItemIds: []
distance: 1,
number: 1,
angle: 0
})
const open = (list) => {
const initialPoint = ref()
const open = (item) => {
console.log(item)
initialPoint.value = item
//
form.value = {
distance: 1,
number: 1,
angle: 0
}
dialogFormVisible.value = true
}
const emit = defineEmits(['addEventListener', 'itemAreaSettingSubmitSuccess'])
const emit = defineEmits(['addEventListener', 'equalDistanceReplicationSucceeded'])
const dialogClose = () => {
emit('addEventListener')
}
@ -83,10 +166,142 @@ const submitForm = async (formEl) => {
if (!formEl) return
await formEl.validate(async (valid, fields) => {
if (valid) {
submitting.value = true
try {
//
if (!validateBoundaries()) {
return
}
// 090西180-90
const angleInDegrees = (form.value.angle * 180) / Math.PI
//
const distanceInPixels = form.value.distance / Number(props.imgBgObj.resolution)
//
const replicationPoints = calculateReplicationPoints(
initialPoint.value,
angleInDegrees,
distanceInPixels,
form.value.number
)
//
emit('equalDistanceReplicationSucceeded', replicationPoints)
//
dialogFormVisible.value = false
message.success(`成功复制 ${form.value.number} 个点`)
} catch (error) {
console.error('等距离复制失败:', error)
message.error('等距离复制失败:' + (error.message || '未知错误'))
} finally {
submitting.value = false
}
}
})
}
//
const validateBoundaries = () => {
const distanceInPixels = form.value.distance / Number(props.imgBgObj.resolution)
const maxDistance = distanceInPixels * form.value.number
//
const angleInDegrees = (form.value.angle * 180) / Math.PI
const maxX =
initialPoint.value.locationX + maxDistance * Math.cos((angleInDegrees * Math.PI) / 180)
const maxY =
initialPoint.value.locationY - maxDistance * Math.sin((angleInDegrees * Math.PI) / 180)
if (maxX < 0 || maxX > props.imgBgObj.width || maxY < 0 || maxY > props.imgBgObj.height) {
message.warning('复制点可能超出地图边界,请检查参数')
return false
}
return true
}
//
const calculateReplicationPoints = (initialPoint, angleInDegrees, distanceInPixels, count) => {
const points = []
for (let i = 1; i <= count; i++) {
//
const currentDistance = distanceInPixels * i
//
// 090西180-90
const newX =
initialPoint.locationX + currentDistance * Math.cos((angleInDegrees * Math.PI) / 180)
const newY =
initialPoint.locationY - currentDistance * Math.sin((angleInDegrees * Math.PI) / 180)
//
const newPoint = generateReplicationPoint(initialPoint, newX, newY, i)
points.push(newPoint)
}
return points
}
//
const generateReplicationPoint = (initialPoint, newX, newY, index) => {
//
const actualLocationX =
Number(props.imgBgObj.origin[0]) + Number(newX) * Number(props.imgBgObj.resolution)
const actualLocationY =
Number(props.imgBgObj.origin[1]) +
(Number(props.imgBgObj.height) - Number(newY)) * Number(props.imgBgObj.resolution)
const { sortNum, createTime, ...restNode } = initialPoint
let dataObj = {}
let dataList = []
let dataJson = ''
if (restNode.type === 1 || restNode.type === 6) {
dataObj = {}
dataList = []
dataJson = ''
} else if (restNode.type === 2) {
dataList = restNode.dataList.map(
({ locationDeep, locationStorey, locationWide, positionMapId, locationTypeId }) => ({
locationDeep,
locationStorey,
locationWide,
positionMapId,
locationTypeId
})
)
dataJson = JSON.stringify(dataList)
} else if (restNode.type === 4) {
dataObj = {
locationDeep: restNode.dataObj.locationDeep,
locationWide: restNode.dataObj.locationWide,
positionMapId: restNode.dataObj.positionMapId
}
dataJson = JSON.stringify(dataObj)
}
return {
...restNode,
id: undefined,
locationX: Number(newX.toFixed(4)),
locationY: Number(newY.toFixed(4)),
actualLocationX: Number(actualLocationX.toFixed(4)),
actualLocationY: Number(actualLocationY.toFixed(4)),
dataObj,
dataList,
dataJson
}
}
//
const setQuickAngle = (angle) => {
form.value.angle = angle
}
defineExpose({ open }) // open
</script>
@ -98,5 +313,92 @@ defineExpose({ open }) // 提供 open 方法,用于打开弹窗
padding: 0px 20px 20px 0;
border-top: none !important;
}
.quick-angles {
display: flex;
gap: 8px;
flex-wrap: wrap;
.el-button {
min-width: 80px;
font-size: 12px;
}
}
.tips {
user-select: text;
padding-left: 110px;
margin-top: 16px;
.tips-title {
display: flex;
align-items: center;
gap: 6px;
margin-bottom: 8px;
color: #606266;
font-size: 14px;
font-weight: 500;
.el-icon {
color: #409eff;
}
}
.tips-content {
background: #f0f9ff;
border: 1px solid #b3d8ff;
border-radius: 4px;
padding: 8px 12px;
.tip-item {
margin-bottom: 6px;
font-size: 13px;
color: #606266;
&:last-child {
margin-bottom: 0;
}
.el-text {
color: #606266;
}
}
}
}
//
.el-form-item {
margin-bottom: 20px;
&.is-error {
.el-input-number {
.el-input__wrapper {
box-shadow: 0 0 0 1px #f56c6c inset;
}
}
}
}
//
.dialog-footer {
.el-button {
min-width: 80px;
&.el-button--primary {
background: #409eff;
border-color: #409eff;
&:hover {
background: #66b1ff;
border-color: #66b1ff;
}
&:active {
background: #3a8ee6;
border-color: #3a8ee6;
}
}
}
}
}
</style>

View File

@ -959,17 +959,21 @@
<GenerateStraightLinesDialog
ref="GenerateStraightLinesDialogRef"
@generate-straight-lines-submit="GenerateStraightLinesSubmit"
@add-event-listener="addEventListener"
/>
<!-- 批量复制节点和路线 -->
<BatchCopyingDialogForm
ref="BatchCopyingDialogFormRef"
:imgBgObj="imgBgObj"
@submit-batch-copying-form-success="submitBatchCopyingFormSuccess"
@add-event-listener="addEventListener"
/>
<!-- 等距复制 -->
<IsometricReplicationDialog
ref="IsometricReplicationDialogRef"
:imgBgObj="imgBgObj"
@add-event-listener="addEventListener"
@equal-distance-replication-succeeded="equalDistanceReplicationSucceeded"
/>
<!-- 节点右击菜单 -->
@ -981,7 +985,17 @@
>
<div class="context-menu-item" @click="deleteSingleNode">删除</div>
<div class="context-menu-item" @click="contextMenuEditNode">编辑节点</div>
<div class="context-menu-item" @click="isometricReplication">等距复制</div>
<div
class="context-menu-item"
@click="isometricReplication"
v-if="
state.contextMenu.currentItem.type === 1 ||
state.contextMenu.currentItem.type === 2 ||
state.contextMenu.currentItem.type === 4 ||
state.contextMenu.currentItem.type === 6
"
>等距复制</div
>
<div
class="context-menu-item"
@click="generateDetectionPoint"
@ -2207,9 +2221,14 @@ const contextMenuEditNode = () => {
//
const isometricReplication = () => {
// IsometricReplicationDialogRef.value.open()
// hideContextMenu()
message.warning('规则暂未制定')
removeEventListener() //
IsometricReplicationDialogRef.value.open(state.contextMenu.currentItem)
hideContextMenu()
}
//
const equalDistanceReplicationSucceeded = (list) => {
state.allMapPointInfo.push(...list)
addEditHistory()
}
//
@ -2952,6 +2971,7 @@ const clickDrawSelectionArea = () => {
message.warning('您选择的路径点存在未保存的')
return
}
removeEventListener() //
GenerateStraightLinesDialogRef.value.open(routeList)
}
//
@ -2983,6 +3003,7 @@ const clickDrawSelectionArea = () => {
top: Math.min(...state.drawSelectionPointList.map((point) => Number(point.locationY))),
bottom: Math.max(...state.drawSelectionPointList.map((point) => Number(point.locationY)))
}
removeEventListener() //
BatchCopyingDialogFormRef.value.open(boundaryValue)
} else {
message.warning('至少选择一个点')
@ -3056,11 +3077,12 @@ const submitBatchCopyingFormSuccess = async (form) => {
dataJson = ''
} else if (restNode.type === 2) {
dataList = restNode.dataList.map(
({ locationDeep, locationStorey, locationWide, positionMapId }) => ({
({ locationDeep, locationStorey, locationWide, positionMapId, locationTypeId }) => ({
locationDeep,
locationStorey,
locationWide,
positionMapId
positionMapId,
locationTypeId
})
)
dataJson = JSON.stringify(dataList)