src/view/car-manage/components/detailOrderModal.vue | ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史 | |
src/view/car-manage/detail.vue | ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史 | |
src/view/car-manage/index.vue | ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史 | |
src/view/car-manage/service.js | ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史 |
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: moment(this.searchForm.date[0]).format('YYYY-MM-DD 00:00:00'), endTime: moment(this.searchForm.date[1]).format('YYYY-MM-DD 23:59:59'), } 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-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.id"></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 = Number(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 }) }