Compare commits
6 Commits
7ebb384f01
...
a62b9557ff
| Author | SHA1 | Date | |
|---|---|---|---|
| a62b9557ff | |||
| 1c23b85d92 | |||
| 72ee175bff | |||
| 771d8447b4 | |||
| acee68ab58 | |||
| 9c2a469e4d |
@@ -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,7 +258,7 @@ 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)
|
||||||
@@ -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
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user