目录
- 背景
- 轨迹回放 - 匀速
- 效果图
- `TrackPlaybackDialog.vue` 代码
- `TMapNew.vue` 代码
- 轨迹回放 - 非匀速
- 效果图
- `TrackPlaybackDialog.vue` 代码
- `TMapNew.vue` 代码
背景
腾讯地图轨迹回放是匀速回放的,但是客户要求根据现实时间,什么时间点在某个点位
- 【腾讯地图轨迹回放是匀速回放】:比如一段轨迹中不同时间有三四个点是在同一位置(类似警员在某一点位N秒没动,不同时间取到的点位是一个),轨迹回放的时候,停留的这个点直接经过了,而不是与实际一致轨迹停留N秒,即 轨迹回放时停留点的时间 ≠ 警员实际停留时间,所有的轨迹回放是匀速的。
- 【客户要求根据现实时间,什么时间点在某个点位】:每两个点之间轨迹回放的时间与实际时间一致,也就是警员在某一点位N秒没动,轨迹回放的时候也要在该点停留N秒之后再继续,即 轨迹回放时停留点的时间 = 警员实际停留时间,所有的轨迹回放不是匀速的。
或者 警员有时开车有时走路,时速是不一样的,要求轨迹回放与实际时速一致,而不是一直匀速
轨迹回放 - 匀速
将所有点位一次回放完成 ----匀速
代码中只需看标注 【主要代码】 的代码,其余代码不重要
效果图
35秒匀速回放所有轨迹
TrackPlaybackDialog.vue
代码
<!-- 轨迹回放 -->
<template>
<el-dialog
v-loading="loading"
:title="title + '轨迹回放'"
width="90%"
top="5vh"
:close-on-click-modal="false"
destroy-on-close
append-to-body
:visible.sync="dialogVisible"
@close="handleClose"
>
<template #title>
<span class="el-dialog__title">{{ title + '轨迹回放' }}</span>
<el-button class="drawer_btn" type="primary" size="mini" @click="drawerHandler">
{{ isShowDrawer ? '隐藏' : '显示' }}部门辖区/执勤区域
</el-button>
</template>
<TMapNew
v-if="dialogVisible"
ref="TMapRef"
map-name="basicInspection"
:id-name="'TrackPlayback' + idNameTail"
@moveEnd="moveEnd"
@updateNowLc="updateNowLc"
/>
<div class="operation">
回放时间:
<!-- <el-time-select
v-model="startTime"
placeholder="起始时间"
size="mini"
:picker-options="startTimeOptions"
/>
至
<el-time-select
v-model="endTime"
class="end_time"
placeholder="结束时间"
size="mini"
:picker-options="endTimeOptions"
/> -->
<el-date-picker
v-model="time"
type="datetimerange"
range-separator="至"
start-placeholder="开始日期"
end-placeholder="结束日期"
size="mini"
format="yyyy-MM-dd HH:mm"
value-format="yyyy-MM-dd HH:mm:ss"
:clearable="false"
/>
<el-button size="mini" @click="moveHandler">{{ isMove ? '停止回放' : '开始回放' }}</el-button>
<el-select v-model="speed" class="speed" placeholder="请选择" size="mini">
<el-option
v-for="item in speedOptions"
:key="item.speed"
:label="item.speed + '倍速'"
:value="item.speed"
/>
</el-select>
<div class="time_line">
<div class="bg">
<div v-for="item in timeRangeList" :key="item" class="item">
<span>{{ item }}</span>
</div>
<div
class="range"
:style="{
'--timeRangeWidth': timeRangeWidth * 100 + '%',
'--timeRangeLeft': timeRangeLeft * 100 + '%'
}"
/>
</div>
</div>
<el-select
v-if="trackType === '1'"
v-model="sblx"
class="sblx"
placeholder="请选择"
size="mini"
>
<el-option
v-for="(item, index) in $common.getDic('mon_jwdc_sblx')"
:key="'sblx_' + index"
:label="item.label"
:value="item.value"
/>
</el-select>
</div>
<div v-show="!!trackType" class="info">
<div class="info_item">
<div class="info_item_label">{{ trackType === '1' ? '民警' : '号牌号码' }}:</div>
<div class="info_item_value mj">
{{ infoData[trackType === '1' ? 'xm' : 'hphm'] | noDataFilter('暂无') }}
</div>
</div>
<div class="info_item">
<div class="info_item_label">部门:</div>
<div class="info_item_value bm">
{{ (infoData.dept_name || infoData.dwjc || infoData.dwmc) | noDataFilter('暂无') }}
</div>
</div>
<div class="info_item">
<div class="info_item_label">里程:</div>
<div class="info_item_value gw">
{{ lc | noDataFilter('暂无') }}{{ lc === 0 || lc ? 'km' : '' }}
</div>
</div>
<div class="info_item">
<div class="info_item_label">当前行驶距离:</div>
<div class="info_item_value gw">{{ curPosition.distanceM | noDataFilter('-') }}米</div>
</div>
<div class="info_item">
<div class="info_item_label">当前坐标:</div>
<div class="info_item_value gw">
{{ curPosition.positionText || `[${curPosition.lat || '-'},${curPosition.lng || '-'}]` }}
</div>
</div>
<div class="info_item">
<div class="info_item_label">当前时间:</div>
<div class="info_item_value gw">
{{ curPosition.gpsTime | noDataFilter('暂无') }}
</div>
</div>
</div>
</el-dialog>
</template>
<script>
import {
queryPoliceTrackDetail,
queryCarTrackDetail,
queryDutyPointList,
queryPoliceTrackList,
queryCarTrackList,
queryAddressByLonLat
} from '@/api/taizhou/service-inspector.js'
import dayjs from 'dayjs'
import { deviceType_MOTO, setDeptArea } from '../index.js'
export default {
name: 'TrackPlaybackDialog',
components: {},
props: {
// currentGlbm: {
// type: Object,
// required: true
// }
idNameTail: {
type: String,
default: ''
}
},
data() {
return {
loading: false,
title: '',
trackType: '',
dialogVisible: false,
infoData: {},
time: [dayjs().format('YYYY-MM-DD') + ' 00:00:00', dayjs().format('YYYY-MM-DD HH:mm:ss')],
// startTime: '',
// endTime: '',
positions: [],
lc: 0,
isMove: false,
speed: 1,
isMoveStartFirst: true,
timeRangeList: [...Array(24).keys()],
timeRangeLeft: '0',
timeRangeWidth: '0',
sblx: '1',
speedOptions: [
{ speed: 1 },
{ speed: 2 },
{ speed: 3 },
{ speed: 4 },
{ speed: 8 },
{ speed: 16 }
],
curPosition: {},
isShowDrawer: true,
dutyAreaInfo: {},
deptAreaInfo: {}
}
},
computed: {
ax() {
const obj = {
1: queryPoliceTrackDetail,
2: queryCarTrackDetail,
4: queryCarTrackDetail
}
return obj[this.trackType]
}
// startTimeOptions() {
// return {
// start: '00:00',
// step: '00:30',
// end: '23:30',
// maxTime: this.endTime
// }
// },
// endTimeOptions() {
// return {
// start: '00:00',
// step: '00:30',
// end: '23:30',
// minTime: this.startTime
// }
// }
},
watch: {
time(val, oldVal) {
if (!this.dialogVisible) return
const [start, end] = val
if (+new Date(end) - +new Date(start) > 24 * 60 * 60 * 1000) {
this.$message.error('回放时间范围必须在一天内,请重新选择')
this.time = oldVal
} else {
this.getTimeRange()
this.queryTrackDetail()
}
},
sblx() {
if (!this.dialogVisible || this.trackType !== '1') return
this.queryTrackDetail()
},
speed() {
if (!this.dialogVisible) return
this.isMoveStartFirst = true
}
},
created() {},
mounted() {},
methods: {
getTimeRange() {
const [start, end] = this.time
const startH = start.slice(11, 13) * 1
const startM = start.slice(14, 16) * 1
const endH = end.slice(11, 13) * 1
const endM = end.slice(14, 16) * 1
const isSameDay = start.slice(8, 10) === end.slice(8, 10)
const allRange = 24 * 60
if (!isSameDay) {
this.timeRangeList = [...Array(24).keys()].map((item) => (item + startH) % 24)
const timeRange = allRange - (startH * 60 + startM) + endH * 60 + endM
this.timeRangeWidth = timeRange / allRange
this.timeRangeLeft = 0
} else {
const timeRange = endH * 60 + endM - (startH * 60 + startM)
this.timeRangeWidth = timeRange / allRange
this.timeRangeLeft = (startH * 60 + startM) / allRange
}
},
open(data, { title = '', type = '' }) {
this.title = title
this.trackType = type
console.log('data--轨迹回放弹框--', data)
this.dialogVisible = true
this.infoData = data.properties || data || {}
this.$nextTick(() => {
// const ssbm = '331002005300' // 测试数据
const ssbm = this.infoData.dept_code || this.infoData.dwdm || this.infoData.ssbm
setDeptArea(
this,
ssbm,
{ isSetCenter: false, clearArea: 'drawerRedPolygon' },
(areaData) => {
this.deptAreaData = areaData
}
) // 部门辖区
/** ** 执勤区域展示 - start ****/
const { dutyAreaInfo } = this.infoData
console.log('dutyAreaInfo----open打印', dutyAreaInfo)
if (dutyAreaInfo) {
const { center, drawerType, ...areaInfo } = dutyAreaInfo
this.dutyAreaInfo[drawerType] = areaInfo // 保存执勤区域数据 - 便于隐藏区域后再展示区域
// 回溯 - 地图中展示的警员执勤区域带过来,轨迹回放这里也展示这个执勤区域
const { drawerAreaHandler } = this.$refs.TMapRef
setTimeout(() => {
drawerAreaHandler(drawerType, areaInfo, drawerType)
}, 1000)
} else {
// 否则,调接口取区域数据,然后回显区域
this.queryDutyPointList() // 区域
}
/** ** 执勤区域展示 - end ****/
})
this.getTimeRange()
setTimeout(() => {
if (this.infoData.timeRange && this.infoData.timeRange[0] && this.infoData.timeRange[1]) {
;[this.time[0], this.time[1]] = this.infoData.timeRange
this.time.push({})
this.time.pop()
} else {
this.trackQueryHandler()
}
}, 200)
},
trackQueryHandler() {
const axObj = {
1: queryPoliceTrackList,
2: queryCarTrackList
}
axObj[this.trackType]({
page: 1,
rows: 10,
queryStartTime: this.time[0],
queryEndTime: this.time[1],
jh: this.infoData.jh
}).then((res) => {
this.$common.CheckCode(res, null, () => {
const cur = (res.data.length && res.data[0]) || {}
if (Object.keys(cur).length === 0) {
this.queryTrackDetail()
} else {
this.time = [cur.kssj || '', cur.jssj || '']
}
})
})
},
drawerHandler() {
this.isShowDrawer = !this.isShowDrawer
const { drawerAreaHandler, clearAreaHandler } = this.$refs.TMapRef
if (this.isShowDrawer) {
Object.keys(this.dutyAreaInfo).forEach((key) => {
drawerAreaHandler(key, this.dutyAreaInfo[key], key)
})
drawerAreaHandler('drawerRedPolygon', this.deptAreaData, 'drawerRedPolygon')
} else {
clearAreaHandler('all')
}
},
queryTrackDetail() {
this.moveEnd()
let time = {}
if (this.time.length) {
time = {
queryStartTime: this.time[0] || '',
queryEndTime: this.time[1] || ''
}
}
const { jh, deviceIndexCode, recentUserId } = this.infoData
const paramsObj = {
1: {
sblx: this.sblx,
jh,
userId: recentUserId
},
2: {
deviceIndexCode
},
4: {
deviceIndexCode
}
}
this.loading = true
this.ax({
...time,
...paramsObj[this.trackType]
})
.then((res) => {
this.$common.CheckCode(
res,
null,
() => {
res = {
code: 200,
msg: null,
data: {
gpsList: [...Array(10).keys()].map((index) => {
const point = [
'109.62616781132476,19.07651100671366',
'109.52052961372806,19.000955875798667',
'109.52779570317193,18.889409778960484',
'109.43389509635472,18.804246369159575',
// '109.55741914575538,18.750798850652597',
// '109.4389254607205,18.687804325385454',
'109.4389254607205,18.687804325385454',
'109.4389254607205,18.687804325385454',
'109.5864836229498,18.66556534760466',
'109.72845239144965,18.73121481942799',
'109.74745608740011,18.87354409698365',
'109.8709801082789,18.91584923707182',
'109.80502610332212,19.07598276944228'
]
const times = [
'2025-02-12 08:00:00',
'2025-02-12 08:00:01',
'2025-02-12 08:00:05',
'2025-02-12 08:00:07',
'2025-02-12 08:00:10',
'2025-02-12 08:00:15',
'2025-02-12 08:00:20',
'2025-02-12 08:00:22',
'2025-02-12 08:00:30',
'2025-02-12 08:00:35'
]
return {
deviceType: '执法记录仪',
deviceName: null,
deptName: '事故中队',
deptCode: '331002000300',
recentUserId: '1000704',
deviceIndexCode: 'K380772',
// lng: '120.217989',
lng: point[index].split(',')[0],
recentUserName: '徐捷',
// gpsTime: this.$dayjs(
// +new Date('2025-02-12 08:00:00') + index * 3 * 1000
// ).format('YYYY-MM-DD HH:mm:ss'),
gpsTime: times[index],
// lat: '30.212518',
lat: point[index].split(',')[1],
// location: '120.217989,30.212518',
location: point[index],
distanceM: index * 10
}
}),
distanceKm: 108.79
},
timestamp: 1748573847277
}
this.lc = res?.data?.distanceKm || 0
this.positions = (res?.data?.gpsList || []).map((item) => {
const nameFieldObj = {
1: 'xm',
2: 'hphm',
3: 'deviceName',
4: 'hphm'
}
const nameObj = {
1: item.recentUserName || '-',
2: this.infoData.hphm || '-',
4: this.infoData.hphm || '-'
}
return {
...item,
jd: item.lng,
wd: item.lat,
// type: item.gjlx,
type:
this.trackType === '2' && this.infoData.deviceType === deviceType_MOTO
? '4'
: this.trackType,
// name: item[nameFieldObj[item.gjlx]]
name: nameObj[this.trackType]
}
})
const { basicInspectionTrack } = this.$refs.TMapRef
basicInspectionTrack(this.positions)
this.loading = false
},
() => {
this.loading = false
}
)
})
.catch(() => {
this.loading = false
})
},
queryDutyPointList() {
queryDutyPointList({
fzr_jh: this.infoData.jh,
queryStartTime: this.time[0],
queryEndTime: this.time[1]
}).then((res) => {
this.$common.CheckCode(res, null, () => {
const typeObj = {
1: 'Circle',
2: 'Line',
3: 'Polygon'
}
const listObj = {
lineList: [],
circleList: [],
polygonList: []
}
res.data.forEach((item) => {
const { zb, post_type } = item
const areaList = zb
? JSON.parse(zb).map((item) => new TMap.LatLng(item.y * 1, item.x * 1))
: []
const info = {
areaList,
center: areaList[0],
circleCenter: areaList[0],
...item
}
listObj[typeObj[post_type].toLowerCase() + 'List'].push(info)
})
Object.keys(typeObj).forEach((key) => {
const type = typeObj[key]
const drawerInfo = {
dataList: listObj[type.toLowerCase() + 'List']
}
this.dutyAreaInfo[`drawer${type}`] = drawerInfo
this.$refs.TMapRef.drawerAreaHandler(`drawer${type}`, drawerInfo, `drawer${type}`)
})
})
})
},
moveHandler() {
this.isMove = !this.isMove
const { basicInspectionMove, basicInspectionResumeMove, resetMoveJwdlength } =
this.$refs.TMapRef
if (this.isMove) {
if (this.isMoveStartFirst) {
this.isMoveStartFirst = false
const startTime = this.positions[0].gpsTime
const endTime = this.positions[this.positions.length - 1].gpsTime
const duration = dayjs(endTime).diff(dayjs(startTime), 'seconds') * 1000
resetMoveJwdlength()
basicInspectionMove(
this.positions.map((item) => {
return new TMap.LatLng(Number(item.wd), Number(item.jd))
}),
duration / this.speed
)
} else {
basicInspectionResumeMove()
}
} else {
this.positionTrans()
this.pauseTrack()
}
},
moveEnd() {
this.isMove = false
this.isMoveStartFirst = true
this.pauseTrack()
},
updateNowLc(index) {
this.curPosition = { ...this.positions[index], positionText: '' }
},
pauseTrack() {
this.$refs?.TMapRef?.basicInspectionPauseTrack()
},
positionTrans() {
this.curPosition.lng &&
this.curPosition.lat &&
queryAddressByLonLat({
longitude: this.curPosition.lng,
latitude: this.curPosition.lat
}).then((res) => {
this.$common.CheckCode(res, null, () => {
this.curPosition.positionText = res.data?.address || ''
})
})
},
handleClose() {
this.pauseTrack()
this.dialogVisible = false
this.infoData = {}
this.time[0] = dayjs().format('YYYY-MM-DD') + ' 00:00:00'
this.time[1] = dayjs().format('YYYY-MM-DD HH:mm:ss')
this.positions = []
this.lc = 0
this.isMove = false
this.isMoveStartFirst = true
this.speed = 1
this.timeRangeList = [...Array(24).keys()]
this.sblx = '1'
this.curPosition = {}
this.isShowDrawer = true
this.dutyAreaInfo = {}
this.deptAreaInfo = {}
}
}
}
</script>
<style lang='scss' scoped>
@import '@/styles/dialog-scss.scss';
$rightInfoWidth: 240px;
>>> .el-dialog {
.el-dialog__header {
.drawer_btn {
margin-left: 10px;
}
}
.el-dialog__body {
height: 90vh;
padding: 0 !important;
position: relative;
#TrackPlayback,
[id^='TrackPlayback'] {
width: 100%;
height: 100%;
}
.z-index {
z-index: 1999;
}
.operation {
width: calc(100% - #{$rightInfoWidth} - 30px);
height: 55px;
line-height: 55px;
padding: 0 10px;
position: absolute;
top: 10px;
left: 0;
background-color: #ffffffa3;
text-align: left;
display: flex;
align-items: center;
@extend .z-index;
// .el-date-editor {
// width: 120px;
// &.end_time {
// margin-right: 10px;
// }
// }
.el-date-editor {
width: 290px;
margin-right: 10px;
.el-range-input {
width: calc(50% - 20px);
}
.el-range-separator {
width: 20px;
}
.el-range__close-icon {
width: 0;
}
}
.speed {
margin: 0 10px;
width: 80px;
}
.sblx {
margin-left: 10px;
width: 100px;
}
.time_line {
flex: 1;
.bg {
$size: 7px;
width: 100%;
height: $size;
border-radius: $size;
display: flex;
background-color: #e4e7ed;
position: relative;
.item {
flex: 1;
position: relative;
z-index: 9;
&::before {
content: '';
position: absolute;
top: 0;
left: -#{$size / 2};
width: $size;
height: $size;
background-color: #fff;
border-radius: $size;
}
span {
position: absolute;
left: -10px;
top: 10px;
display: inline-block;
width: 20px;
height: 20px;
line-height: 20px;
text-align: center;
color: #409eff;
}
}
.range {
$range_size: calc(#{$size} * 2);
position: absolute;
// top: -20px;
width: var(--timeRangeWidth);
height: $size;
left: var(--timeRangeLeft);
background-color: #409eff;
&::before,
&::after {
content: '';
position: absolute;
// top: 0;
top: -5px;
width: $range_size;
height: $range_size;
background-color: #fff;
border-radius: $range_size;
border: 1px solid #c7cbd2;
}
&::before {
left: calc((#{$range_size} / 2) * -1);
}
&::after {
right: calc((#{$range_size} / 2) * -1);
}
}
}
}
}
.info {
width: $rightInfoWidth;
position: absolute;
top: 10px;
right: 20px;
padding: 10px;
background-color: #fff;
border-radius: 8px;
text-align: left;
font-size: 16px;
@extend .z-index;
&_item {
display: flex;
margin-bottom: 5px;
color: #333;
&_label {
}
&_value {
flex: 1;
line-height: 22px;
&.bm,
&.gw {
font-size: 12px;
}
}
}
}
}
}
</style>
TMapNew.vue
代码
<!-- 腾讯地图 -->
<template>
<div :id="idName" class="TMap" />
</template>
<script>
import { mapGetters } from 'vuex'
import policeImg from '@/assets/images/TXMap/icon-auxiliary-police.png'
import carImg from '@/assets/images/TXMap/icon-police-car.png'
import motoImg from '@/assets/images/TXMap/icon-motorcycle.png'
import monitorImg from '@/assets/images/TXMap/icon-monitor.png'
import startImg from '@/assets/images/start.png'
import endImg from '@/assets/images/end.png'
import nameBg from '@/assets/images/TXMap/point_name_bg.png'
export default {
name: 'TMapNew',
components: {},
props: {
mapName: {
type: String,
required: true
},
idName: {
type: String,
default: 'TXMapContanier'
}
},
data() {
return {
map: null,
curZoom: 0,
setLabelZoom: 17,
multiMarker: null, // 点位图标
MultiLabel: null, // 点位图标顶部文字描述
infoWindow: null, // 信息窗口
MultiPolyline: null, // 折线 - 运动轨迹
multiPolylineLayer: {}, // 多个 简单折线
multiCircleLayer: {}, // 多个 简单圆
multiPolygonLayer: {}, // 多个 简单多边形
multiRedPolygonLayer: {}, // 多个 简单多边形 -- 部门辖区
multiLabelLayer: {}, // 多个 label
trackQueryMultiMarker: {
jy: null,
jc: null,
jk: null
},
trackQueryMultiLabel: {
jy: null,
jc: null,
jk: null
},
editor: null,
activeType: 'marker',
activeId: '', // 值格式为 6C5895CE-B42D-4E9B-A8FA-81135761CBDD
moveJwdlength: 0
}
},
computed: {
...mapGetters(['sysConfigData'])
},
mounted() {
// this.initMap()
window.onbeforeunload = () => {
localStorage.removeItem('TXMapIsCanLoad')
}
const timer = setInterval(() => {
const TXMapIsCanLoad = localStorage.getItem('TXMapIsCanLoad')
if (TXMapIsCanLoad === 'true') {
this.initMap()
clearInterval(timer)
}
}, 100)
},
beforeDestroy() {
this.map?.destroy()
this.infoWindow?.close()
this.map = null
this.multiMarker?.setMap(null)
this.MultiLabel?.setMap(null)
this.infoWindow?.setMap(null)
this.MultiPolyline?.setMap(null)
this.multiPolylineLayer = {}
this.multiCircleLayer = {}
this.multiPolygonLayer = {}
this.multiRedPolygonLayer = {}
this.multiLabelLayer = {}
this.trackQueryMultiMarker = {
jy: null,
jc: null,
jk: null
}
this.trackQueryMultiLabel = {
jy: null,
jc: null,
jk: null
}
this.editor = null
},
methods: {
// 初始化地图
initMap() {
this[this.mapName + 'Init']()
},
setMapCenter(center) {
this.map.setCenter(center)
},
setMapZoom(level) {
this.map.setZoom(level)
},
// 创建wmts图层
newWMTSLayer() {
const url = this.sysConfigData.mon_map_wmts_url
const { map } = this
if (!url) return
new TMap.WMTSLayer({
url, // 地图服务地址
map, // 展示图层的地图对象
minZoom: 3, // 最小缩放层级,当地图缩放层级小于该值时该图层不显示,默认为3
maxZoom: 20, // 最大缩放层级,当地图缩放层级大于该值时该图层不显示,默认为20
visible: true, // 是否可见,默认为true
zIndex: 1, // 图层绘制顺序
opacity: 0.9, // 图层透明度,默认为1
params: {
// OGC标准的WMTS地图服务的GetTile接口的参数
layers: 'topp:raster_cgcs2000%3Ataizhou2m_cgcs2000', // 请求的图层名称
tileMatrixSet: 'taizhou2m%3A11' // 瓦片矩阵数据集
}
})
},
// 标记点
// point(markerId, styles, pointArr) {
// this[this.mapName + 'Point'](markerId, styles, pointArr)
// },
// 打开弹框
openInfoWindow(position, content) {
this.infoWindow = new TMap.InfoWindow({
map: this.map,
position: new TMap.LatLng(position[0], position[1]),
offset: { x: 0, y: -32 }, // 设置信息窗相对position偏移像素
content: content
})
},
/** ** 台州勤务督察 -页面地图 start ****/
basicInspectionInit() {
this.basicInspectionInitCommon()
},
setPointMapInit() {
this.basicInspectionInitCommon()
this.$emit('setInitPoint')
this.map.on('click', (e) => {
this.$emit('getPoint', e.latLng)
})
},
setPoint({ jd, wd }, isSetCenter = false) {
isSetCenter && this.setMapCenter(new TMap.LatLng(Number(jd), Number(wd)))
this.MultiMarker = new TMap.MultiMarker({
id: 'marker-layer',
map: this.map,
styles: {
marker: new TMap.MarkerStyle({
width: 25,
height: 35,
anchor: { x: 16, y: 32 }
})
},
geometries: [
{
id: 'demo',
styleId: 'marker',
position: new TMap.LatLng(jd * 1, wd * 1),
properties: {
title: 'marker'
}
}
]
})
},
removePoint() {
this.MultiMarker?.setMap(null)
},
basicInspectionInitCommon() {
var location = (this.sysConfigData.map_location || '121.427648,28.661939').split(',')
// console.log(Number(location[1]), Number(location[0]))
const featuresObj = {
gs: null,
nw: []
}
this.curZoom = this.sysConfigData.map_level || 14
this.map = new TMap.Map(this.idName, {
zoom: this.curZoom,
center: new TMap.LatLng(Number(location[1]), Number(location[0])),
baseMap: {
type: 'vector',
// features: null // 本地跑项目用
// // features: [] // 内网用
features: featuresObj[this.sysConfigData.mon_map_yslx] || null
}
})
/** ** 获取地图首次加载完成 start ****/
this.map.off('tilesloaded', tilesLoad)
this.map.on('tilesloaded', tilesLoad)
const that = this
function tilesLoad() {
console.log('地图加载完成')
that.map.off('tilesloaded', tilesLoad)
}
/** ** 获取地图首次加载完成 end ****/
this.newWMTSLayer()
this.map.removeControl(TMap.constants.DEFAULT_CONTROL_ID.ROTATION) // 移除腾讯地图旋转控件
this.map.removeControl(TMap.constants.DEFAULT_CONTROL_ID.ZOOM) // 移除腾讯地图缩放控件
this.map.on('zoom_changed', (params) => {
console.log('params----zoom_changed', params)
this.curZoom = params.zoom
if (this.curZoom > this.setLabelZoom) {
!Object.keys(this.multiLabelLayer).length && this.$emit('setLabel', this.curZoom)
} else {
this.clearMultiLabel()
}
})
if (this.idName === 'trackQueryTXMapContanier') {
let timer = null
this.map.on('center_changed', (params) => {
clearTimeout(timer)
timer = setTimeout(() => {
const center = params.center
console.log('params----center_changed', params)
console.log('center----center_changed', center)
this.$emit('getPointArr', { jd: center.lng, wd: center.lat, isCenterChange: true })
}, 500)
})
}
},
basicInspectionPoint(markerId, pointArr, isSetCenter = false) {
// console.log('pointArr----basicInspectionPoint', pointArr)
this.MultiMarker?.setMap(null)
this.infoWindow?.setMap(null)
if (!pointArr.length) return
// 初始marker
this.MultiMarker = this.basicInspectionCommonPoint(markerId, pointArr, isSetCenter)
this.MultiMarker.on('click', this.basicInspectionCommonClick)
},
basicInspectionPointText(markerId, pointArr) {
// console.log('pointArr----basicInspectionPointText', pointArr)
this.MultiLabel?.setMap(null)
this.infoWindow?.setMap(null)
if (!pointArr.length) return
// 初始marker
this.MultiLabel = this.basicInspectionCommonPointText(pointArr)
this.MultiLabel.on('click', this.basicInspectionCommonClick)
},
trackQueryPoint(markerId, pointArr, pointTypeArr, pointType, isSetCenter = false) {
console.log('pointArr----trackQueryPoint', pointArr)
const gjlxObj = {
1: 'jy',
2: 'jc',
3: 'jk'
}
const gjlx = gjlxObj[pointType]
;['1', '2', '3'].forEach((item) => {
if (!pointTypeArr.includes(item) || pointType === item) {
this.trackQueryMultiMarker[gjlxObj[item]]?.setMap(null)
}
})
// this.MultiMarker?.setMap(null)
this.infoWindow?.setMap(null)
if (!pointArr.length) return
// 初始marker
this.trackQueryMultiMarker[gjlx] = this.basicInspectionCommonPoint(
markerId,
pointArr,
isSetCenter
)
this.trackQueryMultiMarker[gjlx].on('click', this.basicInspectionCommonClick)
},
trackQueryPointText(markerId, pointArr, pointTypeArr, pointType) {
console.log('pointArr----trackQueryPointText', pointArr)
const gjlxObj = {
1: 'jy',
2: 'jc',
3: 'jk'
}
const gjlx = gjlxObj[pointType]
;['1', '2', '3'].forEach((item) => {
if (!pointTypeArr.includes(item) || pointType === item) {
this.trackQueryMultiLabel[gjlxObj[item]]?.setMap(null)
}
})
// this.MultiLabel?.setMap(null)
this.infoWindow?.setMap(null)
if (!pointArr.length) return
// 初始marker
this.trackQueryMultiLabel[gjlx] = this.basicInspectionCommonPointText(pointArr)
this.trackQueryMultiLabel[gjlx].on('click', this.basicInspectionCommonClick)
},
basicInspectionCommonPoint(markerId, pointArr, isSetCenter) {
if (isSetCenter) {
const first = pointArr[0]
this.setMapCenter(new TMap.LatLng(Number(first.wd), Number(first.jd)))
}
return new TMap.MultiMarker({
id: markerId,
map: this.map,
styles: {
police: new TMap.MarkerStyle({
width: 24,
height: 40,
anchor: { x: 0, y: 0 },
src: policeImg
}),
car: new TMap.MarkerStyle({
width: 50,
height: 25,
anchor: { x: 0, y: 0 },
src: carImg
}),
moto: new TMap.MarkerStyle({
width: 50,
height: 25,
anchor: { x: 0, y: 0 },
src: motoImg
}),
monitor: new TMap.MarkerStyle({
width: 40,
height: 30,
anchor: { x: 0, y: 0 },
src: monitorImg
})
},
geometries: pointArr.map((item, index) => {
const styleIdObj = {
1: 'police',
2: 'car',
3: 'monitor',
4: 'moto'
}
return {
id: index,
// styleId: 'police',
styleId: styleIdObj[item.type || '1'],
position: new TMap.LatLng(item.wd, item.jd),
properties: this.basicInspectionCommonProperties(item, index)
}
})
})
},
basicInspectionCommonPointText(pointArr) {
const commonStyle = {
height: 25, // 高度
anchor: { x: 15, y: 26 }, // 锚点位置
src: nameBg, // 标注点图片url或base64地址
color: '#fff', // 标注点文本颜色
size: 14, // 标注点文本文字大小
offset: { x: 0, y: 0 } // 标注点文本文字基于direction方位的偏移属性
}
return new TMap.MultiMarker({
map: this.map,
styles: {
police: new TMap.MarkerStyle({
width: 60, // 宽度
...commonStyle
}),
car: new TMap.MarkerStyle({
width: 100, // 宽度
...commonStyle
}),
moto: new TMap.MarkerStyle({
width: 100, // 宽度
...commonStyle
}),
monitor: new TMap.MarkerStyle({
width: 100, // 宽度
...commonStyle
})
},
geometries: pointArr.map((item, index) => {
const styleIdObj = {
1: 'police',
2: 'car',
3: 'monitor',
4: 'moto'
}
let content = item.name || ''
if (['3'].includes(item.type)) {
content = content.slice(0, 5) + (content.length > 5 ? '...' : '')
}
return {
styleId: styleIdObj[item.type || '1'],
position: new TMap.LatLng(item.wd, item.jd),
content,
properties: this.basicInspectionCommonProperties(item, index)
}
})
})
},
basicInspectionCommonProperties(item, index) {
return {
type: item.type || '1', // 1警员、2警车、3监控
...item
}
},
basicInspectionCommonClick(data) {
console.log('data--basicInspectionCommonClick--', data)
this.infoWindow?.destroy()
this.$emit('pointClick', data)
},
basicInspectionTrack(trackArr) {
this.MultiPolyline?.setMap(null)
this.multiMarker?.setMap(null)
if (!trackArr.length) return
const trackStart = trackArr[0] || {}
console.log('trackStart----', trackStart)
this.setMapCenter(new TMap.LatLng(Number(trackStart.wd), Number(trackStart.jd)))
this.MultiPolyline = new TMap.MultiPolyline({
map: this.map, // 绘制到目标地图
// 折线样式定义
styles: {
style_blue: new TMap.PolylineStyle({
color: '#3777FF', // 线填充色
width: 4, // 折线宽度
borderWidth: 2, // 边线宽度
borderColor: '#FFF', // 边线颜色
lineCap: 'round', // 线端头方式
eraseColor: 'rgba(190,188,188,1)'
})
},
geometries: [
{
id: 'erasePath',
styleId: 'style_blue',
paths: trackArr.map((item) => {
return new TMap.LatLng(Number(item.wd), Number(item.jd))
})
}
]
})
const iconStyleObj = {
1: {
width: 24,
height: 40,
anchor: { x: 13, y: 30 },
src: policeImg
},
2: {
width: 50,
height: 25,
anchor: { x: 25, y: 12 },
src: carImg
},
4: {
width: 50,
height: 25,
anchor: { x: 25, y: 12 },
src: motoImg
}
}
const nameStyleObj = {
1: {
width: 60, // 宽度
height: 25, // 高度
anchor: { x: 30, y: 55 } // 锚点位置
},
2: {
width: 100, // 宽度
height: 25, // 高度
anchor: { x: 40, y: 38 } // 锚点位置
},
4: {
width: 100, // 宽度
height: 25, // 高度
anchor: { x: 40, y: 38 } // 锚点位置
}
}
this.multiMarker = new TMap.MultiMarker({
map: this.map,
styles: {
icon: new TMap.MarkerStyle({
faceTo: 'screen',
rotate: 0,
...iconStyleObj[trackStart.type]
}),
name: new TMap.MarkerStyle({
src: nameBg, // 标注点图片url或base64地址
color: '#fff', // 标注点文本颜色
size: 14, // 标注点文本文字大小
offset: { x: 0, y: 0 }, // 标注点文本文字基于direction方位的偏移属性
...nameStyleObj[trackStart.type]
})
},
geometries: [
{
id: 'iconMove',
styleId: 'icon',
position: new TMap.LatLng(trackStart.wd, trackStart.jd)
},
{
id: 'nameMove',
styleId: 'name',
position: new TMap.LatLng(trackStart.wd, trackStart.jd),
content: trackStart.name,
properties: trackStart
}
]
})
this.multiMarker.on('move_ended', () => {
this.$emit('moveEnd')
this.resetMoveJwdlength()
})
// 使用marker 移动接口, https://lbs.qq.com/webApi/javascriptGL/glDoc/glDocMarker
// this.basicInspectionMove(path)
this.multiMarker.on('moving', (e, passedDistance) => {
let passedLatLngs = e?.iconMove?.passedLatLngs || [] // 此处取iconMove或nameMove都可以,因为这两个marker的position是相同的
if (this.moveJwdlength < passedLatLngs.length) {
this.moveJwdlength = passedLatLngs.length
this.$emit('updateNowLc', this.moveJwdlength - 1)
}
;['iconMove', 'nameMove'].forEach((key) => {
if (passedLatLngs) {
// 使用路线擦除接口 eraseTo, https://lbs.qq.com/webApi/javascriptGL/glDoc/glDocVector
this.MultiPolyline.eraseTo(
'erasePath',
passedLatLngs.length - 1,
passedLatLngs[passedLatLngs.length - 1]
)
}
})
})
},
resetMoveJwdlength() {
this.moveJwdlength = 0
},
basicInspectionMove(path, duration) {
console.log('duration----打印', duration)
console.log('path--basicInspectionMove--打印', path)
this.multiMarker.moveAlong(
{
iconMove: {
path,
duration
},
nameMove: {
path,
duration
}
},
{
autoRotation: true
}
)
},
// 暂停轨迹回放
basicInspectionPauseTrack() {
this.multiMarker?.pauseMove()
},
// 继续从暂停的轨迹开始回放轨迹
basicInspectionResumeMove() {
this.multiMarker?.resumeMove()
},
// 清除已有图形
clearArea() {
this.editor.select([this.activeId]) // 选中已经绘制的图形
this.editor.delete() // 删除已选中图形
this.activeId = ''
},
selectArea(id) {
this.clearArea()
this.activeType = id
this.editor.setActionMode(TMap.tools.constants.EDITOR_ACTION.DRAW)
this.editor.setActiveOverlay(id)
},
setToolsGeometryEditor() {
console.log('setToolsGeometryEditor----打印')
var polygon = new TMap.MultiPolygon({
map: this.map
})
var circle = new TMap.MultiCircle({
map: this.map
})
this.editor = new TMap.tools.GeometryEditor({
// TMap.tools.GeometryEditor 文档地址:https://lbs.qq.com/webApi/javascriptGL/glDoc/glDocEditor
map: this.map, // 编辑器绑定的地图对象
overlayList: [
// 可编辑图层 文档地址:https://lbs.qq.com/webApi/javascriptGL/glDoc/glDocEditor#4
{
overlay: polygon,
id: 'polygon'
},
{
overlay: circle,
id: 'circle'
}
],
actionMode: TMap.tools.constants.EDITOR_ACTION.DRAW, // 编辑器的工作模式
activeOverlayId: 'marker', // 激活图层
snappable: true // 开启吸附
})
// 监听绘制结束事件,获取绘制几何图形
this.editor.on('draw_complete', (geometry) => {
// this.editor.destroy()
this.editor.setActionMode(TMap.tools.constants.EDITOR_ACTION.INTERACT)
var { id, radius } = geometry
this.activeId = id
const maxRadius = this.sysConfigData.mon_map_maxRadius * 1 || 500
if (!!radius && radius > maxRadius) {
this.clearArea()
this.$message.warning(`圆的半径超过最大限制${maxRadius}米,请重新选择区域`)
this.selectArea(this.activeType)
return
}
this.$emit('drawComplete', geometry)
})
// 绘制失败,返回失败信息
this.editor.on('draw_error', (errInfo) => {
const { errorDesc, errorType } = errInfo
if (errorDesc === 'geometry illegals' && errorType === 1) {
// 多边形自相交错误信息
this.$message.error('仅支持简单多边形,右击取消上一标点或按Esc键取消当前绘制图案')
}
})
},
/** ** 画区域 start ****/
drawerAreaHandler(funName, data, clearType) {
console.log('funName----打印', funName)
console.log('data----打印', data)
this[funName] && this[funName](data, clearType)
data.center && this.setMapCenter(data.center)
},
clearAreaHandler(type = 'all') {
!type && (type = 'all')
console.log('clearAreaHandler --- type----打印', type)
const typeObj = {
drawerLine: 'multiPolylineLayer',
drawerCircle: 'multiCircleLayer',
drawerPolygon: 'multiPolygonLayer'
}
const blueTypeList = ['drawerLine', 'drawerCircle', 'drawerPolygon']
if (['all', 'allDutyArea'].includes(type)) {
blueTypeList.forEach((drawerType) => {
Object.keys(this[typeObj[drawerType]]).forEach((key) => {
this[typeObj[drawerType]][key]?.setMap(null)
})
})
}
if (blueTypeList.includes(type)) {
Object.keys(this[typeObj[type]]).forEach((key) => {
this[typeObj[type]][key]?.setMap(null)
})
}
if (['all', 'drawerRedPolygon'].includes(type)) {
Object.keys(this.multiRedPolygonLayer).forEach((key) => {
this.multiRedPolygonLayer[key]?.setMap(null)
})
}
},
drawerLine(data, clearType) {
console.log('this.$cloneDeep(data)----drawerLine打印', this.$cloneDeep(data))
this.clearAreaHandler(clearType)
this.$cloneDeep(data).dataList.forEach((item, index) => {
this.multiPolylineLayer[index] = new TMap.MultiPolyline({
id: `polyline-layer-${index}`,
map: this.map,
geometries: [
{
id: `line-${index}`, // 折线唯一标识,删除时使用
paths: item.areaList
}
]
})
})
},
drawerCircle(data, clearType) {
console.log('this.$cloneDeep(data)----drawerCircle打印', this.$cloneDeep(data))
this.clearAreaHandler(clearType)
this.$cloneDeep(data).dataList.forEach((item, index) => {
this.multiCircleLayer[index] = new TMap.MultiCircle({
map: this.map,
geometries: [
{
id: `circle-${index}`,
styleId: 'circle',
center: item.circleCenter,
radius: item.radius
}
]
})
})
},
drawerPolygon(data, clearType) {
console.log('this.$cloneDeep(data)----drawerPolygon打印', this.$cloneDeep(data))
this.clearAreaHandler(clearType)
this.$cloneDeep(data).dataList.forEach((item, index) => {
this.multiPolygonLayer[index] = new TMap.MultiPolygon({
id: `polygon-layer-${index}`, // 图层id
map: this.map, // 显示多边形图层的底图
geometries: [
{
id: `polygon-${index}`, // 多边形图形数据的标志信息
styleId: 'polygon', // 样式id
paths: item.areaList, // 多边形的位置信息
properties: {
// 多边形的属性数据
title: 'polygon'
}
}
]
})
})
},
/**
* 【注】
* drawerPolygon 和 drawerRedPolygon 不能合并 - 有的页面需要同时有两种样式的线(比如:部门辖区和执勤区域,两种边框展示要互不影响)
*/
drawerRedPolygon(data, clearType) {
console.log('this.$cloneDeep(data)----drawerRedPolygon打印', this.$cloneDeep(data))
this.clearAreaHandler(clearType)
this.$cloneDeep(data).dataList.forEach((item, index) => {
this.multiRedPolygonLayer[index] = new TMap.MultiPolygon({
id: `multi-polygon-layer-${index}`, // 图层id
map: this.map, // 显示多边形图层的底图
styles: {
// 多边形的相关样式
polygon: new TMap.PolygonStyle({
color: 'rgba(0,91,255,0)', // 面填充色
borderColor: 'rgba(241,30,52,1)', // 边线颜色
borderWidth: 3 // 边线宽度
})
},
geometries: [
{
id: 'multiPolygon', // 多边形图形数据的标志信息
styleId: 'polygon', // 样式id
paths: item.areaList, // 多边形的位置信息
properties: {
// 多边形的属性数据
title: 'multiPolygon'
}
}
]
})
})
},
/** ** 画区域 end ****/
clearMultiLabel() {
Object.keys(this.multiLabelLayer).forEach((key) => {
this.multiLabelLayer[key]?.setMap(null)
delete this.multiLabelLayer[key]
})
},
setMultiLabel(dataList) {
this.clearMultiLabel()
console.log('dataList----setMultiLabel打印', dataList)
if (this.curZoom < this.setLabelZoom) return // 图层层级小于设置的图层层级时,不显示label
this.$cloneDeep(dataList).forEach((item, index) => {
this.multiLabelLayer[index] = new TMap.MultiLabel({
map: this.map,
styles: {
label: new TMap.LabelStyle({
color: '#3777FF', // 颜色属性
size: 20, // 文字大小属性
offset: { x: 0, y: 0 }, // 文字偏移属性单位为像素
angle: 0, // 文字旋转属性
alignment: 'center', // 文字水平对齐属性
verticalAlignment: 'middle' // 文字垂直对齐属性
})
},
geometries: [
{
id: `label-${index}`, // 点图形数据的标志信息
styleId: 'label', // 样式id
position: item.position, // 标注点位置
content: item.content, // 标注文本
properties: {
// 标注点的属性数据
title: 'label'
}
}
]
})
})
}
/** ** 台州勤务督察 -页面地图 end ****/
}
}
</script>
<style lang='scss'>
.qwdc_card {
width: 300px;
background-color: #fff;
// padding: 10px;
text-align: left;
.text_jb {
background: linear-gradient(to bottom, #49befe, #3783fe); /* 从左到右渐变 */
-webkit-background-clip: text; /* Safari/Chrome支持该属性 */
color: transparent; /* 将文本颜色设置为透明 */
}
&_header {
display: flex;
margin-bottom: 5px;
&_pic {
$height: 50px;
width: 40px;
height: $height;
margin-right: 10px;
border: 1px solid #00a4ff;
border-radius: 3px;
background: linear-gradient(180deg, #fff, rgba(0, 121, 254, 0.07) 97%);
text-align: center;
&.iconfont {
line-height: $height;
font-size: 30px;
color: #388bfd;
// @extend .text_jb;
}
}
&_info {
flex: 1;
&_name {
// margin-bottom: 5px;
font-size: 18px;
white-space: pre-wrap;
color: #7f7f7f;
}
&_bm {
font-size: 14px;
color: #d7d7d7;
}
}
}
&_body {
&_item {
margin-bottom: 5px;
display: flex;
&_label {
color: #7f7f7f;
}
&_value {
flex: 1;
white-space: pre-wrap;
line-height: 21px;
font-size: 14px;
color: #aaaaaa;
.zt {
padding: 0 5px;
border: 1px solid transparent;
border-radius: 3px;
font-size: 12px;
margin-right: 5px;
color: #f59a23;
border-color: #f59a23;
&.success {
border-color: #67c23a;
color: #67c23a;
}
&.warning {
border-color: #e6a23c;
color: #e6a23c;
}
}
}
}
}
&_btns {
padding-top: 10px;
border-top: 1px solid #f2f2f2;
position: relative;
i {
margin: 0 5px;
cursor: pointer;
font-size: 16px;
// color: #388bfd;
@extend .text_jb;
}
.tempMessage {
position: absolute;
top: -27px;
left: 0;
background: #000000d1;
padding: 5px 10px;
border-radius: 5px;
color: #fff;
}
}
}
</style>
轨迹回放 - 非匀速
【思路】
- 把所有轨迹分成N小段,保存当前切到的索引 moveRangeIndex(用于最后判断是否所有轨迹走完);
- 每走一段,把这段数据存起来 alreadyMovePoint,用于擦除轨迹;
- 轨迹结束之后,判断 movePointLength 是否小于 所有点位数量,小于的话继续播放下一小段,
- 每一段轨迹回放前判断 moveRangeIndex 是否大于等于 所有点位数量,是的话表明所有轨迹播放完成(改变播放按钮状态),轨迹回放自动停止,并重置已经移动的点位
代码中只需看标注 【主要代码】 的代码,其余代码不重要
效果图
TrackPlaybackDialog.vue
代码
<!-- 轨迹回放 -->
<template>
<el-dialog
v-loading="loading"
:title="title + '轨迹回放'"
width="90%"
top="5vh"
:close-on-click-modal="false"
destroy-on-close
append-to-body
:visible.sync="dialogVisible"
@close="handleClose"
>
<template #title>
<span class="el-dialog__title">{{ title + '轨迹回放' }}</span>
<el-button class="drawer_btn" type="primary" size="mini" @click="drawerHandler">
{{ isShowDrawer ? '隐藏' : '显示' }}部门辖区/执勤区域
</el-button>
</template>
<TMapNew
v-if="dialogVisible"
ref="TMapRef"
map-name="basicInspection"
:id-name="'TrackPlayback' + idNameTail"
:movePointLength="moveRangeIndex"
:allMovePointLength="positions.length"
@continuMove="moveRange"
@moveEnd="moveEnd"
@updateNowLc="updateNowLc"
/>
<div class="operation">
回放时间:
<el-date-picker
v-model="time"
type="datetimerange"
range-separator="至"
start-placeholder="开始日期"
end-placeholder="结束日期"
size="mini"
format="yyyy-MM-dd HH:mm"
value-format="yyyy-MM-dd HH:mm:ss"
:clearable="false"
/>
<el-button size="mini" @click="moveHandler">{{ isMove ? '停止回放' : '开始回放' }}</el-button>
<el-select v-model="speed" class="speed" placeholder="请选择" size="mini">
<el-option
v-for="item in speedOptions"
:key="item.speed"
:label="item.speed + '倍速'"
:value="item.speed"
/>
</el-select>
<div class="time_line">
<div class="bg">
<div v-for="item in timeRangeList" :key="item" class="item">
<span>{{ item }}</span>
</div>
<div
class="range"
:style="{
'--timeRangeWidth': timeRangeWidth * 100 + '%',
'--timeRangeLeft': timeRangeLeft * 100 + '%'
}"
/>
</div>
</div>
<el-select
v-if="trackType === '1'"
v-model="sblx"
class="sblx"
placeholder="请选择"
size="mini"
>
<el-option
v-for="(item, index) in $common.getDic('mon_jwdc_sblx')"
:key="'sblx_' + index"
:label="item.label"
:value="item.value"
/>
</el-select>
</div>
<div v-show="!!trackType" class="info">
<div class="info_item">
<div class="info_item_label">{{ trackType === '1' ? '民警' : '号牌号码' }}:</div>
<div class="info_item_value mj">
{{ infoData[trackType === '1' ? 'xm' : 'hphm'] | noDataFilter('暂无') }}
</div>
</div>
<div class="info_item">
<div class="info_item_label">部门:</div>
<div class="info_item_value bm">
{{ (infoData.dept_name || infoData.dwjc || infoData.dwmc) | noDataFilter('暂无') }}
</div>
</div>
<div class="info_item">
<div class="info_item_label">里程:</div>
<div class="info_item_value gw">
{{ lc | noDataFilter('暂无') }}{{ lc === 0 || lc ? 'km' : '' }}
</div>
</div>
<div class="info_item">
<div class="info_item_label">当前行驶距离:</div>
<div class="info_item_value gw">{{ curPosition.distanceM | noDataFilter('-') }}米</div>
</div>
<div class="info_item">
<div class="info_item_label">当前坐标:</div>
<div class="info_item_value gw">
{{ curPosition.positionText || `[${curPosition.lat || '-'},${curPosition.lng || '-'}]` }}
</div>
</div>
<div class="info_item">
<div class="info_item_label">当前时间:</div>
<div class="info_item_value gw">
{{ curPosition.gpsTime | noDataFilter('暂无') }}
</div>
</div>
</div>
</el-dialog>
</template>
<script>
import {
queryPoliceTrackDetail,
queryCarTrackDetail,
queryDutyPointList,
queryPoliceTrackList,
queryCarTrackList,
queryAddressByLonLat
} from '@/api/taizhou/service-inspector.js'
import dayjs from 'dayjs'
import { deviceType_MOTO, setDeptArea } from '../index.js'
export default {
name: 'TrackPlaybackDialog',
components: {},
props: {
idNameTail: {
type: String,
default: ''
}
},
data() {
return {
loading: false,
title: '',
trackType: '',
dialogVisible: false,
infoData: {},
time: [dayjs().format('YYYY-MM-DD') + ' 00:00:00', dayjs().format('YYYY-MM-DD HH:mm:ss')],
// startTime: '',
// endTime: '',
positions: [],
lc: 0,
isMove: false,
speed: 1,
isMoveStartFirst: true,
timeRangeList: [...Array(24).keys()],
timeRangeLeft: '0',
timeRangeWidth: '0',
sblx: '1',
speedOptions: [
{ speed: 1 },
{ speed: 2 },
{ speed: 3 },
{ speed: 4 },
{ speed: 8 },
{ speed: 16 }
],
curPosition: {},
isShowDrawer: true,
dutyAreaInfo: {},
deptAreaInfo: {},
moveRangeIndex: 0 // 当前切割轨迹回放的索引,切到了第几条数据
}
},
computed: {
ax() {
const obj = {
1: queryPoliceTrackDetail,
2: queryCarTrackDetail,
4: queryCarTrackDetail
}
return obj[this.trackType]
}
},
watch: {
time(val, oldVal) {
if (!this.dialogVisible) return
const [start, end] = val
if (+new Date(end) - +new Date(start) > 24 * 60 * 60 * 1000) {
this.$message.error('回放时间范围必须在一天内,请重新选择')
this.time = oldVal
} else {
this.getTimeRange()
this.queryTrackDetail()
}
},
sblx() {
if (!this.dialogVisible || this.trackType !== '1') return
this.queryTrackDetail()
},
speed() {
if (!this.dialogVisible) return
this.isMoveStartFirst = true
}
},
created() {},
mounted() {},
methods: {
getTimeRange() {
const [start, end] = this.time
const startH = start.slice(11, 13) * 1
const startM = start.slice(14, 16) * 1
const endH = end.slice(11, 13) * 1
const endM = end.slice(14, 16) * 1
const isSameDay = start.slice(8, 10) === end.slice(8, 10)
const allRange = 24 * 60
if (!isSameDay) {
this.timeRangeList = [...Array(24).keys()].map((item) => (item + startH) % 24)
const timeRange = allRange - (startH * 60 + startM) + endH * 60 + endM
this.timeRangeWidth = timeRange / allRange
this.timeRangeLeft = 0
} else {
const timeRange = endH * 60 + endM - (startH * 60 + startM)
this.timeRangeWidth = timeRange / allRange
this.timeRangeLeft = (startH * 60 + startM) / allRange
}
},
open(data, { title = '', type = '' }) {
this.title = title
this.trackType = type
console.log('data--轨迹回放弹框--', data)
this.dialogVisible = true
this.infoData = data.properties || data || {}
this.$nextTick(() => {
// const ssbm = '331002005300' // 测试数据
const ssbm = this.infoData.dept_code || this.infoData.dwdm || this.infoData.ssbm
setDeptArea(
this,
ssbm,
{ isSetCenter: false, clearArea: 'drawerRedPolygon' },
(areaData) => {
this.deptAreaData = areaData
}
) // 部门辖区
/** ** 执勤区域展示 - start ****/
const { dutyAreaInfo } = this.infoData
console.log('dutyAreaInfo----open打印', dutyAreaInfo)
if (dutyAreaInfo) {
const { center, drawerType, ...areaInfo } = dutyAreaInfo
this.dutyAreaInfo[drawerType] = areaInfo // 保存执勤区域数据 - 便于隐藏区域后再展示区域
// 回溯 - 地图中展示的警员执勤区域带过来,轨迹回放这里也展示这个执勤区域
const { drawerAreaHandler } = this.$refs.TMapRef
setTimeout(() => {
drawerAreaHandler(drawerType, areaInfo, drawerType)
}, 1000)
} else {
// 否则,调接口取区域数据,然后回显区域
this.queryDutyPointList() // 区域
}
/** ** 执勤区域展示 - end ****/
})
this.getTimeRange()
setTimeout(() => {
if (this.infoData.timeRange && this.infoData.timeRange[0] && this.infoData.timeRange[1]) {
;[this.time[0], this.time[1]] = this.infoData.timeRange
this.time.push({})
this.time.pop()
} else {
this.trackQueryHandler()
}
}, 200)
},
trackQueryHandler() {
const axObj = {
1: queryPoliceTrackList,
2: queryCarTrackList
}
axObj[this.trackType]({
page: 1,
rows: 10,
queryStartTime: this.time[0],
queryEndTime: this.time[1],
jh: this.infoData.jh
}).then((res) => {
this.$common.CheckCode(res, null, () => {
const cur = (res.data.length && res.data[0]) || {}
if (Object.keys(cur).length === 0) {
this.queryTrackDetail()
} else {
this.time = [cur.kssj || '', cur.jssj || '']
}
})
})
},
drawerHandler() {
this.isShowDrawer = !this.isShowDrawer
const { drawerAreaHandler, clearAreaHandler } = this.$refs.TMapRef
if (this.isShowDrawer) {
Object.keys(this.dutyAreaInfo).forEach((key) => {
drawerAreaHandler(key, this.dutyAreaInfo[key], key)
})
drawerAreaHandler('drawerRedPolygon', this.deptAreaData, 'drawerRedPolygon')
} else {
clearAreaHandler('all')
}
},
queryTrackDetail() {
this.moveEnd()
let time = {}
if (this.time.length) {
time = {
queryStartTime: this.time[0] || '',
queryEndTime: this.time[1] || ''
}
}
const { jh, deviceIndexCode, recentUserId } = this.infoData
const paramsObj = {
1: {
sblx: this.sblx,
jh,
userId: recentUserId
},
2: {
deviceIndexCode
},
4: {
deviceIndexCode
}
}
this.loading = true
this.ax({
...time,
...paramsObj[this.trackType]
})
.then((res) => {
this.$common.CheckCode(
res,
null,
() => {
/** ** 【主要代码】点位数据 ****/
res = {
code: 200,
msg: null,
data: {
gpsList: [...Array(10).keys()].map((index) => {
const point = [
'109.62616781132476,19.07651100671366',
'109.52052961372806,19.000955875798667',
'109.52779570317193,18.889409778960484',
'109.43389509635472,18.804246369159575',
// '109.55741914575538,18.750798850652597',
// '109.4389254607205,18.687804325385454',
// 相同点位 --- 该点停留
'109.4389254607205,18.687804325385454',
'109.4389254607205,18.687804325385454',
'109.5864836229498,18.66556534760466',
'109.72845239144965,18.73121481942799',
'109.74745608740011,18.87354409698365',
'109.8709801082789,18.91584923707182',
'109.80502610332212,19.07598276944228'
]
const times = [
'2025-02-12 08:00:00',
'2025-02-12 08:00:01',
'2025-02-12 08:00:05',
'2025-02-12 08:00:07',
'2025-02-12 08:00:10',
'2025-02-12 08:00:15',
'2025-02-12 08:00:20',
'2025-02-12 08:00:22',
'2025-02-12 08:00:30',
'2025-02-12 08:00:35'
]
return {
deviceType: '执法记录仪',
deviceName: null,
deptName: '事故中队',
deptCode: '331002000300',
recentUserId: '1000704',
deviceIndexCode: 'K380772',
// lng: '120.217989',
lng: point[index].split(',')[0],
recentUserName: '徐捷',
// gpsTime: this.$dayjs(
// +new Date('2025-02-12 08:00:00') + index * 3 * 1000
// ).format('YYYY-MM-DD HH:mm:ss'),
gpsTime: times[index],
// lat: '30.212518',
lat: point[index].split(',')[1],
// location: '120.217989,30.212518',
location: point[index],
distanceM: index * 10
}
}),
distanceKm: 108.79
},
timestamp: 1748573847277
}
this.lc = res?.data?.distanceKm || 0
this.positions = (res?.data?.gpsList || []).map((item) => {
const nameFieldObj = {
1: 'xm',
2: 'hphm',
3: 'deviceName',
4: 'hphm'
}
const nameObj = {
1: item.recentUserName || '-',
2: this.infoData.hphm || '-',
4: this.infoData.hphm || '-'
}
return {
...item,
jd: item.lng,
wd: item.lat,
// type: item.gjlx,
type:
this.trackType === '2' && this.infoData.deviceType === deviceType_MOTO
? '4'
: this.trackType,
// name: item[nameFieldObj[item.gjlx]]
name: nameObj[this.trackType]
}
})
const { basicInspectionTrack } = this.$refs.TMapRef
basicInspectionTrack(this.positions) // 画折线、图标标点
this.loading = false
},
() => {
this.loading = false
}
)
})
.catch(() => {
this.loading = false
})
},
queryDutyPointList() {
queryDutyPointList({
fzr_jh: this.infoData.jh,
queryStartTime: this.time[0],
queryEndTime: this.time[1]
}).then((res) => {
this.$common.CheckCode(res, null, () => {
const typeObj = {
1: 'Circle',
2: 'Line',
3: 'Polygon'
}
const listObj = {
lineList: [],
circleList: [],
polygonList: []
}
res.data.forEach((item) => {
const { zb, post_type } = item
const areaList = zb
? JSON.parse(zb).map((item) => new TMap.LatLng(item.y * 1, item.x * 1))
: []
const info = {
areaList,
center: areaList[0],
circleCenter: areaList[0],
...item
}
listObj[typeObj[post_type].toLowerCase() + 'List'].push(info)
})
Object.keys(typeObj).forEach((key) => {
const type = typeObj[key]
const drawerInfo = {
dataList: listObj[type.toLowerCase() + 'List']
}
this.dutyAreaInfo[`drawer${type}`] = drawerInfo
this.$refs.TMapRef.drawerAreaHandler(`drawer${type}`, drawerInfo, `drawer${type}`)
})
})
})
},
/** ** 【主要代码】主要逻辑处理 start ****/
moveHandler() {
this.isMove = !this.isMove
const { basicInspectionResumeMove, resetMoveJwdlength } = this.$refs.TMapRef
/**
* 判断是移动还是暂停
*/
if (this.isMove) {
// 移动
/**
* 判断是否为第一次回放,即是不是暂停后继续回放
*/
if (this.isMoveStartFirst) {
this.isMoveStartFirst = false
this.moveRangeIndex = 0
resetMoveJwdlength()
this.moveRange()
} else {
basicInspectionResumeMove()
}
} else {
// 暂停
this.positionTrans() // 将当前经纬度调接口翻译为地址
this.pauseTrack() // 暂停轨迹回放
}
},
// 一段一段轨迹回放
moveRange() {
console.log('"moveRange"----打印', 'moveRange')
const { basicInspectionMove, resetMoveJwdlength, addAlreadyMovePoint } = this.$refs.TMapRef
this.updateNowLc(this.moveRangeIndex === 0 ? 0 : this.moveRangeIndex - 1) // 每回放一段轨迹,就更新一次最新走过的点位,要展示最新走过点位的信息
if (this.moveRangeIndex >= this.positions.length) {
/**
* 如果 moveRangeIndex 大于等于所有点位数,即所有点位已回放完,停止轨迹回放,并重置已经移动的点位
*/
this.moveEnd() // 轨迹回放完成
resetMoveJwdlength() // 重置已经移动的点位
return
}
const sliceLength = this.moveRangeIndex === 0 ? 2 : 1 // 每段轨迹回放要截取的点位数量(这里的轨迹划分成小段小段,其实每一小段就是两个点位组成的一条直线,而非多个点位)
console.log('this.moveRangeIndex----打印', this.moveRangeIndex)
const movePath = this.positions.slice(
this.moveRangeIndex - (this.moveRangeIndex === 0 ? 0 : 1), // 除了第一次,其他每次都要截取前一个点
this.moveRangeIndex + sliceLength
) // 当前段轨迹回放的点位
this.moveRangeIndex += sliceLength
console.log('this.moveRangeIndex----打印', this.moveRangeIndex)
const startTime = movePath[0].gpsTime
const endTime = movePath[movePath.length - 1].gpsTime
const duration = (dayjs(endTime).diff(dayjs(startTime), 'seconds') * 1000) / this.speed // 计算轨迹回放所用时长,➗倍速
const startPosition = `${movePath[0].wd},${movePath[0].jd}`
const endPosition = `${movePath[movePath.length - 1].wd},${movePath[movePath.length - 1].jd}`
const noMove = startPosition === endPosition // 判断该段轨迹是否为原地
if (noMove) {
/**
* 某个时间段内(N秒)在原地没有移动,
* 该段轨迹就不回放,在该点停留N秒后,继续回放下一段轨迹
* 但是这段时间的点位要存起来,便于轨迹擦除
*/
let timer = null
clearTimeout(timer)
addAlreadyMovePoint(movePath) // 保存已经过的点位
timer = setTimeout(() => {
// 在该点停留 N秒后,继续回放下一段轨迹
this.moveRange()
}, duration)
} else {
// 轨迹回放
basicInspectionMove(
movePath.map((item) => {
return new TMap.LatLng(Number(item.wd), Number(item.jd))
}),
duration
)
}
},
moveEnd() {
this.isMove = false
this.isMoveStartFirst = true
this.pauseTrack()
this.moveRangeIndex = 0
},
/** ** 【主要代码】主要逻辑处理 end ****/
updateNowLc(index) {
this.curPosition = { ...this.positions[index], positionText: '' }
},
pauseTrack() {
this.$refs?.TMapRef?.basicInspectionPauseTrack()
},
positionTrans() {
this.curPosition.lng &&
this.curPosition.lat &&
queryAddressByLonLat({
longitude: this.curPosition.lng,
latitude: this.curPosition.lat
}).then((res) => {
this.$common.CheckCode(res, null, () => {
this.curPosition.positionText = res.data?.address || ''
})
})
},
handleClose() {
this.pauseTrack()
this.dialogVisible = false
this.infoData = {}
this.time[0] = dayjs().format('YYYY-MM-DD') + ' 00:00:00'
this.time[1] = dayjs().format('YYYY-MM-DD HH:mm:ss')
this.positions = []
this.lc = 0
this.isMove = false
this.isMoveStartFirst = true
this.speed = 1
this.timeRangeList = [...Array(24).keys()]
this.sblx = '1'
this.curPosition = {}
this.isShowDrawer = true
this.dutyAreaInfo = {}
this.deptAreaInfo = {}
}
}
}
</script>
<style lang='scss' scoped>
@import '@/styles/dialog-scss.scss';
$rightInfoWidth: 240px;
>>> .el-dialog {
.el-dialog__header {
.drawer_btn {
margin-left: 10px;
}
}
.el-dialog__body {
height: 90vh;
padding: 0 !important;
position: relative;
#TrackPlayback,
[id^='TrackPlayback'] {
width: 100%;
height: 100%;
}
.z-index {
z-index: 1999;
}
.operation {
width: calc(100% - #{$rightInfoWidth} - 30px);
height: 55px;
line-height: 55px;
padding: 0 10px;
position: absolute;
top: 10px;
left: 0;
background-color: #ffffffa3;
text-align: left;
display: flex;
align-items: center;
@extend .z-index;
.el-date-editor {
width: 290px;
margin-right: 10px;
.el-range-input {
width: calc(50% - 20px);
}
.el-range-separator {
width: 20px;
}
.el-range__close-icon {
width: 0;
}
}
.speed {
margin: 0 10px;
width: 80px;
}
.sblx {
margin-left: 10px;
width: 100px;
}
.time_line {
flex: 1;
.bg {
$size: 7px;
width: 100%;
height: $size;
border-radius: $size;
display: flex;
background-color: #e4e7ed;
position: relative;
.item {
flex: 1;
position: relative;
z-index: 9;
&::before {
content: '';
position: absolute;
top: 0;
left: -#{$size / 2};
width: $size;
height: $size;
background-color: #fff;
border-radius: $size;
}
span {
position: absolute;
left: -10px;
top: 10px;
display: inline-block;
width: 20px;
height: 20px;
line-height: 20px;
text-align: center;
color: #409eff;
}
}
.range {
$range_size: calc(#{$size} * 2);
position: absolute;
// top: -20px;
width: var(--timeRangeWidth);
height: $size;
left: var(--timeRangeLeft);
background-color: #409eff;
&::before,
&::after {
content: '';
position: absolute;
// top: 0;
top: -5px;
width: $range_size;
height: $range_size;
background-color: #fff;
border-radius: $range_size;
border: 1px solid #c7cbd2;
}
&::before {
left: calc((#{$range_size} / 2) * -1);
}
&::after {
right: calc((#{$range_size} / 2) * -1);
}
}
}
}
}
.info {
width: $rightInfoWidth;
position: absolute;
top: 10px;
right: 20px;
padding: 10px;
background-color: #fff;
border-radius: 8px;
text-align: left;
font-size: 16px;
@extend .z-index;
&_item {
display: flex;
margin-bottom: 5px;
color: #333;
&_label {
}
&_value {
flex: 1;
line-height: 22px;
&.bm,
&.gw {
font-size: 12px;
}
}
}
}
}
}
</style>
TMapNew.vue
代码
<!-- 腾讯地图 -->
<template>
<div :id="idName" class="TMap" />
</template>
<script>
import { mapGetters } from 'vuex'
import policeImg from '@/assets/images/TXMap/icon-auxiliary-police.png'
import carImg from '@/assets/images/TXMap/icon-police-car.png'
import motoImg from '@/assets/images/TXMap/icon-motorcycle.png'
import monitorImg from '@/assets/images/TXMap/icon-monitor.png'
import startImg from '@/assets/images/start.png'
import endImg from '@/assets/images/end.png'
import nameBg from '@/assets/images/TXMap/point_name_bg.png'
export default {
name: 'TMapNew',
components: {},
props: {
mapName: {
type: String,
required: true
},
idName: {
type: String,
default: 'TXMapContanier'
},
movePointLength: {
type: Number,
default: 0
},
allMovePointLength: {
type: Number,
default: 0
}
},
data() {
return {
map: null,
curZoom: 0,
setLabelZoom: 17,
multiMarker: null, // 点位图标
MultiLabel: null, // 点位图标顶部文字描述
infoWindow: null, // 信息窗口
MultiPolyline: null, // 折线 - 运动轨迹
multiPolylineLayer: {}, // 多个 简单折线
multiCircleLayer: {}, // 多个 简单圆
multiPolygonLayer: {}, // 多个 简单多边形
multiRedPolygonLayer: {}, // 多个 简单多边形 -- 部门辖区
multiLabelLayer: {}, // 多个 label
trackQueryMultiMarker: {
jy: null,
jc: null,
jk: null
},
trackQueryMultiLabel: {
jy: null,
jc: null,
jk: null
},
editor: null,
activeType: 'marker',
activeId: '', // 值格式为 6C5895CE-B42D-4E9B-A8FA-81135761CBDD
alreadyMovePoint: []
}
},
computed: {
...mapGetters(['sysConfigData'])
},
mounted() {
// this.initMap()
window.onbeforeunload = () => {
localStorage.removeItem('TXMapIsCanLoad')
}
const timer = setInterval(() => {
const TXMapIsCanLoad = localStorage.getItem('TXMapIsCanLoad')
if (TXMapIsCanLoad === 'true') {
this.initMap()
clearInterval(timer)
}
}, 100)
},
beforeDestroy() {
this.map?.destroy()
this.infoWindow?.close()
this.map = null
this.multiMarker?.setMap(null)
this.MultiLabel?.setMap(null)
this.infoWindow?.setMap(null)
this.MultiPolyline?.setMap(null)
this.multiPolylineLayer = {}
this.multiCircleLayer = {}
this.multiPolygonLayer = {}
this.multiRedPolygonLayer = {}
this.multiLabelLayer = {}
this.trackQueryMultiMarker = {
jy: null,
jc: null,
jk: null
}
this.trackQueryMultiLabel = {
jy: null,
jc: null,
jk: null
}
this.editor = null
},
methods: {
// 初始化地图
initMap() {
this[this.mapName + 'Init']()
},
setMapCenter(center) {
this.map.setCenter(center)
},
setMapZoom(level) {
this.map.setZoom(level)
},
// 创建wmts图层
newWMTSLayer() {
const url = this.sysConfigData.mon_map_wmts_url
const { map } = this
if (!url) return
new TMap.WMTSLayer({
url, // 地图服务地址
map, // 展示图层的地图对象
minZoom: 3, // 最小缩放层级,当地图缩放层级小于该值时该图层不显示,默认为3
maxZoom: 20, // 最大缩放层级,当地图缩放层级大于该值时该图层不显示,默认为20
visible: true, // 是否可见,默认为true
zIndex: 1, // 图层绘制顺序
opacity: 0.9, // 图层透明度,默认为1
params: {
// OGC标准的WMTS地图服务的GetTile接口的参数
layers: 'topp:raster_cgcs2000%3Ataizhou2m_cgcs2000', // 请求的图层名称
tileMatrixSet: 'taizhou2m%3A11' // 瓦片矩阵数据集
}
})
},
// 标记点
// point(markerId, styles, pointArr) {
// this[this.mapName + 'Point'](markerId, styles, pointArr)
// },
// 打开弹框
openInfoWindow(position, content) {
this.infoWindow = new TMap.InfoWindow({
map: this.map,
position: new TMap.LatLng(position[0], position[1]),
offset: { x: 0, y: -32 }, // 设置信息窗相对position偏移像素
content: content
})
},
/** ** 台州勤务督察 -页面地图 start ****/
basicInspectionInit() {
this.basicInspectionInitCommon()
},
setPointMapInit() {
this.basicInspectionInitCommon()
this.$emit('setInitPoint')
this.map.on('click', (e) => {
this.$emit('getPoint', e.latLng)
})
},
setPoint({ jd, wd }, isSetCenter = false) {
isSetCenter && this.setMapCenter(new TMap.LatLng(Number(jd), Number(wd)))
this.MultiMarker = new TMap.MultiMarker({
id: 'marker-layer',
map: this.map,
styles: {
marker: new TMap.MarkerStyle({
width: 25,
height: 35,
anchor: { x: 16, y: 32 }
})
},
geometries: [
{
id: 'demo',
styleId: 'marker',
position: new TMap.LatLng(jd * 1, wd * 1),
properties: {
title: 'marker'
}
}
]
})
},
removePoint() {
this.MultiMarker?.setMap(null)
},
basicInspectionInitCommon() {
var location = (this.sysConfigData.map_location || '121.427648,28.661939').split(',')
// console.log(Number(location[1]), Number(location[0]))
const featuresObj = {
gs: null,
nw: []
}
this.curZoom = this.sysConfigData.map_level || 14
this.map = new TMap.Map(this.idName, {
zoom: this.curZoom,
center: new TMap.LatLng(Number(location[1]), Number(location[0])),
baseMap: {
type: 'vector',
// features: null // 本地跑项目用
// // features: [] // 内网用
features: featuresObj[this.sysConfigData.mon_map_yslx] || null
}
})
/** ** 获取地图首次加载完成 start ****/
this.map.off('tilesloaded', tilesLoad)
this.map.on('tilesloaded', tilesLoad)
const that = this
function tilesLoad() {
console.log('地图加载完成')
that.map.off('tilesloaded', tilesLoad)
}
/** ** 获取地图首次加载完成 end ****/
this.newWMTSLayer()
this.map.removeControl(TMap.constants.DEFAULT_CONTROL_ID.ROTATION) // 移除腾讯地图旋转控件
this.map.removeControl(TMap.constants.DEFAULT_CONTROL_ID.ZOOM) // 移除腾讯地图缩放控件
this.map.on('zoom_changed', (params) => {
console.log('params----zoom_changed', params)
this.curZoom = params.zoom
if (this.curZoom > this.setLabelZoom) {
!Object.keys(this.multiLabelLayer).length && this.$emit('setLabel', this.curZoom)
} else {
this.clearMultiLabel()
}
})
if (this.idName === 'trackQueryTXMapContanier') {
let timer = null
this.map.on('center_changed', (params) => {
clearTimeout(timer)
timer = setTimeout(() => {
const center = params.center
console.log('params----center_changed', params)
console.log('center----center_changed', center)
this.$emit('getPointArr', { jd: center.lng, wd: center.lat, isCenterChange: true })
}, 500)
})
}
},
basicInspectionPoint(markerId, pointArr, isSetCenter = false) {
// console.log('pointArr----basicInspectionPoint', pointArr)
this.MultiMarker?.setMap(null)
this.infoWindow?.setMap(null)
if (!pointArr.length) return
// 初始marker
this.MultiMarker = this.basicInspectionCommonPoint(markerId, pointArr, isSetCenter)
this.MultiMarker.on('click', this.basicInspectionCommonClick)
},
basicInspectionPointText(markerId, pointArr) {
// console.log('pointArr----basicInspectionPointText', pointArr)
this.MultiLabel?.setMap(null)
this.infoWindow?.setMap(null)
if (!pointArr.length) return
// 初始marker
this.MultiLabel = this.basicInspectionCommonPointText(pointArr)
this.MultiLabel.on('click', this.basicInspectionCommonClick)
},
trackQueryPoint(markerId, pointArr, pointTypeArr, pointType, isSetCenter = false) {
console.log('pointArr----trackQueryPoint', pointArr)
const gjlxObj = {
1: 'jy',
2: 'jc',
3: 'jk'
}
const gjlx = gjlxObj[pointType]
;['1', '2', '3'].forEach((item) => {
if (!pointTypeArr.includes(item) || pointType === item) {
this.trackQueryMultiMarker[gjlxObj[item]]?.setMap(null)
}
})
// this.MultiMarker?.setMap(null)
this.infoWindow?.setMap(null)
if (!pointArr.length) return
// 初始marker
this.trackQueryMultiMarker[gjlx] = this.basicInspectionCommonPoint(
markerId,
pointArr,
isSetCenter
)
this.trackQueryMultiMarker[gjlx].on('click', this.basicInspectionCommonClick)
},
trackQueryPointText(markerId, pointArr, pointTypeArr, pointType) {
console.log('pointArr----trackQueryPointText', pointArr)
const gjlxObj = {
1: 'jy',
2: 'jc',
3: 'jk'
}
const gjlx = gjlxObj[pointType]
;['1', '2', '3'].forEach((item) => {
if (!pointTypeArr.includes(item) || pointType === item) {
this.trackQueryMultiLabel[gjlxObj[item]]?.setMap(null)
}
})
// this.MultiLabel?.setMap(null)
this.infoWindow?.setMap(null)
if (!pointArr.length) return
// 初始marker
this.trackQueryMultiLabel[gjlx] = this.basicInspectionCommonPointText(pointArr)
this.trackQueryMultiLabel[gjlx].on('click', this.basicInspectionCommonClick)
},
basicInspectionCommonPoint(markerId, pointArr, isSetCenter) {
if (isSetCenter) {
const first = pointArr[0]
this.setMapCenter(new TMap.LatLng(Number(first.wd), Number(first.jd)))
}
return new TMap.MultiMarker({
id: markerId,
map: this.map,
styles: {
police: new TMap.MarkerStyle({
width: 24,
height: 40,
anchor: { x: 0, y: 0 },
src: policeImg
}),
car: new TMap.MarkerStyle({
width: 50,
height: 25,
anchor: { x: 0, y: 0 },
src: carImg
}),
moto: new TMap.MarkerStyle({
width: 50,
height: 25,
anchor: { x: 0, y: 0 },
src: motoImg
}),
monitor: new TMap.MarkerStyle({
width: 40,
height: 30,
anchor: { x: 0, y: 0 },
src: monitorImg
})
},
geometries: pointArr.map((item, index) => {
const styleIdObj = {
1: 'police',
2: 'car',
3: 'monitor',
4: 'moto'
}
return {
id: index,
// styleId: 'police',
styleId: styleIdObj[item.type || '1'],
position: new TMap.LatLng(item.wd, item.jd),
properties: this.basicInspectionCommonProperties(item, index)
}
})
})
},
basicInspectionCommonPointText(pointArr) {
const commonStyle = {
height: 25, // 高度
anchor: { x: 15, y: 26 }, // 锚点位置
src: nameBg, // 标注点图片url或base64地址
color: '#fff', // 标注点文本颜色
size: 14, // 标注点文本文字大小
offset: { x: 0, y: 0 } // 标注点文本文字基于direction方位的偏移属性
}
return new TMap.MultiMarker({
map: this.map,
styles: {
police: new TMap.MarkerStyle({
width: 60, // 宽度
...commonStyle
}),
car: new TMap.MarkerStyle({
width: 100, // 宽度
...commonStyle
}),
moto: new TMap.MarkerStyle({
width: 100, // 宽度
...commonStyle
}),
monitor: new TMap.MarkerStyle({
width: 100, // 宽度
...commonStyle
})
},
geometries: pointArr.map((item, index) => {
const styleIdObj = {
1: 'police',
2: 'car',
3: 'monitor',
4: 'moto'
}
let content = item.name || ''
if (['3'].includes(item.type)) {
content = content.slice(0, 5) + (content.length > 5 ? '...' : '')
}
return {
styleId: styleIdObj[item.type || '1'],
position: new TMap.LatLng(item.wd, item.jd),
content,
properties: this.basicInspectionCommonProperties(item, index)
}
})
})
},
basicInspectionCommonProperties(item, index) {
return {
type: item.type || '1', // 1警员、2警车、3监控
...item
}
},
basicInspectionCommonClick(data) {
console.log('data--basicInspectionCommonClick--', data)
this.infoWindow?.destroy()
this.$emit('pointClick', data)
},
/** ** 【主要代码】主要逻辑处理 start ****/
basicInspectionTrack(trackArr) {
this.MultiPolyline?.setMap(null)
this.multiMarker?.setMap(null)
if (!trackArr.length) return
const trackStart = trackArr[0] || {}
console.log('trackStart----', trackStart)
this.setMapCenter(new TMap.LatLng(Number(trackStart.wd), Number(trackStart.jd))) // 设置中心点
// 画折线
this.MultiPolyline = new TMap.MultiPolyline({
map: this.map, // 绘制到目标地图
// 折线样式定义
styles: {
style_blue: new TMap.PolylineStyle({
color: '#3777FF', // 线填充色
width: 4, // 折线宽度
borderWidth: 2, // 边线宽度
borderColor: '#FFF', // 边线颜色
lineCap: 'round', // 线端头方式
eraseColor: 'rgba(190,188,188,1)'
})
},
geometries: [
{
id: 'erasePath',
styleId: 'style_blue',
paths: trackArr.map((item) => {
return new TMap.LatLng(Number(item.wd), Number(item.jd))
})
}
]
})
const iconStyleObj = {
1: {
width: 24,
height: 40,
anchor: { x: 13, y: 30 },
src: policeImg
},
2: {
width: 50,
height: 25,
anchor: { x: 25, y: 12 },
src: carImg
},
4: {
width: 50,
height: 25,
anchor: { x: 25, y: 12 },
src: motoImg
}
}
const nameStyleObj = {
1: {
width: 60, // 宽度
height: 25, // 高度
anchor: { x: 30, y: 55 } // 锚点位置
},
2: {
width: 100, // 宽度
height: 25, // 高度
anchor: { x: 40, y: 38 } // 锚点位置
},
4: {
width: 100, // 宽度
height: 25, // 高度
anchor: { x: 40, y: 38 } // 锚点位置
}
}
// 标点记图标
this.multiMarker = new TMap.MultiMarker({
map: this.map,
styles: {
icon: new TMap.MarkerStyle({
faceTo: 'screen',
rotate: 0,
...iconStyleObj[trackStart.type]
}),
name: new TMap.MarkerStyle({
src: nameBg, // 标注点图片url或base64地址
color: '#fff', // 标注点文本颜色
size: 14, // 标注点文本文字大小
offset: { x: 0, y: 0 }, // 标注点文本文字基于direction方位的偏移属性
...nameStyleObj[trackStart.type]
})
},
geometries: [
{
id: 'iconMove',
styleId: 'icon',
position: new TMap.LatLng(trackStart.wd, trackStart.jd)
},
{
id: 'nameMove',
styleId: 'name',
position: new TMap.LatLng(trackStart.wd, trackStart.jd),
content: trackStart.name,
properties: trackStart
}
]
})
// 一段轨迹回放结束
this.multiMarker.on('move_ended', () => {
/**
* 当前段轨迹回放完之后,通过判断 已经移动的点位长度 是否小于 总长度,来判断是否已经回放完
* 如果没有播放完成,则继续播放下一段轨迹
*/
if (this.movePointLength <= this.allMovePointLength) {
this.$emit('continuMove') // 继续回放下一段轨迹
}
})
// 使用marker 移动接口, https://lbs.qq.com/webApi/javascriptGL/glDoc/glDocMarker
// this.basicInspectionMove(path)
this.multiMarker.on('moving', (e, passedDistance) => {
let passedLatLngs = e?.iconMove?.passedLatLngs || [] // 此处取iconMove或nameMove都可以,因为这两个marker的position是相同的
passedLatLngs.unshift(...this.alreadyMovePoint.slice(0, -2))
;['iconMove', 'nameMove'].forEach((key) => {
if (passedLatLngs) {
// 使用路线擦除接口 eraseTo, https://lbs.qq.com/webApi/javascriptGL/glDoc/glDocVector
this.MultiPolyline.eraseTo(
'erasePath',
passedLatLngs.length - 1, // 要擦除到的坐标索引 index
passedLatLngs[passedLatLngs.length - 1] // 要擦除的点位数组,官方解释:线段 (坐标索引为[ index -1 , index ] )上擦除点的经纬度坐标( 如果这个坐标不在擦除的索引范围内,会一直擦除到坐标索引为index的点 )。只支持简单折线。
)
}
})
})
},
// 重置已经移动的点位为空数组
resetMoveJwdlength() {
this.alreadyMovePoint = []
},
// 将已经走过的点位存起来,用于后续的路线擦除
addAlreadyMovePoint(path) {
this.alreadyMovePoint.push(...path.slice(this.alreadyMovePoint.length === 0 ? 0 : 1))
},
basicInspectionMove(path, duration) {
console.log('duration----打印', duration)
console.log('path--basicInspectionMove--打印', path)
this.addAlreadyMovePoint(path)
this.multiMarker.moveAlong(
{
iconMove: {
path, // 要经过的点位数组
duration // 轨迹回放时间,官方文档 https://lbs.qq.com/webApi/javascriptGL/glDoc/glDocMarker#8
},
nameMove: {
path, // 要经过的点位数组
duration // 轨迹回放时间,官方文档 https://lbs.qq.com/webApi/javascriptGL/glDoc/glDocMarker#8
}
},
{
autoRotation: true
}
)
},
// 暂停轨迹回放
basicInspectionPauseTrack() {
this.multiMarker?.pauseMove()
},
// 继续从暂停的轨迹开始回放轨迹
basicInspectionResumeMove() {
this.multiMarker?.resumeMove()
},
/** ** 【主要代码】主要逻辑处理 end ****/
// 清除已有图形
clearArea() {
this.editor.select([this.activeId]) // 选中已经绘制的图形
this.editor.delete() // 删除已选中图形
this.activeId = ''
},
selectArea(id) {
this.clearArea()
this.activeType = id
this.editor.setActionMode(TMap.tools.constants.EDITOR_ACTION.DRAW)
this.editor.setActiveOverlay(id)
},
setToolsGeometryEditor() {
console.log('setToolsGeometryEditor----打印')
var polygon = new TMap.MultiPolygon({
map: this.map
})
var circle = new TMap.MultiCircle({
map: this.map
})
this.editor = new TMap.tools.GeometryEditor({
// TMap.tools.GeometryEditor 文档地址:https://lbs.qq.com/webApi/javascriptGL/glDoc/glDocEditor
map: this.map, // 编辑器绑定的地图对象
overlayList: [
// 可编辑图层 文档地址:https://lbs.qq.com/webApi/javascriptGL/glDoc/glDocEditor#4
{
overlay: polygon,
id: 'polygon'
},
{
overlay: circle,
id: 'circle'
}
],
actionMode: TMap.tools.constants.EDITOR_ACTION.DRAW, // 编辑器的工作模式
activeOverlayId: 'marker', // 激活图层
snappable: true // 开启吸附
})
// 监听绘制结束事件,获取绘制几何图形
this.editor.on('draw_complete', (geometry) => {
// this.editor.destroy()
this.editor.setActionMode(TMap.tools.constants.EDITOR_ACTION.INTERACT)
var { id, radius } = geometry
this.activeId = id
const maxRadius = this.sysConfigData.mon_map_maxRadius * 1 || 500
if (!!radius && radius > maxRadius) {
this.clearArea()
this.$message.warning(`圆的半径超过最大限制${maxRadius}米,请重新选择区域`)
this.selectArea(this.activeType)
return
}
this.$emit('drawComplete', geometry)
})
// 绘制失败,返回失败信息
this.editor.on('draw_error', (errInfo) => {
const { errorDesc, errorType } = errInfo
if (errorDesc === 'geometry illegals' && errorType === 1) {
// 多边形自相交错误信息
this.$message.error('仅支持简单多边形,右击取消上一标点或按Esc键取消当前绘制图案')
}
})
},
/** ** 画区域 start ****/
drawerAreaHandler(funName, data, clearType) {
console.log('funName----打印', funName)
console.log('data----打印', data)
this[funName] && this[funName](data, clearType)
data.center && this.setMapCenter(data.center)
},
clearAreaHandler(type = 'all') {
!type && (type = 'all')
console.log('clearAreaHandler --- type----打印', type)
const typeObj = {
drawerLine: 'multiPolylineLayer',
drawerCircle: 'multiCircleLayer',
drawerPolygon: 'multiPolygonLayer'
}
const blueTypeList = ['drawerLine', 'drawerCircle', 'drawerPolygon']
if (['all', 'allDutyArea'].includes(type)) {
blueTypeList.forEach((drawerType) => {
Object.keys(this[typeObj[drawerType]]).forEach((key) => {
this[typeObj[drawerType]][key]?.setMap(null)
})
})
}
if (blueTypeList.includes(type)) {
Object.keys(this[typeObj[type]]).forEach((key) => {
this[typeObj[type]][key]?.setMap(null)
})
}
if (['all', 'drawerRedPolygon'].includes(type)) {
Object.keys(this.multiRedPolygonLayer).forEach((key) => {
this.multiRedPolygonLayer[key]?.setMap(null)
})
}
},
drawerLine(data, clearType) {
console.log('this.$cloneDeep(data)----drawerLine打印', this.$cloneDeep(data))
this.clearAreaHandler(clearType)
this.$cloneDeep(data).dataList.forEach((item, index) => {
this.multiPolylineLayer[index] = new TMap.MultiPolyline({
id: `polyline-layer-${index}`,
map: this.map,
geometries: [
{
id: `line-${index}`, // 折线唯一标识,删除时使用
paths: item.areaList
}
]
})
})
},
drawerCircle(data, clearType) {
console.log('this.$cloneDeep(data)----drawerCircle打印', this.$cloneDeep(data))
this.clearAreaHandler(clearType)
this.$cloneDeep(data).dataList.forEach((item, index) => {
this.multiCircleLayer[index] = new TMap.MultiCircle({
map: this.map,
geometries: [
{
id: `circle-${index}`,
styleId: 'circle',
center: item.circleCenter,
radius: item.radius
}
]
})
})
},
drawerPolygon(data, clearType) {
console.log('this.$cloneDeep(data)----drawerPolygon打印', this.$cloneDeep(data))
this.clearAreaHandler(clearType)
this.$cloneDeep(data).dataList.forEach((item, index) => {
this.multiPolygonLayer[index] = new TMap.MultiPolygon({
id: `polygon-layer-${index}`, // 图层id
map: this.map, // 显示多边形图层的底图
geometries: [
{
id: `polygon-${index}`, // 多边形图形数据的标志信息
styleId: 'polygon', // 样式id
paths: item.areaList, // 多边形的位置信息
properties: {
// 多边形的属性数据
title: 'polygon'
}
}
]
})
})
},
/**
* 【注】
* drawerPolygon 和 drawerRedPolygon 不能合并 - 有的页面需要同时有两种样式的线(比如:部门辖区和执勤区域,两种边框展示要互不影响)
*/
drawerRedPolygon(data, clearType) {
console.log('this.$cloneDeep(data)----drawerRedPolygon打印', this.$cloneDeep(data))
this.clearAreaHandler(clearType)
this.$cloneDeep(data).dataList.forEach((item, index) => {
this.multiRedPolygonLayer[index] = new TMap.MultiPolygon({
id: `multi-polygon-layer-${index}`, // 图层id
map: this.map, // 显示多边形图层的底图
styles: {
// 多边形的相关样式
polygon: new TMap.PolygonStyle({
color: 'rgba(0,91,255,0)', // 面填充色
borderColor: 'rgba(241,30,52,1)', // 边线颜色
borderWidth: 3 // 边线宽度
})
},
geometries: [
{
id: 'multiPolygon', // 多边形图形数据的标志信息
styleId: 'polygon', // 样式id
paths: item.areaList, // 多边形的位置信息
properties: {
// 多边形的属性数据
title: 'multiPolygon'
}
}
]
})
})
},
/** ** 画区域 end ****/
clearMultiLabel() {
Object.keys(this.multiLabelLayer).forEach((key) => {
this.multiLabelLayer[key]?.setMap(null)
delete this.multiLabelLayer[key]
})
},
setMultiLabel(dataList) {
this.clearMultiLabel()
console.log('dataList----setMultiLabel打印', dataList)
if (this.curZoom < this.setLabelZoom) return // 图层层级小于设置的图层层级时,不显示label
this.$cloneDeep(dataList).forEach((item, index) => {
this.multiLabelLayer[index] = new TMap.MultiLabel({
map: this.map,
styles: {
label: new TMap.LabelStyle({
color: '#3777FF', // 颜色属性
size: 20, // 文字大小属性
offset: { x: 0, y: 0 }, // 文字偏移属性单位为像素
angle: 0, // 文字旋转属性
alignment: 'center', // 文字水平对齐属性
verticalAlignment: 'middle' // 文字垂直对齐属性
})
},
geometries: [
{
id: `label-${index}`, // 点图形数据的标志信息
styleId: 'label', // 样式id
position: item.position, // 标注点位置
content: item.content, // 标注文本
properties: {
// 标注点的属性数据
title: 'label'
}
}
]
})
})
}
/** ** 台州勤务督察 -页面地图 end ****/
}
}
</script>
<style lang='scss'>
.qwdc_card {
width: 300px;
background-color: #fff;
// padding: 10px;
text-align: left;
.text_jb {
background: linear-gradient(to bottom, #49befe, #3783fe); /* 从左到右渐变 */
-webkit-background-clip: text; /* Safari/Chrome支持该属性 */
color: transparent; /* 将文本颜色设置为透明 */
}
&_header {
display: flex;
margin-bottom: 5px;
&_pic {
$height: 50px;
width: 40px;
height: $height;
margin-right: 10px;
border: 1px solid #00a4ff;
border-radius: 3px;
background: linear-gradient(180deg, #fff, rgba(0, 121, 254, 0.07) 97%);
text-align: center;
&.iconfont {
line-height: $height;
font-size: 30px;
color: #388bfd;
// @extend .text_jb;
}
}
&_info {
flex: 1;
&_name {
// margin-bottom: 5px;
font-size: 18px;
white-space: pre-wrap;
color: #7f7f7f;
}
&_bm {
font-size: 14px;
color: #d7d7d7;
}
}
}
&_body {
&_item {
margin-bottom: 5px;
display: flex;
&_label {
color: #7f7f7f;
}
&_value {
flex: 1;
white-space: pre-wrap;
line-height: 21px;
font-size: 14px;
color: #aaaaaa;
.zt {
padding: 0 5px;
border: 1px solid transparent;
border-radius: 3px;
font-size: 12px;
margin-right: 5px;
color: #f59a23;
border-color: #f59a23;
&.success {
border-color: #67c23a;
color: #67c23a;
}
&.warning {
border-color: #e6a23c;
color: #e6a23c;
}
}
}
}
}
&_btns {
padding-top: 10px;
border-top: 1px solid #f2f2f2;
position: relative;
i {
margin: 0 5px;
cursor: pointer;
font-size: 16px;
// color: #388bfd;
@extend .text_jb;
}
.tempMessage {
position: absolute;
top: -27px;
left: 0;
background: #000000d1;
padding: 5px 10px;
border-radius: 5px;
color: #fff;
}
}
}
</style>