752 lines
18 KiB
Vue
752 lines
18 KiB
Vue
<template>
|
||
<view class="container">
|
||
<!-- 连接配置区域 -->
|
||
<view class="config-section">
|
||
<view class="section-title">MQTT连接配置</view>
|
||
<form @submit="handleConnect">
|
||
<view class="form-item">
|
||
<label>服务器地址:</label>
|
||
<view class="input-wrapper">
|
||
<input
|
||
v-model="mqttConfig.broker"
|
||
type="text"
|
||
placeholder="如:ws://test.mosquitto.org:8080/mqtt"
|
||
class="form-input"
|
||
/>
|
||
<text class="clear-icon" @click="clearInput('broker')" v-if="mqttConfig.broker">×</text>
|
||
</view>
|
||
</view>
|
||
<view class="form-item" v-if="false">
|
||
<label>客户端ID:</label>
|
||
<view class="input-wrapper">
|
||
<input
|
||
v-model="mqttConfig.clientId"
|
||
type="text"
|
||
placeholder="自动生成或自定义"
|
||
class="form-input"
|
||
/>
|
||
<text class="clear-icon" @click="clearInput('clientId')" v-if="mqttConfig.clientId">×</text>
|
||
</view>
|
||
</view>
|
||
<view class="form-item">
|
||
<label>用户名:</label>
|
||
<view class="input-wrapper">
|
||
<input
|
||
v-model="mqttConfig.username"
|
||
type="text"
|
||
placeholder="可选"
|
||
class="form-input"
|
||
/>
|
||
<text class="clear-icon" @click="clearInput('username')" v-if="mqttConfig.username">×</text>
|
||
</view>
|
||
</view>
|
||
<view class="form-item">
|
||
<label>密码:</label>
|
||
<view class="input-wrapper">
|
||
<input
|
||
v-model="mqttConfig.password"
|
||
type="password"
|
||
placeholder="可选"
|
||
class="form-input"
|
||
/>
|
||
<text class="clear-icon" @click="clearInput('password')" v-if="mqttConfig.password">×</text>
|
||
</view>
|
||
</view>
|
||
<view class="form-item" v-if="false">
|
||
<label>连接超时:</label>
|
||
<view class="input-wrapper">
|
||
<input
|
||
v-model.number="mqttConfig.timeout"
|
||
type="number"
|
||
placeholder="默认30秒"
|
||
class="form-input"
|
||
/>
|
||
<text class="clear-icon" @click="clearInput('timeout')" v-if="mqttConfig.timeout">×</text>
|
||
</view>
|
||
</view>
|
||
|
||
<view class="btn-group">
|
||
<button
|
||
type="primary"
|
||
:disabled="isConnected"
|
||
@click="handleConnect"
|
||
>
|
||
连接
|
||
</button>
|
||
<button
|
||
type="warn"
|
||
:disabled="!isConnected"
|
||
@click="handleDisconnect"
|
||
>
|
||
断开连接
|
||
</button>
|
||
</view>
|
||
</form>
|
||
</view>
|
||
|
||
<!-- 订阅区域 -->
|
||
<view class="subscribe-section" v-if="isConnected">
|
||
<view class="section-title">主题订阅</view>
|
||
<view class="form-item">
|
||
<label>订阅主题:</label>
|
||
<view class="input-wrapper">
|
||
<input
|
||
v-model="subscribeTopic"
|
||
type="text"
|
||
placeholder="如:test/topic"
|
||
class="form-input"
|
||
/>
|
||
<text class="clear-icon" @click="subscribeTopic = ''" v-if="subscribeTopic">×</text>
|
||
</view>
|
||
</view>
|
||
<button
|
||
type="primary"
|
||
@click="handleSubscribe"
|
||
>
|
||
订阅
|
||
</button>
|
||
|
||
<!-- 已订阅主题列表 -->
|
||
<view class="topic-list" v-if="subscribedTopics.length">
|
||
<view class="list-title">已订阅主题:</view>
|
||
<view
|
||
class="topic-item"
|
||
v-for="(topic, index) in subscribedTopics"
|
||
:key="index"
|
||
>
|
||
{{topic}}
|
||
<button
|
||
size="mini"
|
||
type="warn"
|
||
@click="handleUnsubscribe(topic)"
|
||
>
|
||
取消订阅
|
||
</button>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
|
||
|
||
<!-- 消息日志区域 -->
|
||
<view class="log-section">
|
||
<view class="section-title">消息日志</view>
|
||
|
||
<!-- 日志过滤和清空区域 -->
|
||
<view class="log-filter-wrapper">
|
||
<!-- 主题过滤输入框 -->
|
||
<view class="input-wrapper filter-input">
|
||
<input
|
||
v-model="logFilterKeyword"
|
||
type="text"
|
||
placeholder="输入主题关键词过滤日志(模糊匹配)"
|
||
class="form-input"
|
||
/>
|
||
<text class="clear-icon" @click="logFilterKeyword = ''" v-if="logFilterKeyword">×</text>
|
||
</view>
|
||
|
||
<!-- 清空日志按钮 -->
|
||
<button
|
||
size="mini"
|
||
type="warn"
|
||
@click="clearAllLogs"
|
||
:disabled="messageLogs.length === 0"
|
||
class="clear-log-btn"
|
||
>
|
||
清空日志
|
||
</button>
|
||
</view>
|
||
|
||
<!-- 日志内容区域 -->
|
||
<scroll-view
|
||
class="log-content"
|
||
scroll-y="true"
|
||
:style="{height: logHeight + 'px'}"
|
||
>
|
||
<!-- 空日志提示 -->
|
||
<view class="empty-log-tip" v-if="filteredLogs.length === 0">
|
||
{{messageLogs.length === 0 ? '暂无消息日志' : '未找到匹配的日志内容'}}
|
||
</view>
|
||
|
||
<!-- 过滤后的日志列表 -->
|
||
<view
|
||
class="log-item"
|
||
v-for="(log, index) in filteredLogs"
|
||
:key="index"
|
||
:class="{'received': log.type === 'received', 'sent': log.type === 'sent', 'system': log.type === 'system'}"
|
||
>
|
||
<view class="log-time">{{log.time}}</view>
|
||
<view class="log-type">{{log.type === 'received' ? '接收' : log.type === 'sent' ? '发送' : '系统'}}</view>
|
||
<!-- 主题区域 - 支持横向滚动 -->
|
||
<view class="log-row">
|
||
<text class="log-label">主题:</text>
|
||
<view class="log-content-scroll">
|
||
{{log.topic}}
|
||
</view>
|
||
</view>
|
||
<!-- 消息内容区域 - 支持横向滚动 -->
|
||
<view class="log-row">
|
||
<text class="log-label">内容:</text>
|
||
<view class="log-content-scroll">
|
||
{{log.message}}
|
||
</view>
|
||
</view>
|
||
</view>
|
||
</scroll-view>
|
||
</view>
|
||
|
||
<!-- 发布消息区域 -->
|
||
<view class="publish-section" v-if="isConnected">
|
||
<view class="section-title">发布消息</view>
|
||
<view class="form-item">
|
||
<label>发布主题:</label>
|
||
<view class="input-wrapper">
|
||
<input
|
||
v-model="publishTopic"
|
||
type="text"
|
||
placeholder="如:test/topic"
|
||
class="form-input"
|
||
/>
|
||
<text class="clear-icon" @click="publishTopic = ''" v-if="publishTopic">×</text>
|
||
</view>
|
||
</view>
|
||
<view class="form-item">
|
||
<label>消息内容:</label>
|
||
<view class="textarea-wrapper">
|
||
<textarea
|
||
v-model="publishMessage"
|
||
placeholder="输入要发布的消息内容"
|
||
class="form-textarea"
|
||
></textarea>
|
||
<text class="clear-icon textarea-clear" @click="publishMessage = ''" v-if="publishMessage">×</text>
|
||
</view>
|
||
</view>
|
||
<button
|
||
type="primary"
|
||
@click="handlePublish"
|
||
>
|
||
发布消息
|
||
</button>
|
||
</view>
|
||
</view>
|
||
</template>
|
||
|
||
<script>
|
||
import mqtt from 'mqtt/dist/mqtt'
|
||
|
||
export default {
|
||
data() {
|
||
return {
|
||
// MQTT配置
|
||
mqttConfig: {
|
||
broker: 'wxs://mq.xiaoces.com:443/mqtt', // 公共测试服务器
|
||
clientId: 'uniapp_mqtt_' + Math.random().toString(16).substr(2, 8),
|
||
username: 'admin',
|
||
password: 'Admin#12345678',
|
||
timeout: 30
|
||
},
|
||
// 连接状态
|
||
isConnected: false,
|
||
client: null,
|
||
// 订阅相关
|
||
subscribeTopic: '',
|
||
subscribedTopics: [],
|
||
// 发布相关
|
||
publishTopic: '',
|
||
publishMessage: '',
|
||
// 消息日志
|
||
messageLogs: [],
|
||
logHeight: 300, // 日志区域高度
|
||
// 日志过滤
|
||
logFilterKeyword: '' // 日志过滤关键词
|
||
}
|
||
},
|
||
computed: {
|
||
// 过滤后的日志列表
|
||
filteredLogs() {
|
||
if (!this.logFilterKeyword) {
|
||
return this.messageLogs
|
||
}
|
||
// 模糊匹配主题(不区分大小写)
|
||
const keyword = this.logFilterKeyword.toLowerCase()
|
||
return this.messageLogs.filter(log => {
|
||
return log.topic.toLowerCase().includes(keyword)
|
||
})
|
||
}
|
||
},
|
||
onReady() {
|
||
// 计算日志区域高度
|
||
uni.getSystemInfo({
|
||
success: (res) => {
|
||
this.logHeight = res.windowHeight - 650 // 调整高度适配过滤栏
|
||
}
|
||
})
|
||
},
|
||
onUnload() {
|
||
// 页面卸载时断开连接
|
||
if (this.client && this.isConnected) {
|
||
console.info("mqtt链接已关闭")
|
||
this.client.end()
|
||
}
|
||
},
|
||
onHide() {
|
||
// 页面卸载时断开连接
|
||
if (this.client && this.isConnected) {
|
||
console.info("mqtt链接已关闭")
|
||
this.client.end()
|
||
}
|
||
},
|
||
methods: {
|
||
// 清除配置项输入框内容
|
||
clearInput(field) {
|
||
if (field === 'timeout') {
|
||
this.mqttConfig[field] = ''
|
||
} else {
|
||
this.$set(this.mqttConfig, field, '')
|
||
}
|
||
},
|
||
|
||
// 清空所有消息日志
|
||
clearAllLogs() {
|
||
uni.showModal({
|
||
title: '确认清空',
|
||
content: '是否确定清空所有消息日志?',
|
||
success: (res) => {
|
||
if (res.confirm) {
|
||
this.messageLogs = []
|
||
this.logFilterKeyword = '' // 清空过滤关键词
|
||
uni.showToast({
|
||
title: '日志已清空',
|
||
icon: 'success',
|
||
duration: 1500
|
||
})
|
||
}
|
||
}
|
||
})
|
||
},
|
||
|
||
// 添加日志
|
||
addLog(type, topic, message) {
|
||
const time = new Date().toLocaleTimeString()
|
||
this.messageLogs.unshift({
|
||
time,
|
||
type, // received/sent/system
|
||
topic,
|
||
message
|
||
})
|
||
// 限制日志数量
|
||
if (this.messageLogs.length > 100) {
|
||
this.messageLogs.pop()
|
||
}
|
||
},
|
||
|
||
// 连接MQTT服务器
|
||
handleConnect() {
|
||
if (!this.mqttConfig.broker) {
|
||
uni.showToast({
|
||
title: '请输入服务器地址',
|
||
icon: 'none'
|
||
})
|
||
return
|
||
}
|
||
|
||
try {
|
||
// 配置连接选项
|
||
const options = {
|
||
clientId: this.mqttConfig.clientId || 'uniapp_mqtt_' + Math.random().toString(16).substr(2, 8),
|
||
username: this.mqttConfig.username,
|
||
password: this.mqttConfig.password,
|
||
connectTimeout: (this.mqttConfig.timeout || 30) * 1000,
|
||
keepalive: 60,
|
||
clean: true,
|
||
reconnectPeriod: 0
|
||
}
|
||
|
||
// 创建客户端并连接
|
||
this.client = mqtt.connect(this.mqttConfig.broker, options)
|
||
|
||
// 连接成功回调
|
||
this.client.on('connect', () => {
|
||
this.isConnected = true
|
||
this.addLog('system', '', 'MQTT连接成功')
|
||
uni.showToast({
|
||
title: '连接成功',
|
||
icon: 'success'
|
||
})
|
||
})
|
||
|
||
// 接收消息回调
|
||
this.client.on('message', (topic, message) => {
|
||
this.addLog('received', topic, message.toString())
|
||
})
|
||
|
||
// 连接断开回调
|
||
this.client.on('close', () => {
|
||
this.isConnected = false
|
||
this.subscribedTopics = []
|
||
this.addLog('system', '', 'MQTT连接已断开')
|
||
})
|
||
|
||
// 错误回调
|
||
this.client.on('error', (error) => {
|
||
this.isConnected = false
|
||
this.addLog('system', '', '连接错误:' + error.message)
|
||
uni.showToast({
|
||
title: '连接失败:' + error.message,
|
||
icon: 'none',
|
||
duration: 3000
|
||
})
|
||
})
|
||
} catch (error) {
|
||
this.addLog('system', '', '连接异常:' + error.message)
|
||
uni.showToast({
|
||
title: '连接异常:' + error.message,
|
||
icon: 'none',
|
||
duration: 3000
|
||
})
|
||
}
|
||
},
|
||
|
||
// 断开连接
|
||
handleDisconnect() {
|
||
if (this.client) {
|
||
this.client.end()
|
||
this.isConnected = false
|
||
this.subscribedTopics = []
|
||
this.addLog('system', '', '已手动断开MQTT连接')
|
||
uni.showToast({
|
||
title: '已断开连接',
|
||
icon: 'success'
|
||
})
|
||
}
|
||
},
|
||
|
||
// 订阅主题
|
||
handleSubscribe() {
|
||
if (!this.subscribeTopic) {
|
||
uni.showToast({
|
||
title: '请输入订阅主题',
|
||
icon: 'none'
|
||
})
|
||
return
|
||
}
|
||
|
||
if (this.subscribedTopics.includes(this.subscribeTopic)) {
|
||
uni.showToast({
|
||
title: '该主题已订阅',
|
||
icon: 'none'
|
||
})
|
||
return
|
||
}
|
||
|
||
this.client.subscribe(this.subscribeTopic, (error) => {
|
||
if (error) {
|
||
this.addLog('system', this.subscribeTopic, '订阅失败:' + error.message)
|
||
uni.showToast({
|
||
title: '订阅失败',
|
||
icon: 'none'
|
||
})
|
||
} else {
|
||
this.subscribedTopics.push(this.subscribeTopic)
|
||
this.addLog('system', this.subscribeTopic, '订阅成功')
|
||
uni.showToast({
|
||
title: '订阅成功',
|
||
icon: 'success'
|
||
})
|
||
}
|
||
})
|
||
},
|
||
|
||
// 取消订阅
|
||
handleUnsubscribe(topic) {
|
||
this.client.unsubscribe(topic, (error) => {
|
||
if (error) {
|
||
this.addLog('system', topic, '取消订阅失败:' + error.message)
|
||
uni.showToast({
|
||
title: '取消订阅失败',
|
||
icon: 'none'
|
||
})
|
||
} else {
|
||
this.subscribedTopics = this.subscribedTopics.filter(item => item !== topic)
|
||
this.addLog('system', topic, '取消订阅成功')
|
||
uni.showToast({
|
||
title: '取消订阅成功',
|
||
icon: 'success'
|
||
})
|
||
}
|
||
})
|
||
},
|
||
|
||
// 发布消息
|
||
handlePublish() {
|
||
if (!this.publishTopic) {
|
||
uni.showToast({
|
||
title: '请输入发布主题',
|
||
icon: 'none'
|
||
})
|
||
return
|
||
}
|
||
|
||
if (!this.publishMessage) {
|
||
uni.showToast({
|
||
title: '请输入消息内容',
|
||
icon: 'none'
|
||
})
|
||
return
|
||
}
|
||
|
||
this.client.publish(this.publishTopic, this.publishMessage, (error) => {
|
||
if (error) {
|
||
this.addLog('system', this.publishTopic, '发布失败:' + error.message)
|
||
uni.showToast({
|
||
title: '发布失败',
|
||
icon: 'none'
|
||
})
|
||
} else {
|
||
this.addLog('sent', this.publishTopic, this.publishMessage)
|
||
uni.showToast({
|
||
title: '发布成功',
|
||
icon: 'success'
|
||
})
|
||
// 清空消息输入框
|
||
this.publishMessage = ''
|
||
}
|
||
})
|
||
}
|
||
}
|
||
}
|
||
</script>
|
||
|
||
<style scoped>
|
||
.container {
|
||
padding: 15px;
|
||
background-color: #f5f5f5;
|
||
min-height: 100vh;
|
||
}
|
||
|
||
.config-section, .subscribe-section, .publish-section, .log-section {
|
||
background-color: #fff;
|
||
border-radius: 8px;
|
||
padding: 15px;
|
||
margin-bottom: 15px;
|
||
}
|
||
|
||
.section-title {
|
||
font-size: 16px;
|
||
font-weight: bold;
|
||
color: #333;
|
||
border-bottom: 1px solid #eee;
|
||
padding-bottom: 8px;
|
||
margin-bottom: 15px;
|
||
}
|
||
|
||
/* 日志过滤和清空区域 */
|
||
.log-filter-wrapper {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 10px;
|
||
margin-bottom: 15px;
|
||
}
|
||
|
||
/* 过滤输入框 */
|
||
.filter-input {
|
||
flex: 1;
|
||
}
|
||
|
||
/* 清空日志按钮 */
|
||
.clear-log-btn {
|
||
height: 40px;
|
||
line-height: 40px;
|
||
padding: 0 15px;
|
||
white-space: nowrap;
|
||
}
|
||
|
||
.form-item {
|
||
display: flex;
|
||
flex-direction: column;
|
||
margin-bottom: 12px;
|
||
}
|
||
|
||
.form-item label {
|
||
font-size: 14px;
|
||
color: #666;
|
||
margin-bottom: 5px;
|
||
}
|
||
|
||
/* 输入框容器 - 包含清除按钮 */
|
||
.input-wrapper {
|
||
position: relative;
|
||
display: flex;
|
||
align-items: center;
|
||
}
|
||
|
||
.form-input {
|
||
flex: 1;
|
||
height: 40px;
|
||
border: 1px solid #ddd;
|
||
border-radius: 4px;
|
||
padding: 0 30px 0 10px; /* 右侧留出清除按钮空间 */
|
||
font-size: 14px;
|
||
/* 开启横向滚动 */
|
||
white-space: nowrap;
|
||
overflow-x: auto;
|
||
-webkit-overflow-scrolling: touch; /* 顺滑滚动 */
|
||
}
|
||
|
||
/* 文本域容器 */
|
||
.textarea-wrapper {
|
||
position: relative;
|
||
}
|
||
|
||
.form-textarea {
|
||
width: 100%;
|
||
border: 1px solid #ddd;
|
||
border-radius: 4px;
|
||
padding: 10px 30px 10px 10px; /* 右侧留出清除按钮空间 */
|
||
font-size: 14px;
|
||
min-height: 100px;
|
||
box-sizing: border-box;
|
||
}
|
||
|
||
/* 清除按钮样式 */
|
||
.clear-icon {
|
||
position: absolute;
|
||
right: 10px;
|
||
top: 50%;
|
||
transform: translateY(-50%);
|
||
width: 20px;
|
||
height: 20px;
|
||
line-height: 20px;
|
||
text-align: center;
|
||
font-size: 18px;
|
||
color: #999;
|
||
cursor: pointer;
|
||
z-index: 10;
|
||
}
|
||
|
||
/* 文本域清除按钮位置调整 */
|
||
.textarea-clear {
|
||
top: 10px;
|
||
transform: none;
|
||
}
|
||
|
||
.clear-icon:hover {
|
||
color: #f00;
|
||
}
|
||
|
||
.btn-group {
|
||
display: flex;
|
||
gap: 10px;
|
||
margin-top: 20px;
|
||
}
|
||
|
||
.btn-group button {
|
||
flex: 1;
|
||
}
|
||
|
||
.topic-list {
|
||
margin-top: 15px;
|
||
}
|
||
|
||
.list-title {
|
||
font-size: 14px;
|
||
color: #666;
|
||
margin-bottom: 8px;
|
||
}
|
||
|
||
.topic-item {
|
||
display: flex;
|
||
justify-content: space-between;
|
||
align-items: center;
|
||
padding: 8px 10px;
|
||
background-color: #f9f9f9;
|
||
border-radius: 4px;
|
||
margin-bottom: 5px;
|
||
}
|
||
|
||
.log-section {
|
||
margin-top: 15px;
|
||
}
|
||
|
||
.log-content {
|
||
background-color: #f9f9f9;
|
||
border-radius: 4px;
|
||
padding: 10px;
|
||
}
|
||
|
||
/* 空日志提示 */
|
||
.empty-log-tip {
|
||
text-align: center;
|
||
color: #999;
|
||
padding: 20px 0;
|
||
font-size: 14px;
|
||
}
|
||
|
||
.log-item {
|
||
padding: 8px;
|
||
border-bottom: 1px solid #eee;
|
||
margin-bottom: 5px;
|
||
font-size: 14px;
|
||
border-radius: 4px;
|
||
}
|
||
|
||
.log-item.received {
|
||
background-color: #e8f5e9;
|
||
}
|
||
|
||
.log-item.sent {
|
||
background-color: #e3f2fd;
|
||
}
|
||
|
||
.log-item.system {
|
||
background-color: #fff8e1;
|
||
}
|
||
|
||
.log-time {
|
||
color: #999;
|
||
font-size: 12px;
|
||
margin-bottom: 3px;
|
||
}
|
||
|
||
.log-type {
|
||
font-weight: bold;
|
||
margin-bottom: 3px;
|
||
}
|
||
|
||
/* 日志行容器 - 标签+滚动内容 */
|
||
.log-row {
|
||
display: flex;
|
||
align-items: flex-start;
|
||
margin-bottom: 3px;
|
||
width: 100%;
|
||
}
|
||
|
||
/* 日志标签(主题:/内容:) */
|
||
.log-label {
|
||
color: #666;
|
||
flex-shrink: 0; /* 标签不收缩 */
|
||
margin-right: 5px;
|
||
}
|
||
|
||
/* 日志内容滚动容器 */
|
||
.log-content-scroll {
|
||
flex: 1;
|
||
white-space: nowrap; /* 不换行 */
|
||
overflow-x: auto; /* 横向滚动 */
|
||
-webkit-overflow-scrolling: touch; /* 移动端顺滑滚动 */
|
||
padding-bottom: 2px;
|
||
color: #333;
|
||
/* 隐藏滚动条但保留滚动功能 */
|
||
scrollbar-width: none; /* Firefox */
|
||
-ms-overflow-style: none; /* IE/Edge */
|
||
}
|
||
|
||
/* 隐藏Chrome等浏览器的滚动条 */
|
||
.log-content-scroll::-webkit-scrollbar {
|
||
display: none;
|
||
}
|
||
|
||
/* 修复之前的样式冲突 */
|
||
.log-topic, .log-message {
|
||
display: none;
|
||
}
|
||
</style> |