Files
akmon/pages/test/ap_monitor.uvue
2026-01-20 08:04:15 +08:00

316 lines
7.3 KiB
Plaintext

<template>
<view class="page-container">
<view class="nav-tabs">
<button class="nav-tab active">智能网关</button>
<button class="nav-tab" @click="switchPage">设备监测</button>
</view>
<view class="header">
<text class="title">智能网关状态监测</text>
<text class="subtitle">总数: {{ nodes.length }} | 在线: {{ onlineCount }}</text>
</view>
<scroll-view class="grid-container" scroll-y="true">
<view class="grid-layout">
<view v-for="node in nodes" :key="node.id" class="card" :class="isOnline(node) ? 'card-online' : 'card-offline'">
<view class="card-header">
<text class="card-title">{{ node.name }}</text>
<view class="status-badge" :class="isOnline(node) ? 'badge-online' : 'badge-offline'">
<text class="badge-text">{{ isOnline(node) ? '在线' : '离线' }}</text>
</view>
</view>
<view class="card-body">
<view class="info-row">
<text class="label">网关ID:</text>
<text class="value">{{ node.mqtt_client_id }}</text>
</view>
<view class="info-row">
<text class="label">区域:</text>
<text class="value">{{ node.region ?? '演示区域' }}</text>
</view>
<!-- <view class="info-row">
<text class="label">版本:</text>
<text class="value">{{ node.version ?? '-' }}</text>
</view> -->
<view class="info-row">
<text class="label">最后更新:</text>
<text class="value time-value">{{ formatTime(node.updated_at) }}</text>
</view>
<view class="info-row">
<text class="label">更新时间:</text>
<text class="value">{{ getTimeAgo(node.updated_at) }}</text>
</view>
</view>
</view>
</view>
</scroll-view>
</view>
</template>
<script lang="uts">
import GatewayRealtimeService, { GatewayNode, GatewayRealtimeSubscription } from '@/utils/gatewayRealtimeService.uts'
export default {
data() {
return {
nodes: [] as GatewayNode[],
subscription: null as GatewayRealtimeSubscription | null,
now: Date.now(),
timer: null as number | null
}
},
computed: {
onlineCount(): number {
let count = 0
for (let i = 0; i < this.nodes.length; i++) {
if (this.isOnline(this.nodes[i])) {
count++
}
}
return count
}
},
onLoad() {
this.startMonitoring()
this.timer = setInterval(() => {
this.now = Date.now()
}, 1000)
},
onUnload() {
this.stopMonitoring()
if (this.timer != null) {
clearInterval(this.timer!)
this.timer = null
}
},
methods: {
async startMonitoring() {
this.subscription = await GatewayRealtimeService.subscribeGateways({
onInitial: (initialNodes: GatewayNode[]) => {
console.log('Initial nodes loaded:', initialNodes.length)
this.nodes = initialNodes
},
onInsert: (node: GatewayNode) => {
console.log('AP Node Inserted:', node.name)
this.nodes.push(node)
},
onUpdate: (node: GatewayNode) => {
console.log('AP Node Updated:', node.name, node.updated_at)
const index = this.nodes.findIndex((n) => n.id == node.id)
if (index >= 0) {
// 使用 splice 确保视图能够响应数组元素的变化
this.nodes.splice(index, 1, node)
} else {
// 如果列表中没有该节点(可能是初始加载遗漏或新节点),则添加
console.log('Node not found in list, adding it:', node.name)
this.nodes.push(node)
}
},
onDelete: (id: string) => {
const index = this.nodes.findIndex((n) => n.id == id)
if (index >= 0) {
this.nodes.splice(index, 1)
}
},
onError: (err: any) => {
console.error('Gateway monitoring error', err)
uni.showToast({
title: '连接失败',
icon: 'none'
})
}
})
},
stopMonitoring() {
if (this.subscription != null) {
this.subscription!.dispose()
this.subscription = null
}
},
isOnline(node: GatewayNode): boolean {
const updateTime = new Date(node.updated_at).getTime()
// 60 seconds timeout
return (this.now - updateTime) < 60000
},
formatTime(isoString: string): string {
const date = new Date(isoString)
return date.toLocaleTimeString()
},
getTimeAgo(isoString: string): string {
const updateTime = new Date(isoString).getTime()
const diff = Math.floor((this.now - updateTime) / 1000)
if (diff < 0) return '刚刚'
if (diff < 60) return `${diff}秒前`
return `${Math.floor(diff / 60)}分前`
},
switchPage() {
uni.redirectTo({
url: '/pages/test/multi_device_monitor'
})
}
}
}
</script>
<style>
.page-container {
display: flex;
flex-direction: column;
height: 100%;
background-color: #f5f7fa;
}
.nav-tabs {
display: flex;
flex-direction: row;
background-color: #fff;
padding: 10px;
border-bottom: 1px solid #e0e0e0;
}
.nav-tab {
flex: 1;
font-size: 14px;
margin: 0 5px;
background-color: #f5f5f5;
color: #666;
border: none;
border-radius: 4px;
}
.nav-tab.active {
background-color: #007aff;
color: #fff;
}
.header {
padding: 15px 20px;
background-color: #fff;
border-bottom: 1px solid #e0e0e0;
display: flex;
flex-direction: row;
justify-content: space-between;
align-items: center;
}
.title {
font-size: 18px;
font-weight: bold;
color: #333;
}
.subtitle {
font-size: 14px;
color: #666;
}
.grid-container {
flex: 1;
padding: 15px;
}
.grid-layout {
display: flex;
flex-direction: row;
flex-wrap: wrap;
justify-content: space-between;
}
.card {
width: 48%;
background-color: #fff;
border-radius: 8px;
padding: 12px;
margin-bottom: 15px;
box-shadow: 0 2px 4px rgba(0,0,0,0.05);
border-left: 4px solid transparent;
}
.card-online {
border-left-color: #4caf50;
}
.card-offline {
border-left-color: #9e9e9e;
opacity: 0.8;
}
.card-header {
display: flex;
flex-direction: row;
justify-content: space-between;
align-items: center;
margin-bottom: 10px;
padding-bottom: 8px;
border-bottom: 1px solid #f0f0f0;
}
.card-title {
font-size: 16px;
font-weight: bold;
color: #333;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
flex: 1;
}
.status-badge {
padding: 2px 8px;
border-radius: 10px;
margin-left: 8px;
}
.badge-online {
background-color: #e8f5e9;
}
.badge-offline {
background-color: #f5f5f5;
}
.badge-text {
font-size: 10px;
font-weight: bold;
}
.badge-online .badge-text {
color: #2e7d32;
}
.badge-offline .badge-text {
color: #757575;
}
.card-body {
display: flex;
flex-direction: column;
}
.info-row {
display: flex;
flex-direction: row;
margin-bottom: 4px;
align-items: center;
}
.label {
font-size: 12px;
color: #888;
width: 60px;
}
.value {
font-size: 12px;
color: #333;
flex: 1;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.time-value {
font-family: monospace;
}
</style>