|
|
@@ -0,0 +1,1038 @@
|
|
|
+<template>
|
|
|
+ <div class="screen-container">
|
|
|
+ <div class="top-section">
|
|
|
+ <div class="system-title">风门视频图像综合解析系统</div>
|
|
|
+ <div class="top-inner">
|
|
|
+ <div class="top-left">
|
|
|
+ <div class="door-switch">
|
|
|
+ <label></label>
|
|
|
+ <select v-model="selectDoor" @change="switchDoor">
|
|
|
+ <option value="all">全部风门</option>
|
|
|
+ <option v-for="door in doorList" :key="door.id" :value="door.id">
|
|
|
+ {{ door.name }}
|
|
|
+ </option>
|
|
|
+ </select>
|
|
|
+ <button class="btn btn-primary" @click="refreshVideo">刷新视频</button>
|
|
|
+
|
|
|
+ <!-- 当前时间 -->
|
|
|
+ <div class="time-box">当前时间:{{ currentTime }}</div>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <div class="video-double">
|
|
|
+ <div class="video-item">
|
|
|
+ <div class="video-title">前门 [{{ currentDoorName }}]</div>
|
|
|
+ <div class="video-box">
|
|
|
+ <!-- 视频标签:所有属性齐全,兼容所有浏览器 -->
|
|
|
+ <video class="video-bg" autoplay loop muted playsinline webkit-playsinline controls>
|
|
|
+ <source src="@/assets/images/wind-video/video/v1.mp4" type="video/mp4">
|
|
|
+ <!-- 兜底提示:视频不支持时显示 -->
|
|
|
+ 您的浏览器不支持视频播放
|
|
|
+ </video>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ <div class="video-item">
|
|
|
+ <div class="video-title">后门 [{{ currentDoorName }}]</div>
|
|
|
+ <div class="video-box">
|
|
|
+ <video class="video-bg" autoplay loop muted playsinline webkit-playsinline controls>
|
|
|
+ <source src="@/assets/images/wind-video/video/v2.mp4" type="video/mp4">
|
|
|
+ <!-- 兜底提示:视频不支持时显示 -->
|
|
|
+ 您的浏览器不支持视频播放
|
|
|
+ </video>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <div class="top-right">
|
|
|
+ <div class="status-panel">
|
|
|
+ <div class="status-title">风门状态总览</div>
|
|
|
+ <div class="status-grid">
|
|
|
+ <div class="status-item normal">
|
|
|
+ <div class="status-num">{{ statusData.normal }}</div>
|
|
|
+ <div class="status-name">正常运行</div>
|
|
|
+ </div>
|
|
|
+ <div class="status-item offline">
|
|
|
+ <div class="status-num">{{ statusData.offline }}</div>
|
|
|
+ <div class="status-name">设备离线</div>
|
|
|
+ </div>
|
|
|
+ <div class="status-item fault">
|
|
|
+ <div class="status-num">{{ statusData.fault }}</div>
|
|
|
+ <div class="status-name">设备故障</div>
|
|
|
+ </div>
|
|
|
+ <div class="status-item warn">
|
|
|
+ <div class="status-num">{{ statusData.warn }}</div>
|
|
|
+ <div class="status-name">实时预警</div>
|
|
|
+ </div>
|
|
|
+ <div class="status-item car-pass">
|
|
|
+ <div class="status-num">{{ statusData.carPass }}</div>
|
|
|
+ <div class="status-name">今日车通</div>
|
|
|
+ </div>
|
|
|
+ <div class="status-item person-pass">
|
|
|
+ <div class="status-num">{{ statusData.personPass }}</div>
|
|
|
+ <div class="status-name">今日人通</div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <div class="alarm-panel">
|
|
|
+ <div class="alarm-title">
|
|
|
+ <span>实时告警</span>
|
|
|
+ <div>
|
|
|
+ <span class="alarm-badge">{{ alarmList.length }}</span>
|
|
|
+ <button class="btn btn-sm" @click="clearAlarm">清空</button>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ <div class="alarm-scroll" ref="alarmScroll">
|
|
|
+ <div class="alarm-item" v-for="(item, idx) in alarmList" :key="idx"
|
|
|
+ :class="[item.level, item.new ? 'new' : '']">
|
|
|
+ <span class="alarm-time">{{ item.time }}</span>
|
|
|
+ <span class="alarm-door">{{ getDoorName(item.doorId) }}</span>
|
|
|
+ <span class="alarm-type">{{ item.type }}</span>
|
|
|
+ <span class="alarm-desc">{{ item.desc }}</span>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <div class="middle-section">
|
|
|
+ <div class="search-panel">
|
|
|
+ <div class="search-item">
|
|
|
+ <label>时间范围:</label>
|
|
|
+ <input type="datetime-local" v-model="searchForm.startTime" class="search-input">
|
|
|
+ <span class="split">-</span>
|
|
|
+ <input type="datetime-local" v-model="searchForm.endTime" class="search-input">
|
|
|
+ <button class="btn btn-sm btn-default" @click="selectToday">今日</button>
|
|
|
+ <button class="btn btn-sm btn-default" @click="select7d">近7天</button>
|
|
|
+ </div>
|
|
|
+ <div class="search-item">
|
|
|
+ <label>告警类型:</label>
|
|
|
+ <select v-model="searchForm.alarmType" class="search-select">
|
|
|
+ <option value="all">全部类型</option>
|
|
|
+ <option v-for="type in alarmTypeList" :key="type.value" :value="type.value">{{ type.label }}</option>
|
|
|
+ </select>
|
|
|
+ </div>
|
|
|
+ <div class="search-item">
|
|
|
+ <label>风门选择:</label>
|
|
|
+ <select v-model="searchForm.doorId" class="search-select">
|
|
|
+ <option value="all">全部风门</option>
|
|
|
+ <option v-for="door in doorList" :key="door.id" :value="door.id">{{ door.name }}</option>
|
|
|
+ </select>
|
|
|
+ </div>
|
|
|
+ <div class="search-btn-group">
|
|
|
+ <button class="btn btn-primary" @click="handleSearch">一键检索</button>
|
|
|
+ <button class="btn btn-default" @click="resetSearch">重置</button>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <div class="bottom-section">
|
|
|
+ <div class="content-wrap">
|
|
|
+ <div class="img-list">
|
|
|
+ <div class="img-item" v-for="(item, idx) in currentImgList" :key="idx" @click="openImgModal(item)">
|
|
|
+ <img :src="item.imgUrl" class="img-thumbnail">
|
|
|
+ <div class="img-desc">
|
|
|
+ <p class="desc-line1">{{ getDoorName(item.doorId) }}</p>
|
|
|
+ <p class="desc-line2">
|
|
|
+ <span v-if="item.type === '车辆通行'">车辆通行/车牌号{{ item.license }}</span>
|
|
|
+ <span v-else-if="item.type === '人员通行'">人员通行/{{ item.personName }}</span>
|
|
|
+ <span v-else-if="item.type === '风门未关严'">风门未关严 缝隙{{ item.gap }}mm</span>
|
|
|
+ </p>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <div class="right-data-panel">
|
|
|
+ <div class="tab-nav">
|
|
|
+ <div class="tab-item" :class="{ active: activeTab === 'car' }" @click="activeTab = 'car'">车辆通行记录</div>
|
|
|
+ <div class="tab-item" :class="{ active: activeTab === 'person' }" @click="activeTab = 'person'">人员通行记录</div>
|
|
|
+ <div class="tab-item" :class="{ active: activeTab === 'unclose' }" @click="activeTab = 'unclose'">风门未关严记录
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <div class="data-list">
|
|
|
+ <div class="data-header" v-if="activeTab === 'car'">
|
|
|
+ <div class="data-col">序号</div>
|
|
|
+ <div class="data-col">风门名称</div>
|
|
|
+ <div class="data-col">车牌号</div>
|
|
|
+ <div class="data-col">通行时间</div>
|
|
|
+ <div class="data-col">结果</div>
|
|
|
+ </div>
|
|
|
+ <div class="data-header" v-else-if="activeTab === 'person'">
|
|
|
+ <div class="data-col">序号</div>
|
|
|
+ <div class="data-col">风门名称</div>
|
|
|
+ <div class="data-col">人员信息</div>
|
|
|
+ <div class="data-col">通行时间</div>
|
|
|
+ <div class="data-col">结果</div>
|
|
|
+ </div>
|
|
|
+ <div class="data-header" v-else>
|
|
|
+ <div class="data-col">序号</div>
|
|
|
+ <div class="data-col">风门名称</div>
|
|
|
+ <div class="data-col">检测时间</div>
|
|
|
+ <div class="data-col">缝隙(mm)</div>
|
|
|
+ <div class="data-col">等级</div>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <div class="data-row" v-for="(item, idx) in carList" :key="idx" v-if="activeTab === 'car'">
|
|
|
+ <div class="data-col">{{ idx + 1 }}</div>
|
|
|
+ <div class="data-col">{{ getDoorName(item.doorId) }}</div>
|
|
|
+ <div class="data-col">{{ item.license }}</div>
|
|
|
+ <div class="data-col">{{ item.time }}</div>
|
|
|
+ <div class="data-col">{{ item.result }}</div>
|
|
|
+ </div>
|
|
|
+ <div class="data-row" v-for="(item, idx) in personList" :key="idx" v-if="activeTab === 'person'">
|
|
|
+ <div class="data-col">{{ idx + 1 }}</div>
|
|
|
+ <div class="data-col">{{ getDoorName(item.doorId) }}</div>
|
|
|
+ <div class="data-col">{{ item.personInfo }}</div>
|
|
|
+ <div class="data-col">{{ item.time }}</div>
|
|
|
+ <div class="data-col">{{ item.result }}</div>
|
|
|
+ </div>
|
|
|
+ <div class="data-row" v-for="(item, idx) in uncloseList" :key="idx" v-if="activeTab === 'unclose'">
|
|
|
+ <div class="data-col">{{ idx + 1 }}</div>
|
|
|
+ <div class="data-col">{{ getDoorName(item.doorId) }}</div>
|
|
|
+ <div class="data-col">{{ item.time }}</div>
|
|
|
+ <div class="data-col">{{ item.gap }}</div>
|
|
|
+ <div class="data-col" :class="item.level">{{ item.level }}</div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <div class="img-modal" v-show="showImgModal" @click="closeImgModal">
|
|
|
+ <div class="modal-content" @click.stop>
|
|
|
+ <div class="modal-header">
|
|
|
+ <span>图像详情</span>
|
|
|
+ <button class="close-btn" @click="closeImgModal">×</button>
|
|
|
+ </div>
|
|
|
+ <img :src="currentModalImg" class="modal-img">
|
|
|
+ <div class="modal-info">
|
|
|
+ <p>{{ modalInfo }}</p>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+</template>
|
|
|
+
|
|
|
+<script setup lang="ts">
|
|
|
+import { ref, onMounted, onUnmounted } from 'vue'
|
|
|
+import { doorList,statusData,alarmTypeList,createRandomAlarm,currentImgList,carList,personList,uncloseList } from './windDoorVideo.data'
|
|
|
+
|
|
|
+const selectDoor = ref('all')
|
|
|
+const currentDoorId = ref(1)
|
|
|
+const currentDoorName = ref('五盘区402辅运大巷风门')
|
|
|
+const videoUrl1 = ref('https://picsum.photos/800/600?random=1')
|
|
|
+const videoUrl2 = ref('https://picsum.photos/800/600?random=2')
|
|
|
+const showImgModal = ref(false)
|
|
|
+const currentModalImg = ref('')
|
|
|
+const modalInfo = ref('')
|
|
|
+const activeTab = ref('car')
|
|
|
+const currentTime = ref('')
|
|
|
+// https获取监测数据
|
|
|
+let timeTimer: null | NodeJS.Timeout = null;
|
|
|
+let addAlarmTimer: null | NodeJS.Timeout = null;// 新增:定时添加告警定时器
|
|
|
+let alarmTimer: null | NodeJS.Timeout = null; // 原有滚动定时器
|
|
|
+const alarmScroll = ref<any>(null)
|
|
|
+const searchForm = ref({ startTime: '', endTime: '', alarmType: 'all', doorId: 'all' })
|
|
|
+const alarmList = ref<any[]>([
|
|
|
+ { time: '16:30:22', doorId: 2, type: '防夹人预警', desc: '危险区域有人闯入,已触发PLC停止', level: 'red' },
|
|
|
+ { time: '16:28:15', doorId: 4, type: '风门未关严', desc: '门板闭合缝隙15mm,超标10mm', level: 'yellow' },
|
|
|
+ { time: '16:25:36', doorId: 4, type: '异物卡堵', desc: '门体通道检测到石块异物,影响闭合', level: 'orange' },
|
|
|
+ { time: '16:20:18', doorId: 1, type: '违规停留', desc: '禁停区有车辆滞留3分钟,未及时驶离', level: 'gray' },
|
|
|
+ { time: '16:15:45', doorId: 3, type: '密封失效', desc: '疑似风门密封胶条脱落,存在漏风风险', level: 'yellow' },
|
|
|
+ { time: '16:10:30', doorId: 5, type: '车辆撞击', desc: '风门门体疑似受到轻微撞击,需检查变形', level: 'orange' },
|
|
|
+ { time: '16:05:12', doorId: 2, type: '防夹人预警', desc: '风门闭合区检测到人员肢体,紧急止停', level: 'red' },
|
|
|
+ { time: '16:00:58', doorId: 1, type: '风门未关严', desc: '门板闭合缝隙8mm,接近阈值', level: 'gray' },
|
|
|
+ { time: '15:55:33', doorId: 5, type: '异物卡堵', desc: '风门轨道有杂物', level: 'yellow' },
|
|
|
+ { time: '15:50:20', doorId: 3, type: '违规停留', desc: '禁停区有人员逗留,已语音提醒', level: 'gray' }
|
|
|
+])
|
|
|
+
|
|
|
+
|
|
|
+
|
|
|
+// 新增:定时添加新告警到顶部
|
|
|
+const startAddAlarm = () => {
|
|
|
+ // 每5-10秒随机生成1条(可自行调整时间)
|
|
|
+ const randomTime = 5000 + Math.floor(Math.random() * 5000)
|
|
|
+ addAlarmTimer = setInterval(() => {
|
|
|
+ const newAlarm = createRandomAlarm()
|
|
|
+ alarmList.value.unshift(newAlarm) // 插入到列表顶部,实现“新告警优先展示”
|
|
|
+ newAlarm.new = true;
|
|
|
+ // 1.8 秒后清除动画
|
|
|
+ setTimeout(() => {
|
|
|
+ newAlarm.new = false;
|
|
|
+ }, 1800);
|
|
|
+ // 限制列表长度,避免数据过多(保留20条,可调整)
|
|
|
+ if (alarmList.value.length > 20) {
|
|
|
+ alarmList.value.pop()
|
|
|
+ }
|
|
|
+ }, randomTime)
|
|
|
+}
|
|
|
+const getDoorName = (id) => {
|
|
|
+ const d = doorList.find(i => i.id === id)
|
|
|
+ return d ? d.name : '未知风门'
|
|
|
+}
|
|
|
+const switchDoor = () => {
|
|
|
+ if (selectDoor.value !== 'all') {
|
|
|
+ currentDoorId.value = +selectDoor.value
|
|
|
+ currentDoorName.value = getDoorName(currentDoorId.value)
|
|
|
+ videoUrl1.value = `https://picsum.photos/800/600?random=${Math.random()}`
|
|
|
+ videoUrl2.value = `https://picsum.photos/800/600?random=${Math.random()}`
|
|
|
+ }
|
|
|
+}
|
|
|
+const refreshVideo = () => {
|
|
|
+ videoUrl1.value = `https://picsum.photos/800/600?random=${Math.random()}`
|
|
|
+ videoUrl2.value = `https://picsum.photos/800/600?random=${Math.random()}`
|
|
|
+}
|
|
|
+const clearAlarm = () => { alarmList.value = [] }
|
|
|
+const formatDate = (d) => {
|
|
|
+ const y = d.getFullYear()
|
|
|
+ const m = String(d.getMonth() + 1).padStart(2, '0')
|
|
|
+ const dt = String(d.getDate()).padStart(2, '0')
|
|
|
+ const h = String(d.getHours()).padStart(2, '0')
|
|
|
+ const mi = String(d.getMinutes()).padStart(2, '0')
|
|
|
+ return `${y}-${m}-${dt}T${h}:${mi}`
|
|
|
+}
|
|
|
+
|
|
|
+const selectToday = () => {
|
|
|
+ const now = new Date()
|
|
|
+ const st = new Date(now.getFullYear(), now.getMonth(), now.getDate(), 0, 0, 0)
|
|
|
+ searchForm.value.startTime = formatDate(st)
|
|
|
+ searchForm.value.endTime = formatDate(now)
|
|
|
+}
|
|
|
+
|
|
|
+const select7d = () => {
|
|
|
+ const now = new Date()
|
|
|
+ const st = new Date(now.getTime() - 7 * 86400000)
|
|
|
+ searchForm.value.startTime = formatDate(st)
|
|
|
+ searchForm.value.endTime = formatDate(now)
|
|
|
+}
|
|
|
+
|
|
|
+const handleSearch = () => { alert('检索成功') }
|
|
|
+const resetSearch = () => { searchForm.value = { startTime: '', endTime: '', alarmType: 'all', doorId: 'all' } }
|
|
|
+
|
|
|
+const openImgModal = (item) => {
|
|
|
+ showImgModal.value = true
|
|
|
+ currentModalImg.value = item.imgUrl
|
|
|
+ let info = ''
|
|
|
+ if (item.type === '车辆通行') info = `${getDoorName(item.doorId)} | 车辆通行/${item.license}`
|
|
|
+ if (item.type === '人员通行') info = `${getDoorName(item.doorId)} | 人员通行/${item.personName}`
|
|
|
+ if (item.type === '风门未关严') info = `${getDoorName(item.doorId)} | 缝隙${item.gap}mm`
|
|
|
+ modalInfo.value = info
|
|
|
+}
|
|
|
+const closeImgModal = () => { showImgModal.value = false }
|
|
|
+
|
|
|
+const updateTime = () => {
|
|
|
+ const d = new Date()
|
|
|
+ const y = d.getFullYear()
|
|
|
+ const m = String(d.getMonth() + 1).padStart(2, '0')
|
|
|
+ const dt = String(d.getDate()).padStart(2, '0')
|
|
|
+ const h = String(d.getHours()).padStart(2, '0')
|
|
|
+ const mi = String(d.getMinutes()).padStart(2, '0')
|
|
|
+ const se = String(d.getSeconds()).padStart(2, '0')
|
|
|
+ currentTime.value = `${y}-${m}-${dt} ${h}:${mi}:${se}`
|
|
|
+}
|
|
|
+// 恢复告警滚动(优化流畅度)
|
|
|
+const startAlarmScroll = () => {
|
|
|
+ if (!alarmScroll.value) return
|
|
|
+ alarmTimer = setInterval(() => {
|
|
|
+ const scrollDom = alarmScroll.value
|
|
|
+ const firstItem = scrollDom.firstElementChild
|
|
|
+ if (firstItem) {
|
|
|
+ // 平滑滚动到下一条,贴合工业界面体验
|
|
|
+ scrollDom.scrollTop += 28
|
|
|
+ // 滚动到底部时重置
|
|
|
+ if (scrollDom.scrollTop >= scrollDom.scrollHeight - scrollDom.clientHeight) {
|
|
|
+ scrollDom.scrollTop = 0
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }, 1500) // 滚动速度(数值越小越快,可调整)
|
|
|
+}
|
|
|
+
|
|
|
+onMounted(() => {
|
|
|
+ selectToday()
|
|
|
+ updateTime()
|
|
|
+ timeTimer = setInterval(updateTime, 1000)
|
|
|
+ startAlarmScroll() // 恢复原有滚动
|
|
|
+ startAddAlarm() // 新增:启动定时添加新告警
|
|
|
+})
|
|
|
+
|
|
|
+onUnmounted(() => {
|
|
|
+ clearInterval(timeTimer)
|
|
|
+ if (alarmTimer) clearInterval(alarmTimer) // 销毁滚动定时器
|
|
|
+ if (addAlarmTimer) clearInterval(addAlarmTimer) // 销毁新增告警定时器
|
|
|
+})
|
|
|
+</script>
|
|
|
+
|
|
|
+<style lang="less" scoped>
|
|
|
+.screen-container {
|
|
|
+ position: relative;
|
|
|
+ width: 100vw;
|
|
|
+ height: 100%;
|
|
|
+ background: #0a102b;
|
|
|
+ color: #e0efff;
|
|
|
+ font-family: "Microsoft YaHei", sans-serif;
|
|
|
+ /* 关键修复:禁止页面整体滚动,内部区域自己滚动 */
|
|
|
+ overflow: hidden !important;
|
|
|
+ padding: 8px;
|
|
|
+ box-sizing: border-box;
|
|
|
+}
|
|
|
+
|
|
|
+.system-title {
|
|
|
+ text-align: center;
|
|
|
+ font-size: 28px;
|
|
|
+ font-weight: bold;
|
|
|
+ color: #49bbff;
|
|
|
+ text-shadow: 0 0 15px #2a82ff;
|
|
|
+ padding: 6px 0;
|
|
|
+ letter-spacing: 2px;
|
|
|
+ margin: 0 0 4px 0;
|
|
|
+}
|
|
|
+
|
|
|
+.top-section {
|
|
|
+ height: 48%;
|
|
|
+ border: 1px solid #1f4499;
|
|
|
+ border-radius: 8px;
|
|
|
+ background: rgba(8, 18, 48, 0.75);
|
|
|
+ margin-bottom: 6px;
|
|
|
+ overflow: hidden;
|
|
|
+ box-sizing: border-box;
|
|
|
+}
|
|
|
+
|
|
|
+.top-inner {
|
|
|
+ display: flex;
|
|
|
+ height: calc(100% - 40px);
|
|
|
+ padding: 6px 8px;
|
|
|
+ gap: 10px;
|
|
|
+ box-sizing: border-box;
|
|
|
+}
|
|
|
+
|
|
|
+.top-left {
|
|
|
+ flex: 7;
|
|
|
+ display: flex;
|
|
|
+ flex-direction: column;
|
|
|
+ gap: 8px;
|
|
|
+}
|
|
|
+
|
|
|
+.top-right {
|
|
|
+ flex: 3;
|
|
|
+ display: flex;
|
|
|
+ flex-direction: column;
|
|
|
+ gap: 8px;
|
|
|
+}
|
|
|
+
|
|
|
+.door-switch {
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ height: 36px;
|
|
|
+ gap: 10px;
|
|
|
+}
|
|
|
+
|
|
|
+.door-switch label {
|
|
|
+ font-size: 14px;
|
|
|
+ color: #9cc9ff;
|
|
|
+}
|
|
|
+
|
|
|
+.door-switch select {
|
|
|
+ height: 32px;
|
|
|
+ padding: 0 8px;
|
|
|
+ background: rgba(8, 18, 48, 0.8);
|
|
|
+ border: 1px solid #2a5bda;
|
|
|
+ color: #fff;
|
|
|
+ border-radius: 4px;
|
|
|
+ width: 280px;
|
|
|
+}
|
|
|
+
|
|
|
+.time-box {
|
|
|
+ font-size: 15px;
|
|
|
+ color: #49bbff;
|
|
|
+ font-weight: 500;
|
|
|
+ margin: auto;
|
|
|
+ letter-spacing: 1px;
|
|
|
+}
|
|
|
+
|
|
|
+.video-double {
|
|
|
+ display: flex;
|
|
|
+ flex: 1;
|
|
|
+ gap: 8px;
|
|
|
+}
|
|
|
+
|
|
|
+.video-item {
|
|
|
+ flex: 1;
|
|
|
+ border: 1px solid #2255cc;
|
|
|
+ border-radius: 4px;
|
|
|
+ background: #00000080;
|
|
|
+ display: flex;
|
|
|
+ flex-direction: column;
|
|
|
+}
|
|
|
+
|
|
|
+.video-title {
|
|
|
+ height: 30px;
|
|
|
+ line-height: 30px;
|
|
|
+ padding-left: 8px;
|
|
|
+ font-size: 13px;
|
|
|
+ color: #9cc9ff;
|
|
|
+ background: rgba(25, 55, 128, 0.3);
|
|
|
+}
|
|
|
+
|
|
|
+.video-box {
|
|
|
+ position: relative;
|
|
|
+ width: 100%;
|
|
|
+ flex: 1;
|
|
|
+ overflow: hidden;
|
|
|
+}
|
|
|
+
|
|
|
+.video-bg {
|
|
|
+ width: 100%;
|
|
|
+ height: 100%;
|
|
|
+ object-fit: cover;
|
|
|
+ opacity: 0.9;
|
|
|
+}
|
|
|
+
|
|
|
+.ai-mark {
|
|
|
+ position: absolute;
|
|
|
+ border: 2px solid;
|
|
|
+ background: rgba(255, 255, 255, 0.05);
|
|
|
+}
|
|
|
+
|
|
|
+.danger-area {
|
|
|
+ width: 35%;
|
|
|
+ height: 45%;
|
|
|
+ top: 22%;
|
|
|
+ left: 12%;
|
|
|
+ border-color: #ff4757;
|
|
|
+}
|
|
|
+
|
|
|
+.stop-area {
|
|
|
+ width: 25%;
|
|
|
+ height: 30%;
|
|
|
+ bottom: 12%;
|
|
|
+ right: 12%;
|
|
|
+ border-color: #ffa502;
|
|
|
+}
|
|
|
+
|
|
|
+.detect-box {
|
|
|
+ position: absolute;
|
|
|
+ border: 2px solid;
|
|
|
+}
|
|
|
+
|
|
|
+.person {
|
|
|
+ width: 50px;
|
|
|
+ height: 90px;
|
|
|
+ top: 35%;
|
|
|
+ left: 18%;
|
|
|
+ border-color: #32ff80;
|
|
|
+}
|
|
|
+
|
|
|
+.car {
|
|
|
+ width: 100px;
|
|
|
+ height: 50px;
|
|
|
+ top: 65%;
|
|
|
+ right: 18%;
|
|
|
+ border-color: #32b5ff;
|
|
|
+}
|
|
|
+
|
|
|
+.foreign {
|
|
|
+ width: 40px;
|
|
|
+ height: 40px;
|
|
|
+ top: 50%;
|
|
|
+ left: 45%;
|
|
|
+ border-color: #ff9500;
|
|
|
+}
|
|
|
+
|
|
|
+.status-panel,
|
|
|
+.alarm-panel {
|
|
|
+ flex: 1;
|
|
|
+ border: 1px solid #2255cc;
|
|
|
+ border-radius: 4px;
|
|
|
+ background: #00000060;
|
|
|
+ display: flex;
|
|
|
+ flex-direction: column;
|
|
|
+}
|
|
|
+
|
|
|
+.status-title,
|
|
|
+.alarm-title {
|
|
|
+ height: 30px;
|
|
|
+ line-height: 30px;
|
|
|
+ padding: 0 8px;
|
|
|
+ background: rgba(25, 55, 128, 0.3);
|
|
|
+ color: #9cc9ff;
|
|
|
+ font-size: 13px;
|
|
|
+ display: flex;
|
|
|
+ justify-content: space-between;
|
|
|
+ align-items: center;
|
|
|
+}
|
|
|
+
|
|
|
+.status-grid {
|
|
|
+ display: grid;
|
|
|
+ grid-template-columns: 1fr 1fr;
|
|
|
+ grid-template-rows: 1fr 1fr 1fr;
|
|
|
+ gap: 4px;
|
|
|
+ padding: 4px;
|
|
|
+ flex: 1;
|
|
|
+}
|
|
|
+
|
|
|
+.status-item {
|
|
|
+ display: flex;
|
|
|
+ flex-direction: column;
|
|
|
+ align-items: center;
|
|
|
+ justify-content: center;
|
|
|
+ background: rgba(30, 65, 140, 0.2);
|
|
|
+ border-radius: 4px;
|
|
|
+}
|
|
|
+
|
|
|
+.status-num {
|
|
|
+ font-size: 20px;
|
|
|
+ font-weight: bold;
|
|
|
+ color: #fff;
|
|
|
+}
|
|
|
+
|
|
|
+.status-name {
|
|
|
+ font-size: 11px;
|
|
|
+ color: #9cc9ff;
|
|
|
+ margin-top: 2px;
|
|
|
+}
|
|
|
+
|
|
|
+.alarm-badge {
|
|
|
+ display: inline-block;
|
|
|
+ width: 18px;
|
|
|
+ height: 18px;
|
|
|
+ line-height: 18px;
|
|
|
+ text-align: center;
|
|
|
+ background: #ff4757;
|
|
|
+ border-radius: 50%;
|
|
|
+ font-size: 11px;
|
|
|
+ color: #fff;
|
|
|
+ margin-right: 6px;
|
|
|
+}
|
|
|
+
|
|
|
+.alarm-scroll {
|
|
|
+ padding: 4px;
|
|
|
+ flex: 1;
|
|
|
+ overflow: hidden;
|
|
|
+ position: relative;
|
|
|
+}
|
|
|
+
|
|
|
+.alarm-item {
|
|
|
+ padding: 4px 6px;
|
|
|
+ margin-bottom: 3px;
|
|
|
+ border-radius: 4px;
|
|
|
+ font-size: 11px;
|
|
|
+ display: flex;
|
|
|
+ gap: 6px;
|
|
|
+ align-items: center;
|
|
|
+}
|
|
|
+
|
|
|
+.alarm-item.red {
|
|
|
+ background: rgba(255, 60, 60, 0.2);
|
|
|
+ color: #ff9999;
|
|
|
+ border-left: 3px solid #ff4757;
|
|
|
+}
|
|
|
+
|
|
|
+.alarm-item.orange {
|
|
|
+ background: rgba(255, 153, 0, 0.2);
|
|
|
+ color: #ffc37f;
|
|
|
+ border-left: 3px solid #ff9500;
|
|
|
+}
|
|
|
+
|
|
|
+.alarm-item.yellow {
|
|
|
+ background: rgba(255, 230, 0, 0.2);
|
|
|
+ color: #fff380;
|
|
|
+ border-left: 3px solid #ffcc32;
|
|
|
+}
|
|
|
+
|
|
|
+.btn {
|
|
|
+ padding: 5px 10px;
|
|
|
+ background: rgba(42, 91, 218, 0.3);
|
|
|
+ border: 1px solid #2a5bda;
|
|
|
+ color: #fff;
|
|
|
+ border-radius: 4px;
|
|
|
+ cursor: pointer;
|
|
|
+}
|
|
|
+
|
|
|
+.btn-primary {
|
|
|
+ background: #49bbff60;
|
|
|
+ border-color: #49bbff;
|
|
|
+}
|
|
|
+
|
|
|
+.btn-sm {
|
|
|
+ padding: 3px 6px;
|
|
|
+ font-size: 11px;
|
|
|
+}
|
|
|
+
|
|
|
+.btn-default {
|
|
|
+ background: rgba(30, 65, 140, 0.3);
|
|
|
+ border-color: #1f4499;
|
|
|
+}
|
|
|
+
|
|
|
+.middle-section {
|
|
|
+ height: 6%;
|
|
|
+ border: 1px solid #1f4499;
|
|
|
+ border-radius: 8px;
|
|
|
+ background: rgba(8, 18, 48, 0.75);
|
|
|
+ margin-bottom: 6px;
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ padding: 0 15px;
|
|
|
+ box-sizing: border-box;
|
|
|
+}
|
|
|
+
|
|
|
+.search-panel {
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ gap: 15px;
|
|
|
+ width: 100%;
|
|
|
+}
|
|
|
+
|
|
|
+.search-item {
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ gap: 6px;
|
|
|
+}
|
|
|
+
|
|
|
+.search-item label {
|
|
|
+ font-size: 13px;
|
|
|
+ color: #9cc9ff;
|
|
|
+}
|
|
|
+
|
|
|
+.search-input,
|
|
|
+.search-select {
|
|
|
+ height: 30px;
|
|
|
+ padding: 0 6px;
|
|
|
+ background: rgba(8, 18, 48, 0.8);
|
|
|
+ border: 1px solid #2a5bda;
|
|
|
+ color: #fff;
|
|
|
+ border-radius: 4px;
|
|
|
+ width: 160px;
|
|
|
+}
|
|
|
+
|
|
|
+.split {
|
|
|
+ color: #9cc9ff;
|
|
|
+ font-size: 12px;
|
|
|
+}
|
|
|
+
|
|
|
+.search-btn-group {
|
|
|
+ margin-left: auto;
|
|
|
+ display: flex;
|
|
|
+ gap: 8px;
|
|
|
+}
|
|
|
+
|
|
|
+.bottom-section {
|
|
|
+ height: 44%;
|
|
|
+ border: 1px solid #1f4499;
|
|
|
+ border-radius: 8px;
|
|
|
+ background: rgba(8, 18, 48, 0.75);
|
|
|
+ overflow: hidden;
|
|
|
+ box-sizing: border-box;
|
|
|
+}
|
|
|
+
|
|
|
+.content-wrap {
|
|
|
+ display: flex;
|
|
|
+ height: 100%;
|
|
|
+ padding: 8px;
|
|
|
+ gap: 10px;
|
|
|
+ box-sizing: border-box;
|
|
|
+}
|
|
|
+
|
|
|
+.img-list {
|
|
|
+ flex: 2;
|
|
|
+ display: flex;
|
|
|
+ flex-wrap: wrap;
|
|
|
+ gap: 8px;
|
|
|
+ overflow-y: auto;
|
|
|
+ padding: 4px;
|
|
|
+}
|
|
|
+
|
|
|
+.img-item {
|
|
|
+ width: calc(25% - 6px);
|
|
|
+ border: 1px solid #2a5bda;
|
|
|
+ border-radius: 4px;
|
|
|
+ background: #00000060;
|
|
|
+ padding: 4px;
|
|
|
+ cursor: pointer;
|
|
|
+ display: flex;
|
|
|
+ flex-direction: column;
|
|
|
+}
|
|
|
+
|
|
|
+.img-thumbnail {
|
|
|
+ width: 100%;
|
|
|
+ flex: 1;
|
|
|
+ object-fit: cover;
|
|
|
+ border-radius: 4px;
|
|
|
+}
|
|
|
+
|
|
|
+.img-desc {
|
|
|
+ height: 32px;
|
|
|
+ margin-top: 4px;
|
|
|
+ display: flex;
|
|
|
+ flex-direction: column;
|
|
|
+ justify-content: center;
|
|
|
+ gap: 2px;
|
|
|
+}
|
|
|
+
|
|
|
+.desc-line1 {
|
|
|
+ font-size: 10px;
|
|
|
+ color: #9cc9ff;
|
|
|
+ text-align: center;
|
|
|
+ margin: 0;
|
|
|
+ white-space: nowrap;
|
|
|
+ overflow: hidden;
|
|
|
+ text-overflow: ellipsis;
|
|
|
+}
|
|
|
+
|
|
|
+.desc-line2 {
|
|
|
+ font-size: 11px;
|
|
|
+ color: #e0efff;
|
|
|
+ text-align: center;
|
|
|
+ margin: 0;
|
|
|
+ white-space: nowrap;
|
|
|
+ overflow: hidden;
|
|
|
+ text-overflow: ellipsis;
|
|
|
+}
|
|
|
+
|
|
|
+.right-data-panel {
|
|
|
+ flex: 1;
|
|
|
+ display: flex;
|
|
|
+ flex-direction: column;
|
|
|
+ gap: 6px;
|
|
|
+}
|
|
|
+
|
|
|
+.tab-nav {
|
|
|
+ display: flex;
|
|
|
+ height: 32px;
|
|
|
+ background: rgba(25, 55, 128, 0.2);
|
|
|
+ border-radius: 4px;
|
|
|
+ overflow: hidden;
|
|
|
+}
|
|
|
+
|
|
|
+.tab-item {
|
|
|
+ flex: 1;
|
|
|
+ line-height: 32px;
|
|
|
+ text-align: center;
|
|
|
+ font-size: 13px;
|
|
|
+ color: #9cc9ff;
|
|
|
+ cursor: pointer;
|
|
|
+}
|
|
|
+
|
|
|
+.tab-item.active {
|
|
|
+ background: #49bbff40;
|
|
|
+ color: #49bbff;
|
|
|
+ border-bottom: 2px solid #49bbff;
|
|
|
+}
|
|
|
+
|
|
|
+.data-list {
|
|
|
+ flex: 1;
|
|
|
+ border: 1px solid #2a5bda;
|
|
|
+ border-radius: 4px;
|
|
|
+ background: #00000060;
|
|
|
+ overflow: hidden;
|
|
|
+ display: flex;
|
|
|
+ flex-direction: column;
|
|
|
+}
|
|
|
+
|
|
|
+.data-header {
|
|
|
+ display: flex;
|
|
|
+ height: 30px;
|
|
|
+ line-height: 30px;
|
|
|
+ background: rgba(25, 55, 128, 0.3);
|
|
|
+ font-size: 12px;
|
|
|
+}
|
|
|
+
|
|
|
+.data-row {
|
|
|
+ display: flex;
|
|
|
+ height: 30px;
|
|
|
+ line-height: 30px;
|
|
|
+ border-bottom: 1px dashed #1f3e8a;
|
|
|
+ font-size: 11px;
|
|
|
+ color: #a0d3ff;
|
|
|
+}
|
|
|
+
|
|
|
+.data-col {
|
|
|
+ flex: 1;
|
|
|
+ text-align: center;
|
|
|
+ overflow: hidden;
|
|
|
+ text-overflow: ellipsis;
|
|
|
+ white-space: nowrap;
|
|
|
+}
|
|
|
+
|
|
|
+.data-col:first-child {
|
|
|
+ flex: 0 1 50px;
|
|
|
+}
|
|
|
+
|
|
|
+.严重 {
|
|
|
+ color: #ff4757;
|
|
|
+ font-weight: 500;
|
|
|
+}
|
|
|
+
|
|
|
+.一般 {
|
|
|
+ color: #ff9500;
|
|
|
+ font-weight: 500;
|
|
|
+}
|
|
|
+
|
|
|
+.img-modal {
|
|
|
+ position: fixed;
|
|
|
+ top: 0;
|
|
|
+ left: 0;
|
|
|
+ width: 100vw;
|
|
|
+ height: 100vh;
|
|
|
+ background: rgba(0, 0, 0, 0.85);
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ justify-content: center;
|
|
|
+ z-index: 9999;
|
|
|
+}
|
|
|
+
|
|
|
+.modal-content {
|
|
|
+ background: #0a102b;
|
|
|
+ border: 2px solid #49bbff;
|
|
|
+ border-radius: 8px;
|
|
|
+ width: 700px;
|
|
|
+ padding: 12px;
|
|
|
+}
|
|
|
+
|
|
|
+.modal-header {
|
|
|
+ display: flex;
|
|
|
+ justify-content: space-between;
|
|
|
+ margin-bottom: 8px;
|
|
|
+ font-size: 15px;
|
|
|
+ color: #49bbff;
|
|
|
+}
|
|
|
+
|
|
|
+.close-btn {
|
|
|
+ background: none;
|
|
|
+ border: none;
|
|
|
+ color: #fff;
|
|
|
+ font-size: 20px;
|
|
|
+ cursor: pointer;
|
|
|
+}
|
|
|
+
|
|
|
+.modal-img {
|
|
|
+ width: 100%;
|
|
|
+ height: 400px;
|
|
|
+ object-fit: contain;
|
|
|
+ background: #000;
|
|
|
+ border-radius: 4px;
|
|
|
+}
|
|
|
+
|
|
|
+.modal-info {
|
|
|
+ text-align: center;
|
|
|
+ margin-top: 8px;
|
|
|
+ font-size: 13px;
|
|
|
+ color: #e0efff;
|
|
|
+}
|
|
|
+
|
|
|
+/* 统一美化滚动条(深色科技蓝风格) */
|
|
|
+::-webkit-scrollbar {
|
|
|
+ width: 6px;
|
|
|
+ height: 6px;
|
|
|
+}
|
|
|
+
|
|
|
+::-webkit-scrollbar-track {
|
|
|
+ background: rgba(25, 55, 128, 0.2);
|
|
|
+ border-radius: 3px;
|
|
|
+}
|
|
|
+
|
|
|
+::-webkit-scrollbar-thumb {
|
|
|
+ background: #49bbff;
|
|
|
+ border-radius: 3px;
|
|
|
+}
|
|
|
+
|
|
|
+::-webkit-scrollbar-thumb:hover {
|
|
|
+ background: #6cccff;
|
|
|
+}
|
|
|
+
|
|
|
+/* 图片列表区域专属滚动条 */
|
|
|
+.img-list::-webkit-scrollbar {
|
|
|
+ width: 6px;
|
|
|
+}
|
|
|
+
|
|
|
+.img-list::-webkit-scrollbar-track {
|
|
|
+ background: rgba(8, 18, 48, 0.5);
|
|
|
+}
|
|
|
+
|
|
|
+.img-list::-webkit-scrollbar-thumb {
|
|
|
+ background: #49bbff;
|
|
|
+}
|
|
|
+
|
|
|
+/* 新告警插入高亮动画(醒目版) */
|
|
|
+@keyframes alarmFlash {
|
|
|
+ 0% {
|
|
|
+ background: rgba(73, 187, 255, 0.4) !important;
|
|
|
+ transform: scale(1.02);
|
|
|
+ }
|
|
|
+
|
|
|
+ 30% {
|
|
|
+ background: rgba(255, 70, 70, 0.5) !important;
|
|
|
+ }
|
|
|
+
|
|
|
+ 60% {
|
|
|
+ background: rgba(73, 187, 255, 0.4) !important;
|
|
|
+ }
|
|
|
+
|
|
|
+ 100% {
|
|
|
+ background: initial !important;
|
|
|
+ transform: scale(1);
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+/* 新告警动画类名 */
|
|
|
+.alarm-item.new {
|
|
|
+ animation: alarmFlash 1.8s ease-out forwards;
|
|
|
+ position: relative;
|
|
|
+ z-index: 10;
|
|
|
+}
|
|
|
+
|
|
|
+/* 1. 红色高等级告警 持续闪烁(监控级重点提醒) */
|
|
|
+@keyframes redAlarmBlink {
|
|
|
+ 0% {
|
|
|
+ background: rgba(255, 60, 60, 0.2);
|
|
|
+ }
|
|
|
+
|
|
|
+ 50% {
|
|
|
+ background: rgba(255, 60, 60, 0.5);
|
|
|
+ }
|
|
|
+
|
|
|
+ 100% {
|
|
|
+ background: rgba(255, 60, 60, 0.2);
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+/* 2. 新告警 高亮缩放闪烁(插入时醒目提醒) */
|
|
|
+@keyframes newAlarmFlash {
|
|
|
+ 0% {
|
|
|
+ background: rgba(73, 187, 255, 0.4);
|
|
|
+ transform: scale(1.02);
|
|
|
+ box-shadow: 0 0 8px rgba(73, 187, 255, 0.8);
|
|
|
+ }
|
|
|
+
|
|
|
+ 30% {
|
|
|
+ background: rgba(255, 140, 40, 0.5);
|
|
|
+ box-shadow: 0 0 12px rgba(255, 140, 40, 0.8);
|
|
|
+ }
|
|
|
+
|
|
|
+ 60% {
|
|
|
+ background: rgba(73, 187, 255, 0.4);
|
|
|
+ box-shadow: 0 0 8px rgba(73, 187, 255, 0.8);
|
|
|
+ }
|
|
|
+
|
|
|
+ 100% {
|
|
|
+ background: initial;
|
|
|
+ transform: scale(1);
|
|
|
+ box-shadow: none;
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+/* 绑定动画类名 */
|
|
|
+.alarm-item.red {
|
|
|
+ animation: redAlarmBlink 1.2s infinite ease-in-out;
|
|
|
+ /* 红色告警持续闪 */
|
|
|
+}
|
|
|
+
|
|
|
+.alarm-item.new {
|
|
|
+ animation: newAlarmFlash 1.8s ease-out forwards;
|
|
|
+ /* 新告警单次高亮闪 */
|
|
|
+ position: relative;
|
|
|
+ z-index: 10;
|
|
|
+ /* 新告警浮在最上层 */
|
|
|
+}
|
|
|
+</style>
|