右侧实时图像 接口联调 视频流解析

地图图标 控制显示隐藏
This commit is contained in:
fengshiwang 2025-06-04 16:46:08 +08:00
parent f70b2905dd
commit 9ec6f41e6f
8 changed files with 375 additions and 169 deletions

6
package-lock.json generated
View File

@ -13,6 +13,7 @@
"dayjs": "^1.11.10",
"echarts": "^5.5.0",
"element-plus": "^2.6.2",
"hls.js": "^1.6.5",
"lodash-es": "^4.17.21",
"mitt": "^3.0.1",
"mockjs": "^1.1.0",
@ -2892,6 +2893,11 @@
"he": "bin/he"
}
},
"node_modules/hls.js": {
"version": "1.6.5",
"resolved": "https://registry.npmjs.org/hls.js/-/hls.js-1.6.5.tgz",
"integrity": "sha512-KMn5n7JBK+olC342740hDPHnGWfE8FiHtGMOdJPfUjRdARTWj9OB+8c13fnsf9sk1VtpuU2fKSgUjHvg4rNbzQ=="
},
"node_modules/hosted-git-info": {
"version": "2.8.9",
"resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-2.8.9.tgz",

View File

@ -16,6 +16,7 @@
"dayjs": "^1.11.10",
"echarts": "^5.5.0",
"element-plus": "^2.6.2",
"hls.js": "^1.6.5",
"lodash-es": "^4.17.21",
"mitt": "^3.0.1",
"mockjs": "^1.1.0",

View File

@ -86,3 +86,23 @@ export const todayHourly=(param:any={})=>{
export const trafficTrend=(param:any={})=>{
return GET('/device/trafficFlow/trafficTrend',param)
}
/**事件历史记录管理 历史数据 */
export const meventListHistory=(param:any={})=>{
return GET('/device/mevent/listHistory',param)
}
/**数据字典-根据类型获取字典数据 */
export const getDictData = (dictType: string) => {
return GET(`/system/dict/data/type/${dictType}`);
}
/**视频监控设备列表*/
export const getVideoDeviceList = (param: any = {}) => {
return POST('/video/device/list', param);
}
/**视频流 */
export const videoStream=(param:any={})=>{
return GET('/video/stream',param)
}

View File

@ -1,52 +1,79 @@
<script setup lang="ts">
import { ref, computed } from "vue";
import { ref, computed, onMounted } from "vue";
import { meventListHistory } from "@/api/modules/index";
import dayjs from "dayjs";
interface EventItem {
type: string;
location: string;
time: string;
status: "处置中" | "待处置";
}
//
const wrongWayEvents = ref<EventItem[]>([
{
location: "邯郸方向 K34+230",
time: "2025-4-15 12:34:56",
status: "处置中",
},
const allEvents = ref<EventItem[]>([
// {
// type: "",
// location: " K34+230",
// time: "2025-4-15 12:34:56",
// status: "",
// },
// {
// type: "",
// location: " K34+230",
// time: "2025-4-15 12:34:56",
// status: "",
// },
// {
// type: "",
// location: " K34+230",
// time: "2025-4-15 12:34:56",
// status: "",
// },
]);
const roadWorkEvents = ref<EventItem[]>([
{
location: "邯郸方向 K34+230",
time: "2025-4-15 12:34:56",
status: "待处置",
},
]);
// const wrongWayEvents = computed(() =>
// allEvents.value.filter((e) => e.type === "")
// );
// const roadWorkEvents = computed(() =>
// allEvents.value.filter((e) => e.type === "")
// );
const formatTime = (time: string) => {
return dayjs(time).format("YYYY-MM-DD HH:mm:ss");
};
onMounted(()=>{
const endTime = dayjs().endOf('day').format('YYYY-MM-DD HH:mm:ss');
const startTime = dayjs().subtract(6, 'day').startOf('day').format('YYYY-MM-DD HH:mm:ss');
meventListHistory({startTime, endTime}).then((res)=>{
console.log(res, 'rrreeesss')
allEvents.value = res.rows.map((item: any) => ({
type: item.eventType,
location: (item.direction === '1' ? '黄骅港方向 ' : item.direction === '2' ? '邯郸方向 ' : '') + item.pilenum,
time: item.time,
status: item.status == '0' ? '待处置' : '处置中',
}));
})
})
</script>
<template>
<div class="event-task">
<div class="event-section">
<div v-for="(event, index) in allEvents"
:key="index" class="event-section">
<div class="section-header">
<div style="color: #fbfbfc">车辆逆行</div>
<div style="color: #fbfbfc">{{event.type}}</div>
<div
v-if="wrongWayEvents.length"
class="event-status"
:class="{ processing: wrongWayEvents[0].status === '处置中' }"
:class="{
processing: event.status === '处置中',
pending: event.status === '待处置'
}"
>
{{ wrongWayEvents[0].status }}
{{ event.status }}
</div>
</div>
<div class="event-list" v-if="wrongWayEvents.length">
<div class="event-list">
<div
v-for="(event, index) in wrongWayEvents"
:key="index"
class="event-item"
>
<div class="event-info">
@ -55,10 +82,10 @@ const formatTime = (time: string) => {
</div>
</div>
</div>
<div v-else class="empty-state">暂无逆行事件</div>
<!-- <div v-else class="empty-state">暂无逆行事件</div> -->
</div>
<div class="event-section">
<!-- <div class="event-section">
<div class="section-header">
<div style="color: #fbfbfc">道路施工</div>
<div
@ -82,7 +109,7 @@ const formatTime = (time: string) => {
</div>
</div>
<div v-else class="empty-state">暂无施工事件</div>
</div>
</div> -->
</div>
</template>
@ -96,7 +123,22 @@ const formatTime = (time: string) => {
background: transparent;
padding: 20px;
box-sizing: border-box;
overflow-y: scroll;
z-index: 9999;
&::-webkit-scrollbar {
width: 8px;
background: transparent;
}
&::-webkit-scrollbar-thumb {
background: linear-gradient(180deg, #00c6ff 0%, #085b9f 100%);
border-radius: 4px;
}
&::-webkit-scrollbar-track {
background: rgba(0, 198, 255, 0.08);
border-radius: 4px;
}
.event-section {
height: calc(50% - 10px);
background: linear-gradient(
@ -112,7 +154,7 @@ const formatTime = (time: string) => {
display: flex;
flex-direction: column;
position: relative;
overflow: hidden;
// overflow: hidden;
&::before {
content: "";
@ -129,20 +171,20 @@ const formatTime = (time: string) => {
);
}
&::after {
content: "";
position: absolute;
bottom: 0;
left: 0;
right: 0;
height: 1px;
background: linear-gradient(
90deg,
transparent,
rgba(0, 198, 255, 0.3),
transparent
);
}
// &::after {
// content: "";
// position: absolute;
// bottom: 0;
// left: 0;
// right: 0;
// height: 1px;
// background: linear-gradient(
// 90deg,
// transparent,
// rgba(0, 198, 255, 0.3),
// transparent
// );
// }
.section-header {
display: flex;
@ -151,22 +193,22 @@ const formatTime = (time: string) => {
gap: 12px;
margin-bottom: 20px;
padding-bottom: 12px;
border-bottom: 1px solid rgba(0, 198, 255, 0.1);
position: relative;
border: 1px solid rgba(0, 198, 255, 0.1);
// position: relative;
background-color: #0e2d51;
border-radius: 15px;
padding: 14px;
font-size: 22px;
&::after {
content: "";
position: absolute;
bottom: -1px;
left: 0;
width: 100px;
height: 1px;
background: linear-gradient(90deg, #00c6ff, transparent);
}
// &::after {
// content: "";
// position: absolute;
// bottom: -1px;
// left: 0;
// width: 100px;
// height: 1px;
// background: linear-gradient(90deg, #00c6ff, transparent);
// }
i {
width: 32px;
@ -229,7 +271,7 @@ const formatTime = (time: string) => {
.event-list {
flex: 1;
overflow-y: auto;
//overflow-y: auto;
padding-right: 4px;
&::-webkit-scrollbar {
@ -258,36 +300,36 @@ const formatTime = (time: string) => {
position: relative;
overflow: hidden;
&::before {
content: "";
position: absolute;
top: 0;
left: 0;
right: 0;
height: 1px;
background: linear-gradient(
90deg,
transparent,
rgba(0, 198, 255, 0.2),
transparent
);
}
// &::before {
// content: "";
// position: absolute;
// top: 0;
// left: 0;
// right: 0;
// height: 1px;
// background: linear-gradient(
// 90deg,
// transparent,
// rgba(0, 198, 255, 0.2),
// transparent
// );
// }
&:hover {
background: rgba(4, 49, 128, 0.3);
border-color: rgba(0, 198, 255, 0.3);
transform: translateY(-2px);
box-shadow: 0 4px 20px rgba(0, 198, 255, 0.15);
// &:hover {
// background: rgba(4, 49, 128, 0.3);
// border-color: rgba(0, 198, 255, 0.3);
// transform: translateY(-2px);
// box-shadow: 0 4px 20px rgba(0, 198, 255, 0.15);
&::before {
background: linear-gradient(
90deg,
transparent,
rgba(0, 198, 255, 0.4),
transparent
);
}
}
// &::before {
// background: linear-gradient(
// 90deg,
// transparent,
// rgba(0, 198, 255, 0.4),
// transparent
// );
// }
// }
.event-info {
display: flex;

View File

@ -1,7 +1,12 @@
<script setup lang="ts">
import { ref } from "vue";
import { ref, onMounted, nextTick } from "vue";
import demoImage from "@/assets/img/demo.png";
import { getVideoDeviceList, videoStream } from "@/api/modules/index";
import Hls from "hls.js";
import dayjs from "dayjs";
const videoUrl = ref(""); // m3u8
const videoRef = ref<HTMLVideoElement | null>(null);
//
const deviceStatus = ref({
normal: 248,
@ -10,49 +15,50 @@ const deviceStatus = ref({
//
const monitoringGroups = ref([
{
id: 1,
name: "邯港方向",
count: 5,
expanded: true,
points: [
{ id: 1, name: "邯港方向监控001", status: "正常" },
{ id: 2, name: "邯港方向监控002", status: "正常" },
{ id: 3, name: "邯港方向监控003", status: "正常" },
{ id: 4, name: "邯港方向监控004", status: "正常" },
{ id: 5, name: "邯港方向监控005", status: "正常" },
],
},
{
id: 2,
name: "黄驿港方向",
count: 12,
expanded: false,
points: [],
},
{
id: 3,
name: "收费站",
count: 5,
expanded: true,
points: [
{ id: 6, name: "收费站监控001", status: "正常" },
{ id: 7, name: "收费站监控002", status: "正常" },
{ id: 8, name: "收费站监控003", status: "正常" },
{ id: 9, name: "收费站监控004", status: "正常" },
],
},
// {
// id: 1,
// name: "",
// count: 5,
// expanded: true,
// points: [
// { id: 1, name: "001", status: "" },
// { id: 2, name: "002", status: "" },
// { id: 3, name: "003", status: "" },
// { id: 4, name: "004", status: "" },
// { id: 5, name: "005", status: "" },
// ],
// },
// {
// id: 2,
// name: "驿",
// count: 12,
// expanded: false,
// points: [],
// },
// {
// id: 3,
// name: "",
// count: 5,
// expanded: true,
// points: [
// { id: 6, name: "001", status: "" },
// { id: 7, name: "002", status: "" },
// { id: 8, name: "003", status: "" },
// { id: 9, name: "004", status: "" },
// ],
// },
]);
//
const fixedRoadMonitors = ref([
{ id: 101, name: "高速公路监控001", image: demoImage },
{ id: 102, name: "高速公路监控002", image: demoImage },
{ id: 103, name: "高速公路监控003", image: demoImage },
{ id: 104, name: "高速公路监控004", image: demoImage },
{ id: 105, name: "高速公路监控005", image: demoImage },
{ id: 106, name: "高速公路监控006", image: demoImage },
]);
// //
// const fixedRoadMonitors = ref([
// { id: 101, name: "001", src:'' },
// { id: 102, name: "002", src:'' },
// { id: 103, name: "003", src:'' },
// { id: 104, name: "004", src:'' },
// { id: 105, name: "005", src:'' },
// { id: 106, name: "006", src:'' },
// ]);
const fixedRoadMonitors = ref<any[]>([]);
//
const toggleGroup = (group: any) => {
@ -62,15 +68,72 @@ const toggleGroup = (group: any) => {
//
const selectedPoints = ref<any[]>([]);
//
const togglePoint = (point: any) => {
const index = selectedPoints.value.findIndex((p) => p.id === point.id);
// //
// const togglePoint = (point: any) => {
// const index = selectedPoints.value.findIndex((p) => p.channelCode === point.channelCode);
// if (index > -1) {
// selectedPoints.value.splice(index, 1);
// } else {
// selectedPoints.value.push(point);
// }
// videoStream({deviceCode:'00000000001311027500',channelCode:'00000000001311028602',}).then((res)=>{
// videoUrl.value = res;
// nextTick();
// if (videoRef.value) {
// if (Hls.isSupported()) {
// const hls = new Hls();
// hls.loadSource(videoUrl.value);
// hls.attachMedia(videoRef.value);
// } else if (videoRef.value.canPlayType('application/vnd.apple.mpegurl')) {
// videoRef.value.src = videoUrl.value;
// }
// }
// })
// };
const togglePoint = async (point: any) => {
const index = selectedPoints.value.findIndex((p) => p.channelCode === point.channelCode);
if (index > -1) {
//
selectedPoints.value.splice(index, 1);
const monitorIdx = fixedRoadMonitors.value.findIndex(m => m.channelCode === point.channelCode);
if (monitorIdx > -1) {
fixedRoadMonitors.value.splice(monitorIdx, 1);
}
} else {
// 6
if (selectedPoints.value.length >= 6) {
const removed = selectedPoints.value.shift();
// fixedRoadMonitors
const rmIdx = fixedRoadMonitors.value.findIndex(m => m.channelCode === removed.channelCode);
if (rmIdx > -1) fixedRoadMonitors.value.splice(rmIdx, 1);
}
selectedPoints.value.push(point);
// fixedRoadMonitors
const res = await videoStream({ deviceCode: point.deviceCode, channelCode: point.channelCode });
// fixedRoadMonitors6
if (fixedRoadMonitors.value.length >= 6) {
fixedRoadMonitors.value.shift();
}
fixedRoadMonitors.value.push({
...point,
src: res
});
}
};
onMounted(()=>{
const endTime = dayjs().endOf('day').format('YYYY-MM-DD HH:mm:ss');
getVideoDeviceList({endTime:endTime, currentPage: 1, pageSize: 2}).then((res)=>{ //
console.log(res, 'rrrrrrrrrrrrrrrrrrrrrrrrrsssssssssssss')
if(res.code == 200){
res.data.data.forEach((item)=>{
item.expanded = true
})
monitoringGroups.value = res.data.data
}
console.log(monitoringGroups.value, 'monitoringGroups.value')
})
})
</script>
<template>
@ -107,36 +170,36 @@ const togglePoint = (point: any) => {
</div>
<div class="list-content">
<div
v-for="group in monitoringGroups"
:key="group.id"
v-for="(group, index) in monitoringGroups"
:key="index"
class="group-item"
>
<div class="group-header" @click="toggleGroup(group)">
<div class="group-name">
<i class="arrow" :class="{ expanded: group.expanded }"></i>
<!-- <i class="arrow" :class="{ expanded: group.expanded }"></i> -->
<span style="font-size: 22.5px"
>{{ group.name }}{{ group.count }}</span
>{{ group.deviceName }}{{ group.channels.length }}</span
>
</div>
</div>
<div class="group-content" v-show="group.expanded">
<div class="group-content" v-show="group.channels">
<div
v-for="point in group.points"
:key="point.id"
v-for="(point, vic) in group.channels"
:key="point.channelCode"
class="point-item"
:class="{ active: selectedPoints.some((p) => p.id === point.id) }"
:class="{ active: selectedPoints.some((p) => p.channelCode === point.channelCode) }"
@click="togglePoint(point)"
>
<div class="point-info">
<span class="point-name">{{ point.name }}</span>
<span class="point-name">{{ point.channelName }}</span>
<div class="status-wrapper">
<span
class="status-indicator"
:class="point.status === '正常' ? 'normal' : 'warning'"
:class="point.status === 1 ? 'normal' : 'warning'"
></span>
<span
class="point-status"
:class="point.status === '正常' ? 'normal' : 'warning'"
:class="point.status === 1 ? 'normal' : 'warning'"
>
{{ point.status }}
</span>
@ -150,7 +213,7 @@ const togglePoint = (point: any) => {
<!-- 右侧图像网格 - 改为固定显示六张图片 -->
<div class="monitor-grid">
<div class="fixed-grid-container">
<!-- <div class="fixed-grid-container">
<div
v-for="monitor in fixedRoadMonitors"
:key="monitor.id"
@ -161,7 +224,34 @@ const togglePoint = (point: any) => {
<div class="corner-decoration bottom-left"></div>
<div class="corner-decoration bottom-right"></div>
<div class="image-container">
<img :src="monitor.image" :alt="monitor.name" />
<video
ref="videoRef"
v-if="videoUrl"
controls
autoplay
style="width:100%;height:300px;background:#000"
></video>
</div>
</div>
</div> -->
<div class="fixed-grid-container">
<div
v-for="(monitor, idx) in fixedRoadMonitors"
:key="monitor.channelCode || monitor.id || idx"
class="grid-item"
>
<div class="corner-decoration top-left"></div>
<div class="corner-decoration top-right"></div>
<div class="corner-decoration bottom-left"></div>
<div class="corner-decoration bottom-right"></div>
<div class="image-container">
<video
v-if="monitor.src"
:src="monitor.src"
controls
autoplay
style="width:100%;height:300px;background:#000"
/>
</div>
</div>
</div>

View File

@ -324,27 +324,35 @@ const items = ref([
{
defaultImg: eqm1Default,
activeImg: eqm1Active,
isActive: false,
isActive: true, //
type: '44', //
},
{
defaultImg: eqm2Default,
activeImg: eqm2Active,
isActive: false,
isActive: true,
type: '22', //
},
{
defaultImg: eqm3Default,
activeImg: eqm3Active,
isActive: false,
isActive: true,
type: '53', // 广
},
{
defaultImg: eqm4Default,
activeImg: eqm4Active,
isActive: false,
isActive: true,
type: '15', //
},
]);
const toggleSelection = (index: number) => {
items.value[index].isActive = !items.value[index].isActive;
// /
window.dispatchEvent(new CustomEvent('toggle-map-device', {
detail: { type: items.value[index].type, show: items.value[index].isActive }
}));
};
</script>

View File

@ -50,14 +50,14 @@ const iconModules = import.meta.glob("@/assets/img/map/*.png", {
function getIcon(type, active = false) {
const iconName =
{
1: active ? "lianzhen.png" : "lianzhen.png",
2: active ? "datun.png" : "datun.png",
3: active ? "zhaowang.png" : "zhaowang.png",
4: active ? "dongguang.png" : "dongguang.png",
5: active ? "nanpi.png" : "nanpi.png",
6: active ? "zhaizi.png" : "zhaizi.png",
7: active ? "xiaozhuang.png" : "xiaozhuang.png",
8: active ? "warning.png" : "warning.png",
// 1: active ? "lianzhen.png" : "lianzhen.png",
// 2: active ? "datun.png" : "datun.png",
// 3: active ? "zhaowang.png" : "zhaowang.png",
// 4: active ? "dongguang.png" : "dongguang.png",
// 5: active ? "nanpi.png" : "nanpi.png",
// 6: active ? "zhaizi.png" : "zhaizi.png",
// 7: active ? "xiaozhuang.png" : "xiaozhuang.png",
// 8: active ? "warning.png" : "warning.png",
22: active ? "video-active.png" : "video.png",
44: active ? "weather-active.png" : "weather.png",
53: active ? "broadcast-active.png" : "broadcast.png",
@ -101,6 +101,18 @@ const customDevices = [
];
onMounted(async () => {
//
window.addEventListener('toggle-map-device', (e) => {
const { type, show } = e.detail;
showType.value[type] = show;
markerMap.value[type]?.forEach((marker) => {
if (show) {
map.addOverlay(marker);
} else {
map.removeOverlay(marker);
}
});
});
await nextTick();
map = new BMap.Map("baiduMap");
const point = new BMap.Point(116.827009, 37.890385);

View File

@ -1,19 +1,21 @@
<script setup lang="ts">
import { ref, onMounted } from "vue";
import { ref, onMounted, computed } from "vue";
import EventStatus from "./EventStatus.vue";
import EventTask from "./EventTask.vue";
import RealTimeImage from "./RealTimeImage.vue";
import { weatherForecast, weatherHourly, todayStatusCount, todayHourly } from "@/api/modules/index";
import { weatherForecast, weatherHourly, todayStatusCount, todayHourly, getDictData } from "@/api/modules/index";
import { useTodayTime, } from "@/utils/packge";
const { todayTime, getTodayTime } = useTodayTime();
// API
const pendingCount = ref<string>('');
const processingCount = ref<string>('');
const serviceArea = ref<any[]>([]);
const Hub = ref<any[]>([]);
const Intercommunication = ref<any[]>([]);
//
const selectedServiceArea = ref("全部");
const serviceAreas = ref(["全部", "南皮服务区", "东光服务区"]);
const serviceAreas = ref([]);
//
const handleServiceAreaChange = (area: string) => {
selectedServiceArea.value = area;
@ -28,25 +30,49 @@ const areaTypes = ref([
//
const toggleAreaType = (id: string) => {
const areaType = areaTypes.value.find((type) => type.id === id);
if (areaType) {
areaType.selected = !areaType.selected;
}
areaTypes.value.forEach((type) => {
type.selected = type.id === id;
});
};
onMounted(()=>{
todayStatusCount({todayTime: todayTime.value}).then((res: any) => {
if(res.code === 200){
res.data.forEach((item: any) => {
if(item.status == '0'){ //
pendingCount.value = item.count
pendingCount.value = item.count || 0
} else if(item.status == '2'){ //
processingCount.value = item.count
processingCount.value = item.count || 0
}
});
}
})
getDictData("hb_service_area").then((res: any) => { //
if (res.code === 200) {
serviceArea.value = res.data || [];
}
});
getDictData("hb_hub").then((res: any) => { //
if (res.code === 200) {
Hub.value = res.data || [];
}
});
getDictData("hb_interchange").then((res: any) => { //
if (res.code === 200) {
Intercommunication.value = res.data || [];
}
});
})
const currentAreaList = computed(() => {
const selectedType = areaTypes.value.find((type) => type.selected);
if (selectedType?.id === "service") {
return serviceArea.value;
} else if (selectedType?.id === "interchange") {
return Intercommunication.value;
} else if (selectedType?.id === "hub") {
return Hub.value;
}
return [];
});
const isDropdownOpen = ref(false);
</script>
@ -91,16 +117,16 @@ const isDropdownOpen = ref(false);
</div>
<div class="dropdown-menu" v-show="isDropdownOpen">
<div
v-for="area in serviceAreas"
:key="area"
v-for="(item, index) in currentAreaList"
:key="index"
class="dropdown-item"
:class="{ active: selectedServiceArea === area }"
:class="{ active: selectedServiceArea === item.dictLabel }"
@click="
handleServiceAreaChange(area);
handleServiceAreaChange(item.dictLabel);
isDropdownOpen = false;
"
>
{{ area }}
{{ item.dictLabel }}
</div>
</div>
</div>
@ -118,6 +144,7 @@ const isDropdownOpen = ref(false);
height: 100%;
display: flex;
flex-direction: column;
margin-right: 20px;
// justify-content: space-between;
// align-items: flex-start;
.top {