pyt
2025-04-28 ec2a85ce86cb925813de04beacc6e3a61b5cdc4a
Merge branch 'master' of http://120.76.84.145:10101/gitblit/r/H5/shehong-vehicle-supervision
13个文件已修改
1个文件已添加
772 ■■■■ 已修改文件
package.json 4 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/view/car-manage/components/detailOrderModal.vue 206 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/view/car-manage/detail.vue 237 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/view/car-manage/index.vue 66 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/view/car-manage/service.js 18 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/view/complaint/index.vue 11 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/view/home/index.vue 96 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/view/home/service.js 2 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/view/order/component/detailModal.vue 107 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/view/order/index.vue 15 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/view/systemManage/driver/index.vue 4 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/view/systemManage/driver/service.js 2 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/view/systemManage/role/index.vue 2 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/view/systemManage/user/index.vue 2 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
package.json
@@ -3,7 +3,7 @@
  "version": "0.1.0",
  "private": true,
  "scripts": {
    "dev": "vue-cli-service serve",
    "dev": "vue-cli-service serve --port 8089",
    "build": "vue-cli-service build",
    "lint": "vue-cli-service lint"
  },
@@ -49,4 +49,4 @@
    "> 1%",
    "last 2 versions"
  ]
}
}
src/view/car-manage/components/detailOrderModal.vue
New file
@@ -0,0 +1,206 @@
<template>
    <div>
        <el-dialog title="订单详情" :visible.sync="dialogVisible" width="50%" :modal-append-to-body="false"
            :close-on-press-escape="false" :close-on-click-modal="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>
                <el-radio-button label="monitoring">行程监控</el-radio-button>
            </el-radio-group>
            <!-- 订单信息 -->
            <div v-show="tabPosition == 'order'">
                <el-descriptions title="" :column="3">
                    <el-descriptions-item label="公司名称">{{ orderData.enterpriseName }}</el-descriptions-item>
                    <el-descriptions-item label="发起地区划">{{ orderData.drivingLicenseNumber }}</el-descriptions-item>
                    <el-descriptions-item label="订单编号">{{ orderData.code }}</el-descriptions-item>
                    <el-descriptions-item label="机动车驾驶证编号">{{ orderData.drivingLicenseNumber }}</el-descriptions-item>
                    <el-descriptions-item label="驾驶员手机号">{{ orderData.driverPhone }}</el-descriptions-item>
                    <el-descriptions-item label="车辆号牌">{{ orderData.vehicleNumber }}</el-descriptions-item>
                    <el-descriptions-item label="派单时间">{{ orderData.orderDeliveryTime }}</el-descriptions-item>
                    <el-descriptions-item label="订单发起时间">{{ orderData.orderTime }}</el-descriptions-item>
                    <el-descriptions-item label="乘客备注">{{ orderData.remark }}</el-descriptions-item>
                    <el-descriptions-item label="出发地点">{{ orderData.orderPlace }}</el-descriptions-item>
                    <el-descriptions-item label="下车地点">{{ orderData.dropOffPoint }}</el-descriptions-item>
                    <el-descriptions-item label="运价类型编号">{{ orderData.tariffType }}</el-descriptions-item>
                    <el-descriptions-item label="订单金额">¥{{ orderData.orderAmount }}</el-descriptions-item>
                    <el-descriptions-item label="实付价">¥{{ orderData.paymentAmount }}</el-descriptions-item>
                    <el-descriptions-item label="支付方式">{{ orderData.paymentMode }}</el-descriptions-item>
                </el-descriptions>
            </div>
            <!-- 行程轨迹 -->
            <div v-if="tabPosition == 'track'">
                <div class="mapContainer" id="mapContainers"></div>
            </div>
            <!-- 行程监控 -->
            <div v-if="tabPosition == 'monitoring'">
                <PlayLive :serverIp="monitoringData.serverIp" :serverPort="monitoringData.serverPort"
                    :carId="orderData.carId" />
            </div>
        </el-dialog>
    </div>
