Files
akmon/pages/mall/nfc/security/index.uvue
2026-01-20 08:04:15 +08:00

1101 lines
25 KiB
Plaintext

<template>
<view class="security-dashboard">
<!-- 保安信息头部 -->
<view class="security-header">
<view class="guard-info">
<image class="guard-avatar" :src="guardInfo.avatar || '/static/default-avatar.png'" />
<view class="info">
<text class="name">{{ guardInfo.name }}</text>
<text class="position">{{ guardInfo.position }}</text>
<text class="shift">{{ guardInfo.currentShift }}</text>
</view>
</view>
<view class="status-indicator" :class="guardInfo.status">
<text class="status-text">{{ getStatusText(guardInfo.status) }}</text>
</view>
</view>
<!-- 实时监控面板 -->
<view class="monitoring-panel">
<view class="section-title">实时监控</view>
<view class="monitoring-grid">
<view class="monitor-card">
<view class="card-header">
<text class="card-title">门禁状态</text>
<view class="status-dot active"></view>
</view>
<text class="card-value">{{ monitoringData.accessPoints.active }}/{{ monitoringData.accessPoints.total }}</text>
<text class="card-label">在线门禁点</text>
</view>
<view class="monitor-card">
<view class="card-header">
<text class="card-title">今日通行</text>
<view class="status-dot normal"></view>
</view>
<text class="card-value">{{ monitoringData.todayAccess }}</text>
<text class="card-label">人次</text>
</view>
<view class="monitor-card">
<view class="card-header">
<text class="card-title">当前在校</text>
<view class="status-dot normal"></view>
</view>
<text class="card-value">{{ monitoringData.currentInCampus }}</text>
<text class="card-label">人员</text>
</view>
<view class="monitor-card alert" v-if="monitoringData.alerts > 0">
<view class="card-header">
<text class="card-title">异常警报</text>
<view class="status-dot warning"></view>
</view>
<text class="card-value">{{ monitoringData.alerts }}</text>
<text class="card-label">待处理</text>
</view>
</view>
</view>
<!-- 紧急功能区 -->
<view class="emergency-functions">
<view class="section-title">紧急功能</view>
<view class="emergency-grid">
<view class="emergency-btn emergency-lock" @click="handleEmergencyLock">
<image class="emergency-icon" src="/static/icons/emergency-lock.png" />
<text class="emergency-text">紧急封锁</text>
</view>
<view class="emergency-btn emergency-unlock" @click="handleEmergencyUnlock">
<image class="emergency-icon" src="/static/icons/emergency-unlock.png" />
<text class="emergency-text">紧急开启</text>
</view>
<view class="emergency-btn fire-alarm" @click="handleFireAlarm">
<image class="emergency-icon" src="/static/icons/fire-alarm.png" />
<text class="emergency-text">火警响应</text>
</view>
<view class="emergency-btn emergency-call" @click="handleEmergencyCall">
<image class="emergency-icon" src="/static/icons/emergency-call.png" />
<text class="emergency-text">紧急呼叫</text>
</view>
</view>
</view>
<!-- 访客管理 -->
<view class="visitor-management">
<view class="section-header">
<text class="section-title">访客管理</text>
<button class="add-visitor-btn" @click="addNewVisitor">
+ 登记访客
</button>
</view>
<view class="visitor-stats">
<view class="stat-item">
<text class="stat-value">{{ visitorData.todayTotal }}</text>
<text class="stat-label">今日访客</text>
</view>
<view class="stat-item">
<text class="stat-value">{{ visitorData.currentInside }}</text>
<text class="stat-label">在校访客</text>
</view>
<view class="stat-item">
<text class="stat-value">{{ visitorData.pendingApproval }}</text>
<text class="stat-label">待审批</text>
</view>
</view>
<view class="visitor-list">
<view class="visitor-item" v-for="visitor in recentVisitors" :key="visitor.id">
<image class="visitor-photo" :src="visitor.photo || '/static/default-avatar.png'" />
<view class="visitor-info">
<text class="visitor-name">{{ visitor.name }}</text>
<text class="visitor-purpose">{{ visitor.purpose }}</text>
<text class="visitor-time">{{ formatTime(visitor.enterTime) }}</text>
</view>
<view class="visitor-status" :class="visitor.status">
{{ getVisitorStatusText(visitor.status) }}
</view>
<view class="visitor-actions">
<button class="action-btn checkout"
v-if="visitor.status === 'inside'"
@click="checkoutVisitor(visitor)">
离校
</button>
</view>
</view>
</view>
</view>
<!-- 实时日志 -->
<view class="activity-log">
<view class="section-header">
<text class="section-title">实时日志</text>
<view class="log-filter">
<text class="filter-item"
:class="{ active: logFilter === 'all' }"
@click="setLogFilter('all')">全部</text>
<text class="filter-item"
:class="{ active: logFilter === 'access' }"
@click="setLogFilter('access')">门禁</text>
<text class="filter-item"
:class="{ active: logFilter === 'alert' }"
@click="setLogFilter('alert')">警报</text>
</view>
</view>
<scroll-view class="log-scroll" scroll-y="true" :scroll-top="scrollTop">
<view class="log-item"
v-for="log in filteredLogs"
:key="log.id"
:class="log.type">
<view class="log-time">{{ formatLogTime(log.time) }}</view>
<view class="log-content">
<view class="log-location">{{ log.location }}</view>
<text class="log-description">{{ log.description }}</text>
<view class="log-details" v-if="log.details">
<text class="detail-text">{{ log.details }}</text>
</view>
</view>
<view class="log-status" :class="log.status">
<view class="status-indicator"></view>
</view>
</view>
</scroll-view>
</view>
<!-- 快速功能菜单 -->
<view class="quick-functions">
<view class="function-row">
<view class="function-item" @click="goToAccessControl">
<image class="function-icon" src="/static/icons/access-control.png" />
<text class="function-text">门禁控制</text>
</view>
<view class="function-item" @click="goToVideoMonitoring">
<image class="function-icon" src="/static/icons/video.png" />
<text class="function-text">视频监控</text>
</view>
<view class="function-item" @click="goToPatrolRoutes">
<image class="function-icon" src="/static/icons/patrol.png" />
<text class="function-text">巡逻路线</text>
</view>
<view class="function-item" @click="goToIncidentReport">
<image class="function-icon" src="/static/icons/incident.png" />
<text class="function-text">事件上报</text>
</view>
</view>
</view>
<!-- 班次信息 -->
<view class="shift-info">
<view class="section-title">班次信息</view>
<view class="shift-details">
<view class="shift-item">
<text class="shift-label">当前班次</text>
<text class="shift-value">{{ shiftInfo.current }}</text>
</view>
<view class="shift-item">
<text class="shift-label">上班时间</text>
<text class="shift-value">{{ shiftInfo.startTime }}</text>
</view>
<view class="shift-item">
<text class="shift-label">下班时间</text>
<text class="shift-value">{{ shiftInfo.endTime }}</text>
</view>
<view class="shift-item">
<text class="shift-label">交班人员</text>
<text class="shift-value">{{ shiftInfo.nextGuard }}</text>
</view>
</view>
<button class="shift-handover-btn" @click="initiateShiftHandover">
开始交班
</button>
</view>
<!-- 紧急联系人 -->
<view class="emergency-contacts">
<view class="section-title">紧急联系</view>
<view class="contact-list">
<view class="contact-item"
v-for="contact in emergencyContacts"
:key="contact.id"
@click="callContact(contact)">
<view class="contact-icon" :class="contact.type">
<image :src="getContactIcon(contact.type)" />
</view>
<view class="contact-info">
<text class="contact-name">{{ contact.name }}</text>
<text class="contact-number">{{ contact.number }}</text>
</view>
<view class="call-btn">
<image src="/static/icons/phone.png" />
</view>
</view>
</view>
</view>
</view>
</template>
<script>
export default {
data() {
return {
guardInfo: {
name: '王保安',
position: '校园安保',
currentShift: '夜班 18:00-06:00',
status: 'on-duty',
avatar: ''
},
monitoringData: {
accessPoints: {
active: 28,
total: 30
},
todayAccess: 1247,
currentInCampus: 892,
alerts: 2
},
visitorData: {
todayTotal: 45,
currentInside: 12,
pendingApproval: 3
},
recentVisitors: [
{
id: 1,
name: '张先生',
purpose: '学术访问',
enterTime: new Date(Date.now() - 7200000),
status: 'inside',
photo: ''
},
{
id: 2,
name: '李女士',
purpose: '家长会',
enterTime: new Date(Date.now() - 3600000),
status: 'inside',
photo: ''
}
],
activityLogs: [
{
id: 1,
time: new Date(),
location: '东门',
description: '学生刷卡进入',
details: '学号: 2021001, 姓名: 张小明',
type: 'access',
status: 'success'
},
{
id: 2,
time: new Date(Date.now() - 300000),
location: '图书馆',
description: '异常开门尝试',
details: '无效卡片尝试开门',
type: 'alert',
status: 'warning'
}
],
logFilter: 'all',
scrollTop: 0,
shiftInfo: {
current: '夜班',
startTime: '18:00',
endTime: '06:00',
nextGuard: '李保安'
},
emergencyContacts: [
{
id: 1,
name: '保卫处值班室',
number: '110',
type: 'security'
},
{
id: 2,
name: '校医院急诊',
number: '120',
type: 'medical'
},
{
id: 3,
name: '消防中心',
number: '119',
type: 'fire'
}
]
}
},
computed: {
filteredLogs() {
if (this.logFilter === 'all') {
return this.activityLogs
}
return this.activityLogs.filter(log => log.type === this.logFilter)
}
},
onLoad() {
this.loadSecurityData()
this.startRealTimeUpdates()
},
onUnload() {
this.stopRealTimeUpdates()
},
methods: {
loadSecurityData() {
this.loadMonitoringData()
this.loadVisitorData()
this.loadActivityLogs()
},
loadMonitoringData() {
uni.request({
url: '/api/v1/security/monitoring',
success: (res) => {
this.monitoringData = res.data
}
})
},
loadVisitorData() {
uni.request({
url: '/api/v1/security/visitors',
success: (res) => {
this.visitorData = res.data.stats
this.recentVisitors = res.data.recent
}
})
},
loadActivityLogs() {
uni.request({
url: '/api/v1/security/activity-logs',
success: (res) => {
this.activityLogs = res.data
}
})
},
startRealTimeUpdates() {
this.updateInterval = setInterval(() => {
this.loadMonitoringData()
this.loadActivityLogs()
}, 5000) // 每5秒更新一次
},
stopRealTimeUpdates() {
if (this.updateInterval) {
clearInterval(this.updateInterval)
}
},
getStatusText(status) {
const statusMap = {
'on-duty': '值班中',
'off-duty': '下班',
'break': '休息中'
}
return statusMap[status] || '未知'
},
// 紧急功能处理
handleEmergencyLock() {
uni.showModal({
title: '紧急封锁',
content: '确认要进行全校紧急封锁吗?此操作将关闭所有门禁。',
confirmText: '确认封锁',
cancelText: '取消',
success: (res) => {
if (res.confirm) {
this.executeEmergencyLock()
}
}
})
},
executeEmergencyLock() {
uni.showLoading({ title: '执行紧急封锁...' })
uni.request({
url: '/api/v1/security/emergency-lock',
method: 'POST',
success: () => {
uni.hideLoading()
uni.showToast({
title: '紧急封锁已激活',
icon: 'success'
})
this.loadMonitoringData()
},
fail: () => {
uni.hideLoading()
uni.showToast({
title: '操作失败',
icon: 'none'
})
}
})
},
handleEmergencyUnlock() {
uni.showModal({
title: '紧急开启',
content: '确认要进行紧急开启所有门禁吗?',
confirmText: '确认开启',
cancelText: '取消',
success: (res) => {
if (res.confirm) {
this.executeEmergencyUnlock()
}
}
})
},
executeEmergencyUnlock() {
uni.showLoading({ title: '执行紧急开启...' })
uni.request({
url: '/api/v1/security/emergency-unlock',
method: 'POST',
success: () => {
uni.hideLoading()
uni.showToast({
title: '紧急开启已激活',
icon: 'success'
})
this.loadMonitoringData()
}
})
},
handleFireAlarm() {
uni.navigateTo({
url: '/pages/mall/nfc/security/fire-alarm'
})
},
handleEmergencyCall() {
uni.navigateTo({
url: '/pages/mall/nfc/security/emergency-call'
})
},
// 访客管理
addNewVisitor() {
uni.navigateTo({
url: '/pages/mall/nfc/security/visitor-registration'
})
},
checkoutVisitor(visitor) {
uni.showModal({
title: '访客离校',
content: `确认${visitor.name}离校?`,
success: (res) => {
if (res.confirm) {
uni.request({
url: `/api/v1/security/visitors/${visitor.id}/checkout`,
method: 'POST',
success: () => {
uni.showToast({
title: '离校登记成功',
icon: 'success'
})
this.loadVisitorData()
}
})
}
}
})
},
getVisitorStatusText(status) {
const statusMap = {
'inside': '在校',
'left': '已离校',
'pending': '待审批'
}
return statusMap[status] || '未知'
},
// 日志过滤
setLogFilter(filter) {
this.logFilter = filter
},
// 快速功能导航
goToAccessControl() {
uni.navigateTo({ url: '/pages/mall/nfc/security/access-control' })
},
goToVideoMonitoring() {
uni.navigateTo({ url: '/pages/mall/nfc/security/video-monitoring' })
},
goToPatrolRoutes() {
uni.navigateTo({ url: '/pages/mall/nfc/security/patrol-routes' })
},
goToIncidentReport() {
uni.navigateTo({ url: '/pages/mall/nfc/security/incident-report' })
},
// 班次管理
initiateShiftHandover() {
uni.navigateTo({
url: '/pages/mall/nfc/security/shift-handover'
})
},
// 紧急联系
callContact(contact) {
uni.makePhoneCall({
phoneNumber: contact.number,
success: () => {
// 记录通话日志
uni.request({
url: '/api/v1/security/call-log',
method: 'POST',
data: {
contactId: contact.id,
time: new Date()
}
})
}
})
},
getContactIcon(type) {
const icons = {
security: '/static/icons/security-contact.png',
medical: '/static/icons/medical-contact.png',
fire: '/static/icons/fire-contact.png'
}
return icons[type] || icons.security
},
// 时间格式化
formatTime(time) {
return time.toLocaleTimeString('zh-CN', {
hour: '2-digit',
minute: '2-digit'
})
},
formatLogTime(time) {
return time.toLocaleTimeString('zh-CN', {
hour: '2-digit',
minute: '2-digit',
second: '2-digit'
})
}
}
}
</script>
<style>
.security-dashboard {
padding: 20rpx;
background-color: #f5f5f5;
min-height: 100vh;
}
.security-header {
background: linear-gradient(135deg, #dc3545 0%, #c82333 100%);
border-radius: 16rpx;
padding: 30rpx;
margin-bottom: 30rpx;
display: flex;
justify-content: space-between;
align-items: center;
}
.guard-info {
display: flex;
align-items: center;
}
.guard-avatar {
width: 80rpx;
height: 80rpx;
border-radius: 40rpx;
margin-right: 20rpx;
}
.info {
display: flex;
flex-direction: column;
}
.name {
color: white;
font-size: 32rpx;
font-weight: bold;
margin-bottom: 8rpx;
}
.position, .shift {
color: rgba(255, 255, 255, 0.8);
font-size: 24rpx;
margin-bottom: 4rpx;
}
.status-indicator {
background: rgba(255, 255, 255, 0.2);
border-radius: 12rpx;
padding: 16rpx;
}
.status-indicator.on-duty {
background: rgba(40, 167, 69, 0.2);
}
.status-text {
color: white;
font-size: 24rpx;
font-weight: bold;
}
.monitoring-panel, .emergency-functions, .visitor-management,
.activity-log, .quick-functions, .shift-info, .emergency-contacts {
background: white;
border-radius: 16rpx;
padding: 30rpx;
margin-bottom: 30rpx;
}
.section-title {
font-size: 32rpx;
font-weight: bold;
color: #333;
margin-bottom: 20rpx;
}
.section-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 20rpx;
}
.monitoring-grid {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 20rpx;
}
.monitor-card {
padding: 30rpx;
border: 1rpx solid #eee;
border-radius: 12rpx;
text-align: center;
}
.monitor-card.alert {
border-color: #dc3545;
background: #fff5f5;
}
.card-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 20rpx;
}
.card-title {
font-size: 24rpx;
color: #666;
}
.status-dot {
width: 16rpx;
height: 16rpx;
border-radius: 8rpx;
}
.status-dot.active {
background: #28a745;
}
.status-dot.normal {
background: #17a2b8;
}
.status-dot.warning {
background: #ffc107;
}
.card-value {
font-size: 48rpx;
font-weight: bold;
color: #dc3545;
margin-bottom: 8rpx;
display: block;
}
.card-label {
font-size: 24rpx;
color: #666;
}
.emergency-grid {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 20rpx;
}
.emergency-btn {
display: flex;
flex-direction: column;
align-items: center;
padding: 40rpx 20rpx;
border-radius: 12rpx;
border: 2rpx solid;
transition: all 0.3s ease;
}
.emergency-lock {
border-color: #dc3545;
background: #fff5f5;
}
.emergency-unlock {
border-color: #28a745;
background: #f8fff8;
}
.fire-alarm {
border-color: #fd7e14;
background: #fff8f0;
}
.emergency-call {
border-color: #6f42c1;
background: #f8f7ff;
}
.emergency-icon {
width: 64rpx;
height: 64rpx;
margin-bottom: 16rpx;
}
.emergency-text {
font-size: 28rpx;
font-weight: bold;
text-align: center;
}
.add-visitor-btn {
background: #dc3545;
color: white;
border: none;
border-radius: 8rpx;
padding: 12rpx 20rpx;
font-size: 24rpx;
}
.visitor-stats {
display: flex;
justify-content: space-around;
margin-bottom: 30rpx;
padding: 20rpx;
background: #f8f9fa;
border-radius: 12rpx;
}
.stat-item {
text-align: center;
}
.stat-value {
font-size: 36rpx;
font-weight: bold;
color: #dc3545;
margin-bottom: 8rpx;
display: block;
}
.stat-label {
font-size: 24rpx;
color: #666;
}
.visitor-list {
display: flex;
flex-direction: column;
gap: 16rpx;
}
.visitor-item {
display: flex;
align-items: center;
padding: 20rpx;
border: 1rpx solid #eee;
border-radius: 12rpx;
}
.visitor-photo {
width: 60rpx;
height: 60rpx;
border-radius: 30rpx;
margin-right: 20rpx;
}
.visitor-info {
flex: 1;
display: flex;
flex-direction: column;
}
.visitor-name {
font-size: 28rpx;
color: #333;
font-weight: bold;
margin-bottom: 8rpx;
}
.visitor-purpose, .visitor-time {
font-size: 24rpx;
color: #666;
margin-bottom: 4rpx;
}
.visitor-status {
padding: 8rpx 16rpx;
border-radius: 12rpx;
font-size: 20rpx;
margin-right: 20rpx;
}
.visitor-status.inside {
background: #d4edda;
color: #155724;
}
.visitor-status.left {
background: #d1ecf1;
color: #0c5460;
}
.action-btn {
background: #dc3545;
color: white;
border: none;
border-radius: 8rpx;
padding: 12rpx;
font-size: 20rpx;
}
.log-filter {
display: flex;
gap: 20rpx;
}
.filter-item {
font-size: 24rpx;
color: #666;
padding: 8rpx 16rpx;
border-radius: 8rpx;
cursor: pointer;
}
.filter-item.active {
background: #dc3545;
color: white;
}
.log-scroll {
height: 600rpx;
}
.log-item {
display: flex;
align-items: flex-start;
padding: 20rpx;
margin-bottom: 16rpx;
border-radius: 12rpx;
border-left: 6rpx solid;
}
.log-item.access {
border-left-color: #28a745;
background: #f8fff8;
}
.log-item.alert {
border-left-color: #dc3545;
background: #fff5f5;
}
.log-time {
font-size: 20rpx;
color: #666;
min-width: 120rpx;
margin-right: 20rpx;
}
.log-content {
flex: 1;
}
.log-location {
font-size: 24rpx;
color: #333;
font-weight: bold;
margin-bottom: 8rpx;
}
.log-description {
font-size: 24rpx;
color: #666;
margin-bottom: 8rpx;
}
.log-details {
font-size: 20rpx;
color: #999;
}
.log-status {
width: 20rpx;
height: 20rpx;
border-radius: 10rpx;
margin-left: 20rpx;
}
.log-status.success .status-indicator {
background: #28a745;
}
.log-status.warning .status-indicator {
background: #ffc107;
}
.function-row {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 20rpx;
}
.function-item {
display: flex;
flex-direction: column;
align-items: center;
padding: 30rpx 20rpx;
border: 1rpx solid #eee;
border-radius: 12rpx;
}
.function-icon {
width: 48rpx;
height: 48rpx;
margin-bottom: 16rpx;
}
.function-text {
font-size: 24rpx;
color: #333;
text-align: center;
}
.shift-details {
display: flex;
flex-direction: column;
gap: 20rpx;
margin-bottom: 30rpx;
}
.shift-item {
display: flex;
justify-content: space-between;
align-items: center;
padding: 16rpx 0;
border-bottom: 1rpx solid #f0f0f0;
}
.shift-item:last-child {
border-bottom: none;
}
.shift-label {
font-size: 24rpx;
color: #666;
}
.shift-value {
font-size: 24rpx;
color: #333;
font-weight: bold;
}
.shift-handover-btn {
background: #dc3545;
color: white;
border: none;
border-radius: 12rpx;
padding: 20rpx;
font-size: 28rpx;
width: 100%;
}
.contact-list {
display: flex;
flex-direction: column;
gap: 16rpx;
}
.contact-item {
display: flex;
align-items: center;
padding: 20rpx;
border: 1rpx solid #eee;
border-radius: 12rpx;
}
.contact-icon {
width: 48rpx;
height: 48rpx;
border-radius: 24rpx;
margin-right: 20rpx;
display: flex;
align-items: center;
justify-content: center;
}
.contact-icon.security {
background: #fff5f5;
}
.contact-icon.medical {
background: #f0f8ff;
}
.contact-icon.fire {
background: #fff8f0;
}
.contact-icon image {
width: 24rpx;
height: 24rpx;
}
.contact-info {
flex: 1;
display: flex;
flex-direction: column;
}
.contact-name {
font-size: 28rpx;
color: #333;
font-weight: bold;
margin-bottom: 8rpx;
}
.contact-number {
font-size: 24rpx;
color: #666;
}
.call-btn {
width: 48rpx;
height: 48rpx;
background: #dc3545;
border-radius: 24rpx;
display: flex;
align-items: center;
justify-content: center;
}
.call-btn image {
width: 24rpx;
height: 24rpx;
}
</style>