From 78797dee86a129174e0517220cd2f57b0f83ca7c Mon Sep 17 00:00:00 2001 From: Frost-ZX Date: Sun, 19 Jul 2026 17:15:28 +0800 Subject: [PATCH] =?UTF-8?q?style:=20=E6=95=B4=E7=90=86=E4=BB=A3=E7=A0=81?= =?UTF-8?q?=E6=A0=BC=E5=BC=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/.editorconfig | 2 +- app/src/main/index.js | 57 +- app/src/main/ipc-handlers.js | 119 ++-- app/src/main/mqtt-manager.js | 598 +++++++++++------- app/src/preload/index.js | 50 +- app/src/renderer/index.html | 8 +- app/src/renderer/src/App.vue | 99 +-- .../renderer/src/components/ServerModal.vue | 160 +++-- app/src/renderer/src/components/Sidebar.vue | 99 +-- app/src/renderer/src/main.js | 10 +- app/src/renderer/src/stores/mqtt.js | 206 +++--- app/src/renderer/src/views/Dashboard.vue | 182 +++--- app/src/renderer/src/views/Messages.vue | 110 +++- app/src/renderer/src/views/Publish.vue | 260 +++++--- app/src/renderer/src/views/Subscribe.vue | 268 +++++--- 15 files changed, 1354 insertions(+), 874 deletions(-) diff --git a/app/.editorconfig b/app/.editorconfig index cf640d5..9f73416 100644 --- a/app/.editorconfig +++ b/app/.editorconfig @@ -6,4 +6,4 @@ indent_style = space indent_size = 2 end_of_line = lf insert_final_newline = true -trim_trailing_whitespace = true \ No newline at end of file +trim_trailing_whitespace = true diff --git a/app/src/main/index.js b/app/src/main/index.js index 2960d4a..3a63303 100644 --- a/app/src/main/index.js +++ b/app/src/main/index.js @@ -1,10 +1,12 @@ -import { app, shell, BrowserWindow } from 'electron' -import { join } from 'path' -import { electronApp, optimizer, is } from '@electron-toolkit/utils' -import icon from '../../resources/icon.png?asset' -import { registerIpcHandlers } from './ipc-handlers.js' +import { app, shell, BrowserWindow } from 'electron'; +import { join } from 'path'; +import { electronApp, optimizer, is } from '@electron-toolkit/utils'; +import { registerIpcHandlers } from './ipc-handlers.js'; + +import icon from '../../resources/icon.png?asset'; function createWindow() { + // Create the browser window. const mainWindow = new BrowserWindow({ width: 1200, @@ -16,62 +18,67 @@ function createWindow() { ...(process.platform === 'linux' ? { icon } : {}), webPreferences: { preload: join(__dirname, '../preload/index.js'), - sandbox: false - } - }) + sandbox: false, + }, + }); mainWindow.on('ready-to-show', () => { - mainWindow.show() - }) + mainWindow.show(); + }); mainWindow.webContents.setWindowOpenHandler((details) => { - shell.openExternal(details.url) - return { action: 'deny' } - }) + shell.openExternal(details.url); + return { action: 'deny' }; + }); // HMR for renderer base on electron-vite cli. // Load the remote URL for development or the local html file for production. if (is.dev && process.env['ELECTRON_RENDERER_URL']) { - mainWindow.loadURL(process.env['ELECTRON_RENDERER_URL']) + mainWindow.loadURL(process.env['ELECTRON_RENDERER_URL']); } else { - mainWindow.loadFile(join(__dirname, '../renderer/index.html')) + mainWindow.loadFile(join(__dirname, '../renderer/index.html')); } + } // This method will be called when Electron has finished // initialization and is ready to create browser windows. // Some APIs can only be used after this event occurs. app.whenReady().then(() => { + // Set app user model id for windows - electronApp.setAppUserModelId('com.electron') + electronApp.setAppUserModelId('com.electron'); // Default open or close DevTools by F12 in development // and ignore CommandOrControl + R in production. // see https://github.com/alex8088/electron-toolkit/tree/master/packages/utils app.on('browser-window-created', (_, window) => { - optimizer.watchWindowShortcuts(window) - }) + optimizer.watchWindowShortcuts(window); + }); // 注册 IPC 处理器 - registerIpcHandlers() + registerIpcHandlers(); - createWindow() + createWindow(); app.on('activate', function () { // On macOS it's common to re-create a window in the app when the // dock icon is clicked and there are no other windows open. - if (BrowserWindow.getAllWindows().length === 0) createWindow() - }) -}) + if (BrowserWindow.getAllWindows().length === 0) { + createWindow(); + } + }); + +}); // Quit when all windows are closed, except on macOS. There, it's common // for applications and their menu bar to stay active until the user quits // explicitly with Cmd + Q. app.on('window-all-closed', () => { if (process.platform !== 'darwin') { - app.quit() + app.quit(); } -}) +}); // In this file you can include the rest of your app's specific main process // code. You can also put them in separate files and require them here. diff --git a/app/src/main/ipc-handlers.js b/app/src/main/ipc-handlers.js index 58cfc32..a738149 100644 --- a/app/src/main/ipc-handlers.js +++ b/app/src/main/ipc-handlers.js @@ -1,18 +1,20 @@ -import { ipcMain, BrowserWindow } from 'electron' -import mqttManager from './mqtt-manager.js' +import { ipcMain, BrowserWindow } from 'electron'; + +import mqttManager from './mqtt-manager.js'; export function registerIpcHandlers() { + // 获取主窗口(用于推送事件) function getMainWindow() { - const windows = BrowserWindow.getAllWindows() - return windows.length > 0 ? windows[0] : null + const windows = BrowserWindow.getAllWindows(); + return windows.length > 0 ? windows[0] : null; } // 向渲染进程发送事件 function sendToRenderer(channel, data) { - const win = getMainWindow() + const win = getMainWindow(); if (win && !win.isDestroyed()) { - win.webContents.send(channel, data) + win.webContents.send(channel, data); } } @@ -20,100 +22,103 @@ export function registerIpcHandlers() { mqttManager.addListener((event, data) => { switch (event) { case 'status-change': - sendToRenderer('mqtt:status-change', data) - break + sendToRenderer('mqtt:status-change', data); + break; case 'message': - sendToRenderer('mqtt:message', data) - break + sendToRenderer('mqtt:message', data); + break; case 'subscription-change': - sendToRenderer('mqtt:subscription-change', data) - break + sendToRenderer('mqtt:subscription-change', data); + break; case 'subscription-error': - sendToRenderer('mqtt:subscription-error', data) - break + sendToRenderer('mqtt:subscription-error', data); + break; case 'publish-error': - sendToRenderer('mqtt:publish-error', data) - break + sendToRenderer('mqtt:publish-error', data); + break; } - }) + }); // ==================== 服务器管理 ==================== + ipcMain.handle('mqtt:get-servers', () => { - return mqttManager.getServers() - }) + return mqttManager.getServers(); + }); ipcMain.handle('mqtt:add-server', (_, config) => { - return mqttManager.addServer(config) - }) + return mqttManager.addServer(config); + }); ipcMain.handle('mqtt:update-server', (_, id, config) => { - return mqttManager.updateServer(id, config) - }) + return mqttManager.updateServer(id, config); + }); ipcMain.handle('mqtt:delete-server', (_, id) => { - mqttManager.deleteServer(id) - return true - }) + mqttManager.deleteServer(id); + return true; + }); // ==================== 连接管理 ==================== + ipcMain.handle('mqtt:connect', (_, id) => { - return mqttManager.connect(id) - }) + return mqttManager.connect(id); + }); ipcMain.handle('mqtt:disconnect', (_, id) => { - mqttManager.disconnect(id) - return true - }) + mqttManager.disconnect(id); + return true; + }); // ==================== 主题管理 ==================== + ipcMain.handle('mqtt:add-topic', (_, serverId, topic, qos) => { - return mqttManager.addTopic(serverId, topic, qos) - }) + return mqttManager.addTopic(serverId, topic, qos); + }); ipcMain.handle('mqtt:remove-topic', (_, serverId, topic) => { - return mqttManager.removeTopic(serverId, topic) - }) + return mqttManager.removeTopic(serverId, topic); + }); ipcMain.handle('mqtt:subscribe', (_, serverId, topic) => { - return mqttManager.subscribe(serverId, topic) - }) + return mqttManager.subscribe(serverId, topic); + }); ipcMain.handle('mqtt:unsubscribe', (_, serverId, topic) => { - return mqttManager.unsubscribe(serverId, topic) - }) + return mqttManager.unsubscribe(serverId, topic); + }); ipcMain.handle('mqtt:subscribe-all', (_, serverId) => { - return mqttManager.subscribeAll(serverId) - }) + return mqttManager.subscribeAll(serverId); + }); ipcMain.handle('mqtt:update-topic', (_, serverId, topicId, topic, qos) => { - return mqttManager.updateTopic(serverId, topicId, topic, qos) - }) + return mqttManager.updateTopic(serverId, topicId, topic, qos); + }); // ==================== 消息发布 ==================== + ipcMain.handle('mqtt:publish', (_, serverId, topic, payload, opts) => { - return mqttManager.publish(serverId, topic, payload, opts) - }) + return mqttManager.publish(serverId, topic, payload, opts); + }); // ==================== 消息管理 ==================== + ipcMain.handle('mqtt:get-messages', (_, serverId) => { - return mqttManager.getMessages(serverId) - }) + return mqttManager.getMessages(serverId); + }); ipcMain.handle('mqtt:get-publish-history', (_, serverId) => { - return mqttManager.getPublishHistory(serverId) - }) + return mqttManager.getPublishHistory(serverId); + }); ipcMain.handle('mqtt:clear-messages', (_, serverId) => { - mqttManager.clearMessages(serverId) - return true - }) + mqttManager.clearMessages(serverId); + return true; + }); ipcMain.handle('mqtt:clear-publish-history', (_, serverId) => { - mqttManager.clearPublishHistory(serverId) - return true - }) + mqttManager.clearPublishHistory(serverId); + return true; + }); - // 兼容旧的 ping 测试 - ipcMain.on('ping', () => console.log('pong')) } diff --git a/app/src/main/mqtt-manager.js b/app/src/main/mqtt-manager.js index 9220573..05190f3 100644 --- a/app/src/main/mqtt-manager.js +++ b/app/src/main/mqtt-manager.js @@ -1,89 +1,104 @@ -import mqtt from 'mqtt' -import { app } from 'electron' -import { join } from 'path' -import { readFileSync, writeFileSync, existsSync, mkdirSync } from 'fs' +import { app } from 'electron'; +import { readFileSync, writeFileSync, existsSync, mkdirSync } from 'fs'; +import { join } from 'path'; + +import mqtt from 'mqtt'; class MqttManager { - constructor() { - this.clients = new Map() // serverId -> mqtt client - this.servers = new Map() // serverId -> server config - 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() + constructor() { + this.clients = new Map(); // serverId -> mqtt client + this.servers = new Map(); // serverId -> server config + 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 + this.msgIdCounter = (this.msgIdCounter + 1) % Number.MAX_SAFE_INTEGER; + return 'msg_' + Date.now() + '_' + this.msgIdCounter; } // ==================== 数据持久化 ==================== + getDataDir() { - const dir = app.getPath('userData') + + let dir = app.getPath('userData'); + if (!existsSync(dir)) { - mkdirSync(dir, { recursive: true }) + mkdirSync(dir, { recursive: true }); } - return dir + + return dir; + } loadData() { try { if (existsSync(this.dataPath)) { - const raw = readFileSync(this.dataPath, 'utf-8') - const data = JSON.parse(raw) + + let raw = readFileSync(this.dataPath, 'utf-8'); + let data = JSON.parse(raw); + if (data.servers) { data.servers.forEach((s) => { - s.status = 'disconnected' + s.status = 'disconnected'; s.topics = (s.topics || []).map((t) => ({ ...t, id: t.id || 'topic_' + Date.now() + '_' + Math.random().toString(36).slice(2, 6), - subscribed: false - })) - this.servers.set(s.id, s) - }) + subscribed: false, + })); + this.servers.set(s.id, s); + }); } + if (data.messages) { Object.entries(data.messages).forEach(([id, msgs]) => { - this.messages.set(id, msgs) - }) + this.messages.set(id, msgs); + }); } + if (data.publishHistory) { Object.entries(data.publishHistory).forEach(([id, history]) => { - this.publishHistory.set(id, history) - }) + this.publishHistory.set(id, history); + }); } + } else { - this.initDefaultServers() + this.initDefaultServers(); } } catch (e) { - console.error('加载数据失败:', e.message) - this.initDefaultServers() + console.error('加载数据失败:', e.message); + this.initDefaultServers(); } } saveData() { try { - const data = { + + let data = { servers: Array.from(this.servers.values()).map((s) => ({ ...s, status: 'disconnected', - topics: s.topics.map((t) => ({ ...t, subscribed: false })) + topics: s.topics.map((t) => ({ ...t, subscribed: false })), })), messages: Object.fromEntries(this.messages), - publishHistory: Object.fromEntries(this.publishHistory) - } - writeFileSync(this.dataPath, JSON.stringify(data, null, 2), 'utf-8') + publishHistory: Object.fromEntries(this.publishHistory), + }; + + writeFileSync(this.dataPath, JSON.stringify(data, null, 2), 'utf-8'); + } catch (e) { - console.error('保存数据失败:', e.message) + console.error('保存数据失败:', e.message); } } initDefaultServers() { - const defaults = [ + + let defaults = [ { id: 'example_server', name: 'EMQX 公共测试', @@ -98,52 +113,58 @@ class MqttManager { status: 'disconnected', topics: [ { 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 }, + ], + }, + ]; + defaults.forEach((s) => { - this.servers.set(s.id, s) - this.messages.set(s.id, []) - this.publishHistory.set(s.id, []) - }) - this.saveData() + this.servers.set(s.id, s); + this.messages.set(s.id, []); + this.publishHistory.set(s.id, []); + }); + + this.saveData(); + } // ==================== 监听器 ==================== + addListener(callback) { - this.listeners.add(callback) + this.listeners.add(callback); } removeListener(callback) { - this.listeners.delete(callback) + this.listeners.delete(callback); } notify(event, data) { this.listeners.forEach((cb) => { try { - cb(event, data) + cb(event, data); } catch (e) { - console.error('通知失败:', e) + console.error('通知失败:', e); } - }) + }); } // ==================== 服务器管理 ==================== + getServers() { return Array.from(this.servers.values()).map((s) => ({ ...s, - password: s.password ? '******' : '' - })) + password: s.password ? '******' : '', + })); } getServer(id) { - return this.servers.get(id) || null + return this.servers.get(id) || null; } addServer(config) { - const id = 'srv_' + Date.now() + '_' + Math.random().toString(36).slice(2, 6) - const server = { + + let id = 'srv_' + Date.now() + '_' + Math.random().toString(36).slice(2, 6); + let server = { id, name: config.name, host: config.host, @@ -155,18 +176,26 @@ class MqttManager { keepAlive: config.keepAlive || 60, cleanSession: config.cleanSession !== false, status: 'disconnected', - topics: [] - } - this.servers.set(id, server) - this.messages.set(id, []) - this.publishHistory.set(id, []) - this.saveData() - return server + topics: [], + }; + + this.servers.set(id, server); + this.messages.set(id, []); + this.publishHistory.set(id, []); + this.saveData(); + + return server; + } updateServer(id, config) { - const server = this.servers.get(id) - if (!server) return null + + let server = this.servers.get(id); + + if (!server) { + return null; + } + Object.assign(server, { name: config.name, host: config.host, @@ -175,283 +204,373 @@ class MqttManager { clientId: config.clientId || server.clientId, username: config.username || '', keepAlive: config.keepAlive || 60, - cleanSession: config.cleanSession !== false - }) + cleanSession: config.cleanSession !== false, + }); + if (config.password) { - server.password = config.password + server.password = config.password; } - this.saveData() - return server + + this.saveData(); + + return server; + } deleteServer(id) { // 先断开连接 - this.disconnect(id) - this.servers.delete(id) - this.messages.delete(id) - this.publishHistory.delete(id) - this.saveData() + this.disconnect(id); + this.servers.delete(id); + this.messages.delete(id); + this.publishHistory.delete(id); + this.saveData(); } // ==================== 连接管理 ==================== + connect(id) { - const server = this.servers.get(id) - if (!server) return false + + let server = this.servers.get(id); + + if (!server) { + return false; + } // 如果已连接,先断开 if (this.clients.has(id)) { - this.disconnect(id) + this.disconnect(id); } - server.status = 'connecting' - this.notify('status-change', { id, status: 'connecting' }) + server.status = 'connecting'; - const protocol = server.protocol || 'mqtt' - const url = `${protocol}://${server.host}:${server.port}` + this.notify('status-change', { id, status: 'connecting' }); - const options = { + let protocol = server.protocol || 'mqtt'; + let url = `${protocol}://${server.host}:${server.port}`; + + let options = { clientId: server.clientId, username: server.username || undefined, password: server.password || undefined, keepalive: server.keepAlive, clean: server.cleanSession, reconnectPeriod: 5000, - connectTimeout: 10000 - } + connectTimeout: 10000, + }; try { - const client = mqtt.connect(url, options) + + let client = mqtt.connect(url, options); client.on('connect', () => { - server.status = 'connected' - this.notify('status-change', { id, status: 'connected' }) + server.status = 'connected'; + this.notify('status-change', { id, status: 'connected' }); // 自动订阅已有主题 server.topics.forEach((t) => { - this._doSubscribe(id, t.topic, { qos: t.qos }) - }) - }) + this._doSubscribe(id, t.topic, { qos: t.qos }); + }); + }); client.on('error', (err) => { - console.error(`[${server.name}] 连接错误:`, err.message) - server.status = 'error' - this.notify('status-change', { id, status: 'error', error: err.message }) - }) + console.error(`[${server.name}] 连接错误:`, err.message); + server.status = 'error'; + this.notify('status-change', { id, status: 'error', error: err.message }); + }); client.on('close', () => { if (server.status === 'connected') { - server.status = 'disconnected' + server.status = 'disconnected'; server.topics.forEach((t) => { - t.subscribed = false - }) - this.notify('status-change', { id, status: 'disconnected' }) + t.subscribed = false; + }); + this.notify('status-change', { id, status: 'disconnected' }); } - }) + }); client.on('message', (topic, payload, packet) => { - const msg = { - time: - new Date().toLocaleTimeString('zh-CN', { hour12: false }) + - '.' + - String(Date.now() % 1000).padStart(3, '0'), - topic, + + let msg = { + time: new Date().toLocaleTimeString('zh-CN', { hour12: false }) + '.' + String(Date.now() % 1000).padStart(3, '0'), + topic: topic, payload: payload.toString(), qos: packet.qos, retain: packet.retain, direction: 'sub', - id: this.nextMsgId() - } - const msgs = this.messages.get(id) || [] - msgs.unshift(msg) - if (msgs.length > 500) msgs.pop() - this.messages.set(id, msgs) - this.notify('message', { serverId: id, message: msg }) - }) + id: this.nextMsgId(), + }; + + let msgs = this.messages.get(id) || []; + + msgs.unshift(msg); + + if (msgs.length > 500) { + msgs.pop(); + } + + this.messages.set(id, msgs); + this.notify('message', { serverId: id, message: msg }); + + }); + + this.clients.set(id, client); + + return true; - this.clients.set(id, client) - return true } catch (err) { - server.status = 'error' - this.notify('status-change', { id, status: 'error', error: err.message }) - return false + server.status = 'error'; + this.notify('status-change', { id, status: 'error', error: err.message }); + return false; } } disconnect(id) { - const client = this.clients.get(id) + + let client = this.clients.get(id); + if (client) { - client.end(true) - this.clients.delete(id) + client.end(true); + this.clients.delete(id); } - const server = this.servers.get(id) + + let server = this.servers.get(id); + if (server) { - server.status = 'disconnected' + server.status = 'disconnected'; server.topics.forEach((t) => { - t.subscribed = false - }) - this.notify('status-change', { id, status: 'disconnected' }) + t.subscribed = false; + }); + this.notify('status-change', { id, status: 'disconnected' }); } + } // ==================== 订阅管理 ==================== _doSubscribe(serverId, topic, opts) { - const client = this.clients.get(serverId) - const server = this.servers.get(serverId) - if (!client || !server || server.status !== 'connected') return false + + let client = this.clients.get(serverId); + let server = this.servers.get(serverId); + + if (!client || !server || server.status !== 'connected') { + return false; + } client.subscribe(topic, opts, (err) => { if (err) { - console.error(`订阅失败 [${topic}]:`, err.message) - this.notify('subscription-error', { serverId, topic, error: err.message }) + console.error(`订阅失败 [${topic}]:`, err.message); + this.notify('subscription-error', { serverId, topic, error: err.message }); } else { - const t = server.topics.find((t) => t.topic === topic) - if (t) t.subscribed = true - this.notify('subscription-change', { serverId, topic, subscribed: true }) + let t = server.topics.find((t) => t.topic === topic); + if (t) { + t.subscribed = true; + } + this.notify('subscription-change', { serverId, topic, subscribed: true }); } - }) - return true + }); + + return true; + } addTopic(serverId, topic, qos = 1) { - const server = this.servers.get(serverId) - if (!server) return false - if (server.topics.find((t) => t.topic === topic)) return false + let server = this.servers.get(serverId); + if (!server) { + return false; + } + if (server.topics.find((t) => t.topic === topic)) { + return false; + } - const topicId = 'topic_' + Date.now() + '_' + Math.random().toString(36).slice(2, 6) - server.topics.push({ id: topicId, topic, qos, subscribed: false }) - this.saveData() + let topicId = 'topic_' + Date.now() + '_' + Math.random().toString(36).slice(2, 6); + + server.topics.push({ id: topicId, topic, qos, subscribed: false }); + + this.saveData(); // 如果已连接,自动订阅 if (server.status === 'connected') { - this._doSubscribe(serverId, topic, { qos }) + this._doSubscribe(serverId, topic, { qos }); } - return true + + return true; + } removeTopic(serverId, topic) { - const server = this.servers.get(serverId) - if (!server) return false + + let server = this.servers.get(serverId); + + if (!server) { + return false; + } // 先取消订阅 - this.unsubscribe(serverId, topic) - server.topics = server.topics.filter((t) => t.topic !== topic) - this.saveData() - return true + this.unsubscribe(serverId, topic); + + server.topics = server.topics.filter((t) => t.topic !== topic); + + this.saveData(); + + return true; + } subscribe(serverId, topic) { - const server = this.servers.get(serverId) - if (!server) return false - const t = server.topics.find((t) => t.topic === topic) - if (!t) return false - return this._doSubscribe(serverId, topic, { qos: t.qos }) + + let server = this.servers.get(serverId); + + if (!server) { + return false; + } + + let t = server.topics.find((t) => t.topic === topic); + + if (!t) { + return false; + } + + return this._doSubscribe(serverId, topic, { qos: t.qos }); + } unsubscribe(serverId, topic) { - const client = this.clients.get(serverId) - const server = this.servers.get(serverId) - if (!client || !server || server.status !== 'connected') return false + + let client = this.clients.get(serverId); + let server = this.servers.get(serverId); + + if (!client || !server || server.status !== 'connected') { + return false; + } client.unsubscribe(topic, {}, (err) => { if (!err) { - const t = server.topics.find((t) => t.topic === topic) - if (t) t.subscribed = false - this.notify('subscription-change', { serverId, topic, subscribed: false }) + let t = server.topics.find((t) => t.topic === topic); + if (t) { + t.subscribed = false; + } + this.notify('subscription-change', { serverId, topic, subscribed: false }); } - }) - return true + }); + + return true; + } updateTopic(serverId, topicId, newTopic, newQos) { - const server = this.servers.get(serverId) - if (!server) return false - const t = server.topics.find((item) => item.id === topicId) - if (!t) return false - const topicChanged = t.topic !== newTopic + let server = this.servers.get(serverId); + + if (!server) { + return false; + } + + let t = server.topics.find((item) => item.id === topicId); + + if (!t) { + return false; + } + + let topicChanged = t.topic !== newTopic; + if ( topicChanged && server.topics.find((item) => item.topic === newTopic && item.id !== topicId) ) { - return false + return false; } - const oldTopic = t.topic - const wasSubscribed = t.subscribed + let oldTopic = t.topic; + let wasSubscribed = t.subscribed; // 如果主题名变更且已订阅,先取消旧主题订阅 if (topicChanged && wasSubscribed) { - this.unsubscribe(serverId, oldTopic) + this.unsubscribe(serverId, oldTopic); } - t.topic = newTopic - t.qos = newQos + t.topic = newTopic; + t.qos = newQos; // 如果已连接且此前已订阅,重新订阅新主题 if (server.status === 'connected' && wasSubscribed) { - this._doSubscribe(serverId, newTopic, { qos: newQos }) + this._doSubscribe(serverId, newTopic, { qos: newQos }); } - this.saveData() + this.saveData(); this.notify('subscription-change', { serverId, topic: newTopic, subscribed: t.subscribed, - topicId - }) - return true + topicId, + }); + + return true; + } subscribeAll(serverId) { - const client = this.clients.get(serverId) - const server = this.servers.get(serverId) - if (!client || !server || server.status !== 'connected') return false - const topicsToSubscribe = server.topics.filter((t) => !t.subscribed) - if (topicsToSubscribe.length === 0) return true + let client = this.clients.get(serverId); + let server = this.servers.get(serverId); - const subMap = Object.fromEntries(topicsToSubscribe.map((t) => [t.topic, { qos: t.qos }])) + if (!client || !server || server.status !== 'connected') { + return false; + } + + let topicsToSubscribe = server.topics.filter((t) => !t.subscribed); + + if (topicsToSubscribe.length === 0) { + return true; + } + + let subMap = Object.fromEntries(topicsToSubscribe.map((t) => [t.topic, { qos: t.qos }])); client.subscribe(subMap, (err) => { if (err) { - console.error('批量订阅失败:', err.message) + console.error('批量订阅失败:', err.message); topicsToSubscribe.forEach((t) => { - this.notify('subscription-error', { serverId, topic: t.topic, error: err.message }) - }) + this.notify('subscription-error', { serverId, topic: t.topic, error: err.message }); + }); } else { topicsToSubscribe.forEach((t) => { - t.subscribed = true + t.subscribed = true; this.notify('subscription-change', { serverId, topic: t.topic, subscribed: true, - topicId: t.id - }) - }) + topicId: t.id, + }); + }); } - }) - return true + }); + + return true; + } // ==================== 消息发布 ==================== - publish(serverId, topic, payload, opts = {}) { - const client = this.clients.get(serverId) - const server = this.servers.get(serverId) - if (!client || !server || server.status !== 'connected') return false - const pubOpts = { - qos: opts.qos ?? 1, - retain: opts.retain ?? false + publish(serverId, topic, payload, opts = {}) { + + let client = this.clients.get(serverId); + let server = this.servers.get(serverId); + + if (!client || !server || server.status !== 'connected') { + return false; } + let pubOpts = { + qos: opts.qos ?? 1, + retain: opts.retain ?? false, + }; + client.publish(topic, payload, pubOpts, (err) => { if (err) { - console.error(`发布失败 [${topic}]:`, err.message) - this.notify('publish-error', { serverId, topic, error: err.message }) + console.error(`发布失败 [${topic}]:`, err.message); + this.notify('publish-error', { serverId, topic, error: err.message }); } - }) + }); // 记录发布历史 - const pubEntry = { + let pubEntry = { time: new Date().toLocaleTimeString('zh-CN', { hour12: false }) + '.' + @@ -460,55 +579,70 @@ class MqttManager { payload, qos: pubOpts.qos, retain: pubOpts.retain, - direction: 'pub' + direction: 'pub', + }; + + let history = this.publishHistory.get(serverId) || []; + + history.unshift(pubEntry); + + if (history.length > 50) { + history.pop(); } - const history = this.publishHistory.get(serverId) || [] - history.unshift(pubEntry) - if (history.length > 50) history.pop() - this.publishHistory.set(serverId, history) + + this.publishHistory.set(serverId, history); // 同时添加到消息日志 - const pubMsgId = this.nextMsgId() - const msgs = this.messages.get(serverId) || [] + let pubMsgId = this.nextMsgId(); + let msgs = this.messages.get(serverId) || []; + msgs.unshift({ ...pubEntry, - id: pubMsgId - }) - if (msgs.length > 500) msgs.pop() - this.messages.set(serverId, msgs) + id: pubMsgId, + }); + + if (msgs.length > 500) { + msgs.pop(); + } + + this.messages.set(serverId, msgs); this.notify('message', { serverId, message: { ...pubEntry, - id: pubMsgId - } - }) + id: pubMsgId, + }, + }); + + this.saveData(); + + return true; - this.saveData() - return true } // ==================== 消息管理 ==================== + getMessages(serverId) { - return this.messages.get(serverId) || [] + return this.messages.get(serverId) || []; } getPublishHistory(serverId) { - return this.publishHistory.get(serverId) || [] + return this.publishHistory.get(serverId) || []; } clearMessages(serverId) { - this.messages.set(serverId, []) - this.saveData() + this.messages.set(serverId, []); + this.saveData(); } clearPublishHistory(serverId) { - this.publishHistory.set(serverId, []) - this.saveData() + this.publishHistory.set(serverId, []); + this.saveData(); } + } // 单例 -const mqttManager = new MqttManager() -export default mqttManager +const mqttManager = new MqttManager(); +export default mqttManager; diff --git a/app/src/preload/index.js b/app/src/preload/index.js index 0d414cb..fa50a05 100644 --- a/app/src/preload/index.js +++ b/app/src/preload/index.js @@ -1,8 +1,9 @@ -import { contextBridge, ipcRenderer } from 'electron' -import { electronAPI } from '@electron-toolkit/preload' +import { contextBridge, ipcRenderer } from 'electron'; +import { electronAPI } from '@electron-toolkit/preload'; // Custom APIs for renderer const api = { + // 服务器管理 getServers: () => ipcRenderer.invoke('mqtt:get-servers'), addServer: (config) => ipcRenderer.invoke('mqtt:add-server', config), @@ -34,43 +35,44 @@ const api = { // 事件监听 onStatusChange: (callback) => { - const handler = (_, data) => callback(data) - ipcRenderer.on('mqtt:status-change', handler) - return () => ipcRenderer.removeListener('mqtt:status-change', handler) + const handler = (_, data) => callback(data); + ipcRenderer.on('mqtt:status-change', handler); + return () => ipcRenderer.removeListener('mqtt:status-change', handler); }, onMessage: (callback) => { - const handler = (_, data) => callback(data) - ipcRenderer.on('mqtt:message', handler) - return () => ipcRenderer.removeListener('mqtt:message', handler) + const handler = (_, data) => callback(data); + ipcRenderer.on('mqtt:message', handler); + return () => ipcRenderer.removeListener('mqtt:message', handler); }, onSubscriptionChange: (callback) => { - const handler = (_, data) => callback(data) - ipcRenderer.on('mqtt:subscription-change', handler) - return () => ipcRenderer.removeListener('mqtt:subscription-change', handler) + const handler = (_, data) => callback(data); + ipcRenderer.on('mqtt:subscription-change', handler); + return () => ipcRenderer.removeListener('mqtt:subscription-change', handler); }, onSubscriptionError: (callback) => { - const handler = (_, data) => callback(data) - ipcRenderer.on('mqtt:subscription-error', handler) - return () => ipcRenderer.removeListener('mqtt:subscription-error', handler) + const handler = (_, data) => callback(data); + ipcRenderer.on('mqtt:subscription-error', handler); + return () => ipcRenderer.removeListener('mqtt:subscription-error', handler); }, onPublishError: (callback) => { - const handler = (_, data) => callback(data) - ipcRenderer.on('mqtt:publish-error', handler) - return () => ipcRenderer.removeListener('mqtt:publish-error', handler) - } -} + const handler = (_, data) => callback(data); + ipcRenderer.on('mqtt:publish-error', handler); + return () => ipcRenderer.removeListener('mqtt:publish-error', handler); + }, + +}; // Use `contextBridge` APIs to expose Electron APIs to // renderer only if context isolation is enabled, otherwise // just add to the DOM global. if (process.contextIsolated) { try { - contextBridge.exposeInMainWorld('electron', electronAPI) - contextBridge.exposeInMainWorld('api', api) + contextBridge.exposeInMainWorld('electron', electronAPI); + contextBridge.exposeInMainWorld('api', api); } catch (error) { - console.error(error) + console.error(error); } } else { - window.electron = electronAPI - window.api = api + window.electron = electronAPI; + window.api = api; } diff --git a/app/src/renderer/index.html b/app/src/renderer/index.html index f5955ae..38ab89c 100644 --- a/app/src/renderer/index.html +++ b/app/src/renderer/index.html @@ -1,16 +1,16 @@ - - + + + Frost MQTT Client -
- \ No newline at end of file + diff --git a/app/src/renderer/src/App.vue b/app/src/renderer/src/App.vue index 7cb6c31..97a5161 100644 --- a/app/src/renderer/src/App.vue +++ b/app/src/renderer/src/App.vue @@ -1,41 +1,33 @@