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

339 lines
8.6 KiB
Plaintext

<template>
<view class="page-container">
<view class="nav-tabs">
<button class="nav-tab" @click="switchPage">AP网关</button>
<button class="nav-tab active">设备监测</button>
</view>
<view class="header">
<text class="title">设备实时数据监测</text>
</view>
<!-- <view class="input-group">
<view class="input-item">
<text class="label">Room ID:</text>
<input class="input" v-model="roomId" placeholder="例如 room:0001" />
</view>
<view class="input-item">
<text class="label">Device ID:</text>
<input class="input" type="number" v-model="deviceId" placeholder="例如 11" />
</view>
<button class="btn" @click="toggleSubscription">{{ isSubscribing ? '停止监测' : '开始监测' }}</button>
</view> -->
<view class="input-group" style="margin-top: 10px;">
<text class="label" style="width: 100%; margin-bottom: 5px;">测试下行指令 (Test Downlink)</text>
<view class="input-item">
<text class="label">AP ID:</text>
<input class="input" v-model="apId" placeholder="例如 WDDGW20000014" />
</view>
<view class="input-item">
<text class="label">CMD:</text>
<input class="input" type="number" v-model="cmd" placeholder="例如 28689" />
</view>
<view class="input-item">
<text class="label">Dev ID:</text>
<input class="input" v-model="devIdInput" placeholder="例如 [11]" />
</view>
<button class="btn" @click="sendTestDownlink">发送测试指令</button>
</view>
<view class="status-panel">
<text class="status-text">状态: {{ status }}</text>
<text class="status-text">最后更新: {{ lastUpdateTime }}</text>
</view>
<scroll-view class="log-container" scroll-y="true">
<view v-for="(log, index) in logs" :key="index" class="log-item">
<text class="log-text">{{ log }}</text>
</view>
</scroll-view>
<view v-if="currentData != null" class="data-card">
<view class="card-row">
<text class="card-label">心率 (HeartRate):</text>
<text class="card-value highlight">{{ currentData!.heartrate }} bpm</text>
</view>
<view class="card-row">
<text class="card-label">接收时间 (RecvTime):</text>
<text class="card-value">{{ formatTime(currentData!.recvtime) }}</text>
</view>
<view class="card-row">
<text class="card-label">信号 (RSSI):</text>
<text class="card-value">{{ currentData!.rssi }}</text>
</view>
<view class="card-row">
<text class="card-label">活动等级:</text>
<text class="card-value">{{ currentData!.activitylevel }}</text>
</view>
</view>
</view>
</template>
<script lang="uts">
import DeviceRealtimeService, { DeviceBatchItem, DeviceRealtimeSubscription } from '@/utils/deviceRealtimeService.uts'
import supa from '@/components/supadb/aksupainstance.uts'
export default {
data() {
return {
roomId: 'room:0001',
deviceId: 11,
devIdInput: '[11]',
apId: 'WDDGW20000014',
cmd: 28689,
isSubscribing: false,
status: '未连接',
logs: [] as string[],
currentData: null as DeviceBatchItem | null,
subscription: null as DeviceRealtimeSubscription | null,
lastUpdateTime: '-'
}
},
onLoad(options:OnLoadOptions) {
let idStr: string | null = null
if (options['id']) {
idStr = options['id']
} else if (options['deviceId']) {
idStr = options['deviceId']
}
if (idStr != null) {
this.deviceId = parseInt(idStr)
this.devIdInput = `[${idStr}]`
// 自动生成测试用的 AP ID: WDDGW + (20000000 + deviceId)
this.apId = 'WDDGW' + (20000000 + this.deviceId).toString()
this.startSubscription()
}
},
onUnload() {
this.stopSubscription()
},
methods: {
toggleSubscription() {
if (this.isSubscribing) {
this.stopSubscription()
} else {
this.startSubscription()
}
},
async startSubscription() {
if (this.roomId == '') {
this.addLog('错误: Room ID 不能为空')
return
}
this.isSubscribing = true
this.status = '正在连接...'
this.logs = []
this.addLog(`开始订阅 Room: ${this.roomId}, Device: ${this.deviceId}`)
try {
this.subscription = await DeviceRealtimeService.subscribeDevice({
roomId: this.roomId,
deviceId: Number(this.deviceId)
}, {
onData: (data: DeviceBatchItem) => {
this.currentData = data
this.lastUpdateTime = new Date().toLocaleTimeString()
this.status = '接收数据中'
this.addLog(`收到数据: HR=${data.heartrate}, Time=${data.recvtime}`)
},
onError: (err: any) => {
this.status = '错误'
this.addLog(`订阅错误: ${err}`)
this.isSubscribing = false
}
})
this.status = '已订阅 (等待数据)'
} catch (e) {
this.status = '连接失败'
this.addLog(`连接异常: ${e}`)
this.isSubscribing = false
}
},
stopSubscription() {
if (this.subscription != null) {
this.subscription!.dispose()
this.subscription = null
}
this.isSubscribing = false
this.status = '已停止'
this.addLog('停止订阅')
},
addLog(msg: string) {
const time = new Date().toLocaleTimeString()
this.logs.unshift(`[${time}] ${msg}`)
if (this.logs.length > 50) {
this.logs.pop()
}
},
formatTime(ts: number): string {
return new Date(ts).toLocaleTimeString()
},
switchPage() {
uni.redirectTo({
url: '/pages/test/ap_monitor'
})
},
async sendTestDownlink() {
if (this.apId == '') {
this.addLog('错误: AP ID 不能为空')
return
}
const topic = `watch/server/data/${this.apId}`
const payload = {
cmd: Number(this.cmd),
token: Math.floor(Math.random() * 100000),
timeout: 2,
inval: 1,
devid: JSON.parse(this.devIdInput)
}
this.addLog(`正在发送下行指令... Topic: ${topic}`)
const currentUser = supa.user
const userId = currentUser?.getString('id') ?? '00000000-0000-0000-0000-000000000000'
try {
const res = await supa.from('chat_mqtt_downlinks').insert({
topic: topic,
payload: payload,
qos: 1,
conversation_id: '00000000-0000-0000-0000-000000000000',
created_by: userId
}).execute()
if (res.error != null) {
this.addLog(`发送失败: ${res.error}`)
} else {
this.addLog('发送成功! 数据已写入 chat_mqtt_downlinks')
}
} catch (e) {
this.addLog(`发送异常: ${e}`)
}
}
}
}
</script>
<style>
.page-container {
padding: 20px;
display: flex;
flex-direction: column;
height: 100%;
}
.nav-tabs {
display: flex;
flex-direction: row;
background-color: #fff;
padding: 0 0 10px 0;
margin-bottom: 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 {
margin-bottom: 20px;
}
.title {
font-size: 20px;
font-weight: bold;
}
.input-group {
background-color: #f5f5f5;
padding: 15px;
border-radius: 8px;
margin-bottom: 20px;
}
.input-item {
margin-bottom: 10px;
display: flex;
flex-direction: row;
align-items: center;
}
.label {
width: 80px;
font-size: 14px;
}
.input {
flex: 1;
height: 36px;
background-color: #fff;
border-radius: 4px;
padding: 0 10px;
font-size: 14px;
}
.btn {
margin-top: 10px;
background-color: #007aff;
color: #fff;
}
.status-panel {
margin-bottom: 10px;
display: flex;
flex-direction: row;
justify-content: space-between;
}
.status-text {
font-size: 12px;
color: #666;
}
.data-card {
background-color: #e3f2fd;
padding: 20px;
border-radius: 12px;
margin-bottom: 20px;
border: 1px solid #90caf9;
}
.card-row {
display: flex;
flex-direction: row;
justify-content: space-between;
margin-bottom: 8px;
}
.card-label {
font-size: 16px;
color: #333;
}
.card-value {
font-size: 16px;
font-weight: bold;
color: #1565c0;
}
.highlight {
font-size: 24px;
color: #d32f2f;
}
.log-container {
flex: 1;
background-color: #333;
border-radius: 8px;
padding: 10px;
}
.log-item {
margin-bottom: 4px;
}
.log-text {
color: #00e676;
font-size: 12px;
font-family: monospace;
}
</style>