fastbee/yack-app/pagesB/home/device/debug/index.vue
2026-08-23 16:27:10 +08:00

655 lines
16 KiB
Vue
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<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>