<template>
|
<div class="monitor-svg">
|
<!-- 网格背景 -->
|
<div id="bg"></div>
|
<!-- svg区域 -->
|
<div id="svg" :style="{ transform: `scale(${scale})` }">
|
<!-- 缩放和移动 -->
|
<VueDragResize
|
:isResizable="false"
|
:parentScaleX="scale"
|
:parentScaleY="scale"
|
@resizing="resize"
|
@dragging="resize"
|
ref="vdr"
|
>
|
<!-- 底图 -->
|
<div class="svg-bg">
|
<img
|
:src="svgBg"
|
:style="{ width: viewBox.w + 'px', height: viewBox.h + 'px' }"
|
/>
|
</div>
|
<!-- svg -->
|
<div
|
class="svg"
|
v-html="svgHtml"
|
:style="{ width: viewBox.w + 'px', height: viewBox.h + 'px' }"
|
></div>
|
<div class="points-locations" v-if="showLocation">
|
<div class="points">
|
<div
|
class="point"
|
v-for="point in points"
|
:key="point.name"
|
:style="{ top: point.y + 'px', left: point.x + 'px' }"
|
:title="point.name"
|
>
|
<div class="id" v-if="showLocationID">
|
{{ point.name.replace("Point-", "") }}
|
</div>
|
</div>
|
</div>
|
<div class="locations">
|
<div
|
class="location"
|
v-for="location in locations"
|
:key="location.name"
|
:style="{ top: location.y + 'px', left: location.x + 'px' }"
|
:title="location.name"
|
>
|
<div class="id" v-if="showLocationID">
|
{{ location.name.replace("Location-", "") }}
|
</div>
|
</div>
|
</div>
|
</div>
|
<!-- vehicle -->
|
<div class="vehicle-wrap">
|
<div
|
v-for="v in vehicles"
|
:key="v.name"
|
class="vehicle"
|
:class="{ active: currentVehicle && currentVehicle.name == v.name }"
|
:style="{ top: v.Y + 'px', left: v.X + 'px' }"
|
:title="v.name"
|
@click="currentVehicle = v"
|
>
|
<div class="vehicle-name">{{ v.name }}</div>
|
</div>
|
</div>
|
</VueDragResize>
|
|
<!-- 调试用end -->
|
</div>
|
<!-- 操作 -->
|
<div id="action">
|
<div class="action">
|
<Dropdown trigger="click" placement="left">
|
<Icon type="ios-cog" :size="18"></Icon>
|
<DropdownMenu slot="list">
|
<DropdownItem>
|
<Checkbox v-model="showLocation" @on-change="onShowLocationChange"
|
>显示点位</Checkbox
|
>
|
</DropdownItem>
|
<DropdownItem>
|
<Checkbox v-model="showLocationID">显示点位ID</Checkbox>
|
</DropdownItem>
|
</DropdownMenu>
|
</Dropdown>
|
</div>
|
<div class="action" @click="running = !running">
|
<Tooltip v-if="running" content="暂停" placement="left-start">
|
<Icon type="md-pause" :size="18" />
|
</Tooltip>
|
<Tooltip v-else content="开始" placement="left-start">
|
<Icon type="md-play" :size="18" />
|
</Tooltip>
|
</div>
|
</div>
|
<div id="tips">
|
<div class="status">
|
状态:
|
<span v-if="running" style="color: #19be6b">
|
<Icon type="md-radio-button-on" color="#19be6b" />
|
运行中
|
</span>
|
<span v-else style="color: #ff9900">
|
<Icon type="md-radio-button-on" color="#ff9900" />
|
已暂停
|
</span>
|
</div>
|
<div class="op">
|
<Tooltip content="居中视图">
|
<a href="javascript:;" @click="alignView">
|
<Icon type="md-contract" size="16" />
|
</a>
|
</Tooltip>
|
</div>
|
<div class="scale-indicator">
|
<Dropdown
|
trigger="click"
|
style="margin-left: 11px"
|
@on-click="setScale"
|
>
|
<a href="javascript:void(0)">
|
{{ Math.round(scale * 100) }}%
|
<Icon type="ios-arrow-down"></Icon>
|
</a>
|
<DropdownMenu slot="list">
|
<DropdownItem :name="0.25" :selected="scale == 0.25"
|
>25%</DropdownItem
|
>
|
<DropdownItem :name="0.5" :selected="scale == 0.5"
|
>50%</DropdownItem
|
>
|
<DropdownItem :name="0.75" :selected="scale == 0.75"
|
>75%</DropdownItem
|
>
|
<DropdownItem :name="1" :selected="scale == 1">100%</DropdownItem>
|
<DropdownItem :name="1.5" :selected="scale == 1.5"
|
>150%</DropdownItem
|
>
|
<DropdownItem :name="2" :selected="scale == 2">200%</DropdownItem>
|
<DropdownItem :name="3" :selected="scale == 3">300%</DropdownItem>
|
<DropdownItem :name="4" :selected="scale == 4">400%</DropdownItem>
|
<DropdownItem :name="5" :selected="scale == 5">500%</DropdownItem>
|
<DropdownItem :name="6" :selected="scale == 6">600%</DropdownItem>
|
<DropdownItem :name="7" :selected="scale == 7">700%</DropdownItem>
|
<DropdownItem :name="8" :selected="scale == 8">800%</DropdownItem>
|
<DropdownItem :name="9" :selected="scale == 9">900%</DropdownItem>
|
<DropdownItem :name="10" :selected="scale == 10"
|
>1000%</DropdownItem
|
>
|
</DropdownMenu>
|
</Dropdown>
|
<Icon type="md-remove" size="18" @click="scaleDown" />
|
<div class="slider">
|
<Slider
|
v-model="scale"
|
:min="0.1"
|
:max="10"
|
:step="0.1"
|
:tip-format="
|
(val) => {
|
return Math.round(val * 100) + '%';
|
}
|
"
|
></Slider>
|
</div>
|
<Icon type="md-add" size="18" @click="scaleUp" />
|
</div>
|
</div>
|
</div>
|
</template>
|
|
<script>
|
import { webConfig } from "@/utils";
|
import HttpRequest from "@/api/request.js";
|
import VueDragResize from "vue-drag-resize";
|
export default {
|
name: "MonitorSVG",
|
components: {
|
VueDragResize,
|
},
|
data() {
|
return {
|
height: 123,
|
svgName: webConfig.svgName,
|
svgHtml: "",
|
svgBg: "",
|
showRule: true,
|
showGrid: true,
|
showDrawer: true,
|
viewBox: {
|
x: 0,
|
y: 0,
|
w: 0,
|
h: 0,
|
},
|
interval: 5,
|
running: true,
|
showTaskDialog: false,
|
showOrderDialog: false,
|
svgCoord: {
|
a: 0,
|
b: 0,
|
c: 0,
|
d: 0,
|
e: 0,
|
f: 0,
|
},
|
vehicles: [
|
{
|
name: "",
|
x: 0,
|
y: 0,
|
X: 0,
|
Y: 0,
|
t: 0,
|
top: 0,
|
left: 0,
|
d: {}, // 车辆信息
|
},
|
],
|
currentVehicle: undefined,
|
scale: 1,
|
scaleList: [0.5, 1, 1.5, 2, 3, 4, 5],
|
translate: {
|
x: 0,
|
y: 0,
|
},
|
mouseDowning: false,
|
beforeMoveOffset: {
|
x: 0,
|
y: 0,
|
},
|
offset: {
|
x: 0,
|
y: 0,
|
},
|
lastMoveOffset: {
|
x: 0,
|
y: 0,
|
},
|
httpRequest: new HttpRequest(this.apiUrl),
|
apiUrl: webConfig.apiUrl,
|
vehicleListVisible: false, // 显示/隐藏车辆列表
|
orderListVisible: false, // 显示/隐藏订单列表
|
vehicleViewVisible: false, // 显示/隐藏车辆信息
|
orderViewVisible: false, // 显示/隐藏订单信息
|
vehicleViewProps: [
|
{
|
title: "车辆名称",
|
name: "name",
|
},
|
{
|
title: "当前电量",
|
name: "energyLevel",
|
},
|
{
|
title: "车辆当前环境等级",
|
name: "integrationLevel",
|
},
|
{
|
title: "执行订单状态",
|
name: "procState",
|
},
|
{
|
title: "正在处理的订单",
|
name: "transportOrder",
|
},
|
{
|
title: "车辆当前位置",
|
name: "currentPosition",
|
},
|
{
|
title: "车体状态",
|
name: "state",
|
},
|
{
|
title: "错误码",
|
name: "error_code",
|
optional: true,
|
},
|
{
|
title: "错误信息",
|
name: "msg",
|
optional: true,
|
},
|
],
|
orderList: [],
|
currentOrder: undefined,
|
orderViewProps: [
|
{
|
title: "订单编号",
|
name: "name",
|
},
|
{
|
title: "订单类型",
|
name: "type",
|
},
|
{
|
title: "订单状态",
|
name: "state",
|
},
|
{
|
title: "指定车辆",
|
name: "processingVechile",
|
name1: "intendeVechile",
|
optional: true,
|
},
|
{
|
title: "订单目的地",
|
name: "destinations",
|
type: "array",
|
},
|
{
|
title: "错误码",
|
name: "error_code",
|
optional: true,
|
},
|
{
|
title: "错误信息",
|
name: "msg",
|
optional: true,
|
},
|
],
|
integrationLevelVisible: false,
|
vehicleStateVisible: false,
|
showLocation: false, // svg底图上显示站点标记
|
showLocationID: false, // svg底图上显示站点标记的ID
|
points: [], // 站点列表
|
locations: [], // 位置列表
|
};
|
},
|
methods: {
|
loadSvg() {
|
this.svgBg = `remote/${this.svgName}.png`;
|
fetch(`remote/${this.svgName}.svg`)
|
.then((res) => res.text())
|
.then((res) => {
|
this.svgHtml = res;
|
// svg起点坐标和宽高
|
var viewBox = new DOMParser()
|
.parseFromString(res, "text/xml")
|
.querySelector("svg")
|
.getAttribute("viewBox")
|
.split(" ");
|
this.viewBox.x = viewBox[0];
|
this.viewBox.y = viewBox[1];
|
this.viewBox.w = viewBox[2];
|
this.viewBox.h = viewBox[3];
|
});
|
fetch(`remote/${this.svgName}.dat`)
|
.then((res) => res.text())
|
.then((res) => {
|
var list = [];
|
var datas = res
|
.split("\r\r\n")
|
.filter((d) => d != "")
|
.map((d) => d.substr(0, d.length - 1));
|
list.push(...datas[0].split(",").map((d) => parseFloat(d.trim())));
|
list.push(...datas[1].split(",").map((d) => parseFloat(d.trim())));
|
[
|
this.svgCoord.a,
|
this.svgCoord.b,
|
this.svgCoord.c,
|
this.svgCoord.d,
|
this.svgCoord.e,
|
this.svgCoord.f,
|
] = list;
|
});
|
this.rollPosition();
|
},
|
rollPosition() {
|
if (!this.running) {
|
// 位置更新周期
|
setTimeout(() => {
|
this.rollPosition();
|
}, this.interval * 1000);
|
return;
|
}
|
this.httpRequest
|
.get("vehicles")
|
.then((res) => {
|
// 根据接口返回的数据,删除车辆
|
this.vehicles.forEach((v) => {
|
var index = this.vehicles.findIndex((_) => _.name == v.name);
|
if (res.every((d) => d.name != v.name))
|
this.vehicles.splice(index, 1);
|
});
|
// 根据接口返回的数据,增加车辆
|
res.forEach((d) => {
|
if (this.vehicles.every((v) => v.name != d.name))
|
this.vehicles.push({
|
name: d.name,
|
x: 0,
|
y: 0,
|
X: 0,
|
Y: 0,
|
t: 0,
|
top: 0,
|
left: 0,
|
d: d,
|
});
|
});
|
|
// 保存当前车辆
|
if (!this.currentVehicle) this.currentVehicle = this.vehicles[0];
|
|
// 更新车辆位置信息
|
this.vehicles.forEach((vehicle) => {
|
var name = vehicle.name;
|
this.httpRequest
|
.get(`vehicles/getprecisePosition/${name}`)
|
.then((res) => {
|
var x = NaN;
|
var y = NaN;
|
if (res) {
|
x = parseFloat(res.x);
|
y = parseFloat(res.y);
|
}
|
var X = Math.round(
|
this.svgCoord.a * x + this.svgCoord.b * y + this.svgCoord.c
|
);
|
var Y = Math.round(
|
this.svgCoord.d * x + this.svgCoord.e * y + this.svgCoord.f
|
);
|
var left = X * this.scale + this.offset.x * this.scale;
|
var top = Y * this.scale + this.offset.y * this.scale;
|
vehicle.x = x;
|
vehicle.y = y;
|
vehicle.X = X;
|
vehicle.Y = Y;
|
vehicle.top = top;
|
vehicle.left = left;
|
vehicle.t =
|
new Date().getHours() +
|
":" +
|
new Date().getMinutes() +
|
":" +
|
("0" + new Date().getSeconds()).substr(0, 2);
|
});
|
});
|
this.failTimes = 0;
|
})
|
.catch((err) => {
|
if (!this.failTimes) this.failTimes = 0;
|
this.failTimes++;
|
if (this.failTimes > 3) {
|
this.$Message.error({ content: err, duration: 10 });
|
this.failTimes = 0;
|
}
|
})
|
.finally(() => {
|
// 位置更新周期
|
setTimeout(() => {
|
this.rollPosition();
|
}, this.interval * 1000);
|
});
|
},
|
resize(rect) {
|
this.offset = {
|
x: rect.left,
|
y: rect.top,
|
};
|
// this.lastMoveOffset.x = this.offset.x;
|
// this.lastMoveOffset.y = this.offset.y;
|
this.vehicles.forEach((v) => {
|
v.left = v.X * this.scale + this.offset.x * this.scale;
|
v.top = v.Y * this.scale + this.offset.y * this.scale;
|
});
|
},
|
scaleUp() {
|
this.scale += 0.5;
|
},
|
scaleDown() {
|
this.scale -= 0.5;
|
},
|
setScale(scale) {
|
this.scale = scale;
|
},
|
onShowLocationChange(value) {
|
if (value) {
|
this.msg = this.$Message.loading({
|
content: "Loading...",
|
duration: 0,
|
});
|
this.loadLocation();
|
}
|
},
|
loadLocation() {
|
fetch(`remote/${this.svgName}.xml`)
|
.then((res) => res.text())
|
.then((res) => {
|
let xml = new DOMParser().parseFromString(res, "text/xml");
|
window.xml = xml;
|
var points = [].map.call(xml.querySelectorAll("point"), (p) => {
|
var x = p.getAttribute("xPosition");
|
var y = p.getAttribute("yPosition");
|
var X = this.svgCoord.a * x + this.svgCoord.b * y + this.svgCoord.c;
|
var Y = this.svgCoord.d * x + this.svgCoord.e * y + this.svgCoord.f;
|
return {
|
name: p.getAttribute("name"),
|
x: X,
|
y: Y,
|
};
|
});
|
var locations = [].map.call(xml.querySelectorAll("location"), (p) => {
|
var x = p.getAttribute("xPosition");
|
var y = p.getAttribute("yPosition");
|
var X = this.svgCoord.a * x + this.svgCoord.b * y + this.svgCoord.c;
|
var Y = this.svgCoord.d * x + this.svgCoord.e * y + this.svgCoord.f;
|
return {
|
name: p.getAttribute("name"),
|
x: X,
|
y: Y,
|
};
|
});
|
this.points = points;
|
this.locations = locations;
|
})
|
.finally(() => {
|
this.msg();
|
});
|
},
|
alignView() {
|
var w = document.querySelector("#svg").clientWidth;
|
var h = document.querySelector("#svg").clientHeight;
|
this.offset = {
|
x: (w / this.scale - this.viewBox.w) / 2,
|
y: (h / this.scale - this.viewBox.h) / 2,
|
};
|
this.vehicles.forEach((v) => {
|
v.left = v.X * this.scale + this.offset.x * this.scale;
|
v.top = v.Y * this.scale + this.offset.y * this.scale;
|
});
|
this.$refs.vdr.left = this.offset.x;
|
this.$refs.vdr.top = this.offset.y;
|
},
|
},
|
mounted() {
|
this.httpRequest = new HttpRequest(this.apiUrl);
|
this.loadSvg();
|
},
|
};
|
</script>
|
|
<style lang="less" scoped>
|
@import "../../less/monitor.less";
|
.monitor-svg {
|
width: calc(100% - 80px);
|
height: 100%;
|
position: absolute;
|
top: 0;
|
left: calc(@monitor-menu-width + @monitor-menu-margin-right);
|
border-radius: 5px;
|
overflow: hidden;
|
#bg {
|
width: 100%;
|
height: 100%;
|
background-image: url(../../assets/grid-trans.png);
|
position: absolute;
|
top: 0;
|
left: 0;
|
}
|
#svg {
|
position: absolute;
|
width: 100%;
|
height: 100%;
|
transform-origin: top left;
|
}
|
.svg-bg,
|
.svg {
|
position: absolute;
|
top: 0;
|
left: 0;
|
}
|
.vehicle-wrap {
|
position: absolute;
|
top: 0;
|
left: 0;
|
}
|
.vehicle {
|
position: relative;
|
width: 3px;
|
height: 3px;
|
border-radius: 50%;
|
background: #000;
|
transform: translate(-50%, -50%);
|
}
|
.vehicle.active {
|
background: #2d80fd;
|
}
|
.vehicle-name {
|
position: absolute;
|
top: calc(-50% - 2px);
|
left: calc(100% + 3px);
|
transform: translate(13px, -50%);
|
background-color: #ddd;
|
padding: 0px 5px;
|
border: 3px solid #ddd;
|
border-radius: 7px;
|
opacity: 0.9;
|
text-align: center;
|
white-space: nowrap;
|
font-size: 10px;
|
transform: scale(0.3);
|
transform-origin: top left;
|
}
|
|
.vehicle-name:before {
|
content: "";
|
position: absolute;
|
left: -13px;
|
top: 50%;
|
transform: translate(0, -50%);
|
border: 9px solid #ddd;
|
border-color: #ddd;
|
border-width: 9px;
|
border-left: 9px solid transparent;
|
border-right: 9px solid transparent;
|
border-top: 0px solid #ddd;
|
border-bottom: 9px solid #ddd;
|
}
|
#action {
|
position: absolute;
|
right: 0;
|
bottom: 39px;
|
background-color: #2d8cf0;
|
color: #fff;
|
padding: 3px;
|
border-radius: 5px;
|
box-shadow: 0 0 11px rgba(0, 0, 0, 0.6);
|
.action {
|
padding: 3px;
|
cursor: pointer;
|
border-radius: 5px;
|
&:hover {
|
background-color: #fff;
|
color: #2d8cf0;
|
}
|
}
|
}
|
#tips {
|
position: absolute;
|
right: 0;
|
bottom: 0;
|
background-color: rgba(0, 0, 0, 0.8);
|
color: #fff;
|
font-size: 12px;
|
padding: 0 11px;
|
box-shadow: 0 0 11px rgba(0, 0, 0, 0.6);
|
.ivu-icon {
|
transform: translate(-2px, -1px);
|
}
|
.status {
|
display: inline-block;
|
padding-right: 11px;
|
border-right: 1px solid #ddd;
|
}
|
.op {
|
display: inline-block;
|
margin-left: 11px;
|
padding-right: 7px;
|
border-right: 1px solid #fff;
|
vertical-align: 0px;
|
a {
|
color: #fff;
|
vertical-align: -2px;
|
}
|
}
|
.scale-indicator {
|
display: inline-block;
|
padding-left: 11px;
|
a {
|
color: #fff;
|
margin-right: 6px;
|
}
|
.ivu-icon {
|
display: inline-block;
|
vertical-align: middle;
|
cursor: pointer;
|
}
|
.slider {
|
display: inline-block;
|
width: 150px;
|
margin: 0 6px;
|
vertical-align: middle;
|
}
|
}
|
}
|
.points-locations,
|
.points,
|
.locations {
|
position: absolute;
|
top: 0;
|
left: 0;
|
}
|
.point,
|
.location {
|
position: absolute;
|
width: 1px;
|
height: 1px;
|
transform: translate(-50%, -50%);
|
border-radius: 100%;
|
.id {
|
position: absolute;
|
top: -1px;
|
left: calc(100% + 1px);
|
transform: scale(0.15);
|
font-size: 10px;
|
transform-origin: top left;
|
white-space: nowrap;
|
}
|
}
|
.point {
|
background-color: #f00;
|
}
|
.location {
|
background-color: #00f;
|
}
|
}
|
</style>
|