1、实时地图svg展示行驶路线、普通dom展示节点库位等节点信息,优化为行驶路线和路径节点由Canvas批量绘制,库位、设备点等需要交互的节点按照初始展示。

2、车辆列表优化加载速度
This commit is contained in:
yyy 2025-07-14 18:02:32 +08:00
parent 5f0f52d4ef
commit 7ff85039ba
6 changed files with 477 additions and 343 deletions

View File

@ -1,6 +1,6 @@
<template>
<ContentWrap>
<div>
<div v-loading="loading">
<div class="new-top-box">
<div class="new-top-box-left">
<div class="new-top-box-left-title"> 车辆看板 </div>
@ -125,59 +125,66 @@
</div>
<div class="item-inner-right-top">
<div class="swiper-item-box-top-msg">
<el-dropdown>
<div style="flex-shrink: 0">
<el-icon size="20px"><MoreFilled /></el-icon>
</div>
<template #dropdown>
<el-dropdown-menu>
<el-dropdown-item
@click="openForm('update', item.id)"
v-hasPermi="['carBoard:index:edit']"
>编辑</el-dropdown-item
>
<el-dropdown-item
@click="clockCar(item)"
v-hasPermi="['carBoard:index:lock']"
>{{ item.robotTaskModel == 0 ? '解锁' : '锁定' }}</el-dropdown-item
>
<el-dropdown-item
@click="deleteCar(item.id)"
v-hasPermi="['carBoard:index:delete']"
>删除</el-dropdown-item
>
<el-dropdown-item
@click="clearCar(item.robotNo)"
v-hasPermi="['carBoard:index:clear']"
>清除交管</el-dropdown-item
>
<el-dropdown-item
@click="recoveryTask(item.robotNo)"
v-hasPermi="['carBoard:index:recovery']"
>恢复任务</el-dropdown-item
>
<el-dropdown-item
v-if="item.isStop != 1"
@click="stopCar(item.robotNo)"
v-hasPermi="['carBoard:index:stop']"
>暂停车辆</el-dropdown-item
>
<el-dropdown-item
v-if="item.isStop === 1"
@click="recoveryCar(item.robotNo)"
v-hasPermi="['carBoard:index:recoveryCar']"
>恢复车辆</el-dropdown-item
>
</el-dropdown-menu>
<el-popover placement="bottom" trigger="click" width="160">
<template #reference>
<div style="flex-shrink: 0; cursor: pointer">
<el-icon size="20px"><MoreFilled /></el-icon>
</div>
</template>
</el-dropdown>
<div class="popover-menu">
<div
class="popover-menu-item"
@click="openForm('update', item.id)"
v-hasPermi="['carBoard:index:edit']"
>编辑</div
>
<div
class="popover-menu-item"
@click="clockCar(item)"
v-hasPermi="['carBoard:index:lock']"
>{{ item.robotTaskModel == 0 ? '解锁' : '锁定' }}</div
>
<div
class="popover-menu-item"
@click="deleteCar(item.id)"
v-hasPermi="['carBoard:index:delete']"
>删除</div
>
<div
class="popover-menu-item"
@click="clearCar(item.robotNo)"
v-hasPermi="['carBoard:index:clear']"
>清除交管</div
>
<div
class="popover-menu-item"
@click="recoveryTask(item.robotNo)"
v-hasPermi="['carBoard:index:recovery']"
>恢复任务</div
>
<div
class="popover-menu-item"
v-if="item.isStop != 1"
@click="stopCar(item.robotNo)"
v-hasPermi="['carBoard:index:stop']"
>暂停车辆</div
>
<div
class="popover-menu-item"
v-if="item.isStop === 1"
@click="recoveryCar(item.robotNo)"
v-hasPermi="['carBoard:index:recoveryCar']"
>恢复车辆</div
>
</div>
</el-popover>
</div>
</div>
</div>
<div class="item-inner">
<div class="item-inner-left">
<div class="item-inner-left-img-box">
<el-image style="width: 100%; height: 100%" :src="item.url" :fit="'fill'" />
<!-- <el-image style="width: 100%; height: 100%" :src="item.url" :fit="'fill'" /> -->
<el-tooltip
class="box-item"
effect="dark"
@ -477,7 +484,7 @@ const stopCar = (robotNo) => {
})
.then(() => {
CarApi.stopCar({ robotNo }).then((res) => {
if(res){
if (res) {
getCarList()
fetchData()
message.success('暂停成功')
@ -495,7 +502,7 @@ const recoveryCar = (robotNo) => {
})
.then(() => {
CarApi.recoveryCar({ robotNo }).then((res) => {
if(res){
if (res) {
getCarList()
fetchData()
message.success('恢复成功')
@ -584,8 +591,6 @@ onBeforeRouteLeave((to, from, next) => {
.swiperBox {
width: 100%;
/* background: rgba(0, 0, 0, 0.3); */
}
.swiper-item-box {
@ -925,4 +930,21 @@ input::input-placeholder {
width: 20px;
height: 20px;
}
.popover-menu {
display: flex;
flex-direction: column;
min-width: 120px;
padding: 4px 0;
}
.popover-menu-item {
padding: 8px 16px;
cursor: pointer;
font-size: 14px;
color: #333;
transition: background 0.2s;
}
.popover-menu-item:hover {
background: #f5f5f5;
}
</style>

View File

@ -21,33 +21,50 @@
<div class="indexpage-container-box">
<img :src="imgBgObj.imgUrl" class="map-bg" />
<div class="point-information">
<div class="line-list" v-if="legendObj.driveLineShow">
<svg id="svg" :width="imgBgObj.width" :height="imgBgObj.height">
<template v-for="(item, index) in state.mapRouteList" :key="index">
<template v-if="item.method == 0">
<!-- 直线 -->
<line
:x1="Number(item.startPointX) * radio"
:y1="Number(item.startPointY) * radio"
:x2="Number(item.endPointX) * radio"
:y2="Number(item.endPointY) * radio"
:stroke="item.isSelect ? '#f48924' : '#2d72d9'"
:stroke-width="4 * radio"
/>
</template>
<!-- 曲线 -->
<template v-else>
<path
:d="getCurvePath(item)"
:stroke="item.isSelect ? '#f48924' : '#2d72d9'"
:stroke-width="4 * radio"
fill="none"
/>
</template>
</template>
</svg>
<!-- 路线渲染由SVG改为Canvas -->
<canvas
ref="routeCanvasRef"
:width="imgBgObj.width"
:height="imgBgObj.height"
class="route-canvas"
style="position: absolute; left: 0; top: 0; z-index: 1; pointer-events: none"
v-show="legendObj.driveLineShow"
></canvas>
<!-- type=1节点Canvas渲染+自定义tooltip浮层 -->
<canvas
ref="pointCanvasRef"
:width="imgBgObj.width"
:height="imgBgObj.height"
class="point-canvas"
style="position: absolute; left: 0; top: 0; z-index: 2"
@mousemove="handlePointCanvasMouseMove"
@mouseleave="handlePointCanvasMouseLeave"
></canvas>
<div
v-if="tooltip.visible"
:style="{
position: 'fixed',
left: tooltip.x + 'px',
top: tooltip.y + 'px',
background: '#fff',
border: '1px solid #ebeef5',
boxShadow: '0 2px 12px 0 rgba(0,0,0,0.18)',
padding: '10px 16px',
borderRadius: '6px',
zIndex: 9999,
pointerEvents: 'none',
fontSize: '15px',
color: '#303133',
minWidth: '100px',
maxWidth: '320px',
whiteSpace: 'pre-line',
transition: 'opacity 0.2s',
lineHeight: '1.6',
fontFamily: 'PingFang SC, Helvetica Neue, Arial, sans-serif'
}"
>
<div v-html="tooltip.content"></div>
</div>
<!-- 小车 -->
<div v-for="(item, index) in testCarList" :key="item.macAddress">
<el-popover placement="bottom">
@ -70,9 +87,10 @@
<div class="popover-robot-no">{{ item.robotNo }}</div>
</el-popover>
</div>
<!-- 其它类型节点继续DOM渲染 -->
<div
class="map-point-list"
v-for="(item, index) in state.allMapPointInfo"
v-for="(item, index) in filteredMapPoints"
:key="index"
:style="{
left:
@ -299,15 +317,15 @@ import {
nextTick,
onMounted,
onBeforeUnmount,
onUnmounted
onUnmounted,
watch,
computed
} from 'vue'
import * as MapApi from '@/api/map/map'
import WebSocketClient from '../webSocket.js'
import storeDialog from './storeDialog.vue'
import { color } from 'echarts'
import { resetDragPosition } from '@/utils/drag'
import carDialog from './carDialog.vue'
import { is } from 'bpmn-js/lib/util/ModelUtil'
import JSONBigInt from 'json-bigint'
import { propTypes } from '@/utils/propTypes'
import { DICT_TYPE, getIntDictOptions } from '@/utils/dict'
@ -413,17 +431,40 @@ const filterTypeFun = (deviceType) => {
return deviceItem.label
}
const convertActualToBrowser = (pointX, pointY) => {
let resolution = Number(imgBgObj.resolution)
let origin = imgBgObj.origin
//
const convertActualCarToBrowser = (pointX, pointY) => {
const resolution = Number(imgBgObj.resolution)
const [originX, originY] = imgBgObj.origin.map(Number)
const imgHeight = Number(imgBgObj.height)
const carW = Number(carWidth.value)
const carH = Number(carHeight.value)
const y1 = Number(origin[1]) + Number(imgBgObj.height) * resolution
let x = Math.max(Number(pointX) - Number(origin[0]), 0)
let y = Math.max(y1 - Number(pointY), 0)
// yy
const yBottom = originY + imgHeight * resolution
const xPx = Math.max(Number(pointX) - originX, 0) / resolution
const yPx = Math.max(yBottom - Number(pointY), 0) / resolution
//
return {
x: xPx - carW / resolution / 100 / 2,
y: yPx - carH / resolution / 100 / 2
}
}
//
const convertActualToBrowser = (pointX, pointY) => {
const y1 = Number(imgBgObj.origin[1]) + Number(imgBgObj.height) * Number(imgBgObj.resolution)
let x =
Math.round(
(Math.max(Number(pointX) - Number(imgBgObj.origin[0]), 0) / Number(imgBgObj.resolution)) *
10000
) / 10000
let y =
Math.round((Math.max(y1 - Number(pointY), 0) / Number(imgBgObj.resolution)) * 10000) / 10000
return {
x: x / resolution - carWidth.value / resolution / 100 / 2,
y: y / resolution - carHeight.value / resolution / 100 / 2
x,
y
}
}
@ -556,13 +597,16 @@ const replaceHttpWithWs = (str) => {
//
const robotListTimerRef = ref(null)
const robotByFloorAndAreaList = ref([])
const isUnmounted = ref(false)
const getRobotByFloorAndAreaList = async () => {
if (isUnmounted.value) return
try {
const res = await MapApi.getRobotByFloorAndArea({
floor: imgBgObj.floor,
area: imgBgObj.area
})
if (isUnmounted.value) return
robotByFloorAndAreaList.value = res
if (testCarList.value.length) {
// 使 Set
@ -572,7 +616,9 @@ const getRobotByFloorAndAreaList = async () => {
)
}
} catch (error) {
console.error('获取机器人列表失败:', error)
if (!isUnmounted.value) {
console.error('获取机器人列表失败:', error)
}
}
}
@ -598,7 +644,7 @@ const initWebsocket = () => {
requestAnimationFrame(() => {
let data = JSON.parse(jsonMsg.content)
// console.log(data)
// console.log(dayjs().format('HH:mm:ss SSS'))
// console.log(data, dayjs().format('HH:mm:ss SSS'))
let dataList = Object.entries(data).map(([key, value]) => ({
macAddress: key,
@ -673,7 +719,7 @@ const mergeArraysWithoutDelete = (arr1, arr2) => {
//
const processCarData = (car, mapInfo) => {
const browserPos = convertActualToBrowser(car.data.pose2d.x, car.data.pose2d.y)
const browserPos = convertActualCarToBrowser(car.data.pose2d.x, car.data.pose2d.y)
return {
...car,
originWidth: mapInfo.width,
@ -728,8 +774,8 @@ const computedRatio = () => {
item.originWidth = imgBgObj.width
item.originHeight = imgBgObj.height
item.origin = imgBgObj.origin
item.realX = convertActualToBrowser(item.data.pose2d.x, item.data.pose2d.y).x
item.realY = convertActualToBrowser(item.data.pose2d.x, item.data.pose2d.y).y
item.realX = convertActualCarToBrowser(item.data.pose2d.x, item.data.pose2d.y).x
item.realY = convertActualCarToBrowser(item.data.pose2d.x, item.data.pose2d.y).y
})
radio.value = width / imgBgObj.width
@ -799,6 +845,9 @@ const getAllNodeList = async (positionMapId) => {
positionMapId: imgBgObj.positionMapId
})
state.allMapPointInfo?.forEach((item) => {
item.locationX = convertActualToBrowser(item.actualLocationX, item.actualLocationY).x
item.locationY = convertActualToBrowser(item.actualLocationX, item.actualLocationY).y
if (item.type === 1) {
item.locationDeep = 40
item.locationWide = 40
@ -871,6 +920,176 @@ const carDbClick = (item, index) => {
carDialogRef.value.open(JSON.parse(JSON.stringify(item)))
}
const routeCanvasRef = ref(null)
const pointCanvasRef = ref(null)
function getPointTooltipContent(item) {
// el-tooltip
if (item.dataList && item.dataList[0]?.laneName) {
return `<div class='indexpage-popover-item'><div>所属线库:</div><div>${item.dataList[0]?.laneName || ''}</div></div>`
}
if (item.dataList && item.dataList[0]?.areaName) {
return `<div class='indexpage-popover-item'><div>所属区域:</div><div>${item.dataList[0]?.areaName || ''}</div></div>`
}
return `<div class='indexpage-popover-item'><div>节点类型:</div><div>路径点</div><div>${item.sortNum || ''}</div></div>`
}
function handlePointCanvasMouseMove(e) {
const canvas = pointCanvasRef.value
if (!canvas) return
// canvasrect
const rect = canvas.getBoundingClientRect()
const scrollLeft = canvas.scrollLeft || 0
const scrollTop = canvas.scrollTop || 0
const scale = typeof isSizeRadio?.value === 'number' ? isSizeRadio.value : 1
const mouseX = (e.clientX - rect.left + scrollLeft) / scale
const mouseY = (e.clientY - rect.top + scrollTop) / scale
const points = state.allMapPointInfo.filter((item) => item.type === 1)
let hit = null
for (const item of points) {
const x = Number(item.locationX) - Number(item.locationWidePx) / 2
const y = Number(item.locationY) - Number(item.locationDeepPx) / 2
const w = Number(item.locationWidePx)
const h = Number(item.locationDeepPx)
const cx = x + w / 2
const cy = y + h / 2
const r = Math.min(w, h) / 2
if ((mouseX - cx) ** 2 + (mouseY - cy) ** 2 <= r ** 2) {
hit = item
break
}
}
if (hit) {
tooltip.visible = true
const nodeCenterX = Number(hit.locationX)
const nodeCenterY = Number(hit.locationY)
tooltip.x = nodeCenterX + 10
tooltip.y = nodeCenterY - 10
tooltip.content = `<div class='indexpage-popover-item'><div>节点编号:<b>${hit.sortNum ?? ''}</b></div></div>`
} else {
tooltip.visible = false
}
}
function handlePointCanvasMouseLeave() {
tooltip.visible = false
}
// 线Canvas
const drawRoutesOnCanvas = () => {
const canvas = routeCanvasRef.value
if (!canvas || !imgBgObj.width || !imgBgObj.height) return
const ctx = canvas.getContext('2d')
ctx.clearRect(0, 0, canvas.width, canvas.height)
if (!legendObj.driveLineShow) return
if (!Array.isArray(state.mapRouteList)) return
state.mapRouteList.forEach((item) => {
ctx.save()
ctx.strokeStyle = item.isSelect ? '#f48924' : '#2d72d9'
ctx.lineWidth = 4 * radio.value
ctx.beginPath()
if (item.method == 0) {
// 线
ctx.moveTo(Number(item.startPointX) * radio.value, Number(item.startPointY) * radio.value)
ctx.lineTo(Number(item.endPointX) * radio.value, Number(item.endPointY) * radio.value)
} else {
// 线
ctx.moveTo(Number(item.startPointX) * radio.value, Number(item.startPointY) * radio.value)
ctx.bezierCurveTo(
item.beginControlX * radio.value,
item.beginControlY * radio.value,
item.endControlX * radio.value,
item.endControlY * radio.value,
Number(item.endPointX) * radio.value,
Number(item.endPointY) * radio.value
)
}
ctx.stroke()
ctx.restore()
})
}
// type!==1
const filteredMapPoints = computed(() => state.allMapPointInfo.filter((item) => item.type !== 1))
// type=1Canvastooltip
const tooltip = reactive({ visible: false, x: 0, y: 0, content: '' })
// type=1Canvas
const drawPointsOnCanvas = () => {
const canvas = pointCanvasRef.value
if (!canvas || !imgBgObj.width || !imgBgObj.height) return
const ctx = canvas.getContext('2d')
ctx.clearRect(0, 0, canvas.width, canvas.height)
if (!Array.isArray(state.allMapPointInfo)) return
const points = state.allMapPointInfo.filter((item) => item.type === 1)
// 1.
points.forEach((item) => {
const x = (Number(item.locationX) - Number(item.locationWidePx) / 2) * Number(radio.value)
const y = (Number(item.locationY) - Number(item.locationDeepPx) / 2) * Number(radio.value)
const w = Number(item.locationWidePx) * Number(radio.value)
const h = Number(item.locationDeepPx) * Number(radio.value)
ctx.save()
ctx.beginPath()
ctx.arc(x + w / 2, y + h / 2, Math.min(w, h) / 2, 0, 2 * Math.PI)
ctx.fillStyle = '#000' //
ctx.globalAlpha = 1
ctx.fill()
ctx.restore()
})
// 2. legendObj.sortNumberShowtruesortNum
if (legendObj.sortNumberShow && props.sortNumberShow) {
points.forEach((item) => {
if (item.sortNum) {
const x = (Number(item.locationX) - Number(item.locationWidePx) / 2) * Number(radio.value)
const y = (Number(item.locationY) - Number(item.locationDeepPx) / 2) * Number(radio.value)
const w = Number(item.locationWidePx) * Number(radio.value)
const h = Number(item.locationDeepPx) * Number(radio.value)
ctx.save()
ctx.font = '13px sans-serif'
ctx.fillStyle = '#000' //
ctx.textAlign = 'center'
ctx.textBaseline = 'top'
ctx.globalAlpha = 1
ctx.fillText(
String(item.sortNum),
x + w / 2,
y + h / 2 + Math.min(w, h) / 2 + 4 // 4px
)
ctx.restore()
}
})
}
}
// 线
watch(
[() => state.mapRouteList, () => radio.value, () => imgBgObj.width, () => imgBgObj.height],
() => {
nextTick(() => drawRoutesOnCanvas())
},
{ deep: true }
)
// type=1
watch(
[
() => state.allMapPointInfo,
() => radio.value,
() => imgBgObj.width,
() => imgBgObj.height,
() => legendObj.sortNumberShow
],
() => {
nextTick(() => drawPointsOnCanvas())
},
{ deep: true }
)
// /
onMounted(() => {
nextTick(() => drawRoutesOnCanvas())
})
onMounted(() => {
document.addEventListener('fullscreenchange', handleFullscreenChange)
document.addEventListener('webkitfullscreenchange', handleFullscreenChange)
@ -879,16 +1098,22 @@ onMounted(() => {
//
robotListTimerRef.value = setInterval(() => {
if (document.hidden) return //
if (document.hidden || isUnmounted.value) return //
getRobotByFloorAndAreaList()
}, 10000)
//
document.addEventListener('visibilitychange', () => {
if (!document.hidden && robotListTimerRef.value === null) {
robotListTimerRef.value = setInterval(getRobotByFloorAndAreaList, 10000)
const visibilityHandler = () => {
if (!document.hidden && robotListTimerRef.value === null && !isUnmounted.value) {
robotListTimerRef.value = setInterval(() => {
if (document.hidden || isUnmounted.value) return
getRobotByFloorAndAreaList()
}, 10000)
}
})
}
document.addEventListener('visibilitychange', visibilityHandler)
// handler
window.__indexPageVisibilityHandler = visibilityHandler
})
onUnmounted(() => {
@ -896,10 +1121,15 @@ onUnmounted(() => {
document.removeEventListener('webkitfullscreenchange', handleFullscreenChange)
document.removeEventListener('mozfullscreenchange', handleFullscreenChange)
document.removeEventListener('MSFullscreenChange', handleFullscreenChange)
if (window.__indexPageVisibilityHandler) {
document.removeEventListener('visibilitychange', window.__indexPageVisibilityHandler)
delete window.__indexPageVisibilityHandler
}
isUnmounted.value = true
clearTheTimer()
})
onBeforeUnmount(() => {
isUnmounted.value = true
clearTheTimer()
})
@ -934,6 +1164,7 @@ defineExpose({ getMapData, computedRatio, clearTheTimer }) // 提供 open 方法
width: 100%;
height: 100%;
box-sizing: border-box;
position: relative;
.index-page-container {
position: relative;
@ -972,6 +1203,7 @@ defineExpose({ getMapData, computedRatio, clearTheTimer }) // 提供 open 方法
.map-bg {
width: 100%;
height: auto;
position: relative;
}
}
@ -1007,6 +1239,7 @@ defineExpose({ getMapData, computedRatio, clearTheTimer }) // 提供 open 方法
cursor: pointer;
transform: translateZ(0);
will-change: transform;
z-index: 9;
.sort-num {
position: absolute;

View File

@ -1,5 +1,5 @@
<template>
<Dialog v-model="dialogVisible" title="库位信息" width="660" class="task-dialog">
<Dialog v-model="dialogVisible" title="库位信息" width="800" class="task-dialog">
<div class="store-dialog">
<div class="store-dialog-left">
<div
@ -41,7 +41,7 @@
{{ item.locationNo || '--' }}
</div>
</div>
<el-form label-width="auto" ref="formRef">
<el-form label-width="auto" ref="formRef" style="width: 360px">
<el-form-item label="库位编号">
<el-input v-model="floorList[selectIndex].locationNo" :disabled="true" />
</el-form-item>
@ -137,7 +137,7 @@ const { push } = useRouter()
flex-shrink: 0;
.store-dialog-left-item {
width: 230px;
width: 320px;
height: 56px;
line-height: 56px;
text-align: center;
@ -151,6 +151,10 @@ const { push } = useRouter()
cursor: pointer;
border: 1px solid rgba(0, 0, 0, 0);
position: relative;
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
-o-text-overflow: ellipsis;
}
.ellipsis {

View File

@ -1110,6 +1110,9 @@ const inputBoxRef = ref() //文字输入框
const message = useMessage() //
// loading<script setup>
let loading = null
//
const nodeStyle = (item, index) => {
return {
@ -4037,8 +4040,8 @@ const getAllNodeList = async () => {
state.currentIndex = 0
list.forEach((item) => {
item.layerSelectionShow = true //
item.locationX = Number(item.locationX)
item.locationY = Number(item.locationY)
item.locationX = convertActualToBrowser(item.actualLocationX, item.actualLocationY).x
item.locationY = convertActualToBrowser(item.actualLocationX, item.actualLocationY).y
if (item.type === 1) {
item.dataObj = {}
@ -4408,8 +4411,13 @@ const disposeEventPoint = (x, y) => {
//
const convertActualToBrowser = (pointX, pointY) => {
const y1 = Number(imgBgObj.origin[1]) + Number(imgBgObj.height) * Number(imgBgObj.resolution)
let x = Math.max(Number(pointX) - Number(imgBgObj.origin[0]), 0) / Number(imgBgObj.resolution)
let y = Math.max(y1 - Number(pointY), 0) / Number(imgBgObj.resolution)
let x =
Math.round(
(Math.max(Number(pointX) - Number(imgBgObj.origin[0]), 0) / Number(imgBgObj.resolution)) *
10000
) / 10000
let y =
Math.round((Math.max(y1 - Number(pointY), 0) / Number(imgBgObj.resolution)) * 10000) / 10000
return {
x,
@ -4578,9 +4586,23 @@ onBeforeRouteLeave((to, from) => {
}
})
onMounted(() => {
getMapList()
getCheckDistance()
onMounted(async () => {
// loading
loading = ElLoading.service({
lock: true,
text: '加载中,请稍候...',
background: 'rgba(255, 255, 255, 0.7)'
})
try {
await Promise.all([getMapList(), getCheckDistance()])
} catch (e) {
//
message.error(e?.message || '页面加载失败')
} finally {
setTimeout(() => {
loading.close()
}, 1000)
}
window.addEventListener('keydown', handleKeyDown)
})

View File

@ -1,101 +1,105 @@
<template>
<div class="view-box">
<div id="toolbar">对齐时出现辅助线</div>
<div class="container">
<VueDragResizeRotate
:w="200"
:h="200"
:parent="true"
:debug="false"
:min-width="200"
:min-height="200"
:isConflictCheck="true"
:snap="true"
:snapTolerance="10"
@ref-line-params="getRefLineParams"
style="background-color: rgb(174, 213, 129)"
/>
<VueDragResizeRotate
:w="200"
:h="200"
:parent="true"
:x="210"
:debug="false"
:min-width="200"
:min-height="200"
:isConflictCheck="true"
:snap="true"
:snapTolerance="10"
@ref-line-params="getRefLineParams"
style="background-color: rgb(129, 212, 250)"
/>
<VueDragResizeRotate
:w="200"
:h="200"
:parent="true"
:x="420"
:debug="false"
:min-width="200"
:min-height="200"
:isConflictCheck="true"
:snap="true"
:snapTolerance="10"
@ref-line-params="getRefLineParams"
style="background-color: rgb(239, 154, 154)"
/>
<span
class="ref-line v-line"
v-for="(item, index) in vLine"
:key="'v_' + index"
v-show="item.display"
:style="{
left: item.position,
top: item.origin,
height: item.lineLength
}"
></span>
<span
class="ref-line h-line"
v-for="(item, index) in hLine"
:key="'h_' + index"
:style="{
top: item.position,
left: item.origin,
width: item.lineLength
}"
></span>
<div class="map-container">
<div
ref="fullscreenElement"
class="fullscreen-content"
:class="{ 'is-fullscreen': isFullscreen }"
>
<div
v-for="car in cars"
:key="car.id"
class="car"
:style="{ left: car.x + 'px', top: car.y + 'px' }"
>
<img src="@/assets/imgs/indexPage/car1.png" alt="car" class="car-image" />
</div>
</div>
</div>
</template>
<script setup>
import { ref } from 'vue'
import { ref, onMounted, onUnmounted } from 'vue'
const vLine = ref([])
const hLine = ref([])
const fullscreenElement = ref(null)
const isFullscreen = ref(false)
const getRefLineParams = (params) => {
const { vLine: newVLine, hLine: newHLine } = params
vLine.value = newVLine
hLine.value = newHLine
// 20
const cars = ref(
Array.from({ length: 100 }, (_, index) => ({
id: index + 1,
x: Math.random() * 1600, // x
y: Math.random() * 900, // y
dx: (Math.random() - 0.5) * 6, // x
dy: (Math.random() - 0.5) * 6 // y
}))
)
//
let updateInterval
const updateCarPositions = () => {
cars.value = cars.value.map((car) => {
//
let newX = car.x + car.dx
let newY = car.y + car.dy
//
if (newX <= 0 || newX >= 1600) car.dx *= -1
if (newY <= 0 || newY >= 900) car.dy *= -1
return {
...car,
x: newX,
y: newY
}
})
}
onMounted(() => {
// 10010
updateInterval = setInterval(updateCarPositions, 200)
})
onUnmounted(() => {
//
if (updateInterval) {
clearInterval(updateInterval)
}
})
</script>
<style scoped>
.container {
width: 1200px;
.map-container {
position: relative;
}
.fullscreen-content {
width: 1600px;
height: 800px;
background-color: beige;
background-color: #f0f0f0;
position: relative;
overflow: hidden;
}
.ref-line {
.car {
position: absolute;
background-color: rgb(219, 89, 110);
z-index: 9999;
width: 32px;
height: 32px;
transform: translate(-50%, -50%);
}
.v-line {
width: 1px;
.car-image {
width: 100%;
height: 100%;
object-fit: contain;
}
.h-line {
height: 1px;
/* 全屏模式下的样式 */
.fullscreen-content:fullscreen,
.fullscreen-content:-webkit-full-screen,
.fullscreen-content:-moz-full-screen,
.fullscreen-content:-ms-fullscreen {
width: 100vw;
height: 100vh;
background-color: white;
}
</style>

View File

@ -1,151 +0,0 @@
export default {
list: [
{
mediaId: 'r6ryvl6LrxBU0miaST4Y-q-G9pdsmZw0OYG4FzHQkKfpLfEwIH51wy2bxisx8PvW',
content: {
newsItem: [
{
title: '我是标题OOO',
author: '我是作者',
digest: '我是摘要',
content: '我是内容',
contentSourceUrl: 'https://www.iocoder.cn',
thumbMediaId: 'r6ryvl6LrxBU0miaST4Y-pIcmK-zAAId-9TGgy-DrSLhjVuWbuT3ZBjk9K1yQ0Dn',
showCoverPic: 0,
needOpenComment: 0,
onlyFansCanComment: 0,
url: 'http://mp.weixin.qq.com/s?__biz=MzA3NjM4MzQzOQ==&tempkey=MTIxMl9XaFphcmtJVFh3VEc4Q1MxQWwxQ3R5R0JGTXBDM1Q0N2ZFQm8zeUphOFlwNEpXSWxTYm9RQnJ6cHVuN2QxTE56SFBCYXc2RE9NcUxIeS1CQjJuUHhTWjBlN2VOeGRpRi1fZUhwN1FNQjdrQV9yRU9EU0hibHREZmZoVW5acnZrN3ZjaWsxejR3RGpKczBzTHFIM0dFNFZWVkpBc0dWWlAzUEhlVmpnfn4%3D&chksm=1f6354802814dd969ef83c0f3babe555c614270b30bc383beaf7ffd13b0257f0fe5ced9af694#rd',
thumbUrl:
'http://test.yudao.iocoder.cn/r6ryvl6LrxBU0miaST4Y-pIcmK-zAAId-9TGgy-DrSLhjVuWbuT3ZBjk9K1yQ0Dn.png'
},
{
title: '我是标题XXX',
author: '我是作者',
digest: '我是摘要',
content: '我是内容',
contentSourceUrl: 'https://www.iocoder.cn',
thumbMediaId: 'r6ryvl6LrxBU0miaST4Y-pIcmK-zAAId-9TGgy-DrSLhjVuWbuT3ZBjk9K1yQ0Dn',
showCoverPic: 0,
needOpenComment: 0,
onlyFansCanComment: 0,
url: 'http://mp.weixin.qq.com/s?__biz=MzA3NjM4MzQzOQ==&tempkey=MTIxMl9yTlYwOEs1clpwcE5OUEhCQWwxQ3R5R0JGTXBDM1Q0N2ZFQm8zeUphOFlwNEpXSWxTYm9RQnJ6cHVuN0NSMjFqN3N1aUZMbFNVLTZHN2ZDME9qOGp2THk2RFNlSTlKZ3Y1czFVZDdQQm5IeUg3dEppSUtpQUh5SExOOTRkT3dHNUdBdHdWSWlOendlREV3dS1jUEVQbFpiVTZmVW5iRWhZcGdkNTFRfn4%3D&chksm=1f6354802814dd96a403151cd44c7da4eecf0e475d25423e46ecd795b513bafd829a75daef9b#rd',
thumbUrl:
'http://test.yudao.iocoder.cn/r6ryvl6LrxBU0miaST4Y-pIcmK-zAAId-9TGgy-DrSLhjVuWbuT3ZBjk9K1yQ0Dn.png'
}
]
},
updateTime: 1673655730
},
{
mediaId: 'r6ryvl6LrxBU0miaST4Y-jGpXnO73ihN0lsNXknCRQHapp2xgHMRxHKG50LituFe',
content: {
newsItem: [
{
title: '我是标题(修改)',
author: '我是作者',
digest: '我是摘要',
content: '我是内容',
contentSourceUrl: 'https://www.iocoder.cn',
thumbMediaId: 'r6ryvl6LrxBU0miaST4Y-pIcmK-zAAId-9TGgy-DrSLhjVuWbuT3ZBjk9K1yQ0Dn',
showCoverPic: 0,
needOpenComment: 0,
onlyFansCanComment: 0,
url: 'http://mp.weixin.qq.com/s?__biz=MzA3NjM4MzQzOQ==&tempkey=MTIxMl95WVFXYndIZnZJd0t5cjgvQWwxQ3R5R0JGTXBDM1Q0N2ZFQm8zeUphOFlwNEpXSWxTYm9RQnJ6cHVuN1dlNURPbWswbEF4RDd5dVJTdjQ4cm9Cc0Q1TWhpMUh6SE1hVEE3ZHljaHhlZjZYSGF5N2JNSHpDTlh6ajNZbkpGTGpTcUQ4M3NMdW41ZUpXNFZZQ1VKbVlaMVp5ekxEV1czREdsY1dOYTZnfn4%3D&chksm=1f6354be2814dda8e6238037c2ebd52b1c8e80e93249a861ad80e4d40e5ca7207233475ca689#rd',
thumbUrl:
'http://test.yudao.iocoder.cn/r6ryvl6LrxBU0miaST4Y-pIcmK-zAAId-9TGgy-DrSLhjVuWbuT3ZBjk9K1yQ0Dn.png'
}
]
},
updateTime: 1673655584
},
{
mediaId: 'r6ryvl6LrxBU0miaST4Y-v5SrbNCPpD6M_p3TmSrYwTjKogs-0DMJgmjMyNZPeMO',
content: {
newsItem: [
{
title: '1321',
author: '3232',
digest: '1333',
content: '<p>444</p>',
contentSourceUrl: 'http://www.iocoder.cn',
thumbMediaId: 'r6ryvl6LrxBU0miaST4Y-tlQmcl3RdC-Jcgns6IQtf7zenGy3b86WLT7GzUcrb1T',
showCoverPic: 0,
needOpenComment: 0,
onlyFansCanComment: 0,
url: 'http://mp.weixin.qq.com/s?__biz=MzA3NjM4MzQzOQ==&tempkey=MTIxMl9jelJiaDAzbmdpSkJOZ2M2QWwxQ3R5R0JGTXBDM1Q0N2ZFQm8zeUphOFlwNEpXSWxTYm9RQnJ6cHVuNDNXVVc2ZDRYeTY0Zm1weXR6dE9vQWh1TzEwbEpUVnRfVzJyaGFDNXBkZ0ZXM2JFOTNaRHNhOHRUeFdEanhMeS01X01kMUNWQ1BpRER3cjYwTl9pMnpFLUJhZXFucVVfM1pDUXlTUEl1S25nfn4%3D&chksm=1f6354bc2814ddaa56a90ad5bc3d078601c8d1589ba01827a8170587bc830ff9747b5f59c3a0#rd',
thumbUrl:
'http://mmbiz.qpic.cn/mmbiz_png/btUmCVHwbJUoicwBiacjVeQbu6QxgBVrukfSJXz509boa21SpH8OVHAqXCJiaiaAaHQJNxwwsa0gHRXVr0G5EZYamw/0?wx_fmt=png'
}
]
},
updateTime: 1673628969
},
{
mediaId: 'r6ryvl6LrxBU0miaST4Y-vdWrisK5EZbk4Y3tzh8P0PG0eEUbnQrh0BcsEb3WNP0',
content: {
newsItem: [
{
title: 'tudou',
author: 'haha',
digest: '312',
content: '<p>132312</p>',
contentSourceUrl: 'http://www.iocoder.cn',
thumbMediaId: 'r6ryvl6LrxBU0miaST4Y-pgFtUNLu1foMSAMkoOsrQrTZ8EtTMssBLfTtzP0dfjG',
showCoverPic: 0,
needOpenComment: 0,
onlyFansCanComment: 0,
url: 'http://mp.weixin.qq.com/s?__biz=MzA3NjM4MzQzOQ==&tempkey=MTIxMl9qdkJ1ZjBoUmg2Uk9TS3RlQWwxQ3R5R0JGTXBDM1Q0N2ZFQm8zeUphOFlwNEpXSWxTYm9RQnJ6cHVuNVg2aTJsaC1fMkU2eXNacUplN3VDTTZFZkhtMjhuTUZvWkxsNDBRSXExY2tiVXRHb09TaHgtREhzY3doZ0JYeC1TSTZ5eWZldXJsOWtfbV8yMi1aYkcyZ2pOY0haM0Ntb3VSWEtxUGVFRlNBfn4%3D&chksm=1f6354ba2814ddacf0184b24d310483641ef190b1faac098c285eb416c70017e2f54decfa1af#rd',
thumbUrl:
'http://test.yudao.iocoder.cn/r6ryvl6LrxBU0miaST4Y-pgFtUNLu1foMSAMkoOsrQrTZ8EtTMssBLfTtzP0dfjG.png'
}
]
},
updateTime: 1673628760
},
{
mediaId: 'r6ryvl6LrxBU0miaST4Y-u9kTIm1DhWZDdXyxsxUVv2Z5DAB99IPxkIRTUUD206k',
content: {
newsItem: [
{
title: '12',
author: '333',
digest: '123',
content: '123',
contentSourceUrl: 'https://www.iocoder.cn',
thumbMediaId: 'r6ryvl6LrxBU0miaST4Y-jVixJGgnBnkBPRbuVptOW0CHYuQFyiOVNtamctS8xU8',
showCoverPic: 0,
needOpenComment: 0,
onlyFansCanComment: 0,
url: 'http://mp.weixin.qq.com/s?__biz=MzA3NjM4MzQzOQ==&tempkey=MTIxMl9qVVhpSDZUaFJWTzBBWWRVQWwxQ3R5R0JGTXBDM1Q0N2ZFQm8zeUphOFlwNEpXSWxTYm9RQnJ6cHVuNWRnTDJWYmF2NER0clV1bThmQ0xUR3hqQnJkZ3BJSUNmNDJmc0lCZ1dadkVnZ3Z5bkN4YWtVUjhoaWZWYzZURUR4NnpMd0Y4Z3U5aUdib0lkMzI4Rjg3SG9JX2FycTMxbUctOHplaTlQVVhnfn4%3D&chksm=1f6354b62814dda076c778af33f06580165d8aa81f7798d55cfabb1886b5c74d9b2124a3535c#rd',
thumbUrl:
'http://test.yudao.iocoder.cn/r6ryvl6LrxBU0miaST4Y-jVixJGgnBnkBPRbuVptOW0CHYuQFyiOVNtamctS8xU8.jpg'
}
]
},
updateTime: 1673626494
},
{
mediaId: 'r6ryvl6LrxBU0miaST4Y-sO24upobaENDmeByfBTfaozB3aOqSMAV0lGy-UkHXE7',
content: {
newsItem: [
{
title: '我是标题',
author: '我是作者',
digest: '我是摘要',
content: '我是内容',
contentSourceUrl: 'https://www.iocoder.cn',
thumbMediaId: 'r6ryvl6LrxBU0miaST4Y-pIcmK-zAAId-9TGgy-DrSLhjVuWbuT3ZBjk9K1yQ0Dn',
showCoverPic: 0,
needOpenComment: 0,
onlyFansCanComment: 0,
url: 'http://mp.weixin.qq.com/s?__biz=MzA3NjM4MzQzOQ==&tempkey=MTIxMl9LT2dqRnpMNUpsR0hjYWtBQWwxQ3R5R0JGTXBDM1Q0N2ZFQm8zeUphOFlwNEpXSWxTYm9RQnJ6cHVuNGNmazZTdlE5WkxvU0tfX2V5cjV2WjJiR0xjQUhyREFSZWo2eWNrUW9EYVh6ZkpWRXBLR3FmTEV6YldBMno3Q2ZvVXBSdzlaVDc3aFhndEpQWUwzWmFMUWt0YVVURE1VZ1FsQTdPMlRtc3JBfn4%3D&chksm=1f6354aa2814ddbcc2637382f963a8742993ac38ebcebe6e3411df5ac82ac7bbdb391be6494a#rd',
thumbUrl:
'http://test.yudao.iocoder.cn/r6ryvl6LrxBU0miaST4Y-pIcmK-zAAId-9TGgy-DrSLhjVuWbuT3ZBjk9K1yQ0Dn.png'
}
]
},
updateTime: 1673534279
}
],
total: 6
}