SecurityMonitor.vue 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594
  1. <template>
  2. <div class="security-monitor panel">
  3. <!-- Monitor Header -->
  4. <div class="monitor-header">
  5. <span class="monitor-title-cn">&#x1F4F9; &#x5B9E;&#x65F6;&#x76D1;&#x63A7;</span>
  6. <span class="monitor-title-en">CCTV Live Feed</span>
  7. <div class="monitor-status">
  8. <div class="status-dot green"></div>
  9. <span class="monitor-status-text">&#x4FE1;&#x53F7;&#x6B63;&#x5E38;</span>
  10. </div>
  11. </div>
  12. <!-- Monitor Inner: 2-column grid -->
  13. <div class="monitor-inner">
  14. <!-- Left Column: Search + Filter + Tree -->
  15. <div class="monitor-left">
  16. <div class="search-box">
  17. <span class="search-icon">&#x1F50D;</span>
  18. <input
  19. v-model="searchText"
  20. type="text"
  21. placeholder="&#x641C;&#x7D22;&#x697C;&#x680B; / &#x697C;&#x5C42; / &#x623F;&#x95F4;&#x2026;"
  22. />
  23. </div>
  24. <select v-model="selectedUnit" class="filter-select">
  25. <option value="">&#x5168;&#x90E8;&#x4E8C;&#x7EA7;&#x5355;&#x4F4D;</option>
  26. <option>&#x5316;&#x5B66;&#x7814;&#x7A76;&#x6240;</option>
  27. <option>&#x7269;&#x7406;&#x7814;&#x7A76;&#x6240;</option>
  28. <option>&#x751F;&#x7269;&#x7814;&#x7A76;&#x6240;</option>
  29. <option>&#x6750;&#x6599;&#x7814;&#x7A76;&#x6240;</option>
  30. <option>&#x5DE5;&#x7A0B;&#x7814;&#x7A76;&#x6240;</option>
  31. </select>
  32. <div class="tree-wrap">
  33. <tree-node
  34. v-if="treeData"
  35. :node="treeData"
  36. :depth="0"
  37. :selected-label="selectedTreeLabel"
  38. @select="onTreeNodeSelect"
  39. />
  40. </div>
  41. </div>
  42. <!-- Right Column: Breadcrumb + Pager + Camera Grid -->
  43. <div class="monitor-right">
  44. <div class="camera-grid-header">
  45. <div class="camera-breadcrumb">
  46. <span>&#x5B89;&#x79D1;&#x9662;&#x4E3B;&#x56ED;&#x533A;</span> &#x203A;
  47. <span>&#x7EFC;&#x5408;&#x5B9E;&#x9A8C;&#x697C;A</span> &#x203A;
  48. <span>3&#x5C42;</span>
  49. </div>
  50. <div class="camera-pager">
  51. <button class="pager-btn" @click="prevPage">&lsaquo;</button>
  52. <span class="pager-info">{{ currentPage }} / {{ totalPages }} &#x9875;</span>
  53. <button class="pager-btn" @click="nextPage">&rsaquo;</button>
  54. </div>
  55. </div>
  56. <div class="camera-grid">
  57. <div
  58. v-for="(cam, idx) in cameras"
  59. :key="'cam-' + idx"
  60. class="camera-cell"
  61. :class="{ 'ai-cam': idx === 0 }"
  62. >
  63. <!-- AI badge (first cell only) -->
  64. <div v-if="idx === 0" class="camera-ai-badge">&#x1F916; AI&#x68C0;&#x6D4B;</div>
  65. <!-- REC indicator -->
  66. <div class="camera-rec">REC</div>
  67. <!-- Video placeholder -->
  68. <div class="camera-placeholder-text">CCTV</div>
  69. <!-- AI detection box (first cell only) -->
  70. <div v-if="idx === 0" class="ai-detection-box" style="left:28%;top:18%;width:22%;height:38%">
  71. <div class="ai-detection-label">&#x5371;&#x9669;&#x884C;&#x4E3A;: &#x672A;&#x4F69;&#x6234;&#x9632;&#x62A4;</div>
  72. </div>
  73. <!-- Camera label -->
  74. <div class="camera-label">{{ cam }}</div>
  75. </div>
  76. </div>
  77. </div>
  78. </div>
  79. </div>
  80. </template>
  81. <script>
  82. import { getCameraList } from '@/api/screen'
  83. const TreeNode = {
  84. name: 'TreeNode',
  85. props: {
  86. node: { type: Object, required: true },
  87. depth: { type: Number, default: 0 },
  88. selectedLabel: { type: String, default: '' }
  89. },
  90. data() {
  91. return {
  92. expanded: this.depth === 0
  93. }
  94. },
  95. computed: {
  96. isLeaf() {
  97. return !this.node.children || this.node.children.length === 0
  98. },
  99. icon() {
  100. const icons = ['\u{1F3E2}', '\u{1F3D7}\uFE0F', '\u{1F4D0}', '\u{1F52C}']
  101. return icons[Math.min(this.depth, 3)]
  102. },
  103. isSelected() {
  104. return this.node.label === this.selectedLabel
  105. }
  106. },
  107. methods: {
  108. toggle() {
  109. if (!this.isLeaf) {
  110. this.expanded = !this.expanded
  111. }
  112. this.$emit('select', this.node.label)
  113. },
  114. onChildSelect(label) {
  115. this.$emit('select', label)
  116. }
  117. },
  118. template: `
  119. <div class="tree-node">
  120. <div
  121. class="tree-node-label"
  122. :class="{ selected: isSelected }"
  123. @click="toggle"
  124. >
  125. <span class="arrow" :class="{ open: expanded }">
  126. <template v-if="!isLeaf">&#x25B6;</template>
  127. <template v-else>&nbsp;&nbsp;</template>
  128. </span>
  129. <span>{{ icon }}</span>
  130. <span>{{ node.label }}</span>
  131. </div>
  132. <div v-if="!isLeaf" class="tree-children" :class="{ open: expanded }">
  133. <tree-node
  134. v-for="(child, i) in node.children"
  135. :key="i"
  136. :node="child"
  137. :depth="depth + 1"
  138. :selected-label="selectedLabel"
  139. @select="onChildSelect"
  140. />
  141. </div>
  142. </div>
  143. `
  144. }
  145. export default {
  146. name: 'SecurityMonitor',
  147. components: {
  148. TreeNode
  149. },
  150. data() {
  151. return {
  152. searchText: '',
  153. selectedUnit: '',
  154. selectedTreeLabel: '',
  155. currentPage: 1,
  156. totalPages: 3,
  157. cameras: [],
  158. treeData: {
  159. label: '\u5B89\u79D1\u9662\u4E3B\u56ED\u533A',
  160. children: [
  161. {
  162. label: '\u7EFC\u5408\u5B9E\u9A8C\u697CA',
  163. children: [
  164. { label: '1\u5C42', children: [{ label: 'A101 \u5316\u5B66\u5B9E\u9A8C\u5BA4' }, { label: 'A102 \u5206\u6790\u5BA4' }] },
  165. { label: '2\u5C42', children: [{ label: 'A201 \u751F\u7269\u5B9E\u9A8C\u5BA4' }] },
  166. { label: '3\u5C42', children: [{ label: 'A301 \u6709\u673A\u5408\u6210\u5BA4' }, { label: 'A302 \u6838\u78C1\u5171\u632F\u5BA4' }, { label: 'A303 \u8D28\u8C31\u5BA4' }] },
  167. { label: '4\u5C42', children: [{ label: 'A401 X\u5C04\u7EBF\u5BA4' }] }
  168. ]
  169. },
  170. {
  171. label: '\u7269\u7406\u5B9E\u9A8C\u697CB',
  172. children: [
  173. { label: '1\u5C42', children: [{ label: 'B101 \u5149\u5B66\u5B9E\u9A8C\u5BA4' }] },
  174. { label: '2\u5C42', children: [{ label: 'B201 \u4F4E\u6E29\u5B9E\u9A8C\u5BA4' }] }
  175. ]
  176. },
  177. {
  178. label: '\u5DE5\u7A0B\u6280\u672F\u697CC',
  179. children: [
  180. { label: '1\u5C42', children: [{ label: 'C101 \u673A\u68B0\u52A0\u5DE5\u5BA4' }] },
  181. { label: '2\u5C42', children: [{ label: 'C201 \u6750\u6599\u6D4B\u8BD5\u5BA4' }] }
  182. ]
  183. }
  184. ]
  185. }
  186. }
  187. },
  188. created() {
  189. this.fetchCameras()
  190. },
  191. methods: {
  192. async fetchCameras() {
  193. try {
  194. const res = await getCameraList()
  195. if (res.code === 200) {
  196. this.cameras = res.data
  197. }
  198. } catch (e) {
  199. // fallback to static data
  200. this.cameras = [
  201. 'A301 \u6709\u673A\u5408\u6210\u5BA4',
  202. 'A302 \u6838\u78C1\u5171\u632F\u5BA4',
  203. 'A303 \u8D28\u8C31\u5BA4',
  204. 'A301 \u8D70\u5ECA',
  205. 'A302 \u8D70\u5ECA',
  206. 'A303 \u8D70\u5ECA',
  207. 'A\u5C42\u516C\u5171\u533A\u57DF',
  208. 'A\u5C42\u5B89\u5168\u901A\u9053',
  209. 'A\u5C42\u51FA\u5165\u53E3'
  210. ]
  211. }
  212. },
  213. onTreeNodeSelect(label) {
  214. this.selectedTreeLabel = label
  215. },
  216. prevPage() {
  217. if (this.currentPage > 1) {
  218. this.currentPage--
  219. }
  220. },
  221. nextPage() {
  222. if (this.currentPage < this.totalPages) {
  223. this.currentPage++
  224. }
  225. }
  226. }
  227. }
  228. </script>
  229. <style lang="scss" scoped>
  230. .security-monitor {
  231. display: flex;
  232. flex-direction: column;
  233. height: 100%;
  234. min-height: 0;
  235. }
  236. // ========== Monitor Header ==========
  237. .monitor-header {
  238. padding: 20px 30px;
  239. border-bottom: 1px solid $border;
  240. flex-shrink: 0;
  241. display: flex;
  242. align-items: center;
  243. gap: 25px;
  244. background: linear-gradient(90deg, rgba(0, 60, 160, 0.18), transparent);
  245. }
  246. .monitor-title-cn {
  247. font-size: 32px;
  248. font-weight: 600;
  249. color: $cyan;
  250. letter-spacing: 2px;
  251. }
  252. .monitor-title-en {
  253. font-size: 28px;
  254. color: $text-dim;
  255. margin-left: 10px;
  256. }
  257. .monitor-status {
  258. margin-left: auto;
  259. display: flex;
  260. align-items: center;
  261. gap: 15px;
  262. }
  263. .monitor-status-text {
  264. font-size: 28px;
  265. color: $text-dim;
  266. }
  267. // ========== Monitor Inner (2-column grid) ==========
  268. .monitor-inner {
  269. flex: 1;
  270. display: grid;
  271. grid-template-columns: 575px 1fr;
  272. gap: 0;
  273. min-height: 0;
  274. overflow: hidden;
  275. }
  276. // ========== Left Column ==========
  277. .monitor-left {
  278. display: flex;
  279. flex-direction: column;
  280. gap: 15px;
  281. padding: 20px;
  282. border-right: 1px solid $border;
  283. min-height: 0;
  284. overflow: hidden;
  285. }
  286. .search-box {
  287. display: flex;
  288. align-items: center;
  289. gap: 18px;
  290. padding: 15px 25px;
  291. border-radius: 10px;
  292. background: $bg-card;
  293. border: 1px solid $border;
  294. flex-shrink: 0;
  295. .search-icon {
  296. font-size: 32px;
  297. color: $text-dim;
  298. }
  299. input {
  300. flex: 1;
  301. background: none;
  302. border: none;
  303. outline: none;
  304. color: $text;
  305. font-size: 30px;
  306. font-family: inherit;
  307. &::placeholder {
  308. color: $text-dim;
  309. }
  310. }
  311. }
  312. .filter-select {
  313. padding: 12px 25px;
  314. border-radius: 10px;
  315. width: 100%;
  316. flex-shrink: 0;
  317. background: $bg-card;
  318. border: 1px solid $border;
  319. color: $text;
  320. font-size: 28px;
  321. font-family: inherit;
  322. outline: none;
  323. cursor: pointer;
  324. appearance: none;
  325. background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='8'%3E%3Cpath d='M0 0l6 8 6-8z' fill='rgba(30,144,255,0.6)'/%3E%3C/svg%3E");
  326. background-repeat: no-repeat;
  327. background-position: right 25px center;
  328. }
  329. // ========== Tree ==========
  330. .tree-wrap {
  331. flex: 1;
  332. overflow-y: auto;
  333. overflow-x: hidden;
  334. min-height: 0;
  335. &::-webkit-scrollbar {
  336. width: 8px;
  337. }
  338. &::-webkit-scrollbar-thumb {
  339. background: $border;
  340. border-radius: 5px;
  341. }
  342. }
  343. ::v-deep .tree-node-label {
  344. display: flex;
  345. align-items: center;
  346. gap: 15px;
  347. padding: 12px 20px;
  348. border-radius: 8px;
  349. font-size: 28px;
  350. color: $text;
  351. transition: all 0.2s;
  352. user-select: none;
  353. cursor: pointer;
  354. &:hover {
  355. background: $blue-dim;
  356. color: $blue;
  357. }
  358. &.selected {
  359. background: rgba(30, 144, 255, 0.15);
  360. color: $cyan;
  361. border-left: 5px solid $blue;
  362. }
  363. .arrow {
  364. transition: transform 0.2s;
  365. font-size: 25px;
  366. color: $text-dim;
  367. flex-shrink: 0;
  368. &.open {
  369. transform: rotate(90deg);
  370. }
  371. }
  372. }
  373. ::v-deep .tree-children {
  374. padding-left: 35px;
  375. display: none;
  376. &.open {
  377. display: block;
  378. }
  379. }
  380. // ========== Right Column ==========
  381. .monitor-right {
  382. padding: 20px;
  383. display: flex;
  384. flex-direction: column;
  385. gap: 15px;
  386. min-height: 0;
  387. }
  388. .camera-grid-header {
  389. display: flex;
  390. align-items: center;
  391. justify-content: space-between;
  392. flex-shrink: 0;
  393. }
  394. .camera-breadcrumb {
  395. font-size: 30px;
  396. color: $text-dim;
  397. span {
  398. color: $cyan;
  399. }
  400. }
  401. .camera-pager {
  402. display: flex;
  403. align-items: center;
  404. gap: 15px;
  405. }
  406. .pager-btn {
  407. width: 60px;
  408. height: 60px;
  409. border-radius: 8px;
  410. cursor: pointer;
  411. background: $bg-card;
  412. border: 1px solid $border;
  413. color: $text-dim;
  414. font-size: 32px;
  415. display: flex;
  416. align-items: center;
  417. justify-content: center;
  418. transition: all 0.2s;
  419. &:hover {
  420. border-color: $blue;
  421. color: $blue;
  422. }
  423. }
  424. .pager-info {
  425. font-size: 28px;
  426. color: $text-dim;
  427. }
  428. // ========== Camera Grid (3x3) ==========
  429. .camera-grid {
  430. display: grid;
  431. grid-template-columns: repeat(3, 1fr);
  432. grid-template-rows: repeat(3, 1fr);
  433. gap: 12px;
  434. flex: 1;
  435. min-height: 0;
  436. }
  437. .camera-cell {
  438. position: relative;
  439. border-radius: 10px;
  440. overflow: hidden;
  441. background: #020810;
  442. border: 1px solid $border;
  443. display: flex;
  444. align-items: center;
  445. justify-content: center;
  446. &:hover {
  447. border-color: $blue;
  448. }
  449. &.ai-cam {
  450. border-color: rgba(0, 230, 118, 0.4);
  451. }
  452. }
  453. .camera-placeholder-text {
  454. font-size: 48px;
  455. color: rgba(110, 165, 210, 0.15);
  456. letter-spacing: 8px;
  457. font-weight: 700;
  458. user-select: none;
  459. }
  460. .camera-label {
  461. position: absolute;
  462. bottom: 10px;
  463. left: 15px;
  464. font-size: 25px;
  465. color: rgba(255, 255, 255, 0.75);
  466. background: rgba(0, 0, 0, 0.5);
  467. padding: 3px 10px;
  468. border-radius: 5px;
  469. }
  470. .camera-rec {
  471. position: absolute;
  472. top: 10px;
  473. right: 15px;
  474. display: flex;
  475. align-items: center;
  476. gap: 8px;
  477. font-size: 25px;
  478. color: $red;
  479. background: rgba(0, 0, 0, 0.5);
  480. padding: 3px 10px;
  481. border-radius: 5px;
  482. &::before {
  483. content: '';
  484. width: 12px;
  485. height: 12px;
  486. border-radius: 50%;
  487. background: $red;
  488. animation: blinkRed 1s ease-in-out infinite;
  489. }
  490. }
  491. .camera-ai-badge {
  492. position: absolute;
  493. top: 10px;
  494. left: 15px;
  495. font-size: 25px;
  496. color: $green;
  497. background: rgba(0, 30, 15, 0.75);
  498. padding: 3px 10px;
  499. border-radius: 5px;
  500. border: 1px solid rgba(0, 230, 118, 0.3);
  501. z-index: 2;
  502. }
  503. .ai-detection-box {
  504. position: absolute;
  505. border: 5px solid $red;
  506. background: rgba(255, 59, 59, 0.1);
  507. border-radius: 5px;
  508. animation: detBox 1.5s ease-in-out infinite;
  509. }
  510. .ai-detection-label {
  511. position: absolute;
  512. bottom: 100%;
  513. left: 0;
  514. white-space: nowrap;
  515. font-size: 25px;
  516. background: $red;
  517. color: #fff;
  518. padding: 3px 10px;
  519. border-radius: 5px;
  520. margin-bottom: 5px;
  521. }
  522. // ========== Keyframes ==========
  523. @keyframes blinkRed {
  524. 0%, 100% { opacity: 1; }
  525. 50% { opacity: 0.35; }
  526. }
  527. @keyframes detBox {
  528. 0%, 100% {
  529. border-color: #ff3b3b;
  530. box-shadow: 0 0 20px rgba(255, 59, 59, 0.4);
  531. }
  532. 50% {
  533. border-color: #fca5a5;
  534. box-shadow: 0 0 45px rgba(255, 59, 59, 0.7);
  535. }
  536. }
  537. </style>