| ¶Ô±ÈÐÂÎļþ |
| | |
| | | <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> |
| | | |
| | | |