6 Commits

4 changed files with 65 additions and 49 deletions
+13 -23
View File
@@ -10,11 +10,17 @@ class MqttManager {
this.messages = new Map() // serverId -> message[] this.messages = new Map() // serverId -> message[]
this.publishHistory = new Map() // serverId -> publishHistory[] this.publishHistory = new Map() // serverId -> publishHistory[]
this.listeners = new Set() // IPC callback functions this.listeners = new Set() // IPC callback functions
this.msgIdCounter = 0 // 用于生成唯一消息 id
this.dataPath = join(app.getPath('userData'), 'frost-mqtt-data.json') this.dataPath = join(app.getPath('userData'), 'frost-mqtt-data.json')
this.loadData() this.loadData()
} }
nextMsgId() {
this.msgIdCounter = (this.msgIdCounter + 1) % Number.MAX_SAFE_INTEGER
return 'msg_' + Date.now() + '_' + this.msgIdCounter
}
// ==================== 数据持久化 ==================== // ==================== 数据持久化 ====================
getDataDir() { getDataDir() {
const dir = app.getPath('userData') const dir = app.getPath('userData')
@@ -79,7 +85,7 @@ class MqttManager {
initDefaultServers() { initDefaultServers() {
const defaults = [ const defaults = [
{ {
id: 'srv_001', id: 'example_server',
name: 'EMQX 公共测试', name: 'EMQX 公共测试',
host: 'broker.emqx.io', host: 'broker.emqx.io',
port: 1883, port: 1883,
@@ -94,23 +100,6 @@ class MqttManager {
{ id: 'topic_default_001', topic: 'sensor/+/temperature', qos: 1, subscribed: false }, { id: 'topic_default_001', topic: 'sensor/+/temperature', qos: 1, subscribed: false },
{ id: 'topic_default_002', topic: 'device/status', qos: 2, 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) => { defaults.forEach((s) => {
@@ -269,9 +258,9 @@ class MqttManager {
qos: packet.qos, qos: packet.qos,
retain: packet.retain, retain: packet.retain,
direction: 'sub', direction: 'sub',
id: 'msg_' + Date.now() id: this.nextMsgId()
} }
const msgs = this.messages.get(id) || [] const msgs = this.messages.get(id) || []
msgs.unshift(msg) msgs.unshift(msg)
if (msgs.length > 500) msgs.pop() if (msgs.length > 500) msgs.pop()
this.messages.set(id, msgs) this.messages.set(id, msgs)
@@ -479,10 +468,11 @@ class MqttManager {
this.publishHistory.set(serverId, history) this.publishHistory.set(serverId, history)
// 同时添加到消息日志 // 同时添加到消息日志
const pubMsgId = this.nextMsgId()
const msgs = this.messages.get(serverId) || [] const msgs = this.messages.get(serverId) || []
msgs.unshift({ msgs.unshift({
...pubEntry, ...pubEntry,
id: 'msg_' + Date.now() id: pubMsgId
}) })
if (msgs.length > 500) msgs.pop() if (msgs.length > 500) msgs.pop()
this.messages.set(serverId, msgs) this.messages.set(serverId, msgs)
@@ -491,7 +481,7 @@ class MqttManager {
serverId, serverId,
message: { message: {
...pubEntry, ...pubEntry,
id: 'msg_' + Date.now() id: pubMsgId
} }
}) })
-3
View File
@@ -79,9 +79,6 @@ export function useMqttStore() {
try { try {
const data = await window.api.getServers() const data = await window.api.getServers()
servers.value = data servers.value = data
if (data.length > 0 && !activeServerId.value) {
activeServerId.value = data[0].id
}
setupListeners() setupListeners()
} catch (e) { } catch (e) {
console.error('初始化失败:', e) console.error('初始化失败:', e)
+28 -13
View File
@@ -1,12 +1,22 @@
<script setup> <script setup>
import { ref, computed } from 'vue' import { ref, computed } from 'vue'
import { useMqttStore } from '../stores/mqtt.js' 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 store = useMqttStore()
const message = useMessage()
const filter = ref('') const filter = ref('')
async function copyTopic(topic) {
try {
await navigator.clipboard.writeText(topic)
message.success('主题已复制')
} catch {
message.error('复制失败')
}
}
const filteredMessages = computed(() => { const filteredMessages = computed(() => {
if (!filter.value.trim()) return store.messages.value if (!filter.value.trim()) return store.messages.value
const f = filter.value.trim().toLowerCase() const f = filter.value.trim().toLowerCase()
@@ -40,7 +50,7 @@ function formatPayload(payload) {
<template #header> <template #header>
<n-space align="center" justify="space-between" style="width: 100%"> <n-space align="center" justify="space-between" style="width: 100%">
<div> <div>
<span style="font-weight: 700">消息监控</span> <span>消息监控</span>
<span style="font-size: 12px; opacity: 0.55; margin-left: 8px" <span style="font-size: 12px; opacity: 0.55; margin-left: 8px"
>实时查看接收和发送的 MQTT 消息</span >实时查看接收和发送的 MQTT 消息</span
> >
@@ -69,7 +79,7 @@ function formatPayload(payload) {
<span class="msg-dir" :class="m.direction">{{ <span class="msg-dir" :class="m.direction">{{
m.direction === 'pub' ? 'PUB' : 'SUB' m.direction === 'pub' ? 'PUB' : 'SUB'
}}</span> }}</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> <span class="msg-meta">QoS{{ m.qos }}{{ m.retain ? ' R' : '' }}</span>
</div> </div>
<pre class="msg-payload">{{ formatPayload(m.payload) }}</pre> <pre class="msg-payload">{{ formatPayload(m.payload) }}</pre>
@@ -102,7 +112,7 @@ function formatPayload(payload) {
} }
.msg-log { .msg-log {
background: #1e1e2e; background: #f5f7fa;
border-radius: 6px; border-radius: 6px;
padding: 12px; padding: 12px;
height: 100%; height: 100%;
@@ -113,12 +123,12 @@ function formatPayload(payload) {
font-family: monospace; font-family: monospace;
font-size: 12px; font-size: 12px;
line-height: 1.7; line-height: 1.7;
color: #cdd6f4; color: #333;
} }
.msg-entry { .msg-entry {
padding: 8px 0; 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 { .msg-header {
@@ -129,7 +139,7 @@ function formatPayload(payload) {
} }
.msg-time { .msg-time {
color: #6c7086; color: #999;
flex-shrink: 0; flex-shrink: 0;
} }
.msg-dir { .msg-dir {
@@ -138,31 +148,36 @@ function formatPayload(payload) {
width: 28px; width: 28px;
} }
.msg-dir.pub { .msg-dir.pub {
color: #89b4fa; color: #2080f0;
} }
.msg-dir.sub { .msg-dir.sub {
color: #a6e3a1; color: #18a058;
} }
.msg-topic { .msg-topic {
color: #f9e2af; color: #d25a00;
min-width: 0; min-width: 0;
flex: 1; flex: 1;
overflow: hidden; overflow: hidden;
text-overflow: ellipsis; text-overflow: ellipsis;
white-space: nowrap; white-space: nowrap;
cursor: pointer;
}
.msg-topic:hover {
text-decoration: underline;
} }
.msg-payload { .msg-payload {
margin: 6px 0 0; margin: 6px 0 0;
padding: 8px 10px; padding: 8px 10px;
border-radius: 4px; border-radius: 4px;
background: rgba(255, 255, 255, 0.05); background: rgba(0, 0, 0, 0.04);
color: #cdd6f4; color: #333;
font: inherit; font: inherit;
white-space: pre-wrap; white-space: pre-wrap;
overflow-wrap: anywhere; overflow-wrap: anywhere;
} }
.msg-meta { .msg-meta {
color: #6c7086; color: #999;
font-size: 11px; font-size: 11px;
flex-shrink: 0; flex-shrink: 0;
} }
+24 -10
View File
@@ -103,6 +103,15 @@ function handleFormatJson() {
async function handleClearHistory() { async function handleClearHistory() {
await store.clearPublishHistory() await store.clearPublishHistory()
} }
async function copyTopic(topic) {
try {
await navigator.clipboard.writeText(topic)
msg.success('主题已复制')
} catch {
msg.error('复制失败')
}
}
</script> </script>
<template> <template>
@@ -166,7 +175,7 @@ async function handleClearHistory() {
<div class="msg-header"> <div class="msg-header">
<span class="msg-time">{{ m.time }}</span> <span class="msg-time">{{ m.time }}</span>
<span class="msg-dir pub">PUB</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> <span class="msg-meta">QoS{{ m.qos }}{{ m.retain ? ' R' : '' }}</span>
</div> </div>
<pre class="msg-payload">{{ m.payload }}</pre> <pre class="msg-payload">{{ m.payload }}</pre>
@@ -276,7 +285,7 @@ async function handleClearHistory() {
} }
.msg-log { .msg-log {
background: #1e1e2e; background: #f5f7fa;
border-radius: 6px; border-radius: 6px;
padding: 12px; padding: 12px;
height: 100%; height: 100%;
@@ -286,12 +295,12 @@ async function handleClearHistory() {
font-family: monospace; font-family: monospace;
font-size: 12px; font-size: 12px;
line-height: 1.7; line-height: 1.7;
color: #cdd6f4; color: #333;
} }
.msg-entry { .msg-entry {
padding: 8px 0; 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 { .msg-header {
@@ -302,7 +311,7 @@ async function handleClearHistory() {
} }
.msg-time { .msg-time {
color: #6c7086; color: #999;
flex-shrink: 0; flex-shrink: 0;
} }
.msg-dir { .msg-dir {
@@ -311,28 +320,33 @@ async function handleClearHistory() {
width: 28px; width: 28px;
} }
.msg-dir.pub { .msg-dir.pub {
color: #89b4fa; color: #2080f0;
} }
.msg-topic { .msg-topic {
color: #f9e2af; color: #d25a00;
min-width: 0; min-width: 0;
flex: 1; flex: 1;
overflow: hidden; overflow: hidden;
text-overflow: ellipsis; text-overflow: ellipsis;
white-space: nowrap; white-space: nowrap;
cursor: pointer;
}
.msg-topic:hover {
text-decoration: underline;
} }
.msg-payload { .msg-payload {
margin: 6px 0 0; margin: 6px 0 0;
padding: 8px 10px; padding: 8px 10px;
border-radius: 4px; border-radius: 4px;
background: rgba(255, 255, 255, 0.05); background: rgba(0, 0, 0, 0.04);
color: #cdd6f4; color: #333;
font: inherit; font: inherit;
white-space: pre-wrap; white-space: pre-wrap;
overflow-wrap: anywhere; overflow-wrap: anywhere;
} }
.msg-meta { .msg-meta {
color: #6c7086; color: #999;
font-size: 11px; font-size: 11px;
flex-shrink: 0; flex-shrink: 0;
} }