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