20260823:H5页面支持数据调试

This commit is contained in:
George 2026-08-23 16:27:10 +08:00
parent 1e15ae7400
commit 3f5bba9a02
12 changed files with 1625 additions and 11 deletions

View File

@ -63,7 +63,8 @@
"Bash(done)", "Bash(done)",
"Bash(file /tmp/tile_*.png)", "Bash(file /tmp/tile_*.png)",
"Bash(for REF in \"http://localhost:8080/\" \"https://dev.fastbee.cn/\" \"https://www.tianditu.gov.cn/\")", "Bash(for REF in \"http://localhost:8080/\" \"https://dev.fastbee.cn/\" \"https://www.tianditu.gov.cn/\")",
"Read(//tmp/**)" "Read(//tmp/**)",
"Bash(node -e ' *)"
] ]
} }
} }

View File

@ -0,0 +1,42 @@
const http = uni.$u.http;
// 指令编码
export function encode(params) {
return http.get('/iot/message/encode', {
params: params
});
}
// 指令解码
export function decode(params) {
return http.get('/iot/message/decode', {
params: params
});
}
// 平台下发指令
export function messagePost(data) {
return http.post('/iot/message/post', data);
}
// 查询设备指令偏好设置列表
export function preferencesList(params) {
return http.get('/iot/preferences/list', {
params: params
});
}
// 新增设备指令偏好设置
export function addPreferences(data) {
return http.post('/iot/preferences', data);
}
// 修改设备指令偏好设置
export function editPreferences(data) {
return http.put('/iot/preferences', data);
}
// 删除设备指令偏好设置
export function delPreferences(data) {
return http.delete('/iot/preferences/' + data.id);
}

View File