</template>
<script>
import AMapLoader from "@amap/amap-jsapi-loader";
export default {
    data() {
        return {
            dialogVisible: false,
            tabPosition: 'order',
            orderData: {},
            monitoringData: {},
            travelData: [],
        };
    },
    computed: {},
    watch: {
        tabPosition(val) {
            if (val == 'track') {
                this.$nextTick(() => {
                    this.initMap();
                })
                return
            }
        }
    },
    methods: {
        initData(orderData = {}, monitoringData = {}, travelData = []) {
            console.log('////////////////////////');
            this.orderData = orderData
            this.monitoringData = monitoringData
            this.travelData = travelData
            this.dialogVisible = true
        },
        initMap() {
            window._AMapSecurityConfig = {
                securityJsCode: this.$secretKey,
            };
            AMapLoader.load({
                key: this.$mapKey,
                version: "2.0",
                plugins: [
                    "AMap.ToolBar",
                ],
            })
                .then((AMap) => {
                    // 转换 travelData 中的坐标
                    const wgs84Path = this.travelData.map(item => [item.longitude, item.latitude]);
                    const batchSize = 40; // 每次转换 40 对坐标
                    const batches = [];
                    // 分批处理
                    for (let i = 0; i < wgs84Path.length; i += batchSize) {
                        batches.push(wgs84Path.slice(i, i + batchSize));
                    }
                    const gcj02Path = [];
                    const promises = batches.map(batch => {
                        return new Promise((resolve, reject) => {
                            AMap.convertFrom(batch, 'gps', (status, result) => {
                                if (status === 'complete' && result.locations) {
                                    resolve(result.locations);
                                } else {
                                    reject(result);
                                }
                            });
                        });
                    });
                    // 等待所有批次转换完成
                    Promise.all(promises)
                        .then(results => {
                            results.forEach(batchResult => {
                                gcj02Path.push(...batchResult);
                            });
                            // 开始绘制地图
                            this.map = new AMap.Map("mapContainers", {
                                center: gcj02Path[Math.floor(gcj02Path.length / 2)], // 使用转换后的中点坐标
                                zoom: 12,
                            });
                            this.map.addControl(new AMap.ToolBar());
                            // 添加起点和终点标记
                            const marker = [
                                new AMap.Marker({
                                    content: `<div class="custom-content-marker">起点</div>`,
                                    position: gcj02Path[0],
                                    offset: new AMap.Pixel(-35, -25),
                                }),
                                new AMap.Marker({
                                    content: `<div class="custom-content-marker-two">终点</div>`,
                                    position: gcj02Path[gcj02Path.length - 1],
                                    offset: new AMap.Pixel(-35, -25),
                                }),
                            ];
                            this.map.add(marker);
                            // 绘制路径
                            const polyline = new AMap.Polyline({
                                path: gcj02Path,
                                strokeWeight: 3,
                                strokeColor: "red",
                                lineJoin: "round",
                            });
                            this.map.add(polyline);
                            // 强制刷新地图
                            this.$nextTick(() => {
                                this.map.resize();
                            });
                        })
                        .catch(error => {
                            console.error('坐标转换失败', error);
                        });
                })
                .catch((e) => {
                    console.log(e);
                });
        },
        closeClick() {
            this.dialogVisible = false
            this.tabPosition = 'order'
            this.orderData = {}
            this.monitoringData = {}
            this.travelData = []
        },
    },
};
</script>
<style>
.custom-content-marker {
    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 {
    width: 100%;
    height: 100%;
}
</style>
<style scoped lang="less">
::v-deep .el-descriptions .el-descriptions-item__cell {
    padding-bottom: 25px;
}
#mapContainers {
    width: 100%;
    height: 500px;
}
</style>
src/view/car-manage/detail.vue
@@ -28,7 +28,34 @@
                </div>
            </div>
            <div class="info-right flex2">
                <video style="width: 100%;height: 100%;" src="../../assets/homeImg/QQ20241223-103023.mp4"></video>
                <div style="position: relative; width: 100%; height: 330px">
                    <div
                        style="width: 100%; height: 330px; border-radius: 9px; background: #f5f5f5; display: flex; justify-content: center; align-items: center; flex-direction: column">
                        <video style="width: 100%; height: 330px; border-radius: 9px; display: none" id="monitoringCard"
                            ref="monitoringCard" :controls="false" autoPlay width="620">
                        </video>
                        <el-empty description="暂无视频信息" :image-size="80"></el-empty>
                    </div>
                    <canvas id="myCanvas" style="display:none"></canvas>
                    <el-button type="default" style="position: absolute; top: 10px;right: 60px;"
                        @click="goBack()">查看回放</el-button>
                    <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;" @click="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;" @click="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>
        </div>
        <div class="tab-content ml--100 mr--30">
@@ -36,7 +63,7 @@
                <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="index" label="序号"></el-table-column>
                            <el-table-column type="index" width="80" 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>
@@ -50,7 +77,7 @@
                            <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>
                                    <el-button type="text" @click="showDetails(scope.row)">详情</el-button>
                                </template>
                            </el-table-column>
                        </el-table>
@@ -66,7 +93,7 @@
                <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="index" label="序号" fixed width="80"></el-table-column>
                            <el-table-column type="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>
@@ -105,7 +132,7 @@
                            <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 :value-format="'yyyy-MM-dd HH:mm'" v-model="searchForm.date"
                                    <el-date-picker :value-format="'yyyy-MM-dd HH:mm:ss'" v-model="searchForm.date"
                                        type="datetimerange" range-separator="至" start-placeholder="开始日期"
                                        end-placeholder="结束日期">
                                    </el-date-picker>
