<template>
|
<!-- 安全管理 ==> 智能安全帽 => 轨迹回放 -->
|
<div class="main">
|
<!-- table -->
|
<div class="main_left">
|
<div class="main_left_search">
|
<div class="search_item">
|
<span>姓名:</span>
|
<el-input size="mini" v-model="queryInfo.userName" placeholder="请输入姓名" clearable></el-input>
|
</div>
|
<div class="search_item">
|
<span>设备编号:</span>
|
<el-input size="mini" v-model="queryInfo.deviceNum" placeholder="请输入设备编号" clearable></el-input>
|
<el-button icon="el-icon-search" @click="queryTable">查询</el-button>
|
</div>
|
</div>
|
<div class="main_left_table">
|
<cpnTable :table-loading="loading" :table-data="dataList" :table-columns="tableColumns"
|
:row-click="rowClick">
|
</cpnTable>
|
</div>
|
</div>
|
<!-- chart -->
|
<div class="main_right_echart" v-show="!isShowMap">
|
<div class="echart_options">
|
<span>日期:</span>
|
<el-date-picker class="elDatePicker" type="daterange" v-model="chart.date" value-format="yyyy-MM-dd"
|
start-placeholder="开始日期" end-placeholder="结束日期" clearable></el-date-picker>
|
<el-button icon="el-icon-search" @click="queryEchart">查询</el-button>
|
<el-button style="float:right" @click="watchMap">查看</el-button>
|
</div>
|
<div ref="echart_bar" class="echart_bar"></div>
|
<div ref="echart_drop" class="echart_drop"></div>
|
</div>
|
<!-- map -->
|
<div class="main_right_map" v-show="isShowMap">
|
<div class="map_option">
|
<span>日期:</span>
|
<el-date-picker class="elDatePicker" type="daterange" v-model="chart.date" value-format="yyyy-MM-dd"
|
start-placeholder="开始日期" end-placeholder="结束日期" clearable></el-date-picker>
|
<el-button icon="el-icon-search" @click="queryEchart">查询</el-button>
|
<el-button style="float:right" @click="hideMap">返回</el-button>
|
</div>
|
<div class="map_view">
|
<div class="map_view_main">
|
<div id="container"></div>
|
<div id="trackDetail" v-show="isShowTrackDetail">
|
<h2>位置信息统计</h2>
|
<div class="ul_box">
|
<ul>
|
<li :class="{ active: index === maps.trackListIndex }"
|
v-for="(val, index) in maps.trackLists " :key="index" @click="trackListClick(index)">
|
{{ val }}
|
</li>
|
</ul>
|
</div>
|
</div>
|
</div>
|
<div class="map_view_btn">
|
<el-button @click="startAnimation">开始动画</el-button>
|
<el-button @click="pauseAnimation">停止动画</el-button>
|
<el-button @click="resumeAnimation">继续动画</el-button>
|
<el-button @click="stopAnimation">停止动画</el-button>
|
<el-button @click="showTrackDetail">轨迹详情</el-button>
|
</div>
|
</div>
|
</div>
|
</div>
|
</template>
|
|
<script>
|
import AMapLoader from '@amap/amap-jsapi-loader';
|
import cpnTable from '@/components/element/Table'
|
import icon from '@/assets/markerIcon.png'
|
import 'echarts-liquidfill' // 水滴图形
|
export default {
|
data() {
|
return {
|
loading: false,
|
isShowMap: false, // 是否查看轨迹地图
|
isShowTrackDetail: false, // 是否渲染轨迹详情
|
queryInfo: {
|
pageNum: 1,
|
pageSize: 9999,
|
userName: '',
|
deviceNum: '',
|
},
|
dataList: [],
|
tableColumns: [],
|
userId: '', // 用户id
|
// 图表数据
|
chart: {
|
date: [new Date(Date.now() - 30 * 24 * 60 * 60 * 1000).format(), new Date().format()], // 轨迹图表日期.默认近一个月
|
totaltime: '', // 安全帽在线总时长
|
},
|
maps: { // 地图数据
|
date: '',
|
trackLists: [], // 轨迹详情
|
trackListIndex: '',
|
},
|
}
|
},
|
components: {
|
cpnTable
|
},
|
created() {
|
// 添加密钥,获取高德地址使用
|
window._AMapSecurityConfig = {
|
securityJsCode: "aa90b00e27025d1f57286675cc8b232e",
|
}
|
// 存储全局变量.(不响应式)
|
this.maps.times = []
|
this.maps.lineArr = []
|
this.maps.AMap = null
|
this.maps.map = null
|
this.maps.mapMarker = null
|
this.chart.chartInstanceBar = null
|
this.chart.chartInstanceDrop = null
|
this.$http = this.$api.Safety.SmartHelmet.trackBack // 接口api路径
|
|
this.setTableColumn()
|
this.getLists()
|
},
|
mounted() {
|
window.addEventListener("resize", this.screenAdapter)
|
},
|
beforeDestroy() {
|
this.clearMap()
|
this.clearChart()
|
window.removeEventListener("resize", this.screenAdapter)
|
},
|
methods: {
|
setTableColumn() {
|
this.tableColumns = [
|
{selection: true},
|
{name: "用户ID", key: "userId"},
|
{name: "姓名", key: "userName"},
|
{name: "设备编号", key: "deviceNum"},
|
]
|
},
|
getLists() {
|
this.loading = true
|
this.$api.Safety.SmartHelmet.getLists(this.queryInfo).then(res => {
|
if (res.statusMsg === 'ok') {
|
this.dataList = res.data.list
|
}
|
this.loading = false
|
})
|
},
|
// 获取用户在线时长
|
async getOnlineTime() {
|
let out = {}
|
const params = {
|
userId: this.userId,
|
strTime: this.chart.date?.[0],
|
endTime: this.chart.date?.[1],
|
}
|
const {data, statusMsg} = await this.$http.getOnlineTime(params)
|
if (statusMsg === 'ok') {
|
out = {
|
list: data.tHelmetTrajectory,
|
total: data.total,
|
}
|
}
|
return out
|
},
|
// 获取用户轨迹
|
async getTrackLists() {
|
let out = {
|
positions: [], // 经纬度
|
times: [], // 时间
|
}
|
const params = {
|
userId: this.userId,
|
strTime: this.chart.date?.[0],
|
endTime: this.chart.date?.[1],
|
}
|
const {data, statusMsg} = await this.$http.getTrackLists(params)
|
if (statusMsg === 'ok') {
|
data.forEach(item => {
|
out.positions.push([+item.ypoint, +item.xpoint])
|
out.times.push(item.smTime)
|
})
|
}
|
return out
|
},
|
// 初始化图表
|
initChart() {
|
this.chart.chartInstanceBar = this.$echarts.init(this.$refs.echart_bar)
|
this.chart.chartInstanceDrop = this.$echarts.init(this.$refs.echart_drop)
|
this.renderBaseChart()
|
},
|
// 渲染chart基础配置
|
renderBaseChart() {
|
const baseBarOptions = {
|
animationDuration: 1500,
|
tooltip: {
|
trigger: "axis",
|
},
|
legend: {
|
itemWidth: 35,
|
itemHeight: 12,
|
x: 'right',
|
y: 'top',
|
textStyle: {
|
color: "#fff",
|
},
|
},
|
grid: {
|
top: "10%",
|
right: "0",
|
left: "2%",
|
bottom: "14",
|
containLabel: true,
|
},
|
xAxis: [{
|
type: 'category',
|
axisLine: {
|
lineStyle: {
|
width: 2,
|
color: "#B7E4F7",
|
},
|
},
|
axisTick: {
|
show: false,
|
},
|
}],
|
yAxis: [{
|
type: 'value',
|
axisLabel: {
|
formatter: "{value}",
|
color: "#CAD3E0",
|
},
|
splitLine: {
|
lineStyle: {
|
width: 2,
|
type: "dashed",
|
color: "#5A7E9D",
|
},
|
},
|
}],
|
series: [{
|
type: 'bar',
|
name: "在线时长",
|
barWidth: 14,
|
color: new this.$echarts.graphic.LinearGradient(0, 0, 0, 1, [
|
{offset: 0, color: "#C15B3C"},
|
{offset: 1, color: "#FACD91"},
|
]),
|
}],
|
}
|
const baseDropOptions = {
|
series: [{
|
type: 'liquidFill',
|
data: [0],
|
radius: '70%', //图表的大小 值是圆的直径
|
shape: 'circle', //水填充图的形状 circle默认圆形 rect圆角矩形 triangle三角形 diamond菱形 pin水滴状 arrow箭头状 还可以是svg的path
|
center: ['50%', '50%'], //图表相对于盒子的位置
|
outline: {
|
show: false
|
},
|
backgroundStyle: { // 背景配置
|
color: 'transparent', //背景颜色
|
borderWidth: '2',//边框大小
|
borderColor: 'rgba(17, 94, 176, 0.4)', // 边框颜色
|
},
|
itemStyle: {
|
color: new this.$echarts.graphic.LinearGradient(
|
0, 0, 0, 1, //4个参数用于配置渐变色的起止位置, 这4个参数依次对应右/下/左/上四个方位. 而0 0 0 1则代表渐变色从正上方开始
|
[
|
{offset: 0, color: '#2790F4'},
|
{offset: 1, color: '#165190'}
|
] //数组, 用于配置颜色的渐变过程. 每一项为一个对象, 包含offset和color两个参数. offset的范围是0 ~ 1, 用于表示位置
|
)
|
},
|
}]
|
}
|
this.chart.chartInstanceBar.setOption(baseBarOptions)
|
this.chart.chartInstanceDrop.setOption(baseDropOptions)
|
},
|
// 渲染chart内容数据(x,y,series)
|
async renderChart() {
|
const {list, total} = await this.getOnlineTime()
|
const x = [] // x轴数据
|
const y = [] // y轴数据
|
list.forEach(item => {
|
x.push(item.ctime)
|
y.push(item.ltime)
|
})
|
const baseBarOptions = {
|
xAxis: {
|
index: 0,
|
data: x
|
},
|
series: {
|
index: 0,
|
data: y
|
}
|
}
|
const baseDropOptions = {
|
series: {
|
index: 0,
|
data: [0.3],
|
label: {
|
color: '#fff',
|
fontSize: 28,
|
formatter: () => this.handleTime(total),
|
}
|
}
|
}
|
this.chart.chartInstanceBar.setOption(baseBarOptions)
|
this.chart.chartInstanceDrop.setOption(baseDropOptions)
|
},
|
// 处理秒=>天时分
|
handleTime(num) {
|
let second = num * 60
|
let day = 0
|
let hour = 0
|
let minutes = 0
|
if (second < 86400) {
|
day = 0;
|
if (second < 3600) {
|
hour = 0;
|
if (second < 60) {
|
minutes = 0;
|
} else {
|
minutes = Math.floor(second / 60);
|
}
|
} else {
|
hour = Math.floor(second / 3600);
|
if (second - hour * 3600 < 60) {
|
minutes = 0;
|
} else {
|
minutes = Math.floor((second - hour * 3600) / 60);
|
}
|
}
|
} else {
|
day = Math.floor(second / 86400);
|
if (second - 86400 * day < 3600) {
|
hour = 0;
|
minutes = Math.floor((second - 86400 * day) / 60);
|
} else {
|
hour = Math.floor((second - 86400 * day) / 3600);
|
if (second - 86400 * day - 3600 * hour < 60) {
|
minutes = 0;
|
} else {
|
minutes = Math.floor((second - 86400 * day - 3600 * hour) / 60);
|
}
|
}
|
}
|
day = day ? day + "天" : ''
|
hour = hour ? hour + "小时" : ''
|
minutes = minutes && minutes + "分"
|
return day + hour + minutes
|
},
|
rowClick(row) {
|
this.userId = row.userId
|
if (!this.chart.chartInstanceBar) {
|
this.initChart()
|
}
|
this.hideMap()
|
this.renderChart()
|
},
|
queryTable() {
|
this.getLists()
|
},
|
queryEchart() {
|
this.renderChart()
|
},
|
queryMap() {
|
|
},
|
// chart 查看按钮
|
watchMap() {
|
if (!this.userId) {
|
this.$message.warning('请先选择用户');
|
return
|
}
|
this.renderMap()
|
},
|
async renderMap() {
|
this.isShowMap = !this.isShowMap
|
let {positions, times} = await this.getTrackLists()
|
this.maps.times = times
|
this.maps.lineArr = positions
|
AMapLoader.load({
|
key: "1dc3895771d98745fa27bbcc3280464f", // 申请好的Web端开发者Key,首次调用 load 时必填
|
version: "2.0", // 指定要加载的 JSAPI 的版本,缺省时默认为 1.4.15
|
}).then(AMap => {
|
this.maps.AMap = AMap
|
AMap.plugin('AMap.MoveAnimation', () => {
|
// 创建地图
|
this.maps.map = new AMap.Map("container", {
|
resizeEnable: true,
|
viewMode: "2D", // 是否为3D地图模式
|
zoom: 17, // 初始化地图级别
|
})
|
if (positions.length) {
|
// 创建一个 (小人) Icon
|
var personIcon = new AMap.Icon({
|
image: icon, // 图标的取图地址
|
imageSize: new AMap.Size(40, 40), // 图标所用图片大小
|
})
|
// 创建marker实例
|
let mapMarker = this.maps.mapMarker = new AMap.Marker({
|
map: this.maps.map,
|
position: positions[0],
|
icon: personIcon,
|
zIndex: 10,
|
offset: new AMap.Pixel(-20, -36),
|
})
|
// marker提示框
|
let infoWindow = new AMap.InfoWindow({offset: new AMap.Pixel(0, -30)});
|
let markerMouseover = async (e) => {
|
let index = e.target.content
|
let adress = await this.getAdress(positions[index])
|
let time = times[index]
|
let el = `
|
<p><t>地址:</t><span>${adress}</span></p>
|
<p><t>时间:</t><span>${time}</span></p>
|
<p><t>经纬度:</t><span>${positions[index][0]}, ${positions[index][1]}</span></p>
|
`
|
infoWindow.setContent(el)
|
infoWindow.open(this.maps.map, e.target.getPosition())
|
}
|
// 给折线图添加拐点和绑定事件
|
for (var i = 0; i < positions.length; i += 1) {
|
let center = positions[i]
|
let icon = new AMap.Icon({
|
size: new AMap.Size(6, 11), // 图标尺寸
|
image: 'https://webapi.amap.com/theme/v1.3/markers/n/mark_b.png', // Icon的图像
|
imageSize: new AMap.Size(6, 11)
|
})
|
let marker = new AMap.Marker({
|
map: this.maps.map,
|
position: center,
|
icon: icon,
|
offset: new AMap.Pixel(-3, -10)
|
})
|
marker.content = i
|
marker.on("mouseover", markerMouseover)
|
}
|
// 绘制轨迹
|
new AMap.Polyline({
|
map: this.maps.map,
|
path: positions,
|
// showDir: true,
|
strokeColor: "#28F", //线颜色
|
strokeWeight: 3, //线宽
|
})
|
// 已通过轨迹
|
let passedPolyline = new AMap.Polyline({
|
map: this.maps.map,
|
strokeColor: "red", //线颜色
|
strokeWeight: 3, //线宽
|
})
|
mapMarker.on('moving', (e) => {
|
passedPolyline.setPath(e.passedPath);
|
this.maps.map.setCenter(e.target.getPosition(), true)
|
})
|
this.maps.map.setFitView()
|
}
|
})
|
})
|
},
|
// 获取地址
|
getAdress(lnglats) {
|
return new Promise(resolve => {
|
const AMap = this.maps.AMap
|
AMap.plugin('AMap.Geocoder', () => {
|
const geocoder = new AMap.Geocoder({
|
radius: 1000,
|
extensions: "all"
|
})
|
geocoder.getAddress(lnglats, (status, result) => {
|
let address = ''
|
if (status === 'complete' && result.info === 'OK') {
|
address = result.regeocodes ? result.regeocodes : result.regeocode.formattedAddress
|
}
|
resolve(address)
|
})
|
})
|
})
|
},
|
hideMap() {
|
this.clearMap()
|
this.isShowMap = false
|
this.maps.trackLists = []
|
this.maps.trackListIndex = ''
|
},
|
startAnimation() {
|
this.maps.mapMarker?.moveAlong(this.maps.lineArr, {
|
// 每一段的时长
|
duration: 500,//可根据实际采集时间间隔设置
|
// JSAPI2.0 是否延道路自动设置角度在 moveAlong 里设置
|
autoRotation: true,
|
})
|
},
|
pauseAnimation() {
|
this.maps.mapMarker?.pauseMove()
|
},
|
resumeAnimation() {
|
this.maps.mapMarker?.resumeMove()
|
},
|
stopAnimation() {
|
this.maps.mapMarker?.stopMove()
|
},
|
// 显示轨迹详情
|
async showTrackDetail() {
|
this.isShowTrackDetail = !this.isShowTrackDetail
|
if (this.maps.trackLists.length) {
|
return
|
}
|
if (this.maps.times.length && this.maps.lineArr.length) {
|
let tracks = []
|
let adress = await this.getAdress([...this.maps.lineArr])
|
adress.forEach((item, index) => {
|
tracks.push(`位置: ${this.maps.times[index]}(${item.formattedAddress})`)
|
})
|
this.maps.trackLists = tracks
|
}
|
},
|
trackListClick(index) {
|
this.maps.trackListIndex = index
|
},
|
screenAdapter() {
|
this.chart.chartInstanceBar?.resize()
|
this.chart.chartInstanceDrop?.resize()
|
},
|
clearMap() {
|
this.maps.mapMarker?.stopMove()
|
this.maps.map?.destroy()
|
},
|
clearChart() {
|
this.chart.chartInstanceBar?.clear()
|
this.chart.chartInstanceDrop?.clear()
|
},
|
}
|
}
|
</script>
|
|
<style lang="scss" scoped>
|
@import '@/style/layout-main.scss';
|
|
::v-deep .amap-info-content {
|
flex-direction: row;
|
width: 220px;
|
padding: 14px;
|
line-height: 20px;
|
border-radius: 5px;
|
background: #0D1846;
|
|
p {
|
display: flex;
|
width: 100%;
|
}
|
|
t {
|
width: 40px;
|
}
|
|
span {
|
flex: 1;
|
color: #39B5FE;
|
}
|
}
|
|
::v-deep .bottom-center .amap-info-sharp {
|
border-top: 8px solid #0D1846;
|
}
|
|
.active {
|
color: #39B5FE;
|
}
|
|
#container {
|
min-height: 400px;
|
flex: 1.6;
|
}
|
|
#trackDetail {
|
overflow: hidden;
|
display: flex;
|
flex-direction: column;
|
flex: 1;
|
|
h2 {
|
text-indent: 20px;
|
}
|
|
.ul_box {
|
overflow-y: auto;
|
flex: 1;
|
|
ul {
|
margin: 0;
|
padding: 0 16px;
|
}
|
|
li {
|
padding: 10px 0;
|
font-size: 15px;
|
line-height: 24px;
|
list-style: none;
|
border-bottom: 1px dashed #fff;
|
cursor: pointer;
|
}
|
}
|
}
|
|
.main {
|
display: flex;
|
flex-direction: row;
|
color: #EBEBEB;
|
|
.main_left {
|
display: flex;
|
flex-direction: column;
|
width: 380px;
|
padding: 14px;
|
margin-right: 18px;
|
border: 1px solid #39B5FE;
|
border-radius: 10px;
|
box-shadow: rgba(0, 145, 255, 0.7) 0 0 18px inset;
|
box-sizing: content-box;
|
|
.search_item {
|
span {
|
display: inline-block;
|
min-width: 54px;
|
margin-right: 6px;
|
text-align: right;
|
}
|
|
.el-input {
|
width: 220px;
|
margin: 0 28px 16px 0;
|
}
|
}
|
|
::v-deep.main_left_table {
|
overflow: hidden;
|
|
.el-table__row {
|
cursor: pointer
|
}
|
|
.el-table {
|
display: flex;
|
flex-direction: column;
|
|
.el-table__body-wrapper {
|
overflow-y: auto;
|
flex: 1;
|
}
|
}
|
}
|
}
|
|
.main_right_echart {
|
overflow: hidden;
|
display: flex;
|
flex-direction: column;
|
flex: 1;
|
padding: 20px;
|
border: 1px solid #39B5FE;
|
background-color: rgba(0, 52, 121, 0.3);
|
border-radius: 10px;
|
box-shadow: rgba(0, 145, 255, 0.7) 0 0 18px inset;
|
box-sizing: content-box;
|
|
.echart_options {
|
span {
|
display: inline-block;
|
margin-right: 6px;
|
text-align: right;
|
}
|
|
.elDatePicker {
|
width: 260px;
|
margin: 0 10px 14px 0;
|
}
|
}
|
|
.echart_bar {
|
flex: 1;
|
}
|
|
.echart_drop {
|
flex: 1.1;
|
}
|
}
|
|
.main_right_map {
|
display: flex;
|
flex-direction: column;
|
position: absolute;
|
right: 0;
|
left: 430px;
|
top: 0;
|
bottom: 0;
|
padding: 20px;
|
|
.map_option {
|
span {
|
display: inline-block;
|
margin-right: 6px;
|
}
|
|
.elDatePicker {
|
width: 260px;
|
margin: 0 10px 14px 0;
|
}
|
}
|
|
.map_view {
|
overflow: hidden;
|
display: flex;
|
flex-direction: column;
|
flex: 1;
|
|
.map_view_main {
|
overflow: hidden;
|
display: flex;
|
flex: 1;
|
}
|
|
.map_view_btn {
|
margin-top: 10px;
|
}
|
}
|
}
|
}
|
</style>
|