批量复制

This commit is contained in:
yyy 2025-06-11 15:24:30 +08:00
parent dd713321b1
commit 2e148ea659
4 changed files with 361 additions and 36 deletions

View File

@ -5,7 +5,7 @@ VITE_DEV=true
# 请求路径
# VITE_BASE_URL='http://192.168.77.50:48080'
# VITE_BASE_URL='http://10.10.100.19:48080'
# VITE_BASE_URL='http://10.10.100.15:48080'
VITE_BASE_URL='http://10.10.5.5:48080'
# 文件上传类型server - 后端上传, client - 前端直连上传,仅支持 S3 服务

View File

@ -0,0 +1,125 @@
<template>
<!-- 批量复制 -->
<Dialog
v-model="dialogFormVisible"
title="批量复制"
width="500"
class="batch-copying-dialog-form"
@close="dialogClose"
>
<el-form :model="batchCopyingForm" label-width="100" ref="BatchCopyingFormRef" :rules="rules">
<el-form-item label="X轴偏移量" prop="x" required>
<el-input-number
v-model="batchCopyingForm.x"
placeholder="请输入"
controls-position="right"
style="width: 100%"
precision="2"
/>
<el-text type="info" size="small">X轴往左为负值往右为正值</el-text>
</el-form-item>
<el-form-item label="Y轴偏移量" prop="y" required>
<el-input-number
v-model="batchCopyingForm.y"
placeholder="请输入"
controls-position="right"
style="width: 100%"
precision="2"
/>
<el-text type="info" size="small">X轴往上为负值往下为正值</el-text>
</el-form-item>
</el-form>
<template #footer>
<div class="dialog-footer">
<el-button @click="dialogFormVisible = false">取消</el-button>
<el-button type="primary" @click="submitLineLibraryForm()"> 确定 </el-button>
</div>
</template>
</Dialog>
</template>
<script setup>
import { reactive, ref } from 'vue'
import * as MapApi from '@/api/map/map'
import { DICT_TYPE, getIntDictOptions } from '@/utils/dict'
const message = useMessage() //
const props = defineProps({
imgBgObj: {
type: Object,
default: () => {}
}
})
//
const batchCopyingForm = ref({
x: 0,
y: 0
})
const boundaryValue = ref()
const BatchCopyingFormRef = ref()
const dialogFormVisible = ref(false) //
const validateXValue = (rule, value, callback) => {
let maxLeft = -Number(boundaryValue.value.left)
let maxRight = Number(props.imgBgObj.width) - Number(boundaryValue.value.right)
if (value < maxLeft || value > maxRight) {
callback(new Error(`不能超出地图宽度,可偏移范围为${maxLeft}${maxRight}`))
} else {
callback()
}
}
const validateYValue = (rule, value, callback) => {
let maxTop = -Number(boundaryValue.value.top)
let maxBottom = Number(props.imgBgObj.height) - Number(boundaryValue.value.bottom)
if (value < maxTop || value > maxBottom) {
callback(new Error(`不能超出地图宽度,可偏移范围为${maxTop}${maxBottom}`))
} else {
callback()
}
}
const rules = reactive({
x: [
{ required: true, message: '请输入X轴偏移量', trigger: 'blur' },
{ validator: validateXValue, trigger: 'blur' }
],
y: [
{ required: true, message: '请输入Y轴偏移量', trigger: 'blur' },
{ validator: validateYValue, trigger: 'blur' }
]
})
const open = (boundaryObj) => {
boundaryValue.value = boundaryObj
batchCopyingForm.value.x = 0
batchCopyingForm.value.y = 0
dialogFormVisible.value = true
}
const emit = defineEmits(['addEventListener', 'submitBatchCopyingFormSuccess'])
const dialogClose = () => {
emit('addEventListener')
}
const submitLineLibraryForm = async () => {
await BatchCopyingFormRef.value.validate(async (valid, fields) => {
if (valid) {
dialogFormVisible.value = false
emit('submitBatchCopyingFormSuccess', batchCopyingForm.value)
}
})
}
defineExpose({ open }) // open
</script>
<style lang="scss">
.batch-copying-dialog-form {
padding: 0px;
.el-dialog__footer {
padding: 0px 20px 20px 0;
border-top: none !important;
}
}
</style>

