| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594 |
- <template>
- <div class="security-monitor panel">
- <!-- Monitor Header -->
- <div class="monitor-header">
- <span class="monitor-title-cn">📹 实时监控</span>
- <span class="monitor-title-en">CCTV Live Feed</span>
- <div class="monitor-status">
- <div class="status-dot green"></div>
- <span class="monitor-status-text">信号正常</span>
- </div>
- </div>
- <!-- Monitor Inner: 2-column grid -->
- <div class="monitor-inner">
- <!-- Left Column: Search + Filter + Tree -->
- <div class="monitor-left">
- <div class="search-box">
- <span class="search-icon">🔍</span>
- <input
- v-model="searchText"
- type="text"
- placeholder="搜索楼栋 / 楼层 / 房间…"
- />
- </div>
- <select v-model="selectedUnit" class="filter-select">
- <option value="">全部二级单位</option>
- <option>化学研究所</option>
- <option>物理研究所</option>
- <option>生物研究所</option>
- <option>材料研究所</option>
- <option>工程研究所</option>
- </select>
- <div class="tree-wrap">
- <tree-node
- v-if="treeData"
- :node="treeData"
- :depth="0"
- :selected-label="selectedTreeLabel"
- @select="onTreeNodeSelect"
- />
- </div>
- </div>
- <!-- Right Column: Breadcrumb + Pager + Camera Grid -->
- <div class="monitor-right">
- <div class="camera-grid-header">
- <div class="camera-breadcrumb">
- <span>安科院主园区</span> ›
- <span>综合实验楼A</span> ›
- <span>3层</span>
- </div>
- <div class="camera-pager">
- <button class="pager-btn" @click="prevPage">‹</button>
- <span class="pager-info">{{ currentPage }} / {{ totalPages }} 页</span>
- <button class="pager-btn" @click="nextPage">›</button>
- </div>
- </div>
- <div class="camera-grid">
- <div
- v-for="(cam, idx) in cameras"
- :key="'cam-' + idx"
- class="camera-cell"
- :class="{ 'ai-cam': idx === 0 }"
- >
- <!-- AI badge (first cell only) -->
- <div v-if="idx === 0" class="camera-ai-badge">🤖 AI检测</div>
- <!-- REC indicator -->
- <div class="camera-rec">REC</div>
- <!-- Video placeholder -->
- <div class="camera-placeholder-text">CCTV</div>
- <!-- AI detection box (first cell only) -->
- <div v-if="idx === 0" class="ai-detection-box" style="left:28%;top:18%;width:22%;height:38%">
- <div class="ai-detection-label">危险行为: 未佩戴防护</div>
- </div>
- <!-- Camera label -->
- <div class="camera-label">{{ cam }}</div>
- </div>
- </div>
- </div>
- </div>
- </div>
- </template>
- <script>
- import { getCameraList } from '@/api/screen'
- const TreeNode = {
- name: 'TreeNode',
- props: {
- node: { type: Object, required: true },
- depth: { type: Number, default: 0 },
- selectedLabel: { type: String, default: '' }
- },
- data() {
- return {
- expanded: this.depth === 0
- }
- },
- computed: {
- isLeaf() {
- return !this.node.children || this.node.children.length === 0
- },
- icon() {
- const icons = ['\u{1F3E2}', '\u{1F3D7}\uFE0F', '\u{1F4D0}', '\u{1F52C}']
- return icons[Math.min(this.depth, 3)]
- },
- isSelected() {
- return this.node.label === this.selectedLabel
- }
- },
- methods: {
- toggle() {
- if (!this.isLeaf) {
- this.expanded = !this.expanded
- }
- this.$emit('select', this.node.label)
- },
- onChildSelect(label) {
- this.$emit('select', label)
- }
- },
- template: `
- <div class="tree-node">
- <div
- class="tree-node-label"
- :class="{ selected: isSelected }"
- @click="toggle"
- >
- <span class="arrow" :class="{ open: expanded }">
- <template v-if="!isLeaf">▶</template>
- <template v-else> </template>
- </span>
- <span>{{ icon }}</span>
- <span>{{ node.label }}</span>
- </div>
- <div v-if="!isLeaf" class="tree-children" :class="{ open: expanded }">
- <tree-node
- v-for="(child, i) in node.children"
- :key="i"
- :node="child"
- :depth="depth + 1"
- :selected-label="selectedLabel"
- @select="onChildSelect"
- />
- </div>
- </div>
- `
- }
- export default {
- name: 'SecurityMonitor',
- components: {
- TreeNode
- },
- data() {
- return {
- searchText: '',
- selectedUnit: '',
- selectedTreeLabel: '',
- currentPage: 1,
- totalPages: 3,
- cameras: [],
- treeData: {
- label: '\u5B89\u79D1\u9662\u4E3B\u56ED\u533A',
- children: [
- {
- label: '\u7EFC\u5408\u5B9E\u9A8C\u697CA',
- children: [
- { label: '1\u5C42', children: [{ label: 'A101 \u5316\u5B66\u5B9E\u9A8C\u5BA4' }, { label: 'A102 \u5206\u6790\u5BA4' }] },
- { label: '2\u5C42', children: [{ label: 'A201 \u751F\u7269\u5B9E\u9A8C\u5BA4' }] },
- { label: '3\u5C42', children: [{ label: 'A301 \u6709\u673A\u5408\u6210\u5BA4' }, { label: 'A302 \u6838\u78C1\u5171\u632F\u5BA4' }, { label: 'A303 \u8D28\u8C31\u5BA4' }] },
- { label: '4\u5C42', children: [{ label: 'A401 X\u5C04\u7EBF\u5BA4' }] }
- ]
- },
- {
- label: '\u7269\u7406\u5B9E\u9A8C\u697CB',
- children: [
- { label: '1\u5C42', children: [{ label: 'B101 \u5149\u5B66\u5B9E\u9A8C\u5BA4' }] },
- { label: '2\u5C42', children: [{ label: 'B201 \u4F4E\u6E29\u5B9E\u9A8C\u5BA4' }] }
- ]
- },
- {
- label: '\u5DE5\u7A0B\u6280\u672F\u697CC',
- children: [
- { label: '1\u5C42', children: [{ label: 'C101 \u673A\u68B0\u52A0\u5DE5\u5BA4' }] },
- { label: '2\u5C42', children: [{ label: 'C201 \u6750\u6599\u6D4B\u8BD5\u5BA4' }] }
- ]
- }
- ]
- }
- }
- },
- created() {
- this.fetchCameras()
- },
- methods: {
- async fetchCameras() {
- try {
- const res = await getCameraList()
- if (res.code === 200) {
- this.cameras = res.data
- }
- } catch (e) {
- // fallback to static data
- this.cameras = [
- 'A301 \u6709\u673A\u5408\u6210\u5BA4',
- 'A302 \u6838\u78C1\u5171\u632F\u5BA4',
- 'A303 \u8D28\u8C31\u5BA4',
- 'A301 \u8D70\u5ECA',
- 'A302 \u8D70\u5ECA',
- 'A303 \u8D70\u5ECA',
- 'A\u5C42\u516C\u5171\u533A\u57DF',
- 'A\u5C42\u5B89\u5168\u901A\u9053',
- 'A\u5C42\u51FA\u5165\u53E3'
- ]
- }
- },
- onTreeNodeSelect(label) {
- this.selectedTreeLabel = label
- },
- prevPage() {
- if (this.currentPage > 1) {
- this.currentPage--
- }
- },
- nextPage() {
- if (this.currentPage < this.totalPages) {
- this.currentPage++
- }
- }
- }
- }
- </script>
- <style lang="scss" scoped>
- .security-monitor {
- display: flex;
- flex-direction: column;
- height: 100%;
- min-height: 0;
- }
- // ========== Monitor Header ==========
- .monitor-header {
- padding: 20px 30px;
- border-bottom: 1px solid $border;
- flex-shrink: 0;
- display: flex;
- align-items: center;
- gap: 25px;
- background: linear-gradient(90deg, rgba(0, 60, 160, 0.18), transparent);
- }
- .monitor-title-cn {
- font-size: 32px;
- font-weight: 600;
- color: $cyan;
- letter-spacing: 2px;
- }
- .monitor-title-en {
- font-size: 28px;
- color: $text-dim;
- margin-left: 10px;
- }
- .monitor-status {
- margin-left: auto;
- display: flex;
- align-items: center;
- gap: 15px;
- }
- .monitor-status-text {
- font-size: 28px;
- color: $text-dim;
- }
- // ========== Monitor Inner (2-column grid) ==========
- .monitor-inner {
- flex: 1;
- display: grid;
- grid-template-columns: 575px 1fr;
- gap: 0;
- min-height: 0;
- overflow: hidden;
- }
- // ========== Left Column ==========
- .monitor-left {
- display: flex;
- flex-direction: column;
- gap: 15px;
- padding: 20px;
- border-right: 1px solid $border;
- min-height: 0;
- overflow: hidden;
- }
- .search-box {
- display: flex;
- align-items: center;
- gap: 18px;
- padding: 15px 25px;
- border-radius: 10px;
- background: $bg-card;
- border: 1px solid $border;
- flex-shrink: 0;
- .search-icon {
- font-size: 32px;
- color: $text-dim;
- }
- input {
- flex: 1;
- background: none;
- border: none;
- outline: none;
- color: $text;
- font-size: 30px;
- font-family: inherit;
- &::placeholder {
- color: $text-dim;
- }
- }
- }
- .filter-select {
- padding: 12px 25px;
- border-radius: 10px;
- width: 100%;
- flex-shrink: 0;
- background: $bg-card;
- border: 1px solid $border;
- color: $text;
- font-size: 28px;
- font-family: inherit;
- outline: none;
- cursor: pointer;
- appearance: none;
- 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");
- background-repeat: no-repeat;
- background-position: right 25px center;
- }
- // ========== Tree ==========
- .tree-wrap {
- flex: 1;
- overflow-y: auto;
- overflow-x: hidden;
- min-height: 0;
- &::-webkit-scrollbar {
- width: 8px;
- }
- &::-webkit-scrollbar-thumb {
- background: $border;
- border-radius: 5px;
- }
- }
- ::v-deep .tree-node-label {
- display: flex;
- align-items: center;
- gap: 15px;
- padding: 12px 20px;
- border-radius: 8px;
- font-size: 28px;
- color: $text;
- transition: all 0.2s;
- user-select: none;
- cursor: pointer;
- &:hover {
- background: $blue-dim;
- color: $blue;
- }
- &.selected {
- background: rgba(30, 144, 255, 0.15);
- color: $cyan;
- border-left: 5px solid $blue;
- }
- .arrow {
- transition: transform 0.2s;
- font-size: 25px;
- color: $text-dim;
- flex-shrink: 0;
- &.open {
- transform: rotate(90deg);
- }
- }
- }
- ::v-deep .tree-children {
- padding-left: 35px;
- display: none;
- &.open {
- display: block;
- }
- }
- // ========== Right Column ==========
- .monitor-right {
- padding: 20px;
- display: flex;
- flex-direction: column;
- gap: 15px;
- min-height: 0;
- }
- .camera-grid-header {
- display: flex;
- align-items: center;
- justify-content: space-between;
- flex-shrink: 0;
- }
- .camera-breadcrumb {
- font-size: 30px;
- color: $text-dim;
- span {
- color: $cyan;
- }
- }
- .camera-pager {
- display: flex;
- align-items: center;
- gap: 15px;
- }
- .pager-btn {
- width: 60px;
- height: 60px;
- border-radius: 8px;
- cursor: pointer;
- background: $bg-card;
- border: 1px solid $border;
- color: $text-dim;
- font-size: 32px;
- display: flex;
- align-items: center;
- justify-content: center;
- transition: all 0.2s;
- &:hover {
- border-color: $blue;
- color: $blue;
- }
- }
- .pager-info {
- font-size: 28px;
- color: $text-dim;
- }
- // ========== Camera Grid (3x3) ==========
- .camera-grid {
- display: grid;
- grid-template-columns: repeat(3, 1fr);
- grid-template-rows: repeat(3, 1fr);
- gap: 12px;
- flex: 1;
- min-height: 0;
- }
- .camera-cell {
- position: relative;
- border-radius: 10px;
- overflow: hidden;
- background: #020810;
- border: 1px solid $border;
- display: flex;
- align-items: center;
- justify-content: center;
- &:hover {
- border-color: $blue;
- }
- &.ai-cam {
- border-color: rgba(0, 230, 118, 0.4);
- }
- }
- .camera-placeholder-text {
- font-size: 48px;
- color: rgba(110, 165, 210, 0.15);
- letter-spacing: 8px;
- font-weight: 700;
- user-select: none;
- }
- .camera-label {
- position: absolute;
- bottom: 10px;
- left: 15px;
- font-size: 25px;
- color: rgba(255, 255, 255, 0.75);
- background: rgba(0, 0, 0, 0.5);
- padding: 3px 10px;
- border-radius: 5px;
- }
- .camera-rec {
- position: absolute;
- top: 10px;
- right: 15px;
- display: flex;
- align-items: center;
- gap: 8px;
- font-size: 25px;
- color: $red;
- background: rgba(0, 0, 0, 0.5);
- padding: 3px 10px;
- border-radius: 5px;
- &::before {
- content: '';
- width: 12px;
- height: 12px;
- border-radius: 50%;
- background: $red;
- animation: blinkRed 1s ease-in-out infinite;
- }
- }
- .camera-ai-badge {
- position: absolute;
- top: 10px;
- left: 15px;
- font-size: 25px;
- color: $green;
- background: rgba(0, 30, 15, 0.75);
- padding: 3px 10px;
- border-radius: 5px;
- border: 1px solid rgba(0, 230, 118, 0.3);
- z-index: 2;
- }
- .ai-detection-box {
- position: absolute;
- border: 5px solid $red;
- background: rgba(255, 59, 59, 0.1);
- border-radius: 5px;
- animation: detBox 1.5s ease-in-out infinite;
- }
- .ai-detection-label {
- position: absolute;
- bottom: 100%;
- left: 0;
- white-space: nowrap;
- font-size: 25px;
- background: $red;
- color: #fff;
- padding: 3px 10px;
- border-radius: 5px;
- margin-bottom: 5px;
- }
- // ========== Keyframes ==========
- @keyframes blinkRed {
- 0%, 100% { opacity: 1; }
- 50% { opacity: 0.35; }
- }
- @keyframes detBox {
- 0%, 100% {
- border-color: #ff3b3b;
- box-shadow: 0 0 20px rgba(255, 59, 59, 0.4);
- }
- 50% {
- border-color: #fca5a5;
- box-shadow: 0 0 45px rgba(255, 59, 59, 0.7);
- }
- }
- </style>
|