From b23cf4d854c798c5c390caf24a1b0b5a4a9f6488 Mon Sep 17 00:00:00 2001 From: Frost-ZX Date: Sun, 19 Jul 2026 23:08:58 +0800 Subject: [PATCH] =?UTF-8?q?chore:=20=E5=AE=8C=E5=96=84=E6=B3=A8=E9=87=8A?= =?UTF-8?q?=EF=BC=8C=E6=B7=BB=E5=8A=A0=20JSDoc?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/jsconfig.json | 24 ++ app/src/main/index.js | 37 +-- app/src/main/ipc-handlers.js | 15 +- app/src/main/mqtt-manager.js | 187 ++++++++++++ app/src/preload/index.js | 9 +- app/src/renderer/src/App.vue | 20 ++ .../renderer/src/components/ServerModal.vue | 12 + app/src/renderer/src/components/Sidebar.vue | 30 ++ app/src/renderer/src/main.js | 3 + app/src/renderer/src/stores/mqtt.js | 276 +++++++++++++++++- app/src/renderer/src/views/Dashboard.vue | 15 +- app/src/renderer/src/views/Messages.vue | 18 ++ app/src/renderer/src/views/Publish.vue | 24 ++ app/src/renderer/src/views/Subscribe.vue | 121 ++++++-- app/types/jsdoc.d.ts | 114 ++++++++ 15 files changed, 846 insertions(+), 59 deletions(-) create mode 100644 app/jsconfig.json create mode 100644 app/types/jsdoc.d.ts diff --git a/app/jsconfig.json b/app/jsconfig.json new file mode 100644 index 0000000..2fc4a74 --- /dev/null +++ b/app/jsconfig.json @@ -0,0 +1,24 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "ESNext", + "moduleResolution": "bundler", + "allowJs": true, + "checkJs": false, + "noEmit": true, + "strict": false, + "baseUrl": ".", + "paths": { + "@types/*": ["types/*"] + } + }, + "include": [ + "src/**/*", + "types/**/*" + ], + "exclude": [ + "node_modules", + "dist", + "out" + ] +} diff --git a/app/src/main/index.js b/app/src/main/index.js index 3a63303..708b589 100644 --- a/app/src/main/index.js +++ b/app/src/main/index.js @@ -5,9 +5,13 @@ import { registerIpcHandlers } from './ipc-handlers.js'; import icon from '../../resources/icon.png?asset'; +/** + * 创建主窗口 + * @returns {BrowserWindow} 主窗口实例 + */ function createWindow() { - // Create the browser window. + // 创建浏览器窗口 const mainWindow = new BrowserWindow({ width: 1200, height: 800, @@ -22,17 +26,19 @@ function createWindow() { }, }); + // 窗口准备就绪后显示,避免白屏 mainWindow.on('ready-to-show', () => { mainWindow.show(); }); + // 拦截新窗口打开请求,使用系统默认浏览器打开外部链接 mainWindow.webContents.setWindowOpenHandler((details) => { 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. + // 基于 electron-vite cli 的热更新 + // 开发环境加载远程 URL,生产环境加载本地 HTML 文件 if (is.dev && process.env['ELECTRON_RENDERER_URL']) { mainWindow.loadURL(process.env['ELECTRON_RENDERER_URL']); } else { @@ -41,17 +47,16 @@ function createWindow() { } -// 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. +// 当 Electron 完成初始化并准备好创建浏览器窗口时调用此方法 +// 某些 API 只能在此事件触发后使用 app.whenReady().then(() => { - // Set app user model id for windows + // 为 Windows 设置应用用户模型 ID 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 + // 开发环境通过 F12 打开/关闭 DevTools + // 生产环境忽略 CommandOrControl + R 刷新 + // 详见 https://github.com/alex8088/electron-toolkit/tree/master/packages/utils app.on('browser-window-created', (_, window) => { optimizer.watchWindowShortcuts(window); }); @@ -62,8 +67,7 @@ app.whenReady().then(() => { 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. + // 在 macOS 上,当点击应用图标且没有其他窗口打开时,通常会重新创建一个窗口 if (BrowserWindow.getAllWindows().length === 0) { createWindow(); } @@ -71,14 +75,13 @@ app.whenReady().then(() => { }); -// 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. +// 当所有窗口关闭时退出应用,macOS 除外 +// 在 macOS 上,应用及其菜单栏通常会保持活动状态,直到用户通过 Cmd + Q 显式退出 app.on('window-all-closed', () => { if (process.platform !== 'darwin') { 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 f566162..c3bb23e 100644 --- a/app/src/main/ipc-handlers.js +++ b/app/src/main/ipc-handlers.js @@ -2,15 +2,26 @@ import { ipcMain, BrowserWindow } from 'electron'; import mqttManager from './mqtt-manager.js'; +/** + * 注册所有 IPC 处理器 + * 将渲染进程的请求转发给 MQTT 管理器,并把 MQTT 事件推送给渲染进程 + */ export function registerIpcHandlers() { - // 获取主窗口(用于推送事件) + /** + * 获取当前所有窗口中的第一个主窗口 + * @returns {BrowserWindow|null} 主窗口实例或 null + */ function getMainWindow() { const windows = BrowserWindow.getAllWindows(); return windows.length > 0 ? windows[0] : null; } - // 向渲染进程发送事件 + /** + * 向渲染进程发送事件 + * @param {string} channel - IPC 通道名称 + * @param {*} data - 要发送的数据 + */ function sendToRenderer(channel, data) { const win = getMainWindow(); if (win && !win.isDestroyed()) { diff --git a/app/src/main/mqtt-manager.js b/app/src/main/mqtt-manager.js index e0d3c31..6bde1ed 100644 --- a/app/src/main/mqtt-manager.js +++ b/app/src/main/mqtt-manager.js @@ -4,6 +4,27 @@ import { join } from 'path'; import mqtt from 'mqtt'; +// ==================== 类型定义 ==================== + +/** + * 从公共类型定义文件引入 MQTT 相关类型 + * @typedef {import('@types/jsdoc').MqttQoS} MqttQoS + * @typedef {import('@types/jsdoc').MqttServerStatus} MqttServerStatus + * @typedef {import('@types/jsdoc').MqttTopic} MqttTopic + * @typedef {import('@types/jsdoc').TopicConfig} TopicConfig + * @typedef {import('@types/jsdoc').ServerConfig} ServerConfig + * @typedef {import('@types/jsdoc').MqttServer} MqttServer + * @typedef {import('@types/jsdoc').PublishOptions} PublishOptions + * @typedef {import('@types/jsdoc').MqttMessage} MqttMessage + * @typedef {import('@types/jsdoc').PublishRecord} PublishRecord + */ + +// ==================== 类定义 ==================== + +/** + * MQTT 管理器 + * 负责 MQTT 服务器配置持久化、连接管理、主题订阅/发布、消息存储等核心逻辑 + */ class MqttManager { constructor() { @@ -17,6 +38,10 @@ class MqttManager { this.loadData(); } + /** + * 生成唯一消息 id + * @returns {string} 消息唯一标识 + */ nextMsgId() { this.msgIdCounter = (this.msgIdCounter + 1) % Number.MAX_SAFE_INTEGER; return 'msg_' + Date.now() + '_' + this.msgIdCounter; @@ -24,6 +49,10 @@ class MqttManager { // ==================== 数据持久化 ==================== + /** + * 获取用户数据目录,若不存在则创建 + * @returns {string} 用户数据目录路径 + */ getDataDir() { let dir = app.getPath('userData'); @@ -36,6 +65,10 @@ class MqttManager { } + /** + * 从本地文件加载服务器、消息和发布历史数据 + * 文件不存在或解析失败时初始化默认服务器 + */ loadData() { try { if (existsSync(this.dataPath)) { @@ -76,6 +109,9 @@ class MqttManager { } } + /** + * 将当前服务器、消息和发布历史保存到本地文件 + */ saveData() { try { @@ -96,6 +132,9 @@ class MqttManager { } } + /** + * 初始化默认示例服务器 + */ initDefaultServers() { let defaults = [ @@ -133,14 +172,27 @@ class MqttManager { // ==================== 监听器 ==================== + /** + * 添加 MQTT 事件监听器 + * @param {Function} callback - 回调函数,接收 (event, data) 参数 + */ addListener(callback) { this.listeners.add(callback); } + /** + * 移除 MQTT 事件监听器 + * @param {Function} callback - 要移除的回调函数 + */ removeListener(callback) { this.listeners.delete(callback); } + /** + * 通知所有监听器 + * @param {string} event - 事件名称 + * @param {*} data - 事件数据 + */ notify(event, data) { this.listeners.forEach((cb) => { try { @@ -153,6 +205,10 @@ class MqttManager { // ==================== 服务器管理 ==================== + /** + * 获取所有服务器列表 + * @returns {MqttServer[]} 服务器配置数组 + */ getServers() { return Array.from(this.servers.values()).map((s) => ({ ...s, @@ -160,10 +216,19 @@ class MqttManager { })); } + /** + * 根据 id 获取服务器配置 + * @param {string} id - 服务器 id + * @returns {MqttServer | null} 服务器配置或 null + */ getServer(id) { return this.servers.get(id) || null; } + /** + * 获取服务器配置字段 schema + * @returns {Record} 字段名到处理函数的映射 + */ getServerConfigSchema() { return { name: (c, e) => c.name ?? e?.name, @@ -181,6 +246,13 @@ class MqttManager { }; } + /** + * 根据配置构建服务器字段 + * @param {ServerConfig} config - 新配置 + * @param {MqttServer | null} existing - 已有服务器配置 + * @param {{ keepExistingPassword?: boolean }} options - 可选项 + * @returns {ServerConfig} 处理后的字段对象 + */ buildServerFields(config, existing = null, options = {}) { let schema = this.getServerConfigSchema(); let fields = {}; @@ -190,6 +262,11 @@ class MqttManager { return fields; } + /** + * 构建主题对象 + * @param {TopicConfig} t - 原始主题数据 + * @returns {MqttTopic} 标准化后的主题对象 + */ buildTopic(t) { return { id: t.id || 'topic_' + Date.now() + '_' + Math.random().toString(36).slice(2, 6), @@ -199,6 +276,11 @@ class MqttManager { }; } + /** + * 添加服务器 + * @param {ServerConfig} config - 服务器配置 + * @returns {MqttServer} 创建后的服务器对象 + */ addServer(config) { let id = 'srv_' + Date.now() + '_' + Math.random().toString(36).slice(2, 6); @@ -218,6 +300,12 @@ class MqttManager { } + /** + * 更新服务器配置 + * @param {string} id - 服务器 id + * @param {ServerConfig} config - 新的服务器配置 + * @returns {MqttServer | null} 更新后的服务器对象或 null + */ updateServer(id, config) { let server = this.servers.get(id); @@ -234,6 +322,10 @@ class MqttManager { } + /** + * 删除服务器,并断开其连接 + * @param {string} id - 服务器 id + */ deleteServer(id) { // 先断开连接 this.disconnect(id); @@ -243,6 +335,10 @@ class MqttManager { this.saveData(); } + /** + * 导出所有服务器配置(不含运行时状态) + * @returns {ServerConfig[]} 服务器配置数组 + */ exportServers() { let schemaKeys = Object.keys(this.getServerConfigSchema()); return Array.from(this.servers.values()).map((s) => { @@ -255,6 +351,11 @@ class MqttManager { }); } + /** + * 构建导出用的主题对象 + * @param {MqttTopic} t - 主题对象 + * @returns {TopicConfig} 导出主题对象 + */ buildTopicExport(t) { return { id: t.id, @@ -263,6 +364,11 @@ class MqttManager { }; } + /** + * 导入服务器配置列表,会覆盖现有配置 + * @param {ServerConfig[]} serverList - 服务器配置数组 + * @returns {MqttServer[]} 导入后的服务器列表 + */ importServers(serverList) { if (!Array.isArray(serverList)) { throw new Error('导入格式错误:必须是服务器配置数组'); @@ -302,6 +408,11 @@ class MqttManager { // ==================== 连接管理 ==================== + /** + * 连接指定服务器 + * @param {string} id - 服务器 id + * @returns {boolean} 是否成功发起连接 + */ connect(id) { let server = this.servers.get(id); @@ -399,6 +510,10 @@ class MqttManager { } } + /** + * 断开指定服务器的连接 + * @param {string} id - 服务器 id + */ disconnect(id) { let client = this.clients.get(id); @@ -421,6 +536,14 @@ class MqttManager { } // ==================== 订阅管理 ==================== + + /** + * 执行订阅操作(内部方法) + * @param {string} serverId - 服务器 id + * @param {string} topic - 主题名称 + * @param {PublishOptions} opts - 订阅选项 + * @returns {boolean} 是否发起订阅 + */ _doSubscribe(serverId, topic, opts) { let client = this.clients.get(serverId); @@ -447,6 +570,13 @@ class MqttManager { } + /** + * 为主题列表添加新主题 + * @param {string} serverId - 服务器 id + * @param {string} topic - 主题名称 + * @param {MqttQoS} qos - QoS 等级 + * @returns {MqttTopic | null} 添加后的主题对象或 null + */ addTopic(serverId, topic, qos = 1) { let server = this.servers.get(serverId); if (!server) { @@ -472,6 +602,12 @@ class MqttManager { } + /** + * 移除主题并取消订阅 + * @param {string} serverId - 服务器 id + * @param {string} topic - 主题名称 + * @returns {boolean} 是否移除成功 + */ removeTopic(serverId, topic) { let server = this.servers.get(serverId); @@ -491,6 +627,12 @@ class MqttManager { } + /** + * 订阅指定主题 + * @param {string} serverId - 服务器 id + * @param {string} topic - 主题名称 + * @returns {boolean} 是否发起订阅 + */ subscribe(serverId, topic) { let server = this.servers.get(serverId); @@ -509,6 +651,12 @@ class MqttManager { } + /** + * 取消订阅指定主题 + * @param {string} serverId - 服务器 id + * @param {string} topic - 主题名称 + * @returns {boolean} 是否发起取消订阅 + */ unsubscribe(serverId, topic) { let client = this.clients.get(serverId); @@ -532,6 +680,14 @@ class MqttManager { } + /** + * 更新主题信息,必要时取消旧主题并重新订阅新主题 + * @param {string} serverId - 服务器 id + * @param {string} topicId - 主题 id + * @param {string} newTopic - 新的主题名称 + * @param {number} newQos - 新的 QoS 等级 + * @returns {boolean} 是否更新成功 + */ updateTopic(serverId, topicId, newTopic, newQos) { let server = this.servers.get(serverId); @@ -583,6 +739,11 @@ class MqttManager { } + /** + * 批量订阅服务器下所有未订阅的主题 + * @param {string} serverId - 服务器 id + * @returns {boolean} 是否发起批量订阅 + */ subscribeAll(serverId) { let client = this.clients.get(serverId); @@ -625,6 +786,14 @@ class MqttManager { // ==================== 消息发布 ==================== + /** + * 向指定主题发布消息 + * @param {string} serverId - 服务器 id + * @param {string} topic - 目标主题 + * @param {string} payload - 消息内容 + * @param {PublishOptions} opts - 发布选项 + * @returns {boolean} 是否发起发布 + */ publish(serverId, topic, payload, opts = {}) { let client = this.clients.get(serverId); @@ -700,19 +869,37 @@ class MqttManager { // ==================== 消息管理 ==================== + /** + * 获取指定服务器的消息列表 + * @param {string} serverId - 服务器 id + * @returns {MqttMessage[]} 消息数组 + */ getMessages(serverId) { return this.messages.get(serverId) || []; } + /** + * 获取指定服务器的发布历史 + * @param {string} serverId - 服务器 id + * @returns {PublishRecord[]} 发布历史数组 + */ getPublishHistory(serverId) { return this.publishHistory.get(serverId) || []; } + /** + * 清空指定服务器的消息列表 + * @param {string} serverId - 服务器 id + */ clearMessages(serverId) { this.messages.set(serverId, []); this.saveData(); } + /** + * 清空指定服务器的发布历史 + * @param {string} serverId - 服务器 id + */ clearPublishHistory(serverId) { this.publishHistory.set(serverId, []); this.saveData(); diff --git a/app/src/preload/index.js b/app/src/preload/index.js index a5f4c43..0523096 100644 --- a/app/src/preload/index.js +++ b/app/src/preload/index.js @@ -1,7 +1,10 @@ import { contextBridge, ipcRenderer } from 'electron'; import { electronAPI } from '@electron-toolkit/preload'; -// Custom APIs for renderer +/** + * 渲染进程可调用的自定义 API + * 封装了所有与主进程通信的 IPC 调用及事件监听 + */ const api = { // 服务器管理 @@ -64,9 +67,7 @@ const api = { }; -// Use `contextBridge` APIs to expose Electron APIs to -// renderer only if context isolation is enabled, otherwise -// just add to the DOM global. +// 根据是否启用上下文隔离,使用 contextBridge 暴露 API 或挂载到 window if (process.contextIsolated) { try { contextBridge.exposeInMainWorld('electron', electronAPI); diff --git a/app/src/renderer/src/App.vue b/app/src/renderer/src/App.vue index a88b46b..11694e4 100644 --- a/app/src/renderer/src/App.vue +++ b/app/src/renderer/src/App.vue @@ -10,8 +10,10 @@ import Subscribe from './views/Subscribe.vue'; import Publish from './views/Publish.vue'; import Messages from './views/Messages.vue'; +// 全局状态 const store = useMqttStore(); +// 主题覆盖配置 const themeOverrides = { common: { primaryColor: '#0ea5a0', @@ -37,28 +39,46 @@ const themeOverrides = { }, }; +// 当前激活的标签页 const activeTab = ref('dashboard'); +// 是否显示服务器编辑弹窗 const showServerModal = ref(false); +// 当前编辑的服务器数据,null 表示新增 const editServerData = ref(null); +// 组件挂载时初始化 Store onMounted(async () => { await store.init(); }); +/** + * 打开新增服务器弹窗 + */ function handleAddServer() { editServerData.value = null; showServerModal.value = true; } +/** + * 打开编辑服务器弹窗 + * @param {import('@types/jsdoc').MqttServer} server - 要编辑的服务器对象 + */ function handleEditServer(server) { editServerData.value = server; showServerModal.value = true; } +/** + * 服务器保存后的回调 + */ function handleServerSaved() { // 刷新数据 } +/** + * 切换标签页 + * @param {string} tab - 标签页名称 + */ function handleSwitchTab(tab) { activeTab.value = tab; } diff --git a/app/src/renderer/src/components/ServerModal.vue b/app/src/renderer/src/components/ServerModal.vue index 7d7eb58..44baea0 100644 --- a/app/src/renderer/src/components/ServerModal.vue +++ b/app/src/renderer/src/components/ServerModal.vue @@ -3,16 +3,19 @@ import { ref, watch } from 'vue'; import { NModal, NForm, NGrid, NFormItemGi, NInput, NInputNumber, NSelect, NSwitch, NButton, useMessage } from 'naive-ui'; import { useMqttStore } from '../stores/mqtt.js'; +// 弹窗显示状态与编辑数据 const props = defineProps({ show: Boolean, editServer: Object, }); +// 更新 show、保存完成事件 const emit = defineEmits(['update:show', 'saved']); const store = useMqttStore(); const msg = useMessage(); +// 表单引用与数据 const formRef = ref(null); const form = ref({ name: '', @@ -29,12 +32,14 @@ const form = ref({ reconnectInterval: 5000, }); +// 表单校验规则 const rules = { name: { required: true, message: '请输入服务器名称', trigger: 'blur' }, host: { required: true, message: '请输入主机地址', trigger: 'blur' }, port: { required: true, type: 'number', message: '请输入端口号', trigger: 'blur' }, }; +// 协议下拉选项 const protocolOptions = [ { label: 'mqtt:// (默认)', value: 'mqtt' }, { label: 'mqtts:// (TLS)', value: 'mqtts' }, @@ -42,8 +47,12 @@ const protocolOptions = [ { label: 'wss:// (WebSocket TLS)', value: 'wss' }, ]; +// 是否为编辑模式 const isEdit = ref(false); +/** + * 监听弹窗显示状态,打开时初始化表单 + */ watch( () => props.show, (val) => { @@ -85,6 +94,9 @@ watch( } ); +/** + * 保存服务器配置 + */ async function handleSave() { try { diff --git a/app/src/renderer/src/components/Sidebar.vue b/app/src/renderer/src/components/Sidebar.vue index 9601b99..471b540 100644 --- a/app/src/renderer/src/components/Sidebar.vue +++ b/app/src/renderer/src/components/Sidebar.vue @@ -3,12 +3,21 @@ import { ref } from 'vue'; import { NLayoutSider, NButton, NTag, useMessage, useDialog } from 'naive-ui'; import { useMqttStore } from '../stores/mqtt.js'; +// 组件事件定义 const emit = defineEmits(['add-server', 'edit-server']); + +// 全局状态与消息/对话框 API const store = useMqttStore(); const message = useMessage(); const dialog = useDialog(); + +// 文件输入控件引用,用于导入配置 const fileInput = ref(null); +/** + * 切换服务器连接状态 + * @param {import('@types/jsdoc').MqttServer} server - 服务器对象 + */ async function handleConnect(server) { if (server.status === 'connected') { await store.disconnectServer(server.id); @@ -18,10 +27,18 @@ async function handleConnect(server) { } } +/** + * 编辑服务器 + * @param {import('@types/jsdoc').MqttServer} server - 服务器对象 + */ function handleEdit(server) { emit('edit-server', server); } +/** + * 删除服务器,弹出确认对话框 + * @param {import('@types/jsdoc').MqttServer} server - 服务器对象 + */ function handleDelete(server) { dialog.warning({ title: '确认删除', @@ -35,6 +52,9 @@ function handleDelete(server) { }); } +/** + * 导出服务器配置到本地 JSON 文件 + */ async function handleExport() { try { let data = await store.exportServers(); @@ -51,10 +71,17 @@ async function handleExport() { } } +/** + * 触发文件导入点击 + */ function handleImportClick() { fileInput.value?.click(); } +/** + * 处理导入的 JSON 配置文件 + * @param {Event} event - 文件选择事件 + */ async function handleImportFile(event) { let file = event.target.files?.[0]; if (!file) return; @@ -86,6 +113,7 @@ async function handleImportFile(event) { >