655 lines
16 KiB
Vue
655 lines
16 KiB
Vue
<template>
|
||
<view class="debug-wrap">
|
||
<!-- 设备信息 -->
|
||
<view class="device-info">
|
||
<text class="label">{{$tt('home.serialNumber')}}:</text>
|
||
<text class="value">{{device.serialNumber}}</text>
|
||
</view>
|
||
|
||
<!-- 日志列表 -->
|
||
<view class="log-section">
|
||
<view class="log-item" v-for="(item, index) in logList" :key="index"
|
||
:class="{'send-item': item.type === 0, 'receive-item': item.type === 1, 'report-item': item.type === 2}">
|
||
<view class="item-head">
|
||
<text class="type-tag" :class="'type-' + item.type">
|
||
{{item.type === 0 ? $tt('debug.sendType') : item.type === 1 ? $tt('debug.reveiceType') : $tt('debug.reportType')}}
|
||
</text>
|
||
<text class="time">{{item.time}}</text>
|
||
<text class="analysis-btn" v-if="item.type !== 0 && !item.analysis" @click="handleAnalysis(item, index)">
|
||
{{$tt('debug.analysis')}}
|
||
</text>
|
||
<text class="analysised-tag" v-if="item.type !== 0 && item.analysis">
|
||
{{$tt('debug.analysised')}}
|
||
</text>
|
||
</view>
|
||
<view class="item-content">
|
||
<!-- JSON格式 -->
|
||
<view v-if="isValidJson(item.valueAscii || item.value)" class="json-wrap">
|
||
<text class="format-label">JSON:</text>
|
||
<text class="json-text">{{formatJsonValue(item.value, item.valueAscii)}}</text>
|
||
</view>
|
||
<!-- HEX + ASCII -->
|
||
<block v-else>
|
||
<view v-if="item.value" class="hex-wrap">
|
||
<text class="format-label">HEX:</text>
|
||
<text class="hex-text">{{item.value}}</text>
|
||
</view>
|
||
<view v-if="item.valueAscii && hasReadableAscii(item.valueAscii)" class="ascii-wrap">
|
||
<text class="format-label">ASCII:</text>
|
||
<text class="ascii-text">{{item.valueAscii}}</text>
|
||
</view>
|
||
</block>
|
||
</view>
|
||
<!-- 解析结果 -->
|
||
<view class="analysis-result" v-if="item.type !== 0 && item.analysis">
|
||
<view class="result-row">
|
||
<text class="result-label">{{$tt('debug.name')}}:</text>
|
||
<text class="result-value">{{item.analysisVal.name || '--'}}</text>
|
||
</view>
|
||
<view class="result-row">
|
||
<text class="result-label">{{$tt('debug.identifier')}}:</text>
|
||
<text class="result-value">{{item.analysisVal.id || '--'}}</text>
|
||
</view>
|
||
<view class="result-row">
|
||
<text class="result-label">{{$tt('debug.value')}}:</text>
|
||
<text class="result-value">{{item.analysisVal.value || '--'}}</text>
|
||
</view>
|
||
<view class="result-row" v-if="item.analysisList && item.analysisList.length > 1">
|
||
<text class="more-btn" @click="showMoreAnalysis(item)">{{$tt('debug.more')}}</text>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
<u-empty mode="list" :show="logList.length === 0" marginTop="30"></u-empty>
|
||
</view>
|
||
|
||
<!-- 快捷指令区域 -->
|
||
<view class="quick-command-wrap">
|
||
<view class="section-title">
|
||
<text>{{$tt('debug.quickCommand')}}</text>
|
||
<text class="add-btn" @click="handleAddCommand">{{$tt('debug.addCommand')}}</text>
|
||
</view>
|
||
<u-swipe-action>
|
||
<u-swipe-action-item v-for="(item, index) in quickList" :key="index"
|
||
:options="swipeOptions" @click="handleSwipeClick($event, item)">
|
||
<view class="quick-item" @click="handleQuickClick(item)">
|
||
<view class="quick-item-name">{{item.name}}</view>
|
||
<view class="quick-item-cmd">{{item.command}}</view>
|
||
</view>
|
||
</u-swipe-action-item>
|
||
</u-swipe-action>
|
||
<u-empty mode="list" :show="quickList.length === 0" marginTop="20"></u-empty>
|
||
</view>
|
||
|
||
<!-- 底部输入区域 -->
|
||
<view class="bottom-input">
|
||
<view class="input-row">
|
||
<view class="type-picker" @click="showDataTypePicker = true">
|
||
<text>{{currentDataTypeLabel}}</text>
|
||
<u-icon name="arrow-down" size="12" color="#606266"></u-icon>
|
||
</view>
|
||
<input class="send-input" v-model="sendVal" :placeholder="$tt('debug.inputPlaceholder')"
|
||
@confirm="handleSend" />
|
||
<view class="send-btn" @click="handleSend">{{$tt('debug.send')}}</view>
|
||
</view>
|
||
</view>
|
||
|
||
<!-- 数据类型选择器 -->
|
||
<u-action-sheet :show="showDataTypePicker" :actions="dataTypeActions" @select="onDataTypeSelect"
|
||
@close="showDataTypePicker = false"></u-action-sheet>
|
||
|
||
<!-- 删除确认弹窗 -->
|
||
<u-modal :show="showDeleteModal" showCancelButton :confirmText="$tt('common.confirm')"
|
||
@cancel="() => showDeleteModal = false" :title="$tt('debug.deleteCommand')" :content="deleteContent"
|
||
@confirm="handleConfirmDelete">
|
||
</u-modal>
|
||
|
||
<!-- 更多解析弹窗 -->
|
||
<u-popup :show="showMorePopup" mode="bottom" :round="10" @close="() => showMorePopup = false">
|
||
<view class="more-popup">
|
||
<view class="popup-title">{{$tt('debug.analysis')}}</view>
|
||
<scroll-view scroll-y style="max-height: 400px;">
|
||
<view class="more-item" v-for="(item, idx) in moreList" :key="idx">
|
||
<view class="more-row">
|
||
<text class="more-label">{{$tt('debug.name')}}:</text>
|
||
<text>{{item.name}}</text>
|
||
</view>
|
||
<view class="more-row">
|
||
<text class="more-label">{{$tt('debug.identifier')}}:</text>
|
||
<text>{{item.id}}</text>
|
||
</view>
|
||
<view class="more-row">
|
||
<text class="more-label">{{$tt('debug.value')}}:</text>
|
||
<text>{{item.value}}</text>
|
||
</view>
|
||
</view>
|
||
</scroll-view>
|
||
</view>
|
||
</u-popup>
|
||
</view>
|
||
</template>
|
||
|
||
<script>
|
||
import { formatDate } from '@/utils/common.js';
|
||
import { messagePost, preferencesList, delPreferences } from '@/apis/modules/mqttTest.js';
|
||
|
||
export default {
|
||
data() {
|
||
return {
|
||
device: {},
|
||
logList: [],
|
||
sendVal: '',
|
||
dataType: 'hex',
|
||
dataTypeActions: [
|
||
{ name: 'HEX', value: 'hex' },
|
||
{ name: 'JSON', value: 'json' },
|
||
{ name: 'ASCII', value: 'ascii' }
|
||
],
|
||
showDataTypePicker: false,
|
||
quickList: [],
|
||
swipeOptions: [
|
||
{ text: this.$tt('debug.editCommand'), style: { backgroundColor: '#3c9cff' } },
|
||
{ text: this.$tt('debug.deleteCommand'), style: { backgroundColor: '#ff7e7e' } }
|
||
],
|
||
showDeleteModal: false,
|
||
deleteContent: '',
|
||
deleteItem: null,
|
||
showMorePopup: false,
|
||
moreList: [],
|
||
};
|
||
},
|
||
computed: {
|
||
currentDataTypeLabel() {
|
||
const item = this.dataTypeActions.find(i => i.value === this.dataType);
|
||
return item ? item.name : 'HEX';
|
||
}
|
||
},
|
||
onLoad(option) {
|
||
this.device = JSON.parse(decodeURIComponent(option.device));
|
||
this.getQuickList();
|
||
this.registerBus();
|
||
},
|
||
onUnload() {
|
||
this.unregisterBus();
|
||
},
|
||
onShow() {
|
||
this.getQuickList();
|
||
},
|
||
methods: {
|
||
// 注册事件总线
|
||
registerBus() {
|
||
this.$bus.$on('updateData', this.handleUpdateData);
|
||
},
|
||
// 注销事件总线
|
||
unregisterBus() {
|
||
this.$bus.$off('updateData', this.handleUpdateData);
|
||
},
|
||
// 处理MQTT数据上报
|
||
handleUpdateData(params) {
|
||
const { serialNumber, data } = params;
|
||
if (serialNumber !== this.device.serialNumber) return;
|
||
this.logList.push({
|
||
type: 2,
|
||
time: formatDate(new Date()),
|
||
value: data.sources || '',
|
||
valueAscii: data.sourcesAscii || '',
|
||
loading: false,
|
||
analysis: false,
|
||
analysisList: data.message || [],
|
||
analysisVal: {}
|
||
});
|
||
},
|
||
// 获取快捷指令列表
|
||
getQuickList() {
|
||
preferencesList({
|
||
serialNumber: this.device.serialNumber,
|
||
productId: this.device.productId
|
||
}).then(res => {
|
||
if (res.code === 200) {
|
||
this.quickList = res.rows || [];
|
||
}
|
||
}).catch(err => {
|
||
console.log(err);
|
||
});
|
||
},
|
||
// 数据类型选择
|
||
onDataTypeSelect(item) {
|
||
this.dataType = item.value;
|
||
this.showDataTypePicker = false;
|
||
},
|
||
// 发送指令
|
||
handleSend() {
|
||
if (!this.sendVal) {
|
||
uni.showToast({ icon: 'none', title: this.$tt('debug.sendEmpty') });
|
||
return;
|
||
}
|
||
messagePost({
|
||
message: this.sendVal,
|
||
serialNumber: this.device.serialNumber,
|
||
topicName: '/function/get',
|
||
dataType: this.dataType
|
||
}).then(res => {
|
||
if (res.code === 200) {
|
||
this.logList.push({
|
||
type: 0,
|
||
time: formatDate(new Date()),
|
||
value: this.sendVal,
|
||
valueAscii: '',
|
||
loading: false,
|
||
analysis: false,
|
||
analysisList: [],
|
||
analysisVal: {}
|
||
});
|
||
} else {
|
||
uni.showToast({ icon: 'none', title: res.msg || this.$tt('debug.sendFail') });
|
||
}
|
||
}).catch(err => {
|
||
console.log(err);
|
||
uni.showToast({ icon: 'none', title: this.$tt('debug.sendFail') });
|
||
});
|
||
},
|
||
// 解析数据
|
||
handleAnalysis(item, index) {
|
||
if (item.analysisList && item.analysisList.length > 0) {
|
||
item.analysis = true;
|
||
item.analysisVal = {
|
||
name: item.analysisList[0].name,
|
||
value: item.analysisList[0].value,
|
||
id: item.analysisList[0].id
|
||
};
|
||
this.$set(this.logList, index, item);
|
||
} else {
|
||
uni.showToast({ icon: 'none', title: this.$tt('debug.analysisEmpty') });
|
||
}
|
||
},
|
||
// 显示更多解析
|
||
showMoreAnalysis(item) {
|
||
this.moreList = item.analysisList || [];
|
||
this.showMorePopup = true;
|
||
},
|
||
// 判断是否有效JSON
|
||
isValidJson(value) {
|
||
if (!value) return false;
|
||
try {
|
||
const parsed = JSON.parse(value);
|
||
return typeof parsed === 'object' && parsed !== null;
|
||
} catch (e) {
|
||
return false;
|
||
}
|
||
},
|
||
// 格式化JSON
|
||
formatJsonValue(hexValue, asciiValue) {
|
||
if (asciiValue) {
|
||
try {
|
||
const json = JSON.parse(asciiValue);
|
||
return JSON.stringify(json, null, 2);
|
||
} catch (e) {}
|
||
}
|
||
if (hexValue) {
|
||
try {
|
||
const json = JSON.parse(hexValue);
|
||
return JSON.stringify(json, null, 2);
|
||
} catch (e) {
|
||
try {
|
||
const hexStr = hexValue.replace(/\s+/g, '');
|
||
let jsonStr = '';
|
||
for (let i = 0; i < hexStr.length; i += 2) {
|
||
const hexByte = hexStr.substr(i, 2);
|
||
const charCode = parseInt(hexByte, 16);
|
||
if (charCode >= 32 && charCode <= 126) {
|
||
jsonStr += String.fromCharCode(charCode);
|
||
}
|
||
}
|
||
if (jsonStr) {
|
||
const json = JSON.parse(jsonStr);
|
||
return JSON.stringify(json, null, 2);
|
||
}
|
||
} catch (e2) {}
|
||
}
|
||
}
|
||
return '数据不是有效的JSON格式';
|
||
},
|
||
// 判断是否有可读ASCII
|
||
hasReadableAscii(asciiValue) {
|
||
if (!asciiValue) return false;
|
||
const printableRegex = /[\x20-\x7E]/;
|
||
return printableRegex.test(asciiValue) && asciiValue.trim().length > 0;
|
||
},
|
||
// 快捷指令点击
|
||
handleQuickClick(item) {
|
||
this.sendVal = item.command;
|
||
},
|
||
// 滑动按钮点击
|
||
handleSwipeClick(e, item) {
|
||
const { index } = e;
|
||
if (index === 0) {
|
||
uni.navigateTo({
|
||
url: `/pagesB/home/device/debug/edit?device=${encodeURIComponent(JSON.stringify(this.device))}&item=${encodeURIComponent(JSON.stringify(item))}`
|
||
});
|
||
} else if (index === 1) {
|
||
this.deleteItem = item;
|
||
this.deleteContent = this.$tt('debug.deleteConfirm');
|
||
this.showDeleteModal = true;
|
||
}
|
||
},
|
||
// 确认删除
|
||
handleConfirmDelete() {
|
||
if (!this.deleteItem) return;
|
||
const deleteId = this.deleteItem.id;
|
||
this.showDeleteModal = false;
|
||
delPreferences({ id: deleteId }).then(res => {
|
||
if (res.code === 200) {
|
||
uni.showToast({ icon: 'success', title: this.$tt('debug.deleteSuccess') });
|
||
this.getQuickList();
|
||
} else {
|
||
uni.showToast({ icon: 'none', title: res.msg || this.$tt('debug.deleteFail') });
|
||
}
|
||
}).catch(err => {
|
||
console.log(err);
|
||
uni.showToast({ icon: 'none', title: this.$tt('debug.deleteFail') });
|
||
}).finally(() => {
|
||
this.deleteItem = null;
|
||
});
|
||
},
|
||
// 新建指令
|
||
handleAddCommand() {
|
||
uni.navigateTo({
|
||
url: `/pagesB/home/device/debug/edit?device=${encodeURIComponent(JSON.stringify(this.device))}`
|
||
});
|
||
}
|
||
}
|
||
};
|
||
</script>
|
||
|
||
<style>
|
||
page {
|
||
background: #eef3f7;
|
||
}
|
||
</style>
|
||
|
||
<style lang="scss" scoped>
|
||
.debug-wrap {
|
||
padding-bottom: 140rpx;
|
||
|
||
.device-info {
|
||
padding: 24rpx 30rpx;
|
||
background: #fff;
|
||
margin-bottom: 20rpx;
|
||
|
||
.label {
|
||
color: #909399;
|
||
}
|
||
|
||
.value {
|
||
color: #303133;
|
||
font-weight: 500;
|
||
}
|
||
}
|
||
|
||
.log-section {
|
||
padding: 0 20rpx;
|
||
|
||
.log-item {
|
||
background: #fff;
|
||
border-radius: 10rpx;
|
||
padding: 20rpx;
|
||
margin-bottom: 20rpx;
|
||
|
||
&.send-item {
|
||
border-left: 6rpx solid #e6a23c;
|
||
}
|
||
|
||
&.receive-item {
|
||
border-left: 6rpx solid #67c23a;
|
||
}
|
||
|
||
&.report-item {
|
||
border-left: 6rpx solid #409eff;
|
||
}
|
||
|
||
.item-head {
|
||
display: flex;
|
||
align-items: center;
|
||
margin-bottom: 16rpx;
|
||
|
||
.type-tag {
|
||
font-size: 24rpx;
|
||
padding: 4rpx 12rpx;
|
||
border-radius: 4rpx;
|
||
margin-right: 16rpx;
|
||
|
||
&.type-0 {
|
||
background: rgba(230, 162, 60, 0.1);
|
||
color: #e6a23c;
|
||
}
|
||
|
||
&.type-1 {
|
||
background: rgba(103, 194, 58, 0.1);
|
||
color: #67c23a;
|
||
}
|
||
|
||
&.type-2 {
|
||
background: rgba(64, 158, 255, 0.1);
|
||
color: #409eff;
|
||
}
|
||
}
|
||
|
||
.time {
|
||
flex: 1;
|
||
font-size: 24rpx;
|
||
color: #909399;
|
||
}
|
||
|
||
.analysis-btn {
|
||
font-size: 24rpx;
|
||
color: #409eff;
|
||
padding: 4rpx 12rpx;
|
||
background: rgba(64, 158, 255, 0.1);
|
||
border-radius: 4rpx;
|
||
}
|
||
|
||
.analysised-tag {
|
||
font-size: 24rpx;
|
||
color: #909399;
|
||
padding: 4rpx 12rpx;
|
||
background: #f4f4f5;
|
||
border-radius: 4rpx;
|
||
}
|
||
}
|
||
|
||
.item-content {
|
||
.format-label {
|
||
font-size: 24rpx;
|
||
color: #606266;
|
||
margin-right: 12rpx;
|
||
font-weight: 500;
|
||
}
|
||
|
||
.json-wrap, .hex-wrap, .ascii-wrap {
|
||
margin-bottom: 12rpx;
|
||
word-wrap: break-word;
|
||
|
||
&:last-child {
|
||
margin-bottom: 0;
|
||
}
|
||
}
|
||
|
||
.json-text, .hex-text, .ascii-text {
|
||
font-size: 26rpx;
|
||
color: #303133;
|
||
font-family: 'Courier New', monospace;
|
||
word-break: break-all;
|
||
}
|
||
}
|
||
|
||
.analysis-result {
|
||
margin-top: 16rpx;
|
||
padding-top: 16rpx;
|
||
border-top: 1rpx solid #eee;
|
||
|
||
.result-row {
|
||
display: flex;
|
||
margin-bottom: 10rpx;
|
||
|
||
&:last-child {
|
||
margin-bottom: 0;
|
||
}
|
||
|
||
.result-label {
|
||
font-size: 24rpx;
|
||
color: #909399;
|
||
min-width: 120rpx;
|
||
}
|
||
|
||
.result-value {
|
||
font-size: 24rpx;
|
||
color: #303133;
|
||
flex: 1;
|
||
}
|
||
|
||
.more-btn {
|
||
font-size: 24rpx;
|
||
color: #409eff;
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
.quick-command-wrap {
|
||
margin: 20rpx 20rpx 40rpx;
|
||
|
||
.section-title {
|
||
display: flex;
|
||
justify-content: space-between;
|
||
align-items: center;
|
||
margin-bottom: 20rpx;
|
||
|
||
text {
|
||
font-size: 32rpx;
|
||
font-weight: 500;
|
||
color: #303133;
|
||
}
|
||
|
||
.add-btn {
|
||
font-size: 28rpx;
|
||
color: #409eff;
|
||
font-weight: normal;
|
||
}
|
||
}
|
||
|
||
.quick-item {
|
||
background: #fff;
|
||
padding: 20rpx;
|
||
border-radius: 10rpx;
|
||
margin-bottom: 16rpx;
|
||
|
||
.quick-item-name {
|
||
font-size: 28rpx;
|
||
color: #303133;
|
||
font-weight: 500;
|
||
margin-bottom: 8rpx;
|
||
}
|
||
|
||
.quick-item-cmd {
|
||
font-size: 24rpx;
|
||
color: #909399;
|
||
word-break: break-all;
|
||
}
|
||
}
|
||
}
|
||
|
||
.bottom-input {
|
||
position: fixed;
|
||
bottom: 0;
|
||
left: 0;
|
||
right: 0;
|
||
background: #fff;
|
||
padding: 20rpx;
|
||
padding-bottom: calc(20rpx + env(safe-area-inset-bottom));
|
||
border-top: 1rpx solid #eee;
|
||
z-index: 100;
|
||
|
||
.input-row {
|
||
display: flex;
|
||
align-items: center;
|
||
|
||
.type-picker {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
width: 100rpx;
|
||
height: 70rpx;
|
||
background: #f4f4f5;
|
||
border-radius: 8rpx;
|
||
margin-right: 16rpx;
|
||
|
||
text {
|
||
font-size: 26rpx;
|
||
color: #606266;
|
||
margin-right: 4rpx;
|
||
}
|
||
}
|
||
|
||
.send-input {
|
||
flex: 1;
|
||
height: 70rpx;
|
||
background: #f4f4f5;
|
||
border-radius: 8rpx;
|
||
padding: 0 20rpx;
|
||
font-size: 26rpx;
|
||
margin-right: 16rpx;
|
||
}
|
||
|
||
.send-btn {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
width: 120rpx;
|
||
height: 70rpx;
|
||
background: #3c9cff;
|
||
border-radius: 8rpx;
|
||
font-size: 26rpx;
|
||
color: #fff;
|
||
}
|
||
}
|
||
}
|
||
|
||
.more-popup {
|
||
padding: 30rpx;
|
||
|
||
.popup-title {
|
||
font-size: 32rpx;
|
||
font-weight: 500;
|
||
text-align: center;
|
||
margin-bottom: 20rpx;
|
||
}
|
||
|
||
.more-item {
|
||
background: #f4f4f5;
|
||
padding: 20rpx;
|
||
border-radius: 10rpx;
|
||
margin-bottom: 16rpx;
|
||
|
||
.more-row {
|
||
display: flex;
|
||
margin-bottom: 10rpx;
|
||
|
||
&:last-child {
|
||
margin-bottom: 0;
|
||
}
|
||
|
||
.more-label {
|
||
min-width: 120rpx;
|
||
font-size: 24rpx;
|
||
color: #909399;
|
||
}
|
||
|
||
text {
|
||
font-size: 24rpx;
|
||
color: #303133;
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
</style> |