@@ -125,12 +152,13 @@
            </el-tabs>
        </div>
        <DetailModal ref="detailModal" :detail="detail" />
        <DetailOrderModal ref="detailOrder" />
        <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>
                        }})</span></div><i @click="closeDrawer" class="el-icon-s-unfold color1 pointer"></i>
            </div>
            <hr class="mt--10" />
            <div class="pl--15 pr--15">
@@ -202,10 +230,12 @@
<script>
import AMapLoader from "@amap/amap-jsapi-loader";
import DetailModal from "./components/detailModal.vue";
import { getCarDetail, getCarOrder, getCarWarning, getCarTrack, getCarVideo,getDetail } from './service'
import DetailOrderModal from "./components/detailOrderModal.vue";
import { getCarDetail, getCarOrder, getCarWarning, getCarTrack, getCarVideo, getDetail, getOrderInfo, getOrderTravel, getOrderMonitoring, playDetection, closeRealVideo } from './service'
import moment from "moment";
export default {
    name: "detail",
    components: { DetailModal },
    components: { DetailModal, DetailOrderModal },
    data() {
        return {
            id: '',
@@ -220,6 +250,7 @@
            activeName: 'first',
            routeList: [],
            videoObj: {},
            loading: false,
            drawer: false,
            showWarnDetail: false,
            info: {},
@@ -235,15 +266,185 @@
            })
            getCarVideo({ id: this.$route.query.id }).then(res => {
                this.videoObj = res;
                this.initVideoPlayer();
            })
        }
    },
    destroyed() {
        this.destroyPlayer();
    },
    methods: {
        goBack() {
            this.$router.push('/car-playback?id=' + this.$route.query.id)
        },
        // 初始化视频播放器
        initVideoPlayer() {
            // 先销毁之前的播放器
            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.videoObj.serverIp}:${this.videoObj.serverPort}/live?port=1935&app=flv&stream=${this.$route.query.id}`, // 后端拿到的视频路径
                            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) => {
                                // 显示视频元素
                                video.style.display = 'block';
                                // 隐藏空状态
                                const emptyElement = video.parentElement.querySelector('.el-empty');
                                if (emptyElement) {
                                    emptyElement.style.display = 'none';
                                }
                                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;
                    // 恢复空状态的显示
                    const video = document.getElementById("monitoringCard");
                    if (video) {
                        video.style.display = 'none';
                        const emptyElement = video.parentElement.querySelector('.el-empty');
                        if (emptyElement) {
                            emptyElement.style.display = 'block';
                        }
                    }
                });
            }
        },
        // 处理视频错误
        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>'
            );
        },
        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);
        },
        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();
            }
        },
        showDetails(row) {
            this.loading = true
            Promise.all([getOrderInfo(row.id), getOrderTravel({ id: row.id })]).then(res => {
                getOrderMonitoring({ id: row.id }).then(resp => {
                    this.$refs.detailOrder.initData(res[0], resp, res[1])
                    this.loading = false
                }).catch(err => {
                    this.$refs.detailOrder.initData(res[0], {}, res[1])
                    this.loading = false
                })
            }).catch(err => {
                this.loading = false
            })
        },
        closeDrawer() {
            this.drawer = false
            this.showWarnDetail = false
        },
        // 查看详情
        viewDetail(row) {
@@ -261,7 +462,7 @@
                    securityJsCode: this.$secretKey,
                };
                AMapLoader.load({
                    key:this.$mapKey,
                    key: this.$mapKey,
                    version: "2.0",
                    plugins: [
                        "AMap.ToolBar",
@@ -296,7 +497,7 @@
                    pageSize: 10,
                    total: 0,
                }
                this.getList(this.detail.vehicleNumber)
                //销毁地图
                if (this.map) {
@@ -318,8 +519,8 @@
                    pageCurr: 1,
                    pageSize: 10,
                    total: 0,
                    startTime: this.searchForm.date[0],
                    endTime: this.searchForm.date[1],
                    startTime: new Date(this.searchForm.date[0]).getTime() / 1000,
                    endTime: new Date(this.searchForm.date[1]).getTime() / 1000,
                }
                getCarTrack({ ...this.searchForm, vehicleNumber: this.detail.vehicleNumber }).then(res => {
                    this.routeList = res;
@@ -332,10 +533,10 @@
        },
        reset() {
            this.searchForm = {
               pageCurr: 1,
               pageSize: 10,
               total: 0,
               date: undefined,
                pageCurr: 1,
                pageSize: 10,
                total: 0,
                date: undefined,
            }
        },
        getList(vehicleNumber) {
@@ -510,6 +711,7 @@
    width: 100%;
    height: 600px;
}
#mapContainers {
    width: 100%;
    height: 500px;
@@ -535,6 +737,7 @@
        }
    }
}
.color1 {
    color: #0E6EFD;
}
src/view/car-manage/index.vue
@@ -3,23 +3,23 @@
        <div class="form flex j-between mt--23" style="align-items: end;">
            <div class="form-left ml--30">
                <el-form :inline="true" :model="searchForm" class="demo-form-inline">
                    <el-form-item label="车辆号:" prop="carNumber" class="unset_m" style="margin-right: 15px;">
                        <el-input v-model="searchForm.carNumber" placeholder="请输入"></el-input>
                    <el-form-item label="车牌号:" prop="vehicleNumber" class="unset_m" style="margin-right: 15px;">
                        <el-input v-model="searchForm.vehicleNumber" placeholder="请输入"></el-input>
                    </el-form-item>
                    <el-form-item label="公司名称:" prop="companyName" class="unset_m" style="margin-right: 15px;">
                        <el-input v-model="searchForm.companyName" placeholder="请输入"></el-input>
                    <el-form-item label="公司名称:" prop="enterpriseName" class="unset_m" style="margin-right: 15px;">
                        <el-input v-model="searchForm.enterpriseName" placeholder="请输入"></el-input>
                    </el-form-item>
                    <el-form-item label="所属车主:" prop="ownerName" class="unset_m" style="margin-right: 15px;">
                        <el-input v-model="searchForm.ownerName" placeholder="请输入"></el-input>
                    <el-form-item label="所属车主:" prop="driverName" class="unset_m" style="margin-right: 15px;">
                        <el-input v-model="searchForm.driverName" placeholder="请输入"></el-input>
                    </el-form-item>
                    <el-form-item label="车辆颜色:" prop="carColor" class="unset_m" style="margin-right: 15px;">
                    <el-form-item label="车身颜色:" prop="carColor" class="unset_m" style="margin-right: 15px;">
                        <el-input v-model="searchForm.carColor" placeholder="请输入"></el-input>
                    </el-form-item>
                    <el-form-item label="车辆经营区域:" prop="operationArea" class="unset_m" style="margin-right: 15px;">
                        <el-input v-model="searchForm.operationArea" placeholder="请输入"></el-input>
                    <el-form-item label="车辆经营区域:" prop="area" class="unset_m" style="margin-right: 15px;">
                        <el-input v-model="searchForm.area" placeholder="请输入"></el-input>
                    </el-form-item>
                    <el-form-item label="车辆型号:" prop="carModel" class="unset_m" style="margin-right: 15px;">
                        <el-input v-model="searchForm.carModel" placeholder="请输入"></el-input>
                    <el-form-item label="车辆型号:" prop="brandModel" class="unset_m" style="margin-right: 15px;">
                        <el-input v-model="searchForm.brandModel" placeholder="请输入"></el-input>
                    </el-form-item>
                    <el-form-item label="核定载客位:" prop="startNum" class="unset_m" style="margin-right: 15px;">
                        <el-form-item prop="startNum" style="margin-right: unset !important;">
@@ -30,13 +30,13 @@
                            <el-input class="w--90" v-model="searchForm.endNum" placeholder="请输入最大值"></el-input>
                        </el-form-item>
                    </el-form-item>
                    <el-form-item label="车辆运营类型:" prop="operationType" class="unset_m" style="margin-right: 15px;">
                        <el-select :popper-append-to-body="false" v-model="searchForm.operationType" placeholder="请选择">
                            <el-option v-for="(item,index) in options" :key="index" :label="item.name" :value="item.id"></el-option>
                    <el-form-item label="车辆运营类型:" prop="operateType" class="unset_m" style="margin-right: 15px;">
                        <el-select :popper-append-to-body="false" v-model="searchForm.operateType" placeholder="请选择">
                            <el-option v-for="(item,index) in options" :key="index" :label="item.name" :value="item.name"></el-option>
                        </el-select>
                    </el-form-item>
                    <el-form-item label="车辆状态:" prop="carStatus" class="unset_m" style="margin-right: 15px;">
                        <el-select :popper-append-to-body="false" v-model="searchForm.carStatus" placeholder="请选择">
                    <el-form-item label="车辆状态:" prop="status" class="unset_m" style="margin-right: 15px;">
                        <el-select :popper-append-to-body="false" v-model="searchForm.status" placeholder="请选择">
                            <el-option label="在线" value="1"></el-option>
                            <el-option label="异常" value="2"></el-option>
                            <el-option label="离线" value="3"></el-option>
@@ -69,8 +69,8 @@
                <el-table-column prop="status" label="车辆状态">
                    <template slot-scope="scope">
                       <el-tag v-if="scope.row.status == 1" type="success">在线</el-tag>
                       <el-tag v-if="scope.row.status == 2" type="warning">异常</el-tag>
                       <el-tag v-if="scope.row.status == 4" type="warning">故障</el-tag>
                       <el-tag v-if="scope.row.status == 2" type="danger">异常</el-tag>
                       <el-tag v-if="scope.row.status == 4" type="danger">故障</el-tag>
                       <el-tag v-if="scope.row.status == 3" type="info">离线</el-tag>
                    </template>
                </el-table-column>
@@ -99,16 +99,16 @@
    data() {
        return {
            searchForm: {
                carNumber: '', // 车辆号
                companyName: '', // 公司名称
                ownerName: '', // 所属车主
                vehicleNumber: '', // 车辆号
                enterpriseName: '', // 公司名称
                driverName: '', // 所属车主
                carColor: '', // 车辆颜色
                operationArea: '', // 车辆经营区域
                carModel: '', // 车辆型号
                area: '', // 车辆经营区域
                brandModel: '', // 车辆型号
                startNum: '', // 核定载客位最小值
                endNum: '', // 核定载客位最大值
                operationType: '', // 车辆运营类型
                carStatus: '', // 车辆状态
                operateType: '', // 车辆运营类型
                status: '', // 车辆状态
                total: 0,
                pageCurr: 1,
                pageSize: 10
@@ -125,7 +125,7 @@
        const query = this.$route.query;
        if (query && Object.keys(query).length > 0) {
            if(query.id){
                this.searchForm.operationType = Number(query.id);
                this.searchForm.operateType = query.id;
            }
        }
        this.getList();
@@ -133,16 +133,16 @@
    methods: {
        reset() {
            this.searchForm = {
                carNumber: '',
                companyName: '',
                ownerName: '',
                vehicleNumber: '',
                enterpriseName: '',
                driverName: '',
                carColor: '',
                operationArea: '',
                carModel: '',
                area: '',
                brandModel: '',
                minSeats: '',
                maxSeats: '',
                operationType: '',
                carStatus: '',
                operateType: '',
                status: '',
                total: 0,
                pageCurr: 1,
                pageSize: 10
src/view/car-manage/service.js
@@ -40,4 +40,20 @@
export const getDetail = (params) => {
    console.log(params)
    return axios.get('/system/warn/getCarWarnInfo', {params})
}
}
// 获取订单详情
export const getOrderInfo = (id) => {
    return axios.get(`/system/order/getOrderInfo/${id}`)
}
// 获取订单行程轨迹
export const getOrderTravel = (params) => {
    return axios.get(`/system/order/getOrderTravel`, { params })
}
// 获取订单监控
export const getOrderMonitoring = (params) => {
    return axios.get(`/system/order/getOrderMonitoring`, { params })
}
src/view/complaint/index.vue
@@ -35,7 +35,7 @@
                @click="exportExcell">导出</el-button>
        </div>
        <div class="table-box ml--30 mt--23 mr--30">
            <el-table :data="tableData" border stripe style="width: 100%" :height="height" :element-loading-text="'正在加载...'">
            <el-table :data="tableData" border stripe style="width: 100%" :element-loading-text="'正在加载...'">
                <el-table-column type="index" width="55" label="序号"></el-table-column>
                <el-table-column prop="carName" label="车辆名称"></el-table-column>
                <el-table-column prop="vehicleNumber" label="车牌号码"></el-table-column>
@@ -68,6 +68,7 @@
<script>
import { getComplainList } from './service'
import { exportExcell } from '@/utils/utils'
import moment from 'moment'
export default {
    data() {
@@ -104,8 +105,8 @@
            let obj = { ...this.searchForm }
            delete obj.total
            if (obj.selectTime) {
                obj.startTime = moment(obj.selectTime[0]).format('YYYY-MM-DD HH-mm-ss')
                obj.endTime = moment(obj.selectTime[1]).format('YYYY-MM-DD HH-mm-ss')
                obj.startTime = moment(obj.selectTime[0]).format('YYYY-MM-DD') + ' 00:00:00'
                obj.endTime = moment(obj.selectTime[1]).format('YYYY-MM-DD') + ' 23:59:59'
                delete obj.selectTime
            }
            exportExcell('投诉记录导出', obj, '/system/complain/exportComplainList')
@@ -122,8 +123,8 @@
            let obj = { ...this.searchForm }
            delete obj.total
            if (obj.selectTime) {
                obj.startTime = moment(obj.selectTime[0]).format('YYYY-MM-DD HH-mm-ss')
                obj.endTime = moment(obj.selectTime[1]).format('YYYY-MM-DD HH-mm-ss')
                obj.startTime = moment(obj.selectTime[0]).format('YYYY-MM-DD') + ' 00:00:00'
                obj.endTime = moment(obj.selectTime[1]).format('YYYY-MM-DD') + ' 23:59:59'
                delete obj.selectTime
            }
            getComplainList(obj).then(res => {
src/view/home/index.vue
@@ -10,7 +10,7 @@
            class="countCard"
            v-for="(item, index) in carCountData.slice(0, 3)"
            :key="item.id"
            @click="toCarManage(item.id)"
            @click="toCarManage(item.name)"
          >
            <img class="iconImg" :src="imgList[index]" />
            <div>
@@ -23,7 +23,7 @@
          <div
            class="countCard"
            v-for="(item, index) in carCountData.slice(3, 7)"
            @click="toCarManage(item.id)"
            @click="toCarManage(item.name)"
            :key="item.id"
          >
            <img class="iconImg" :src="imgList[index + 3]" />
@@ -167,7 +167,7 @@
      </div>
      <!-- 预警情况统计 -->
      <div class="warnCount">
        <div class="title">预警情况统计</div>
        <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>
@@ -175,7 +175,7 @@
      </div>
      <!-- 预警排行榜(前10) -->
      <div class="warnRank">
        <div class="title">预警排行榜(前10)</div>
        <div class="title mt-0">预警排行榜(前10)</div>
        <div class="rankChart" id="rankChart">
          <div class="rankItem" v-for="(item, index) in rankList" :key="index">
            <div class="left">{{ item.name }}</div>
@@ -283,6 +283,9 @@
    this.getWarnTop10Data();
    this.initMap();
    if (this.timer) {
      clearInterval(this.timer);
    }
    // 设置定时器,每分钟刷新一次数据
    this.timer = setInterval(() => {
      this.getCarCountData();
@@ -413,7 +416,7 @@
    // 获取预警列表数据
    async getWarnListData() {
      try {
        const res = await getCarWarnList();
        const res = await getCarWarnList({pageNum:1,pageSize:100000});
        this.warnList = res.records;
      } catch (error) {
        this.$message.error("获取预警列表数据失败");
@@ -611,12 +614,12 @@
    },
    // 获取视频地址
    async getVideoUrl(carId) {
      this.carId = 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 {};
@@ -624,7 +627,7 @@
    },
    // 初始化视频播放器
    initVideoPlayer(videoUrl) {
    initVideoPlayer() {
      console.log('11111',this.serverIp,'2222222222',this.serverPort)
      // 先销毁之前的播放器
      if (this.flvPlayer) {
@@ -654,7 +657,7 @@
              enableStashBuffer: false, // 启用数据缓存机制,提高视频的流畅度和稳定性。
              stashInitialSize: 1024, // 初始缓存大小。单位:字节。建议针对直播:调整为1024kb
              stashInitialTime: 0.2, // 缓存初始时间。单位:秒。建议针对直播:调整为200毫秒
              seekType: "range", // 建议将其设置为“range”模式,以便更快地加载视频数据,提高视频的实时性。
              seekType: "range", // 建议将其设置为"range"模式,以便更快地加载视频数据,提高视频的实时性。
              lazyLoad: false, //关闭懒加载模式,从而提高视频的实时性。建议针对直播:调整为false
              lazyLoadMaxDuration: 0.2, // 懒加载的最大时长。单位:秒。建议针对直播:调整为200毫秒
              deferLoadAfterSourceOpen: false, // 不预先加载视频数据,在 MSE(Media Source Extensions)打开后立即加载数据,提高视频的实时性。建议针对直播:调整为false
@@ -665,6 +668,14 @@
            this.flvPlayer
              .play()
              .then((res) => {
                // 显示视频元素
                video.style.display = 'block';
                // 隐藏空状态
                const emptyElement = video.parentElement.querySelector('.el-empty');
                if (emptyElement) {
                  emptyElement.style.display = 'none';
                }
                this.videoTimer = setInterval(() => {
                  playDetection(this.carId);
                }, 5000);
@@ -695,6 +706,16 @@
          this.flvPlayer.detachMediaElement();
          this.flvPlayer.destroy();
          this.flvPlayer = null;
          // 恢复空状态的显示
          const video = document.getElementById("monitoringCard");
          if (video) {
            video.style.display = 'none';
            const emptyElement = video.parentElement.querySelector('.el-empty');
            if (emptyElement) {
              emptyElement.style.display = 'block';
            }
          }
        });
      }
    },
@@ -714,16 +735,18 @@
    listRender(record) {
      return `<div style="background: #ffffff; padding: 24px 20px;z-index: 999">
        <div style="position: relative; width: 460px; height: 330px">
          <video
          <div style="width: 460px; height: 330px; border-radius: 9px; background: #f5f5f5; display: flex; justify-content: center; align-items: center; flex-direction: column">
            <video
              ref="video"
               style="width: 460px; height: 330px; border-radius: 9px"
               id="monitoringCard"
               ref="monitoringCard"
                :controls="false"
                autoPlay
                width="620">
              </video>
              style="width: 460px; height: 330px; border-radius: 9px; display: none"
              id="monitoringCard"
              ref="monitoringCard"
              :controls="false"
              autoPlay
              width="620">
            </video>
            <el-empty description="暂无视频信息" :image-size="80"></el-empty>
          </div>
          <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;
@@ -819,6 +842,18 @@
            },
            axisLabel: {
              color: "rgba(0, 0, 0, 0.45)",
              interval: 0, // 强制显示所有标签
              width: 60, // 设置标签宽度
              height: 20, // 设置标签高度
              overflow: 'truncate', // 截断模式
              ellipsis: true, // 超出显示省略号
              rotate: -40, // 可以根据需要调整角度
              formatter: function(value) {
                if (value.length > 4) {
                  return value.substring(0, 4) + '...';
                }
                return value;
              }
            },
          },
        ],
