1101 lines
25 KiB
Plaintext
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>
|