董国庆
2025-04-18 65ed743f1b195c6a850792f86466a2f03c8b0724
Merge branch 'master' of http://120.76.84.145:10101/gitblit/r/H5/shehong-vehicle-supervision
13个文件已修改
3个文件已添加
370 ■■■■ 已修改文件
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 47 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/view/car-manage/index.vue 4 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/view/car-manage/service.js 6 ●●●●● 补丁 | 查看 | 原始文档 | 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
@@ -173,7 +173,7 @@
            <div v-if="showWarnDetail" class="fixed">
                <div class="card">
                    <div class="title fs--18 color2">视频信号遮挡报警</div>
                    <div id="mapContainer"></div>
                    <div id="mapContainers"></div>
                    <div class="">
                        <div class="flex mt--15">
                            <div class="w--100 shrink0 color1">司机:</div>
@@ -271,7 +271,7 @@
                    ],
                })
                    .then((AMap) => {
                        this.map = new AMap.Map("mapContainer", {
                        this.map = new AMap.Map("mapContainers", {
                            center: [row.lon, row.lat],
                            zoom: 15,
                        });
@@ -510,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>
src/view/car-manage/service.js
@@ -34,4 +34,10 @@
//获取车辆详情实时视频
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/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)
                }
            })