1.页面代码
<template>
<div class="map-outbox">
<div id="container"></div>
<div class="map-control">
<Icon v-if="!isPlay" class="play-icon" type="ios-play" @click="isPlay = true; navgControl(playIcon)" />
<Icon v-else class="play-icon" type="ios-pause" @click="isPlay = false; navgControl('pause')" />
<span class="passed-time">{{ passedTime }}</span>
<Slider class="map-slider" v-model="sliderVal" @on-change="sliderChange" :tip-format="hideFormat" :step="0.0001">
</Slider>
<div class="map-times" @mouseenter="isTimesChoose = true" @mouseleave="isTimesChoose = false">
<div class="times-show">倍速 {{ times }}</div>
<div class="choose-box">
<ul v-show="isTimesChoose">
<li v-for="item in speedList" :key="item" :class="{ active: times === item }" @click="changeSpeed(item)">倍速
{{ item }}</li>
</ul>
</div>
</div>
<span class="passed-time">{{ totalTime }}</span>
</div>
</div>
</template>
<script setup>
import car from "/static/car.png";
import { lazyAMapApiLoaderInstance } from '@vuemap/vue-amap';
import { onMounted, onUnmounted, ref, watch } from 'vue'
let map = ref(null)
let isOnSlider = ref(false)
let playIcon = ref('resume');
let isTimesChoose = ref(false)
let passedTime = ref('00:00:00')
let totalTime = ref('00:00:00')
let isPlay = ref(false)
let sliderVal = ref(0)
let speed = ref(100)
let times = ref(1)
let navgtr = ref(null)
let speedList = ref([8, 4, 2, 1])
let pathList = ref([])
let trackList = ref([])
let pathSimplifierIns = ref(null)
let linePathData = ref(null);
let marksData = ref(null);
let emits = defineEmits(['handleqktraV2IdId'])
onMounted(() => {
let param = {
resizeEnable: true,
zoom: 15,
center: [116.47984, 39.998524],
};
lazyAMapApiLoaderInstance.then(() => {
map.value = new AMap.Map("container", param);
uni.request({
url: 'http://localhost:8000/admin/dashboard/getTrackData',
data: {
server: 1
},
header: {
'batoken': '6f86fa8a-9dec-433a-84ee-eac718b99243'
},
success: (res) => {
console.log('@1', res.data);
linePathData.value = res.data.data.list
marksData.value = res.data.data.point
console.log('@2', linePathData.value);
init();
}
});
});
})
const init = () => {
let iconList = [];
marksData.value.forEach(item => {
addMarker(item);
});
let list = linePathData.value;
let len = list.length;
let startPoint = list[0];
let endPoint = list[len - 1];
pathList.value.splice(0, pathList.value.length);
list.forEach(item => {
pathList.value.push([item.longitude, item.latitude]);
item.stampTime = new Date(item.time).getTime();
});
list.forEach((item, i) => {
let next = list[i + 1];
if (next) {
item.intervalTime = (next.stampTime - item.stampTime) / 1000;
item.nextDistance = distanceFun(
[item.longitude, item.latitude],
[next.longitude, next.latitude]
);
item.nextSpeed =
item.nextDistance / 1000 / (item.intervalTime / 60 / 60);
}
});
totalTime.value = getTime(
(endPoint.stampTime - startPoint.stampTime) / 1000
);
trackList.value = list;
setPath();
drag();
}
const setPath = () => {
AMapUI.load(["ui/misc/PathSimplifier", "lib/$"], function (
PathSimplifier
) {
if (!PathSimplifier.supportCanvas) {
console.log("当前环境不支持 Canvas!");
return;
}
let len = trackList.value.length;
let startPoint = trackList.value[0];
let endPoint = trackList.value[len - 1];
function onload() {
pathSimplifierIns.value.renderLater();
}
function onerror() {
infoContent;
console.log("图片加载失败!");
}
pathSimplifierIns.value = new PathSimplifier({
zIndex: 100,
map: map.value,
getPath: function (pathData) {
return pathData.path;
},
getHoverTitle: function (pathData) {
return pathData.index;
},
autoSetFitView: true,
renderOptions: {
pathNavigatorStyle: {
initRotateDegree: 0,
width: 20,
height: 32,
autoRotate: true,
lineJoin: "round",
content: PathSimplifier.Render.Canvas.getImageContent(
car,
onload,
onerror
),
fillStyle: null,
strokeStyle: null,
lineWidth: 1,
pathLinePassedStyle: {
lineWidth: 6,
strokeStyle: "#2cdf4d"
}
},
pathLineStyle: {
lineWidth: 6,
strokeStyle: "#2e9c08"
},
pathLineHoverStyle: {
lineWidth: 0,
borderWidth: 0
},
pathLineSelectedStyle: {
lineWidth: 6,
borderWidth: 0,
strokeStyle: "#2e9c08"
},
pathTolerance: 0,
keyPointTolerance: -1,
renderAllPointsIfNumberBelow: 0
}
});
pathSimplifierIns.value.setData([
{
name: "轨迹",
path: pathList.value
}
]);
pathSimplifierIns.value.setFitView(-1);
let startSpeed = speedFun(
pathList.value[0],
pathList.value[1],
startPoint.intervalTime
);
startSpeed === 0 && (startSpeed = 0.1);
navgtr.value = pathSimplifierIns.value.createPathNavigator(0, {
loop: false,
speed: startSpeed * times.value
});
let infoContent = `<p class="info-window">时间:<span>${startPoint.time} `;
let infoWindow = new AMap.InfoWindow({
anchor: "bottom-center",
content: infoContent
});
infoWindow.open(map.value, pathList.value[0]);
navgtr.value.on("move", function () {
playIcon.value = "resume";
let idx = this.getCursor().idx;
let tail = this.getCursor().tail;
let totalIdx = idx + tail;
let point = trackList.value[idx];
if (idx < len - 1) {
point.nextSpeed === 0 && (point.nextSpeed = 0.1);
navgtr.value.setSpeed(point.nextSpeed * times.value);
}
point &&
point.time &&
infoWindow.setContent(
`<p class="info-window">时间:<span>${point.time}`
);
infoWindow.open(map.value, navgtr.value.getPosition());
!isOnSlider.value && (sliderVal.value = (totalIdx / len) * 100);
let sTime = parseInt(
(((endPoint.stampTime - startPoint.stampTime) / 1000) *
sliderVal.value) /
100
);
passedTime.value = getTime(sTime);
if (navgtr.value.isCursorAtPathEnd()) {
playIcon.value = "start";
isPlay.value = false;
sliderVal.value = 100;
passedTime.value = totalTime.value;
}
});
navgtr.value.on("start resume", function () {
navgtr.value._startTime = Date.now();
navgtr.value._startDist = this.getMovedDistance();
});
navgtr.value.on("stop pause", function () {
navgtr.value._movedTime = Date.now() - navgtr.value._startTime;
navgtr.value._movedDist =
this.getMovedDistance() - navgtr.value._startDist;
});
});
}
const navgControl = (action) => {
if (action === "start") {
sliderVal.value = 0;
passedTime.value = "00:00:00";
setTimeout(() => {
navgtr.value[action]();
}, 300);
} else {
navgtr.value[action]();
}
}
const getTime = (sTime) => {
let ss;
let mm = "00";
let hh = "00";
if (sTime > 60) {
let s = sTime % 60;
ss = s > 9 ? s : "0" + s;
let mTime = parseInt(sTime / 60);
if (mTime > 60) {
let m = mTime % 60;
mm = m > 9 ? m : "0" + m;
hh = parseInt(mTime / 60);
} else {
mm = mTime > 9 ? mTime : "0" + mTime;
}
} else {
ss = sTime > 9 ? sTime : "0" + sTime;
}
return hh + ":" + mm + ":" + ss;
}
const hideFormat = () => {
return null;
}
const distanceFun = (point1, point2) => {
let p1 = new AMap.LngLat(point1[0], point1[1]);
let p2 = new AMap.LngLat(point2[0], point2[1]);
let distance = Math.round(p1.distance(p2));
return distance;
}
const speedFun = (point1, point2, time) => {
let distance = distanceFun(point1, point2);
if (distance === 0) {
return 0;
} else {
let speed = distance / 1000 / (time / 60 / 60);
return speed;
}
}
const drag = (isRemove) => {
let el = document.getElementsByClassName("ivu-slider-button-wrap")[0];
let el2 = document.getElementsByClassName("ivu-slider-wrap")[0];
if (isRemove) {
el && el.removeEventListener("mousedown", openSlider.value, false);
document.removeEventListener("mouseup", closeSlider.value, false);
el2 && el2.removeEventListener("click", sliderChange.value, false);
return false;
}
el2.addEventListener("click", sliderChange.value, false);
el.addEventListener("mousedown", openSlider.value, false);
document.addEventListener("mouseup", closeSlider.value, false);
}
const openSlider = () => {
isOnSlider.value = true;
}
const closeSlider = () => {
isOnSlider.value = false;
}
const changeSpeed = (item) => {
isTimesChoose.value = false;
times.value = item;
}
const sliderChange = (val) => {
let newVal = typeof val === "number" ? val : sliderVal.value;
let num = parseInt((newVal / 100) * pathList.value.length);
let decimal =
String((newVal / 100) * pathList.value.length).split(".")[1] || 0;
navgtr.value.moveToPoint(num, Number("0." + decimal));
pathSimplifierIns.value.renderLater();
}
const addMarker = (item) => {
let marker = new AMap.Marker({
icon:
"//a.amap.com/jsapi_demos/static/demo-center/icons/poi-marker-default.png",
position: [item.longitude, item.latitude],
offset: new AMap.Pixel(-13, -30)
});
marker.setMap(map.value);
}
const cancelsubmitfotm = () => {
if (isPlay.value) {
isPlay.value = false;
navgControl("pause");
}
navgtr.value = null;
drag(true);
emits("handleqktraV2IdId", this.traV2Id)
dialogFormVisible.value = false;
}
onUnmounted(() => {
navgtr.value = ref(null);
drag(true);
})
watch(() => sliderVal.value,
(val) => {
if (!isOnSlider.value) {
return false;
}
sliderChange(val);
},
{
deep: true,
})
</script>
<style>
.map-outbox {
width: 100%;
padding: 40rpx;
border: 1px solid #ddd;
margin: auto;
}
#container {
width: 100%;
height: 50vh;
}
.map-control {
position: fixed;
bottom: 0;
width: 100%;
left: 0;
z-index: 200;
height: 160rpx;
line-height: 160rpx;
color: #fff;
background-image: linear-gradient(to top, rgba(0, 0, 0, 0.7), transparent);
padding: 0 40rpx;
}
.play-icon {
font-size: 36rpx;
}
.map-slider {
display: inline-block;
width: 45%;
margin-left: 15rpx;
position: relative;
top: 28rpx;
}
.passed-time {
position: relative;
top: 8rpx;
display: inline-block;
margin-left: 15rpx;
font-size: 28rpx;
}
.map-times {
display: inline-block;
position: relative;
margin-left: 15rpx;
}
.map-times .times-show {
padding: 0 10rpx;
line-height: 48rpx;
font-size: 26rpx;
border: 1px solid #fff;
border-radius: 4rpx;
cursor: default;
}
.map-times .choose-box {
position: absolute;
top: -135rpx;
left: -6rpx;
height: 162rpx;
transition: all 0.5s linear;
}
.map-times ul {
background: rgba(0, 0, 0, 0.7);
padding: 10rpx;
width: 150rpx;
text-align: center;
border-radius: 3rpx;
}
.map-times li {
height: 36rpx;
line-height: 36rpx;
cursor: pointer;
}
.map-times li.active {
color: #ff8533;
}
.map-times li:hover {
font-size: 13px;
}
</style>
2.前端api封装 (不需要, 直接使用uni.request)
3.后端控制器
<?php
namespace app\admin\controller;
use app\admin\model\Fence;
use app\common\controller\Backend;
class Dashboard extends Backend
{
public function initialize(): void
{
parent::initialize();
}
public function getTrackData()
{
$model = new \app\admin\model\GeoData();
$machineNo = '14499999999';
$list = $model->getTrackData($machineNo);
$count = count($list);
$point = [
$list[0],
$list[$count - 1]
];
$center = $model->getCenter($list[0], $list[$count - 1]);
$this->success('', [
'count' => $count,
'point' => $point,
'list' => $list,
'center' => $center
]);
}
}
4.模型
<?php
namespace app\admin\model;
use think\Model;
class GeoData extends Model
{
protected $name = 'geo_data';
protected $autoWriteTimestamp = false;
public function getLongitudeAttr($value): float
{
return (float)$value;
}
public function getLatitudeAttr($value): float
{
return (float)$value;
}
public function getSpeedAttr($value): float
{
return (float)$value;
}
public function getTrackData($machineNo)
{
$res = $this->field('longitude,latitude,speed,device_time time')
->where("machine_no", $machineNo)
->select();
return $res;
}
public function getCenter($pointA, $pointB) {
$latA = $pointA['latitude'];
$lngA = $pointA['longitude'];
$latB = $pointB['latitude'];
$lngB = $pointB['longitude'];
$centerLat = ($latA + $latB) / 2;
$centerLng = ($lngA + $lngB) / 2;
return [$centerLng, $centerLat];
}
}