pyt
2025-04-21 dc7262f5f6131c25e9ae0a70e473e68a960ad093
Merge branch 'master' of http://120.76.84.145:10101/gitblit/r/H5/shehong-vehicle-supervision
15个文件已修改
3个文件已添加
3384 ■■■■■ 已修改文件
src/assets/loginBG.png 补丁 | 查看 | 原始文档 | blame | 历史
src/components/PlayLive/index.vue 91 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/components/PlayLive/service.js 11 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/layouts/index.vue 45 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main.js 3 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/store/index.js 2 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/utils/request.js 2 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/view/car-manage/detail.vue 379 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/view/car-manage/index.vue 11 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/view/car-manage/service.js 11 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/view/home/index.vue 2656 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/view/home/service.js 14 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/view/login/index.vue 25 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/view/order/component/detailModal.vue 85 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/view/order/index.vue 30 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/view/systemManage/user/components/addEdit.vue 2 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/view/systemManage/user/components/disb.vue 2 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/view/systemManage/user/components/resetPassWord.vue 15 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/assets/loginBG.png
src/components/PlayLive/index.vue
New file
@@ -0,0 +1,91 @@
<template>
    <div>
        <video id="video" width="100%" height="100%" muted controls></video>
    </div>
</template>
<script>
import { playDetection, closeRealVideo } from './service'
import flvjs from 'flv.js'
export default {
    props: {
        serverIp: {
            type: String,
            required: ''
        },
        serverPort: {
            type: Number,
            required: null
        },
        carId: {
            type: Number,
            required: null
        },
    },
    data() {
        return {
            flvPlayer: null,
            timer: null,
        }
    },
    mounted() {
        this.playDetection()
    },
    beforeDestroy() {
        this.destroyPlayer();
    },
    methods: {
        playDetection() {
            if (flvjs.isSupported()) {
                playDetection(this.carId).then(res => {
                    this.flvPlayer = flvjs.createPlayer({
                        type: 'flv',//视频类型
                        isLive: true,//是否为直播
                        cors: true,//是否开启跨域
                        hasAudio: false,//是否开启音频
                        hasVideo: true,//是否开启视频
                        url: `http://${this.serverIp}:${this.serverPort}/live?port=1935&app=flv&stream=${this.carId}`,  // 后端拿到的视频路径
                        enableWorker: true, //启用 Web Worker 进程来加速视频的解码和处理过程
                        enableStashBuffer: false, // 启用数据缓存机制,提高视频的流畅度和稳定性。
                        stashInitialSize: 1024, // 初始缓存大小。单位:字节。建议针对直播:调整为1024kb
                        stashInitialTime: 0.2, // 缓存初始时间。单位:秒。建议针对直播:调整为200毫秒
                        seekType: 'range', // 建议将其设置为“range”模式,以便更快地加载视频数据,提高视频的实时性。
                        lazyLoad: false, //关闭懒加载模式,从而提高视频的实时性。建议针对直播:调整为false
                        lazyLoadMaxDuration: 0.2, // 懒加载的最大时长。单位:秒。建议针对直播:调整为200毫秒
                        deferLoadAfterSourceOpen: false // 不预先加载视频数据,在 MSE(Media Source Extensions)打开后立即加载数据,提高视频的实时性。建议针对直播:调整为false
                    });
                    let video = document.getElementById('video');
                    this.flvPlayer.attachMediaElement(video); // video容器
                    this.flvPlayer.load();
                    this.flvPlayer.play().then(res => {
                        this.timer = setInterval(() => {
                            playDetection(this.carId)
                        }, 5000)
                    }).catch(err => {
                        this.destroyPlayer();
                    })
                    // 错误监听
                    this.flvPlayer.on('error', (err) => {
                        this.destroyPlayer();
                    });
                })
            }
        },
        destroyPlayer() {
            // 销毁播放器释放资源
            if (this.flvPlayer) {
                if (this.timer) clearInterval(this.timer)
                closeRealVideo(this.carId).then(res => {
                    this.flvPlayer.pause();
                    this.flvPlayer.unload();
                    this.flvPlayer.detachMediaElement();
                    this.flvPlayer.destroy();
                    this.flvPlayer = null;
                })
            }
        }
    }
}
</script>
<style></style>
src/components/PlayLive/service.js
New file
@@ -0,0 +1,11 @@
import axios from '@/utils/request';
// 通知后端开始获取视频流
export const playDetection = (id) => {
    return axios.get(`/system/car/playDetection/${id}`)
}
// 通知后端开始关闭视频流
export const closeRealVideo = (id) => {
    return axios.get(`/system/car/closeRealVideo/${id}`)
}
src/layouts/index.vue
@@ -1,6 +1,6 @@
<template>
    <div class="sticky top0 layout">
        <div class="header relative pointer">
        <div class="header relative">
            <div @click="$router.push('/home')" class="title">
                <img src="@/assets/logo.png" alt="">
                射洪“两客一危”监管平台
@@ -8,13 +8,14 @@
            <div></div>
            <div class="flex a-center pr--40">
                <div class="flex a-center mr--72">
                    <img src="@/assets/header/photo.png" class="w--32 h--32 shrink0 mr--10" />
                    <div class="fs-- 18 lh--25 color2">admin</div>
                    <img src="@/assets/header/photo.png" class="w--32 h--32 shrink0 mr--10"
                        style="border-radius: 50%;" />
                    <div class="fs-- 18 lh--25 color2">{{ $store.state.userInfo.nickName }}</div>
                </div>
                <div class="dropdown" @mouseenter="toggleDropdown(true)" @mouseleave="toggleDropdown(false)">
                    <img src="@/assets/header/more.png" class="w--16 h--16" />
                    <div v-if="isOpen" class="dropdown-menu">
                        <div class="dropdown-item" v-for="item in menuItems" :key="item.text">
                        <div @click="clickItem(item)" class="dropdown-item" v-for="item in menuItems" :key="item.text">
                            <i :class="item.icon"></i> {{ item.text }}
                        </div>
                    </div>
@@ -60,12 +61,20 @@
        <div class="main">
            <router-view></router-view>
        </div>
        <ResetPassword v-if="passwordVisible" :row="row" :dialogVisible="passwordVisible"
            @close="passwordVisible = false, row = {}" @confirm="passwordConfirm" />
    </div>
