Compare commits

...

6 Commits

4 changed files with 65 additions and 49 deletions

View File

@@ -10,11 +10,17 @@ class MqttManager {
this.messages = new Map() // serverId -> message[]
this.publishHistory = new Map() // serverId -> publishHistory[]
this.listeners = new Set() // IPC callback functions
this.msgIdCounter = 0 // 用于生成唯一消息 id
this.dataPath = join(app.getPath('userData'), 'frost-mqtt-data.json')
this.loadData()
}
nextMsgId() {
this.msgIdCounter = (this.msgIdCounter + 1) % Number.MAX_SAFE_INTEGER
return 'msg_' + Date.now() + '_' + this.msgIdCounter
}
// ==================== 数据持久化 ====================
getDataDir() {
const dir = app.getPath('userData')
@@ -79,7 +85,7 @@ class MqttManager {
initDefaultServers() {
const defaults = [
{
id: 'srv_001',
id: 'example_server',
name: 'EMQX 公共测试',
host: 'broker.emqx.io',
port: 1883,
@@ -94,23 +100,6 @@ class MqttManager {
{ id: 'topic_default_001', topic: 'sensor/+/temperature', qos: 1, subscribed: false },
{ id: 'topic_default_002', topic: 'device/status', qos: 2, subscribed: false }
]
},
{
id: 'srv_002',
name: '本地 Mosquitto',
host: 'localhost',
port: 1883,
protocol: 'mqtt',
clientId: 'frost_local_' + Math.random().toString(36).slice(2, 8),
username: '',
password: '',
keepAlive: 60,
cleanSession: true,
status: 'disconnected',
topics: [
{ id: 'topic_default_003', topic: 'home/+/light', qos: 1, subscribed: false },
{ id: 'topic_default_004', topic: 'home/alarm', qos: 2, subscribed: false }
]
}
]
defaults.forEach((s) => {
@@ -269,9 +258,9 @@ class MqttManager {
qos: packet.qos,
retain: packet.retain,
direction: 'sub',
id: 'msg_' + Date.now()
}
const msgs = this.messages.get(id) || []
id: this.nextMsgId()
}
const msgs = this.messages.get(id) || []
msgs.unshift(msg)
if (msgs.length > 500) msgs.pop()
this.messages.set(id, msgs)
@@ -479,10 +468,11 @@ class MqttManager {
this.publishHistory.set(serverId, history)
// 同时添加到消息日志
const pubMsgId = this.nextMsgId()
const msgs = this.messages.get(serverId) || []
msgs.unshift({
...pubEntry,
id: 'msg_' + Date.now()
id: pubMsgId
})
if (msgs.length > 500) msgs.pop()
this.messages.set(serverId, msgs)
@@ -491,7 +481,7 @@ class MqttManager {
serverId,
message: {
...pubEntry,
id: 'msg_' + Date.now()
id: pubMsgId
}
})

View File

@@ -79,9 +79,6 @@ export function useMqttStore() {
try {
const data = await window.api.getServers()
servers.value = data
if (data.length > 0 && !activeServerId.value) {
activeServerId.value = data[0].id
}
setupListeners()
} catch (e) {
console.error('初始化失败:', e)

View File

@@ -1,12 +1,22 @@
<script setup>
import { ref, computed } from 'vue'
import { useMqttStore } from '../stores/mqtt.js'
import { NCard, NSpace, NInput, NButton, NEmpty } from 'naive-ui'
import { NCard, NSpace, NInput, NButton, NEmpty, useMessage } from 'naive-ui'
const store = useMqttStore()
const message = useMessage()
const filter = ref('')
async function copyTopic(topic) {
try {
await navigator.clipboard.writeText(topic)
message.success('主题已复制')
} catch {
message.error('复制失败')
}
}
const filteredMessages = computed(() => {
if (!filter.value.trim()) return store.messages.value
const f = filter.value.trim().toLowerCase()
@@ -40,7 +50,7 @@ function formatPayload(payload) {
<template #header>
<n-space align="center" justify="space-between" style="width: 100%">
<div>
<span style="font-weight: 700">消息监控</span>
<span>消息监控</span>
<span style="font-size: 12px; opacity: 0.55; margin-left: 8px"
>实时查看接收和发送的 MQTT 消息</span
>
@@ -69,7 +79,7 @@ function formatPayload(payload) {
<span class="msg-dir" :class="m.direction">{{
m.direction === 'pub' ? 'PUB' : 'SUB'
}}</span>
<span class="msg-topic" :title="m.topic">{{ m.topic }}</span>
<span class="msg-topic" :title="m.topic" @click="copyTopic(m.topic)">{{ m.topic }}</span>
<span class="msg-meta">QoS{{ m.qos }}{{ m.retain ? ' R' : '' }}</span>
</div>
<pre class="msg-payload">{{ formatPayload(m.payload) }}</pre>
@@ -102,7 +112,7 @@ function formatPayload(payload) {
}
.msg-log {
background: #1e1e2e;
background: #f5f7fa;
border-radius: 6px;
padding: 12px;
height: 100%;
@@ -113,12 +123,12 @@ function formatPayload(payload) {
font-family: monospace;
font-size: 12px;
line-height: 1.7;
color: #cdd6f4;
color: #333;
}
.msg-entry {
padding: 8px 0;
border-bottom: 1px solid rgba(255, 255, 255, 0.08);
border-bottom: 1px solid rgba(0, 0, 0, 0.06);
}
.msg-header {
@@ -129,7 +139,7 @@ function formatPayload(payload) {
}
.msg-time {
color: #6c7086;
color: #999;
flex-shrink: 0;
}
.msg-dir {
@@ -138,31 +148,36 @@ function formatPayload(payload) {
width: 28px;
}
.msg-dir.pub {
color: #89b4fa;
color: #2080f0;
}
.msg-dir.sub {
color: #a6e3a1;
color: #18a058;
}
.msg-topic {
color: #f9e2af;
color: #d25a00;
min-width: 0;
flex: 1;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
cursor: pointer;
}
.msg-topic:hover {
text-decoration: underline;
}
.msg-payload {
margin: 6px 0 0;
padding: 8px 10px;
border-radius: 4px;
background: rgba(255, 255, 255, 0.05);
color: #cdd6f4;
background: rgba(0, 0, 0, 0.04);
color: #333;
font: inherit;
white-space: pre-wrap;
overflow-wrap: anywhere;
}
.msg-meta {
color: #6c7086;
color: #999;
font-size: 11px;
flex-shrink: 0;
}

View File

@@ -103,6 +103,15 @@ function handleFormatJson() {
async function handleClearHistory() {
await store.clearPublishHistory()
}
async function copyTopic(topic) {
try {
await navigator.clipboard.writeText(topic)
msg.success('主题已复制')
} catch {
msg.error('复制失败')
}
}
</script>
<template>
@@ -166,7 +175,7 @@ async function handleClearHistory() {
<div class="msg-header">
<span class="msg-time">{{ m.time }}</span>
<span class="msg-dir pub">PUB</span>
<span class="msg-topic" :title="m.topic">{{ m.topic }}</span>
<span class="msg-topic" :title="m.topic" @click="copyTopic(m.topic)">{{ m.topic }}</span>
<span class="msg-meta">QoS{{ m.qos }}{{ m.retain ? ' R' : '' }}</span>
</div>
<pre class="msg-payload">{{ m.payload }}</pre>
@@ -276,7 +285,7 @@ async function handleClearHistory() {
}
.msg-log {
background: #1e1e2e;
background: #f5f7fa;
border-radius: 6px;
padding: 12px;
height: 100%;
@@ -286,12 +295,12 @@ async function handleClearHistory() {
font-family: monospace;
font-size: 12px;
line-height: 1.7;
color: #cdd6f4;
color: #333;
}
.msg-entry {
padding: 8px 0;
border-bottom: 1px solid rgba(255, 255, 255, 0.08);
border-bottom: 1px solid rgba(0, 0, 0, 0.06);
}
.msg-header {
@@ -302,7 +311,7 @@ async function handleClearHistory() {
}
.msg-time {
color: #6c7086;
color: #999;
flex-shrink: 0;
}
.msg-dir {
@@ -311,28 +320,33 @@ async function handleClearHistory() {
width: 28px;
}
.msg-dir.pub {
color: #89b4fa;
color: #2080f0;
}
.msg-topic {
color: #f9e2af;
color: #d25a00;
min-width: 0;
flex: 1;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
cursor: pointer;
}
.msg-topic:hover {
text-decoration: underline;
}
.msg-payload {
margin: 6px 0 0;
padding: 8px 10px;
border-radius: 4px;
background: rgba(255, 255, 255, 0.05);
color: #cdd6f4;
background: rgba(0, 0, 0, 0.04);
color: #333;
font: inherit;
white-space: pre-wrap;
overflow-wrap: anywhere;
}
.msg-meta {
color: #6c7086;
color: #999;
font-size: 11px;
flex-shrink: 0;
}