@@ -927,7 +962,7 @@
  .leftMap {
    // width: 100%;
    height: 100%;
    flex: 1;
    flex: 3;
    display: flex;
    position: relative;
@@ -945,7 +980,7 @@
    right: 513px;
    display: flex;
    justify-content: space-between;
    width: calc(100% - 570px);
    width: calc(100% - 770px);
    .title {
      font-weight: 600;
@@ -1126,13 +1161,15 @@
  }
  .right {
    width: 493px;
    // width: 493px;
    flex: 1;
    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;
    overflow-y: auto;
    .title {
      margin-top: 30px;
@@ -1143,6 +1180,9 @@
      text-transform: none;
      margin-bottom: 10px;
    }
    .mt-0{
      margin-top: 0 !important;
    }
    .firCard {
      display: flex;
@@ -1150,7 +1190,7 @@
      .companyCard {
        width: 140px;
        height: 90px;
        // height: 90px;
        background: #f4f4ff;
        border-radius: 0px 10px 10px 0px;
        position: relative;
@@ -1160,7 +1200,7 @@
          left: 0;
          top: 0;
          width: 4px;
          height: 90px;
          // height: 90px;
          background: #0e6efd;
          border-radius: 2px;
        }
@@ -1297,8 +1337,8 @@
      position: relative;
      #countChart {
        width: 453px;
        height: 150px;
        width: 100%;
        height: 180px;
      }
      .noData {
@@ -1310,7 +1350,7 @@
        bottom: 0;
        left: 0;
        width: 100%;
        height: 150px;
        height: 180px;
      }
    }
@@ -1318,8 +1358,8 @@
      position: relative;
      .rankChart {
        width: 453px;
        height: 300px;
        width: 100%;
        // height: 300px;
        .rankItem {
          display: flex;
@@ -1330,7 +1370,7 @@
            flex: 2;
            padding-right: 25px;
            font-weight: 400;
            font-size: 12px;
            font-size: 13px;
            color: rgba(0, 0, 0, 0.45);
            line-height: 17px;
            text-align: right;
src/view/home/service.js
@@ -13,7 +13,7 @@
}
// 获取车辆预警列表
export const getCarWarnList = (data) => {
    return axios.get('/system/warn/getCarWarnList', data)
    return axios.get('/system/warn/getCarWarnList', {params:data})
}
// 获取车辆预警情况统计
export const getWarnGroupCount = (data) => {
src/view/order/component/detailModal.vue
@@ -1,7 +1,7 @@
<template>
    <div>
        <el-dialog title="订单详情" :visible.sync="dialogVisible" width="50%" :modal-append-to-body="false"
            @close="closeClick">
            :close-on-press-escape="false" :close-on-click-modal="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>
@@ -82,47 +82,78 @@
                ],
            })
                .then((AMap) => {
                    this.map = new AMap.Map("mapContainer", {
                        center: [this.travelData[this.travelData.length / 2].longitude, this.travelData[this.travelData.length / 2].latitude],
                        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">
                                        起点
                                    </div>`;
                    // 转换 travelData 中的坐标
                    const wgs84Path = this.travelData.map(item => [item.longitude, item.latitude]);
                    const batchSize = 40; // 每次转换 40 对坐标
                    const batches = [];
                    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), //相对于基点的偏移位置
                    // 分批处理
                    for (let i = 0; i < wgs84Path.length; i += batchSize) {
                        batches.push(wgs84Path.slice(i, i + batchSize));
                    }
                    const gcj02Path = [];
                    const promises = batches.map(batch => {
                        return new Promise((resolve, reject) => {
                            AMap.convertFrom(batch, 'gps', (status, result) => {
                                if (status === 'complete' && result.locations) {
                                    resolve(result.locations);
                                } else {
                                    reject(result);
                                }
                            });
                        });
                    });
                    // 等待所有批次转换完成
                    Promise.all(promises)
                        .then(results => {
                            results.forEach(batchResult => {
                                gcj02Path.push(...batchResult);
                            });
                            // 开始绘制地图
                            this.map = new AMap.Map("mapContainer", {
                                center: gcj02Path[Math.floor(gcj02Path.length / 2)], // 使用转换后的中点坐标
                                zoom: 12,
                            });
                            this.map.addControl(new AMap.ToolBar());
                            // 添加起点和终点标记
                            const marker = [
                                new AMap.Marker({
                                    content: `<div class="custom-content-marker">起点</div>`,
                                    position: gcj02Path[0],
                                    offset: new AMap.Pixel(-35, -25),
                                }),
                                new AMap.Marker({
                                    content: `<div class="custom-content-marker-two">终点</div>`,
                                    position: gcj02Path[gcj02Path.length - 1],
                                    offset: new AMap.Pixel(-35, -25),
                                }),
                            ];
                            this.map.add(marker);
                            // 绘制路径
                            const polyline = new AMap.Polyline({
                                path: gcj02Path,
                                strokeWeight: 3,
                                strokeColor: "red",
                                lineJoin: "round",
                            });
                            this.map.add(polyline);
                            // 强制刷新地图
                            this.$nextTick(() => {
                                this.map.resize();
                            });
                        })
                    ]
                    this.map.add(marker);
                    let polyline = new AMap.Polyline({
                        path: path,
                        strokeWeight: 3, //线条宽度
                        strokeColor: "red", //线条颜色
                        lineJoin: "round", //折线拐点连接处样式
                    });
                    this.map.add(polyline);
                    // 强制刷新地图
                    this.$nextTick(() => {
                        this.map.resize();
                    });
                        .catch(error => {
                            console.error('坐标转换失败', error);
                        });
                })
                .catch((e) => {
                    console.log(e);
                });
        },
        closeClick() {
src/view/order/index.vue
@@ -49,8 +49,7 @@
                @click="exportExcell">导出</el-button>
        </div>
        <div class="table-box ml--30 mt--23 mr--30">
            <el-table :data="tableData" border stripe style="width: 100%" :height="height"
                :element-loading-text="'正在加载...'">
            <el-table :data="tableData" border stripe style="width: 100%" :element-loading-text="'正在加载...'">
                <el-table-column type="index" width="55" label="序号" />
                <el-table-column prop="code" label="订单编号" />
                <el-table-column prop="vehicleNumber" label="车牌号" />
@@ -158,10 +157,14 @@
        },
        showDetail(row) {
            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
            Promise.all([getOrderInfo(row.id), getOrderTravel({ id: row.id })]).then(res => {
                getOrderMonitoring({ id: row.id }).then(resp => {
                    this.$refs.detailModal.initData(res[0], resp, res[1])
                    this.loading = false
                }).catch(err => {
                    this.$refs.detailModal.initData(res[0], {}, res[1])
                    this.loading = false
                })
            }).catch(err => {
                this.loading = false
            })
src/view/systemManage/driver/index.vue
@@ -25,7 +25,7 @@
            </div>
        </div>
        <div class="table-box ml--30 mt--23 mr--30">
            <el-table :data="tableData" border stripe style="width: 100%" :height="height" :element-loading-text="'正在加载...'">
            <el-table :data="tableData" border stripe style="width: 100%" :element-loading-text="'正在加载...'">
                <el-table-column type="index" width="55" label="序号"></el-table-column>
                <el-table-column prop="driverName" label="机动车驾驶员姓名"></el-table-column>
                <el-table-column prop="enterpriseName" label="车辆所属公司"></el-table-column>
@@ -111,7 +111,7 @@
        },
        showDetail(row) {
            getDriverInfo(row.id).then(res=>{
                this.$refs.detailModal.initData(res)
            })
        },
    }
src/view/systemManage/driver/service.js
@@ -2,7 +2,7 @@
// 获取司机详情
export const getDriverInfo = (id) => {
    return axios.get(`/system/driver/getDriverInfo/{id}/${id}`)
    return axios.get(`/system/driver/getDriverInfo/${id}`)
}
// 获取司机列表
src/view/systemManage/role/index.vue
@@ -15,7 +15,7 @@
      <div class="add_btn">
        <el-button icon="el-icon-plus" @click="add" type="primary">添加角色</el-button>
      </div>
      <el-table ref="tableSort" v-loading="listLoading" :height="height" stripe :data="data"
      <el-table ref="tableSort" v-loading="listLoading" stripe :data="data"
        :element-loading-text="elementLoadingText">
        <el-table-column type="index" width="55" label="序号"></el-table-column>
        <el-table-column prop="roleName" label="角色名称"></el-table-column>
src/view/systemManage/user/index.vue
@@ -36,7 +36,7 @@
        @click="dialogVisible = true">删除</el-button> -->
    </div>
    <div class="table-box ml--30 mt--23 mr--30">
      <el-table :data="data" border stripe style="width: 100%" :height="height" :element-loading-text="'正在加载...'">
      <el-table :data="data" border stripe style="width: 100%" :element-loading-text="'正在加载...'">
        <el-table-column prop="nickName" label="姓名"></el-table-column>
        <el-table-column prop="phonenumber" label="联系电话">
        </el-table-column>