zn-admin-vue3-wcs/src/views/board/allBoard/index.vue
yyy 2bc20f05f0 实时地图封装为通用组件 在整体看板适应看板宽度 在实时地图初始展示地图图片宽度
编辑地图生成直线改为映射到两点线段中
生成线库和区域改为前端将线库id、区域id、线库名称、区域名称等在地图保存接口传递给后端
2025-04-02 10:28:31 +08:00

789 lines
23 KiB
Vue

<template>
<!-- 顶部统计 -->
<div class="top-box" v-if="data">
<el-row :gutter="20">
<el-col :span="12">
<div class="grid-content ep-bg-purple">
<el-card style="max-width: 100%" shadow="never">
<el-row :gutter="5" v-if="data.taskStatusVO">
<el-col :span="4">
<div class="top-item">
<div class="top-item-title"> 任务总数 </div>
<div class="top-item-num"> {{ data.taskStatusVO.tasksNumber || 0 }} </div>
</div>
</el-col>
<el-col :span="4">
<div class="top-item">
<div class="top-item-title"> 执行中 </div>
<div class="top-item-num"> {{ data.taskStatusVO.underwayNum || 0 }} </div>
</div>
</el-col>
<el-col :span="4">
<div class="top-item">
<div class="top-item-title"> 待执行 </div>
<div class="top-item-num"> {{ data.taskStatusVO.pendingExecutionNum || 0 }} </div>
</div>
</el-col>
<el-col :span="4">
<div class="top-item">
<div class="top-item-title"> 已完成 </div>
<div class="top-item-num"> {{ data.taskStatusVO.completedNum || 0 }} </div>
</div>
</el-col>
<el-col :span="4">
<div class="top-item">
<div class="top-item-title"> 已取消 </div>
<div class="top-item-num" style="color: #a6a6a6">
{{ data.cancelledNum || 0 }}
</div>
</div>
</el-col>
<el-col :span="4">
<div class="top-item" style="border: none; color: #c60606">
<div class="top-item-title"> 异常 </div>
<div class="top-item-num" style="color: #c60606">
{{ data.taskStatusVO.abnormalNum || 0 }}
</div>
</div>
</el-col>
</el-row>
</el-card>
</div>
</el-col>
<el-col :span="12">
<div class="grid-content ep-bg-purple">
<el-card style="max-width: 100%" shadow="never">
<el-row :gutter="5" v-if="data.statistics">
<el-col :span="4">
<div class="top-item">
<div class="top-item-title"> 车辆总数 </div>
<div class="top-item-num"> {{ data.statistics.total || 0 }} </div>
</div>
</el-col>
<el-col :span="4">
<div class="top-item">
<div class="top-item-title"> 任务中 </div>
<div class="top-item-num"> {{ data.statistics.inTask || 0 }} </div>
</div>
</el-col>
<el-col :span="4">
<div class="top-item">
<div class="top-item-title"> 锁定 </div>
<div class="top-item-num"> {{ data.statistics.doLock || 0 }} </div>
</div>
</el-col>
<el-col :span="4">
<div class="top-item">
<div class="top-item-title"> 充电中 </div>
<div class="top-item-num"> {{ data.statistics.charge || 0 }} </div>
</div>
</el-col>
<el-col :span="4">
<div class="top-item">
<div class="top-item-title"> 离线 </div>
<div class="top-item-num" style="color: #a6a6a6">
{{ data.statistics.offline || 0 }}
</div>
</div>
</el-col>
<el-col :span="4">
<div class="top-item" style="border: none; color: #c60606">
<div class="top-item-title"> 故障 </div>
<div class="top-item-num" style="color: #c60606">
{{ data.statistics.fault || 0 }}
</div>
</div>
</el-col>
</el-row>
</el-card>
</div>
</el-col>
</el-row>
</div>
<!-- 下部分 -->
<div class="bottom-box">
<el-row :gutter="10">
<!-- 左边 -->
<el-col :span="6">
<div class="bottom-box-item" v-if="data">
<div class="bottom-box-item-table">
<div class="bottom-box-item-title">
<div class="bottom-box-item-title-left"> 执行中 </div>
<div class="bottom-box-item-title-right" @click="toManyTask('执行中')">
查看更多
</div>
</div>
<div>
<el-table :data="data.underway" style="width: 100%">
<el-table-column prop="taskNo" label="任务编号" />
<el-table-column prop="skuNumber" label="车辆编号" />
<el-table-column prop="startTime" label="开始时间" :formatter="dateFormatter" />
<el-table-column prop="address" label="任务阶段" />
</el-table>
</div>
</div>
</div>
<div class="bottom-box-item" style="margin-top: 8px" v-if="data">
<div class="bottom-box-item-table">
<div class="bottom-box-item-title">
<div class="bottom-box-item-title-left"> 待执行 </div>
<div class="bottom-box-item-title-right" @click="toManyTask('未开始')">
查看更多
</div>
</div>
<div>
<el-table :data="data.pendingExecution" style="width: 100%">
<el-table-column prop="taskNo" label="任务编号" />
<el-table-column prop="priorilty" label="优先级" />
<el-table-column prop="createTime" label="生成时间" :formatter="dateFormatter" />
</el-table>
</div>
</div>
</div>
</el-col>
<!-- 实时地图 -->
<el-col :span="12">
<div style="margin-bottom: 10px">
<el-cascader
v-model="mapValue"
:options="list"
@change="handleChangeMap"
style="width: 160px"
/>
</div>
<div style="width: 100%; padding-bottom: 120px" class="map-box-allBoard">
<indexPage ref="indexPageRef" :isAllBoard="true" :isFullScreen="true" />
</div>
<div
style="position: fixed; bottom: 20px"
:style="{ width: widthVal + 'px', left: leftVal + 'px' }"
v-if="data && data.deviceStatusInfoVOS"
>
<div
ref="scrollContainer"
class="scroll-container"
@mousedown="startDrag"
@mousemove="onDrag"
@mouseup="endDrag"
@mouseleave="endDrag"
>
<div class="content">
<div
v-for="(n, i) in data.deviceStatusInfoVOS"
:key="i"
class="item"
:class="{ noBoarder: i === data.deviceStatusInfoVOS.length - 1 }"
>
<div class="scroll-container-item-left">
<div class="scroll-container-item-left-title">{{
filterTypeFun(n.deviceType, typeList)
}}</div>
<div class="scroll-container-item-left-img">
<img
:src="n.defaultImage"
alt=""
object-fit="contain"
style="width: 100%; height: 100%"
/>
</div>
</div>
<div class="scroll-container-item-right">
<div class="scroll-container-item-right-item">
<div class="scroll-container-item-right-item-title"> 数量: </div>
<div class="scroll-container-item-right-item-num">
{{ n.totalNum || 0 }}
</div>
</div>
<div class="scroll-container-item-right-item" style="margin-top: 3px">
<div class="scroll-container-item-right-item-title"> 正常数量: </div>
<div class="scroll-container-item-right-item-num">
{{ n.normalNum || 0 }}
</div>
</div>
<div class="scroll-container-item-right-item" style="margin-top: 3px">
<div class="scroll-container-item-right-item-title"> 异常数量: </div>
<div class="scroll-container-item-right-item-num" style="color: #c60606">
{{ n.abnormalNum || 0 }}
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</el-col>
<!-- 右边 -->
<el-col :span="6">
<div class="bottom-box-item" v-if="data">
<div class="bottom-box-item-table">
<div class="bottom-box-item-title">
<div class="bottom-box-item-title-left"> 异常信息 </div>
<div class="bottom-box-item-title-right" @click="toManyWarnMsg"> 查看更多 </div>
</div>
<div>
<el-table :data="data.robotWarnMsgDOS" style="width: 100%">
<el-table-column prop="robotNo" label="车辆编号" />
<el-table-column prop="warnMsg" label="异常信息">
<template #default="scope">
<div class="warn-msg">
<div
class="warn-msg-color"
:style="{ backgroundColor: computedBackgroundColor(scope.row.warnLevel) }"
>
</div>
<div class="warn-msg-text">
{{ scope.row.warnMsg || '' }}
</div>
</div>
</template>
</el-table-column>
<el-table-column prop="createTime" label="发生时间" :formatter="dateFormatter" />
</el-table>
</div>
</div>
</div>
<div class="bottom-box-item" style="margin-top: 8px" v-if="data">
<div class="bottom-box-item-table">
<div class="bottom-box-item-title">
<div class="bottom-box-item-title-left"> 车辆信息 </div>
<div class="bottom-box-item-title-right" @click="goCarBord"> 查看更多 </div>
</div>
<div>
<el-table :data="data.robotElectricityLevelVOS" style="width: 100%">
<el-table-column prop="robotNo" label="车辆编号" />
<el-table-column prop="doLock" label="车辆状态">
<template #default="scope">
<span>{{ scope.row.doLock === 0 ? '正常' : '锁定' }}</span>
</template>
</el-table-column>
<el-table-column prop="batSoc" label="电量百分比">
<template #default="scope">
<div v-if="scope.row.batSoc === null">—</div>
<div class="battery-box-all">
<div class="battery-box" v-if="scope.row.batSoc !== null">
<img
src="@/assets/imgs/allBoard/dianlianggreen.png"
alt=""
class="battery-box-img"
v-if="scope.row.batSoc >= 40"
/>
<img
src="@/assets/imgs/allBoard/dianliangyellow.png"
alt=""
class="battery-box-img"
v-if="scope.row.batSoc < 40 && scope.row.batSoc >= 20"
/>
<img
src="@/assets/imgs/allBoard/dianliangred.png"
alt=""
class="battery-box-img"
v-if="scope.row.batSoc < 20"
/>
<div class="battery-box-inner">
<div
class="battery-box-inner-inner"
:class="
scope.row.batSoc >= 40
? 'green-bg'
: scope.row.batSoc < 40 && scope.row.batSoc >= 20
? 'yellow-bg'
: 'red-bg'
"
:style="{ width: scope.row.batSoc + '%' }"
>
</div>
</div>
</div>
<span class="battery-box-text" v-if="scope.row.batSoc !== null"
>{{ scope.row.batSoc || 0 }}%</span
>
</div>
</template>
</el-table-column>
</el-table>
</div>
</div>
</div>
</el-col>
</el-row>
</div>
</template>
<script setup>
import { ref, reactive, onMounted, onBeforeUnmount } from 'vue'
import { dateFormatter } from '@/utils/formatTime'
import * as ChartsApi from '@/api/boardCharts'
import { DICT_TYPE, getIntDictOptions, getDictOptions } from '@/utils/dict'
import indexPage from '../../mapPage/realTimeMap/components/indexPage.vue'
import * as MapApi from '@/api/map/map'
const router = useRouter() // 路由对象
const indexPageRef = ref(null)
const typeList = ref([])
defineOptions({ name: 'BoardAllBoard' })
const data = ref({
taskStatusVO: {
//顶部左边统计
pendingExecutionNum: 0,
underwayNum: 0,
completedNum: 0,
cancelledNum: 0,
abnormalNum: 0,
tasksNumber: 0
},
statistics: {
//顶部右边 统计
total: 0,
inTask: 0,
doLock: 0,
charge: 0,
offline: 0,
fault: 0
},
underway: [], //执行中
pendingExecution: [], //待执行
robotWarnMsgDOS: [], //异常信息
deviceStatusInfoVOS: [], //设备信息
robotElectricityLevelVOS: [] //车辆信息
})
//获取数据
const getAllData = async () => {
let datas = await ChartsApi.bulletinBoardGet()
console.log(datas)
data.value = datas
}
const widthVal = ref(0)
const leftVal = ref(0)
const getWidth = (val) => {
widthVal.value = val
console.log(widthVal.value)
}
const getLeftPx = (val) => {
leftVal.value = val
}
//返回异常颜色
const computedBackgroundColor = (warnLevel) => {
switch (warnLevel) {
case 1:
return '#1677FF'
case 2:
return '#F1CD0B'
case 3:
return '#E07300'
case 4:
return '#C60606'
default:
return ''
}
}
//前往更多任务
const toManyTask = (type) => {
// console.log(getIntDictOptions(DICT_TYPE.ROBOT_TASK_STATUS))
// return
router.push({
path: '/task-management/task-list',
query: {
type: type
}
})
}
//前往异常信息
const toManyWarnMsg = () => {
router.push({
path: '/carError'
})
}
// 前往车辆看板
const goCarBord = () => {
router.push({
path: '/board/carBoard'
})
}
const list = ref([]) // 地图区域列表
const mapValue = ref([]) //选中的区域绑定的数组值
const mapInfo = ref(null) //选中的区域
//获取地图区域
const getList = async () => {
let data = await MapApi.getPositionMapGetMap()
let mapList = []
for (let key in data) {
mapList.push({
floor: key,
label: key + '层',
value: key,
children: data[key]
})
}
if (mapList.length) {
mapList.forEach((item) => {
if (item.children.length) {
item.children.forEach((child) => {
child.label = child.area
child.value = child.id
})
}
})
}
list.value = mapList
console.log(list.value, data)
if (mapValue.value.length) {
handleChangeMap(mapValue.value)
} else {
mapValue.value = [list.value[0].value, list.value[0].children[0].value]
mapInfo.value = list.value[0].children[0]
? JSON.parse(JSON.stringify(list.value[0].children[0]))
: {
mapId: '',
floor: '',
area: ''
}
indexPageRef.value.getMapData(mapInfo.value)
}
}
//切换地图选择
const handleChangeMap = async (e) => {
mapInfo.value = findChildrenByValues(list.value, e)
? findChildrenByValues(list.value, e)
: {
mapId: '',
floor: '',
area: ''
}
console.log(mapInfo.value)
indexPageRef.value.getMapData(mapInfo.value)
}
// 筛选出对应的区域对象
const findChildrenByValues = (tree, values) => {
if (!tree || tree.length === 0) {
return null
}
function traverse(node) {
if (node.children) {
for (let child of node.children) {
if (values.includes(child.value)) {
return child
}
let result = traverse(child)
if (result) {
return result
}
}
}
return null
}
for (let root of tree) {
if (values.includes(root.value)) {
if (root.children) {
for (let child of root.children) {
if (values.includes(child.value)) {
return child
}
let result = traverse(child)
if (result) {
return result
}
}
}
}
let result = traverse(root)
if (result) {
return result
}
}
return null
}
const scrollContainer = ref(null)
let isDragging = false
let startX = 0
let scrollLeft = 0
const startDrag = (e) => {
isDragging = true
const rect = scrollContainer.value.getBoundingClientRect()
startX = e.clientX - rect.left
scrollLeft = scrollContainer.value.scrollLeft
scrollContainer.value.style.cursor = 'grabbing'
}
const onDrag = (e) => {
if (!isDragging) return
const rect = scrollContainer.value.getBoundingClientRect()
const mouseX = e.clientX - rect.left
const dragDistance = (mouseX - startX) * 2.5 // 速度系数
scrollContainer.value.scrollLeft = scrollLeft - dragDistance
}
const endDrag = () => {
isDragging = false
scrollContainer.value.style.cursor = 'grab'
}
const getElementWidthByClass = (className) => {
const element = document.querySelector(`.${className}`)
if (element) {
// return window.getComputedStyle(element).width
const widthWithUnit = window.getComputedStyle(element).width
return widthWithUnit.slice(0, -2)
}
return null
}
const getLeft = () => {
let indexpageContainer = document.getElementsByClassName('map-box-allBoard')[0]
// console.log('距离左边的距离', indexpageContainer.getBoundingClientRect().left)
leftVal.value = indexpageContainer.getBoundingClientRect().left
}
const getWidthPx = () => {
let width = getElementWidthByClass('map-box-allBoard')
// console.log("pppppppppppppp",width)
widthVal.value = width
}
const getLeftWidth = () => {
nextTick(() => {
getWidthPx()
getLeft()
})
}
//根据type和列表返回对应中文
const filterTypeFun = (type, list) => {
if (list.length) {
let obj = list.find((item) => {
return item.value == type
})
return obj == undefined ? type : obj.label
} else {
return type
}
}
onMounted(() => {
typeList.value = getDictOptions(DICT_TYPE.DEVICE_TYPE)
getAllData()
getList()
getLeftWidth()
window.addEventListener('resize', getLeftWidth)
})
onBeforeUnmount(() => {
window.removeEventListener('resize', getLeftWidth)
})
</script>
<style scoped>
.top-box {
width: 100%;
}
.top-item {
width: 100%;
display: flex;
flex-direction: column;
align-items: center;
border-right: 1px solid #e7eaec;
}
.top-item-title {
font-family:
PingFangSC,
PingFang SC;
font-weight: 500;
font-size: 18px;
color: #0d162a;
margin-bottom: 8px;
}
.top-item-num {
font-family:
PingFangSC,
PingFang SC;
font-weight: 500;
font-size: 24px;
color: #00329f;
}
.bottom-box {
width: 100%;
margin-top: 12px;
}
.bottom-box-item {
width: 100%;
}
.bottom-box-item-title {
width: 100%;
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 8px;
}
.bottom-box-item-title-left {
font-family:
PingFangSC,
PingFang SC;
font-weight: 500;
font-size: 14px;
color: #0d162a;
}
.bottom-box-item-title-right {
font-family:
PingFangSC,
PingFang SC;
font-weight: 400;
font-size: 14px;
color: #1677ff;
cursor: pointer;
}
.bottom-box-item-table {
width: 100%;
}
.battery-box {
width: 22px;
height: 11px;
position: relative;
}
.battery-box-img {
width: 100%;
height: 100%;
vertical-align: top;
}
.battery-box-inner {
width: 17px;
height: 8px;
position: absolute;
left: 50%;
top: 50%;
transform: translateX(-50%) translateY(-50%);
}
.battery-box-inner-inner {
height: 100%;
}
.green-bg {
background: #52c41a;
}
.yellow-bg {
background: #f1cd0b;
}
.red-bg {
background: #c60606;
}
.battery-box-text {
font-family:
PingFangSC,
PingFang SC;
font-weight: 400;
font-size: 12px;
color: #0d162a;
margin-left: 3px;
}
.battery-box-all {
display: flex;
align-items: center;
flex-wrap: wrap;
}
.warn-msg {
display: flex;
}
.warn-msg-color {
width: 8px;
height: 8px;
flex-shrink: 0;
border-radius: 50%;
margin-right: 4px;
margin-top: 4px;
}
.warn-msg-text {
flex: 1;
font-family:
PingFangSC,
PingFang SC;
font-weight: 400;
font-size: 12px;
color: #0d162a;
margin-top: -3px;
}
/* 容器样式 */
.scroll-container {
width: 100%;
height: 104px;
overflow: hidden;
position: relative;
cursor: grab;
user-select: none;
background: #fff;
margin-top: 10px;
}
/* 隐藏滚动条 */
.scroll-container::-webkit-scrollbar {
display: none;
}
/* 内容布局 */
.content {
display: inline-flex;
height: 100%;
padding: 20px 0;
white-space: nowrap;
}
/* 单个项目样式 */
.item {
display: inline-flex;
align-items: center;
padding: 17px 30px;
background: #fff;
transition: transform 0.2s;
border-right: 2px solid #e9e9e9;
}
/* .item:hover {
transform: translateY(-3px);
} */
/* 拖拽时状态 */
.scroll-container:active {
cursor: grabbing;
}
.scroll-container-item-left-title {
font-family:
PingFangSC,
PingFang SC;
font-weight: 500;
font-size: 14px;
color: #0d162a;
margin-bottom: 14px;
flex-shrink: 0;
}
.scroll-container-item-left-img {
width: 40px;
height: 40px;
vertical-align: top;
flex-shrink: 0;
}
.scroll-container-item-right {
flex-shrink: 0;
margin-left: 16px;
}
.noBoarder {
border-right: none;
}
.scroll-container-item-right-item {
display: flex;
flex-shrink: 0;
}
.scroll-container-item-right-item-title {
font-family:
PingFangSC,
PingFang SC;
font-weight: 400;
font-size: 12px;
color: #0d162a;
flex-shrink: 0;
}
.scroll-container-item-right-item-num {
font-family:
PingFangSC,
PingFang SC;
font-weight: 400;
font-size: 12px;
color: #0d162a;
flex-shrink: 0;
}
</style>