From 5672f352d0ba114e2ae96c8cefad6c74ae6d2934 Mon Sep 17 00:00:00 2001
From: unknown <280848880@qq.com>
Date: 星期一, 23 十月 2023 10:38:28 +0800
Subject: [PATCH] 苏州-web:智能安全帽提交

---
 web/src/views/SecureManage/SmartHelmet/TrackBack.vue |  741 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++
 1 files changed, 738 insertions(+), 3 deletions(-)

diff --git a/web/src/views/SecureManage/SmartHelmet/TrackBack.vue b/web/src/views/SecureManage/SmartHelmet/TrackBack.vue
index 1939462..a943fa9 100644
--- a/web/src/views/SecureManage/SmartHelmet/TrackBack.vue
+++ b/web/src/views/SecureManage/SmartHelmet/TrackBack.vue
@@ -1,3 +1,738 @@
-<template>
-    <div>轨迹回放</div>
-</template>
\ No newline at end of file
+<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],
+                strTime: '2023-06-01',
+                endTime: '2023-06-31',
+            }
+            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],
+                strTime: '2023-06-01',
+                endTime: '2023-06-31',
+            }
+            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>
\ No newline at end of file

--
Gitblit v1.9.3