@ -1,5 +1,5 @@
// H5端开发和生产环境协议 // H5端开发和生产环境协议
let protocalDev = "wss://"; let protocalDev = "ws://";
let protocalProd = "wss://"; let protocalProd = "wss://";
// 条件编译,微信端和移动端使用wxs协议 // 条件编译,微信端和移动端使用wxs协议
@ -19,7 +19,7 @@ const CONFIG = {
development: { development: {
officialWebUrl: 'http://localhost:8080/', officialWebUrl: 'http://localhost:8080/',
baseUrl: 'http://localhost:8080/', baseUrl: 'http://localhost:8080/',
mqttServer: protocalDev + 'localhost/mqtt', mqttServer: protocalDev + '47.121.180.229:8083/mqtt',
decoderUrl: 'http://localhost:8080/', decoderUrl: 'http://localhost:8080/',
qqmapKey, qqmapKey,
xinzhiKey, xinzhiKey,

View File

@ -49,6 +49,8 @@
"navBar.updateDevice": "Device Update", "navBar.updateDevice": "Device Update",
"navBar.eventLog": "Event Log", "navBar.eventLog": "Event Log",
"navBar.orderLog": "Instruction log", "navBar.orderLog": "Instruction log",
"navBar.dataDebug": "Data Debug",
"navBar.commandBuilder": "Command Builder",
"navBar.player": "Player", "navBar.player": "Player",
"common.add": "add", "common.add": "add",
"common.delete": "Delete", "common.delete": "Delete",
@ -240,6 +242,72 @@
"createBtn": "Create Device", "createBtn": "Create Device",
"content": "This feature requires login to use. Do you want to log in?" "content": "This feature requires login to use. Do you want to log in?"
}, },
"debug": {
"title": "Data Debug",
"send": "Send",
"reveiceType": "Reply",
"reportType": "Report",
"sendType": "Send",
"hex": "HEX",
"json": "JSON",
"ascii": "ASCII",
"inputPlaceholder": "Please enter the content to send",
"typePlaceholder": "Select format",
"analysis": "Parse",
"analysised": "Parsed",
"name": "Name",
"identifier": "Identifier",
"value": "Value",
"more": "More",
"quickCommand": "Quick Commands",
"addCommand": "Add Command",
"editCommand": "Edit Command",
"slaveAddress": "Slave Address",
"issueType": "Command Type",
"functionCode": "Function Code",
"startAddress": "Start Register Address",
"registerCount": "Register Count",
"coilCount": "Coil Count",
"writeCoilValue": "Coil Value",
"writeRegisterValue": "Register Value",
"registerValue": "Register Value",
"reportValue": "Report Value",
"coilValue": "Coil Value",
"generateCommand": "Generate",
"generatedCommand": "Generated Command",
"saveAsCommand": "Save As Command",
"copy": "Copy",
"cancel": "Cancel",
"confirm": "Confirm",
"commandName": "Command Name",
"inputCommandName": "Please enter command name",
"commandContent": "Command Content",
"commandScope": "Command Scope",
"productCommand": "Product Command (shared by same product)",
"deviceCommand": "Device Command (this device only)",
"deleteCommand": "Delete Command",
"deleteConfirm": "Are you sure to delete this command?",
"reportCommand": "Device Report",
"issueCommand": "Platform Issue",
"code01": "01 Read Coils",
"code02": "02 Read Discrete Inputs",
"code03": "03 Read Holding Registers",
"code04": "04 Read Input Registers",
"code05": "05 Write Single Coil",
"code06": "06 Write Single Register",
"code15": "15 Write Multiple Coils",
"code16": "16 Write Multiple Registers",
"sendEmpty": "Please enter the content to send",
"sendFail": "Send failed",
"analysisEmpty": "No data to parse",
"analysisFail": "Parse failed",
"generateFail": "Generate failed",
"saveSuccess": "Saved successfully",
"saveFail": "Save failed",
"deleteSuccess": "Deleted successfully",
"deleteFail": "Delete failed",
"copySuccess": "Copied successfully"
},
"about": { "about": {
"about": "about", "about": "about",
"authorQQ": "Author QQ : 164770707", "authorQQ": "Author QQ : 164770707",

View File

@ -46,6 +46,8 @@
"navBar.updateDevice": "设备修改", "navBar.updateDevice": "设备修改",
"navBar.eventLog": "事件日志", "navBar.eventLog": "事件日志",
"navBar.orderLog": "指令日志", "navBar.orderLog": "指令日志",
"navBar.dataDebug": "数据调试",
"navBar.commandBuilder": "指令生成",
"navBar.player": "播放器", "navBar.player": "播放器",
"common.fastbee": "岩安测控", "common.fastbee": "岩安测控",
"common.add": "添加", "common.add": "添加",
@ -241,6 +243,72 @@
"createBtn": "创建设备", "createBtn": "创建设备",
"content": "该功能需要登录才可使用,是否去登录?" "content": "该功能需要登录才可使用,是否去登录?"
}, },
"debug": {
"title": "数据调试",
"send": "发送",
"reveiceType": "回复",
"reportType": "上报",
"sendType": "下发",
"hex": "HEX",
"json": "JSON",
"ascii": "ASCII",
"inputPlaceholder": "请输入要发送的内容",
"typePlaceholder": "选择格式",
"analysis": "解析",
"analysised": "已解析",
"name": "参数名",
"identifier": "标识符",
"value": "参数值",
"more": "更多",
"quickCommand": "快捷指令",
"addCommand": "新建指令",
"editCommand": "编辑指令",
"slaveAddress": "从机地址",
"issueType": "指令类型",
"functionCode": "功能码",
"startAddress": "起始寄存器地址",
"registerCount": "寄存器个数",
"coilCount": "线圈个数",
"writeCoilValue": "线圈值",
"writeRegisterValue": "寄存器值",
"registerValue": "寄存器值",
"reportValue": "上报值",
"coilValue": "线圈值",
"generateCommand": "生成指令",
"generatedCommand": "生成的指令",
"saveAsCommand": "存为指令",
"copy": "复制",
"cancel": "取消",
"confirm": "确定",
"commandName": "指令名称",
"inputCommandName": "请输入指令名称",
"commandContent": "指令内容",
"commandScope": "指令范围",
"productCommand": "产品指令(同类产品通用)",
"deviceCommand": "设备指令(仅本设备)",
"deleteCommand": "删除指令",
"deleteConfirm": "确认删除该指令吗?",
"reportCommand": "设备上报",
"issueCommand": "平台下发",
"code01": "01 读线圈",
"code02": "02 读离散输入",
"code03": "03 读保持寄存器",
"code04": "04 读输入寄存器",
"code05": "05 写单个线圈",
"code06": "06 写单个寄存器",
"code15": "15 写多个线圈",
"code16": "16 写多个寄存器",
"sendEmpty": "请输入要发送的内容",
"sendFail": "发送失败",
"analysisEmpty": "暂无可解析的数据",
"analysisFail": "解析失败",
"generateFail": "生成失败",
"saveSuccess": "保存成功",
"saveFail": "保存失败",
"deleteSuccess": "删除成功",
"deleteFail": "删除失败",
"copySuccess": "复制成功"
},
"about": { "about": {
"about": "关于", "about": "关于",
"authorQQ": "作者企鹅:164770707", "authorQQ": "作者企鹅:164770707",

View File

@ -533,6 +533,26 @@
"enablePullDownRefresh": true "enablePullDownRefresh": true
} }
}, },
{
"path": "home/device/debug/index",
"meta": {
"title": "%navBar.dataDebug%"
},
"style": {
"navigationBarTitleText": "%navBar.dataDebug%",
"enablePullDownRefresh": false
}
},
{
"path": "home/device/debug/edit",
"meta": {
"title": "%navBar.commandBuilder%"
},
"style": {
"navigationBarTitleText": "%navBar.commandBuilder%",
"enablePullDownRefresh": false
}
},
{ {
"path": "login/register", "path": "login/register",
"style": { "style": {

View File

@ -242,7 +242,8 @@
pageSize: 10, pageSize: 10,
groupId: 0, groupId: 0,
deviceName: null, deviceName: null,
showChild: true showChild: true,
entrance: 2
}, },
loadStatus: 'loadmore', // 加载更多 loadStatus: 'loadmore', // 加载更多
deviceList: [], // 设备列表 deviceList: [], // 设备列表

View File

@ -331,12 +331,17 @@
this.deviceForm.rssid = message.rssid; this.deviceForm.rssid = message.rssid;
} }
}; };
//不是modbus不转发到子页面,其他设备的页面有回调方法 /*发送设备上报到数据调试页面*/
if (this.isSubDev) { if (topic.endsWith('ws/service')) {
/*发送设备上报到子模块*/ console.log('接收到【设备数据上报】主题:', topic)
if (topic.endsWith('ws/service')) { console.log('接收到【设备数据上报】主题:', message)
console.log('接收到【设备数据上报】主题:', topic) this.$bus.$emit("updateData", {
console.log('接收到【设备数据上报】主题:', message) serialNumber: topics[2],
productId: this.deviceForm.productId,
data: message,
})
//不是modbus不转发到子页面,其他设备的页面有回调方法
if (this.isSubDev) {
this.$bus.$emit("updateDeviceStatus", { this.$bus.$emit("updateDeviceStatus", {
serialNumber: topics[2], serialNumber: topics[2],
productId: this.deviceForm.productId, productId: this.deviceForm.productId,

View File

@ -6,6 +6,8 @@
@click="handleEventLogClick"></u-cell> @click="handleEventLogClick"></u-cell>
<u-cell icon="bookmark" :title="$tt('home.Instructionlog')" :isLink="true" size="large" <u-cell icon="bookmark" :title="$tt('home.Instructionlog')" :isLink="true" size="large"
@click="handleOrderLogClick"></u-cell> @click="handleOrderLogClick"></u-cell>
<u-cell icon="server-man" :title="$tt('debug.title')" :isLink="true" size="large"
@click="handleDebugClick"></u-cell>
</u-cell-group> </u-cell-group>
</view> </view>
</view> </view>
@ -58,6 +60,17 @@
uni.navigateTo({ uni.navigateTo({
url: `/pagesB/home/device/log/order?deviceId=${this.deviceId}&serialNumber=${this.serialNumber}` url: `/pagesB/home/device/log/order?deviceId=${this.deviceId}&serialNumber=${this.serialNumber}`
}); });
},
handleDebugClick () {
const device = {
deviceId: this.deviceInfo.deviceId,
serialNumber: this.deviceInfo.serialNumber,
productId: this.deviceInfo.productId,
protocolCode: this.deviceInfo.protocolCode,
};
uni.navigateTo({
url: `/pagesB/home/device/debug/index?device=${encodeURIComponent(JSON.stringify(device))}`
});
} }
} }
}; };

View File

@ -0,0 +1,615 @@
<template>
<view class="edit-wrap">
<!-- 表单区域 -->
<view class="form-section">
<u-cell-group :border="false">
<!-- 从机地址 -->
<u-cell :title="$tt('debug.slaveAddress')" required>
<u--input slot="value" v-model="form.slaveId" type="number" border="none" placeholder="01" inputAlign="right"></u--input>
</u-cell>
<!-- 指令类型 -->
<u-cell :title="$tt('debug.issueType')" required isLink @click="showIssueTypeSheet = true">
<text slot="value">{{currentIssueTypeLabel}}</text>
</u-cell>
<!-- 功能码 -->
<u-cell :title="$tt('debug.functionCode')" required isLink @click="showFunctionCodeSheet = true" :disabled="form.issueType === '0xFFAA'">
<text slot="value">{{currentFunctionCodeLabel}}</text>
</u-cell>
<!-- 起始地址 -->
<u-cell :title="$tt('debug.startAddress')" required>
<view slot="value" class="hex-input-wrap">
<input v-model="form.startAddress" type="number" class="input" @input="syncStartAddressHex" />
<text class="hex-hint">0x{{form.startAddressHex}}</text>
</view>
</u-cell>
<!-- 个数(根据功能码显示) -->
<u-cell :title="countLabel" v-if="showCount">
<u-number-box slot="value" v-model="form.count" :min="1" :max="100"></u-number-box>
</u-cell>
<!-- 写值(05/06功能码) -->
<u-cell :title="writeValueLabel" v-if="showWriteValue">
<view slot="value" class="hex-input-wrap">
<input v-model="form.writeValue" type="number" class="input" @input="syncWriteValueHex" />
<text class="hex-hint">0x{{form.writeValueHex}}</text>
</view>
</u-cell>
</u-cell-group>
<!-- 批量寄存器值(功能码16) -->
<view class="batch-section" v-if="showRegisterValues">
<view class="batch-title">{{$tt('debug.registerValue')}}</view>
<view class="batch-list">
<view class="batch-item" v-for="(item, index) in registerValues" :key="index">
<text class="batch-label">#{{index}}</text>
<input v-model="item.value" type="number" class="batch-input" @input="syncRegisterHex(index)" />
<text class="hex-hint">0x{{item.valueHex}}</text>
</view>
</view>
</view>
<!-- 批量线圈值(功能码15) -->
<view class="batch-section" v-if="showCoilValues">
<view class="batch-title">{{$tt('debug.coilValue')}}</view>
<view class="batch-list">
<view class="batch-item" v-for="(item, index) in coilValues" :key="index">
<text class="batch-label">#{{index}}</text>
<u-switch v-model="item.value" activeValue="1" inactiveValue="0"></u-switch>
</view>
</view>
</view>
<!-- 批量上报值(0xFFAA) -->
<view class="batch-section" v-if="showReportValues">
<view class="batch-title">{{$tt('debug.reportValue')}}</view>
<view class="batch-list">
<view class="batch-item" v-for="(item, index) in reportValues" :key="index">
<text class="batch-label">#{{index}}</text>
<input v-model="item.value" type="number" class="batch-input" @input="syncReportHex(index)" />
<text class="hex-hint">0x{{item.valueHex}}</text>
</view>
</view>
</view>
</view>
<!-- 生成结果 -->
<view class="result-section">
<view class="result-header">
<text class="result-title">{{$tt('debug.generatedCommand')}}</text>
<view class="result-actions">
<text class="action-btn" @click="handleSave">{{$tt('debug.saveAsCommand')}}</text>
<text class="action-btn copy" @click="handleCopy">{{$tt('debug.copy')}}</text>
</view>
</view>
<view class="result-content" v-if="generatedCommand">
<text>{{generatedCommand}}</text>
</view>
<view class="result-empty" v-else>
<text>点击下方按钮生成指令</text>
</view>
</view>
<!-- 底部按钮 -->
<view class="bottom-btns">
<u-button type="default" @click="handleCancel">{{$tt('debug.cancel')}}</u-button>
<u-button type="primary" @click="handleGenerate" :loading="generating">{{$tt('debug.generateCommand')}}</u-button>
</view>
<!-- 指令类型选择 -->
<u-action-sheet :show="showIssueTypeSheet" :actions="issueTypeActions" @select="onIssueTypeSelect"
@close="showIssueTypeSheet = false"></u-action-sheet>
<!-- 功能码选择 -->
<u-action-sheet :show="showFunctionCodeSheet" :actions="functionCodeActions" @select="onFunctionCodeSelect"
@close="showFunctionCodeSheet = false"></u-action-sheet>
<!-- 保存弹窗 -->
<u-popup :show="showSavePopup" mode="bottom" :round="10" @close="showSavePopup = false">
<view class="save-popup">
<view class="popup-title">{{$tt('debug.saveAsCommand')}}</view>
<u-cell-group :border="false">
<u-cell :title="$tt('debug.commandName')" required>
<u--input slot="value" v-model="saveForm.name" :placeholder="$tt('debug.inputCommandName')" border="none" inputAlign="right"></u--input>
</u-cell>
<u-cell :title="$tt('debug.commandContent')">
<text slot="value" class="cmd-text">{{saveForm.command}}</text>
</u-cell>
<u-cell :title="$tt('debug.commandScope')">
<u-radio-group slot="value" v-model="saveForm.isProductCommand" placement="row">
<u-radio :name="true" :label="$tt('debug.productCommand')" iconSize="16"></u-radio>
<u-radio :name="false" :label="$tt('debug.deviceCommand')" iconSize="16"></u-radio>
</u-radio-group>
</u-cell>
</u-cell-group>
<view class="popup-btns">
<u-button type="default" @click="showSavePopup = false">{{$tt('debug.cancel')}}</u-button>
<u-button type="primary" @click="handleConfirmSave" :loading="saving">{{$tt('debug.confirm')}}</u-button>
</view>
</view>
</u-popup>
</view>
</template>
<script>
import { int2hex } from '@/utils/common.js';
import { encode, addPreferences, editPreferences } from '@/apis/modules/mqttTest.js';
export default {
data() {
return {
device: {},
editItem: null,
form: {
slaveId: '01',
issueType: '0xFFDD',
functionCode: '01',
startAddress: '0',
startAddressHex: '0000',
count: 1,
writeValue: '0',
writeValueHex: '0000'
},
registerValues: [],
coilValues: [],
reportValues: [],
showIssueTypeSheet: false,
showFunctionCodeSheet: false,
issueTypeActions: [
{ name: this.$tt('debug.issueCommand'), value: '0xFFDD' },
{ name: this.$tt('debug.reportCommand'), value: '0xFFAA' }
],
functionCodeActions: [
{ name: this.$tt('debug.code01'), value: '01' },
{ name: this.$tt('debug.code02'), value: '02' },
{ name: this.$tt('debug.code03'), value: '03' },
{ name: this.$tt('debug.code04'), value: '04' },
{ name: this.$tt('debug.code05'), value: '05' },
{ name: this.$tt('debug.code06'), value: '06' },
{ name: this.$tt('debug.code15'), value: '15' },
{ name: this.$tt('debug.code16'), value: '16' }
],
generatedCommand: '',
generating: false,
showSavePopup: false,
saveForm: {
id: null,
name: '',
command: '',
isProductCommand: false
},
saving: false
};
},
computed: {
currentIssueTypeLabel() {
const item = this.issueTypeActions.find(i => i.value === this.form.issueType);
return item ? item.name : this.$tt('debug.issueCommand');
},
currentFunctionCodeLabel() {
const item = this.functionCodeActions.find(i => i.value === this.form.functionCode);
return item ? item.name : this.$tt('debug.code01');
},
showCount() {
if (this.form.issueType === '0xFFAA') return false;
return ['01', '02', '03', '04', '15', '16'].includes(this.form.functionCode);
},
showWriteValue() {
if (this.form.issueType === '0xFFAA') return false;
return ['05', '06'].includes(this.form.functionCode);
},
showRegisterValues() {
if (this.form.issueType === '0xFFAA') return false;
return this.form.functionCode === '16';
},
showCoilValues() {
if (this.form.issueType === '0xFFAA') return false;
return this.form.functionCode === '15';
},
showReportValues() {
return this.form.issueType === '0xFFAA';
},
countLabel() {
switch (this.form.functionCode) {
case '01':
case '02':
case '15':
return this.$tt('debug.coilCount');
default:
return this.$tt('debug.registerCount');
}
},
writeValueLabel() {
return this.form.functionCode === '05' ? this.$tt('debug.writeCoilValue') : this.$tt('debug.writeRegisterValue');
}
},
watch: {
'form.count'() {
this.updateBatchValues();
},
'form.issueType'(val) {
if (val === '0xFFAA') {
this.form.functionCode = '03';
}
this.updateBatchValues();
},
'form.functionCode'() {
this.updateBatchValues();
}
},
onLoad(option) {
if (option.device) {
this.device = JSON.parse(decodeURIComponent(option.device));
}
if (option.item) {
this.editItem = JSON.parse(decodeURIComponent(option.item));
this.saveForm.id = this.editItem.id;
this.saveForm.name = this.editItem.name;
this.saveForm.command = this.editItem.command;
this.saveForm.isProductCommand = this.editItem.productId != null;
this.generatedCommand = this.editItem.command;
}
this.updateBatchValues();
},
methods: {
// 更新批量值数组
updateBatchValues() {
// 批量寄存器值
if (this.showRegisterValues) {
const len = this.form.count;
while (this.registerValues.length < len) {
this.registerValues.push({ value: '0', valueHex: '0000' });
}
while (this.registerValues.length > len) {
this.registerValues.pop();
}
} else {
this.registerValues = [];
}
// 批量线圈值
if (this.showCoilValues) {
const len = this.form.count;
while (this.coilValues.length < len) {
this.coilValues.push({ value: '0' });
}
while (this.coilValues.length > len) {
this.coilValues.pop();
}
} else {
this.coilValues = [];
}
// 批量上报值
if (this.showReportValues) {
const len = this.form.count;
while (this.reportValues.length < len) {
this.reportValues.push({ value: '0', valueHex: '0000' });
}
while (this.reportValues.length > len) {
this.reportValues.pop();
}
} else {
this.reportValues = [];
}
},
// 同步起始地址的十六进制值
syncStartAddressHex() {
this.form.startAddressHex = int2hex(this.form.startAddress);
},
// 同步写值的十六进制值
syncWriteValueHex() {
this.form.writeValueHex = int2hex(this.form.writeValue);
},
// 同步寄存器值的十六进制
syncRegisterHex(index) {
const item = this.registerValues[index];
item.valueHex = int2hex(item.value);
this.$set(this.registerValues, index, item);
},
// 同步上报值的十六进制
syncReportHex(index) {
const item = this.reportValues[index];
item.valueHex = int2hex(item.value);
this.$set(this.reportValues, index, item);
},
// 选择指令类型
onIssueTypeSelect(item) {
this.form.issueType = item.value;
this.showIssueTypeSheet = false;
},
// 选择功能码
onFunctionCodeSelect(item) {
this.form.functionCode = item.value;
this.showFunctionCodeSheet = false;
},
// 生成指令
handleGenerate() {
this.generating = true;
const params = {
slaveId: parseInt(this.form.slaveId) || 1,
address: parseInt(this.form.startAddress) || 0,
code: parseInt(this.form.functionCode),
start: this.form.issueType,
protocolCode: this.device.protocolCode,
serialNumber: this.device.serialNumber
};
// 上报模式
if (this.form.issueType === '0xFFAA') {
params.address = parseInt(this.form.startAddress) || 0;
params.bitCount = this.form.count * 2;
params.data = this.reportValues.map(i => parseInt(i.value) || 0);
} else {
switch (this.form.functionCode) {
case '01':
case '02':
case '03':
case '04':
params.count = this.form.count;
break;
case '05':
case '06':
params.writeData = parseInt(this.form.writeValue) || 0;
break;
case '15':
params.count = this.form.count;
params.bitString = this.coilValues.map(i => i.value).join('');
break;
case '16':
params.count = this.form.count;
params.tenWriteData = this.registerValues.map(i => parseInt(i.value) || 0);
break;
}
}
encode(params).then(res => {
if (res.code === 200) {
this.generatedCommand = res.msg;
uni.showToast({ icon: 'success', title: this.$tt('debug.generateCommand') + '成功' });
} else {
uni.showToast({ icon: 'none', title: res.msg || this.$tt('debug.generateFail') });
}
}).catch(err => {
console.log(err);
uni.showToast({ icon: 'none', title: this.$tt('debug.generateFail') });
}).finally(() => {
this.generating = false;
});
},
// 保存为快捷指令
handleSave() {
if (!this.generatedCommand) {
uni.showToast({ icon: 'none', title: '请先生成指令' });
return;
}
this.saveForm.command = this.generatedCommand;
this.showSavePopup = true;
},
// 确认保存
handleConfirmSave() {
if (!this.saveForm.name) {
uni.showToast({ icon: 'none', title: this.$tt('debug.inputCommandName') });
return;
}
this.saving = true;
const data = {
command: this.saveForm.command,
name: this.saveForm.name,
serialNumber: this.saveForm.isProductCommand ? null : this.device.serialNumber,
productId: this.saveForm.isProductCommand ? this.device.productId : null
};
const apiCall = this.saveForm.id ? editPreferences({ ...data, id: this.saveForm.id }) : addPreferences(data);
apiCall.then(res => {
if (res.code === 200) {
uni.showToast({ icon: 'success', title: this.$tt('debug.saveSuccess') });
this.showSavePopup = false;
uni.navigateBack();
} else {
uni.showToast({ icon: 'none', title: res.msg || this.$tt('debug.saveFail') });
}
}).catch(err => {
console.log(err);
uni.showToast({ icon: 'none', title: this.$tt('debug.saveFail') });
}).finally(() => {
this.saving = false;
});
},
// 复制指令
handleCopy() {
if (!this.generatedCommand) {
uni.showToast({ icon: 'none', title: '请先生成指令' });
return;
}
uni.setClipboardData({
data: this.generatedCommand,
success: () => {
uni.showToast({ icon: 'success', title: this.$tt('debug.copySuccess') });
}
});
},
// 取消
handleCancel() {
uni.navigateBack();
}
}
};
</script>
<style>
page {
background: #eef3f7;
}
</style>
<style lang="scss" scoped>
.edit-wrap {
padding-bottom: 140rpx;
.form-section {
margin: 20rpx;
background: #fff;
border-radius: 10rpx;
overflow: hidden;
}
.hex-input-wrap {
display: flex;
align-items: center;
.input {
width: 120rpx;
text-align: right;
font-size: 28rpx;
}
.hex-hint {
margin-left: 12rpx;
font-size: 24rpx;
color: #909399;
font-family: 'Courier New', monospace;
}
}
.batch-section {
margin: 20rpx;
padding: 20rpx;
background: #fff;
border-radius: 10rpx;
.batch-title {
font-size: 28rpx;
font-weight: 500;
color: #303133;
margin-bottom: 16rpx;
}
.batch-list {
.batch-item {
display: flex;
align-items: center;
padding: 16rpx 0;
border-bottom: 1rpx solid #f4f4f5;
&:last-child {
border-bottom: none;
}
.batch-label {
font-size: 26rpx;
color: #606266;
min-width: 80rpx;
}
.batch-input {
flex: 1;
text-align: right;
font-size: 26rpx;
}
.hex-hint {
margin-left: 12rpx;
font-size: 24rpx;
color: #909399;
font-family: 'Courier New', monospace;
min-width: 100rpx;
}
}
}
}
.result-section {
margin: 20rpx;
padding: 20rpx;
background: #fff;
border-radius: 10rpx;
.result-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 16rpx;
.result-title {
font-size: 28rpx;
font-weight: 500;
color: #303133;
}
.result-actions {
.action-btn {
font-size: 26rpx;
color: #409eff;
margin-left: 24rpx;
&.copy {
color: #67c23a;
}
}
}
}
.result-content {
padding: 20rpx;
background: #f4f4f5;
border-radius: 8rpx;
word-wrap: break-word;
text {
font-size: 28rpx;
font-weight: 600;
color: #303133;
font-family: 'Courier New', monospace;
}
}
.result-empty {
padding: 20rpx;
text-align: center;
text {
font-size: 26rpx;
color: #909399;
}
}
}
.bottom-btns {
position: fixed;
bottom: 0;
left: 0;
right: 0;
display: flex;
padding: 20rpx;
padding-bottom: calc(20rpx + env(safe-area-inset-bottom));
background: #fff;
border-top: 1rpx solid #eee;
z-index: 100;
}
.save-popup {
padding: 30rpx;
.popup-title {
font-size: 32rpx;
font-weight: 500;
text-align: center;
margin-bottom: 20rpx;
}
.cmd-text {
font-size: 24rpx;
color: #909399;
word-break: break-all;
}
.popup-btns {
display: flex;
margin-top: 30rpx;
}
}
}
</style>

View File

@ -0,0 +1,655 @@
<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>

View File

@ -36,3 +36,129 @@ export function navigateBackTo (url) {
const backnum = num - index - 1; const backnum = num - index - 1;
uni.navigateBack({ delta: backnum }); uni.navigateBack({ delta: backnum });
} }
/**
* 深复制
* @param {any} obj 需要复制的对象
*/
export function deepClone (obj) {
if (typeof obj == 'object') {
const objClone = Array.isArray(obj) ? [] : {}
for (const key in obj) {
if (Object.prototype.hasOwnProperty.call(obj, key)) {
if (obj[key] && typeof obj[key] == 'object') {
objClone[key] = deepClone(obj[key])
} else {
objClone[key] = obj[key]
}
}
}
return objClone
}
return obj
}
/**
* @desc 时间戳转日期毫秒
* @param {Date} format 时间格式 Y:年,M:月,D:天 h:小时,m:分钟,s:秒 默认:'Y/M/D h:m'
*/
export function formatDate (arg, format) {
let now
if (!arg) {
return null
} else {
now = new Date(arg)
}
format = format || 'Y.M.D h:m'
const year = now.getFullYear()
let month = now.getMonth() + 1
month = month >= 10 ? month : '0' + month
let date = now.getDate()
date = date >= 10 ? date : '0' + date
let hour = now.getHours()
hour = hour >= 10 ? hour : '0' + hour
let minute = now.getMinutes()
minute = minute >= 10 ? minute : '0' + minute
let second = now.getSeconds()
second = second >= 10 ? second : '0' + second
return format
.replace('Y', year)
.replace('M', month)
.replace('D', date)
.replace('h', hour)
.replace('m', minute)
.replace('s', second)
}
/**
* 复制文本(uni-app 剪贴板)
* @param {string} copytext
*/
export function copyText (copytext) {
uni.setClipboardData({
data: copytext,
success: () => {},
})
return { type: 'success', message: '复制成功' }
}
/**
* hex转10进制
* @param hexStr
*/
export function hex2int (hexStr) {
let twoStr = parseInt(hexStr, 16).toString(2) // 将十六转十进制,再转2进制
let bitNum = hexStr.length * 4 // 1个字节 = 8bit ,0xff 一个 "f"就是4位
if (twoStr.length < bitNum) {
while (twoStr.length < bitNum) {
twoStr = "0" + twoStr
}
}
if (twoStr.substring(0, 1) == "0") {
// 正数
const two = parseInt(twoStr, 2) // 二进制转十进制
return two
} else {
// 负数
let twoStr_unsign = ""
let two = parseInt(twoStr, 2) - 1 // 补码:(负数)反码+1,符号位不变;相对十进制来说也是 +1,但这里是负数,+1就是绝对值数据-1
twoStr = two.toString(2)
twoStr_unsign = twoStr.substring(1, bitNum) // 舍弃首位(符号位)
// 去除首字符,将0转为1,将1转为0 反码
twoStr_unsign = twoStr_unsign.replace(/0/g, "z")
twoStr_unsign = twoStr_unsign.replace(/1/g, "0")
twoStr_unsign = twoStr_unsign.replace(/z/g, "1")
two = -parseInt(twoStr_unsign, 2)
return two
}
}
/**
* 10进制转16进制
* @param hexStr
*/
export function int2hex (hexStr) {
let hexInt = typeof hexStr == 'string' ? parseInt(hexStr, 10) : hexStr
let hex = ''
const LENGTH_04 = 4
const LENGTH_15 = 15
if (hexInt >= 0) {
hex = hexInt.toString(16).toLocaleLowerCase()
} else {
let twoStr = (-hexInt - 1).toString(2)
let sb = '000000000000000'
twoStr = sb.slice(0, LENGTH_15 - twoStr.length) + twoStr
// 将0转为1,将1转为0 反码
twoStr = twoStr.replace(/0/g, "z")
twoStr = twoStr.replace(/1/g, "0")
twoStr = twoStr.replace(/z/g, "1")
hex = parseInt(('1' + twoStr), 2).toString(16).toLocaleLowerCase()
}
// 长度不等于4 补全
if (hex.length != LENGTH_04) {
let sb = "0000"
hex = sb.slice(0, LENGTH_04 - hex.length) + hex
}
return hex.toUpperCase()
}