316 lines
7.3 KiB
Plaintext
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>
|