<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> 
 |