</template>
<script>
import routes from '@/router/router'
import { mapMutations } from 'vuex';
import ResetPassword from '@/view/systemManage/user/components/resetPassWord.vue'
import { updatePwd } from '@/view/systemManage/user/service'
export default {
    components: {
        ResetPassword,
    },
    data() {
        return {
            routesList: routes,
@@ -74,12 +83,36 @@
            menuItems: [
                { text: '密码设置' },
                { text: '退出登录' },
            ]
            ],
            passwordVisible: false,
            row: {}
        };
    },
    created() {
    },
    methods: {
        ...mapMutations(['clearToken']),
        passwordConfirm(form) {
            updatePwd(form).then(() => {
                this.row = {}
                this.passwordVisible = false
                this.msgsuccess('修改密码成功')
            })
        },
        clickItem(item) {
            switch (item.text) {
                case '密码设置':
                    this.row = this.$store.state.userInfo
                    this.passwordVisible = true
                    break;
                case '退出登录':
                    this.clearToken()
                    window.location.replace(`/`);
                    break;
                default:
                    break;
            }
        },
        pushPath(path) {
            this.$router.push(path)
            if (this.routerIsOpen) this.routerIsOpen = false
@@ -99,11 +132,13 @@
    display: flex;
    flex-direction: column;
    height: 100%;
    .main {
        flex: 1;
        overflow: auto;
    }
}
.bgColor1 {
    background-color: #0E6EFD;
}
src/main.js
@@ -9,13 +9,16 @@
import {
  Message
} from 'element-ui'
import PlayLive from '@/components/PlayLive'
Vue.use(ElementUI)
Vue.prototype.$cookies = cookies;
Vue.prototype.$baseURL = apiConfig.baseURL
Vue.prototype.$mapKey = apiConfig.mapKey
Vue.prototype.$secretKey = apiConfig.secretKey
Vue.prototype.$store = store
Vue.config.productionTip = false
Vue.component('PlayLive', PlayLive)
/* 全局TableHeight */
Vue.prototype.$baseTableHeight = (formType) => {
  let height = window.innerHeight
src/store/index.js
@@ -6,7 +6,7 @@
export default new Vuex.Store({
  state: {
    token: localStorage.getItem('token') || sessionStorage.getItem('token') || '',
    userInfo: localStorage.getItem('userInfo') || {}
    userInfo: JSON.parse(localStorage.getItem('userInfo')) || {}
  },
  mutations: {
    setToken(state, token) {
src/utils/request.js
@@ -10,7 +10,7 @@
const service = axios.create({
  baseURL: apiConfig.baseURL,
  withCredentials: false, // 当跨域请求时发送cookie
  timeout: 30000, // request timeout
  timeout: 60000, // request timeout
})
// 对 POST 请求参数进行排序
const sortPostParams = (params) => {
src/view/car-manage/detail.vue
@@ -7,14 +7,14 @@
                </div>
                <div class="info-content-left ml--100">
                    <el-descriptions :column="2">
                        <el-descriptions-item label="公司名称">{{detail.enterpriseName}}</el-descriptions-item>
                        <el-descriptions-item label="车牌号码">{{detail.vehicleNumber}}</el-descriptions-item>
                        <el-descriptions-item label="车牌颜色">{{detail.licensePlateColor}}</el-descriptions-item>
                        <el-descriptions-item label="车辆营运类型">{{detail.operateType}}</el-descriptions-item>
                        <el-descriptions-item label="所属车主">{{detail.driverName}}</el-descriptions-item>
                        <el-descriptions-item label="联系电话">{{detail.driverPhone}}</el-descriptions-item>
                        <el-descriptions-item label="经营区域">{{detail.operatingArea}}</el-descriptions-item>
                        <el-descriptions-item label="车辆年度审验">{{detail.annualReviewStatus}}</el-descriptions-item>
                        <el-descriptions-item label="公司名称">{{ detail.enterpriseName }}</el-descriptions-item>
                        <el-descriptions-item label="车牌号码">{{ detail.vehicleNumber }}</el-descriptions-item>
                        <el-descriptions-item label="车牌颜色">{{ detail.licensePlateColor }}</el-descriptions-item>
                        <el-descriptions-item label="车辆营运类型">{{ detail.operateType }}</el-descriptions-item>
                        <el-descriptions-item label="所属车主">{{ detail.driverName }}</el-descriptions-item>
                        <el-descriptions-item label="联系电话">{{ detail.driverPhone }}</el-descriptions-item>
                        <el-descriptions-item label="经营区域">{{ detail.operatingArea }}</el-descriptions-item>
                        <el-descriptions-item label="车辆年度审验">{{ detail.annualReviewStatus }}</el-descriptions-item>
                        <el-descriptions-item label="驾驶证" v-if="detail.drivingLicense">
                            <img class="img-size" :src="detail.drivingLicense"></img>
                        </el-descriptions-item>
@@ -36,19 +36,19 @@
                <el-tab-pane label="订单记录" name="first">
                    <div class="table-box mt--23">
                        <el-table :data="tableData" border stripe style="width: 100%">
                            <el-table-column prop="date" label="序号"></el-table-column>
                            <el-table-column prop="name" label="订单编号"></el-table-column>
                            <el-table-column prop="name" label="车牌号"></el-table-column>
                            <el-table-column prop="name" label="车辆颜色"></el-table-column>
                            <el-table-column prop="name" label="车辆所属公司"></el-table-column>
                            <el-table-column prop="name" label="上车地点"></el-table-column>
                            <el-table-column prop="name" label="下车地点"></el-table-column>
                            <el-table-column prop="name" label="载客里程"></el-table-column>
                            <el-table-column prop="name" label="驾驶员姓名"></el-table-column>
                            <el-table-column prop="name" label="驾驶员电话"></el-table-column>
                            <el-table-column prop="name" label="派单时间"></el-table-column>
                            <el-table-column prop="name" label="订单金额"></el-table-column>
                            <el-table-column prop="name" label="操作">
                            <el-table-column prop="index" label="序号"></el-table-column>
                            <el-table-column prop="code" label="订单编号"></el-table-column>
                            <el-table-column prop="vehicleNumber" label="车牌号"></el-table-column>
                            <el-table-column prop="licensePlateColor" label="车牌颜色"></el-table-column>
                            <el-table-column prop="enterpriseName" label="车辆所属公司"></el-table-column>
                            <el-table-column prop="boardingPoint" label="上车地点"></el-table-column>
                            <el-table-column prop="dropOffPoint" label="下车地点"></el-table-column>
                            <el-table-column prop="passengerMileage" label="载客里程"></el-table-column>
                            <el-table-column prop="driverName" label="驾驶员姓名"></el-table-column>
                            <el-table-column prop="driverPhone" label="驾驶员电话"></el-table-column>
                            <el-table-column prop="orderDeliveryTime" label="派单时间"></el-table-column>
                            <el-table-column prop="orderAmount" label="订单金额"></el-table-column>
                            <el-table-column prop="option" label="操作">
                                <template slot-scope="scope">
                                    <el-button type="text" @click="handle(scope.$index, scope.row)">详情</el-button>
                                </template>
@@ -66,24 +66,29 @@
                <el-tab-pane label="预警记录" name="second">
                    <div class="table-box mt--23">
                        <el-table :data="tableData" border stripe style="width: 100%">
                            <el-table-column prop="date" label="序号" fixed width="80"></el-table-column>
                            <el-table-column prop="name" label="车辆名称" width="120" fixed></el-table-column>
                            <el-table-column prop="name" label="车牌号码" width="120" fixed></el-table-column>
                            <el-table-column prop="name" label="持续报警" width="120"></el-table-column>
                            <el-table-column prop="name" label="近15分钟情况" width="120"></el-table-column>
                            <el-table-column prop="name" label="驾驶员名称" width="120"></el-table-column>
                            <el-table-column prop="name" label="所属公司" width="120"></el-table-column>
                            <el-table-column prop="name" label="终端编号" width="120"></el-table-column>
                            <el-table-column prop="name" label="开始报警时间" width="120"></el-table-column>
                            <el-table-column prop="name" label="结束报警时间" width="120"></el-table-column>
                            <el-table-column prop="name" label="持续时长" width="120"></el-table-column>
                            <el-table-column prop="name" label="持续里程数" width="120"></el-table-column>
                            <el-table-column prop="name" label="报警类型" width="120"></el-table-column>
                            <el-table-column prop="name" label="报警次数" width="120"></el-table-column>
                            <el-table-column prop="name" label="处理状态" width="120"></el-table-column>
                            <el-table-column prop="name" label="处理人" width="120"></el-table-column>
                            <el-table-column prop="name" label="处理时间" width="120"></el-table-column>
                            <el-table-column prop="name" label="处理描述" width="240"></el-table-column>
                            <el-table-column prop="index" label="序号" fixed width="80"></el-table-column>
                            <el-table-column prop="carName" label="车辆名称" width="120" fixed></el-table-column>
                            <el-table-column prop="vehicleNumber" label="车牌号码" width="120" fixed></el-table-column>
                            <el-table-column prop="keepWarn" label="持续报警" width="120"></el-table-column>
                            <el-table-column prop="name" label="近15分钟情况">
                                <template #default="{ row }">
                                    <img src="@/assets/homeImg/eye-fill.png" alt="" @click="viewDetail(row)"
                                        style="width: 30px;cursor: pointer;">
                                </template>
                            </el-table-column>
                            <el-table-column prop="driverName" label="驾驶员名称" width="120"></el-table-column>
                            <el-table-column prop="enterpriseName" label="所属公司" width="120"></el-table-column>
                            <el-table-column prop="terminalNumber" label="终端编号" width="120"></el-table-column>
                            <el-table-column prop="startTime" label="开始报警时间" width="120"></el-table-column>
                            <el-table-column prop="endTime" label="结束报警时间" width="120"></el-table-column>
                            <el-table-column prop="keepTime" label="持续时长" width="120"></el-table-column>
                            <el-table-column prop="keepDistance" label="持续里程数" width="120"></el-table-column>
                            <el-table-column prop="warnType" label="报警类型" width="120"></el-table-column>
                            <el-table-column prop="warnNumber" label="报警次数" width="120"></el-table-column>
                            <el-table-column prop="treatmentState" label="处理状态" width="120"></el-table-column>
                            <el-table-column prop="treatmentUser" label="处理人" width="120"></el-table-column>
                            <el-table-column prop="treatmentTime" label="处理时间" width="120"></el-table-column>
                            <el-table-column prop="treatmentRemark" label="处理描述" width="240"></el-table-column>
                        </el-table>
                        <div class="pagination-box relative mt--23 flex j-end">
@@ -100,8 +105,9 @@
                            <el-form :inline="true" :model="searchForm" class="demo-form-inline">
                                <el-form-item label="选择轨迹时间范围:" prop="level" class="unset_m"
                                    style="margin-right: 15px;">
                                    <el-date-picker v-model="searchForm.date" type="datetimerange" range-separator="至"
                                        start-placeholder="开始日期" end-placeholder="结束日期" value-format="yyyy-MM-dd">
                                    <el-date-picker :value-format="'yyyy-MM-dd HH:mm'" v-model="searchForm.date"
                                        type="datetimerange" range-separator="至" start-placeholder="开始日期"
                                        end-placeholder="结束日期">
                                    </el-date-picker>
                                </el-form-item>
                            </el-form>
@@ -118,15 +124,85 @@
                </el-tab-pane>
            </el-tabs>
        </div>
        <DetailModal ref="detailModal" :detail="detail"
  />
        <DetailModal ref="detailModal" :detail="detail" />
        <el-drawer :visible.sync="drawer" append-to-body :size="450" @close="closeDrawer">
            <div class="flex j-between a-center fs--20 pl--15 pr--15">
                <!-- 使用 Tailwind CSS 的内联十六进制颜色类 -->
                <div>{{ info.vehicleNumber }}<span v-if="info.warnList && info.warnList.length > 0">({{
                    info.warnList.length
                }})</span></div><i @click="closeDrawer" class="el-icon-s-unfold color1 pointer"></i>
            </div>
            <hr class="mt--10" />
            <div class="pl--15 pr--15">
                <div class="flex mt--15">
                    <div class="w--100 shrink0 color1">当前司机:</div>
                    <div class="color2">{{ info.driverName }}</div>
                </div>
                <div class="flex mt--15">
                    <div class="w--100 shrink0 color1">当前车速:</div>
                    <div class="color2">{{ info.speed }}km/h</div>
                </div>
                <div class="flex mt--15">
                    <div class="w--100 shrink0 color1">当前位置:</div>
                    <div class="color2">{{ info.nowAddress }}</div>
                </div>
                <div v-if="info.imageUrl" class="flex mt--15">
                    <div class="w--100 shrink0 color1">抓拍照片:</div>
                    <el-image style="height: 100px" :src="info.imageUrl" :preview-src-list="[info.imageUrl]">
                    </el-image>
                </div>
            </div>
            <hr class="mt--10" />
            <div class="flex j-between a-center fs--15 pl--15 pr--15 mt--15 color2">
                近15分钟报警
            </div>
            <div class="block pl--15 pr--15 mt--15">
                <el-timeline>
                    <el-timeline-item v-for="(item, index) in info.warnList" :key="index" color='#0E6EFD'
                        :timestamp="item.warnTime" placement="top">
                        <div @click="initMap1(item)">
                            <el-card class="pointer">
                                <h4>{{ item.warnType }}</h4>
                                <p class="color1">{{ item.speed }}km/h</p>
                            </el-card>
                        </div>
                    </el-timeline-item>
                </el-timeline>
            </div>
            <div v-if="showWarnDetail" class="fixed">
                <div class="card">
                    <div class="title fs--18 color2">视频信号遮挡报警</div>
                    <div id="mapContainers"></div>
                    <div class="">
                        <div class="flex mt--15">
                            <div class="w--100 shrink0 color1">司机:</div>
                            <div class="color2">{{ info.driverName }}</div>
                        </div>
                        <div class="flex mt--15">
                            <div class="w--100 shrink0 color1">车速:</div>
                            <div class="color2">{{ activeInfo.speed }}km/h</div>
                        </div>
                        <div class="flex mt--15">
                            <div class="w--100 shrink0 color1">时间:</div>
                            <div class="color2">{{ activeInfo.warnTime }}</div>
                        </div>
                        <div class="flex mt--15">
                            <div class="w--100 shrink0 color1">地点:</div>
                            <div class="color2">{{ activeInfo.address }}</div>
                        </div>
                    </div>
                </div>
            </div>
        </el-drawer>
    </div>
</template>
<script>
import AMapLoader from "@amap/amap-jsapi-loader";
import DetailModal from "./components/detailModal.vue";
import { getCarDetail,getCarOrder,getCarWarning,getCarTrack } from './service'
import { getCarDetail, getCarOrder, getCarWarning, getCarTrack, getCarVideo,getDetail } from './service'
export default {
    name: "detail",
    components: { DetailModal },
@@ -137,61 +213,146 @@
            searchForm: {
                pageCurr: 1,
                pageSize: 10,
                total: 0
                total: 0,
                date: undefined,
            },
            detail:{},
            detail: {},
            activeName: 'first',
            routeList:[],
            routeList: [],
            videoObj: {},
            drawer: false,
            showWarnDetail: false,
            info: {},
            activeInfo: {},
            map: null
        }
    },
    mounted() {
        if(this.$route.query.id){
            getCarDetail({id: this.$route.query.id}).then(res => {
        if (this.$route.query.id) {
            getCarDetail({ id: this.$route.query.id }).then(res => {
                this.detail = res;
                this.getList(res.vehicleNumber);
            })
            getCarVideo({ id: this.$route.query.id }).then(res => {
                this.videoObj = res;
            })
        }
    },
    methods: {
        closeDrawer() {
            this.drawer = false
            this.showWarnDetail = false
        },
        // 查看详情
        viewDetail(row) {
            this.drawer = true
            getDetail({ vehicleNumber: row.vehicleNumber }).then(res => {
                this.info = res
            })
        },
        // 初始化地图
        initMap1(row) {
            this.showWarnDetail = true
            this.activeInfo = row
            this.$nextTick(() => {
                window._AMapSecurityConfig = {
                    securityJsCode: this.$secretKey,
                };
                AMapLoader.load({
                    key:this.$mapKey,
                    version: "2.0",
                    plugins: [
                        "AMap.ToolBar",
                        "AMap.AutoComplete",
                        "AMap.Geocoder",
                        "AMap.MarkerCluster",
                    ],
                })
                    .then((AMap) => {
                        this.map = new AMap.Map("mapContainers", {
                            center: [row.lon, row.lat],
                            zoom: 15,
                        });
                        // 添加标记
                        new AMap.Marker({
                            position: [row.lon, row.lat],
                            map: this.map,
                            title: row.warnType
                        });
                    })
                    .catch((e) => {
                        console.log(e);
                    });
            })
        },
        handleClick(e) {
            this.activeName = e.name
            if(e.name != 'third'){
            if (e.name != 'third') {
                this.searchForm = {
                    pageCurr: 1,
                    pageSize: 10,
                    total: 0,
                }
                this.getList(this.detail.vehicleNumber)
            }else{
                getCarTrack({...this.searchForm,vehicleNumber:this.detail.vehicleNumber}).then(res => {
                  this.routeList = res;
                  if(res.length > 0){
                    this.initMap();
                  }
                //销毁地图
                if (this.map) {
                    this.map.destroy();
                }
            } else {
                getCarTrack({ ...this.searchForm, vehicleNumber: this.detail.vehicleNumber }).then(res => {
                    this.routeList = res;
                    if (res.length > 0) {
                        this.initMap();
                    }
                })
            }
        },
        search() {
            if (this.searchForm.date != undefined) {
         },
        reset() {
                this.searchForm = {
                    pageCurr: 1,
                    pageSize: 10,
                    total: 0,
                    startTime: this.searchForm.date[0],
                    endTime: this.searchForm.date[1],
                }
                getCarTrack({ ...this.searchForm, vehicleNumber: this.detail.vehicleNumber }).then(res => {
                    this.routeList = res;
                    if (res.length > 0) {
                        this.initMap();
                    }
                })
            }
        },
        getList(vehicleNumber) {
            if(this.activeName == 'first'){
                getCarOrder({...this.searchForm,vehicleNumber:vehicleNumber}).then(res => {
        reset() {
            this.searchForm = {
               pageCurr: 1,
               pageSize: 10,
               total: 0,
               date: undefined,
            }
        },
        getList(vehicleNumber) {
            if (this.activeName == 'first') {
                getCarOrder({ ...this.searchForm, vehicleNumber: vehicleNumber }).then(res => {
                    this.tableData = res.records;
                    this.searchForm.total = res.total;
                })
            }else{
                getCarWarning({...this.searchForm,vehicleNumber:vehicleNumber}).then(res => {
                    this.tableData = res.records;
                })
            } else {
                getCarWarning({ ...this.searchForm, vehicleNumber: vehicleNumber }).then(res => {
                    this.tableData = res.records;
                    this.searchForm.total = res.total;
                })
            }
        },
        showDetail() {
            this.$refs.detailModal.dialogVisible = true
        },
@@ -213,23 +374,50 @@
                    });
                    this.map.addControl(new AMap.ToolBar());
                    let path = [
                        // new AMap.LngLat(105.57, 30.51),
                        // new AMap.LngLat(116.382122, 39.901176),
                        // new AMap.LngLat(116.387271, 39.912501),
                        // new AMap.LngLat(116.398258, 39.9046),
                    ]
                    let path = []
                    const iconMap = {
                        出租车: {
                            icon: require("../../assets/homeImg/taxi.png"),
                            size: new AMap.Size(75, 37),
                        },
                        公交车: {
                            icon: require("../../assets/homeImg/bus.png"),
                            size: new AMap.Size(62, 34),
                        },
                        危险品: {
                            icon: require("../../assets/homeImg/risk.png"),
                            size: new AMap.Size(69, 32),
                        },
                        郊游: {
                            icon: require("../../assets/homeImg/outing.png"),
                            size: new AMap.Size(61, 31),
                        },
                        货运: {
                            icon: require("../../assets/homeImg/expressage.png"),
                            size: new AMap.Size(60, 31),
                        },
                        网约车: {
                            icon: require("../../assets/homeImg/online.png"),
                            size: new AMap.Size(75, 33),
                        },
                        客运: {
                            icon: require("../../assets/homeImg/passenger.png"),
                            size: new AMap.Size(69, 31),
                        },
                    };
                    this.routeList.forEach(item => {
                        path.push(new AMap.LngLat(item.longitude, item.latitude))
                    })
                    const content = `<div class="custom-content-marker">
                                        <img src="${require("@/assets/logo.png")}">
                                        <img src="${iconMap[this.detail.operateType].icon}">
                                    </div>`;
                    const marker = new AMap.Marker({
                        content: content, //自定义点标记覆盖物内容
                        position: [105.57, 30.51], //基点位置
                        offset: new AMap.Pixel(-30, -15), //相对于基点的偏移位置
                        size: iconMap[this.detail.operateType].size,
                    });
                    this.map.add(marker);
                    let polyline = new AMap.Polyline({
@@ -322,4 +510,47 @@
    width: 100%;
    height: 600px;
}
#mapContainers {
    width: 100%;
    height: 500px;
    margin: 20px 0;
}
::v-deep .el-drawer__body {
    position: relative;
    .fixed {
        right: 470px;
        top: 0;
        bottom: 0;
        margin: auto;
        display: flex;
        align-items: center;
        .card {
            background: #fff;
            width: 500px;
            max-height: 1000px;
            padding: 15px;
        }
    }
}
.color1 {
    color: #0E6EFD;
}
.color2 {
    color: rgb(52, 52, 52);
}
::v-deep .pagination-popper {
    position: fixed !important;
    transform:
        scale(calc(1 / var(--scale))) translate(calc(100px * (1 - 1 / var(--scale))),
            calc(5px * (1 - 1 / var(--scale)))) !important;
    transform-origin: right top !important;
    right: calc(30px * (1 - 1 / var(--scale))) !important;
    margin-top: 5px;
    min-width: 100px !important;
}
</style>
src/view/car-manage/index.vue
@@ -52,10 +52,6 @@
                    size="small">重置</el-button>
            </div>
        </div>
        <div class="table-box-btn mt--23 ml--30">
            <el-button class="search-button h--40 w--90 fs--14" icon="el-icon-top" type="primary" size="small"
                @click="exportExcell">导出</el-button>
        </div>
        <div class="table-box ml--30 mt--23 mr--30">
            <el-table :data="tableData" border stripe style="width: 100%">
                <el-table-column type="index" label="序号" width="60"></el-table-column>
@@ -125,6 +121,13 @@
        getCarType().then(res => {
            this.options = res;
        });
        // 判断URL参数并赋值
        const query = this.$route.query;
        if (query && Object.keys(query).length > 0) {
            if(query.id){
                this.searchForm.operationType = Number(query.id);
            }
        }
        this.getList();
    },
    methods: {
src/view/car-manage/service.js
@@ -29,4 +29,15 @@
//获取车辆详情行程轨迹
export const getCarTrack = (data) => {
    return axios.get(`/system/car/getCarTravel`, {params:data}) 
}
//获取车辆详情实时视频
export const getCarVideo = (data) => {
    return axios.get(`/system/car/getRealVideo/${data.id}`)
}
// 获取车辆预警详情
export const getDetail = (params) => {
    console.log(params)
    return axios.get('/system/warn/getCarWarnInfo', {params})
}
src/view/home/index.vue
@@ -1,1275 +1,1383 @@
<template>
  <div class="flex homePage">
    <!-- 头部 -->
    <div class="mapTop">
      <!-- 车辆统计 -->
      <div class="carCount">
        <div class="title">车辆统计</div>
        <div class="fir">
          <div
            class="countCard"
            v-for="(item, index) in carCountData.slice(0, 3)"
            :key="item.id"
          >
            <img class="iconImg" :src="imgList[index]" />
            <div>
              <div class="name">{{ item.name || "" }}(辆)</div>
              <div class="num">{{ item.carNum || 0 }}</div>
            </div>
          </div>
        </div>
        <div class="sec">
          <div
            class="countCard"
            v-for="(item, index) in carCountData.slice(3, 7)"
            :key="item.id"
          >
            <img class="iconImg" :src="imgList[index + 3]" />
            <div>
              <div class="name">{{ item.name || "" }}(辆)</div>
              <div class="num">{{ item.carNum || 0 }}</div>
            </div>
          </div>
        </div>
      </div>
      <!-- 车辆状态 -->
      <div class="carStatus">
        <div class="title">车辆状态</div>
        <div class="statusFir">
          <div class="statusCard">
            <div class="statusLeft">
              <div class="name">在线</div>
              <div class="num">{{ carStatusData.online || 0 }}</div>
            </div>
            <el-progress
              type="circle"
              :width="20"
              :show-text="false"
              stroke-linecap="butt"
              :percentage="carStatusData.onlinePercent"
              color="rgba(91, 143, 249, 1)"
              define-back-color="rgba(91, 143, 249, 0.25)"
              class="progressCard"
            ></el-progress>
          </div>
          <div class="statusLine"></div>
          <div class="statusCard">
            <div class="statusLeft">
              <div class="name">离线</div>
              <div class="num">{{ carStatusData.offline || 0 }}</div>
            </div>
            <el-progress
              type="circle"
              :width="20"
              :show-text="false"
              stroke-linecap="butt"
              :percentage="carStatusData.offlinePercent"
              color="rgba(93, 112, 146, 1)"
              define-back-color="rgba(93, 112, 146, 0.25)"
              class="progressCard"
            ></el-progress>
          </div>
        </div>
        <div class="statusSec">
          <div class="statusCard">
            <div class="statusLeft">
              <div class="name">故障</div>
              <div class="num">{{ carStatusData.breakdown || 0 }}</div>
            </div>
            <el-progress
              type="circle"
              :width="20"
              :show-text="false"
              stroke-linecap="butt"
              :percentage="carStatusData.breakdownPercent"
              color="rgba(253, 83, 118, 1)"
              define-back-color="rgba(253, 83, 118, 0.25)"
              class="progressCard"
            ></el-progress>
          </div>
          <div class="statusLine"></div>
          <div class="statusCard">
            <div class="statusLeft">
              <div class="name">异常</div>
              <div class="num">{{ carStatusData.abnormal || 0 }}</div>
            </div>
            <el-progress
              type="circle"
              :width="20"
              :show-text="false"
              stroke-linecap="butt"
              :percentage="carStatusData.abnormalPercent"
              color="rgba(246, 189, 22, 1)"
              define-back-color="rgba(246, 189, 22, 0.25)"
              class="progressCard"
            ></el-progress>
          </div>
        </div>
      </div>
    </div>
    <!-- 左边 地图 -->
    <div class="leftMap">
      <div class="mapContainer" id="mapContainer"></div>
    </div>
    <!-- 右边 内容 -->
    <div class="right">
      <div class="firCard">
        <div class="companyCard">
          <div class="lineCard"></div>
          <div class="name">运营公司(家)</div>
          <div class="value">{{ carStatusData.enterprise || 0 }}</div>
        </div>
        <div class="companyCard">
          <div class="lineCard"></div>
          <div class="name">运营车辆(辆)</div>
          <div class="value">{{ carStatusData.car || 0 }}</div>
        </div>
        <div class="companyCard">
          <div class="lineCard"></div>
          <div class="name">驾驶员(人)</div>
          <div class="value">{{ carStatusData.driver || 0 }}</div>
        </div>
      </div>
      <!-- 今日预警 -->
      <div class="todayWarn">
        <div class="title">今日预警</div>
        <div class="warnList" v-if="warnList.length > 0">
          <div
            class="warnItem"
            v-for="(item, index) in warnList"
            :key="index"
            :class="
              item.warnLevel
                ? ['oneWarn', 'twoWarn', 'threeWarn', 'fourWarn'][
                    item.warnLevel - 1
                  ]
                : 'fiveWarn'
            "
          >
            <div class="grade">
              {{
                item.warnLevel
                  ? ["一级", "二级", "三级", "四级"][item.warnLevel - 1]
                  : "-"
              }}
            </div>
            <div class="info">
              {{ item.vehicleNumber  }} {{ item.warnType  }}
              {{ item.keepTime }} {{ item.startTime }}
            </div>
          </div>
        </div>
        <div class="noData" v-else>
          <el-empty description="暂无数据" :image-size="80"></el-empty>
        </div>
      </div>
      <!-- 预警情况统计 -->
      <div class="warnCount">
        <div class="title">预警情况统计</div>
        <div class="countChart" id="countChart"></div>
        <div class="noData" v-if="countList.length == 0">
          <el-empty description="暂无数据" :image-size="80"></el-empty>
        </div>
      </div>
      <!-- 预警排行榜(前10) -->
      <div class="warnRank">
        <div class="title">预警排行榜(前10)</div>
        <div class="rankChart" id="rankChart">
          <div class="rankItem" v-for="(item, index) in rankList" :key="index">
            <div class="left">{{ item.name }}</div>
            <div
              class="rankRight"
              :class="[0, 1, 2].includes(index) ? 'rankColor' : ''"
            >
              <div class="rank" :style="{ width: item.percentage + '%' }"></div>
            </div>
          </div>
        </div>
        <div class="noData" v-if="rankList.length == 0">
          <el-empty description="暂无数据" :image-size="80"></el-empty>
        </div>
      </div>
    </div>
  </div>
</template>
<script>
import * as echarts from "echarts";
import html2canvas from "html2canvas";
import AMapLoader from "@amap/amap-jsapi-loader";
import flvjs from "flv.js";
import {
  getCarCount,
  getCarStatusCount,
  getMapCarList,
  getCarWarnList,
  getWarnGroupCount,
  getWarnGroupCountTop10,
  getCarInfoById,
} from "./service";
export default {
  data() {
    return {
      flvPlayer: null,
      activeIndex: "1",
      activeIndex2: "1",
      timer: null,
      markers: [],
      map: null,
      AMap: null,
      infoWindow: null,
      imgList: [
        require("../../assets//homeImg/img1.png"),
        require("../../assets//homeImg/img2.png"),
        require("../../assets//homeImg/img3.png"),
        require("../../assets//homeImg/img4.png"),
        require("../../assets//homeImg/img5.png"),
        require("../../assets//homeImg/img6.png"),
        require("../../assets//homeImg/img7.png"),
      ],
      countList: [], //预警情况统计数据
      rankList: [], //预警排行榜数据
      carList: [], //车辆列表数据
      carCountData: [], //车辆统计数据
      carStatusData: {}, //车辆状态数据
      warnList: [], //预警列表数据
    };
  },
  watch: {
    map(val) {
      if (val) {
        HTMLCanvasElement.prototype.getContext = (function (origFn) {
          return function (type, attributes) {
            if (type.indexOf("webgl") > -1) {
              attributes = Object.assign({}, attributes, {
                preserveDrawingBuffer: true,
              });
            }
            return origFn.call(this, type, attributes);
          };
        })(HTMLCanvasElement.prototype.getContext);
      }
    },
  },
  created() {
    window.toCarDetail = (record) => {
      this.toCarDetail(record);
    };
    window.fullScreen = () => {
      this.fullScreen();
    };
    window.shotScreen = () => {
      this.shotScreen();
    };
  },
  mounted() {
    // 调用所有接口
    this.getCarCountData();
    this.getCarStatusData();
    // this.getMapCarData(); // 移除这里的调用,因为 initMap 中会调用
    this.getWarnListData();
    this.getWarnGroupData();
    this.getWarnTop10Data();
    this.initMap();
    // 设置定时器,每分钟刷新一次数据
    this.timer = setInterval(() => {
      this.getCarCountData();
      this.getCarStatusData();
      this.getMapCarData(); // 保留定时器中的调用
      this.getWarnListData();
      this.getWarnGroupData();
      this.getWarnTop10Data();
    }, 60000);
  },
  beforeDestroy() {
    if (this.timer) {
      clearInterval(this.timer);
    }
    if (this.markers && this.markers.length > 0) {
      this.markers.forEach((marker) => {
        marker.setMap(null);
      });
      this.markers = [];
    }
    if (this.infoWindow) {
      this.infoWindow.close();
    }
    if (this.flvPlayer) {
      this.flvPlayer.destroy();
      this.flvPlayer = null;
    }
  },
  beforeRouteLeave(to, from, next) {
    if (this.infoWindow) {
      this.infoWindow.close();
    }
    if (this.flvPlayer) {
      this.flvPlayer.destroy();
      this.flvPlayer = null;
    }
    next();
  },
  methods: {
    // 获取车辆统计数据
    async getCarCountData() {
      try {
        const res = await getCarCount();
        this.carCountData = res;
      } catch (error) {
        this.$message.error("获取车辆统计数据失败");
      }
    },
    // 获取车辆状态数据
    async getCarStatusData() {
      try {
        const res = await getCarStatusCount();
        // 设置默认值为0,防止空值
        const online = Number(res.online) || 0;
        const offline = Number(res.offline) || 0;
        const breakdown = Number(res.breakdown) || 0;
        const abnormal = Number(res.abnormal) || 0;
        const enterprise = Number(res.enterprise) || 0;
        const car = Number(res.car) || 0;
        const driver = Number(res.driver) || 0;
        // 计算总数
        const total = online + offline + breakdown + abnormal;
        // 计算百分比,如果总数为0则百分比为0
        const onlinePercent =
          total > 0 ? Math.round((online / total) * 100) : 0;
        const offlinePercent =
          total > 0 ? Math.round((offline / total) * 100) : 0;
        const breakdownPercent =
          total > 0 ? Math.round((breakdown / total) * 100) : 0;
        const abnormalPercent =
          total > 0 ? Math.round((abnormal / total) * 100) : 0;
        // 更新数据
        this.carStatusData = {
          online,
          offline,
          breakdown,
          abnormal,
          enterprise,
          car,
          driver,
          total,
          onlinePercent,
          offlinePercent,
          breakdownPercent,
          abnormalPercent,
        };
      } catch (error) {
        this.$message.error("获取车辆状态数据失败");
        // 设置默认值
        this.carStatusData = {
          online: 0,
          offline: 0,
          breakdown: 0,
          abnormal: 0,
          enterprise: 0,
          car: 0,
          driver: 0,
          total: 0,
          onlinePercent: 0,
          offlinePercent: 0,
          breakdownPercent: 0,
          abnormalPercent: 0,
        };
      }
    },
    // 获取地图车辆数据
    async getMapCarData() {
      try {
        const res = await getMapCarList();
        this.carList = res;
        // 确保地图已初始化后再更新标记
        if (this.AMap && this.map) {
          this.updateMarkers(res);
        }
      } catch (error) {
        this.$message.error("获取地图车辆数据失败");
      }
    },
    // 获取预警列表数据
    async getWarnListData() {
      try {
        const res = await getCarWarnList();
        this.warnList = res.records;
      } catch (error) {
        this.$message.error("获取预警列表数据失败");
      }
    },
    // 获取预警统计情况数据
    async getWarnGroupData() {
      try {
        const res = await getWarnGroupCount();
        this.countList = res;
        this.getCountList();
      } catch (error) {
        this.$message.error("获取预警统计情况数据失败");
      }
    },
    // 获取预警排行数据
    async getWarnTop10Data() {
      try {
        const res = await getWarnGroupCountTop10();
        // 判断返回的数组是否为空
        if (!res || res.length === 0) {
          this.rankList = [];
          return;
        }
        // 计算所有num的总和
        const total = res.reduce((sum, item) => sum + (item.num || 0), 0);
        // 为每个数据项添加百分比属性
        this.rankList = res.map((item) => ({
          ...item,
          percentage:
            total > 0 ? (((item.num || 0) / total) * 100).toFixed(2) : 0,
        }));
      } catch (error) {
        this.$message.error("获取预警排行数据失败");
        this.rankList = [];
      }
    },
    // 初始化地图
    initMap() {
      window._AMapSecurityConfig = {
        securityJsCode: "37ce61ae86efa5ad82b649a277f5097c",
      };
      AMapLoader.load({
        key: "67968c82f27c7e2cb9f40c1a9aa3042b",
        version: "2.0",
        plugins: [
          "AMap.ToolBar",
          "AMap.AutoComplete",
          "AMap.Geocoder",
          "AMap.MarkerCluster",
        ],
      })
        .then((AMap) => {
          this.AMap = AMap;
          this.map = new AMap.Map("mapContainer", {
            center: [105.574542, 30.5061493],
            zoom: 8,
          });
          this.infoWindow = new AMap.InfoWindow({
            offset: new AMap.Pixel(30, 30),
            autoMove: true,
            anchor: "top-center",
          });
          this.getMapCarData();
        })
        .catch((e) => {
          this.$message.error("地图加载失败");
        });
    },
    // 更新地图标记
    updateMarkers(arr) {
      if (!this.AMap) {
        return;
      }
      if (this.markers && this.markers.length > 0) {
        this.markers.forEach((marker) => {
          marker.setMap(null);
        });
        this.markers = [];
      }
      if (arr.length > 0) {
        const iconMap = {
          出租车: {
            icon: require("../../assets/homeImg/taxi.png"),
            size: new this.AMap.Size(75, 37),
          },
          公交车: {
            icon: require("../../assets/homeImg/bus.png"),
            size: new this.AMap.Size(62, 34),
          },
          危险品: {
            icon: require("../../assets/homeImg/risk.png"),
            size: new this.AMap.Size(69, 32),
          },
          郊游: {
            icon: require("../../assets/homeImg/outing.png"),
            size: new this.AMap.Size(61, 31),
          },
          货运: {
            icon: require("../../assets/homeImg/expressage.png"),
            size: new this.AMap.Size(60, 31),
          },
          网约车: {
            icon: require("../../assets/homeImg/online.png"),
            size: new this.AMap.Size(75, 33),
          },
          客运: {
            icon: require("../../assets/homeImg/passenger.png"),
            size: new this.AMap.Size(69, 31),
          },
        };
        arr.forEach((item, index) => {
          // 检查必要字段
          if (!item.operateType || !item.longitude || !item.latitude) {
            return;
          }
          const iconConfig = iconMap[item.operateType];
          if (!iconConfig) {
            return;
          }
          let marker = new this.AMap.Marker({
            position: [Number(item.longitude), Number(item.latitude)],
            map: this.map,
            icon: new this.AMap.Icon({
              size: iconConfig.size,
              image: iconConfig.icon,
              imageSize: iconConfig.size,
              imageOffset: new this.AMap.Pixel(0, 0),
            }),
          });
          // 添加点击事件
          marker.on("click", async (e) => {
            // 如果已经有视频在播放,先销毁
            if (this.flvPlayer) {
              this.flvPlayer.destroy();
              this.flvPlayer = null;
            }
            // 显示loading
            this.infoWindow.setContent(
              '<div style="padding: 20px;text-align: center;">加载中...</div>'
            );
            this.infoWindow.open(this.map, e.target.getPosition());
            try {
              // 并行请求车辆信息和视频地址
              const [carInfoRes, videoRes] = await Promise.all([
                this.getCarInfo(item.id),
                this.getVideoUrl(item.id),
              ]);
              // 更新弹窗内容
              this.infoWindow.setContent(
                this.listRender({
                  ...item,
                  ...carInfoRes.data,
                  videoUrl: videoRes.data.url,
                })
              );
              if (flvjs.isSupported()) {
                this.flvPlayer = flvjs.createPlayer({
                  type: "flv",
                  isLive: true,
                  cors: true,
                  hasAudio: true,
                  hasVideo: true,
                  url: videoRes.data.url,
                  enableWorker: true,
                  enableStashBuffer: false,
                  seekType: "range",
                });
                let video = document.getElementById("monitoringCard");
                this.flvPlayer.attachMediaElement(video);
                this.flvPlayer.load();
                this.flvPlayer.play();
              }
            } catch (error) {
              this.infoWindow.setContent(
                '<div style="padding: 20px;text-align: center;color: red;">获取车辆信息失败</div>'
              );
            }
          });
          // 将marker添加到数组中
          this.markers.push(marker);
        });
      }
    },
    // 获取车辆信息
    async getCarInfo(carId) {
      try {
        const res = await getCarInfoById({ id: carId });
        if (res) {
          return {
            data: {
              ...res,
              id: carId,
            },
          };
        }
        return {
          data: {
            id: carId,
          },
        };
      } catch (error) {
        return {
          data: {
            licensePlate: "",
            driver: "",
            location: "",
            coordinates: "",
            speed: "0km/h",
            drivingTime: "0小时0分钟",
          },
        };
      }
    },
    // 获取视频地址
    async getVideoUrl(carId) {
      // TODO: 替换为实际的API调用
      return new Promise((resolve) => {
        setTimeout(() => {
          resolve({
            data: {
              url: "https://interactive-examples.mdn.mozilla.net/media/cc0-videos/flower.mp4",
            },
          });
        }, 500);
      });
    },
    listRender(record) {
      return `<div style="background: #ffffff; padding: 24px 20px;z-index: 999">
        <div style="position: relative; width: 460px; height: 330px">
          <video
            crossorigin="anonymous"
            style="width: 460px; height: 330px; border-radius: 9px"
            id="monitoringCard"
            :controls="false"
            autoplay
            src="${record.videoUrl}"
            width="620">
          </video>
          <div style="position: absolute; right: 11px; top: 10px">
            <div style="display: flex;flex-direction: column;align-items: center;justify-content: center;
              background: #ffffff; padding: 3px 10px; border-radius: 6px;margin-bottom: 10px;" onclick="fullScreen()">
              <img style="width: 20px; height: 20px" src="${require("../../assets//homeImg/full.png")}" />
              <div style="font-size: 12px;font-weight: 400; line-height: 17px; color: rgba(0, 0, 0, 0.88);">全屏</div>
            </div>
            <div style="display: flex;flex-direction: column;align-items: center; justify-content: center;
              background: #ffffff;padding: 3px 10px;border-radius: 6px;" onclick="shotScreen()">
              <img style="width: 20px; height: 20px" src="${require("../../assets//homeImg/slot.png")}" />
              <div style="font-size: 12px; font-weight: 400; line-height: 17px; color: rgba(0, 0, 0, 0.88);">截屏</div>
            </div>
          </div>
        </div>
        <div style="display: flex;justify-content: space-between;margin-top: 15px;margin-bottom: 12px;">
          <div style="font-weight: 500;font-size: 18px;color: rgba(0, 0, 0, 0.85);line-height: 25px;">车牌号:${
            record.vehicleNumber || ""
          }</div>
          <div style="font-weight: 500; font-size: 18px;color: rgba(0, 0, 0, 0.85);line-height: 25px;">驾驶员:${
            record.driverName||''
          }</div>
        </div>
        <div style="display: flex; justify-content: space-between">
          <div style="font-weight: 500; font-size: 14px; color: rgba(0, 0, 0, 0.65);line-height: 26px;">位置:${
            record.location
          }</div>
          <div style="font-weight: 500;font-size: 14px; color: rgba(0, 0, 0, 0.65);line-height: 26px;">经纬度:${
            record.longitude +',' + record.latitude
          }</div>
        </div>
        <div style="display: flex; justify-content: space-between">
          <div style="font-weight: 500;font-size: 14px; color: rgba(0, 0, 0, 0.65);line-height: 26px;">当前时速:${
            record.speed
          }</div>
          <div style="font-weight: 500;font-size: 14px; color: rgba(0, 0, 0, 0.65);line-height: 26px;">驾驶时长:${
            record.drivingTime
          }</div>
        </div>
        <div style="margin-top: 14px;display: flex;justify-content: flex-end;align-items: center;cursor: pointer;" onclick="toCarDetail(${
          record.id
        })">
          <div style="font-weight: 400;font-size: 18px; color: rgba(22, 119, 255, 1);line-height: 25px;">车辆详情</div>
          <img style="width:18px;height: 18px;margin-left: 8px;" src="${require("../../assets//homeImg/right.png")}" />
        </div>
      </div>`;
    },
    // 获取预警情况统计
    getCountList() {
      echarts.dispose(document.getElementById("countChart"));
      if (this.countList.length > 0) {
        this.chartTnit();
      }
    },
    chartTnit() {
      // 基于准备好的dom,初始化echarts实例
      const myChart = echarts.init(document.getElementById("countChart"));
      // 绘制数量图表
      myChart.setOption({
        tooltip: {
          trigger: "axis",
          axisPointer: {
            type: "shadow",
          },
        },
        grid: {
          width: "auto",
          height: "auto",
          top: "5%",
          left: "3%",
          right: "4%",
          bottom: "0%",
          containLabel: true,
        },
        xAxis: [
          {
            type: "category",
            data: this.countList.map((item) => item.warnType),
            axisTick: {
              alignWithLabel: true,
              lineStyle: {
                color: "#777777",
              },
            },
            axisLabel: {
              color: "rgba(0, 0, 0, 0.45)",
            },
          },
        ],
        yAxis: [
          {
            type: "value",
          },
        ],
        series: [
          {
            type: "bar",
            barWidth: "20px",
            itemStyle: {
              borderRadius: [20, 20, 20, 20],
              color: (params) => {
                return ["#5B8FF9", "#5AD8A6", "#F6BD16", "#6DC8EC", "#945FB9"][
                  params.dataIndex
                ];
              },
            },
            data: this.countList.map((item) => item.num),
          },
        ],
      });
      myChart.resize();
    },
    // 跳转车辆详情
    toCarDetail(id) {
      this.$router.push({
        path: "/car-detail",
        query: {
          id: id,
        },
      });
    },
    fullScreen() {
      const video = document.getElementById("monitoringCard");
      if (video.requestFullscreen) {
        video.requestFullscreen();
      } else if (video.mozRequestFullScreen) {
        // Firefox
        video.mozRequestFullScreen();
      } else if (video.webkitRequestFullscreen) {
        // Chrome, Safari and Opera
        video.webkitRequestFullscreen();
      } else if (video.msRequestFullscreen) {
        // IE/Edge
        video.msRequestFullscreen();
      }
    },
    shotScreen() {
      // 获取video和canvas元素
      const video = document.getElementById("monitoringCard");
      const canvas = document.getElementById("myCanvas");
      // 设置canvas的宽度和高度与video相同
      canvas.width = video.videoWidth;
      canvas.height = video.videoHeight;
      // 获取canvas的2d绘图上下文
      const context = canvas.getContext("2d");
      // 将当前video帧绘制到canvas上
      context.drawImage(video, 0, 0, canvas.width, canvas.height);
      setTimeout(() => {
        // 将canvas内容转换为图片
        let dataURL = canvas.toDataURL("image/png");
        this.downloadImage(dataURL);
      }, 100);
    },
    downloadImage(base64) {
      const link = document.createElement("a");
      link.href = base64;
      link.download = "screenshot.png"; // 你希望下载的文件名
      document.body.appendChild(link);
      link.click();
      document.body.removeChild(link);
    },
  },
};
</script>
<style scoped lang="less">
.homePage {
  display: flex;
  height: 100vh;
  position: relative;
  .leftMap {
    // width: 100%;
    height: 100%;
    flex: 1;
    display: flex;
    position: relative;
    #mapContainer {
      flex: 1;
      width: 100%;
      height: 100%;
    }
  }
  .mapTop {
    z-index: 99;
    position: absolute;
    top: 20px;
    left: 20px;
    right: 513px;
    display: flex;
    justify-content: space-between;
    width: calc(100% - 570px);
    .title {
      font-weight: 600;
      font-size: 18px;
      color: #000000;
      line-height: 25px;
      text-transform: none;
      text-align: center;
    }
    .carCount {
      flex: 8;
      background: #ffffff;
      box-shadow: 0px 0px 10px 0px rgba(0, 0, 0, 0.2);
      border-radius: 10px;
      padding: 14px 20px 20px 20px;
      margin-right: 20px;
      .fir {
        display: flex;
        justify-content: space-between;
        margin-bottom: 10px;
        margin-top: 11px;
        .countCard {
          margin-right: 20px;
        }
        .countCard:last-child {
          margin-right: none;
        }
      }
      .sec {
        display: flex;
        justify-content: space-between;
        .countCard {
          margin-right: 15px;
        }
        .countCard:last-child {
          margin-right: none;
        }
      }
      .countCard {
        flex: 1;
        background: linear-gradient(
          180deg,
          rgba(246, 246, 252, 0) 0%,
          #f3f4f8 100%
        );
        box-shadow: inset 0px -1px 4px 0px #ffffff;
        border-radius: 10px;
        border: 1px solid #f1f1f1;
        padding: 14px 20px 18px 14px;
        display: flex;
        justify-content: space-between;
        .iconImg {
          width: 30px;
          height: 30px;
          border-radius: 10px;
        }
        .iconImg:nth-child(1) {
          box-shadow: 0px 7px 14px 0px rgba(14, 110, 253, 0.4);
        }
        .iconImg:nth-child(2) {
          box-shadow: 0px 7px 14px 0px rgba(255, 102, 39, 0.5);
        }
        .iconImg:nth-child(3) {
          box-shadow: 0px 7px 14px 0px rgba(254, 41, 94, 0.5);
        }
        .iconImg:nth-child(4) {
          box-shadow: 0px 7px 14px 0px rgba(248, 204, 65, 0.5);
        }
        .iconImg:nth-child(5) {
          box-shadow: 0px 7px 14px 0px rgba(2, 179, 118, 0.5);
        }
        .iconImg:nth-child(6) {
          box-shadow: 0px 7px 14px 0px rgba(169, 14, 253, 0.4);
        }
        .iconImg:nth-child(7) {
          box-shadow: 0px 7px 14px 0px rgba(109, 200, 236, 0.5);
        }
        .name {
          font-weight: 500;
          font-size: 12px;
          color: rgba(0, 0, 0, 0.6);
          line-height: 17px;
          margin-top: 2px;
        }
        .num {
          font-weight: 900;
          font-size: 16px;
          color: rgba(0, 0, 0, 0.8);
          line-height: 19px;
          text-align: right;
          margin-top: 1px;
        }
      }
    }
    .carStatus {
      flex: 5;
      background: #ffffff;
      box-shadow: 0px 0px 10px 0px rgba(0, 0, 0, 0.2);
      border-radius: 10px;
      padding: 14px 84px 20px 84px;
      .statusFir {
        margin-top: 23px;
        margin-bottom: 28px;
        display: flex;
        justify-content: space-between;
        align-content: center;
      }
      .statusSec {
        display: flex;
        justify-content: space-between;
        align-content: center;
      }
      .statusCard {
        display: flex;
        align-content: center;
        .statusLeft {
          margin-right: 9px;
          display: flex;
          flex-direction: column;
          justify-content: center;
          .name {
            font-weight: 500;
            font-size: 12px;
            color: rgba(0, 0, 0, 0.6);
            line-height: 17px;
          }
          .num {
            font-weight: 900;
            font-size: 16px;
            color: rgba(0, 0, 0, 0.8);
            line-height: 19px;
          }
        }
        .progressCard {
          width: 56px;
          height: 56px;
          ::v-deep .el-progress-circle {
            width: 56px !important;
            height: 56px !important;
          }
        }
      }
      .statusLine {
        width: 1px;
        height: 46px;
        border: 1px solid #dedede;
        box-sizing: border-box;
      }
    }
  }
  .right {
    width: 493px;
    height: calc(100% - 20px);
    margin: 20px 17px 0 20px;
    background: #ffffff;
    box-shadow: 0px 0px 10px 0px rgba(0, 0, 0, 0.2);
    border-radius: 10px 10px 0px 0px;
    padding: 20px;
    .title {
      margin-top: 30px;
      font-weight: 600;
      font-size: 18px;
      color: rgba(0, 0, 0, 0.88);
      line-height: 25px;
      text-transform: none;
      margin-bottom: 10px;
    }
    .firCard {
      display: flex;
      justify-content: space-between;
      .companyCard {
        width: 140px;
        height: 90px;
        background: #f4f4ff;
        border-radius: 0px 10px 10px 0px;
        position: relative;
        .lineCard {
          position: absolute;
          left: 0;
          top: 0;
          width: 4px;
          height: 90px;
          background: #0e6efd;
          border-radius: 2px;
        }
        .name {
          margin: 18px 0 12px 19px;
          font-size: 12px;
          color: rgba(0, 0, 0, 0.6);
          line-height: 17px;
        }
        .value {
          margin-bottom: 22px;
          text-align: center;
          font-weight: 900;
          font-size: 18px;
          color: #0e6efd;
          line-height: 21px;
        }
      }
    }
    .todayWarn {
      .warnList {
        height: 134px;
        overflow-y: auto;
        .warnItem {
          height: 26px;
          background: rgba(39, 129, 255, 0.06);
          border-radius: 4px;
          padding: 2px;
          display: flex;
          align-content: center;
          margin-bottom: 10px;
          .grade {
            height: 22px;
            background: #e6f4ff;
            border-radius: 4px;
            border: 1px solid #bae0ff;
            margin-right: 11px;
            box-sizing: border-box;
            padding: 1px 8px;
            font-weight: 400;
            font-size: 12px;
            color: #1677ff;
            line-height: 20px;
          }
          .info {
            font-size: 12px;
            color: rgba(0, 0, 0, 0.8);
            line-height: 26px;
          }
        }
        .warnItem:last-child {
          margin-bottom: 0 !important;
        }
        .oneWarn {
          background: rgba(255, 77, 79, 0.06);
          .grade {
            background: #fff1f0;
            border: 1px solid #ffccc7;
            color: rgba(255, 77, 79, 1);
          }
        }
        .twoWarn {
          background: rgba(250, 173, 20, 0.06);
          .grade {
            background: #fffbe6;
            border-radius: 4px;
            border: 1px solid #fff1b8;
            color: rgba(250, 173, 20, 1);
          }
        }
        .threeWarn {
          background: rgba(39, 129, 255, 0.06);
          .grade {
            background: #e6f4ff;
            border: 1px solid #bae0ff;
            color: #1677ff;
          }
        }
        .fourWarn {
          background: rgba(82, 196, 26, 0.06);
          .grade {
            background: #f6ffed;
            border-radius: 4px;
            border: 1px solid #d9f7be;
            color: rgba(82, 196, 26, 1);
          }
        }
        .fiveWarn {
          background: rgba(214, 219, 228, 0.3);
          .grade {
            background: rgba(214, 219, 228, 0.3);
            border-radius: 4px;
            border: 1px solid rgba(214, 219, 228, 0.6);
            color: rgba(0, 0, 0, 0.8);
          }
        }
      }
      .warnList::-webkit-scrollbar {
        width: 8px;
        height: 8px;
        background-color: #f5f5f5;
      }
      .warnList::-webkit-scrollbar-thumb {
        background-color: #ccc;
        border-radius: 4px;
      }
      .warnList::-webkit-scrollbar-thumb:hover {
        background-color: #aaa;
      }
    }
    .warnCount {
      position: relative;
      #countChart {
        width: 453px;
        height: 150px;
      }
      .noData {
        display: flex;
        flex-direction: column;
        align-content: center;
        justify-content: center;
        position: absolute;
        bottom: 0;
        left: 0;
        width: 100%;
        height: 150px;
      }
    }
    .warnRank {
      position: relative;
      .rankChart {
        width: 453px;
        height: 300px;
        .rankItem {
          display: flex;
          align-content: center;
          margin-bottom: 14px;
          .left {
            flex: 2;
            padding-right: 25px;
            font-weight: 400;
            font-size: 12px;
            color: rgba(0, 0, 0, 0.45);
            line-height: 17px;
            text-align: right;
          }
          .rankRight {
            flex: 5;
            height: 15px;
            background: rgba(93, 112, 146, 0.2);
            border-radius: 8px;
            .rank {
              height: 15px;
              border-radius: 8px;
              background: rgba(93, 112, 146, 0.6);
            }
          }
          .rankColor {
            background: rgba(91, 143, 249, 0.2);
            .rank {
              background: rgba(91, 143, 249, 0.85);
            }
          }
        }
        .rankItem:last-child {
          margin-bottom: 0 !important;
        }
      }
      .noData {
        display: flex;
        flex-direction: column;
        align-content: center;
        justify-content: center;
        position: absolute;
        bottom: 0;
        left: 0;
        width: 100%;
        height: 300px;
      }
    }
  }
}
<template>
  <div class="flex homePage">
    <!-- 头部 -->
    <div class="mapTop">
      <!-- 车辆统计 -->
      <div class="carCount">
        <div class="title">车辆统计</div>
        <div class="fir">
          <div
            class="countCard"
            v-for="(item, index) in carCountData.slice(0, 3)"
            :key="item.id"
            @click="toCarManage(item.id)"
          >
            <img class="iconImg" :src="imgList[index]" />
            <div>
              <div class="name">{{ item.name || "" }}(辆)</div>
              <div class="num">{{ item.carNum || 0 }}</div>
            </div>
          </div>
        </div>
        <div class="sec">
          <div
            class="countCard"
            v-for="(item, index) in carCountData.slice(3, 7)"
            @click="toCarManage(item.id)"
            :key="item.id"
          >
            <img class="iconImg" :src="imgList[index + 3]" />
            <div>
              <div class="name">{{ item.name || "" }}(辆)</div>
              <div class="num">{{ item.carNum || 0 }}</div>
            </div>
          </div>
        </div>
      </div>
      <!-- 车辆状态 -->
      <div class="carStatus">
        <div class="title">车辆状态</div>
        <div class="statusFir">
          <div class="statusCard">
            <div class="statusLeft">
              <div class="name">在线</div>
              <div class="num">{{ carStatusData.online || 0 }}</div>
            </div>
            <el-progress
              type="circle"
              :width="20"
              :show-text="false"
              stroke-linecap="butt"
              :percentage="carStatusData.onlinePercent"
              color="rgba(91, 143, 249, 1)"
              define-back-color="rgba(91, 143, 249, 0.25)"
              class="progressCard"
            ></el-progress>
          </div>
          <div class="statusLine"></div>
          <div class="statusCard">
            <div class="statusLeft">
              <div class="name">离线</div>
              <div class="num">{{ carStatusData.offline || 0 }}</div>
            </div>
            <el-progress
              type="circle"
              :width="20"
              :show-text="false"
              stroke-linecap="butt"
              :percentage="carStatusData.offlinePercent"
              color="rgba(93, 112, 146, 1)"
              define-back-color="rgba(93, 112, 146, 0.25)"
              class="progressCard"
            ></el-progress>
          </div>
        </div>
        <div class="statusSec">
          <div class="statusCard">
            <div class="statusLeft">
              <div class="name">故障</div>
              <div class="num">{{ carStatusData.breakdown || 0 }}</div>
            </div>
            <el-progress
              type="circle"
              :width="20"
              :show-text="false"
              stroke-linecap="butt"
              :percentage="carStatusData.breakdownPercent"
              color="rgba(253, 83, 118, 1)"
              define-back-color="rgba(253, 83, 118, 0.25)"
              class="progressCard"
            ></el-progress>
          </div>
          <div class="statusLine"></div>
          <div class="statusCard">
            <div class="statusLeft">
              <div class="name">异常</div>
              <div class="num">{{ carStatusData.abnormal || 0 }}</div>
            </div>
            <el-progress
              type="circle"
              :width="20"
              :show-text="false"
              stroke-linecap="butt"
              :percentage="carStatusData.abnormalPercent"
              color="rgba(246, 189, 22, 1)"
              define-back-color="rgba(246, 189, 22, 0.25)"
              class="progressCard"
            ></el-progress>
          </div>
        </div>
      </div>
    </div>
    <!-- 左边 地图 -->
    <div class="leftMap">
      <div class="mapContainer" id="mapContainer"></div>
    </div>
    <!-- 右边 内容 -->
    <div class="right">
      <div class="firCard">
        <div class="companyCard">
          <div class="lineCard"></div>
          <div class="name">运营公司(家)</div>
          <div class="value">{{ carStatusData.enterprise || 0 }}</div>
        </div>
        <div class="companyCard">
          <div class="lineCard"></div>
          <div class="name">运营车辆(辆)</div>
          <div class="value">{{ carStatusData.car || 0 }}</div>
        </div>
        <div class="companyCard">
          <div class="lineCard"></div>
          <div class="name">驾驶员(人)</div>
          <div class="value">{{ carStatusData.driver || 0 }}</div>
        </div>
      </div>
      <!-- 今日预警 -->
      <div class="todayWarn">
        <div class="title">今日预警</div>
        <div class="warnList" v-if="warnList.length > 0">
          <div
            class="warnItem"
            v-for="(item, index) in warnList"
            :key="index"
            :class="
              item.warnLevel
                ? ['oneWarn', 'twoWarn', 'threeWarn', 'fourWarn'][
                    item.warnLevel - 1
                  ]
                : 'fiveWarn'
            "
          >
            <div class="grade">
              {{
                item.warnLevel
                  ? ["一级", "二级", "三级", "四级"][item.warnLevel - 1]
                  : "-"
              }}
            </div>
            <div class="info">
              {{ item.vehicleNumber }} {{ item.warnType }} {{ item.keepTime }}
              {{ item.startTime }}
            </div>
          </div>
        </div>
        <div class="noData" v-else>
          <el-empty description="暂无数据" :image-size="80"></el-empty>
        </div>
      </div>
      <!-- 预警情况统计 -->
      <div class="warnCount">
        <div class="title">预警情况统计</div>
        <div class="countChart" id="countChart"></div>
        <div class="noData" v-if="countList.length == 0">
          <el-empty description="暂无数据" :image-size="80"></el-empty>
        </div>
      </div>
      <!-- 预警排行榜(前10) -->
      <div class="warnRank">
        <div class="title">预警排行榜(前10)</div>
        <div class="rankChart" id="rankChart">
          <div class="rankItem" v-for="(item, index) in rankList" :key="index">
            <div class="left">{{ item.name }}</div>
            <div
              class="rankRight"
              :class="[0, 1, 2].includes(index) ? 'rankColor' : ''"
            >
              <div class="rank" :style="{ width: item.percentage + '%' }"></div>
            </div>
          </div>
        </div>
        <div class="noData" v-if="rankList.length == 0">
          <el-empty description="暂无数据" :image-size="80"></el-empty>
        </div>
      </div>
    </div>
  </div>
</template>
<script>
import * as echarts from "echarts";
import html2canvas from "html2canvas";
import AMapLoader from "@amap/amap-jsapi-loader";
import flvjs from "flv.js";
import {
  getCarCount,
  getCarStatusCount,
  getMapCarList,
  getCarWarnList,
  getWarnGroupCount,
  getWarnGroupCountTop10,
  getRealVideo,
  playDetection,
  closeRealVideo,
} from "./service";
export default {
  data() {
    return {
      flvPlayer: null,
      videoTimer: null,
      activeIndex: "1",
      activeIndex2: "1",
      timer: null,
      markers: [],
      map: null,
      AMap: null,
      infoWindow: null,
      imgList: [
        require("../../assets//homeImg/img1.png"),
        require("../../assets//homeImg/img2.png"),
        require("../../assets//homeImg/img3.png"),
        require("../../assets//homeImg/img4.png"),
        require("../../assets//homeImg/img5.png"),
        require("../../assets//homeImg/img6.png"),
        require("../../assets//homeImg/img7.png"),
      ],
      countList: [], //预警情况统计数据
      rankList: [], //预警排行榜数据
      carList: [], //车辆列表数据
      carCountData: [], //车辆统计数据
      carStatusData: {}, //车辆状态数据
      warnList: [], //预警列表数据
      serverIp: "", //监控ip
      serverPort: "", //监控端口
      carId: "", //监控车辆
    };
  },
  watch: {
    map(val) {
      if (val) {
        HTMLCanvasElement.prototype.getContext = (function (origFn) {
          return function (type, attributes) {
            if (type.indexOf("webgl") > -1) {
              attributes = Object.assign({}, attributes, {
                preserveDrawingBuffer: true,
              });
            }
            return origFn.call(this, type, attributes);
          };
        })(HTMLCanvasElement.prototype.getContext);
      }
    },
  },
  filters: {},
  created() {
    window.toCarDetail = (record) => {
      this.toCarDetail(record);
    };
    window.fullScreen = () => {
      this.fullScreen();
    };
    window.shotScreen = () => {
      this.shotScreen();
    };
  },
  mounted() {
    // 调用所有接口
    this.getCarCountData();
    this.getCarStatusData();
    // this.getMapCarData(); // 移除这里的调用,因为 initMap 中会调用
    this.getWarnListData();
    this.getWarnGroupData();
    this.getWarnTop10Data();
    this.initMap();
    // 设置定时器,每分钟刷新一次数据
    this.timer = setInterval(() => {
      this.getCarCountData();
      this.getCarStatusData();
      this.getMapCarData(); // 保留定时器中的调用
      this.getWarnListData();
      this.getWarnGroupData();
      this.getWarnTop10Data();
    }, 60000);
  },
  beforeDestroy() {
    if (this.timer) {
      clearInterval(this.timer);
    }
    this.destroyPlayer();
    if (this.markers && this.markers.length > 0) {
      this.markers.forEach((marker) => {
        marker.setMap(null);
      });
      this.markers = [];
    }
    if (this.infoWindow) {
      this.infoWindow.close();
    }
    if (this.flvPlayer) {
      this.flvPlayer.destroy();
      this.flvPlayer = null;
    }
  },
  beforeRouteLeave(to, from, next) {
    if (this.infoWindow) {
      this.infoWindow.close();
    }
    if (this.flvPlayer) {
      this.flvPlayer.destroy();
      this.flvPlayer = null;
    }
    next();
  },
  methods: {
    toCarManage(id) {
      this.$router.push({
        path: "/car-manage",
        query: { id },
      });
    },
    // 获取车辆统计数据
    async getCarCountData() {
      try {
        const res = await getCarCount();
        this.carCountData = res;
      } catch (error) {
        this.$message.error("获取车辆统计数据失败");
      }
    },
    // 获取车辆状态数据
    async getCarStatusData() {
      try {
        const res = await getCarStatusCount();
        // 设置默认值为0,防止空值
        const online = Number(res.online) || 0;
        const offline = Number(res.offline) || 0;
        const breakdown = Number(res.breakdown) || 0;
        const abnormal = Number(res.abnormal) || 0;
        const enterprise = Number(res.enterprise) || 0;
        const car = Number(res.car) || 0;
        const driver = Number(res.driver) || 0;
        // 计算总数
        const total = online + offline + breakdown + abnormal;
        // 计算百分比,如果总数为0则百分比为0
        const onlinePercent =
          total > 0 ? Math.round((online / total) * 100) : 0;
        const offlinePercent =
          total > 0 ? Math.round((offline / total) * 100) : 0;
        const breakdownPercent =
          total > 0 ? Math.round((breakdown / total) * 100) : 0;
        const abnormalPercent =
          total > 0 ? Math.round((abnormal / total) * 100) : 0;
        // 更新数据
        this.carStatusData = {
          online,
          offline,
          breakdown,
          abnormal,
          enterprise,
          car,
          driver,
          total,
          onlinePercent,
          offlinePercent,
          breakdownPercent,
          abnormalPercent,
        };
      } catch (error) {
        this.$message.error("获取车辆状态数据失败");
        // 设置默认值
        this.carStatusData = {
          online: 0,
          offline: 0,
          breakdown: 0,
          abnormal: 0,
          enterprise: 0,
          car: 0,
          driver: 0,
          total: 0,
          onlinePercent: 0,
          offlinePercent: 0,
          breakdownPercent: 0,
          abnormalPercent: 0,
        };
      }
    },
    // 获取地图车辆数据
    async getMapCarData() {
      try {
        const res = await getMapCarList();
        this.carList = res;
        // 确保地图已初始化后再更新标记
        if (this.AMap && this.map) {
          this.updateMarkers(res);
        }
      } catch (error) {
        this.$message.error("获取地图车辆数据失败");
      }
    },
    // 获取预警列表数据
    async getWarnListData() {
      try {
        const res = await getCarWarnList();
        this.warnList = res.records;
      } catch (error) {
        this.$message.error("获取预警列表数据失败");
      }
    },
    // 获取预警统计情况数据
    async getWarnGroupData() {
      try {
        const res = await getWarnGroupCount();
        this.countList = res;
        this.getCountList();
      } catch (error) {
        this.$message.error("获取预警统计情况数据失败");
      }
    },
    // 获取预警排行数据
    async getWarnTop10Data() {
      try {
        const res = await getWarnGroupCountTop10();
        // 判断返回的数组是否为空
        if (!res || res.length === 0) {
          this.rankList = [];
          return;
        }
        // 计算所有num的总和
        const total = res.reduce((sum, item) => sum + (item.num || 0), 0);
        // 为每个数据项添加百分比属性
        this.rankList = res.map((item) => ({
          ...item,
          percentage:
            total > 0 ? (((item.num || 0) / total) * 100).toFixed(2) : 0,
        }));
      } catch (error) {
        this.$message.error("获取预警排行数据失败");
        this.rankList = [];
      }
    },
    // 初始化地图
    initMap() {
      window._AMapSecurityConfig = {
        securityJsCode: this.$secretKey,
      };
      AMapLoader.load({
        key: this.$mapKey,
        version: "2.0",
        plugins: [
          "AMap.ToolBar",
          "AMap.AutoComplete",
          "AMap.Geocoder",
          "AMap.MarkerCluster",
          "AMap.Geocoder",
        ],
      })
        .then((AMap) => {
          this.AMap = AMap;
          this.map = new AMap.Map("mapContainer", {
            center: [105.574542, 30.5061493],
            zoom: 8,
          });
          this.infoWindow = new AMap.InfoWindow({
            offset: new AMap.Pixel(30, 30),
            autoMove: true,
            anchor: "top-center",
          });
          // 添加信息弹窗关闭事件监听
          this.infoWindow.on("close", () => {
            console.log("关闭信息弹窗1111111111111111111");
            this.destroyPlayer();
          });
          this.getMapCarData();
        })
        .catch((e) => {
          this.$message.error("地图加载失败");
        });
    },
    // 更新地图标记
    updateMarkers(arr) {
      if (!this.AMap) {
        return;
      }
      if (this.markers && this.markers.length > 0) {
        this.markers.forEach((marker) => {
          marker.setMap(null);
        });
        this.markers = [];
      }
      if (arr.length > 0) {
        const iconMap = {
          出租车: {
            icon: require("../../assets/homeImg/taxi.png"),
            size: new this.AMap.Size(75, 37),
          },
          公交车: {
            icon: require("../../assets/homeImg/bus.png"),
            size: new this.AMap.Size(62, 34),
          },
          危险品: {
            icon: require("../../assets/homeImg/risk.png"),
            size: new this.AMap.Size(69, 32),
          },
          郊游: {
            icon: require("../../assets/homeImg/outing.png"),
            size: new this.AMap.Size(61, 31),
          },
          货运: {
            icon: require("../../assets/homeImg/expressage.png"),
            size: new this.AMap.Size(60, 31),
          },
          网约车: {
            icon: require("../../assets/homeImg/online.png"),
            size: new this.AMap.Size(75, 33),
          },
          客运: {
            icon: require("../../assets/homeImg/passenger.png"),
            size: new this.AMap.Size(69, 31),
          },
        };
        arr.forEach((item, index) => {
          // 检查必要字段
          if (!item.operateType || !item.longitude || !item.latitude) {
            return;
          }
          const iconConfig = iconMap[item.operateType];
          if (!iconConfig) {
            return;
          }
          let marker = new this.AMap.Marker({
            position: [Number(item.longitude), Number(item.latitude)],
            map: this.map,
            icon: new this.AMap.Icon({
              size: iconConfig.size,
              image: iconConfig.icon,
              imageSize: iconConfig.size,
              imageOffset: new this.AMap.Pixel(0, 0),
            }),
          });
          // 添加点击事件
          marker.on("click", async (e) => {
            // 如果已经有视频在播放,先销毁
            if (this.flvPlayer) {
              this.flvPlayer.destroy();
              this.flvPlayer = null;
            }
            // 显示loading
            this.infoWindow.setContent(
              '<div style="padding: 20px;text-align: center;">加载中...</div>'
            );
            this.infoWindow.open(this.map, e.target.getPosition());
            try {
              // 使用高德地图API获取地址信息
              const geocoder = new this.AMap.Geocoder();
              const location = [Number(item.longitude), Number(item.latitude)];
              const [addressResult, videoRes] = await Promise.all([
                new Promise((resolve) => {
                  geocoder.getAddress(location, (status, result) => {
                    if (status === "complete" && result.regeocode) {
                      resolve(result.regeocode.formattedAddress);
                    } else {
                      resolve("未知地址");
                    }
                  });
                }),
                this.getVideoUrl(item.id),
              ]);
              // 更新弹窗内容
              this.infoWindow.setContent(
                this.listRender({
                  ...item,
                  drivingTime: this.formatterTime(item.drivingTime || 0),
                  location: addressResult,
                })
              );
              this.initVideoPlayer();
            } catch (error) {
              this.infoWindow.setContent(
                '<div style="padding: 20px;text-align: center;color: red;">获取车辆信息失败</div>'
              );
            }
          });
          // 将marker添加到数组中
          this.markers.push(marker);
        });
      }
    },
    // 获取视频地址
    async getVideoUrl(carId) {
      try {
        const res = await getRealVideo({ id: carId });
        // 将RTSP流转换为FLV流
        this.serverIp = res.serverIp;
        this.serverPort = res.serverPort;
        this.carId = carId;
      } catch (error) {
        console.error("获取视频地址失败", error);
        return {};
      }
    },
    // 初始化视频播放器
    initVideoPlayer(videoUrl) {
      console.log('11111',this.serverIp,'2222222222',this.serverPort)
      // 先销毁之前的播放器
      if (this.flvPlayer) {
        this.flvPlayer.destroy();
        this.flvPlayer = null;
      }
      // 获取video元素
      const video = document.getElementById("monitoringCard");
      if (!video) {
        console.error("Video element not found");
        return;
      }
      // 检查flv.js是否支持
      if (flvjs.isSupported()) {
        try {
          playDetection(this.carId).then((res) => {
            this.flvPlayer = flvjs.createPlayer({
              type: "flv", //视频类型
              isLive: true, //是否为直播
              cors: true, //是否开启跨域
              hasAudio: false, //是否开启音频
              hasVideo: true, //是否开启视频
              url: `http://${this.serverIp}:${this.serverPort}/live?port=1935&app=flv&stream=${this.carId}`, // 后端拿到的视频路径
              enableWorker: true, //启用 Web Worker 进程来加速视频的解码和处理过程
              enableStashBuffer: false, // 启用数据缓存机制,提高视频的流畅度和稳定性。
              stashInitialSize: 1024, // 初始缓存大小。单位:字节。建议针对直播:调整为1024kb
              stashInitialTime: 0.2, // 缓存初始时间。单位:秒。建议针对直播:调整为200毫秒
              seekType: "range", // 建议将其设置为“range”模式,以便更快地加载视频数据,提高视频的实时性。
              lazyLoad: false, //关闭懒加载模式,从而提高视频的实时性。建议针对直播:调整为false
              lazyLoadMaxDuration: 0.2, // 懒加载的最大时长。单位:秒。建议针对直播:调整为200毫秒
              deferLoadAfterSourceOpen: false, // 不预先加载视频数据,在 MSE(Media Source Extensions)打开后立即加载数据,提高视频的实时性。建议针对直播:调整为false
            });
            let video = document.getElementById("monitoringCard");
            this.flvPlayer.attachMediaElement(video); // video容器
            this.flvPlayer.load();
            this.flvPlayer
              .play()
              .then((res) => {
                this.videoTimer = setInterval(() => {
                  playDetection(this.carId);
                }, 5000);
              })
              .catch((err) => {
                this.destroyPlayer();
              });
            // 错误监听
            this.flvPlayer.on("error", (err) => {
              this.destroyPlayer();
            });
          });
        } catch (error) {
          console.error("创建播放器失败:", error);
        }
      } else {
        console.error("当前浏览器不支持flv.js");
      }
    },
    destroyPlayer() {
      // 销毁播放器释放资源
      if (this.flvPlayer) {
        if (this.videoTimer) clearInterval(this.videoTimer);
        closeRealVideo(this.carId).then((res) => {
          this.flvPlayer.pause();
          this.flvPlayer.unload();
          this.flvPlayer.detachMediaElement();
          this.flvPlayer.destroy();
          this.flvPlayer = null;
        });
      }
    },
    // 处理视频错误
    handleVideoError(event) {
      console.error("视频加载失败", event);
      if (this.flvPlayer) {
        this.flvPlayer.destroy();
        this.flvPlayer = null;
      }
      this.infoWindow.setContent(
        '<div style="padding: 20px;text-align: center;color: red;">视频加载失败,请稍后重试</div>'
      );
    },
    listRender(record) {
      return `<div style="background: #ffffff; padding: 24px 20px;z-index: 999">
        <div style="position: relative; width: 460px; height: 330px">
          <video
              ref="video"
               style="width: 460px; height: 330px; border-radius: 9px"
               id="monitoringCard"
               ref="monitoringCard"
                :controls="false"
                autoPlay
                width="620">
              </video>
          <canvas id="myCanvas" style="display:none"></canvas>
          <div style="position: absolute; right: 11px; top: 10px">
            <div style="display: flex;flex-direction: column;align-items: center;justify-content: center;
              background: #ffffff; padding: 3px 10px; border-radius: 6px;margin-bottom: 10px;" onclick="fullScreen()">
              <img style="width: 20px; height: 20px" src="${require("../../assets//homeImg/full.png")}" />
              <div style="font-size: 12px;font-weight: 400; line-height: 17px; color: rgba(0, 0, 0, 0.88);">全屏</div>
            </div>
            <div style="display: flex;flex-direction: column;align-items: center; justify-content: center;
              background: #ffffff;padding: 3px 10px;border-radius: 6px;" onclick="shotScreen()">
              <img style="width: 20px; height: 20px" src="${require("../../assets//homeImg/slot.png")}" />
              <div style="font-size: 12px; font-weight: 400; line-height: 17px; color: rgba(0, 0, 0, 0.88);">截屏</div>
            </div>
          </div>
        </div>
        <div style="display: flex;justify-content: space-between;margin-top: 15px;margin-bottom: 12px;">
          <div style="font-weight: 500;font-size: 18px;color: rgba(0, 0, 0, 0.85);line-height: 25px;">车牌号:${
            record.vehicleNumber || ""
          }</div>
          <div style="font-weight: 500; font-size: 18px;color: rgba(0, 0, 0, 0.85);line-height: 25px;">驾驶员:${
            record.driverName || ""
          }</div>
        </div>
        <div style="display: flex; justify-content: space-between">
          <div style="font-weight: 500; font-size: 14px; color: rgba(0, 0, 0, 0.65);line-height: 26px;width: 200px;overflow: hidden;text-overflow: ellipsis;white-space: nowrap;" title="${
            record.location
          }">位置:${record.location}</div>
          <div style="font-weight: 500;font-size: 14px; color: rgba(0, 0, 0, 0.65);line-height: 26px;">经纬度:${
            record.longitude + "," + record.latitude
          }</div>
        </div>
        <div style="display: flex; justify-content: space-between">
          <div style="font-weight: 500;font-size: 14px; color: rgba(0, 0, 0, 0.65);line-height: 26px;">当前时速:${
            record.speed || ""
          }${record.speed && "km/h"}</div>
          <div style="font-weight: 500;font-size: 14px; color: rgba(0, 0, 0, 0.65);line-height: 26px;">驾驶时长:${
            record.drivingTime
          }</div>
        </div>
        <div style="margin-top: 14px;display: flex;justify-content: flex-end;align-items: center;cursor: pointer;" onclick="toCarDetail(${
          record.id
        })">
          <div style="font-weight: 400;font-size: 18px; color: rgba(22, 119, 255, 1);line-height: 25px;">车辆详情</div>
          <img style="width:18px;height: 18px;margin-left: 8px;" src="${require("../../assets//homeImg/right.png")}" />
        </div>
      </div>`;
    },
    formatterTime(value) {
      if (!value) return "";
      const hours = Math.floor(value / 60);
      const minutes = value % 60;
      if (hours > 0) {
        return `${hours}小时${minutes}分钟`;
      } else {
        return `${minutes}分钟`;
      }
    },
    // 获取预警情况统计
    getCountList() {
      echarts.dispose(document.getElementById("countChart"));
      if (this.countList.length > 0) {
        this.chartTnit();
      }
    },
    chartTnit() {
      // 基于准备好的dom,初始化echarts实例
      const myChart = echarts.init(document.getElementById("countChart"));
      // 绘制数量图表
      myChart.setOption({
        tooltip: {
          trigger: "axis",
          axisPointer: {
            type: "shadow",
          },
        },
        grid: {
          width: "auto",
          height: "auto",
          top: "5%",
          left: "3%",
          right: "4%",
          bottom: "0%",
          containLabel: true,
        },
        xAxis: [
          {
            type: "category",
            data: this.countList.map((item) => item.warnType),
            axisTick: {
              alignWithLabel: true,
              lineStyle: {
                color: "#777777",
              },
            },
            axisLabel: {
              color: "rgba(0, 0, 0, 0.45)",
            },
          },
        ],
        yAxis: [
          {
            type: "value",
          },
        ],
        series: [
          {
            type: "bar",
            barWidth: "20px",
            itemStyle: {
              borderRadius: [20, 20, 20, 20],
              color: (params) => {
                return [
                  "#5B8FF9",
                  "#5AD8A6",
                  "#F6BD16",
                  "#6DC8EC",
                  "#945FB9",
                  "rgba(248, 204, 65, 0.5)",
                  "rgba(2, 179, 118, 0.5)",
                  "rgba(254, 41, 94, 0.5)",
                  "rgba(255, 102, 39, 0.5)",
                  "rgba(169, 14, 253, 0.5)",
                  "rgba(109, 200, 236, 0.5)",
                ][params.dataIndex];
              },
            },
            data: this.countList.map((item) => item.num),
          },
        ],
      });
      myChart.resize();
    },
    // 跳转车辆详情
    toCarDetail(id) {
      this.$router.push({
        path: "/car-detail",
        query: {
          id: id,
        },
      });
    },
    fullScreen() {
      const video = document.getElementById("monitoringCard");
      if (video.requestFullscreen) {
        video.requestFullscreen();
      } else if (video.mozRequestFullScreen) {
        // Firefox
        video.mozRequestFullScreen();
      } else if (video.webkitRequestFullscreen) {
        // Chrome, Safari and Opera
        video.webkitRequestFullscreen();
      } else if (video.msRequestFullscreen) {
        // IE/Edge
        video.msRequestFullscreen();
      }
    },
    getRandomColor() {
      const letters = "0123456789ABCDEF";
      let color = "#";
      for (let i = 0; i < 6; i++) {
        color += letters[Math.floor(Math.random() * 16)];
      }
      return color;
    },
    shotScreen() {
      // 获取video和canvas元素
      const video = document.getElementById("monitoringCard");
      const canvas = document.getElementById("myCanvas");
      // 设置canvas的宽度和高度与video相同
      canvas.width = video.videoWidth;
      canvas.height = video.videoHeight;
      // 获取canvas的2d绘图上下文
      const context = canvas.getContext("2d");
      // 将当前video帧绘制到canvas上
      context.drawImage(video, 0, 0, canvas.width, canvas.height);
      setTimeout(() => {
        // 将canvas内容转换为图片
        let dataURL = canvas.toDataURL("image/png");
        this.downloadImage(dataURL);
      }, 100);
    },
    downloadImage(base64) {
      const link = document.createElement("a");
      link.href = base64;
      link.download = "screenshot.png"; // 你希望下载的文件名
      document.body.appendChild(link);
      link.click();
      document.body.removeChild(link);
    },
  },
};
</script>
<style scoped lang="less">
.homePage {
  display: flex;
  height: 100vh;
  position: relative;
  .leftMap {
    // width: 100%;
    height: 100%;
    flex: 1;
    display: flex;
    position: relative;
    #mapContainer {
      flex: 1;
      width: 100%;
      height: 100%;
    }
  }
  .mapTop {
    z-index: 99;
    position: absolute;
    top: 20px;
    left: 20px;
    right: 513px;
    display: flex;
    justify-content: space-between;
    width: calc(100% - 570px);
    .title {
      font-weight: 600;
      font-size: 18px;
      color: #000000;
      line-height: 25px;
      text-transform: none;
      text-align: center;
    }
    .carCount {
      flex: 8;
      background: #ffffff;
      box-shadow: 0px 0px 10px 0px rgba(0, 0, 0, 0.2);
      border-radius: 10px;
      padding: 14px 20px 20px 20px;
      margin-right: 20px;
      .fir {
        display: flex;
        justify-content: space-between;
        margin-bottom: 10px;
        margin-top: 11px;
        .countCard {
          margin-right: 20px;
        }
        .countCard:last-child {
          margin-right: none;
        }
      }
      .sec {
        display: flex;
        justify-content: space-between;
        .countCard {
          margin-right: 15px;
        }
        .countCard:last-child {
          margin-right: none;
        }
      }
      .countCard {
        flex: 1;
        background: linear-gradient(
          180deg,
          rgba(246, 246, 252, 0) 0%,
          #f3f4f8 100%
        );
        box-shadow: inset 0px -1px 4px 0px #ffffff;
        border-radius: 10px;
        border: 1px solid #f1f1f1;
        padding: 14px 20px 18px 14px;
        display: flex;
        justify-content: space-between;
        .iconImg {
          width: 30px;
          height: 30px;
          border-radius: 10px;
        }
        .iconImg:nth-child(1) {
          box-shadow: 0px 7px 14px 0px rgba(14, 110, 253, 0.4);
        }
        .iconImg:nth-child(2) {
          box-shadow: 0px 7px 14px 0px rgba(255, 102, 39, 0.5);
        }
        .iconImg:nth-child(3) {
          box-shadow: 0px 7px 14px 0px rgba(254, 41, 94, 0.5);
        }
        .iconImg:nth-child(4) {
          box-shadow: 0px 7px 14px 0px rgba(248, 204, 65, 0.5);
        }
        .iconImg:nth-child(5) {
          box-shadow: 0px 7px 14px 0px rgba(2, 179, 118, 0.5);
        }
        .iconImg:nth-child(6) {
          box-shadow: 0px 7px 14px 0px rgba(169, 14, 253, 0.4);
        }
        .iconImg:nth-child(7) {
          box-shadow: 0px 7px 14px 0px rgba(109, 200, 236, 0.5);
        }
        .name {
          font-weight: 500;
          font-size: 12px;
          color: rgba(0, 0, 0, 0.6);
          line-height: 17px;
          margin-top: 2px;
        }
        .num {
          font-weight: 900;
          font-size: 16px;
          color: rgba(0, 0, 0, 0.8);
          line-height: 19px;
          text-align: right;
          margin-top: 1px;
        }
      }
    }
    .carStatus {
      flex: 5;
      background: #ffffff;
      box-shadow: 0px 0px 10px 0px rgba(0, 0, 0, 0.2);
      border-radius: 10px;
      padding: 14px 84px 20px 84px;
      .statusFir {
        margin-top: 23px;
        margin-bottom: 28px;
        display: flex;
        justify-content: space-between;
        align-content: center;
      }
      .statusSec {
        display: flex;
        justify-content: space-between;
        align-content: center;
      }
      .statusCard {
        display: flex;
        align-content: center;
        .statusLeft {
          margin-right: 9px;
          display: flex;
          flex-direction: column;
          justify-content: center;
          .name {
            font-weight: 500;
            font-size: 12px;
            color: rgba(0, 0, 0, 0.6);
            line-height: 17px;
          }
          .num {
            font-weight: 900;
            font-size: 16px;
            color: rgba(0, 0, 0, 0.8);
            line-height: 19px;
          }
        }
        .progressCard {
          width: 56px;
          height: 56px;
          ::v-deep .el-progress-circle {
            width: 56px !important;
            height: 56px !important;
          }
        }
      }
      .statusLine {
        width: 1px;
        height: 46px;
        border: 1px solid #dedede;
        box-sizing: border-box;
      }
    }
  }
  .right {
    width: 493px;
    height: calc(100% - 20px);
    margin: 20px 17px 0 20px;
    background: #ffffff;
    box-shadow: 0px 0px 10px 0px rgba(0, 0, 0, 0.2);
    border-radius: 10px 10px 0px 0px;
    padding: 20px;
    .title {
      margin-top: 30px;
      font-weight: 600;
      font-size: 18px;
      color: rgba(0, 0, 0, 0.88);
      line-height: 25px;
      text-transform: none;
      margin-bottom: 10px;
    }
    .firCard {
      display: flex;
      justify-content: space-between;
      .companyCard {
        width: 140px;
        height: 90px;
        background: #f4f4ff;
        border-radius: 0px 10px 10px 0px;
        position: relative;
        .lineCard {
          position: absolute;
          left: 0;
          top: 0;
          width: 4px;
          height: 90px;
          background: #0e6efd;
          border-radius: 2px;
        }
        .name {
          margin: 18px 0 12px 19px;
          font-size: 12px;
          color: rgba(0, 0, 0, 0.6);
          line-height: 17px;
        }
        .value {
          margin-bottom: 22px;
          text-align: center;
          font-weight: 900;
          font-size: 18px;
          color: #0e6efd;
          line-height: 21px;
        }
      }
    }
    .todayWarn {
      .warnList {
        height: 134px;
        overflow-y: auto;
        .warnItem {
          height: 26px;
          background: rgba(39, 129, 255, 0.06);
          border-radius: 4px;
          padding: 2px;
          display: flex;
          align-content: center;
          margin-bottom: 10px;
          .grade {
            height: 22px;
            background: #e6f4ff;
            border-radius: 4px;
            border: 1px solid #bae0ff;
            margin-right: 11px;
            box-sizing: border-box;
            padding: 1px 8px;
            font-weight: 400;
            font-size: 12px;
            color: #1677ff;
            line-height: 20px;
          }
          .info {
            font-size: 12px;
            color: rgba(0, 0, 0, 0.8);
            line-height: 26px;
          }
        }
        .warnItem:last-child {
          margin-bottom: 0 !important;
        }
        .oneWarn {
          background: rgba(255, 77, 79, 0.06);
          .grade {
            background: #fff1f0;
            border: 1px solid #ffccc7;
            color: rgba(255, 77, 79, 1);
          }
        }
        .twoWarn {
          background: rgba(250, 173, 20, 0.06);
          .grade {
            background: #fffbe6;
            border-radius: 4px;
            border: 1px solid #fff1b8;
            color: rgba(250, 173, 20, 1);
          }
        }
        .threeWarn {
          background: rgba(39, 129, 255, 0.06);
          .grade {
            background: #e6f4ff;
            border: 1px solid #bae0ff;
            color: #1677ff;
          }
        }
        .fourWarn {
          background: rgba(82, 196, 26, 0.06);
          .grade {
            background: #f6ffed;
            border-radius: 4px;
            border: 1px solid #d9f7be;
            color: rgba(82, 196, 26, 1);
          }
        }
        .fiveWarn {
          background: rgba(214, 219, 228, 0.3);
          .grade {
            background: rgba(214, 219, 228, 0.3);
            border-radius: 4px;
            border: 1px solid rgba(214, 219, 228, 0.6);
            color: rgba(0, 0, 0, 0.8);
          }
        }
      }
      .warnList::-webkit-scrollbar {
        width: 8px;
        height: 8px;
        background-color: #f5f5f5;
      }
      .warnList::-webkit-scrollbar-thumb {
        background-color: #ccc;
        border-radius: 4px;
      }
      .warnList::-webkit-scrollbar-thumb:hover {
        background-color: #aaa;
      }
    }
    .warnCount {
      position: relative;
      #countChart {
        width: 453px;
        height: 150px;
      }
      .noData {
        display: flex;
        flex-direction: column;
        align-content: center;
        justify-content: center;
        position: absolute;
        bottom: 0;
        left: 0;
        width: 100%;
        height: 150px;
      }
    }
    .warnRank {
      position: relative;
      .rankChart {
        width: 453px;
        height: 300px;
        .rankItem {
          display: flex;
          align-content: center;
          margin-bottom: 14px;
          .left {
            flex: 2;
            padding-right: 25px;
            font-weight: 400;
            font-size: 12px;
            color: rgba(0, 0, 0, 0.45);
            line-height: 17px;
            text-align: right;
            overflow: hidden;
            white-space: nowrap;
            text-overflow: ellipsis;
          }
          .rankRight {
            flex: 5;
            height: 15px;
            background: rgba(93, 112, 146, 0.2);
            border-radius: 8px;
            .rank {
              height: 15px;
              border-radius: 8px;
              background: rgba(93, 112, 146, 0.6);
            }
          }
          .rankColor {
            background: rgba(91, 143, 249, 0.2);
            .rank {
              background: rgba(91, 143, 249, 0.85);
            }
          }
        }
        .rankItem:last-child {
          margin-bottom: 0 !important;
        }
      }
      .noData {
        display: flex;
        flex-direction: column;
        align-content: center;
        justify-content: center;
        position: absolute;
        bottom: 0;
        left: 0;
        width: 100%;
        height: 300px;
      }
    }
  }
}
</style>
src/view/home/service.js
@@ -26,4 +26,18 @@
// 获取预警排行统计前10
export const getCarInfoById = (data) => {
    return axios.get(`/system/car/getCarInfo/${data.id}`, data)
}
// 获取监控
export const getRealVideo = (data) => {
    return axios.get(`/system/car/getRealVideo/${data.id}`, data)
}
// 通知后端开始获取视频流
export const playDetection = (id) => {
    return axios.get(`/system/car/playDetection/${id}`)
}
// 通知后端开始关闭视频流
export const closeRealVideo = (id) => {
    return axios.get(`/system/car/closeRealVideo/${id}`)
}
src/view/login/index.vue
@@ -1,12 +1,12 @@
<template>
  <div>
  <div class="bgImg">
    <div class="login_box">
      <div class="fs--40 fw-bold">
        <h1>
          射洪“两客一危”监管平台
        </h1>
      </div>
      <div class="mt--20 txt-center py--20 px--20 br--10 box-s1">
      <div class="mt--20 txt-center py--20 px--20">
        <div class="fs--20 fw-bold">登陆</div>
        <div class="mt--20">
          <el-input class="w100" prefix-icon="el-icon-user" placeholder="账号" v-model="username" />
@@ -57,15 +57,13 @@
    document.removeEventListener("keydown", this.handleKeyDown);
  },
  methods: {
    ...mapMutations(['setToken', 'clearToken', 'setUserInfo']),
    ...mapMutations(['setToken', 'setUserInfo']),
    login() {
      if (!this.rulesLogin()) return
      this.loginLoading = true;
      loginPwd({
        username: this.username,
        password: CryptoJS.HmacMD5(this.password, 'password').toString(
          CryptoJS.enc.Hex,
        )
        password: CryptoJS.MD5(this.password).toString()
      }).then(res => {
        localStorage.setItem('client', generateRandomString(16));
        this.loginLoading = false;
@@ -129,15 +127,24 @@
};
</script>
<style scoped lang="less">
.bgImg {
  background-image: url('../../assets/loginBG.png');
  background-size: cover;
  background-position: center;
  background-repeat: no-repeat;
  width: 100%;
  height: 100%;
}
.login_box {
  position: absolute;
  top: 50%;
  left: 50%;
  transform: translate(-50%, -50%);
}
.box-s1 {
  background-color: #fff;
  box-shadow: 0px 0px 10px 0px rgba(0, 0, 0, 0.2);
  border-radius: 10px;
  padding: 20px;
}
.code_box {
src/view/order/component/detailModal.vue
@@ -1,6 +1,7 @@
<template>
    <div>
        <el-dialog title="订单详情" :visible.sync="dialogVisible" width="50%" :modal-append-to-body="false">
        <el-dialog title="订单详情" :visible.sync="dialogVisible" width="50%" :modal-append-to-body="false"
            @close="closeClick">
            <el-radio-group v-model="tabPosition" style="margin-bottom: 30px;">
                <el-radio-button label="order">订单信息</el-radio-button>
                <el-radio-button label="track">行程轨迹</el-radio-button>
@@ -27,11 +28,14 @@
                </el-descriptions>
            </div>
            <!-- 行程轨迹 -->
            <div v-show="tabPosition == 'track'">
            <div v-if="tabPosition == 'track'">
                <div class="mapContainer" id="mapContainer"></div>
            </div>
            <!-- 行程监控 -->
            <div v-show="tabPosition == 'monitoring'"></div>
            <div v-if="tabPosition == 'monitoring'">
                <PlayLive :serverIp="monitoringData.serverIp" :serverPort="monitoringData.serverPort"
                    :carId="orderData.carId" />
            </div>
        </el-dialog>
    </div>
</template>
@@ -45,19 +49,26 @@
            tabPosition: 'order',
            orderData: {},
            monitoringData: {},
            travelData: []
            travelData: [],
        };
    },
    computed: {},
    watch: {},
    created() { },
    watch: {
        tabPosition(val) {
            if (val == 'track') {
                this.$nextTick(() => {
                    this.initMap();
                })
                return
            }
        }
    },
    methods: {
        initData(orderData = {}, monitoringData = {}, travelData = {},) {
        initData(orderData = {}, monitoringData = {}, travelData = []) {
            this.orderData = orderData
            this.monitoringData = monitoringData
            this.travelData = travelData
            this.dialogVisible = true
            this.initMap();
        },
        initMap() {
            window._AMapSecurityConfig = {
@@ -73,42 +84,76 @@
                .then((AMap) => {
                    this.map = new AMap.Map("mapContainer", {
                        center: [this.travelData[this.travelData.length / 2].longitude, this.travelData[this.travelData.length / 2].latitude],
                        zoom: 15,
                        zoom: 12,
                    });
                    this.map.addControl(new AMap.ToolBar());
                    let path = this.travelData.map(item => {
                        return new AMap.LngLat(item.longitude, item.latitude)
                    })
                    const content = `<div class="custom-content-marker">
                                        <img src="${require("@/assets/logo.png")}">
                                        起点
                                    </div>`;
                    const marker = new AMap.Marker({
                        content: content, //自定义点标记覆盖物内容
                        position: [this.travelData[0].longitude, this.travelData[0].latitude], //基点位置
                        offset: new AMap.Pixel(-30, -15), //相对于基点的偏移位置
                    });
                    const contentTwo = `<div class="custom-content-marker-two">
                                        终点
                                    </div>`;
                    const marker = [
                        new AMap.Marker({
                            content: content, //自定义点标记覆盖物内容
                            position: [this.travelData[0].longitude, this.travelData[0].latitude], //基点位置
                            offset: new AMap.Pixel(-35, -25), //相对于基点的偏移位置
                        }),
                        new AMap.Marker({
                            content: contentTwo, //自定义点标记覆盖物内容
                            position: [this.travelData[this.travelData.length - 1].longitude, this.travelData[this.travelData.length - 1].latitude], //基点位置
                            offset: new AMap.Pixel(-35, -25), //相对于基点的偏移位置
                        })
                    ]
                    this.map.add(marker);
                    let polyline = new AMap.Polyline({
                        path: path,
                        strokeWeight: 2, //线条宽度
                        strokeWeight: 3, //线条宽度
                        strokeColor: "red", //线条颜色
                        lineJoin: "round", //折线拐点连接处样式
                    });
                    this.map.add(polyline);
                    // 强制刷新地图
                    this.$nextTick(() => {
                        this.map.resize();
                    });
                })
                .catch((e) => {
                });
        },
        closeClick() {
            this.dialogVisible = false
        }
            this.tabPosition = 'order'
            this.orderData = {}
            this.monitoringData = {}
            this.travelData = []
        },
    },
};
</script>
<style>
.custom-content-marker {
    width: 30px;
    height: 30px;
    width: 50px;
    height: 50px;
    background-color: blue;
    color: #fff;
    border-radius: 50%;
    line-height: 50px;
    text-align: center;
}
.custom-content-marker-two {
    width: 50px;
    height: 50px;
    background-color: orange;
    color: #fff;
    border-radius: 50%;
    line-height: 50px;
    text-align: center;
}
.custom-content-marker img {
@@ -123,6 +168,6 @@
#mapContainer {
    width: 100%;
    height: 300px;
    height: 500px;
}
</style>
src/view/order/index.vue
@@ -1,5 +1,5 @@
<template>
    <div>
    <div v-loading="loading" element-loading-text="加载中...">
        <div class="form flex a-center j-between mt--23">
            <div class="form-left ml--30">
                <el-form :inline="true" :model="searchForm">
@@ -87,7 +87,7 @@
<script>
import DetailModal from "./component/detailModal"
import { exportExcell } from '@/utils/utils'
import { getOrderList, getOrderInfo, getOrderMonitoring, getOrderTravel } from './service'
import { getOrderList, getOrderInfo, getOrderMonitoring, getOrderTravel, } from './service'
import moment from "moment/moment";
export default {
@@ -102,6 +102,7 @@
                pageSize: 10
            },
            tableData: [],
            loading: false,
        };
    },
    computed: {
@@ -132,7 +133,7 @@
                obj.orderDeliveryTimeEnd = moment(obj.orderDeliveryTime[1]).format('YYYY-MM-DD HH-mm-ss')
                delete obj.orderDeliveryTime
            }
            exportExcell('投诉记录导出', obj, '/system/order/exportOrderList')
            exportExcell('订单记录导出', obj, '/system/order/exportOrderList')
        },
        handleSizeChange(e) {
            this.searchForm.pageSize = e
@@ -156,9 +157,13 @@
            })
        },
        showDetail(row) {
            // Promise.all([getOrderInfo(row.id), getOrderMonitoring({ id: row.id }), getOrderTravel({ id: row.id })]).then(res => {
            Promise.all([getOrderInfo(row.id), getOrderTravel({ id: row.id })]).then(res => {
                this.$refs.detailModal.initData(res[0], {}, res[2])
            this.loading = true
            Promise.all([getOrderInfo(row.id), getOrderMonitoring({ id: row.id }), getOrderTravel({ id: row.id })]).then(res => {
                // Promise.all([getOrderInfo(row.id), getOrderTravel({ id: row.id })]).then(res => {
                this.$refs.detailModal.initData(res[0], res[1], res[2])
                this.loading = false
            }).catch(err => {
                this.loading = false
            })
        },
    }
@@ -184,4 +189,17 @@
        width: 100% !important;
    }
}
::v-deep .el-loading-mask {
    display: flex;
    align-items: center;
    justify-content: center;
    .el-loading-spinner {
        width: unset;
        display: flex;
        flex-direction: column;
        align-items: center;
    }
}
</style>
src/view/systemManage/user/components/addEdit.vue
@@ -1,7 +1,7 @@
<template>
    <div>
        <el-dialog :visible.sync="dialogVisible" @close="$emit('close')" :title="row.deptId ? '编辑人员' : '添加人员'"
            width="30%">
            width="30%"  :modal-append-to-body="false">
            <el-form ref="form" :model="form" :rules="rules" label-width="80px">
                <el-form-item label="姓名" prop="nickName">
                    <el-input v-model="form.nickName" placeholder="请输入" style="width: 50%;"></el-input>
src/view/systemManage/user/components/disb.vue
@@ -1,6 +1,6 @@
<template>
    <div>
        <el-dialog :visible.sync="dialogVisible" @close="$emit('close')" title="禁用人员" width="30%">
        <el-dialog :visible.sync="dialogVisible" @close="$emit('close')" title="禁用人员" width="30%"  :modal-append-to-body="false">
            <el-form ref="form" :model="form" label-width="80px">
                <el-form-item label="姓名" prop="nickName">
                    {{ form.nickName }}
src/view/systemManage/user/components/resetPassWord.vue
@@ -1,13 +1,13 @@
<template>
    <div>
        <el-dialog :visible.sync="dialogVisible" @close="$emit('close')" title="重置密码"
            width="30%">
        <el-dialog :visible.sync="dialogVisible" @close="$emit('close')" title="重置密码" width="30%"
            :modal-append-to-body="false">
            <el-form ref="form" :model="form" :rules="rules" label-width="80px">
                <el-form-item label="姓名" prop="nickName">
                    <el-input :disabled="true" v-model="form.nickName" placeholder="请输入" style="width: 50%;"></el-input>
                </el-form-item>
                <el-form-item label="登陆账号" prop="account">
                    <el-input :disabled="true" v-model="form.account" placeholder="请输入" style="width: 50%;"></el-input>
                    <el-input :disabled="true" v-model="form.userName" placeholder="请输入" style="width: 50%;"></el-input>
                </el-form-item>
                <el-form-item label="新密码" prop="password">
                    <el-input v-model="form.password" placeholder="请输入" style="width: 50%;"></el-input>
@@ -25,6 +25,7 @@
</template>
<script>
import CryptoJS from 'crypto-js';
export default {
    props: {
        dialogVisible: {
@@ -41,14 +42,15 @@
            form: { status: true },
            rules: {
                password: [{ required: true, message: '请新密码', trigger: 'blur' }],
                confirmPassword: [{ required: true, message: '请确认密码', trigger: 'blur' },{
                confirmPassword: [{ required: true, message: '请确认密码', trigger: 'blur' }, {
                    validator: (rule, value, callback) => {
                        if (value !== this.form.password) {
                            callback(new Error('两次输入密码不一致!'));
                        } else {
                            callback();
                        }
                }}],
                    }
                }],
            }
        };
    },
@@ -58,7 +60,7 @@
            this.form = {
                userId: this.row.userId,
                nickName: this.row.nickName,
                account: this.row.userName,
                userName: this.row.userName,
            }
        }
    },
@@ -71,6 +73,7 @@
                    this.form.orderNum = 0
                    this.form.ancestors = 0
                    this.form.parentId = 100
                    this.form.password = CryptoJS.MD5(this.form.password).toString()
                    this.$emit('confirm', this.form)
                }
            })