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