index.vue 25 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000
  1. <!-- <template>
  2. <div class="screen-container">
  3. <div class="top-section">
  4. <div class="system-title">局部风机无人值守演示系统</div>
  5. <div class="top-inner">
  6. <div class="top-left">
  7. <div class="fan-switch">
  8. <label></label>
  9. <select v-model="selectFan" @change="switchFan">
  10. <option value="all">全部风机</option>
  11. <option v-for="fan in fanList" :key="fan.id" :value="fan.id">
  12. {{ fan.name }}
  13. </option>
  14. </select>
  15. <button class="btn btn-primary" @click="refreshVideo">刷新视频</button>
  16. <div class="time-box">当前时间:{{ currentTime }}</div>
  17. </div>
  18. <div class="video-single">
  19. <div class="video-item">
  20. <div class="video-title">风机实时监控 [{{ currentFanName }}]</div>
  21. <div class="video-box">
  22. <video class="video-bg" autoplay loop muted playsinline webkit-playsinline controls>
  23. <source src="@/assets/images/wind-video/video/fan-main.mp4" type="video/mp4" />
  24. </video>
  25. </div>
  26. </div>
  27. <div class="video-item alarm-video-replace">
  28. <div class="alarm-title">
  29. <span>井下设备操作日志</span>
  30. <div>
  31. <span class="alarm-badge">{{ operationLog.length }}</span>
  32. <button class="btn btn-sm" @click="clearLog">清空</button>
  33. </div>
  34. </div>
  35. <div class="alarm-scroll" ref="logScroll">
  36. <div class="alarm-item" v-for="(item, idx) in operationLog" :key="idx" :class="[item.status, item.new ? 'new' : '']">
  37. <div class="alarm-full-line">
  38. <span class="alarm-time">{{ item.time }}</span>
  39. <span class="alarm-fan">{{ item.fanName }}</span>
  40. <span class="alarm-type">{{ item.operateType }}</span>
  41. <span class="alarm-desc">{{ item.operator }} | {{ item.compareResult }}</span>
  42. </div>
  43. </div>
  44. </div>
  45. </div>
  46. </div>
  47. </div>
  48. <div class="top-right">
  49. <div class="status-panel">
  50. <div class="status-title">风机状态总览</div>
  51. <div class="status-grid">
  52. <div class="status-item normal">
  53. <div class="status-num">{{ statusData.normalFan }}</div>
  54. <div class="status-name">风机正常</div>
  55. </div>
  56. <div class="status-item offline">
  57. <div class="status-num">{{ statusData.offlineFan }}</div>
  58. <div class="status-name">风机离线</div>
  59. </div>
  60. <div class="status-item fault">
  61. <div class="status-num">{{ statusData.faultFan }}</div>
  62. <div class="status-name">风机故障</div>
  63. </div>
  64. <div class="status-item warn">
  65. <div class="status-num">{{ statusData.warn }}</div>
  66. <div class="status-name">实时预警</div>
  67. </div>
  68. <div class="status-item person-online">
  69. <div class="status-num">{{ statusData.realTimePerson }}</div>
  70. <div class="status-name">实时人数</div>
  71. </div>
  72. <div class="status-item person-total">
  73. <div class="status-num">{{ statusData.totalPerson }}</div>
  74. <div class="status-name">今日总进入</div>
  75. </div>
  76. </div>
  77. </div>
  78. <div class="area-stats-panel">
  79. <div class="status-title">区域停留统计</div>
  80. <div class="area-stats-list">
  81. <div class="area-stats-item" v-for="(item, idx) in areaStatsList" :key="idx">
  82. <div class="area-name">{{ item.areaName }}</div>
  83. <div class="area-data">
  84. <span class="person-count">{{ item.personCount }}人</span>
  85. <span class="stay-time">平均{{ item.avgStayTime }}</span>
  86. </div>
  87. </div>
  88. </div>
  89. </div>
  90. </div>
  91. </div>
  92. </div>
  93. <div class="middle-section">
  94. <div class="search-panel">
  95. <div class="search-item">
  96. <label>时间范围:</label>
  97. <input type="datetime-local" v-model="searchForm.startTime" class="search-input" />
  98. <span class="split">-</span>
  99. <input type="datetime-local" v-model="searchForm.endTime" class="search-input" />
  100. <button class="btn btn-sm btn-default" @click="selectToday">今日</button>
  101. <button class="btn btn-sm btn-default" @click="select7d">近7天</button>
  102. </div>
  103. <div class="search-item">
  104. <label>操作类型:</label>
  105. <select v-model="searchForm.operateType" class="search-select">
  106. <option value="all">全部操作</option>
  107. <option value="风机调频">风机调频</option>
  108. <option value="风机切换">风机切换</option>
  109. <option value="参数调整">参数调整</option>
  110. <option value="复位操作">复位操作</option>
  111. </select>
  112. </div>
  113. <div class="search-item">
  114. <label>风机选择:</label>
  115. <select v-model="searchForm.fanId" class="search-select">
  116. <option value="all">全部风机</option>
  117. <option v-for="fan in fanList" :key="fan.id" :value="fan.id">{{ fan.name }}</option>
  118. </select>
  119. </div>
  120. <div class="search-btn-group">
  121. <button class="btn btn-primary" @click="handleSearch">一键检索</button>
  122. <button class="btn btn-default" @click="resetSearch">重置</button>
  123. </div>
  124. </div>
  125. </div>
  126. <div class="bottom-section">
  127. <div class="content-wrap">
  128. <div class="img-list">
  129. <div class="img-item" v-for="(item, idx) in currentImgList" :key="idx" @click="openImgModal(item)">
  130. <img :src="item.imgUrl" class="img-thumbnail" />
  131. <div class="img-desc">
  132. <p class="desc-line1">{{ getFanName(item.fanId) }}-{{ getAreaName(item.areaId) }}</p>
  133. <p class="desc-line2">{{ item.personName }} 停留{{ item.stayTime }}</p>
  134. </div>
  135. </div>
  136. </div>
  137. <div class="right-data-panel">
  138. <div class="tab-nav">
  139. <div class="tab-item" :class="{ active: activeTab === 'personTrack' }" @click="activeTab = 'personTrack'"> 人员轨迹</div>
  140. <div class="tab-item" :class="{ active: activeTab === 'areaStay' }" @click="activeTab = 'areaStay'">区域统计 </div>
  141. <div class="tab-item" :class="{ active: activeTab === 'warnRecord' }" @click="activeTab = 'warnRecord'">预警记录 </div>
  142. </div>
  143. <div class="data-list">
  144. <div class="data-header" v-if="activeTab === 'personTrack'">
  145. <div class="data-col">序号</div>
  146. <div class="data-col">风机</div>
  147. <div class="data-col">区域</div>
  148. <div class="data-col">姓名</div>
  149. <div class="data-col">区队</div>
  150. <div class="data-col">进入</div>
  151. <div class="data-col">离开</div>
  152. <div class="data-col">时长</div>
  153. </div>
  154. <div class="data-header" v-else-if="activeTab === 'areaStay'">
  155. <div class="data-col">序号</div>
  156. <div class="data-col">区域</div>
  157. <div class="data-col">人数</div>
  158. <div class="data-col">最长</div>
  159. <div class="data-col">最短</div>
  160. <div class="data-col">平均</div>
  161. <div class="data-col">总计</div>
  162. </div>
  163. <div class="data-header" v-else>
  164. <div class="data-col">序号</div>
  165. <div class="data-col">风机</div>
  166. <div class="data-col">区域</div>
  167. <div class="data-col">姓名</div>
  168. <div class="data-col">类型</div>
  169. <div class="data-col">时间</div>
  170. <div class="data-col">状态</div>
  171. </div>
  172. <div class="data-row" v-for="(item, idx) in personTrackList" :key="idx" v-if="activeTab === 'personTrack'">
  173. <div class="data-col">{{ idx + 1 }}</div>
  174. <div class="data-col">{{ getFanName(item.fanId) }}</div>
  175. <div class="data-col">{{ getAreaName(item.areaId) }}</div>
  176. <div class="data-col">{{ item.personName }}</div>
  177. <div class="data-col">{{ item.teamName }}</div>
  178. <div class="data-col">{{ item.enterTime }}</div>
  179. <div class="data-col">{{ item.leaveTime }}</div>
  180. <div class="data-col">{{ item.stayTime }}</div>
  181. </div>
  182. <div class="data-row" v-for="(item, idx) in areaStayList" :key="idx" v-if="activeTab === 'areaStay'">
  183. <div class="data-col">{{ idx + 1 }}</div>
  184. <div class="data-col">{{ item.areaName }}</div>
  185. <div class="data-col">{{ item.personCount }}</div>
  186. <div class="data-col">{{ item.maxStay }}</div>
  187. <div class="data-col">{{ item.minStay }}</div>
  188. <div class="data-col">{{ item.avgStay }}</div>
  189. <div class="data-col">{{ item.totalStay }}</div>
  190. </div>
  191. <div class="data-row" v-for="(item, idx) in warnRecordList" :key="idx" v-if="activeTab === 'warnRecord'">
  192. <div class="data-col">{{ idx + 1 }}</div>
  193. <div class="data-col">{{ getFanName(item.fanId) }}</div>
  194. <div class="data-col">{{ getAreaName(item.areaId) }}</div>
  195. <div class="data-col">{{ item.personName }}</div>
  196. <div class="data-col">{{ item.warnType }}</div>
  197. <div class="data-col">{{ item.warnTime }}</div>
  198. <div class="data-col" :class="item.handleStatus === '已处理' ? 'handled' : 'unhandled'">{{ item.handleStatus }}</div>
  199. </div>
  200. </div>
  201. </div>
  202. </div>
  203. </div>
  204. <div class="img-modal" v-show="showImgModal" @click="closeImgModal">
  205. <div class="modal-content" @click.stop>
  206. <div class="modal-header">
  207. <span>现场抓拍详情</span>
  208. <button class="close-btn" @click="closeImgModal">×</button>
  209. </div>
  210. <img :src="currentModalImg" class="modal-img" />
  211. <div class="modal-info">
  212. <p>{{ modalInfo.fanName }} - {{ modalInfo.areaName }}</p>
  213. <p>人员:{{ modalInfo.personName }} | 所属区队:{{ modalInfo.teamName }}</p>
  214. <p>进入时间:{{ modalInfo.enterTime }} | 离开时间:{{ modalInfo.leaveTime }} | 停留时长:{{ modalInfo.stayTime }}</p>
  215. </div>
  216. </div>
  217. </div>
  218. </div>
  219. </template>
  220. <script setup lang="ts">
  221. import { ref, onMounted, onUnmounted, nextTick } from 'vue';
  222. import {
  223. fanList,
  224. areaList,
  225. authPersons,
  226. statusData,
  227. areaStatsList,
  228. currentImgList,
  229. personTrackList,
  230. areaStayList,
  231. warnRecordList,
  232. } from './fanLocalVideo.data';
  233. // 工具方法
  234. const getFanName = (id) => fanList.find((i) => i.id === id)?.name || '未知风机';
  235. const getAreaName = (id) => areaList.find((i) => i.id === id)?.name || '未知区域';
  236. const selectFan = ref('all');
  237. const currentFanName = ref('1号掘进工作面局部风机');
  238. const currentTime = ref('');
  239. let timeTimer: null | NodeJS.Timeout = null;
  240. // ==============================================
  241. // 核心:井下设备操作日志(PLC + 配电柜 + 人员比对)
  242. // ==============================================
  243. const logScroll = ref<any>(null);
  244. let logTimer: null | NodeJS.Timeout = null;
  245. let addLogTimer: null | NodeJS.Timeout = null;
  246. // 操作日志列表(最多7条)
  247. const operationLog = ref([
  248. { time: '10:12:20', fanName: '1号风机', operateType: '风机调频', operator: '张三', compareResult: '人员准入匹配', status: 'green' },
  249. { time: '11:05:10', fanName: '2号风机', operateType: '参数调整', operator: '李四', compareResult: '人员准入匹配', status: 'green' },
  250. { time: '14:30:00', fanName: '3号风机', operateType: '切换主备', operator: '未知人员', compareResult: '未检测到人员', status: 'red' },
  251. ]);
  252. // 搜索
  253. const searchForm = ref({ startTime: '', endTime: '', operateType: 'all', fanId: 'all' });
  254. const activeTab = ref('personTrack');
  255. const showImgModal = ref(false);
  256. const currentModalImg = ref('');
  257. const modalInfo = ref({}) as any;
  258. // 生成PLC/配电柜操作记录 + 人员比对
  259. const createOperationLog = () => {
  260. const now = new Date();
  261. const time = `${String(now.getHours()).padStart(2, 0)}:${String(now.getMinutes()).padStart(2, 0)}:${String(now.getSeconds()).padStart(2, 0)}`;
  262. const fan = fanList[Math.floor(Math.random() * fanList.length)];
  263. const operateTypes = ['风机调频', '切换主备', '参数调整', '复位操作'];
  264. const operateType = operateTypes[Math.floor(Math.random() * operateTypes.length)];
  265. // 随机操作人员
  266. const allOps = [...authPersons, '未知人员', '外来人员'];
  267. const operator = allOps[Math.floor(Math.random() * allOps.length)];
  268. // 人员准入比对逻辑
  269. const compareResult = authPersons.includes(operator) ? '人员准入匹配' : '未检测到人员';
  270. const status = compareResult === '人员准入匹配' ? 'green' : 'red';
  271. return {
  272. time,
  273. fanName: fan.name,
  274. operateType,
  275. operator,
  276. compareResult,
  277. status,
  278. };
  279. };
  280. // 自动生成日志,最多保留7条
  281. const startAddOperationLog = () => {
  282. addLogTimer = setInterval(() => {
  283. const log = createOperationLog();
  284. operationLog.value.unshift(log);
  285. log.new = true;
  286. setTimeout(() => (log.new = false), 1800);
  287. // 最多7条
  288. if (operationLog.value.length > 7) {
  289. operationLog.value.pop();
  290. }
  291. }, 6000);
  292. };
  293. // 日志滚动
  294. const startLogScroll = () => {
  295. nextTick(() => {
  296. if (!logScroll.value) return;
  297. clearInterval(logTimer);
  298. logTimer = setInterval(() => {
  299. logScroll.value.scrollTop += 32;
  300. if (logScroll.value.scrollTop >= logScroll.value.scrollHeight - logScroll.value.clientHeight) {
  301. logScroll.value.scrollTop = 0;
  302. }
  303. }, 1500);
  304. });
  305. };
  306. // 清空日志
  307. const clearLog = () => {
  308. operationLog.value = [];
  309. };
  310. const switchFan = () => {
  311. if (selectFan.value !== 'all') currentFanName.value = getFanName(+selectFan.value);
  312. };
  313. const refreshVideo = () => {
  314. const v = document.querySelector('.video-bg');
  315. if (v) v.src = `/videos/fan-main.mp4?${Math.random()}`;
  316. };
  317. const formatDate = (d) => {
  318. const y = d.getFullYear();
  319. const m = String(d.getMonth() + 1).padStart(2, 0);
  320. const dt = String(d.getDate()).padStart(2, 0);
  321. const h = String(d.getHours()).padStart(2, 0);
  322. const mi = String(d.getMinutes()).padStart(2, 0);
  323. return `${y}-${m}-${dt}T${h}:${mi}`;
  324. };
  325. const selectToday = () => {
  326. const now = new Date();
  327. searchForm.value.startTime = formatDate(new Date(now.getFullYear(), now.getMonth(), now.getDate()));
  328. searchForm.value.endTime = formatDate(now);
  329. };
  330. const select7d = () => {
  331. const now = new Date();
  332. searchForm.value.startTime = formatDate(new Date(now - 7 * 86400000));
  333. searchForm.value.endTime = formatDate(now);
  334. };
  335. const handleSearch = () => alert('检索成功');
  336. const resetSearch = () => {
  337. searchForm.value = { startTime: '', endTime: '', operateType: 'all', fanId: 'all' };
  338. selectToday();
  339. };
  340. const openImgModal = (item) => {
  341. showImgModal.value = true;
  342. currentModalImg.value = item.imgUrl;
  343. modalInfo.value = {
  344. fanName: getFanName(item.fanId),
  345. areaName: getAreaName(item.areaId),
  346. personName: item.personName,
  347. teamName: item.teamName,
  348. enterTime: item.enterTime,
  349. leaveTime: item.leaveTime,
  350. stayTime: item.stayTime,
  351. };
  352. };
  353. const closeImgModal = () => (showImgModal.value = false);
  354. const updateTime = () => {
  355. const d = new Date();
  356. currentTime.value = `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, 0)}-${String(d.getDate()).padStart(2, 0)} ${String(
  357. d.getHours()
  358. ).padStart(2, 0)}:${String(d.getMinutes()).padStart(2, 0)}:${String(d.getSeconds()).padStart(2, 0)}`;
  359. };
  360. onMounted(() => {
  361. selectToday();
  362. updateTime();
  363. timeTimer = setInterval(updateTime, 1000);
  364. startLogScroll();
  365. startAddOperationLog();
  366. });
  367. onUnmounted(() => {
  368. clearInterval(timeTimer);
  369. clearInterval(logTimer);
  370. clearInterval(addLogTimer);
  371. });
  372. </script>
  373. <style lang="less" scoped>
  374. .screen-container {
  375. width: 100%;
  376. height: 100%;
  377. background: #0a102b;
  378. color: #e0efff;
  379. font-family: 'Microsoft YaHei', sans-serif;
  380. overflow: hidden !important;
  381. padding: 8px;
  382. box-sizing: border-box;
  383. }
  384. .system-title {
  385. text-align: center;
  386. font-size: 28px;
  387. font-weight: bold;
  388. color: #49bbff;
  389. text-shadow: 0 0 15px #2a82ff;
  390. padding: 6px 0;
  391. letter-spacing: 2px;
  392. margin: 0 0 4px 0;
  393. }
  394. .top-section {
  395. height: 48%;
  396. border: 1px solid #1f4499;
  397. border-radius: 8px;
  398. background: rgba(8, 18, 48, 0.75);
  399. margin-bottom: 6px;
  400. overflow: hidden;
  401. box-sizing: border-box;
  402. }
  403. .top-inner {
  404. display: flex;
  405. height: calc(100% - 40px);
  406. padding: 6px 8px;
  407. gap: 10px;
  408. box-sizing: border-box;
  409. }
  410. .top-left {
  411. flex: 7;
  412. display: flex;
  413. flex-direction: column;
  414. gap: 8px;
  415. }
  416. .top-right {
  417. flex: 3;
  418. display: flex;
  419. flex-direction: column;
  420. gap: 8px;
  421. }
  422. .fan-switch {
  423. display: flex;
  424. align-items: center;
  425. height: 36px;
  426. gap: 10px;
  427. }
  428. .fan-switch select {
  429. height: 32px;
  430. padding: 0 8px;
  431. background: rgba(8, 18, 48, 0.8);
  432. border: 1px solid #2a5bda;
  433. color: #fff;
  434. border-radius: 4px;
  435. width: 280px;
  436. }
  437. .time-box {
  438. font-size: 15px;
  439. color: #49bbff;
  440. font-weight: 500;
  441. margin: auto;
  442. letter-spacing: 1px;
  443. }
  444. .video-single {
  445. display: flex;
  446. flex: 1;
  447. gap: 8px;
  448. }
  449. .video-item {
  450. flex: 1;
  451. border: 1px solid #2255cc;
  452. border-radius: 4px;
  453. background: #00000080;
  454. display: flex;
  455. flex-direction: column;
  456. height: 100%;
  457. }
  458. .video-title {
  459. height: 30px;
  460. line-height: 30px;
  461. padding-left: 8px;
  462. font-size: 13px;
  463. color: #9cc9ff;
  464. background: rgba(25, 55, 128, 0.3);
  465. }
  466. .video-box {
  467. position: relative;
  468. width: 100%;
  469. flex: 1;
  470. overflow: hidden;
  471. }
  472. .video-bg {
  473. width: 100%;
  474. height: 100%;
  475. object-fit: cover;
  476. opacity: 0.9;
  477. }
  478. .alarm-video-replace {
  479. display: flex;
  480. flex-direction: column;
  481. height: 100%;
  482. }
  483. .alarm-title {
  484. height: 30px;
  485. line-height: 30px;
  486. padding: 0 8px;
  487. background: rgba(25, 55, 128, 0.3);
  488. color: #9cc9ff;
  489. font-size: 13px;
  490. display: flex;
  491. justify-content: space-between;
  492. align-items: center;
  493. }
  494. .alarm-scroll {
  495. flex: 1;
  496. padding: 4px;
  497. overflow: hidden;
  498. height: 100%;
  499. max-height: 100%;
  500. }
  501. .alarm-item {
  502. padding: 6px 8px;
  503. margin-bottom: 4px;
  504. border-radius: 4px;
  505. font-size: 12px;
  506. border-left: 3px solid transparent;
  507. display: block;
  508. }
  509. .alarm-full-line {
  510. width: 100%;
  511. display: block;
  512. white-space: nowrap;
  513. overflow: hidden;
  514. text-overflow: ellipsis;
  515. }
  516. .alarm-time,
  517. .alarm-fan,
  518. .alarm-type,
  519. .alarm-desc {
  520. margin-right: 12px;
  521. line-height: 24px;
  522. }
  523. .alarm-desc {
  524. margin-right: 0;
  525. }
  526. /* 操作日志样式:匹配=绿色,不匹配=红色 */
  527. .alarm-item.green {
  528. background: rgba(0, 255, 100, 0.15);
  529. color: #9cffcc;
  530. border-left-color: #00ff66;
  531. }
  532. .alarm-item.red {
  533. background: rgba(255, 60, 60, 0.2);
  534. color: #ff9999;
  535. border-left-color: #ff4757;
  536. }
  537. .status-panel,
  538. .area-stats-panel {
  539. flex: 1;
  540. border: 1px solid #2255cc;
  541. border-radius: 4px;
  542. background: #00000060;
  543. display: flex;
  544. flex-direction: column;
  545. }
  546. .status-title {
  547. height: 30px;
  548. line-height: 30px;
  549. padding: 0 8px;
  550. background: rgba(25, 55, 128, 0.3);
  551. color: #9cc9ff;
  552. font-size: 13px;
  553. }
  554. .status-grid {
  555. display: grid;
  556. grid-template-columns: 1fr 1fr;
  557. grid-template-rows: 1fr 1fr 1fr;
  558. gap: 4px;
  559. padding: 4px;
  560. flex: 1;
  561. }
  562. .status-item {
  563. display: flex;
  564. flex-direction: column;
  565. align-items: center;
  566. justify-content: center;
  567. background: rgba(30, 65, 140, 0.2);
  568. border-radius: 4px;
  569. }
  570. .status-num {
  571. font-size: 20px;
  572. font-weight: bold;
  573. color: #fff;
  574. }
  575. .status-name {
  576. font-size: 11px;
  577. color: #9cc9ff;
  578. margin-top: 2px;
  579. }
  580. .area-stats-list {
  581. flex: 1;
  582. padding: 8px;
  583. display: flex;
  584. flex-direction: column;
  585. gap: 5px;
  586. overflow-y: auto;
  587. }
  588. .area-stats-item {
  589. display: flex;
  590. justify-content: space-between;
  591. align-items: center;
  592. padding: 6px 10px;
  593. background: rgba(30, 65, 140, 0.25);
  594. border-radius: 6px;
  595. font-size: 13px;
  596. }
  597. .area-name {
  598. color: #9cc9ff;
  599. width: 70px;
  600. }
  601. .area-data {
  602. display: flex;
  603. gap: 15px;
  604. }
  605. .person-count {
  606. color: #32ff80;
  607. font-weight: bold;
  608. }
  609. .stay-time {
  610. color: #ffa502;
  611. }
  612. .alarm-badge {
  613. display: inline-block;
  614. width: 18px;
  615. height: 18px;
  616. line-height: 18px;
  617. text-align: center;
  618. background: #49bbff;
  619. border-radius: 50%;
  620. font-size: 11px;
  621. color: #fff;
  622. margin-right: 6px;
  623. }
  624. .btn {
  625. padding: 5px 10px;
  626. background: rgba(42, 91, 218, 0.3);
  627. border: 1px solid #2a5bda;
  628. color: #fff;
  629. border-radius: 4px;
  630. cursor: pointer;
  631. }
  632. .btn-primary {
  633. background: #49bbff60;
  634. border-color: #49bbff;
  635. }
  636. .btn-sm {
  637. padding: 3px 6px;
  638. font-size: 11px;
  639. }
  640. .btn-default {
  641. background: rgba(30, 65, 140, 0.3);
  642. border-color: #1f4499;
  643. }
  644. .middle-section {
  645. height: 6%;
  646. border: 1px solid #1f4499;
  647. border-radius: 8px;
  648. background: rgba(8, 18, 48, 0.75);
  649. margin-bottom: 6px;
  650. display: flex;
  651. align-items: center;
  652. padding: 0 15px;
  653. box-sizing: border-box;
  654. }
  655. .search-panel {
  656. display: flex;
  657. align-items: center;
  658. gap: 15px;
  659. width: 100%;
  660. }
  661. .search-item {
  662. display: flex;
  663. align-items: center;
  664. gap: 6px;
  665. }
  666. .search-item label {
  667. font-size: 13px;
  668. color: #9cc9ff;
  669. }
  670. .search-input,
  671. .search-select {
  672. height: 30px;
  673. padding: 0 6px;
  674. background: rgba(8, 18, 48, 0.8);
  675. border: 1px solid #2a5bda;
  676. color: #fff;
  677. border-radius: 4px;
  678. width: 160px;
  679. }
  680. .split {
  681. color: #9cc9ff;
  682. font-size: 12px;
  683. }
  684. .search-btn-group {
  685. margin-left: auto;
  686. display: flex;
  687. gap: 8px;
  688. }
  689. .bottom-section {
  690. height: 44%;
  691. border: 1px solid #1f4499;
  692. border-radius: 8px;
  693. background: rgba(8, 18, 48, 0.75);
  694. overflow: hidden;
  695. box-sizing: border-box;
  696. }
  697. .content-wrap {
  698. display: flex;
  699. height: 100%;
  700. padding: 8px;
  701. gap: 10px;
  702. box-sizing: border-box;
  703. }
  704. .img-list {
  705. flex: 2;
  706. display: flex;
  707. flex-wrap: wrap;
  708. gap: 8px;
  709. overflow-y: auto;
  710. padding: 4px;
  711. align-content: flex-start;
  712. }
  713. .right-data-panel {
  714. flex: 1;
  715. display: flex;
  716. flex-direction: column;
  717. gap: 6px;
  718. }
  719. .img-item {
  720. width: calc(25% - 6px);
  721. border: 1px solid #2a5bda;
  722. border-radius: 4px;
  723. background: #00000060;
  724. padding: 4px;
  725. cursor: pointer;
  726. display: flex;
  727. flex-direction: column;
  728. height: 160px;
  729. }
  730. .img-thumbnail {
  731. width: 100%;
  732. height: 120px;
  733. object-fit: contain;
  734. border-radius: 4px;
  735. background: #000;
  736. }
  737. .img-desc {
  738. height: 32px;
  739. margin-top: 4px;
  740. display: flex;
  741. flex-direction: column;
  742. justify-content: center;
  743. gap: 2px;
  744. }
  745. .desc-line1 {
  746. font-size: 10px;
  747. color: #9cc9ff;
  748. text-align: center;
  749. margin: 0;
  750. white-space: nowrap;
  751. overflow: hidden;
  752. text-overflow: ellipsis;
  753. }
  754. .desc-line2 {
  755. font-size: 11px;
  756. color: #e0efff;
  757. text-align: center;
  758. margin: 0;
  759. white-space: nowrap;
  760. overflow: hidden;
  761. text-overflow: ellipsis;
  762. }
  763. .tab-nav {
  764. display: flex;
  765. height: 32px;
  766. background: rgba(25, 55, 128, 0.2);
  767. border-radius: 4px;
  768. overflow: hidden;
  769. }
  770. .tab-item {
  771. flex: 1;
  772. line-height: 32px;
  773. text-align: center;
  774. font-size: 13px;
  775. color: #9cc9ff;
  776. cursor: pointer;
  777. }
  778. .tab-item.active {
  779. background: #49bbff40;
  780. color: #49bbff;
  781. border-bottom: 2px solid #49bbff;
  782. }
  783. .data-list {
  784. flex: 1;
  785. border: 1px solid #2a5bda;
  786. border-radius: 4px;
  787. background: #00000060;
  788. overflow: hidden;
  789. display: flex;
  790. flex-direction: column;
  791. }
  792. .data-header {
  793. display: flex;
  794. height: 30px;
  795. line-height: 30px;
  796. background: rgba(25, 55, 128, 0.3);
  797. font-size: 12px;
  798. }
  799. .data-row {
  800. display: flex;
  801. height: 30px;
  802. line-height: 30px;
  803. border-bottom: 1px dashed #1f3e8a;
  804. font-size: 11px;
  805. color: #a0d3ff;
  806. }
  807. .data-col {
  808. flex: 1;
  809. text-align: center;
  810. overflow: hidden;
  811. text-overflow: ellipsis;
  812. white-space: nowrap;
  813. }
  814. .data-col:first-child {
  815. flex: 0 1 50px;
  816. }
  817. .handled {
  818. color: #32ff80;
  819. font-weight: bold;
  820. }
  821. .unhandled {
  822. color: #ff4757;
  823. font-weight: bold;
  824. }
  825. .img-modal {
  826. position: fixed;
  827. top: 0;
  828. left: 0;
  829. width: 100vw;
  830. height: 100vh;
  831. background: rgba(0, 0, 0, 0.85);
  832. display: flex;
  833. align-items: center;
  834. justify-content: center;
  835. z-index: 9999;
  836. }
  837. .modal-content {
  838. background: #0a102b;
  839. border: 2px solid #49bbff;
  840. border-radius: 8px;
  841. width: 700px;
  842. padding: 12px;
  843. }
  844. .modal-header {
  845. display: flex;
  846. justify-content: space-between;
  847. margin-bottom: 8px;
  848. font-size: 15px;
  849. color: #49bbff;
  850. }
  851. .close-btn {
  852. background: none;
  853. border: none;
  854. color: #fff;
  855. font-size: 20px;
  856. cursor: pointer;
  857. }
  858. .modal-img {
  859. width: 100%;
  860. height: 400px;
  861. object-fit: contain;
  862. background: #000;
  863. border-radius: 4px;
  864. }
  865. .modal-info {
  866. text-align: center;
  867. margin-top: 8px;
  868. font-size: 13px;
  869. color: #e0efff;
  870. }
  871. ::-webkit-scrollbar {
  872. width: 6px;
  873. height: 6px;
  874. }
  875. ::-webkit-scrollbar-track {
  876. background: rgba(25, 55, 128, 0.2);
  877. border-radius: 3px;
  878. }
  879. ::-webkit-scrollbar-thumb {
  880. background: #49bbff;
  881. border-radius: 3px;
  882. }
  883. @keyframes newAlarmFlash {
  884. 0% {
  885. background: rgba(73, 187, 255, 0.4);
  886. transform: scale(1.02);
  887. }
  888. 100% {
  889. background: initial;
  890. transform: scale(1);
  891. }
  892. }
  893. .alarm-item.new {
  894. animation: newAlarmFlash 1.8s forwards;
  895. }
  896. </style> -->