agri-app/pages/demo/index.vue

744 lines
18 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

<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.min'
export default {
data() {
return {
// MQTT配置
mqttConfig: {
broker: 'ws://122.51.109.52:9001/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) {
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>