<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="selectedPlant" placeholder="选择厂区" size="small" style="width: 160px" @change="filterZones">
|
<el-option v-for="plant in plants" :key="plant.id" :label="plant.name" :value="plant.id" />
|
</el-select>
|
<el-switch v-model="onlyCritical" inline-prompt :active-text="'仅关键区'" :inactive-text="'全部'" @change="filterZones" />
|
</div>
|
</div>
|
</template>
|
<el-table :data="filteredZones" border style="width: 100%" height="320">
|
<el-table-column type="index" width="60" label="序号" align="center" />
|
<el-table-column prop="name" label="区域名称" min-width="160" show-overflow-tooltip />
|
<el-table-column prop="zoneType" label="类型" width="120" />
|
<el-table-column label="双门联动" width="120" align="center">
|
<template #default="{ row }">
|
<el-tag v-if="row.dualAccess" type="success">已启用</el-tag>
|
<el-tag v-else type="info">未启用</el-tag>
|
</template>
|
</el-table-column>
|
<el-table-column label="在线人数" width="100" align="center">
|
<template #default="{ row }">{{ row.currentPersons }}</template>
|
</el-table-column>
|
<el-table-column label="安全状态" width="140" align="center">
|
<template #default="{ row }">
|
<el-tag :type="row.status === '正常' ? 'success' : row.status === '预警' ? 'warning' : 'danger'">
|
{{ row.status }}
|
</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="toggleDual(row)">
|
{{ row.dualAccess ? '停用双门' : '启用双门' }}
|
</el-button>
|
<el-button link type="success" size="small" @click="openAccessSim(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-input v-model="accessSim.personId" placeholder="人员工号" size="small" style="width: 140px" />
|
<el-select v-model="accessSim.targetZoneId" placeholder="选择目标区域" size="small" style="width: 180px">
|
<el-option v-for="z in zones" :key="z.id" :label="z.name" :value="z.id" />
|
</el-select>
|
<el-button type="primary" size="small" @click="simulateAccess">检验准入</el-button>
|
</div>
|
</div>
|
</template>
|
<el-descriptions :column="3" border size="small" v-if="accessResult">
|
<el-descriptions-item label="工号">{{ accessResult.person.id }}({{ accessResult.person.dept }})</el-descriptions-item>
|
<el-descriptions-item label="培训状态">
|
<el-tag :type="accessResult.person.training.valid ? 'success' : 'danger'">
|
{{ accessResult.person.training.valid ? '有效' : '失效/未完成' }}
|
</el-tag>
|
</el-descriptions-item>
|
<el-descriptions-item label="目标区域">{{ accessResult.zone.name }}</el-descriptions-item>
|
<el-descriptions-item label="最近培训">{{ accessResult.person.training.lastDate }}</el-descriptions-item>
|
<el-descriptions-item label="适岗证有效期">{{ accessResult.person.training.expireDate }}</el-descriptions-item>
|
<el-descriptions-item label="准入结果">
|
<el-tag :type="accessResult.allowed ? 'success' : 'danger'">{{ accessResult.allowed ? '允许进入' : '禁止进入' }}</el-tag>
|
</el-descriptions-item>
|
</el-descriptions>
|
<el-empty v-else description="请输入人员与区域进行检验" />
|
</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-select v-model="stayThreshold" size="small" style="width: 140px">
|
<el-option :value="10" label="阈值 10 分钟" />
|
<el-option :value="20" label="阈值 20 分钟" />
|
<el-option :value="30" label="阈值 30 分钟" />
|
</el-select>
|
<el-switch v-model="alarmOn" inline-prompt :active-text="'告警开'" :inactive-text="'告警关'" />
|
</div>
|
</div>
|
</template>
|
<el-timeline style="max-height: 520px; overflow: auto">
|
<el-timeline-item v-for="(item, idx) in alarms" :key="idx" :type="item.level" :timestamp="item.time">
|
<div class="alarm-item">
|
<div class="title">
|
{{ item.personId }} · {{ item.zoneName }} · 滞留 {{ item.stayMins }} 分钟
|
</div>
|
<div class="desc">设备:{{ item.deviceId }}(信号强度 {{ item.rssi }} dBm)</div>
|
</div>
|
</el-timeline-item>
|
</el-timeline>
|
</el-card>
|
</el-col>
|
</el-row>
|
</div>
|
<el-dialog v-model="doorSimVisible" title="门禁开门模拟" width="420px">
|
<el-form :model="doorSim" label-width="90px">
|
<el-form-item label="区域">
|
<el-input v-model="doorSim.zoneName" disabled />
|
</el-form-item>
|
<el-form-item label="门禁1">
|
<el-switch v-model="doorSim.door1" />
|
</el-form-item>
|
<el-form-item label="门禁2">
|
<el-switch v-model="doorSim.door2" />
|
</el-form-item>
|
<el-alert type="info" show-icon :closable="false" title="双门均为开启方可通行" />
|
</el-form>
|
<template #footer>
|
<el-button @click="doorSimVisible = false">关闭</el-button>
|
<el-button type="primary" :disabled="!(doorSim.door1 && doorSim.door2)" @click="confirmPass">通行</el-button>
|
</template>
|
</el-dialog>
|
</template>
|
|
<script setup>
|
import { ref, reactive, computed, onMounted } from "vue";
|
|
// 厂区与区域(煤炭行业语义、尽量贴近真实)
|
const plants = ref([
|
{ id: "P01", name: "一号选煤厂" },
|
{ id: "P02", name: "二号洗煤分厂" },
|
]);
|
const zones = ref([
|
{ id: "Z01", plantId: "P01", name: "中控室", zoneType: "控制室", dualAccess: true, currentPersons: 4, status: "正常" },
|
{ id: "Z02", plantId: "P01", name: "煤场A区", zoneType: "堆存区", dualAccess: true, currentPersons: 12, status: "预警" },
|
{ id: "Z03", plantId: "P01", name: "危险品库", zoneType: "危化品", dualAccess: true, currentPersons: 1, status: "正常" },
|
{ id: "Z04", plantId: "P01", name: "高压配电室", zoneType: "电气间", dualAccess: true, currentPersons: 2, status: "正常" },
|
{ id: "Z05", plantId: "P02", name: "皮带廊北段", zoneType: "输送廊道", dualAccess: false, currentPersons: 5, status: "正常" },
|
{ id: "Z06", plantId: "P02", name: "筛分车间", zoneType: "作业区", dualAccess: false, currentPersons: 9, status: "预警" },
|
]);
|
|
const selectedPlant = ref(plants.value[0].id);
|
const onlyCritical = ref(true);
|
const filteredZones = ref([]);
|
|
function filterZones() {
|
const data = zones.value.filter((z) => z.plantId === selectedPlant.value);
|
filteredZones.value = onlyCritical.value ? data.filter((z) => z.dualAccess) : data;
|
}
|
|
function toggleDual(row) {
|
row.dualAccess = !row.dualAccess;
|
filterZones();
|
}
|
|
// 门禁开门模拟
|
const doorSimVisible = ref(false);
|
const doorSim = reactive({ zoneId: "", zoneName: "", door1: false, door2: false });
|
function openAccessSim(row) {
|
doorSim.zoneId = row.id;
|
doorSim.zoneName = row.name;
|
doorSim.door1 = false;
|
doorSim.door2 = false;
|
doorSimVisible.value = true;
|
}
|
function confirmPass() {
|
doorSimVisible.value = false;
|
}
|
|
// 培训联动模拟
|
const persons = ref([
|
{ id: "EMP1001", dept: "生产一队", training: { valid: true, lastDate: "2025-09-12", expireDate: "2026-09-12" } },
|
{ id: "EMP1018", dept: "机电班", training: { valid: false, lastDate: "2024-07-03", expireDate: "2025-07-03" } },
|
{ id: "EMP1022", dept: "安监科", training: { valid: true, lastDate: "2025-08-01", expireDate: "2026-08-01" } },
|
]);
|
const accessSim = reactive({ personId: "", targetZoneId: "" });
|
const accessResult = ref(null);
|
|
function simulateAccess() {
|
const person = persons.value.find((p) => p.id === accessSim.personId);
|
const zone = zones.value.find((z) => z.id === accessSim.targetZoneId);
|
if (!person || !zone) {
|
accessResult.value = null;
|
return;
|
}
|
const allowed = person.training.valid && (zone.zoneType !== "危化品" || person.dept === "安监科");
|
accessResult.value = { allowed, person, zone };
|
}
|
|
// 佩戴设备滞留告警(假数据定时推送)
|
const stayThreshold = ref(20);
|
const alarmOn = ref(true);
|
const alarms = ref([
|
{ time: "09:35", level: "warning", personId: "EMP1001", zoneName: "煤场A区", stayMins: 18, deviceId: "TAG-7A12", rssi: -67 },
|
]);
|
|
let timer = null;
|
function pushMockAlarm() {
|
if (!alarmOn.value) return;
|
const candidates = [
|
{ personId: "EMP1018", zoneName: "筛分车间", base: 12 },
|
{ personId: "EMP1022", zoneName: "高压配电室", base: 9 },
|
{ personId: "EMP1001", zoneName: "煤场A区", base: 16 },
|
];
|
const pick = candidates[Math.floor(Math.random() * candidates.length)];
|
const stay = pick.base + Math.floor(Math.random() * 10);
|
if (stay >= stayThreshold.value) {
|
const now = new Date();
|
const hh = String(now.getHours()).padStart(2, "0");
|
const mm = String(now.getMinutes()).padStart(2, "0");
|
alarms.value.unshift({
|
time: `${hh}:${mm}`,
|
level: stay >= stayThreshold.value + 10 ? "danger" : "warning",
|
personId: pick.personId,
|
zoneName: pick.zoneName,
|
stayMins: stay,
|
deviceId: `TAG-${Math.random().toString(16).slice(2, 6).toUpperCase()}`,
|
rssi: -60 - Math.floor(Math.random() * 15),
|
});
|
if (alarms.value.length > 30) alarms.value.pop();
|
}
|
}
|
|
onMounted(() => {
|
filterZones();
|
timer = setInterval(pushMockAlarm, 4500);
|
});
|
|
// 离开时清理
|
if (import.meta.hot) {
|
import.meta.hot.dispose(() => {
|
if (timer) clearInterval(timer);
|
});
|
}
|
</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 .title {
|
font-weight: 600;
|
margin-bottom: 4px;
|
}
|
.alarm-item .desc {
|
color: #666;
|
font-size: 12px;
|
}
|
</style>
|