| ¶Ô±ÈÐÂÎļþ | 
 |  |  | 
 |  |  | <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> | 
 |  |  |  |