huminmin
2026-06-01 a563ea879ef5fb6897e76d2df661e465dce2ab9b
src/views/monitorManagement/areaControl/index.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,264 @@
<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>