chore: 完善注释,添加 JSDoc
This commit is contained in:
24
app/jsconfig.json
Normal file
24
app/jsconfig.json
Normal file
@@ -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"
|
||||
]
|
||||
}
|
||||
@@ -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.
|
||||
// 在这个文件中,你可以包含应用特定的主进程代码
|
||||
// 也可以将它们放到单独的文件中,然后在这里引入
|
||||
|
||||
@@ -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()) {
|
||||
|
||||
@@ -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<string, Function>} 字段名到处理函数的映射
|
||||
*/
|
||||
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();
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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) {
|
||||
>
|
||||
<div class="sidebar-container">
|
||||
|
||||
<!-- 侧边栏头部 Logo 与标题 -->
|
||||
<div class="sidebar-header">
|
||||
<div class="sidebar-logo">F</div>
|
||||
<div>
|
||||
@@ -94,6 +122,7 @@ async function handleImportFile(event) {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 服务器列表 -->
|
||||
<div class="server-list">
|
||||
<div
|
||||
v-for="srv in store.servers.value"
|
||||
@@ -155,6 +184,7 @@ async function handleImportFile(event) {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 侧边栏底部操作按钮 -->
|
||||
<div class="sidebar-footer">
|
||||
<n-button
|
||||
type="primary"
|
||||
|
||||
@@ -2,6 +2,9 @@ import { createApp } from 'vue';
|
||||
|
||||
import App from './App.vue';
|
||||
|
||||
/**
|
||||
* 创建并挂载 Vue 应用
|
||||
*/
|
||||
const app = createApp(App);
|
||||
|
||||
app.mount('#app');
|
||||
|
||||
@@ -1,15 +1,117 @@
|
||||
import { ref, computed } from 'vue';
|
||||
|
||||
// 全局状态(单例)
|
||||
// ==================== 类型定义 ====================
|
||||
|
||||
/**
|
||||
* 从公共类型定义文件引入 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
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef {Object} StatusChangeData 服务器状态变更事件数据
|
||||
* @property {string} id 服务器 id
|
||||
* @property {MqttServerStatus} status 新状态
|
||||
* @property {string} [error] 错误信息(状态为 error 时存在)
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef {Object} MessageEventData 消息接收事件数据
|
||||
* @property {string} serverId 所属服务器 id
|
||||
* @property {MqttMessage} message 消息对象
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef {Object} SubscriptionChangeData 订阅状态变更事件数据
|
||||
* @property {string} serverId 所属服务器 id
|
||||
* @property {string} topic 主题名称
|
||||
* @property {string} [topicId] 主题 id
|
||||
* @property {boolean} subscribed 是否已订阅
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef {Object} MqttStore
|
||||
* @property {import('vue').Ref<MqttServer[]>} servers 所有服务器列表
|
||||
* @property {import('vue').Ref<string | null>} activeServerId 当前活动服务器 ID
|
||||
* @property {import('vue').ComputedRef<MqttServer | null>} activeServer 当前活动服务器对象
|
||||
* @property {import('vue').Ref<MqttMessage[]>} messages 当前活动服务器消息列表
|
||||
* @property {import('vue').Ref<PublishRecord[]>} publishHistory 当前活动服务器发布历史
|
||||
* @property {import('vue').Ref<boolean>} loading 全局加载状态
|
||||
* @property {import('vue').ComputedRef<MqttServer[]>} connectedServers 已连接服务器列表
|
||||
* @property {import('vue').ComputedRef<number>} totalTopics 所有服务器主题总数
|
||||
* @property {import('vue').ComputedRef<number>} subscribedTopics 已订阅主题总数
|
||||
* @property {import('vue').ComputedRef<number>} totalMessages 当前活动服务器消息总数
|
||||
* @property {() => Promise<void>} init 初始化 Store
|
||||
* @property {(id: string | null) => void} setActiveServer 设置当前活动服务器
|
||||
* @property {(config: ServerConfig) => Promise<MqttServer>} addServer 添加服务器
|
||||
* @property {(id: string, config: ServerConfig) => Promise<MqttServer | null>} updateServer 更新服务器
|
||||
* @property {(id: string) => Promise<void>} deleteServer 删除服务器
|
||||
* @property {() => Promise<ServerConfig[]>} exportServers 导出服务器配置
|
||||
* @property {(serverList: ServerConfig[]) => Promise<MqttServer[]>} importServers 导入服务器配置
|
||||
* @property {(id: string) => Promise<boolean>} connectServer 连接服务器
|
||||
* @property {(id: string) => Promise<void>} disconnectServer 断开服务器
|
||||
* @property {(serverId: string, topic: string, qos: MqttQoS) => Promise<MqttTopic | null>} addTopic 添加主题
|
||||
* @property {(serverId: string, topic: string) => Promise<void>} removeTopic 移除主题
|
||||
* @property {(serverId: string, topic: string) => Promise<boolean>} subscribeTopic 订阅主题
|
||||
* @property {(serverId: string, topic: string) => Promise<boolean>} unsubscribeTopic 取消订阅主题
|
||||
* @property {(serverId: string) => Promise<boolean>} subscribeAllTopics 批量订阅所有主题
|
||||
* @property {(serverId: string, topicId: string, topic: string, qos: MqttQoS) => Promise<boolean>} updateTopic 更新主题
|
||||
* @property {(serverId: string, topic: string, payload: string, opts: PublishOptions) => Promise<boolean>} publishMessage 发布消息
|
||||
* @property {() => Promise<void>} refreshMessages 刷新消息列表
|
||||
* @property {() => Promise<void>} refreshPublishHistory 刷新发布历史
|
||||
* @property {() => Promise<void>} clearMessages 清空消息列表
|
||||
* @property {() => Promise<void>} clearPublishHistory 清空发布历史
|
||||
*/
|
||||
|
||||
// ==================== 全局状态(单例) ====================
|
||||
|
||||
/**
|
||||
* 所有已配置的服务器列表,按名称排序
|
||||
* @type {import('vue').Ref<MqttServer[]>}
|
||||
*/
|
||||
const servers = ref([]);
|
||||
|
||||
/**
|
||||
* 当前在界面中选中的服务器 ID
|
||||
* @type {import('vue').Ref<string | null>}
|
||||
*/
|
||||
const activeServerId = ref(null);
|
||||
|
||||
/**
|
||||
* 当前活动服务器的消息列表(包含订阅接收与本地发布的消息)
|
||||
* @type {import('vue').Ref<MqttMessage[]>}
|
||||
*/
|
||||
const messages = ref([]);
|
||||
|
||||
/**
|
||||
* 当前活动服务器的发布历史记录
|
||||
* @type {import('vue').Ref<PublishRecord[]>}
|
||||
*/
|
||||
const publishHistory = ref([]);
|
||||
|
||||
/**
|
||||
* Store 初始化或异步操作时的全局加载状态
|
||||
* @type {import('vue').Ref<boolean>}
|
||||
*/
|
||||
const loading = ref(false);
|
||||
|
||||
// 清理函数集合
|
||||
/**
|
||||
* IPC 监听器清理函数集合
|
||||
* @type {Array<() => void>}
|
||||
*/
|
||||
let cleanupFns = [];
|
||||
|
||||
/**
|
||||
* 设置主进程事件监听器
|
||||
* 包括状态变化、消息接收、订阅变化等事件
|
||||
*/
|
||||
function setupListeners() {
|
||||
|
||||
// 清理旧监听器
|
||||
@@ -17,61 +119,102 @@ function setupListeners() {
|
||||
cleanupFns = [];
|
||||
|
||||
cleanupFns.push(
|
||||
window.api.onStatusChange((data) => {
|
||||
const srv = servers.value.find((s) => s.id === data.id);
|
||||
window.api.onStatusChange((/** @type {StatusChangeData} */ data) => {
|
||||
|
||||
let srv = servers.value.find((s) => {
|
||||
return s.id === data.id;
|
||||
});
|
||||
|
||||
if (srv) {
|
||||
|
||||
srv.status = data.status;
|
||||
|
||||
if (data.status === 'disconnected') {
|
||||
srv.topics.forEach((t) => {
|
||||
t.subscribed = false;
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
})
|
||||
);
|
||||
|
||||
cleanupFns.push(
|
||||
window.api.onMessage((data) => {
|
||||
window.api.onMessage((/** @type {MessageEventData} */ data) => {
|
||||
if (data.serverId === activeServerId.value) {
|
||||
|
||||
messages.value.unshift(data.message);
|
||||
|
||||
if (messages.value.length > 500) {
|
||||
messages.value.pop();
|
||||
}
|
||||
|
||||
}
|
||||
})
|
||||
);
|
||||
|
||||
cleanupFns.push(
|
||||
window.api.onSubscriptionChange((data) => {
|
||||
window.api.onSubscriptionChange((/** @type {SubscriptionChangeData} */ data) => {
|
||||
if (data.serverId === activeServerId.value) {
|
||||
const srv = servers.value.find((s) => s.id === data.serverId);
|
||||
|
||||
let srv = servers.value.find((s) => {
|
||||
return s.id === data.serverId;
|
||||
});
|
||||
|
||||
if (srv) {
|
||||
const t = srv.topics.find((t) => t.topic === data.topic || t.id === data.topicId);
|
||||
|
||||
let t = srv.topics.find((t) => {
|
||||
return t.topic === data.topic || t.id === data.topicId;
|
||||
});
|
||||
|
||||
if (t) {
|
||||
t.subscribed = data.subscribed;
|
||||
if (data.topic && data.topic !== t.topic) {
|
||||
t.topic = data.topic;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
})
|
||||
);
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* MQTT 全局状态管理 Store
|
||||
* @returns {MqttStore} 包含状态、计算属性和方法的对象
|
||||
*/
|
||||
export function useMqttStore() {
|
||||
|
||||
/**
|
||||
* 当前选中的服务器
|
||||
* @type {import('vue').ComputedRef<MqttServer | null>}
|
||||
*/
|
||||
let activeServer = computed(
|
||||
() => servers.value.find((s) => s.id === activeServerId.value) || null
|
||||
);
|
||||
|
||||
/**
|
||||
* 已连接的服务器列表
|
||||
* @type {import('vue').ComputedRef<MqttServer[]>}
|
||||
*/
|
||||
let connectedServers = computed(() => servers.value.filter((s) => s.status === 'connected'));
|
||||
|
||||
/**
|
||||
* 所有服务器主题总数
|
||||
* @type {import('vue').ComputedRef<number>}
|
||||
*/
|
||||
let totalTopics = computed(() =>
|
||||
servers.value.reduce((sum, s) => sum + (s.topics?.length || 0), 0)
|
||||
);
|
||||
|
||||
/**
|
||||
* 已订阅主题总数
|
||||
* @type {import('vue').ComputedRef<number>}
|
||||
*/
|
||||
let subscribedTopics = computed(() =>
|
||||
servers.value.reduce(
|
||||
(sum, s) => sum + (s.topics?.filter((t) => t.subscribed)?.length || 0),
|
||||
@@ -79,13 +222,26 @@ export function useMqttStore() {
|
||||
)
|
||||
);
|
||||
|
||||
/**
|
||||
* 当前活动服务器消息总数
|
||||
* @type {import('vue').ComputedRef<number>}
|
||||
*/
|
||||
let totalMessages = computed(() => messages.value.length);
|
||||
|
||||
/**
|
||||
* 按服务器名称对服务器列表排序(中文环境)
|
||||
* @returns {void}
|
||||
*/
|
||||
function sortServers() {
|
||||
servers.value.sort((a, b) => a.name.localeCompare(b.name, 'zh-CN'));
|
||||
}
|
||||
|
||||
// ==================== 初始化 ====================
|
||||
|
||||
/**
|
||||
* 初始化 Store,从主进程加载服务器列表并设置监听
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
async function init() {
|
||||
|
||||
loading.value = true;
|
||||
@@ -107,6 +263,11 @@ export function useMqttStore() {
|
||||
}
|
||||
|
||||
// ==================== 服务器管理 ====================
|
||||
|
||||
/**
|
||||
* 设置当前活动服务器并刷新相关数据
|
||||
* @param {string|null} id - 服务器 id
|
||||
*/
|
||||
function setActiveServer(id) {
|
||||
|
||||
activeServerId.value = id;
|
||||
@@ -117,6 +278,11 @@ export function useMqttStore() {
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* 添加服务器
|
||||
* @param {ServerConfig} config - 服务器配置
|
||||
* @returns {Promise<MqttServer>} 添加后的服务器对象
|
||||
*/
|
||||
async function addServer(config) {
|
||||
|
||||
let server = await window.api.addServer(config);
|
||||
@@ -132,6 +298,12 @@ export function useMqttStore() {
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新服务器配置
|
||||
* @param {string} id - 服务器 id
|
||||
* @param {ServerConfig} config - 新的服务器配置
|
||||
* @returns {Promise<MqttServer | null>} 更新后的服务器对象或 null
|
||||
*/
|
||||
async function updateServer(id, config) {
|
||||
|
||||
let result = await window.api.updateServer(id, config);
|
||||
@@ -148,6 +320,11 @@ export function useMqttStore() {
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除服务器
|
||||
* @param {string} id - 服务器 id
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
async function deleteServer(id) {
|
||||
|
||||
await window.api.deleteServer(id);
|
||||
@@ -161,10 +338,19 @@ export function useMqttStore() {
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* 导出所有服务器配置
|
||||
* @returns {Promise<ServerConfig[]>} 服务器配置数组
|
||||
*/
|
||||
async function exportServers() {
|
||||
return await window.api.exportServers();
|
||||
}
|
||||
|
||||
/**
|
||||
* 导入服务器配置列表
|
||||
* @param {ServerConfig[]} serverList - 服务器配置数组
|
||||
* @returns {Promise<MqttServer[]>} 导入后的服务器列表
|
||||
*/
|
||||
async function importServers(serverList) {
|
||||
let data = await window.api.importServers(serverList);
|
||||
servers.value = data;
|
||||
@@ -175,15 +361,33 @@ export function useMqttStore() {
|
||||
return data;
|
||||
}
|
||||
|
||||
/**
|
||||
* 连接指定服务器
|
||||
* @param {string} id - 服务器 id
|
||||
* @returns {Promise<boolean>} 是否成功发起连接
|
||||
*/
|
||||
async function connectServer(id) {
|
||||
return await window.api.connect(id);
|
||||
}
|
||||
|
||||
/**
|
||||
* 断开指定服务器
|
||||
* @param {string} id - 服务器 id
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
async function disconnectServer(id) {
|
||||
await window.api.disconnect(id);
|
||||
}
|
||||
|
||||
// ==================== 主题管理 ====================
|
||||
|
||||
/**
|
||||
* 为指定服务器添加主题
|
||||
* @param {string} serverId - 服务器 id
|
||||
* @param {string} topic - 主题名称
|
||||
* @param {MqttQoS} qos - QoS 等级
|
||||
* @returns {Promise<MqttTopic | null>} 添加后的主题对象或 null
|
||||
*/
|
||||
async function addTopic(serverId, topic, qos) {
|
||||
|
||||
let result = await window.api.addTopic(serverId, topic, qos);
|
||||
@@ -199,6 +403,12 @@ export function useMqttStore() {
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* 移除指定服务器的主题
|
||||
* @param {string} serverId - 服务器 id
|
||||
* @param {string} topic - 主题名称
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
async function removeTopic(serverId, topic) {
|
||||
|
||||
await window.api.removeTopic(serverId, topic);
|
||||
@@ -211,23 +421,57 @@ export function useMqttStore() {
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* 订阅指定主题
|
||||
* @param {string} serverId - 服务器 id
|
||||
* @param {string} topic - 主题名称
|
||||
* @returns {Promise<boolean>} 是否发起订阅
|
||||
*/
|
||||
async function subscribeTopic(serverId, topic) {
|
||||
return await window.api.subscribe(serverId, topic);
|
||||
}
|
||||
|
||||
/**
|
||||
* 取消订阅指定主题
|
||||
* @param {string} serverId - 服务器 id
|
||||
* @param {string} topic - 主题名称
|
||||
* @returns {Promise<boolean>} 是否发起取消订阅
|
||||
*/
|
||||
async function unsubscribeTopic(serverId, topic) {
|
||||
return await window.api.unsubscribe(serverId, topic);
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量订阅指定服务器下所有未订阅主题
|
||||
* @param {string} serverId - 服务器 id
|
||||
* @returns {Promise<boolean>} 是否发起批量订阅
|
||||
*/
|
||||
async function subscribeAllTopics(serverId) {
|
||||
return await window.api.subscribeAll(serverId);
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新指定服务器的主题信息
|
||||
* @param {string} serverId - 服务器 id
|
||||
* @param {string} topicId - 主题 id
|
||||
* @param {string} topic - 新的主题名称
|
||||
* @param {MqttQoS} qos - 新的 QoS 等级
|
||||
* @returns {Promise<boolean>} 是否更新成功
|
||||
*/
|
||||
async function updateTopic(serverId, topicId, topic, qos) {
|
||||
return await window.api.updateTopic(serverId, topicId, topic, qos);
|
||||
}
|
||||
|
||||
// ==================== 消息发布 ====================
|
||||
|
||||
/**
|
||||
* 发布消息到指定主题
|
||||
* @param {string} serverId - 服务器 id
|
||||
* @param {string} topic - 目标主题
|
||||
* @param {string} payload - 消息内容
|
||||
* @param {PublishOptions} opts - 发布选项
|
||||
* @returns {Promise<boolean>} 是否发起发布
|
||||
*/
|
||||
async function publishMessage(serverId, topic, payload, opts) {
|
||||
|
||||
let result = await window.api.publish(serverId, topic, payload, opts);
|
||||
@@ -242,18 +486,30 @@ export function useMqttStore() {
|
||||
|
||||
// ==================== 消息管理 ====================
|
||||
|
||||
/**
|
||||
* 刷新当前活动服务器的消息列表
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
async function refreshMessages() {
|
||||
if (activeServerId.value) {
|
||||
messages.value = await window.api.getMessages(activeServerId.value);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 刷新当前活动服务器的发布历史
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
async function refreshPublishHistory() {
|
||||
if (activeServerId.value) {
|
||||
publishHistory.value = await window.api.getPublishHistory(activeServerId.value);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 清空当前活动服务器的消息列表
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
async function clearMessages() {
|
||||
if (activeServerId.value) {
|
||||
await window.api.clearMessages(activeServerId.value);
|
||||
@@ -261,6 +517,10 @@ export function useMqttStore() {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 清空当前活动服务器的发布历史
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
async function clearPublishHistory() {
|
||||
if (activeServerId.value) {
|
||||
await window.api.clearPublishHistory(activeServerId.value);
|
||||
|
||||
@@ -2,9 +2,18 @@
|
||||
import { useMqttStore } from '../stores/mqtt.js';
|
||||
import { NGrid, NGridItem, NCard, NStatistic, NTag, NDescriptions, NDescriptionsItem, NSpace, NButton, NList, NListItem, useMessage } from 'naive-ui';
|
||||
|
||||
// 全局状态与消息 API
|
||||
const store = useMqttStore();
|
||||
const msg = useMessage();
|
||||
|
||||
// 组件事件定义
|
||||
const emit = defineEmits(['switch-tab', 'edit-server']);
|
||||
|
||||
/**
|
||||
* 处理快捷操作:切换标签页并选中服务器
|
||||
* @param {import('@types/jsdoc').MqttServer} srv - 服务器对象
|
||||
* @param {string} action - 目标标签页名称
|
||||
*/
|
||||
function handleAction(srv, action) {
|
||||
if (srv.id === store.activeServerId.value) {
|
||||
emit('switch-tab', action);
|
||||
@@ -14,6 +23,10 @@ function handleAction(srv, action) {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 切换当前服务器连接状态
|
||||
* @param {import('@types/jsdoc').MqttServer} srv - 服务器对象
|
||||
*/
|
||||
async function handleToggleConnection(srv) {
|
||||
if (srv.status === 'connected') {
|
||||
await store.disconnectServer(srv.id);
|
||||
@@ -22,8 +35,6 @@ async function handleToggleConnection(srv) {
|
||||
await store.connectServer(srv.id);
|
||||
}
|
||||
}
|
||||
|
||||
const emit = defineEmits(['switch-tab', 'edit-server']);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
|
||||
@@ -3,11 +3,17 @@ import { ref, computed } from 'vue';
|
||||
import { useMqttStore } from '../stores/mqtt.js';
|
||||
import { NCard, NSpace, NInput, NButton, NEmpty, useMessage } from 'naive-ui';
|
||||
|
||||
// 全局状态与消息 API
|
||||
const store = useMqttStore();
|
||||
const message = useMessage();
|
||||
|
||||
// 主题过滤关键字
|
||||
const filter = ref('');
|
||||
|
||||
/**
|
||||
* 复制主题到剪贴板
|
||||
* @param {string} topic - 主题名称
|
||||
*/
|
||||
async function copyTopic(topic) {
|
||||
try {
|
||||
await navigator.clipboard.writeText(topic);
|
||||
@@ -17,6 +23,10 @@ async function copyTopic(topic) {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据过滤关键字筛选后的消息列表
|
||||
* @type {import('vue').ComputedRef<Array<Object>>}
|
||||
*/
|
||||
const filteredMessages = computed(() => {
|
||||
|
||||
if (!filter.value.trim()) {
|
||||
@@ -31,10 +41,18 @@ const filteredMessages = computed(() => {
|
||||
|
||||
})
|
||||
|
||||
/**
|
||||
* 清空当前活动服务器的消息列表
|
||||
*/
|
||||
async function handleClear() {
|
||||
await store.clearMessages();
|
||||
}
|
||||
|
||||
/**
|
||||
* 格式化消息 Payload,若可解析为 JSON 则格式化输出
|
||||
* @param {string} payload - 消息内容
|
||||
* @returns {string} 格式化后的内容
|
||||
*/
|
||||
function formatPayload(payload) {
|
||||
try {
|
||||
return JSON.stringify(JSON.parse(payload), null, 2);
|
||||
|
||||
@@ -3,19 +3,26 @@ import { ref, watch } from 'vue';
|
||||
import { useMqttStore } from '../stores/mqtt.js';
|
||||
import { NCard, NSpace, NFormItem, NInput, NSelect, NSwitch, NButton, NEmpty, NTabs, NTabPane, useMessage } from 'naive-ui';
|
||||
|
||||
// 全局状态与消息 API
|
||||
const store = useMqttStore();
|
||||
const msg = useMessage();
|
||||
|
||||
// 发布表单状态
|
||||
const topic = ref('');
|
||||
const payload = ref('');
|
||||
const qos = ref(1);
|
||||
const retain = ref(false);
|
||||
|
||||
// 当前激活的标签页
|
||||
const activePublishTab = ref('publish');
|
||||
|
||||
// localStorage 持久化 key
|
||||
const formStorageKey = 'mqtt-client.publish-form';
|
||||
const publishTabStorageKey = 'mqtt-client.publish-active-tab';
|
||||
|
||||
let formReady = false;
|
||||
|
||||
// 从 localStorage 恢复发布表单
|
||||
try {
|
||||
|
||||
let savedForm = JSON.parse(localStorage.getItem(formStorageKey) || '{}');
|
||||
@@ -40,6 +47,7 @@ try {
|
||||
localStorage.removeItem(formStorageKey);
|
||||
}
|
||||
|
||||
// 从 localStorage 恢复当前标签页
|
||||
try {
|
||||
|
||||
let savedTab = localStorage.getItem(publishTabStorageKey);
|
||||
@@ -52,6 +60,7 @@ try {
|
||||
localStorage.removeItem(publishTabStorageKey);
|
||||
}
|
||||
|
||||
// 表单变化时持久化
|
||||
watch([topic, payload, qos, retain], () => {
|
||||
|
||||
if (!formReady) {
|
||||
@@ -70,18 +79,23 @@ watch([topic, payload, qos, retain], () => {
|
||||
|
||||
});
|
||||
|
||||
// 标签页变化时持久化
|
||||
watch(activePublishTab, (tab) => {
|
||||
localStorage.setItem(publishTabStorageKey, tab);
|
||||
});
|
||||
|
||||
formReady = true;
|
||||
|
||||
// QoS 下拉选项
|
||||
const qosOptions = [
|
||||
{ label: 'QoS 0 - 最多一次', value: 0 },
|
||||
{ label: 'QoS 1 - 至少一次', value: 1 },
|
||||
{ label: 'QoS 2 - 恰好一次', value: 2 },
|
||||
];
|
||||
|
||||
/**
|
||||
* 发布消息到当前服务器
|
||||
*/
|
||||
async function handlePublish() {
|
||||
|
||||
let srv = store.activeServer.value;
|
||||
@@ -111,6 +125,9 @@ async function handlePublish() {
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* 格式化 Payload 为 JSON
|
||||
*/
|
||||
function handleFormatJson() {
|
||||
try {
|
||||
|
||||
@@ -124,10 +141,17 @@ function handleFormatJson() {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 清空发布历史
|
||||
*/
|
||||
async function handleClearHistory() {
|
||||
await store.clearPublishHistory();
|
||||
}
|
||||
|
||||
/**
|
||||
* 复制主题到剪贴板
|
||||
* @param {string} topic - 主题名称
|
||||
*/
|
||||
async function copyTopic(topic) {
|
||||
try {
|
||||
await navigator.clipboard.writeText(topic);
|
||||
|
||||
@@ -1,105 +1,139 @@
|
||||
<script setup>
|
||||
import { ref, watch } from 'vue';
|
||||
import { useMqttStore } from '../stores/mqtt.js';
|
||||
import {
|
||||
NCard,
|
||||
NSpace,
|
||||
NFormItem,
|
||||
NInput,
|
||||
NSelect,
|
||||
NButton,
|
||||
NEmpty,
|
||||
NList,
|
||||
NListItem,
|
||||
NTag,
|
||||
NEllipsis,
|
||||
useMessage,
|
||||
} from 'naive-ui';
|
||||
import { NCard, NSpace, NFormItem, NInput, NSelect, NButton, NEmpty, NList, NListItem, NTag, NEllipsis, useMessage } from 'naive-ui';
|
||||
|
||||
// 全局状态与消息 API
|
||||
const store = useMqttStore();
|
||||
const msg = useMessage();
|
||||
|
||||
// 主题输入与 QoS 选择
|
||||
const topicInput = ref('');
|
||||
const qos = ref(2);
|
||||
|
||||
// 编辑状态
|
||||
const editingTopicId = ref(null);
|
||||
const editingTopic = ref('');
|
||||
const editingQos = ref(2);
|
||||
|
||||
// QoS 下拉选项
|
||||
const qosOptions = [
|
||||
{ label: 'QoS 0 - 最多一次', value: 0 },
|
||||
{ label: 'QoS 1 - 至少一次', value: 1 },
|
||||
{ label: 'QoS 2 - 恰好一次', value: 2 },
|
||||
];
|
||||
|
||||
// 表单本地存储 key
|
||||
const formStorageKey = 'mqtt-client.subscribe-form';
|
||||
|
||||
let formReady = false;
|
||||
|
||||
// 从 localStorage 恢复表单输入
|
||||
try {
|
||||
const savedForm = JSON.parse(localStorage.getItem(formStorageKey) || '{}');
|
||||
|
||||
let savedForm = JSON.parse(localStorage.getItem(formStorageKey) || '{}');
|
||||
|
||||
if (typeof savedForm.topicInput === 'string') {
|
||||
topicInput.value = savedForm.topicInput;
|
||||
}
|
||||
|
||||
if ([0, 1, 2].includes(savedForm.qos)) {
|
||||
qos.value = savedForm.qos;
|
||||
}
|
||||
|
||||
} catch {
|
||||
localStorage.removeItem(formStorageKey);
|
||||
}
|
||||
|
||||
// 表单变化时持久化到 localStorage
|
||||
watch([topicInput, qos], () => {
|
||||
|
||||
if (!formReady) {
|
||||
return;
|
||||
}
|
||||
|
||||
localStorage.setItem(
|
||||
formStorageKey,
|
||||
JSON.stringify({ topicInput: topicInput.value, qos: qos.value })
|
||||
);
|
||||
|
||||
});
|
||||
|
||||
formReady = true;
|
||||
|
||||
/**
|
||||
* 添加新主题
|
||||
*/
|
||||
async function handleAddTopic() {
|
||||
const topic = topicInput.value.trim();
|
||||
|
||||
let topic = topicInput.value.trim();
|
||||
|
||||
if (!topic) {
|
||||
msg.warning('请输入主题名称');
|
||||
return;
|
||||
}
|
||||
const srv = store.activeServer.value;
|
||||
|
||||
let srv = store.activeServer.value;
|
||||
|
||||
if (!srv) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (srv.topics && srv.topics.find((t) => t.topic === topic)) {
|
||||
msg.warning('该主题已存在');
|
||||
return;
|
||||
}
|
||||
const result = await store.addTopic(srv.id, topic, qos.value);
|
||||
|
||||
let result = await store.addTopic(srv.id, topic, qos.value);
|
||||
|
||||
if (result) {
|
||||
msg.success('主题已添加' + (srv.status === 'connected' ? '并已订阅' : ''));
|
||||
topicInput.value = '';
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* 移除主题
|
||||
* @param {string} topic - 主题名称
|
||||
*/
|
||||
async function handleRemoveTopic(topic) {
|
||||
const srv = store.activeServer.value;
|
||||
|
||||
let srv = store.activeServer.value;
|
||||
|
||||
if (!srv) {
|
||||
return;
|
||||
}
|
||||
|
||||
await store.removeTopic(srv.id, topic);
|
||||
|
||||
msg.info('主题已移除');
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* 切换单个主题订阅状态
|
||||
* @param {string} topic - 主题名称
|
||||
*/
|
||||
async function handleToggleSub(topic) {
|
||||
const srv = store.activeServer.value;
|
||||
|
||||
let srv = store.activeServer.value;
|
||||
|
||||
if (!srv) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (srv.status !== 'connected') {
|
||||
msg.warning('请先连接服务器');
|
||||
return;
|
||||
}
|
||||
const t = srv.topics.find((t) => t.topic === topic);
|
||||
|
||||
let t = srv.topics.find((t) => t.topic === topic);
|
||||
|
||||
if (!t) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (t.subscribed) {
|
||||
await store.unsubscribeTopic(srv.id, topic);
|
||||
msg.info('已取消订阅: ' + topic);
|
||||
@@ -107,43 +141,68 @@ async function handleToggleSub(topic) {
|
||||
await store.subscribeTopic(srv.id, topic);
|
||||
msg.info('已订阅: ' + topic);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量订阅当前服务器下所有未订阅主题
|
||||
*/
|
||||
async function handleSubscribeAll() {
|
||||
const srv = store.activeServer.value;
|
||||
|
||||
let srv = store.activeServer.value;
|
||||
|
||||
if (!srv) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (srv.status !== 'connected') {
|
||||
msg.warning('请先连接服务器');
|
||||
return;
|
||||
}
|
||||
|
||||
await store.subscribeAllTopics(srv.id);
|
||||
msg.success('已订阅全部主题');
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* 进入主题编辑状态
|
||||
* @param {import('@types/jsdoc').MqttTopic} t - 主题对象
|
||||
*/
|
||||
function startEdit(t) {
|
||||
editingTopicId.value = t.id || t.topic;
|
||||
editingTopic.value = t.topic;
|
||||
editingQos.value = t.qos ?? 2;
|
||||
}
|
||||
|
||||
/**
|
||||
* 取消主题编辑
|
||||
*/
|
||||
function cancelEdit() {
|
||||
editingTopicId.value = null;
|
||||
editingTopic.value = '';
|
||||
editingQos.value = 2;
|
||||
}
|
||||
|
||||
/**
|
||||
* 保存主题编辑
|
||||
* @param {import('@types/jsdoc').MqttTopic} t - 主题对象
|
||||
*/
|
||||
async function handleEditSave(t) {
|
||||
const srv = store.activeServer.value;
|
||||
|
||||
let srv = store.activeServer.value;
|
||||
|
||||
if (!srv) {
|
||||
return;
|
||||
}
|
||||
const topicName = editingTopic.value.trim();
|
||||
|
||||
let topicName = editingTopic.value.trim();
|
||||
|
||||
if (!topicName) {
|
||||
msg.warning('请输入主题名称');
|
||||
return;
|
||||
}
|
||||
|
||||
if (
|
||||
srv.topics.find(
|
||||
(item) => item.topic === topicName && (item.id || item.topic) !== editingTopicId.value
|
||||
@@ -152,22 +211,28 @@ async function handleEditSave(t) {
|
||||
msg.warning('该主题已存在');
|
||||
return;
|
||||
}
|
||||
const topicId = t.id || t.topic;
|
||||
const result = await store.updateTopic(srv.id, topicId, topicName, editingQos.value);
|
||||
|
||||
let topicId = t.id || t.topic;
|
||||
let result = await store.updateTopic(srv.id, topicId, topicName, editingQos.value);
|
||||
|
||||
if (result) {
|
||||
msg.success('主题已更新');
|
||||
cancelEdit();
|
||||
} else {
|
||||
msg.error('主题更新失败');
|
||||
}
|
||||
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
|
||||
<!-- 内容区域 -->
|
||||
<div
|
||||
v-if="store.activeServer.value"
|
||||
class="subscribe-view"
|
||||
>
|
||||
|
||||
<!-- 添加主题 -->
|
||||
<n-card
|
||||
title="添加订阅主题"
|
||||
@@ -323,11 +388,15 @@ async function handleEditSave(t) {
|
||||
</n-list>
|
||||
</div>
|
||||
</n-card>
|
||||
|
||||
</div>
|
||||
|
||||
<!-- 空状态 -->
|
||||
<n-empty
|
||||
v-else
|
||||
description="请先在左侧选择一个 MQTT 服务器"
|
||||
/>
|
||||
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
|
||||
114
app/types/jsdoc.d.ts
vendored
Normal file
114
app/types/jsdoc.d.ts
vendored
Normal file
@@ -0,0 +1,114 @@
|
||||
/**
|
||||
* 公共 MQTT 类型定义
|
||||
* 供 renderer 与 main 进程共享,避免在多个模块中重复定义 JSDoc @typedef
|
||||
*/
|
||||
|
||||
/** MQTT QoS 等级 */
|
||||
export type MqttQoS = 0 | 1 | 2;
|
||||
|
||||
/** MQTT 服务器连接状态 */
|
||||
export type MqttServerStatus = 'disconnected' | 'connecting' | 'connected' | 'error';
|
||||
|
||||
/** MQTT 主题 */
|
||||
export interface MqttTopic {
|
||||
/** 主题唯一标识 */
|
||||
id: string;
|
||||
/** 主题名称(支持通配符) */
|
||||
topic: string;
|
||||
/** 订阅 QoS 等级 */
|
||||
qos: MqttQoS;
|
||||
/** 当前是否已订阅 */
|
||||
subscribed: boolean;
|
||||
}
|
||||
|
||||
/** 主题配置(用于导入/导出或表单) */
|
||||
export interface TopicConfig {
|
||||
/** 主题唯一标识,未提供时由后端生成 */
|
||||
id?: string;
|
||||
/** 主题名称(支持通配符) */
|
||||
topic: string;
|
||||
/** 订阅 QoS 等级,默认 1 */
|
||||
qos?: MqttQoS;
|
||||
}
|
||||
|
||||
/** MQTT 服务器配置(不含运行时状态) */
|
||||
export interface ServerConfig {
|
||||
/** 显示名称 */
|
||||
name: string;
|
||||
/** 主机地址 */
|
||||
host: string;
|
||||
/** 端口号,默认 1883 */
|
||||
port?: number;
|
||||
/** 连接协议,默认 'mqtt' */
|
||||
protocol?: 'mqtt' | 'mqtts' | 'ws' | 'wss';
|
||||
/** 客户端 ID,默认随机生成 */
|
||||
clientId?: string;
|
||||
/** 用户名,默认空字符串 */
|
||||
username?: string;
|
||||
/** 密码,默认空字符串 */
|
||||
password?: string;
|
||||
/** 心跳间隔(秒),默认 60 */
|
||||
keepAlive?: number;
|
||||
/** 是否清理会话,默认 true */
|
||||
cleanSession?: boolean;
|
||||
/** 连接超时(毫秒),默认 10000 */
|
||||
connectTimeout?: number;
|
||||
/** 是否自动重连,默认 true */
|
||||
reconnect?: boolean;
|
||||
/** 重连间隔(毫秒),默认 5000 */
|
||||
reconnectInterval?: number;
|
||||
/** 初始主题列表 */
|
||||
topics?: TopicConfig[];
|
||||
}
|
||||
|
||||
/** MQTT 服务器运行时对象 */
|
||||
export interface MqttServer extends ServerConfig {
|
||||
/** 服务器唯一标识 */
|
||||
id: string;
|
||||
/** 当前连接状态 */
|
||||
status: MqttServerStatus;
|
||||
/** 该服务器下的主题列表 */
|
||||
topics: MqttTopic[];
|
||||
}
|
||||
|
||||
/** 发布/订阅选项 */
|
||||
export interface PublishOptions {
|
||||
/** QoS 等级,默认 1 */
|
||||
qos?: MqttQoS;
|
||||
/** 是否保留消息,默认 false */
|
||||
retain?: boolean;
|
||||
}
|
||||
|
||||
/** MQTT 消息(订阅接收或本地发布) */
|
||||
export interface MqttMessage {
|
||||
/** 消息唯一标识 */
|
||||
id: string;
|
||||
/** 接收/发送时间(格式为 HH:mm:ss.SSS) */
|
||||
time: string;
|
||||
/** 消息主题 */
|
||||
topic: string;
|
||||
/** 消息负载内容 */
|
||||
payload: string;
|
||||
/** 消息 QoS 等级 */
|
||||
qos: MqttQoS;
|
||||
/** 是否为保留消息 */
|
||||
retain: boolean;
|
||||
/** 消息方向:pub 表示发布,sub 表示订阅 */
|
||||
direction: 'pub' | 'sub';
|
||||
}
|
||||
|
||||
/** 发布历史记录 */
|
||||
export interface PublishRecord {
|
||||
/** 发布时间(格式为 HH:mm:ss.SSS) */
|
||||
time: string;
|
||||
/** 目标主题 */
|
||||
topic: string;
|
||||
/** 发布内容 */
|
||||
payload: string;
|
||||
/** 发布 QoS 等级 */
|
||||
qos: MqttQoS;
|
||||
/** 是否保留消息 */
|
||||
retain: boolean;
|
||||
/** 固定为 pub */
|
||||
direction: 'pub';
|
||||
}
|
||||
Reference in New Issue
Block a user