| ¶Ô±ÈÐÂÎļþ |
| | |
| | | <template> |
| | | <div class="app-container"> |
| | | <el-row :gutter="16"> |
| | | <!-- 左侧ï¼è§é¢çæ§åè¡¨ä¸ææè®°å½ --> |
| | | <el-col :span="16"> |
| | | <!-- è§é¢çæ§å表 --> |
| | | <el-card shadow="never" class="section-card"> |
| | | <template #header> |
| | | <div class="card-header"> |
| | | <span>è§é¢çæ§ç¹ä½ç®¡ç</span> |
| | | <div class="header-actions"> |
| | | <el-select v-model="selectedArea" placeholder="éæ©åºå" size="small" style="width: 160px" @change="filterCameras"> |
| | | <el-option label="å
¨é¨åºå" value="all" /> |
| | | <el-option v-for="area in areas" :key="area.id" :label="area.name" :value="area.id" /> |
| | | </el-select> |
| | | <el-select v-model="cameraStatus" placeholder="设å¤ç¶æ" size="small" style="width: 120px" @change="filterCameras"> |
| | | <el-option label="å
¨é¨ç¶æ" value="all" /> |
| | | <el-option label="å¨çº¿" value="online" /> |
| | | <el-option label="离线" value="offline" /> |
| | | </el-select> |
| | | </div> |
| | | </div> |
| | | </template> |
| | | <el-table :data="filteredCameras" border style="width: 100%" max-height="320"> |
| | | <el-table-column type="index" width="50" label="åºå·" align="center" /> |
| | | <el-table-column prop="name" label="çæ§ç¹ä½" min-width="140" show-overflow-tooltip /> |
| | | <el-table-column prop="areaName" label="æå±åºå" width="120" /> |
| | | <el-table-column label="设å¤ç¶æ" width="100" align="center"> |
| | | <template #default="{ row }"> |
| | | <el-tag :type="row.status === 'online' ? 'success' : 'danger'" size="small"> |
| | | {{ row.status === 'online' ? 'å¨çº¿' : '离线' }} |
| | | </el-tag> |
| | | </template> |
| | | </el-table-column> |
| | | <el-table-column label="AIè¯å«" width="100" align="center"> |
| | | <template #default="{ row }"> |
| | | <el-tag v-if="row.aiEnabled" type="success" size="small">å·²å¯ç¨</el-tag> |
| | | <el-tag v-else type="info" size="small">æªå¯ç¨</el-tag> |
| | | </template> |
| | | </el-table-column> |
| | | <el-table-column label="é¨ç¦èå¨" width="100" align="center"> |
| | | <template #default="{ row }"> |
| | | <el-tag v-if="row.doorLinked" type="primary" size="small">å·²ç»å®</el-tag> |
| | | <el-tag v-else type="info" size="small">æªç»å®</el-tag> |
| | | </template> |
| | | </el-table-column> |
| | | <el-table-column label="æä½" width="180" align="center" fixed="right"> |
| | | <template #default="{ row }"> |
| | | <el-button link type="primary" size="small" @click="viewRealtime(row)">宿¶ç»é¢</el-button> |
| | | <el-button link type="success" size="small" @click="viewCaptures(row)">ææè®°å½</el-button> |
| | | </template> |
| | | </el-table-column> |
| | | </el-table> |
| | | </el-card> |
| | | |
| | | <!-- é¨ç¦ææè®°å½ --> |
| | | <el-card shadow="never" class="section-card"> |
| | | <template #header> |
| | | <div class="card-header"> |
| | | <span>é¨ç¦ææè®°å½</span> |
| | | <div class="header-actions"> |
| | | <el-date-picker |
| | | v-model="captureDate" |
| | | type="daterange" |
| | | range-separator="è³" |
| | | start-placeholder="å¼å§æ¥æ" |
| | | end-placeholder="ç»ææ¥æ" |
| | | size="small" |
| | | style="width: 260px" |
| | | @change="loadCaptures" |
| | | /> |
| | | <el-select v-model="captureEventType" placeholder="äºä»¶ç±»å" size="small" style="width: 100px" clearable> |
| | | <el-option label="å
¨é¨" value="" /> |
| | | <el-option label="è¿å
¥" value="entry" /> |
| | | <el-option label="离å¼" value="exit" /> |
| | | </el-select> |
| | | <el-input v-model="captureSearch" placeholder="æç´¢å·¥å·/åºå" size="small" style="width: 140px" clearable> |
| | | <template #prefix> |
| | | <el-icon><Search /></el-icon> |
| | | </template> |
| | | </el-input> |
| | | <el-button type="primary" size="small" @click="exportCaptures"> |
| | | <el-icon><Download /></el-icon> |
| | | å¯¼åº |
| | | </el-button> |
| | | </div> |
| | | </div> |
| | | </template> |
| | | |
| | | <!-- ç»è®¡ä¿¡æ¯ --> |
| | | <div class="capture-stats"> |
| | | <el-row :gutter="16"> |
| | | <el-col :span="6"> |
| | | <div class="stat-item"> |
| | | <div class="stat-label">仿¥ææ</div> |
| | | <div class="stat-value">{{ captureStats.today }}</div> |
| | | </div> |
| | | </el-col> |
| | | <el-col :span="6"> |
| | | <div class="stat-item"> |
| | | <div class="stat-label">è¿å
¥è®°å½</div> |
| | | <div class="stat-value" style="color: #67c23a">{{ captureStats.entry }}</div> |
| | | </div> |
| | | </el-col> |
| | | <el-col :span="6"> |
| | | <div class="stat-item"> |
| | | <div class="stat-label">离å¼è®°å½</div> |
| | | <div class="stat-value" style="color: #e6a23c">{{ captureStats.exit }}</div> |
| | | </div> |
| | | </el-col> |
| | | <el-col :span="6"> |
| | | <div class="stat-item"> |
| | | <div class="stat-label">ä½å¹é
度</div> |
| | | <div class="stat-value" style="color: #f56c6c">{{ captureStats.lowMatch }}</div> |
| | | </div> |
| | | </el-col> |
| | | </el-row> |
| | | </div> |
| | | |
| | | <el-table |
| | | :data="paginatedCaptures" |
| | | border |
| | | style="width: 100%" |
| | | max-height="350" |
| | | @selection-change="handleCaptureSelection" |
| | | > |
| | | <el-table-column type="selection" width="45" align="center" /> |
| | | <el-table-column type="index" width="50" label="åºå·" align="center" :index="indexMethod" /> |
| | | <el-table-column prop="time" label="æææ¶é´" width="155" sortable /> |
| | | <el-table-column prop="personId" label="å·¥å·" width="110" show-overflow-tooltip /> |
| | | <el-table-column prop="department" label="é¨é¨" width="100" show-overflow-tooltip /> |
| | | <el-table-column prop="areaName" label="åºå" width="110" show-overflow-tooltip /> |
| | | <el-table-column label="é¨ç¦äºä»¶" width="90" align="center"> |
| | | <template #default="{ row }"> |
| | | <el-tag :type="row.eventType === 'entry' ? 'success' : 'warning'" size="small"> |
| | | {{ row.eventType === 'entry' ? 'è¿å
¥' : '离å¼' }} |
| | | </el-tag> |
| | | </template> |
| | | </el-table-column> |
| | | <el-table-column label="人è¸å¹é
" width="95" align="center" sortable :sort-method="(a, b) => a.faceMatch - b.faceMatch"> |
| | | <template #default="{ row }"> |
| | | <el-tag :type="getFaceMatchType(row.faceMatch)" size="small"> |
| | | {{ row.faceMatch }}% |
| | | </el-tag> |
| | | </template> |
| | | </el-table-column> |
| | | <el-table-column prop="cameraName" label="æå头" width="120" show-overflow-tooltip /> |
| | | <el-table-column label="æä½" width="150" align="center" fixed="right"> |
| | | <template #default="{ row }"> |
| | | <el-button link type="primary" size="small" @click="viewSnapshot(row)"> |
| | | <el-icon><View /></el-icon> |
| | | æ¥ç |
| | | </el-button> |
| | | <el-button link type="success" size="small" @click="downloadSnapshot(row)"> |
| | | <el-icon><Download /></el-icon> |
| | | ä¸è½½ |
| | | </el-button> |
| | | </template> |
| | | </el-table-column> |
| | | </el-table> |
| | | |
| | | <!-- å页 --> |
| | | <div class="pagination-container"> |
| | | <el-pagination |
| | | v-model:current-page="capturePage" |
| | | v-model:page-size="capturePageSize" |
| | | :page-sizes="[10, 20, 50, 100]" |
| | | :total="filteredCaptures.length" |
| | | layout="total, sizes, prev, pager, next, jumper" |
| | | @size-change="handleSizeChange" |
| | | @current-change="handleCurrentChange" |
| | | /> |
| | | </div> |
| | | </el-card> |
| | | </el-col> |
| | | |
| | | <!-- å³ä¾§ï¼è¿è§åè¦ --> |
| | | <el-col :span="8"> |
| | | <el-card shadow="never" class="section-card"> |
| | | <template #header> |
| | | <div class="card-header"> |
| | | <span>è¿è§è¡ä¸ºåè¦</span> |
| | | <div class="header-actions"> |
| | | <el-badge :value="unreadAlarmCount" :max="99" type="danger"> |
| | | <el-switch v-model="alarmEnabled" inline-prompt active-text="åè¦å¼" inactive-text="åè¦å
³" /> |
| | | </el-badge> |
| | | </div> |
| | | </div> |
| | | </template> |
| | | <el-tabs v-model="alarmTab" type="border-card" style="height: 650px"> |
| | | <el-tab-pane label="å
¨é¨åè¦" name="all"> |
| | | <div> |
| | | <el-timeline style="max-height: 580px; overflow-y: auto; padding-right: 10px"> |
| | | <el-timeline-item |
| | | v-for="(alarm, idx) in displayAlarms" |
| | | :key="idx" |
| | | :type="getAlarmType(alarm.type)" |
| | | :timestamp="alarm.time" |
| | | :hollow="alarm.handled" |
| | | > |
| | | <div class="alarm-item" :class="{ handled: alarm.handled }"> |
| | | <div class="alarm-header"> |
| | | <el-tag :type="getAlarmTagType(alarm.type)" size="small">{{ alarm.typeText }}</el-tag> |
| | | <span class="alarm-area">{{ alarm.areaName }}</span> |
| | | </div> |
| | | <div class="alarm-content"> |
| | | {{ alarm.description }} |
| | | </div> |
| | | <div class="alarm-footer"> |
| | | <span class="alarm-camera">æå头: {{ alarm.cameraName }}</span> |
| | | <el-button |
| | | v-if="!alarm.handled" |
| | | link |
| | | type="primary" |
| | | size="small" |
| | | @click="handleAlarm(alarm)" |
| | | > |
| | | å¤ç |
| | | </el-button> |
| | | <span v-else class="handled-text">å·²å¤ç</span> |
| | | </div> |
| | | </div> |
| | | </el-timeline-item> |
| | | </el-timeline> |
| | | </div> |
| | | </el-tab-pane> |
| | | <el-tab-pane label="强é¯åè¦" name="intrusion"> |
| | | <div> |
| | | <el-timeline style="max-height: 580px; overflow-y: auto; padding-right: 10px"> |
| | | <el-timeline-item |
| | | v-for="(alarm, idx) in intrusionAlarms" |
| | | :key="idx" |
| | | type="danger" |
| | | :timestamp="alarm.time" |
| | | > |
| | | <div class="alarm-item"> |
| | | <div class="alarm-header"> |
| | | <el-tag type="danger" size="small">强é¯åè¦</el-tag> |
| | | <span class="alarm-area">{{ alarm.areaName }}</span> |
| | | </div> |
| | | <div class="alarm-content">{{ alarm.description }}</div> |
| | | </div> |
| | | </el-timeline-item> |
| | | </el-timeline> |
| | | </div> |
| | | </el-tab-pane> |
| | | <el-tab-pane label="å°¾éåè¦" name="tailgating"> |
| | | <div> |
| | | <el-timeline style="max-height: 580px; overflow-y: auto; padding-right: 10px"> |
| | | <el-timeline-item |
| | | v-for="(alarm, idx) in tailgatingAlarms" |
| | | :key="idx" |
| | | type="warning" |
| | | :timestamp="alarm.time" |
| | | > |
| | | <div class="alarm-item"> |
| | | <div class="alarm-header"> |
| | | <el-tag type="warning" size="small">å°¾éåè¦</el-tag> |
| | | <span class="alarm-area">{{ alarm.areaName }}</span> |
| | | </div> |
| | | <div class="alarm-content">{{ alarm.description }}</div> |
| | | </div> |
| | | </el-timeline-item> |
| | | </el-timeline> |
| | | </div> |
| | | </el-tab-pane> |
| | | <el-tab-pane label="å¤äººéè¡" name="multiple"> |
| | | <div> |
| | | <el-timeline style="max-height: 580px; overflow-y: auto; padding-right: 10px"> |
| | | <el-timeline-item |
| | | v-for="(alarm, idx) in multipleAlarms" |
| | | :key="idx" |
| | | type="warning" |
| | | :timestamp="alarm.time" |
| | | > |
| | | <div class="alarm-item"> |
| | | <div class="alarm-header"> |
| | | <el-tag type="warning" size="small">å¤äººéè¡</el-tag> |
| | | <span class="alarm-area">{{ alarm.areaName }}</span> |
| | | </div> |
| | | <div class="alarm-content">{{ alarm.description }}</div> |
| | | </div> |
| | | </el-timeline-item> |
| | | </el-timeline> |
| | | </div> |
| | | </el-tab-pane> |
| | | </el-tabs> |
| | | </el-card> |
| | | </el-col> |
| | | </el-row> |
| | | |
| | | <!-- 宿¶ç»é¢å¯¹è¯æ¡ --> |
| | | <el-dialog v-model="realtimeVisible" :title="`宿¶çæ§ - ${currentCamera.name}`" width="800px"> |
| | | <div class="video-container"> |
| | | <div class="video-placeholder"> |
| | | <el-icon :size="80" color="#909399"><VideoCameraFilled /></el-icon> |
| | | <div class="video-info"> |
| | | <p>æå头: {{ currentCamera.name }}</p> |
| | | <p>ä½ç½®: {{ currentCamera.areaName }}</p> |
| | | <p>ç¶æ: <el-tag :type="currentCamera.status === 'online' ? 'success' : 'danger'" size="small">{{ currentCamera.status === 'online' ? 'å¨çº¿' : '离线' }}</el-tag></p> |
| | | <p class="tip">ï¼å®é
ç¯å¢å°æ¾ç¤ºå®æ¶è§é¢æµï¼</p> |
| | | </div> |
| | | </div> |
| | | </div> |
| | | <template #footer> |
| | | <el-button @click="realtimeVisible = false">å
³é</el-button> |
| | | <el-button type="primary" @click="captureSnapshot">æå¨ææ</el-button> |
| | | </template> |
| | | </el-dialog> |
| | | |
| | | <!-- å¿«ç
§æ¥çå¯¹è¯æ¡ --> |
| | | <el-dialog v-model="snapshotVisible" title="ææå¿«ç
§è¯¦æ
" width="800px" :close-on-click-modal="false"> |
| | | <div class="snapshot-dialog-body"> |
| | | <el-row :gutter="20"> |
| | | <el-col :span="14"> |
| | | <div class="snapshot-preview"> |
| | | <div class="snapshot-placeholder"> |
| | | <el-icon :size="80" color="#909399"><Picture /></el-icon> |
| | | <p>ææå¾çé¢è§</p> |
| | | <p class="tip">ï¼å®é
ç¯å¢å°æ¾ç¤ºé«æ¸
ææå¾çï¼</p> |
| | | <div class="snapshot-tools"> |
| | | <el-button-group> |
| | | <el-button size="small"> |
| | | <el-icon><ZoomIn /></el-icon> |
| | | æ¾å¤§ |
| | | </el-button> |
| | | <el-button size="small"> |
| | | <el-icon><ZoomOut /></el-icon> |
| | | ç¼©å° |
| | | </el-button> |
| | | <el-button size="small"> |
| | | <el-icon><RefreshRight /></el-icon> |
| | | æè½¬ |
| | | </el-button> |
| | | </el-button-group> |
| | | </div> |
| | | </div> |
| | | </div> |
| | | </el-col> |
| | | <el-col :span="10"> |
| | | <el-descriptions :column="1" border size="small"> |
| | | <el-descriptions-item label="æææ¶é´"> |
| | | <el-icon><Clock /></el-icon> |
| | | {{ currentSnapshot.time }} |
| | | </el-descriptions-item> |
| | | <el-descriptions-item label="å·¥å·"> |
| | | <el-icon><User /></el-icon> |
| | | {{ currentSnapshot.personId }} |
| | | </el-descriptions-item> |
| | | <el-descriptions-item label="é¨é¨"> |
| | | {{ currentSnapshot.department }} |
| | | </el-descriptions-item> |
| | | <el-descriptions-item label="æå±åºå"> |
| | | <el-icon><Location /></el-icon> |
| | | {{ currentSnapshot.areaName }} |
| | | </el-descriptions-item> |
| | | <el-descriptions-item label="æå头"> |
| | | <el-icon><VideoCameraFilled /></el-icon> |
| | | {{ currentSnapshot.cameraName }} |
| | | </el-descriptions-item> |
| | | <el-descriptions-item label="äºä»¶ç±»å"> |
| | | <el-tag :type="currentSnapshot.eventType === 'entry' ? 'success' : 'warning'" size="small"> |
| | | {{ currentSnapshot.eventType === 'entry' ? 'è¿å
¥' : '离å¼' }} |
| | | </el-tag> |
| | | </el-descriptions-item> |
| | | <el-descriptions-item label="人è¸å¹é
度"> |
| | | <el-progress |
| | | :percentage="currentSnapshot.faceMatch" |
| | | :color="currentSnapshot.faceMatch >= 90 ? '#67c23a' : '#e6a23c'" |
| | | :stroke-width="12" |
| | | > |
| | | <span style="font-size: 12px">{{ currentSnapshot.faceMatch }}%</span> |
| | | </el-progress> |
| | | </el-descriptions-item> |
| | | <el-descriptions-item label="使¸©æ£æµ"> |
| | | <span :style="{ color: currentSnapshot.temperature > 37.3 ? '#f56c6c' : '#67c23a' }"> |
| | | {{ currentSnapshot.temperature }}°C |
| | | </span> |
| | | <el-tag v-if="currentSnapshot.temperature > 37.3" type="danger" size="small" style="margin-left: 8px"> |
| | | å¼å¸¸ |
| | | </el-tag> |
| | | </el-descriptions-item> |
| | | <el-descriptions-item label="å£ç½©ä½©æ´"> |
| | | <el-tag :type="currentSnapshot.maskWearing ? 'success' : 'warning'" size="small"> |
| | | {{ currentSnapshot.maskWearing ? '已佩æ´' : 'æªä½©æ´' }} |
| | | </el-tag> |
| | | </el-descriptions-item> |
| | | <el-descriptions-item label="å®å
¨å¸½"> |
| | | <el-tag :type="currentSnapshot.helmetWearing ? 'success' : 'danger'" size="small"> |
| | | {{ currentSnapshot.helmetWearing ? '已佩æ´' : 'æªä½©æ´' }} |
| | | </el-tag> |
| | | </el-descriptions-item> |
| | | </el-descriptions> |
| | | </el-col> |
| | | </el-row> |
| | | |
| | | <div class="snapshot-notes"> |
| | | <el-divider content-position="left">夿³¨ä¿¡æ¯</el-divider> |
| | | <el-input |
| | | v-model="currentSnapshot.notes" |
| | | type="textarea" |
| | | :rows="3" |
| | | placeholder="æ·»å 夿³¨ä¿¡æ¯..." |
| | | /> |
| | | </div> |
| | | |
| | | </div> |
| | | |
| | | <template #footer> |
| | | <div class="dialog-footer-custom"> |
| | | <div> |
| | | <el-button size="small" @click="printSnapshot"> |
| | | <el-icon><Printer /></el-icon> |
| | | æå° |
| | | </el-button> |
| | | </div> |
| | | <div> |
| | | <el-button @click="snapshotVisible = false">å
³é</el-button> |
| | | <el-button type="success" @click="downloadSnapshot(currentSnapshot)"> |
| | | <el-icon><Download /></el-icon> |
| | | ä¸è½½å¾ç |
| | | </el-button> |
| | | <el-button type="primary" @click="saveNotes">ä¿å夿³¨</el-button> |
| | | </div> |
| | | </div> |
| | | </template> |
| | | </el-dialog> |
| | | </div> |
| | | </template> |
| | | |
| | | <script setup> |
| | | import { ref, reactive, computed, onMounted, onBeforeUnmount } from "vue"; |
| | | import { |
| | | VideoCameraFilled, Picture, Search, Download, View, |
| | | Clock, User, Location, ZoomIn, ZoomOut, RefreshRight, Printer |
| | | } from "@element-plus/icons-vue"; |
| | | import { ElMessage, ElMessageBox } from "element-plus"; |
| | | |
| | | // åºåæ°æ® |
| | | const areas = ref([ |
| | | { id: "A01", name: "ç
¤åºå
¥å£" }, |
| | | { id: "A02", name: "æ´ç
¤è½¦é´" }, |
| | | { id: "A03", name: "å±é©ååº" }, |
| | | { id: "A04", name: "䏿§å®¤" }, |
| | | { id: "A05", name: "é
çµå®¤" }, |
| | | ]); |
| | | |
| | | // æåå¤´æ°æ® |
| | | const cameras = ref([ |
| | | { id: "C001", name: "ç
¤åºå
¥å£ä¸é¨", areaId: "A01", areaName: "ç
¤åºå
¥å£", status: "online", aiEnabled: true, doorLinked: true }, |
| | | { id: "C002", name: "ç
¤åºå
¥å£è¥¿é¨", areaId: "A01", areaName: "ç
¤åºå
¥å£", status: "online", aiEnabled: true, doorLinked: true }, |
| | | { id: "C003", name: "æ´ç
¤è½¦é´ä¸»éé", areaId: "A02", areaName: "æ´ç
¤è½¦é´", status: "online", aiEnabled: true, doorLinked: true }, |
| | | { id: "C004", name: "æ´ç
¤è½¦é´ä¸ä¾§", areaId: "A02", areaName: "æ´ç
¤è½¦é´", status: "online", aiEnabled: false, doorLinked: false }, |
| | | { id: "C005", name: "å±é©ååºå¤§é¨", areaId: "A03", areaName: "å±é©ååº", status: "online", aiEnabled: true, doorLinked: true }, |
| | | { id: "C006", name: "å±é©ååºå
é¨", areaId: "A03", areaName: "å±é©ååº", status: "offline", aiEnabled: true, doorLinked: false }, |
| | | { id: "C007", name: "䏿§å®¤å
¥å£", areaId: "A04", areaName: "䏿§å®¤", status: "online", aiEnabled: true, doorLinked: true }, |
| | | { id: "C008", name: "é
çµå®¤é¨ç¦", areaId: "A05", areaName: "é
çµå®¤", status: "online", aiEnabled: true, doorLinked: true }, |
| | | ]); |
| | | |
| | | const selectedArea = ref("all"); |
| | | const cameraStatus = ref("all"); |
| | | const filteredCameras = ref([]); |
| | | |
| | | function filterCameras() { |
| | | let result = cameras.value; |
| | | if (selectedArea.value !== "all") { |
| | | result = result.filter(c => c.areaId === selectedArea.value); |
| | | } |
| | | if (cameraStatus.value !== "all") { |
| | | result = result.filter(c => c.status === cameraStatus.value); |
| | | } |
| | | filteredCameras.value = result; |
| | | } |
| | | |
| | | // é¨ç¦ææè®°å½ |
| | | const captureDate = ref([new Date(), new Date()]); |
| | | const captureSearch = ref(""); |
| | | const captureEventType = ref(""); |
| | | const capturePage = ref(1); |
| | | const capturePageSize = ref(10); |
| | | const selectedCaptures = ref([]); |
| | | |
| | | // çææ´å¤æ¨¡ææ°æ® |
| | | const generateCaptureData = () => { |
| | | const departments = ["ç产ä¸é", "æºçµç", "å®çç§", "è°åº¦å®¤", "åéªå®¤", "è¿è¾é", "ç»´ä¿®ç", "ä»å¨ç§"]; |
| | | const areaList = ["ç
¤åºå
¥å£", "æ´ç
¤è½¦é´", "å±é©ååº", "䏿§å®¤", "é
çµå®¤"]; |
| | | const cameraList = [ |
| | | "ç
¤åºå
¥å£ä¸é¨", "ç
¤åºå
¥å£è¥¿é¨", "æ´ç
¤è½¦é´ä¸»éé", "æ´ç
¤è½¦é´ä¸ä¾§", |
| | | "å±é©ååºå¤§é¨", "å±é©ååºå
é¨", "䏿§å®¤å
¥å£", "é
çµå®¤é¨ç¦" |
| | | ]; |
| | | |
| | | const data = []; |
| | | const now = new Date(); |
| | | |
| | | for (let i = 0; i < 86; i++) { |
| | | const randomMinutes = Math.floor(Math.random() * 1440); // 24å°æ¶å
|
| | | const captureTime = new Date(now.getTime() - randomMinutes * 60 * 1000); |
| | | const hh = String(captureTime.getHours()).padStart(2, "0"); |
| | | const mm = String(captureTime.getMinutes()).padStart(2, "0"); |
| | | const ss = String(captureTime.getSeconds()).padStart(2, "0"); |
| | | |
| | | const area = areaList[Math.floor(Math.random() * areaList.length)]; |
| | | const eventType = Math.random() > 0.5 ? "entry" : "exit"; |
| | | const faceMatch = Math.floor(Math.random() * 15) + 85; // 85-100 |
| | | const temperature = (36.0 + Math.random() * 1.5).toFixed(1); |
| | | |
| | | data.push({ |
| | | id: i + 1, |
| | | time: `2025-10-28 ${hh}:${mm}:${ss}`, |
| | | personId: `EMP${String(1001 + i).padStart(4, '0')}`, |
| | | department: departments[Math.floor(Math.random() * departments.length)], |
| | | areaName: area, |
| | | cameraName: cameraList[Math.floor(Math.random() * cameraList.length)], |
| | | eventType: eventType, |
| | | faceMatch: faceMatch, |
| | | temperature: parseFloat(temperature), |
| | | maskWearing: Math.random() > 0.1, |
| | | helmetWearing: Math.random() > 0.15, |
| | | notes: "" |
| | | }); |
| | | } |
| | | |
| | | return data.sort((a, b) => b.time.localeCompare(a.time)); |
| | | }; |
| | | |
| | | const captures = ref(generateCaptureData()); |
| | | |
| | | // ç»è®¡ä¿¡æ¯ |
| | | const captureStats = computed(() => { |
| | | const today = captures.value.length; |
| | | const entry = captures.value.filter(c => c.eventType === 'entry').length; |
| | | const exit = captures.value.filter(c => c.eventType === 'exit').length; |
| | | const lowMatch = captures.value.filter(c => c.faceMatch < 90).length; |
| | | |
| | | return { today, entry, exit, lowMatch }; |
| | | }); |
| | | |
| | | // è¿æ»¤ææè®°å½ |
| | | const filteredCaptures = computed(() => { |
| | | let result = captures.value; |
| | | |
| | | // æå
³é®è¯æç´¢ |
| | | if (captureSearch.value) { |
| | | const keyword = captureSearch.value.toLowerCase(); |
| | | result = result.filter(c => |
| | | c.personId.toLowerCase().includes(keyword) || |
| | | c.areaName.toLowerCase().includes(keyword) |
| | | ); |
| | | } |
| | | |
| | | // æäºä»¶ç±»åè¿æ»¤ |
| | | if (captureEventType.value) { |
| | | result = result.filter(c => c.eventType === captureEventType.value); |
| | | } |
| | | |
| | | return result; |
| | | }); |
| | | |
| | | // åé¡µæ°æ® |
| | | const paginatedCaptures = computed(() => { |
| | | const start = (capturePage.value - 1) * capturePageSize.value; |
| | | const end = start + capturePageSize.value; |
| | | return filteredCaptures.value.slice(start, end); |
| | | }); |
| | | |
| | | // åé¡µç´¢å¼æ¹æ³ |
| | | const indexMethod = (index) => { |
| | | return (capturePage.value - 1) * capturePageSize.value + index + 1; |
| | | }; |
| | | |
| | | function loadCaptures() { |
| | | ElMessage.success("å·²å è½½é宿¥æèå´çææè®°å½"); |
| | | } |
| | | |
| | | function handleSizeChange(val) { |
| | | capturePageSize.value = val; |
| | | capturePage.value = 1; |
| | | } |
| | | |
| | | function handleCurrentChange(val) { |
| | | capturePage.value = val; |
| | | } |
| | | |
| | | function handleCaptureSelection(selection) { |
| | | selectedCaptures.value = selection; |
| | | } |
| | | |
| | | function getFaceMatchType(faceMatch) { |
| | | if (faceMatch >= 95) return 'success'; |
| | | if (faceMatch >= 90) return 'info'; |
| | | if (faceMatch >= 85) return 'warning'; |
| | | return 'danger'; |
| | | } |
| | | |
| | | // å¯¼åºææè®°å½ |
| | | function exportCaptures() { |
| | | if (filteredCaptures.value.length === 0) { |
| | | ElMessage.warning("没æå¯å¯¼åºçè®°å½"); |
| | | return; |
| | | } |
| | | ElMessage.success(`æ£å¨å¯¼åº ${filteredCaptures.value.length} æ¡ææè®°å½...`); |
| | | // å®é
ç¯å¢ä¸è¿éä¼è°ç¨å¯¼åºæ¥å£ |
| | | } |
| | | |
| | | // ä¸è½½å¿«ç
§ |
| | | function downloadSnapshot(capture) { |
| | | ElMessage.success(`æ£å¨ä¸è½½ ${capture.personId} çææå¾ç...`); |
| | | // å®é
ç¯å¢ä¸è¿éä¼ä¸è½½å¾çæä»¶ |
| | | } |
| | | |
| | | // æå°å¿«ç
§ |
| | | function printSnapshot() { |
| | | ElMessage.info("æ£å¨å夿å°..."); |
| | | // å®é
ç¯å¢ä¸è¿éä¼è°ç¨æå°åè½ |
| | | } |
| | | |
| | | // ä¿å夿³¨ |
| | | function saveNotes() { |
| | | ElMessage.success("夿³¨ä¿¡æ¯å·²ä¿å"); |
| | | snapshotVisible.value = false; |
| | | } |
| | | |
| | | // åè¦æ°æ® |
| | | const alarmEnabled = ref(true); |
| | | const alarmTab = ref("all"); |
| | | const alarms = ref([ |
| | | { |
| | | id: 1, |
| | | time: "09:25:15", |
| | | type: "intrusion", |
| | | typeText: "强é¯åè¦", |
| | | areaName: "å±é©ååº", |
| | | cameraName: "å±é©ååºå¤§é¨", |
| | | description: "æ£æµå°æªææäººå强è¡é¯å
¥ï¼æªå·å¡ç´æ¥å¼é¨", |
| | | handled: false |
| | | }, |
| | | { |
| | | id: 2, |
| | | time: "09:18:42", |
| | | type: "tailgating", |
| | | typeText: "å°¾éåè¦", |
| | | areaName: "䏿§å®¤", |
| | | cameraName: "䏿§å®¤å
¥å£", |
| | | description: "æ£æµå°å°¾éè¡ä¸ºï¼ä¸äººå·å¡å两人è¿å
¥", |
| | | handled: false |
| | | }, |
| | | { |
| | | id: 3, |
| | | time: "09:10:28", |
| | | type: "multiple", |
| | | typeText: "å¤äººéè¡", |
| | | areaName: "ç
¤åºå
¥å£", |
| | | cameraName: "ç
¤åºå
¥å£ä¸é¨", |
| | | description: "æ£æµå°3äººåæ¶éè¿å人é¨ç¦éé", |
| | | handled: true |
| | | }, |
| | | ]); |
| | | |
| | | const unreadAlarmCount = computed(() => alarms.value.filter(a => !a.handled).length); |
| | | |
| | | const displayAlarms = computed(() => { |
| | | if (alarmTab.value === "all") return alarms.value; |
| | | return alarms.value.filter(a => a.type === alarmTab.value); |
| | | }); |
| | | |
| | | const intrusionAlarms = computed(() => alarms.value.filter(a => a.type === "intrusion")); |
| | | const tailgatingAlarms = computed(() => alarms.value.filter(a => a.type === "tailgating")); |
| | | const multipleAlarms = computed(() => alarms.value.filter(a => a.type === "multiple")); |
| | | |
| | | function getAlarmType(type) { |
| | | const typeMap = { intrusion: "danger", tailgating: "warning", multiple: "warning" }; |
| | | return typeMap[type] || "info"; |
| | | } |
| | | |
| | | function getAlarmTagType(type) { |
| | | const typeMap = { intrusion: "danger", tailgating: "warning", multiple: "warning" }; |
| | | return typeMap[type] || "info"; |
| | | } |
| | | |
| | | function handleAlarm(alarm) { |
| | | alarm.handled = true; |
| | | ElMessage.success("åè¦å·²æ 记为已å¤ç"); |
| | | } |
| | | |
| | | // 宿¶ç»é¢ |
| | | const realtimeVisible = ref(false); |
| | | const currentCamera = ref({}); |
| | | |
| | | function viewRealtime(camera) { |
| | | currentCamera.value = camera; |
| | | realtimeVisible.value = true; |
| | | } |
| | | |
| | | function captureSnapshot() { |
| | | ElMessage.success("æææåï¼å·²ä¿åè³ææè®°å½"); |
| | | } |
| | | |
| | | // æ¥çææè®°å½è¯¦æ
|
| | | const snapshotVisible = ref(false); |
| | | const currentSnapshot = ref({}); |
| | | |
| | | function viewSnapshot(capture) { |
| | | currentSnapshot.value = capture; |
| | | snapshotVisible.value = true; |
| | | } |
| | | |
| | | function viewCaptures(camera) { |
| | | ElMessage.info(`æ¥ç ${camera.name} çå岿æè®°å½`); |
| | | } |
| | | |
| | | // 模æåè¦æ¨é |
| | | let alarmTimer = null; |
| | | const alarmTemplates = [ |
| | | { type: "intrusion", typeText: "强é¯åè¦", areas: ["å±é©ååº", "é
çµå®¤", "䏿§å®¤"] }, |
| | | { type: "tailgating", typeText: "å°¾éåè¦", areas: ["䏿§å®¤", "é
çµå®¤", "å±é©ååº"] }, |
| | | { type: "multiple", typeText: "å¤äººéè¡", areas: ["ç
¤åºå
¥å£", "æ´ç
¤è½¦é´"] }, |
| | | ]; |
| | | |
| | | function pushMockAlarm() { |
| | | if (!alarmEnabled.value) return; |
| | | |
| | | const template = alarmTemplates[Math.floor(Math.random() * alarmTemplates.length)]; |
| | | const area = template.areas[Math.floor(Math.random() * template.areas.length)]; |
| | | const camera = cameras.value.find(c => c.areaName === area); |
| | | |
| | | const now = new Date(); |
| | | const hh = String(now.getHours()).padStart(2, "0"); |
| | | const mm = String(now.getMinutes()).padStart(2, "0"); |
| | | const ss = String(now.getSeconds()).padStart(2, "0"); |
| | | |
| | | let description = ""; |
| | | if (template.type === "intrusion") { |
| | | description = "æ£æµå°æªææäººå强è¡é¯å
¥ï¼æªå·å¡ç´æ¥å¼é¨"; |
| | | } else if (template.type === "tailgating") { |
| | | description = "æ£æµå°å°¾éè¡ä¸ºï¼ä¸äººå·å¡å两人è¿å
¥"; |
| | | } else if (template.type === "multiple") { |
| | | const count = Math.floor(Math.random() * 3) + 2; |
| | | description = `æ£æµå°${count}äººåæ¶éè¿å人é¨ç¦éé`; |
| | | } |
| | | |
| | | alarms.value.unshift({ |
| | | id: Date.now(), |
| | | time: `${hh}:${mm}:${ss}`, |
| | | type: template.type, |
| | | typeText: template.typeText, |
| | | areaName: area, |
| | | cameraName: camera ? camera.name : area + "çæ§", |
| | | description: description, |
| | | handled: false |
| | | }); |
| | | |
| | | // éå¶åè¦å表é¿åº¦ |
| | | if (alarms.value.length > 50) { |
| | | alarms.value = alarms.value.slice(0, 50); |
| | | } |
| | | } |
| | | |
| | | onMounted(() => { |
| | | filterCameras(); |
| | | // æ¯é8ç§æ¨¡æä¸æ¬¡åè¦ |
| | | alarmTimer = setInterval(pushMockAlarm, 8000); |
| | | }); |
| | | |
| | | onBeforeUnmount(() => { |
| | | if (alarmTimer) { |
| | | clearInterval(alarmTimer); |
| | | } |
| | | }); |
| | | </script> |
| | | |
| | | <style scoped lang="scss"> |
| | | .section-card { |
| | | margin-bottom: 16px; |
| | | } |
| | | |
| | | .card-header { |
| | | display: flex; |
| | | align-items: center; |
| | | justify-content: space-between; |
| | | } |
| | | |
| | | .header-actions { |
| | | display: flex; |
| | | gap: 8px; |
| | | align-items: center; |
| | | } |
| | | |
| | | .alarm-item { |
| | | padding: 8px; |
| | | background: #f5f7fa; |
| | | border-radius: 4px; |
| | | |
| | | &.handled { |
| | | opacity: 0.6; |
| | | } |
| | | } |
| | | |
| | | .alarm-header { |
| | | display: flex; |
| | | align-items: center; |
| | | gap: 8px; |
| | | margin-bottom: 6px; |
| | | } |
| | | |
| | | .alarm-area { |
| | | font-weight: 600; |
| | | color: #303133; |
| | | } |
| | | |
| | | .alarm-content { |
| | | color: #606266; |
| | | font-size: 14px; |
| | | margin-bottom: 6px; |
| | | line-height: 1.5; |
| | | } |
| | | |
| | | .alarm-footer { |
| | | display: flex; |
| | | justify-content: space-between; |
| | | align-items: center; |
| | | font-size: 12px; |
| | | color: #909399; |
| | | } |
| | | |
| | | .alarm-camera { |
| | | flex: 1; |
| | | } |
| | | |
| | | .handled-text { |
| | | color: #67c23a; |
| | | font-size: 12px; |
| | | } |
| | | |
| | | .video-container { |
| | | width: 100%; |
| | | height: 450px; |
| | | background: #000; |
| | | display: flex; |
| | | align-items: center; |
| | | justify-content: center; |
| | | } |
| | | |
| | | .video-placeholder { |
| | | text-align: center; |
| | | color: #909399; |
| | | } |
| | | |
| | | .video-info { |
| | | margin-top: 20px; |
| | | |
| | | p { |
| | | margin: 8px 0; |
| | | font-size: 14px; |
| | | } |
| | | |
| | | .tip { |
| | | color: #c0c4cc; |
| | | font-size: 12px; |
| | | margin-top: 16px; |
| | | } |
| | | } |
| | | |
| | | .snapshot-placeholder { |
| | | margin-top: 20px; |
| | | height: 300px; |
| | | background: #f5f7fa; |
| | | display: flex; |
| | | flex-direction: column; |
| | | align-items: center; |
| | | justify-content: center; |
| | | border-radius: 4px; |
| | | color: #909399; |
| | | |
| | | p { |
| | | margin: 8px 0; |
| | | } |
| | | |
| | | .tip { |
| | | color: #c0c4cc; |
| | | font-size: 12px; |
| | | } |
| | | } |
| | | |
| | | :deep(.el-tabs--border-card) { |
| | | border: none; |
| | | box-shadow: none; |
| | | } |
| | | |
| | | :deep(.el-tabs__content) { |
| | | padding: 10px; |
| | | } |
| | | |
| | | // ææè®°å½ç»è®¡ |
| | | .capture-stats { |
| | | margin-bottom: 16px; |
| | | padding: 16px; |
| | | background: #f5f7fa; |
| | | border-radius: 4px; |
| | | } |
| | | |
| | | .stat-item { |
| | | text-align: center; |
| | | padding: 8px; |
| | | background: #fff; |
| | | border-radius: 4px; |
| | | } |
| | | |
| | | .stat-label { |
| | | font-size: 13px; |
| | | color: #909399; |
| | | margin-bottom: 8px; |
| | | } |
| | | |
| | | .stat-value { |
| | | font-size: 24px; |
| | | font-weight: bold; |
| | | color: #303133; |
| | | } |
| | | |
| | | // åé¡µå®¹å¨ |
| | | .pagination-container { |
| | | margin-top: 16px; |
| | | display: flex; |
| | | justify-content: flex-end; |
| | | } |
| | | |
| | | // å¿«ç
§é¢è§ |
| | | .snapshot-preview { |
| | | width: 100%; |
| | | } |
| | | |
| | | .snapshot-placeholder { |
| | | width: 100%; |
| | | height: 420px; |
| | | background: #f5f7fa; |
| | | display: flex; |
| | | flex-direction: column; |
| | | align-items: center; |
| | | justify-content: center; |
| | | border-radius: 4px; |
| | | border: 1px dashed #dcdfe6; |
| | | color: #909399; |
| | | |
| | | p { |
| | | margin: 8px 0; |
| | | font-size: 14px; |
| | | } |
| | | |
| | | .tip { |
| | | color: #c0c4cc; |
| | | font-size: 12px; |
| | | } |
| | | } |
| | | |
| | | .snapshot-tools { |
| | | margin-top: 20px; |
| | | } |
| | | |
| | | .snapshot-notes { |
| | | margin-top: 20px; |
| | | |
| | | :deep(.el-divider__text) { |
| | | font-weight: 600; |
| | | color: #606266; |
| | | } |
| | | } |
| | | |
| | | .dialog-footer-custom { |
| | | display: flex; |
| | | justify-content: space-between; |
| | | align-items: center; |
| | | |
| | | > div { |
| | | display: flex; |
| | | gap: 8px; |
| | | } |
| | | } |
| | | |
| | | // æè¿°åè¡¨æ ·å¼ä¼å |
| | | :deep(.el-descriptions__label) { |
| | | width: 100px; |
| | | } |
| | | |
| | | :deep(.el-descriptions__content) { |
| | | display: flex; |
| | | align-items: center; |
| | | gap: 4px; |
| | | } |
| | | </style> |
| | | |