unknown
2023-10-23 5672f352d0ba114e2ae96c8cefad6c74ae6d2934
web/src/views/SecureManage/SmartHelmet/TrackBack.vue
@@ -1,3 +1,738 @@
<template>
    <div>轨迹回放</div>
</template>
<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>