View File

@ -883,7 +883,7 @@ onMounted(() => {
//
document.addEventListener('visibilitychange', () => {
if (!document.hidden && robotListTimerRef.value === null) {
robotListTimerRef.value = setInterval(getRobotByFloorAndAreaList, 5000)
robotListTimerRef.value = setInterval(getRobotByFloorAndAreaList, 10000)
}
})
})

View File

@ -284,6 +284,7 @@
toolbarSwitchType === 'createRegion' ||
toolbarSwitchType === 'drawRoute' ||
toolbarSwitchType === 'generateLine' ||
toolbarSwitchType === 'batchCopying' ||
toolbarSwitchType === 'bulkDelete'))
"
></div>
@ -294,6 +295,7 @@
toolbarSwitchType === 'createRegion' ||
toolbarSwitchType === 'drawRoute' ||
toolbarSwitchType === 'generateLine' ||
toolbarSwitchType === 'batchCopying' ||
toolbarSwitchType === 'bulkDelete')
"
type="danger"
@ -898,6 +900,12 @@
ref="GenerateStraightLinesDialogRef"
@generate-straight-lines-submit="GenerateStraightLinesSubmit"
/>
<!-- 批量复制节点和路线 -->
<BatchCopyingDialogForm
ref="BatchCopyingDialogFormRef"
:imgBgObj="imgBgObj"
@submit-batch-copying-form-success="submitBatchCopyingFormSuccess"
/>
</div>
</template>
@ -910,6 +918,7 @@ import editNodeProperties from './components-tool/editNodeProperties.vue'
import textFormToolDialog from './components-tool/textFormToolDialog.vue'
import equipmentToolDialog from './components-tool/equipmentToolDialog.vue'
import itemAreaSettingDialog from './components-tool/itemAreaSettingDialog.vue'
import BatchCopyingDialogForm from './components-tool/BatchCopyingDialogForm.vue'
import lineLibrarySettingDialog from './components-tool/lineLibrarySettingDialog.vue'
import layerSelectionToolDialog from './components-tool/layerSelectionToolDialog.vue'
import itemAreaManagementDialog from './components-tool/itemAreaManagementDialog.vue'
@ -928,6 +937,7 @@ const lineLibraryManagementDialogRef = ref() //线库管理
const itemAreaManagementDialogRef = ref() //
const lineLibrarySettingDialogRef = ref() //线
const itemAreaSettingDialogRef = ref() //
const BatchCopyingDialogFormRef = ref() //
const equipmentToolDialogRef = ref() //
const textFormToolDialogRef = ref() //
const editMapRouteDialogRef = ref() //线
@ -1519,6 +1529,13 @@ const state = reactive({
icon: 'ep:delete',
isActive: false
},
{
switchType: 'batchCopying',
name: '批量复制',
icon: 'ep:document-copy',
isActive: false
},
{
switchType: 'showSortNum',
name: '节点序号',
@ -1566,6 +1583,7 @@ const state = reactive({
drawSelectionAreaBox: { x: 0, y: 0, width: 0, height: 0 }, //
drawSelectionStartPoint: { x: 0, y: 0 }, //
drawSelectionPointList: [], //list
drawSelectionRouteList: [], //线list
textFormToolShow: false, //
showInputBox: false, //
inputBoxStyle: {
@ -1700,7 +1718,8 @@ const toolbarClick = async (item) => {
toolbarSwitchType.value === 'generateLine' ||
toolbarSwitchType.value === 'createLineLibrary' ||
toolbarSwitchType.value === 'createRegion' ||
toolbarSwitchType.value === 'bulkDelete'
toolbarSwitchType.value === 'bulkDelete' ||
toolbarSwitchType.value === 'batchCopying'
) {
state.cursorStyle = 'crosshair'
} else if (
@ -1734,6 +1753,7 @@ const toolbarClick = async (item) => {
toolbarSwitchType.value === 'createLineLibrary' ||
toolbarSwitchType.value === 'createRegion' ||
toolbarSwitchType.value === 'bulkDelete' ||
toolbarSwitchType.value === 'batchCopying' ||
toolbarSwitchType.value === 'editRoute' ||
toolbarSwitchType.value === 'generateLine'
) {
@ -1936,6 +1956,9 @@ const toolbarClick = async (item) => {
case 'bulkDelete':
//
break
case 'batchCopying':
//
break
case 'showSortNum':
//
//
@ -2280,7 +2303,8 @@ const startDrawSelection = (event) => {
toolbarSwitchType.value === 'createRegion' ||
toolbarSwitchType.value === 'drawRoute' ||
toolbarSwitchType.value == 'generateLine' ||
toolbarSwitchType.value === 'bulkDelete'
toolbarSwitchType.value === 'bulkDelete' ||
toolbarSwitchType.value === 'batchCopying'
) {
const { x, y } = disposeEventPoints(event)
@ -2301,7 +2325,8 @@ const updateDrawSelection = throttle((event) => {
toolbarSwitchType.value === 'createRegion' ||
toolbarSwitchType.value === 'drawRoute' ||
toolbarSwitchType.value === 'generateLine' ||
toolbarSwitchType.value === 'bulkDelete'
toolbarSwitchType.value === 'bulkDelete' ||
toolbarSwitchType.value === 'batchCopying'
) {
if (state.drawSelectionAreaShow) {
const { x, y } = disposeEventPoints(event)
@ -2405,7 +2430,8 @@ const endDrawSelection = (event) => {
toolbarSwitchType.value === 'createRegion' ||
toolbarSwitchType.value === 'drawRoute' ||
toolbarSwitchType.value === 'generateLine' ||
toolbarSwitchType.value === 'bulkDelete'
toolbarSwitchType.value === 'bulkDelete' ||
toolbarSwitchType.value === 'batchCopying'
) {
// 使 requestAnimationFrame
requestAnimationFrame(() => {
@ -2421,26 +2447,64 @@ const endDrawSelection = (event) => {
//
const clickDrawSelectionArea = () => {
let points = state.allMapPointInfo
state.drawSelectionPointList = []
state.drawSelectionRouteList = []
//
const allSelectedPoints = new Map()
const allSelectedRoutes = new Set()
state.allDrawSelectionAreaBox.forEach((box) => {
points.forEach((point) => {
//
state.allMapPointInfo.forEach((point) => {
if (
point.locationX >= box.x &&
point.locationX <= box.x + box.width &&
point.locationY >= box.y &&
point.locationY <= box.y + box.height
) {
state.drawSelectionPointList.push(point)
// 使
const pointKey = `${point.locationX},${point.locationY},${point.type}`
if (!allSelectedPoints.has(pointKey)) {
allSelectedPoints.set(pointKey, point)
}
}
})
// 线
if (toolbarSwitchType.value === 'batchCopying') {
state.mapRouteList.forEach((route) => {
const startPointInBox =
route.startPointX >= box.x &&
route.startPointX <= box.x + box.width &&
route.startPointY >= box.y &&
route.startPointY <= box.y + box.height
const endPointInBox =
route.endPointX >= box.x &&
route.endPointX <= box.x + box.width &&
route.endPointY >= box.y &&
route.endPointY <= box.y + box.height
if (startPointInBox && endPointInBox) {
// 使IDID
const routeKey = `${route.startingPointId}-${route.endPointId}`
if (!allSelectedRoutes.has(routeKey)) {
allSelectedRoutes.add(routeKey)
}
}
})
}
})
//
state.drawSelectionPointList = Array.from(allSelectedPoints.values())
state.drawSelectionRouteList = state.mapRouteList.filter((route) =>
allSelectedRoutes.has(`${route.startingPointId}-${route.endPointId}`)
)
//
state.allDrawSelectionAreaBox = []
//
state.drawSelectionPointList = deduplicateArrayById(state.drawSelectionPointList)
//
let binLocation = state.drawSelectionPointList.filter((item) => item.type === 2)
//
@ -2578,7 +2642,120 @@ const clickDrawSelectionArea = () => {
state.currentItemIndex = -1
addEditHistory()
}
//
if (toolbarSwitchType.value === 'batchCopying') {
if (state.drawSelectionPointList.length > 0) {
let boundaryValue = {
left: Math.min(...state.drawSelectionPointList.map((point) => Number(point.locationX))),
right: Math.max(...state.drawSelectionPointList.map((point) => Number(point.locationX))),
top: Math.min(...state.drawSelectionPointList.map((point) => Number(point.locationY))),
bottom: Math.max(...state.drawSelectionPointList.map((point) => Number(point.locationY)))
}
BatchCopyingDialogFormRef.value.open(boundaryValue)
} else {
message.warning('至少选择一个点')
}
}
}
//
const submitBatchCopyingFormSuccess = async (form) => {
let newPoints = JSON.parse(JSON.stringify(state.drawSelectionPointList))
let newRoutes = JSON.parse(JSON.stringify(state.drawSelectionRouteList))
// ID
const nodeIdMap = new Map()
//
const newPointList = await Promise.all(
newPoints.map(async (node) => {
const newId = await MapApi.getNodeId()
nodeIdMap.set(node.id, newId)
const locationX = Number(node.locationX) + Number(form.x)
const locationY = Number(node.locationY) + Number(form.y)
const actualPoint = disposeEventPoint(locationX, locationY)
const { sortNum, createTime, ...restNode } = node
return {
...restNode,
id: newId,
locationX,
locationY,
actualLocationX: actualPoint.actualLocationX,
actualLocationY: actualPoint.actualLocationY
}
})
)
// 线
const newRouteList = newRoutes.map((route) => {
const newRoute = { ...route }
//
if (nodeIdMap.has(route.startingPointId)) {
newRoute.startingPointId = nodeIdMap.get(route.startingPointId)
newRoute.startPointX = (Number(route.startPointX) + Number(form.x)).toString()
newRoute.startPointY = (Number(route.startPointY) + Number(form.y)).toString()
const actualStartPoint = disposeEventPoint(newRoute.startPointX, newRoute.startPointY)
newRoute.actualStartPointX = actualStartPoint.actualLocationX
newRoute.actualStartPointY = actualStartPoint.actualLocationY
//
if (route.method == 1) {
newRoute.beginControlX = (Number(route.beginControlX) + Number(form.x)).toString()
newRoute.beginControlY = (Number(route.beginControlY) + Number(form.y)).toString()
const actualBeginControl = disposeEventPoint(newRoute.beginControlX, newRoute.beginControlY)
newRoute.actualBeginControlX = actualBeginControl.actualLocationX
newRoute.actualBeginControlY = actualBeginControl.actualLocationY
} else {
newRoute.beginControlX = 0
newRoute.beginControlY = 0
newRoute.actualBeginControlX = 0
newRoute.actualBeginControlY = 0
}
}
//
if (nodeIdMap.has(route.endPointId)) {
newRoute.endPointId = nodeIdMap.get(route.endPointId)
newRoute.endPointX = (Number(route.endPointX) + Number(form.x)).toString()
newRoute.endPointY = (Number(route.endPointY) + Number(form.y)).toString()
const actualEndPoint = disposeEventPoint(newRoute.endPointX, newRoute.endPointY)
newRoute.actualEndPointX = actualEndPoint.actualLocationX
newRoute.actualEndPointY = actualEndPoint.actualLocationY
//
if (route.method == 1) {
newRoute.endControlX = (Number(route.endControlX) + Number(form.x)).toString()
newRoute.endControlY = (Number(route.endControlY) + Number(form.y)).toString()
const actualEndControl = disposeEventPoint(newRoute.endControlX, newRoute.endControlY)
newRoute.actualEndControlX = actualEndControl.actualLocationX
newRoute.actualEndControlY = actualEndControl.actualLocationY
} else {
newRoute.endControlX = 0
newRoute.endControlY = 0
newRoute.actualEndControlX = 0
newRoute.actualEndControlY = 0
}
}
const { id, createTime, startingSortNum, endPointSortNum, ...restRoute } = newRoute
return restRoute
})
//
console.log(newPointList, newRouteList)
state.allMapPointInfo.push(...newPointList)
state.mapRouteList.push(...newRouteList)
message.success('复制成功')
addEditHistory()
}
//线
const GenerateStraightLinesSubmit = (pointList, form) => {
// 使 requestAnimationFrame
@ -2630,24 +2807,13 @@ const GenerateStraightLinesSubmit = (pointList, form) => {
}
return item
})
//
state.allMapPointInfo = updatedPoints
state.mapRouteList = updatedRoutes
addEditHistory()
})
}
//
const deduplicateArrayById = (arr) => {
const idSet = new Set()
return arr.filter((item) => {
if (idSet.has(item.id)) {
return false
}
idSet.add(item.id)
return true
})
}
//线 id
const findDuplicateLocationIds = (data) => {
const locationMap = {}
@ -3341,6 +3507,7 @@ const saveMap = async () => {
message.success('保存成功')
} catch (error) {
loading.close()
message.error(error?.message || '保存失败')
}
}
//
@ -3374,11 +3541,19 @@ const saveNodeList = async () => {
item.dataJson = JSON.stringify(item.dataObj)
}
})
await MapApi.batchSaveOrEditOrDelMapItem(imgBgObj.positionMapId, list)
try {
await MapApi.batchSaveOrEditOrDelMapItem(imgBgObj.positionMapId, list)
} catch (error) {
throw new Error('节点保存失败:' + (error?.message || '未知错误'))
}
}
//线
const saveMapRoute = async () => {
await MapApi.createOrEditOrDelPositionMapLine(imgBgObj.positionMapId, state.mapRouteList)
try {
await MapApi.createOrEditOrDelPositionMapLine(imgBgObj.positionMapId, state.mapRouteList)
} catch (error) {
throw new Error('路线保存失败:' + (error?.message || '未知错误'))
}
}
//线 线
const submitLineLibraryFormSuccess = (obj) => {
@ -3556,15 +3731,7 @@ const getLineMidArrowPath = (item) => {
return `M ${startXArrow} ${startYArrow} L ${endXArrow} ${endYArrow}`
}
// 线
const getBezierMidArrowPath = (item) => {
const path = document.createElementNS('http://www.w3.org/2000/svg', 'path')
path.setAttribute('d', item.curvePath)
const length = path.getTotalLength()
const midPoint = path.getPointAtLength(length / 2)
const prevPoint = path.getPointAtLength(length / 2 - 1)
return `M ${prevPoint.x} ${prevPoint.y} L ${midPoint.x} ${midPoint.y}`
}
// 线 x
const computedCurveTextX = (item) => {
return (
@ -3851,6 +4018,8 @@ const findClosestPoint = (x, y) => {
padding: 0 0.75rem;
.top-tool-list {
max-width: calc(100vw - 260px);
overflow-x: auto;
display: flex;
align-items: center;
text-align: center;
@ -3896,11 +4065,13 @@ const findClosestPoint = (x, y) => {
.right-tool-list {
position: absolute;
right: 0;
top: 64px;
top: 70px;
background-color: #fff;
z-index: 999;
text-align: center;
box-shadow: rgba(0, 0, 0, 0.05) 0rem 0rem 0rem 0.0625rem;
max-height: calc(100vh - 200px);
overflow-y: auto;
.tool-item {
cursor: pointer;
@ -4008,4 +4179,33 @@ const findClosestPoint = (x, y) => {
height: 20px;
}
}
//
::-webkit-scrollbar {
width: 10px;
height: 10px;
}
::-webkit-scrollbar-track {
width: 6px;
background: rgba(#101f1c, 0.1);
-webkit-border-radius: 2em;
-moz-border-radius: 2em;
border-radius: 2em;
}
::-webkit-scrollbar-thumb {
background-color: rgba(144, 147, 153, 0.7);
background-clip: padding-box;
min-height: 28px;
-webkit-border-radius: 2em;
-moz-border-radius: 2em;
border-radius: 2em;
transition: background-color 0.3s;
cursor: pointer;
}
::-webkit-scrollbar-thumb:hover {
background-color: rgba(144, 147, 153, 0.3);
}
</style>