fix: 解析客流上报数据
All checks were successful
iot-test-platform CI/CD / build-and-deploy (push) Successful in 41s

This commit is contained in:
lzh
2025-12-19 13:53:23 +08:00
parent 3959e69e45
commit 1b9bfb7bb2
7 changed files with 301 additions and 79 deletions

View File

@@ -113,7 +113,7 @@
<nav class="navbar navbar-expand-lg navbar-dark bg-dark flex-shrink-0">
<div class="container-fluid">
<a class="navbar-brand" href="#">
<i class="fas fa-network-wired me-2"></i>IoT 数据监控平台
<i class="fas fa-network-wired me-2"></i>IoT 数据测试平台
</a>
<div class="d-flex align-items-center">
<span class="text-light me-3">
@@ -143,29 +143,9 @@
<li class="nav-item">
<button class="nav-link py-2 small" @click="mode='custom'" :class="{active: mode==='custom'}">自定义</button>
</li>
<li class="nav-item">
<button class="nav-link py-2 small" @click="mode='command'" :class="{active: mode==='command'}">指令下发</button>
</li>
</ul>
<div v-if="mode === 'badge'">
<div v-if="mode === 'command'">
<div class="mb-3">
<label class="form-label small text-muted">接口类型</label>
<select v-model="commandForm.apiType" class="form-select form-select-sm" @change="updateJsonTemplate">
<option value="location">位置查询 (8201)</option>
<option value="text">文本下发 (8300)</option>
<option value="general">通用指令 (API)</option>
</select>
</div>
<div class="mb-3">
<label class="form-label small text-muted">请求参数 (JSON)</label>
<textarea v-model="commandForm.jsonBody" class="form-control font-monospace form-control-sm" rows="8"></textarea>
</div>
<button @click="sendCommand" class="btn btn-primary btn-sm w-100">
<i class="fas fa-terminal me-2"></i>发送指令
</button>
</div>
<div class="mb-3">
<label class="form-label small text-muted">工牌 ID</label>
<input v-model="badgeForm.id" class="form-control form-control-sm" placeholder="BADGE-001">
@@ -183,9 +163,26 @@
<option value="sos">SOS报警</option>
</select>
</div>
<button @click="sendBadgeData" class="btn btn-primary btn-sm w-100">
<button @click="sendBadgeData" class="btn btn-primary btn-sm w-100 mb-3">
<i class="fas fa-paper-plane me-2"></i>发送工牌数据
</button>
<hr class="my-3">
<h6 class="text-muted small mb-3"><i class="fas fa-terminal me-1"></i>指令调试 (8300/8201)</h6>
<div class="mb-2">
<label class="form-label small text-muted">指令类型</label>
<select v-model="commandForm.apiType" class="form-select form-select-sm" @change="updateJsonTemplate">
<option value="location">位置查询 (8201)</option>
<option value="text">文本下发 (8300)</option>
<option value="general">通用指令 (API)</option>
</select>
</div>
<div class="mb-2">
<textarea v-model="commandForm.jsonBody" class="form-control font-monospace form-control-sm" rows="4" placeholder="JSON 参数"></textarea>
</div>
<button @click="sendCommand" class="btn btn-outline-secondary btn-sm w-100">
发送指令
</button>
</div>
<div v-if="mode === 'counter'">
@@ -216,6 +213,9 @@
<button @click="sendCustomJson" class="btn btn-secondary btn-sm w-100">发送自定义数据</button>
</div>
<div v-if="mode === 'command'">
<!-- 移除原有的指令下发面板,已合并到工牌 Tab -->
</div>
</div>
</div>
</div>
@@ -288,15 +288,35 @@
<!-- 详细蓝牙列表 (折叠展示) -->
<div v-if="badge.bluetooth && badge.bluetooth.length > 0" class="mt-2 bg-light rounded p-2" style="font-size: 0.75rem;">
<div v-for="(ble, idx) in badge.bluetooth.slice(0, 4)" :key="idx" class="d-flex justify-content-between text-muted">
<span>MAC: {{ ble.mac }}</span>
<span>RSSI: {{ ble.rssi }} dBm</span>
<div v-for="(ble, idx) in badge.bluetooth.slice(0, 4)" :key="idx" class="d-flex justify-content-between text-muted align-items-center mb-1">
<span class="font-monospace" style="font-size: 0.7rem;">{{ ble.mac }}</span>
<div class="d-flex align-items-center" :title="ble.rssi + ' dBm'">
<div class="progress me-1" style="width: 30px; height: 4px; background-color: #e9ecef;">
<div class="progress-bar" role="progressbar"
:style="{ width: getRssiPercentage(ble.rssi) + '%', backgroundColor: getRssiColor(ble.rssi) }">
</div>
</div>
<span style="width: 25px; text-align: right;">{{ ble.rssi }}</span>
</div>
</div>
<div v-if="badge.bluetooth.length > 4" class="text-center text-primary" style="font-size: 0.7rem;">
+{{ badge.bluetooth.length - 4 }} 更多...
</div>
</div>
<!-- 语音播报下发 -->
<div class="mt-2 pt-2 border-top">
<div class="input-group input-group-sm">
<input type="text" class="form-control form-control-sm"
v-model="ttsInputs[badge.id]"
placeholder="输入语音播报内容..."
@keyup.enter="sendTTS(badge.id)">
<button class="btn btn-outline-primary" type="button" @click="sendTTS(badge.id)">
<i class="fas fa-bullhorn"></i>
</button>
</div>
</div>
<!-- 按键事件提示 -->
<div v-if="badge.lastButtonEvent" class="mt-2 alert alert-warning p-1 mb-0 text-center" style="font-size: 0.75rem;">
<i class="fas fa-hand-pointer me-1"></i>
@@ -330,24 +350,42 @@
</div>
<div v-for="(counter, id) in counters" :key="id" class="card mb-3 border-0 shadow-sm">
<div class="card-body p-3">
<div class="d-flex justify-content-between mb-3 border-bottom pb-2">
<h6 class="fw-bold mb-0 text-success"><i class="fas fa-door-open me-2"></i>{{ counter.id }}</h6>
<small class="text-muted">{{ formatTime(counter.lastUpdate) }}</small>
<div class="d-flex justify-content-between mb-2">
<div>
<h6 class="fw-bold mb-1 text-success"><i class="fas fa-door-open me-2"></i>{{ counter.id }}</h6>
<small class="text-muted d-block" style="font-size: 0.75rem;">
RSSI:
<span :class="getRssiColorClass(counter.RSSI)">{{ counter.RSSI }} dBm</span>
</small>
</div>
<div class="text-end">
<small class="text-muted d-block">{{ formatTime(counter.lastUpdate) }}</small>
<span class="badge bg-light text-dark border mt-1">v{{ counter.version }}</span>
</div>
</div>
<div class="row text-center">
<div class="row text-center border-top border-bottom py-2 my-2">
<div class="col-4 border-end">
<div class="h3 mb-0 text-primary">{{ counter.inCount }}</div>
<small class="text-muted text-uppercase" style="font-size: 0.7rem;">In</small>
<div class="h4 mb-0 text-primary">{{ counter.inCount }}</div>
<small class="text-muted text-uppercase" style="font-size: 0.7rem;">今日进入</small>
</div>
<div class="col-4 border-end">
<div class="h3 mb-0 text-warning">{{ counter.outCount }}</div>
<small class="text-muted text-uppercase" style="font-size: 0.7rem;">Out</small>
<div class="h4 mb-0 text-warning">{{ counter.outCount }}</div>
<small class="text-muted text-uppercase" style="font-size: 0.7rem;">今日离开</small>
</div>
<div class="col-4">
<div class="h3 mb-0 text-dark">{{ counter.inCount - counter.outCount }}</div>
<small class="text-muted text-uppercase" style="font-size: 0.7rem;">Stay</small>
<div class="h4 mb-0 text-dark">{{ counter.inCount - counter.outCount }}</div>
<small class="text-muted text-uppercase" style="font-size: 0.7rem;">当前滞留</small>
</div>
</div>
<!-- 最近一条数据记录 -->
<div v-if="counter.latestData" class="small text-muted d-flex justify-content-between">
<span><i class="far fa-clock me-1"></i>{{ formatCounterTime(counter.latestData.time) }}</span>
<span>
<i class="fas fa-battery-half me-1"></i>Rx:{{counter.latestData.rxBat}}% / Tx:{{counter.latestData.txBat}}%
</span>
</div>
</div>
</div>
</div>
@@ -416,7 +454,8 @@
// 实时数据存储
badges: {}, // Map: id -> badge data
counters: {}, // Map: id -> counter data
logs: []
logs: [],
ttsInputs: {} // Map: badgeId -> input string
}
},
mounted() {
@@ -473,10 +512,42 @@
}
}
// 2. 处理计数器数据 (识别 type=counter)
else if (data.type === 'counter' && data.id) {
this.counters[data.id] = {
...data,
// 2. 处理计数器数据 (识别 type=counter OR 含有 uuid 字段的复杂JSON)
else if ((data.type === 'counter' && data.id) || (data.uuid && data.data && Array.isArray(data.data))) {
// 统一 ID 字段
const id = data.id || data.uuid;
// 尝试解析复杂 JSON 结构
let totalIn = data.inCount || 0;
let totalOut = data.outCount || 0;
let latestData = null;
if (data.data && Array.isArray(data.data)) {
// 累加 data 数组中的流量,或者取最新一条(取决于业务逻辑,这里假设是累积值或者我们显示最新状态)
// 通常计数器上报的是当次时间段内的增量或者当前的累计值。
// 这里假设我们需要展示最新的累积值,或者自行累加。
// 简化处理:显示最新一条数据的 in/out或者如果后端没做累加前端简单累加
// 由于示例数据看起来像是一段时间的记录,我们取最后一条作为"最新状态"
// 找出时间最近的一条
const sortedData = [...data.data].sort((a, b) => b.time.localeCompare(a.time));
if (sortedData.length > 0) {
latestData = sortedData[0];
// 如果 JSON 里的 in/out 是累计值,直接使用;如果是增量,需要累加。
// 根据示例 "in": 22看起来像累计值。
totalIn = latestData.in || 0; // Fix: use || 0 to avoid undefined
totalOut = latestData.out || 0; // Fix: use || 0 to avoid undefined
}
}
this.counters[id] = {
id: id,
// 优先使用外层字段,没有则使用解析出的字段
inCount: totalIn,
outCount: totalOut,
RSSI: data.RSSI,
version: data.version,
latestData: latestData,
lastUpdate: now
};
}
@@ -615,7 +686,11 @@
// 辅助函数
formatTime(date) {
if (!date) return '';
return new Date(date).toLocaleTimeString();
try {
return new Date(date).toLocaleTimeString();
} catch (e) {
return '';
}
},
getBatteryColor(level) {
if (level > 60) return 'text-success';
@@ -640,6 +715,75 @@
action = `长按 ${keyNum}号键`;
}
return action;
},
// RSSI 可视化
getRssiPercentage(rssi) {
// 假设 -100dBm 为 0%-50dBm 为 100%
const min = -100;
const max = -50;
if (rssi >= max) return 100;
if (rssi <= min) return 0;
return ((rssi - min) / (max - min)) * 100;
},
getRssiColor(rssi) {
if (rssi >= -60) return '#28a745'; // Green
if (rssi >= -75) return '#ffc107'; // Yellow
if (rssi >= -85) return '#fd7e14'; // Orange
return '#dc3545'; // Red
},
getRssiColorClass(rssi) {
if (!rssi) return 'text-muted';
if (rssi >= -60) return 'text-success';
if (rssi >= -75) return 'text-warning';
if (rssi >= -85) return 'text-warning'; // Orange-ish
return 'text-danger';
},
formatCounterTime(str) {
// 20250501093000 -> 09:30:00
if (!str || str.length < 14) return str;
return str.substring(8, 10) + ':' + str.substring(10, 12) + ':' + str.substring(12, 14);
},
// 发送 TTS
async sendTTS(phone) {
const content = this.ttsInputs[phone];
if (!content || !content.trim()) {
alert('请输入播报内容');
return;
}
try {
const formData = new FormData();
formData.append('phone', phone);
formData.append('content', content);
// Flag 9 = 0x08 (TTS) | 0x01 (紧急/指令) -> 混合模式,确保立刻播报
formData.append('flag', 9);
const res = await fetch('/api/v1/device/command/text', {
method: 'POST',
body: formData
});
const result = await res.json();
if (result.code === 200) {
// 清空输入框
this.ttsInputs[phone] = '';
// 记录日志到界面以便反馈
this.addLog({ source: 'SYSTEM', message: `${phone} 下发语音: ${content}` });
} else {
alert('下发失败: ' + result.message);
}
} catch (e) {
alert('网络异常: ' + e.message);
}
},
// 内部日志辅助
addLog(data) {
const now = new Date().toLocaleTimeString();
this.logs.unshift({
time: now,
source: data.source,
data: data.message || JSON.stringify(data)
});
if (this.logs.length > 50) this.logs.pop();
}
}
}).mount('#app')