style: 整理代码格式
This commit is contained in:
@@ -6,4 +6,4 @@ indent_style = space
|
|||||||
indent_size = 2
|
indent_size = 2
|
||||||
end_of_line = lf
|
end_of_line = lf
|
||||||
insert_final_newline = true
|
insert_final_newline = true
|
||||||
trim_trailing_whitespace = true
|
trim_trailing_whitespace = true
|
||||||
|
|||||||
@@ -1,10 +1,12 @@
|
|||||||
import { app, shell, BrowserWindow } from 'electron'
|
import { app, shell, BrowserWindow } from 'electron';
|
||||||
import { join } from 'path'
|
import { join } from 'path';
|
||||||
import { electronApp, optimizer, is } from '@electron-toolkit/utils'
|
import { electronApp, optimizer, is } from '@electron-toolkit/utils';
|
||||||
import icon from '../../resources/icon.png?asset'
|
import { registerIpcHandlers } from './ipc-handlers.js';
|
||||||
import { registerIpcHandlers } from './ipc-handlers.js'
|
|
||||||
|
import icon from '../../resources/icon.png?asset';
|
||||||
|
|
||||||
function createWindow() {
|
function createWindow() {
|
||||||
|
|
||||||
// Create the browser window.
|
// Create the browser window.
|
||||||
const mainWindow = new BrowserWindow({
|
const mainWindow = new BrowserWindow({
|
||||||
width: 1200,
|
width: 1200,
|
||||||
@@ -16,62 +18,67 @@ function createWindow() {
|
|||||||
...(process.platform === 'linux' ? { icon } : {}),
|
...(process.platform === 'linux' ? { icon } : {}),
|
||||||
webPreferences: {
|
webPreferences: {
|
||||||
preload: join(__dirname, '../preload/index.js'),
|
preload: join(__dirname, '../preload/index.js'),
|
||||||
sandbox: false
|
sandbox: false,
|
||||||
}
|
},
|
||||||
})
|
});
|
||||||
|
|
||||||
mainWindow.on('ready-to-show', () => {
|
mainWindow.on('ready-to-show', () => {
|
||||||
mainWindow.show()
|
mainWindow.show();
|
||||||
})
|
});
|
||||||
|
|
||||||
mainWindow.webContents.setWindowOpenHandler((details) => {
|
mainWindow.webContents.setWindowOpenHandler((details) => {
|
||||||
shell.openExternal(details.url)
|
shell.openExternal(details.url);
|
||||||
return { action: 'deny' }
|
return { action: 'deny' };
|
||||||
})
|
});
|
||||||
|
|
||||||
// HMR for renderer base on electron-vite cli.
|
// HMR for renderer base on electron-vite cli.
|
||||||
// Load the remote URL for development or the local html file for production.
|
// Load the remote URL for development or the local html file for production.
|
||||||
if (is.dev && process.env['ELECTRON_RENDERER_URL']) {
|
if (is.dev && process.env['ELECTRON_RENDERER_URL']) {
|
||||||
mainWindow.loadURL(process.env['ELECTRON_RENDERER_URL'])
|
mainWindow.loadURL(process.env['ELECTRON_RENDERER_URL']);
|
||||||
} else {
|
} else {
|
||||||
mainWindow.loadFile(join(__dirname, '../renderer/index.html'))
|
mainWindow.loadFile(join(__dirname, '../renderer/index.html'));
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// This method will be called when Electron has finished
|
// This method will be called when Electron has finished
|
||||||
// initialization and is ready to create browser windows.
|
// initialization and is ready to create browser windows.
|
||||||
// Some APIs can only be used after this event occurs.
|
// Some APIs can only be used after this event occurs.
|
||||||
app.whenReady().then(() => {
|
app.whenReady().then(() => {
|
||||||
|
|
||||||
// Set app user model id for windows
|
// Set app user model id for windows
|
||||||
electronApp.setAppUserModelId('com.electron')
|
electronApp.setAppUserModelId('com.electron');
|
||||||
|
|
||||||
// Default open or close DevTools by F12 in development
|
// Default open or close DevTools by F12 in development
|
||||||
// and ignore CommandOrControl + R in production.
|
// and ignore CommandOrControl + R in production.
|
||||||
// see https://github.com/alex8088/electron-toolkit/tree/master/packages/utils
|
// see https://github.com/alex8088/electron-toolkit/tree/master/packages/utils
|
||||||
app.on('browser-window-created', (_, window) => {
|
app.on('browser-window-created', (_, window) => {
|
||||||
optimizer.watchWindowShortcuts(window)
|
optimizer.watchWindowShortcuts(window);
|
||||||
})
|
});
|
||||||
|
|
||||||
// 注册 IPC 处理器
|
// 注册 IPC 处理器
|
||||||
registerIpcHandlers()
|
registerIpcHandlers();
|
||||||
|
|
||||||
createWindow()
|
createWindow();
|
||||||
|
|
||||||
app.on('activate', function () {
|
app.on('activate', function () {
|
||||||
// On macOS it's common to re-create a window in the app when the
|
// 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.
|
// 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
|
// 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
|
// for applications and their menu bar to stay active until the user quits
|
||||||
// explicitly with Cmd + Q.
|
// explicitly with Cmd + Q.
|
||||||
app.on('window-all-closed', () => {
|
app.on('window-all-closed', () => {
|
||||||
if (process.platform !== 'darwin') {
|
if (process.platform !== 'darwin') {
|
||||||
app.quit()
|
app.quit();
|
||||||
}
|
}
|
||||||
})
|
});
|
||||||
|
|
||||||
// In this file you can include the rest of your app's specific main process
|
// 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.
|
// code. You can also put them in separate files and require them here.
|
||||||
|
|||||||
@@ -1,18 +1,20 @@
|
|||||||
import { ipcMain, BrowserWindow } from 'electron'
|
import { ipcMain, BrowserWindow } from 'electron';
|
||||||
import mqttManager from './mqtt-manager.js'
|
|
||||||
|
import mqttManager from './mqtt-manager.js';
|
||||||
|
|
||||||
export function registerIpcHandlers() {
|
export function registerIpcHandlers() {
|
||||||
|
|
||||||
// 获取主窗口(用于推送事件)
|
// 获取主窗口(用于推送事件)
|
||||||
function getMainWindow() {
|
function getMainWindow() {
|
||||||
const windows = BrowserWindow.getAllWindows()
|
const windows = BrowserWindow.getAllWindows();
|
||||||
return windows.length > 0 ? windows[0] : null
|
return windows.length > 0 ? windows[0] : null;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 向渲染进程发送事件
|
// 向渲染进程发送事件
|
||||||
function sendToRenderer(channel, data) {
|
function sendToRenderer(channel, data) {
|
||||||
const win = getMainWindow()
|
const win = getMainWindow();
|
||||||
if (win && !win.isDestroyed()) {
|
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) => {
|
mqttManager.addListener((event, data) => {
|
||||||
switch (event) {
|
switch (event) {
|
||||||
case 'status-change':
|
case 'status-change':
|
||||||
sendToRenderer('mqtt:status-change', data)
|
sendToRenderer('mqtt:status-change', data);
|
||||||
break
|
break;
|
||||||
case 'message':
|
case 'message':
|
||||||
sendToRenderer('mqtt:message', data)
|
sendToRenderer('mqtt:message', data);
|
||||||
break
|
break;
|
||||||
case 'subscription-change':
|
case 'subscription-change':
|
||||||
sendToRenderer('mqtt:subscription-change', data)
|
sendToRenderer('mqtt:subscription-change', data);
|
||||||
break
|
break;
|
||||||
case 'subscription-error':
|
case 'subscription-error':
|
||||||
sendToRenderer('mqtt:subscription-error', data)
|
sendToRenderer('mqtt:subscription-error', data);
|
||||||
break
|
break;
|
||||||
case 'publish-error':
|
case 'publish-error':
|
||||||
sendToRenderer('mqtt:publish-error', data)
|
sendToRenderer('mqtt:publish-error', data);
|
||||||
break
|
break;
|
||||||
}
|
}
|
||||||
})
|
});
|
||||||
|
|
||||||
// ==================== 服务器管理 ====================
|
// ==================== 服务器管理 ====================
|
||||||
|
|
||||||
ipcMain.handle('mqtt:get-servers', () => {
|
ipcMain.handle('mqtt:get-servers', () => {
|
||||||
return mqttManager.getServers()
|
return mqttManager.getServers();
|
||||||
})
|
});
|
||||||
|
|
||||||
ipcMain.handle('mqtt:add-server', (_, config) => {
|
ipcMain.handle('mqtt:add-server', (_, config) => {
|
||||||
return mqttManager.addServer(config)
|
return mqttManager.addServer(config);
|
||||||
})
|
});
|
||||||
|
|
||||||
ipcMain.handle('mqtt:update-server', (_, id, config) => {
|
ipcMain.handle('mqtt:update-server', (_, id, config) => {
|
||||||
return mqttManager.updateServer(id, config)
|
return mqttManager.updateServer(id, config);
|
||||||
})
|
});
|
||||||
|
|
||||||
ipcMain.handle('mqtt:delete-server', (_, id) => {
|
ipcMain.handle('mqtt:delete-server', (_, id) => {
|
||||||
mqttManager.deleteServer(id)
|
mqttManager.deleteServer(id);
|
||||||
return true
|
return true;
|
||||||
})
|
});
|
||||||
|
|
||||||
// ==================== 连接管理 ====================
|
// ==================== 连接管理 ====================
|
||||||
|
|
||||||
ipcMain.handle('mqtt:connect', (_, id) => {
|
ipcMain.handle('mqtt:connect', (_, id) => {
|
||||||
return mqttManager.connect(id)
|
return mqttManager.connect(id);
|
||||||
})
|
});
|
||||||
|
|
||||||
ipcMain.handle('mqtt:disconnect', (_, id) => {
|
ipcMain.handle('mqtt:disconnect', (_, id) => {
|
||||||
mqttManager.disconnect(id)
|
mqttManager.disconnect(id);
|
||||||
return true
|
return true;
|
||||||
})
|
});
|
||||||
|
|
||||||
// ==================== 主题管理 ====================
|
// ==================== 主题管理 ====================
|
||||||
|
|
||||||
ipcMain.handle('mqtt:add-topic', (_, serverId, topic, qos) => {
|
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) => {
|
ipcMain.handle('mqtt:remove-topic', (_, serverId, topic) => {
|
||||||
return mqttManager.removeTopic(serverId, topic)
|
return mqttManager.removeTopic(serverId, topic);
|
||||||
})
|
});
|
||||||
|
|
||||||
ipcMain.handle('mqtt:subscribe', (_, serverId, topic) => {
|
ipcMain.handle('mqtt:subscribe', (_, serverId, topic) => {
|
||||||
return mqttManager.subscribe(serverId, topic)
|
return mqttManager.subscribe(serverId, topic);
|
||||||
})
|
});
|
||||||
|
|
||||||
ipcMain.handle('mqtt:unsubscribe', (_, serverId, topic) => {
|
ipcMain.handle('mqtt:unsubscribe', (_, serverId, topic) => {
|
||||||
return mqttManager.unsubscribe(serverId, topic)
|
return mqttManager.unsubscribe(serverId, topic);
|
||||||
})
|
});
|
||||||
|
|
||||||
ipcMain.handle('mqtt:subscribe-all', (_, serverId) => {
|
ipcMain.handle('mqtt:subscribe-all', (_, serverId) => {
|
||||||
return mqttManager.subscribeAll(serverId)
|
return mqttManager.subscribeAll(serverId);
|
||||||
})
|
});
|
||||||
|
|
||||||
ipcMain.handle('mqtt:update-topic', (_, serverId, topicId, topic, qos) => {
|
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) => {
|
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) => {
|
ipcMain.handle('mqtt:get-messages', (_, serverId) => {
|
||||||
return mqttManager.getMessages(serverId)
|
return mqttManager.getMessages(serverId);
|
||||||
})
|
});
|
||||||
|
|
||||||
ipcMain.handle('mqtt:get-publish-history', (_, serverId) => {
|
ipcMain.handle('mqtt:get-publish-history', (_, serverId) => {
|
||||||
return mqttManager.getPublishHistory(serverId)
|
return mqttManager.getPublishHistory(serverId);
|
||||||
})
|
});
|
||||||
|
|
||||||
ipcMain.handle('mqtt:clear-messages', (_, serverId) => {
|
ipcMain.handle('mqtt:clear-messages', (_, serverId) => {
|
||||||
mqttManager.clearMessages(serverId)
|
mqttManager.clearMessages(serverId);
|
||||||
return true
|
return true;
|
||||||
})
|
});
|
||||||
|
|
||||||
ipcMain.handle('mqtt:clear-publish-history', (_, serverId) => {
|
ipcMain.handle('mqtt:clear-publish-history', (_, serverId) => {
|
||||||
mqttManager.clearPublishHistory(serverId)
|
mqttManager.clearPublishHistory(serverId);
|
||||||
return true
|
return true;
|
||||||
})
|
});
|
||||||
|
|
||||||
// 兼容旧的 ping 测试
|
|
||||||
ipcMain.on('ping', () => console.log('pong'))
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,89 +1,104 @@
|
|||||||
import mqtt from 'mqtt'
|
import { app } from 'electron';
|
||||||
import { app } from 'electron'
|
import { readFileSync, writeFileSync, existsSync, mkdirSync } from 'fs';
|
||||||
import { join } from 'path'
|
import { join } from 'path';
|
||||||
import { readFileSync, writeFileSync, existsSync, mkdirSync } from 'fs'
|
|
||||||
|
import mqtt from 'mqtt';
|
||||||
|
|
||||||
class MqttManager {
|
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')
|
constructor() {
|
||||||
this.loadData()
|
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() {
|
nextMsgId() {
|
||||||
this.msgIdCounter = (this.msgIdCounter + 1) % Number.MAX_SAFE_INTEGER
|
this.msgIdCounter = (this.msgIdCounter + 1) % Number.MAX_SAFE_INTEGER;
|
||||||
return 'msg_' + Date.now() + '_' + this.msgIdCounter
|
return 'msg_' + Date.now() + '_' + this.msgIdCounter;
|
||||||
}
|
}
|
||||||
|
|
||||||
// ==================== 数据持久化 ====================
|
// ==================== 数据持久化 ====================
|
||||||
|
|
||||||
getDataDir() {
|
getDataDir() {
|
||||||
const dir = app.getPath('userData')
|
|
||||||
|
let dir = app.getPath('userData');
|
||||||
|
|
||||||
if (!existsSync(dir)) {
|
if (!existsSync(dir)) {
|
||||||
mkdirSync(dir, { recursive: true })
|
mkdirSync(dir, { recursive: true });
|
||||||
}
|
}
|
||||||
return dir
|
|
||||||
|
return dir;
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
loadData() {
|
loadData() {
|
||||||
try {
|
try {
|
||||||
if (existsSync(this.dataPath)) {
|
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) {
|
if (data.servers) {
|
||||||
data.servers.forEach((s) => {
|
data.servers.forEach((s) => {
|
||||||
s.status = 'disconnected'
|
s.status = 'disconnected';
|
||||||
s.topics = (s.topics || []).map((t) => ({
|
s.topics = (s.topics || []).map((t) => ({
|
||||||
...t,
|
...t,
|
||||||
id: t.id || 'topic_' + Date.now() + '_' + Math.random().toString(36).slice(2, 6),
|
id: t.id || 'topic_' + Date.now() + '_' + Math.random().toString(36).slice(2, 6),
|
||||||
subscribed: false
|
subscribed: false,
|
||||||
}))
|
}));
|
||||||
this.servers.set(s.id, s)
|
this.servers.set(s.id, s);
|
||||||
})
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
if (data.messages) {
|
if (data.messages) {
|
||||||
Object.entries(data.messages).forEach(([id, msgs]) => {
|
Object.entries(data.messages).forEach(([id, msgs]) => {
|
||||||
this.messages.set(id, msgs)
|
this.messages.set(id, msgs);
|
||||||
})
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
if (data.publishHistory) {
|
if (data.publishHistory) {
|
||||||
Object.entries(data.publishHistory).forEach(([id, history]) => {
|
Object.entries(data.publishHistory).forEach(([id, history]) => {
|
||||||
this.publishHistory.set(id, history)
|
this.publishHistory.set(id, history);
|
||||||
})
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
} else {
|
} else {
|
||||||
this.initDefaultServers()
|
this.initDefaultServers();
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error('加载数据失败:', e.message)
|
console.error('加载数据失败:', e.message);
|
||||||
this.initDefaultServers()
|
this.initDefaultServers();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
saveData() {
|
saveData() {
|
||||||
try {
|
try {
|
||||||
const data = {
|
|
||||||
|
let data = {
|
||||||
servers: Array.from(this.servers.values()).map((s) => ({
|
servers: Array.from(this.servers.values()).map((s) => ({
|
||||||
...s,
|
...s,
|
||||||
status: 'disconnected',
|
status: 'disconnected',
|
||||||
topics: s.topics.map((t) => ({ ...t, subscribed: false }))
|
topics: s.topics.map((t) => ({ ...t, subscribed: false })),
|
||||||
})),
|
})),
|
||||||
messages: Object.fromEntries(this.messages),
|
messages: Object.fromEntries(this.messages),
|
||||||
publishHistory: Object.fromEntries(this.publishHistory)
|
publishHistory: Object.fromEntries(this.publishHistory),
|
||||||
}
|
};
|
||||||
writeFileSync(this.dataPath, JSON.stringify(data, null, 2), 'utf-8')
|
|
||||||
|
writeFileSync(this.dataPath, JSON.stringify(data, null, 2), 'utf-8');
|
||||||
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error('保存数据失败:', e.message)
|
console.error('保存数据失败:', e.message);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
initDefaultServers() {
|
initDefaultServers() {
|
||||||
const defaults = [
|
|
||||||
|
let defaults = [
|
||||||
{
|
{
|
||||||
id: 'example_server',
|
id: 'example_server',
|
||||||
name: 'EMQX 公共测试',
|
name: 'EMQX 公共测试',
|
||||||
@@ -98,52 +113,58 @@ class MqttManager {
|
|||||||
status: 'disconnected',
|
status: 'disconnected',
|
||||||
topics: [
|
topics: [
|
||||||
{ id: 'topic_default_001', topic: 'sensor/+/temperature', qos: 1, subscribed: false },
|
{ id: 'topic_default_001', topic: 'sensor/+/temperature', qos: 1, subscribed: false },
|
||||||
{ id: 'topic_default_002', topic: 'device/status', qos: 2, subscribed: false }
|
{ id: 'topic_default_002', topic: 'device/status', qos: 2, subscribed: false },
|
||||||
]
|
],
|
||||||
}
|
},
|
||||||
]
|
];
|
||||||
|
|
||||||
defaults.forEach((s) => {
|
defaults.forEach((s) => {
|
||||||
this.servers.set(s.id, s)
|
this.servers.set(s.id, s);
|
||||||
this.messages.set(s.id, [])
|
this.messages.set(s.id, []);
|
||||||
this.publishHistory.set(s.id, [])
|
this.publishHistory.set(s.id, []);
|
||||||
})
|
});
|
||||||
this.saveData()
|
|
||||||
|
this.saveData();
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ==================== 监听器 ====================
|
// ==================== 监听器 ====================
|
||||||
|
|
||||||
addListener(callback) {
|
addListener(callback) {
|
||||||
this.listeners.add(callback)
|
this.listeners.add(callback);
|
||||||
}
|
}
|
||||||
|
|
||||||
removeListener(callback) {
|
removeListener(callback) {
|
||||||
this.listeners.delete(callback)
|
this.listeners.delete(callback);
|
||||||
}
|
}
|
||||||
|
|
||||||
notify(event, data) {
|
notify(event, data) {
|
||||||
this.listeners.forEach((cb) => {
|
this.listeners.forEach((cb) => {
|
||||||
try {
|
try {
|
||||||
cb(event, data)
|
cb(event, data);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error('通知失败:', e)
|
console.error('通知失败:', e);
|
||||||
}
|
}
|
||||||
})
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// ==================== 服务器管理 ====================
|
// ==================== 服务器管理 ====================
|
||||||
|
|
||||||
getServers() {
|
getServers() {
|
||||||
return Array.from(this.servers.values()).map((s) => ({
|
return Array.from(this.servers.values()).map((s) => ({
|
||||||
...s,
|
...s,
|
||||||
password: s.password ? '******' : ''
|
password: s.password ? '******' : '',
|
||||||
}))
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
getServer(id) {
|
getServer(id) {
|
||||||
return this.servers.get(id) || null
|
return this.servers.get(id) || null;
|
||||||
}
|
}
|
||||||
|
|
||||||
addServer(config) {
|
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,
|
id,
|
||||||
name: config.name,
|
name: config.name,
|
||||||
host: config.host,
|
host: config.host,
|
||||||
@@ -155,18 +176,26 @@ class MqttManager {
|
|||||||
keepAlive: config.keepAlive || 60,
|
keepAlive: config.keepAlive || 60,
|
||||||
cleanSession: config.cleanSession !== false,
|
cleanSession: config.cleanSession !== false,
|
||||||
status: 'disconnected',
|
status: 'disconnected',
|
||||||
topics: []
|
topics: [],
|
||||||
}
|
};
|
||||||
this.servers.set(id, server)
|
|
||||||
this.messages.set(id, [])
|
this.servers.set(id, server);
|
||||||
this.publishHistory.set(id, [])
|
this.messages.set(id, []);
|
||||||
this.saveData()
|
this.publishHistory.set(id, []);
|
||||||
return server
|
this.saveData();
|
||||||
|
|
||||||
|
return server;
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
updateServer(id, config) {
|
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, {
|
Object.assign(server, {
|
||||||
name: config.name,
|
name: config.name,
|
||||||
host: config.host,
|
host: config.host,
|
||||||
@@ -175,283 +204,373 @@ class MqttManager {
|
|||||||
clientId: config.clientId || server.clientId,
|
clientId: config.clientId || server.clientId,
|
||||||
username: config.username || '',
|
username: config.username || '',
|
||||||
keepAlive: config.keepAlive || 60,
|
keepAlive: config.keepAlive || 60,
|
||||||
cleanSession: config.cleanSession !== false
|
cleanSession: config.cleanSession !== false,
|
||||||
})
|
});
|
||||||
|
|
||||||
if (config.password) {
|
if (config.password) {
|
||||||
server.password = config.password
|
server.password = config.password;
|
||||||
}
|
}
|
||||||
this.saveData()
|
|
||||||
return server
|
this.saveData();
|
||||||
|
|
||||||
|
return server;
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
deleteServer(id) {
|
deleteServer(id) {
|
||||||
// 先断开连接
|
// 先断开连接
|
||||||
this.disconnect(id)
|
this.disconnect(id);
|
||||||
this.servers.delete(id)
|
this.servers.delete(id);
|
||||||
this.messages.delete(id)
|
this.messages.delete(id);
|
||||||
this.publishHistory.delete(id)
|
this.publishHistory.delete(id);
|
||||||
this.saveData()
|
this.saveData();
|
||||||
}
|
}
|
||||||
|
|
||||||
// ==================== 连接管理 ====================
|
// ==================== 连接管理 ====================
|
||||||
|
|
||||||
connect(id) {
|
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)) {
|
if (this.clients.has(id)) {
|
||||||
this.disconnect(id)
|
this.disconnect(id);
|
||||||
}
|
}
|
||||||
|
|
||||||
server.status = 'connecting'
|
server.status = 'connecting';
|
||||||
this.notify('status-change', { id, status: 'connecting' })
|
|
||||||
|
|
||||||
const protocol = server.protocol || 'mqtt'
|
this.notify('status-change', { id, status: 'connecting' });
|
||||||
const url = `${protocol}://${server.host}:${server.port}`
|
|
||||||
|
|
||||||
const options = {
|
let protocol = server.protocol || 'mqtt';
|
||||||
|
let url = `${protocol}://${server.host}:${server.port}`;
|
||||||
|
|
||||||
|
let options = {
|
||||||
clientId: server.clientId,
|
clientId: server.clientId,
|
||||||
username: server.username || undefined,
|
username: server.username || undefined,
|
||||||
password: server.password || undefined,
|
password: server.password || undefined,
|
||||||
keepalive: server.keepAlive,
|
keepalive: server.keepAlive,
|
||||||
clean: server.cleanSession,
|
clean: server.cleanSession,
|
||||||
reconnectPeriod: 5000,
|
reconnectPeriod: 5000,
|
||||||
connectTimeout: 10000
|
connectTimeout: 10000,
|
||||||
}
|
};
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const client = mqtt.connect(url, options)
|
|
||||||
|
let client = mqtt.connect(url, options);
|
||||||
|
|
||||||
client.on('connect', () => {
|
client.on('connect', () => {
|
||||||
server.status = 'connected'
|
server.status = 'connected';
|
||||||
this.notify('status-change', { id, status: 'connected' })
|
this.notify('status-change', { id, status: 'connected' });
|
||||||
// 自动订阅已有主题
|
// 自动订阅已有主题
|
||||||
server.topics.forEach((t) => {
|
server.topics.forEach((t) => {
|
||||||
this._doSubscribe(id, t.topic, { qos: t.qos })
|
this._doSubscribe(id, t.topic, { qos: t.qos });
|
||||||
})
|
});
|
||||||
})
|
});
|
||||||
|
|
||||||
client.on('error', (err) => {
|
client.on('error', (err) => {
|
||||||
console.error(`[${server.name}] 连接错误:`, err.message)
|
console.error(`[${server.name}] 连接错误:`, err.message);
|
||||||
server.status = 'error'
|
server.status = 'error';
|
||||||
this.notify('status-change', { id, status: 'error', error: err.message })
|
this.notify('status-change', { id, status: 'error', error: err.message });
|
||||||
})
|
});
|
||||||
|
|
||||||
client.on('close', () => {
|
client.on('close', () => {
|
||||||
if (server.status === 'connected') {
|
if (server.status === 'connected') {
|
||||||
server.status = 'disconnected'
|
server.status = 'disconnected';
|
||||||
server.topics.forEach((t) => {
|
server.topics.forEach((t) => {
|
||||||
t.subscribed = false
|
t.subscribed = false;
|
||||||
})
|
});
|
||||||
this.notify('status-change', { id, status: 'disconnected' })
|
this.notify('status-change', { id, status: 'disconnected' });
|
||||||
}
|
}
|
||||||
})
|
});
|
||||||
|
|
||||||
client.on('message', (topic, payload, packet) => {
|
client.on('message', (topic, payload, packet) => {
|
||||||
const msg = {
|
|
||||||
time:
|
let msg = {
|
||||||
new Date().toLocaleTimeString('zh-CN', { hour12: false }) +
|
time: new Date().toLocaleTimeString('zh-CN', { hour12: false }) + '.' + String(Date.now() % 1000).padStart(3, '0'),
|
||||||
'.' +
|
topic: topic,
|
||||||
String(Date.now() % 1000).padStart(3, '0'),
|
|
||||||
topic,
|
|
||||||
payload: payload.toString(),
|
payload: payload.toString(),
|
||||||
qos: packet.qos,
|
qos: packet.qos,
|
||||||
retain: packet.retain,
|
retain: packet.retain,
|
||||||
direction: 'sub',
|
direction: 'sub',
|
||||||
id: this.nextMsgId()
|
id: this.nextMsgId(),
|
||||||
}
|
};
|
||||||
const msgs = this.messages.get(id) || []
|
|
||||||
msgs.unshift(msg)
|
let msgs = this.messages.get(id) || [];
|
||||||
if (msgs.length > 500) msgs.pop()
|
|
||||||
this.messages.set(id, msgs)
|
msgs.unshift(msg);
|
||||||
this.notify('message', { serverId: id, message: 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) {
|
} catch (err) {
|
||||||
server.status = 'error'
|
server.status = 'error';
|
||||||
this.notify('status-change', { id, status: 'error', error: err.message })
|
this.notify('status-change', { id, status: 'error', error: err.message });
|
||||||
return false
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
disconnect(id) {
|
disconnect(id) {
|
||||||
const client = this.clients.get(id)
|
|
||||||
|
let client = this.clients.get(id);
|
||||||
|
|
||||||
if (client) {
|
if (client) {
|
||||||
client.end(true)
|
client.end(true);
|
||||||
this.clients.delete(id)
|
this.clients.delete(id);
|
||||||
}
|
}
|
||||||
const server = this.servers.get(id)
|
|
||||||
|
let server = this.servers.get(id);
|
||||||
|
|
||||||
if (server) {
|
if (server) {
|
||||||
server.status = 'disconnected'
|
server.status = 'disconnected';
|
||||||
server.topics.forEach((t) => {
|
server.topics.forEach((t) => {
|
||||||
t.subscribed = false
|
t.subscribed = false;
|
||||||
})
|
});
|
||||||
this.notify('status-change', { id, status: 'disconnected' })
|
this.notify('status-change', { id, status: 'disconnected' });
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ==================== 订阅管理 ====================
|
// ==================== 订阅管理 ====================
|
||||||
_doSubscribe(serverId, topic, opts) {
|
_doSubscribe(serverId, topic, opts) {
|
||||||
const client = this.clients.get(serverId)
|
|
||||||
const server = this.servers.get(serverId)
|
let client = this.clients.get(serverId);
|
||||||
if (!client || !server || server.status !== 'connected') return false
|
let server = this.servers.get(serverId);
|
||||||
|
|
||||||
|
if (!client || !server || server.status !== 'connected') {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
client.subscribe(topic, opts, (err) => {
|
client.subscribe(topic, opts, (err) => {
|
||||||
if (err) {
|
if (err) {
|
||||||
console.error(`订阅失败 [${topic}]:`, err.message)
|
console.error(`订阅失败 [${topic}]:`, err.message);
|
||||||
this.notify('subscription-error', { serverId, topic, error: err.message })
|
this.notify('subscription-error', { serverId, topic, error: err.message });
|
||||||
} else {
|
} else {
|
||||||
const t = server.topics.find((t) => t.topic === topic)
|
let t = server.topics.find((t) => t.topic === topic);
|
||||||
if (t) t.subscribed = true
|
if (t) {
|
||||||
this.notify('subscription-change', { serverId, topic, subscribed: true })
|
t.subscribed = true;
|
||||||
|
}
|
||||||
|
this.notify('subscription-change', { serverId, topic, subscribed: true });
|
||||||
}
|
}
|
||||||
})
|
});
|
||||||
return true
|
|
||||||
|
return true;
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
addTopic(serverId, topic, qos = 1) {
|
addTopic(serverId, topic, qos = 1) {
|
||||||
const server = this.servers.get(serverId)
|
let server = this.servers.get(serverId);
|
||||||
if (!server) return false
|
if (!server) {
|
||||||
if (server.topics.find((t) => t.topic === topic)) return false
|
return false;
|
||||||
|
}
|
||||||
|
if (server.topics.find((t) => t.topic === topic)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
const topicId = 'topic_' + Date.now() + '_' + Math.random().toString(36).slice(2, 6)
|
let topicId = 'topic_' + Date.now() + '_' + Math.random().toString(36).slice(2, 6);
|
||||||
server.topics.push({ id: topicId, topic, qos, subscribed: false })
|
|
||||||
this.saveData()
|
server.topics.push({ id: topicId, topic, qos, subscribed: false });
|
||||||
|
|
||||||
|
this.saveData();
|
||||||
|
|
||||||
// 如果已连接,自动订阅
|
// 如果已连接,自动订阅
|
||||||
if (server.status === 'connected') {
|
if (server.status === 'connected') {
|
||||||
this._doSubscribe(serverId, topic, { qos })
|
this._doSubscribe(serverId, topic, { qos });
|
||||||
}
|
}
|
||||||
return true
|
|
||||||
|
return true;
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
removeTopic(serverId, topic) {
|
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)
|
this.unsubscribe(serverId, topic);
|
||||||
server.topics = server.topics.filter((t) => t.topic !== topic)
|
|
||||||
this.saveData()
|
server.topics = server.topics.filter((t) => t.topic !== topic);
|
||||||
return true
|
|
||||||
|
this.saveData();
|
||||||
|
|
||||||
|
return true;
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
subscribe(serverId, topic) {
|
subscribe(serverId, topic) {
|
||||||
const server = this.servers.get(serverId)
|
|
||||||
if (!server) return false
|
let server = this.servers.get(serverId);
|
||||||
const t = server.topics.find((t) => t.topic === topic)
|
|
||||||
if (!t) return false
|
if (!server) {
|
||||||
return this._doSubscribe(serverId, topic, { qos: t.qos })
|
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) {
|
unsubscribe(serverId, topic) {
|
||||||
const client = this.clients.get(serverId)
|
|
||||||
const server = this.servers.get(serverId)
|
let client = this.clients.get(serverId);
|
||||||
if (!client || !server || server.status !== 'connected') return false
|
let server = this.servers.get(serverId);
|
||||||
|
|
||||||
|
if (!client || !server || server.status !== 'connected') {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
client.unsubscribe(topic, {}, (err) => {
|
client.unsubscribe(topic, {}, (err) => {
|
||||||
if (!err) {
|
if (!err) {
|
||||||
const t = server.topics.find((t) => t.topic === topic)
|
let t = server.topics.find((t) => t.topic === topic);
|
||||||
if (t) t.subscribed = false
|
if (t) {
|
||||||
this.notify('subscription-change', { serverId, topic, subscribed: false })
|
t.subscribed = false;
|
||||||
|
}
|
||||||
|
this.notify('subscription-change', { serverId, topic, subscribed: false });
|
||||||
}
|
}
|
||||||
})
|
});
|
||||||
return true
|
|
||||||
|
return true;
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
updateTopic(serverId, topicId, newTopic, newQos) {
|
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 (
|
if (
|
||||||
topicChanged &&
|
topicChanged &&
|
||||||
server.topics.find((item) => item.topic === newTopic && item.id !== topicId)
|
server.topics.find((item) => item.topic === newTopic && item.id !== topicId)
|
||||||
) {
|
) {
|
||||||
return false
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
const oldTopic = t.topic
|
let oldTopic = t.topic;
|
||||||
const wasSubscribed = t.subscribed
|
let wasSubscribed = t.subscribed;
|
||||||
|
|
||||||
// 如果主题名变更且已订阅,先取消旧主题订阅
|
// 如果主题名变更且已订阅,先取消旧主题订阅
|
||||||
if (topicChanged && wasSubscribed) {
|
if (topicChanged && wasSubscribed) {
|
||||||
this.unsubscribe(serverId, oldTopic)
|
this.unsubscribe(serverId, oldTopic);
|
||||||
}
|
}
|
||||||
|
|
||||||
t.topic = newTopic
|
t.topic = newTopic;
|
||||||
t.qos = newQos
|
t.qos = newQos;
|
||||||
|
|
||||||
// 如果已连接且此前已订阅,重新订阅新主题
|
// 如果已连接且此前已订阅,重新订阅新主题
|
||||||
if (server.status === 'connected' && wasSubscribed) {
|
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', {
|
this.notify('subscription-change', {
|
||||||
serverId,
|
serverId,
|
||||||
topic: newTopic,
|
topic: newTopic,
|
||||||
subscribed: t.subscribed,
|
subscribed: t.subscribed,
|
||||||
topicId
|
topicId,
|
||||||
})
|
});
|
||||||
return true
|
|
||||||
|
return true;
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
subscribeAll(serverId) {
|
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)
|
let client = this.clients.get(serverId);
|
||||||
if (topicsToSubscribe.length === 0) return true
|
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) => {
|
client.subscribe(subMap, (err) => {
|
||||||
if (err) {
|
if (err) {
|
||||||
console.error('批量订阅失败:', err.message)
|
console.error('批量订阅失败:', err.message);
|
||||||
topicsToSubscribe.forEach((t) => {
|
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 {
|
} else {
|
||||||
topicsToSubscribe.forEach((t) => {
|
topicsToSubscribe.forEach((t) => {
|
||||||
t.subscribed = true
|
t.subscribed = true;
|
||||||
this.notify('subscription-change', {
|
this.notify('subscription-change', {
|
||||||
serverId,
|
serverId,
|
||||||
topic: t.topic,
|
topic: t.topic,
|
||||||
subscribed: true,
|
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 = {
|
publish(serverId, topic, payload, opts = {}) {
|
||||||
qos: opts.qos ?? 1,
|
|
||||||
retain: opts.retain ?? false
|
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) => {
|
client.publish(topic, payload, pubOpts, (err) => {
|
||||||
if (err) {
|
if (err) {
|
||||||
console.error(`发布失败 [${topic}]:`, err.message)
|
console.error(`发布失败 [${topic}]:`, err.message);
|
||||||
this.notify('publish-error', { serverId, topic, error: err.message })
|
this.notify('publish-error', { serverId, topic, error: err.message });
|
||||||
}
|
}
|
||||||
})
|
});
|
||||||
|
|
||||||
// 记录发布历史
|
// 记录发布历史
|
||||||
const pubEntry = {
|
let pubEntry = {
|
||||||
time:
|
time:
|
||||||
new Date().toLocaleTimeString('zh-CN', { hour12: false }) +
|
new Date().toLocaleTimeString('zh-CN', { hour12: false }) +
|
||||||
'.' +
|
'.' +
|
||||||
@@ -460,55 +579,70 @@ class MqttManager {
|
|||||||
payload,
|
payload,
|
||||||
qos: pubOpts.qos,
|
qos: pubOpts.qos,
|
||||||
retain: pubOpts.retain,
|
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)
|
this.publishHistory.set(serverId, history);
|
||||||
if (history.length > 50) history.pop()
|
|
||||||
this.publishHistory.set(serverId, history)
|
|
||||||
|
|
||||||
// 同时添加到消息日志
|
// 同时添加到消息日志
|
||||||
const pubMsgId = this.nextMsgId()
|
let pubMsgId = this.nextMsgId();
|
||||||
const msgs = this.messages.get(serverId) || []
|
let msgs = this.messages.get(serverId) || [];
|
||||||
|
|
||||||
msgs.unshift({
|
msgs.unshift({
|
||||||
...pubEntry,
|
...pubEntry,
|
||||||
id: pubMsgId
|
id: pubMsgId,
|
||||||
})
|
});
|
||||||
if (msgs.length > 500) msgs.pop()
|
|
||||||
this.messages.set(serverId, msgs)
|
if (msgs.length > 500) {
|
||||||
|
msgs.pop();
|
||||||
|
}
|
||||||
|
|
||||||
|
this.messages.set(serverId, msgs);
|
||||||
|
|
||||||
this.notify('message', {
|
this.notify('message', {
|
||||||
serverId,
|
serverId,
|
||||||
message: {
|
message: {
|
||||||
...pubEntry,
|
...pubEntry,
|
||||||
id: pubMsgId
|
id: pubMsgId,
|
||||||
}
|
},
|
||||||
})
|
});
|
||||||
|
|
||||||
|
this.saveData();
|
||||||
|
|
||||||
|
return true;
|
||||||
|
|
||||||
this.saveData()
|
|
||||||
return true
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ==================== 消息管理 ====================
|
// ==================== 消息管理 ====================
|
||||||
|
|
||||||
getMessages(serverId) {
|
getMessages(serverId) {
|
||||||
return this.messages.get(serverId) || []
|
return this.messages.get(serverId) || [];
|
||||||
}
|
}
|
||||||
|
|
||||||
getPublishHistory(serverId) {
|
getPublishHistory(serverId) {
|
||||||
return this.publishHistory.get(serverId) || []
|
return this.publishHistory.get(serverId) || [];
|
||||||
}
|
}
|
||||||
|
|
||||||
clearMessages(serverId) {
|
clearMessages(serverId) {
|
||||||
this.messages.set(serverId, [])
|
this.messages.set(serverId, []);
|
||||||
this.saveData()
|
this.saveData();
|
||||||
}
|
}
|
||||||
|
|
||||||
clearPublishHistory(serverId) {
|
clearPublishHistory(serverId) {
|
||||||
this.publishHistory.set(serverId, [])
|
this.publishHistory.set(serverId, []);
|
||||||
this.saveData()
|
this.saveData();
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 单例
|
// 单例
|
||||||
const mqttManager = new MqttManager()
|
const mqttManager = new MqttManager();
|
||||||
export default mqttManager
|
export default mqttManager;
|
||||||
|
|||||||
@@ -1,8 +1,9 @@
|
|||||||
import { contextBridge, ipcRenderer } from 'electron'
|
import { contextBridge, ipcRenderer } from 'electron';
|
||||||
import { electronAPI } from '@electron-toolkit/preload'
|
import { electronAPI } from '@electron-toolkit/preload';
|
||||||
|
|
||||||
// Custom APIs for renderer
|
// Custom APIs for renderer
|
||||||
const api = {
|
const api = {
|
||||||
|
|
||||||
// 服务器管理
|
// 服务器管理
|
||||||
getServers: () => ipcRenderer.invoke('mqtt:get-servers'),
|
getServers: () => ipcRenderer.invoke('mqtt:get-servers'),
|
||||||
addServer: (config) => ipcRenderer.invoke('mqtt:add-server', config),
|
addServer: (config) => ipcRenderer.invoke('mqtt:add-server', config),
|
||||||
@@ -34,43 +35,44 @@ const api = {
|
|||||||
|
|
||||||
// 事件监听
|
// 事件监听
|
||||||
onStatusChange: (callback) => {
|
onStatusChange: (callback) => {
|
||||||
const handler = (_, data) => callback(data)
|
const handler = (_, data) => callback(data);
|
||||||
ipcRenderer.on('mqtt:status-change', handler)
|
ipcRenderer.on('mqtt:status-change', handler);
|
||||||
return () => ipcRenderer.removeListener('mqtt:status-change', handler)
|
return () => ipcRenderer.removeListener('mqtt:status-change', handler);
|
||||||
},
|
},
|
||||||
onMessage: (callback) => {
|
onMessage: (callback) => {
|
||||||
const handler = (_, data) => callback(data)
|
const handler = (_, data) => callback(data);
|
||||||
ipcRenderer.on('mqtt:message', handler)
|
ipcRenderer.on('mqtt:message', handler);
|
||||||
return () => ipcRenderer.removeListener('mqtt:message', handler)
|
return () => ipcRenderer.removeListener('mqtt:message', handler);
|
||||||
},
|
},
|
||||||
onSubscriptionChange: (callback) => {
|
onSubscriptionChange: (callback) => {
|
||||||
const handler = (_, data) => callback(data)
|
const handler = (_, data) => callback(data);
|
||||||
ipcRenderer.on('mqtt:subscription-change', handler)
|
ipcRenderer.on('mqtt:subscription-change', handler);
|
||||||
return () => ipcRenderer.removeListener('mqtt:subscription-change', handler)
|
return () => ipcRenderer.removeListener('mqtt:subscription-change', handler);
|
||||||
},
|
},
|
||||||
onSubscriptionError: (callback) => {
|
onSubscriptionError: (callback) => {
|
||||||
const handler = (_, data) => callback(data)
|
const handler = (_, data) => callback(data);
|
||||||
ipcRenderer.on('mqtt:subscription-error', handler)
|
ipcRenderer.on('mqtt:subscription-error', handler);
|
||||||
return () => ipcRenderer.removeListener('mqtt:subscription-error', handler)
|
return () => ipcRenderer.removeListener('mqtt:subscription-error', handler);
|
||||||
},
|
},
|
||||||
onPublishError: (callback) => {
|
onPublishError: (callback) => {
|
||||||
const handler = (_, data) => callback(data)
|
const handler = (_, data) => callback(data);
|
||||||
ipcRenderer.on('mqtt:publish-error', handler)
|
ipcRenderer.on('mqtt:publish-error', handler);
|
||||||
return () => ipcRenderer.removeListener('mqtt:publish-error', handler)
|
return () => ipcRenderer.removeListener('mqtt:publish-error', handler);
|
||||||
}
|
},
|
||||||
}
|
|
||||||
|
};
|
||||||
|
|
||||||
// Use `contextBridge` APIs to expose Electron APIs to
|
// Use `contextBridge` APIs to expose Electron APIs to
|
||||||
// renderer only if context isolation is enabled, otherwise
|
// renderer only if context isolation is enabled, otherwise
|
||||||
// just add to the DOM global.
|
// just add to the DOM global.
|
||||||
if (process.contextIsolated) {
|
if (process.contextIsolated) {
|
||||||
try {
|
try {
|
||||||
contextBridge.exposeInMainWorld('electron', electronAPI)
|
contextBridge.exposeInMainWorld('electron', electronAPI);
|
||||||
contextBridge.exposeInMainWorld('api', api)
|
contextBridge.exposeInMainWorld('api', api);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(error)
|
console.error(error);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
window.electron = electronAPI
|
window.electron = electronAPI;
|
||||||
window.api = api
|
window.api = api;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,16 +1,16 @@
|
|||||||
<!doctype html>
|
<!DOCTYPE html>
|
||||||
<html>
|
<html lang="zh-CN">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8" />
|
<meta charset="UTF-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
<title>Frost MQTT Client</title>
|
<title>Frost MQTT Client</title>
|
||||||
<meta
|
<meta
|
||||||
http-equiv="Content-Security-Policy"
|
http-equiv="Content-Security-Policy"
|
||||||
content="default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data:; connect-src 'self' ws: wss: mqtt: mqtts:"
|
content="default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data:; connect-src 'self' ws: wss: mqtt: mqtts:"
|
||||||
/>
|
/>
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
<body>
|
<body>
|
||||||
<div id="app"></div>
|
<div id="app"></div>
|
||||||
<script type="module" src="/src/main.js"></script>
|
<script type="module" src="/src/main.js"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@@ -1,41 +1,33 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import { ref, onMounted } from 'vue'
|
import { ref, onMounted } from 'vue';
|
||||||
import {
|
import { NLayout, NMessageProvider, NDialogProvider, NConfigProvider, NTabs, NTabPane, zhCN, dateZhCN } from 'naive-ui';
|
||||||
NLayout,
|
import { useMqttStore } from './stores/mqtt.js';
|
||||||
NMessageProvider,
|
|
||||||
NDialogProvider,
|
|
||||||
NConfigProvider,
|
|
||||||
NTabs,
|
|
||||||
NTabPane,
|
|
||||||
zhCN,
|
|
||||||
dateZhCN
|
|
||||||
} from 'naive-ui'
|
|
||||||
import Sidebar from './components/Sidebar.vue'
|
|
||||||
import ServerModal from './components/ServerModal.vue'
|
|
||||||
import Dashboard from './views/Dashboard.vue'
|
|
||||||
import Subscribe from './views/Subscribe.vue'
|
|
||||||
import Publish from './views/Publish.vue'
|
|
||||||
import Messages from './views/Messages.vue'
|
|
||||||
import { useMqttStore } from './stores/mqtt.js'
|
|
||||||
|
|
||||||
const store = useMqttStore()
|
import Sidebar from './components/Sidebar.vue';
|
||||||
|
import ServerModal from './components/ServerModal.vue';
|
||||||
|
import Dashboard from './views/Dashboard.vue';
|
||||||
|
import Subscribe from './views/Subscribe.vue';
|
||||||
|
import Publish from './views/Publish.vue';
|
||||||
|
import Messages from './views/Messages.vue';
|
||||||
|
|
||||||
const activeTab = ref('dashboard')
|
const store = useMqttStore();
|
||||||
const showServerModal = ref(false)
|
|
||||||
const editServerData = ref(null)
|
const activeTab = ref('dashboard');
|
||||||
|
const showServerModal = ref(false);
|
||||||
|
const editServerData = ref(null);
|
||||||
|
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
await store.init()
|
await store.init();
|
||||||
})
|
});
|
||||||
|
|
||||||
function handleAddServer() {
|
function handleAddServer() {
|
||||||
editServerData.value = null
|
editServerData.value = null;
|
||||||
showServerModal.value = true
|
showServerModal.value = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleEditServer(server) {
|
function handleEditServer(server) {
|
||||||
editServerData.value = server
|
editServerData.value = server;
|
||||||
showServerModal.value = true
|
showServerModal.value = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleServerSaved() {
|
function handleServerSaved() {
|
||||||
@@ -43,32 +35,60 @@ function handleServerSaved() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function handleSwitchTab(tab) {
|
function handleSwitchTab(tab) {
|
||||||
activeTab.value = tab
|
activeTab.value = tab;
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<n-config-provider :locale="zhCN" :date-locale="dateZhCN" :theme="null">
|
<n-config-provider
|
||||||
|
:locale="zhCN"
|
||||||
|
:date-locale="dateZhCN"
|
||||||
|
:theme="null"
|
||||||
|
>
|
||||||
<n-message-provider placement="bottom">
|
<n-message-provider placement="bottom">
|
||||||
<n-dialog-provider>
|
<n-dialog-provider>
|
||||||
<n-layout has-sider position="absolute">
|
<n-layout
|
||||||
|
has-sider
|
||||||
|
position="absolute"
|
||||||
|
>
|
||||||
|
|
||||||
<!-- 侧边栏 -->
|
<!-- 侧边栏 -->
|
||||||
<Sidebar @add-server="handleAddServer" @edit-server="handleEditServer" />
|
<Sidebar
|
||||||
|
@add-server="handleAddServer"
|
||||||
|
@edit-server="handleEditServer"
|
||||||
|
/>
|
||||||
|
|
||||||
<!-- 主内容区 -->
|
<!-- 主内容区 -->
|
||||||
<n-layout content-class="main-layout-content">
|
<n-layout content-class="main-layout-content">
|
||||||
<n-tabs v-model:value="activeTab" type="line" size="medium" style="padding: 0 16px">
|
<n-tabs
|
||||||
<n-tab-pane name="dashboard" tab="仪表盘">
|
v-model:value="activeTab"
|
||||||
|
type="line"
|
||||||
|
size="medium"
|
||||||
|
style="padding: 0 16px"
|
||||||
|
>
|
||||||
|
<n-tab-pane
|
||||||
|
name="dashboard"
|
||||||
|
tab="仪表盘"
|
||||||
|
>
|
||||||
<div class="tab-content dashboard-content">
|
<div class="tab-content dashboard-content">
|
||||||
<Dashboard @switch-tab="handleSwitchTab" @edit-server="handleEditServer" />
|
<Dashboard
|
||||||
|
@switch-tab="handleSwitchTab"
|
||||||
|
@edit-server="handleEditServer"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</n-tab-pane>
|
</n-tab-pane>
|
||||||
<n-tab-pane name="subscribe" tab="主题订阅">
|
<n-tab-pane
|
||||||
|
name="subscribe"
|
||||||
|
tab="主题订阅"
|
||||||
|
>
|
||||||
<div class="tab-content">
|
<div class="tab-content">
|
||||||
<Subscribe />
|
<Subscribe />
|
||||||
</div>
|
</div>
|
||||||
</n-tab-pane>
|
</n-tab-pane>
|
||||||
<n-tab-pane name="messages" tab="消息中心">
|
<n-tab-pane
|
||||||
|
name="messages"
|
||||||
|
tab="消息中心"
|
||||||
|
>
|
||||||
<div class="tab-content">
|
<div class="tab-content">
|
||||||
<div class="message-center-layout">
|
<div class="message-center-layout">
|
||||||
<section class="publish-panel">
|
<section class="publish-panel">
|
||||||
@@ -88,6 +108,7 @@ function handleSwitchTab(tab) {
|
|||||||
:edit-server="editServerData"
|
:edit-server="editServerData"
|
||||||
@saved="handleServerSaved"
|
@saved="handleServerSaved"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
</n-layout>
|
</n-layout>
|
||||||
</n-dialog-provider>
|
</n-dialog-provider>
|
||||||
</n-message-provider>
|
</n-message-provider>
|
||||||
@@ -95,9 +116,7 @@ function handleSwitchTab(tab) {
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
html,
|
html, body, #app {
|
||||||
body,
|
|
||||||
#app {
|
|
||||||
margin: 0;
|
margin: 0;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
|
|||||||
@@ -1,31 +1,19 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import { ref, watch } from 'vue'
|
import { ref, watch } from 'vue';
|
||||||
import {
|
import { NModal, NForm, NFormItem, NGrid, NFormItemGi, NInput, NInputNumber, NSelect, NSwitch, NButton, useMessage } from 'naive-ui';
|
||||||
NModal,
|
import { useMqttStore } from '../stores/mqtt.js';
|
||||||
NForm,
|
|
||||||
NFormItem,
|
|
||||||
NGrid,
|
|
||||||
NFormItemGi,
|
|
||||||
NInput,
|
|
||||||
NInputNumber,
|
|
||||||
NSelect,
|
|
||||||
NSwitch,
|
|
||||||
NButton,
|
|
||||||
useMessage
|
|
||||||
} from 'naive-ui'
|
|
||||||
import { useMqttStore } from '../stores/mqtt.js'
|
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
show: Boolean,
|
show: Boolean,
|
||||||
editServer: Object
|
editServer: Object,
|
||||||
})
|
});
|
||||||
|
|
||||||
const emit = defineEmits(['update:show', 'saved'])
|
const emit = defineEmits(['update:show', 'saved']);
|
||||||
|
|
||||||
const store = useMqttStore()
|
const store = useMqttStore();
|
||||||
const msg = useMessage()
|
const msg = useMessage();
|
||||||
|
|
||||||
const formRef = ref(null)
|
const formRef = ref(null);
|
||||||
const form = ref({
|
const form = ref({
|
||||||
name: '',
|
name: '',
|
||||||
host: '',
|
host: '',
|
||||||
@@ -35,30 +23,30 @@ const form = ref({
|
|||||||
username: '',
|
username: '',
|
||||||
password: '',
|
password: '',
|
||||||
keepAlive: 60,
|
keepAlive: 60,
|
||||||
cleanSession: true
|
cleanSession: true,
|
||||||
})
|
});
|
||||||
|
|
||||||
const rules = {
|
const rules = {
|
||||||
name: { required: true, message: '请输入服务器名称', trigger: 'blur' },
|
name: { required: true, message: '请输入服务器名称', trigger: 'blur' },
|
||||||
host: { required: true, message: '请输入主机地址', trigger: 'blur' },
|
host: { required: true, message: '请输入主机地址', trigger: 'blur' },
|
||||||
port: { required: true, type: 'number', message: '请输入端口号', trigger: 'blur' }
|
port: { required: true, type: 'number', message: '请输入端口号', trigger: 'blur' },
|
||||||
}
|
};
|
||||||
|
|
||||||
const protocolOptions = [
|
const protocolOptions = [
|
||||||
{ label: 'mqtt:// (默认)', value: 'mqtt' },
|
{ label: 'mqtt:// (默认)', value: 'mqtt' },
|
||||||
{ label: 'mqtts:// (TLS)', value: 'mqtts' },
|
{ label: 'mqtts:// (TLS)', value: 'mqtts' },
|
||||||
{ label: 'ws:// (WebSocket)', value: 'ws' },
|
{ label: 'ws:// (WebSocket)', value: 'ws' },
|
||||||
{ label: 'wss:// (WebSocket TLS)', value: 'wss' }
|
{ label: 'wss:// (WebSocket TLS)', value: 'wss' },
|
||||||
]
|
];
|
||||||
|
|
||||||
const isEdit = ref(false)
|
const isEdit = ref(false);
|
||||||
|
|
||||||
watch(
|
watch(
|
||||||
() => props.show,
|
() => props.show,
|
||||||
(val) => {
|
(val) => {
|
||||||
if (val) {
|
if (val) {
|
||||||
if (props.editServer) {
|
if (props.editServer) {
|
||||||
isEdit.value = true
|
isEdit.value = true;
|
||||||
form.value = {
|
form.value = {
|
||||||
name: props.editServer.name,
|
name: props.editServer.name,
|
||||||
host: props.editServer.host,
|
host: props.editServer.host,
|
||||||
@@ -68,10 +56,10 @@ watch(
|
|||||||
username: props.editServer.username || '',
|
username: props.editServer.username || '',
|
||||||
password: '',
|
password: '',
|
||||||
keepAlive: props.editServer.keepAlive || 60,
|
keepAlive: props.editServer.keepAlive || 60,
|
||||||
cleanSession: props.editServer.cleanSession !== false
|
cleanSession: props.editServer.cleanSession !== false,
|
||||||
}
|
};
|
||||||
} else {
|
} else {
|
||||||
isEdit.value = false
|
isEdit.value = false;
|
||||||
form.value = {
|
form.value = {
|
||||||
name: '',
|
name: '',
|
||||||
host: '',
|
host: '',
|
||||||
@@ -81,61 +69,101 @@ watch(
|
|||||||
username: '',
|
username: '',
|
||||||
password: '',
|
password: '',
|
||||||
keepAlive: 60,
|
keepAlive: 60,
|
||||||
cleanSession: true
|
cleanSession: true,
|
||||||
}
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
)
|
);
|
||||||
|
|
||||||
async function handleSave() {
|
async function handleSave() {
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await formRef.value?.validate()
|
await formRef.value?.validate();
|
||||||
} catch {
|
} catch {
|
||||||
return
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
|
||||||
if (isEdit.value && props.editServer) {
|
if (isEdit.value && props.editServer) {
|
||||||
await store.updateServer(props.editServer.id, { ...form.value })
|
await store.updateServer(props.editServer.id, { ...form.value });
|
||||||
msg.success('服务器配置已更新')
|
msg.success('服务器配置已更新');
|
||||||
} else {
|
} else {
|
||||||
await store.addServer({ ...form.value })
|
await store.addServer({ ...form.value });
|
||||||
msg.success('服务器已添加')
|
msg.success('服务器已添加');
|
||||||
}
|
}
|
||||||
emit('saved')
|
|
||||||
emit('update:show', false)
|
emit('saved');
|
||||||
|
emit('update:show', false);
|
||||||
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
msg.error('操作失败: ' + (e.message || '未知错误'))
|
msg.error('操作失败: ' + (e.message || '未知错误'));
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<n-modal
|
<n-modal
|
||||||
:show="show"
|
|
||||||
:mask-closable="false"
|
:mask-closable="false"
|
||||||
preset="card"
|
:show="show"
|
||||||
:title="isEdit ? '编辑 MQTT 服务器' : '添加 MQTT 服务器'"
|
:title="isEdit ? '编辑 MQTT 服务器' : '添加 MQTT 服务器'"
|
||||||
|
preset="card"
|
||||||
style="width: 520px; max-width: 90vw"
|
style="width: 520px; max-width: 90vw"
|
||||||
@update:show="$emit('update:show', $event)"
|
@update:show="$emit('update:show', $event)"
|
||||||
>
|
>
|
||||||
<n-form ref="formRef" :model="form" :rules="rules" label-placement="top" size="medium">
|
<n-form
|
||||||
<n-grid :cols="2" :x-gap="12">
|
ref="formRef"
|
||||||
<n-form-item-gi label="服务器名称" path="name">
|
:model="form"
|
||||||
<n-input v-model:value="form.name" placeholder="例如: 本地开发服务器" />
|
:rules="rules"
|
||||||
|
label-placement="top"
|
||||||
|
size="medium"
|
||||||
|
>
|
||||||
|
<n-grid
|
||||||
|
:cols="2"
|
||||||
|
:x-gap="12"
|
||||||
|
>
|
||||||
|
<n-form-item-gi
|
||||||
|
label="服务器名称"
|
||||||
|
path="name"
|
||||||
|
>
|
||||||
|
<n-input
|
||||||
|
v-model:value="form.name"
|
||||||
|
placeholder="例如: 本地开发服务器"
|
||||||
|
/>
|
||||||
</n-form-item-gi>
|
</n-form-item-gi>
|
||||||
<n-form-item-gi label="主机地址" path="host">
|
<n-form-item-gi
|
||||||
<n-input v-model:value="form.host" placeholder="例如: broker.emqx.io" />
|
label="主机地址"
|
||||||
|
path="host"
|
||||||
|
>
|
||||||
|
<n-input
|
||||||
|
v-model:value="form.host"
|
||||||
|
placeholder="例如: broker.emqx.io"
|
||||||
|
/>
|
||||||
</n-form-item-gi>
|
</n-form-item-gi>
|
||||||
<n-form-item-gi label="端口" path="port">
|
<n-form-item-gi
|
||||||
<n-input-number v-model:value="form.port" :min="1" :max="65535" style="width: 100%" />
|
label="端口"
|
||||||
|
path="port"
|
||||||
|
>
|
||||||
|
<n-input-number
|
||||||
|
v-model:value="form.port"
|
||||||
|
:min="1"
|
||||||
|
:max="65535"
|
||||||
|
style="width: 100%"
|
||||||
|
/>
|
||||||
</n-form-item-gi>
|
</n-form-item-gi>
|
||||||
<n-form-item-gi label="协议">
|
<n-form-item-gi label="协议">
|
||||||
<n-select v-model:value="form.protocol" :options="protocolOptions" />
|
<n-select
|
||||||
|
v-model:value="form.protocol"
|
||||||
|
:options="protocolOptions"
|
||||||
|
/>
|
||||||
</n-form-item-gi>
|
</n-form-item-gi>
|
||||||
<n-form-item-gi label="客户端 ID">
|
<n-form-item-gi label="客户端 ID">
|
||||||
<n-input v-model:value="form.clientId" placeholder="自动生成" />
|
<n-input
|
||||||
|
v-model:value="form.clientId"
|
||||||
|
placeholder="自动生成"
|
||||||
|
/>
|
||||||
</n-form-item-gi>
|
</n-form-item-gi>
|
||||||
<n-form-item-gi label="Keep Alive (秒)">
|
<n-form-item-gi label="Keep Alive (秒)">
|
||||||
<n-input-number
|
<n-input-number
|
||||||
@@ -146,10 +174,17 @@ async function handleSave() {
|
|||||||
/>
|
/>
|
||||||
</n-form-item-gi>
|
</n-form-item-gi>
|
||||||
<n-form-item-gi label="用户名">
|
<n-form-item-gi label="用户名">
|
||||||
<n-input v-model:value="form.username" placeholder="可选" />
|
<n-input
|
||||||
|
v-model:value="form.username"
|
||||||
|
placeholder="可选"
|
||||||
|
/>
|
||||||
</n-form-item-gi>
|
</n-form-item-gi>
|
||||||
<n-form-item-gi label="密码">
|
<n-form-item-gi label="密码">
|
||||||
<n-input v-model:value="form.password" type="password" placeholder="可选" />
|
<n-input
|
||||||
|
v-model:value="form.password"
|
||||||
|
type="password"
|
||||||
|
placeholder="可选"
|
||||||
|
/>
|
||||||
</n-form-item-gi>
|
</n-form-item-gi>
|
||||||
</n-grid>
|
</n-grid>
|
||||||
<n-form-item label="Clean Session">
|
<n-form-item label="Clean Session">
|
||||||
@@ -158,8 +193,13 @@ async function handleSave() {
|
|||||||
</n-form>
|
</n-form>
|
||||||
<template #footer>
|
<template #footer>
|
||||||
<div style="display: flex; justify-content: flex-end; gap: 8px">
|
<div style="display: flex; justify-content: flex-end; gap: 8px">
|
||||||
<n-button @click="$emit('update:show', false)">取消</n-button>
|
<n-button
|
||||||
<n-button type="primary" @click="handleSave">{{ isEdit ? '更新' : '保存' }}</n-button>
|
@click="$emit('update:show', false)"
|
||||||
|
>取消</n-button>
|
||||||
|
<n-button
|
||||||
|
type="primary"
|
||||||
|
@click="handleSave"
|
||||||
|
>{{ isEdit ? '更新' : '保存' }}</n-button>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
</n-modal>
|
</n-modal>
|
||||||
|
|||||||
@@ -1,23 +1,23 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import { NLayoutSider, NButton, NTag, useMessage, useDialog } from 'naive-ui'
|
import { NLayoutSider, NButton, NTag, useMessage, useDialog } from 'naive-ui';
|
||||||
import { useMqttStore } from '../stores/mqtt.js'
|
import { useMqttStore } from '../stores/mqtt.js';
|
||||||
|
|
||||||
const emit = defineEmits(['add-server', 'edit-server'])
|
const emit = defineEmits(['add-server', 'edit-server']);
|
||||||
const store = useMqttStore()
|
const store = useMqttStore();
|
||||||
const message = useMessage()
|
const message = useMessage();
|
||||||
const dialog = useDialog()
|
const dialog = useDialog();
|
||||||
|
|
||||||
async function handleConnect(server) {
|
async function handleConnect(server) {
|
||||||
if (server.status === 'connected') {
|
if (server.status === 'connected') {
|
||||||
await store.disconnectServer(server.id)
|
await store.disconnectServer(server.id);
|
||||||
message.info('已断开连接: ' + server.name)
|
message.info('已断开连接: ' + server.name);
|
||||||
} else {
|
} else {
|
||||||
await store.connectServer(server.id)
|
await store.connectServer(server.id);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleEdit(server) {
|
function handleEdit(server) {
|
||||||
emit('edit-server', server)
|
emit('edit-server', server);
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleDelete(server) {
|
function handleDelete(server) {
|
||||||
@@ -27,10 +27,10 @@ function handleDelete(server) {
|
|||||||
positiveText: '删除',
|
positiveText: '删除',
|
||||||
negativeText: '取消',
|
negativeText: '取消',
|
||||||
onPositiveClick: async () => {
|
onPositiveClick: async () => {
|
||||||
await store.deleteServer(server.id)
|
await store.deleteServer(server.id);
|
||||||
message.info('服务器已删除')
|
message.info('服务器已删除');
|
||||||
}
|
},
|
||||||
})
|
});
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
@@ -43,9 +43,13 @@ function handleDelete(server) {
|
|||||||
show-trigger="bar"
|
show-trigger="bar"
|
||||||
:native-scrollbar="true"
|
:native-scrollbar="true"
|
||||||
content-class="sidebar-scroll-container"
|
content-class="sidebar-scroll-container"
|
||||||
:content-style="{ height: '100%', overflow: 'hidden' }"
|
:content-style="{
|
||||||
|
height: '100%',
|
||||||
|
overflow: 'hidden',
|
||||||
|
}"
|
||||||
>
|
>
|
||||||
<div class="sidebar-container">
|
<div class="sidebar-container">
|
||||||
|
|
||||||
<div class="sidebar-header">
|
<div class="sidebar-header">
|
||||||
<div class="sidebar-logo">F</div>
|
<div class="sidebar-logo">F</div>
|
||||||
<div>
|
<div>
|
||||||
@@ -65,12 +69,21 @@ function handleDelete(server) {
|
|||||||
<div class="server-item-top">
|
<div class="server-item-top">
|
||||||
<span class="server-name">{{ srv.name }}</span>
|
<span class="server-name">{{ srv.name }}</span>
|
||||||
<div class="server-actions">
|
<div class="server-actions">
|
||||||
<n-button text size="tiny" @click.stop="handleEdit(srv)">
|
<n-button
|
||||||
|
text
|
||||||
|
size="tiny"
|
||||||
|
@click.stop="handleEdit(srv)"
|
||||||
|
>
|
||||||
<template #icon>
|
<template #icon>
|
||||||
<span style="font-size: 14px">✎</span>
|
<span style="font-size: 14px">✎</span>
|
||||||
</template>
|
</template>
|
||||||
</n-button>
|
</n-button>
|
||||||
<n-button text size="tiny" type="error" @click.stop="handleDelete(srv)">
|
<n-button
|
||||||
|
text
|
||||||
|
size="tiny"
|
||||||
|
type="error"
|
||||||
|
@click.stop="handleDelete(srv)"
|
||||||
|
>
|
||||||
<template #icon>
|
<template #icon>
|
||||||
<span style="font-size: 14px">✕</span>
|
<span style="font-size: 14px">✕</span>
|
||||||
</template>
|
</template>
|
||||||
@@ -81,55 +94,39 @@ function handleDelete(server) {
|
|||||||
<n-tag
|
<n-tag
|
||||||
:bordered="false"
|
:bordered="false"
|
||||||
size="small"
|
size="small"
|
||||||
:type="
|
:type="srv.status === 'connected' ? 'success' : srv.status === 'connecting' ? 'warning' : 'default'"
|
||||||
srv.status === 'connected'
|
|
||||||
? 'success'
|
|
||||||
: srv.status === 'connecting'
|
|
||||||
? 'warning'
|
|
||||||
: 'default'
|
|
||||||
"
|
|
||||||
>
|
>
|
||||||
<template #icon>
|
<template #icon>
|
||||||
<span class="status-dot" :class="srv.status" />
|
<span
|
||||||
|
class="status-dot"
|
||||||
|
:class="srv.status"
|
||||||
|
/>
|
||||||
</template>
|
</template>
|
||||||
{{ srv.protocol }}://{{ srv.host }}:{{ srv.port }}
|
{{ srv.protocol }}://{{ srv.host }}:{{ srv.port }}
|
||||||
</n-tag>
|
</n-tag>
|
||||||
</div>
|
</div>
|
||||||
<div class="server-bottom">
|
<div class="server-bottom">
|
||||||
<span class="server-status-text">
|
<span
|
||||||
{{
|
class="server-status-text"
|
||||||
srv.status === 'connected'
|
>{{ srv.status === 'connected' ? '已连接' : srv.status === 'connecting' ? '连接中...' : srv.status === 'error' ? '连接失败' : '未连接' }}</span>
|
||||||
? '已连接'
|
|
||||||
: srv.status === 'connecting'
|
|
||||||
? '连接中...'
|
|
||||||
: srv.status === 'error'
|
|
||||||
? '连接失败'
|
|
||||||
: '未连接'
|
|
||||||
}}
|
|
||||||
</span>
|
|
||||||
<n-button
|
<n-button
|
||||||
size="tiny"
|
size="tiny"
|
||||||
:type="
|
:type="srv.status === 'connected' ? 'error' : srv.status === 'connecting' ? 'warning' : 'success'"
|
||||||
srv.status === 'connected'
|
|
||||||
? 'error'
|
|
||||||
: srv.status === 'connecting'
|
|
||||||
? 'warning'
|
|
||||||
: 'success'
|
|
||||||
"
|
|
||||||
:disabled="srv.status === 'connecting'"
|
:disabled="srv.status === 'connecting'"
|
||||||
@click.stop="handleConnect(srv)"
|
@click.stop="handleConnect(srv)"
|
||||||
>
|
>{{ srv.status === 'connected' ? '断开' : srv.status === 'connecting' ? '...' : '连接' }}</n-button>
|
||||||
{{
|
|
||||||
srv.status === 'connected' ? '断开' : srv.status === 'connecting' ? '...' : '连接'
|
|
||||||
}}
|
|
||||||
</n-button>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="sidebar-footer">
|
<div class="sidebar-footer">
|
||||||
<n-button type="primary" block @click="$emit('add-server')"> + 添加服务器 </n-button>
|
<n-button
|
||||||
|
type="primary"
|
||||||
|
block
|
||||||
|
@click="$emit('add-server')"
|
||||||
|
> + 添加服务器</n-button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</n-layout-sider>
|
</n-layout-sider>
|
||||||
</template>
|
</template>
|
||||||
@@ -259,13 +256,16 @@ function handleDelete(server) {
|
|||||||
.status-dot.connected {
|
.status-dot.connected {
|
||||||
background: #18a058;
|
background: #18a058;
|
||||||
}
|
}
|
||||||
|
|
||||||
.status-dot.connecting {
|
.status-dot.connecting {
|
||||||
background: #f0a020;
|
background: #f0a020;
|
||||||
animation: pulse 1s infinite;
|
animation: pulse 1s infinite;
|
||||||
}
|
}
|
||||||
|
|
||||||
.status-dot.disconnected {
|
.status-dot.disconnected {
|
||||||
background: #999;
|
background: #999;
|
||||||
}
|
}
|
||||||
|
|
||||||
.status-dot.error {
|
.status-dot.error {
|
||||||
background: #d03050;
|
background: #d03050;
|
||||||
}
|
}
|
||||||
@@ -275,6 +275,7 @@ function handleDelete(server) {
|
|||||||
100% {
|
100% {
|
||||||
opacity: 1;
|
opacity: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
50% {
|
50% {
|
||||||
opacity: 0.3;
|
opacity: 0.3;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
import { createApp } from 'vue'
|
import { createApp } from 'vue';
|
||||||
import App from './App.vue'
|
|
||||||
|
|
||||||
const app = createApp(App)
|
import App from './App.vue';
|
||||||
app.mount('#app')
|
|
||||||
|
const app = createApp(App);
|
||||||
|
|
||||||
|
app.mount('#app');
|
||||||
|
|||||||
@@ -1,202 +1,249 @@
|
|||||||
import { ref, computed } from 'vue'
|
import { ref, computed } from 'vue';
|
||||||
|
|
||||||
// 全局状态(单例)
|
// 全局状态(单例)
|
||||||
const servers = ref([])
|
const servers = ref([]);
|
||||||
const activeServerId = ref(null)
|
const activeServerId = ref(null);
|
||||||
const messages = ref([])
|
const messages = ref([]);
|
||||||
const publishHistory = ref([])
|
const publishHistory = ref([]);
|
||||||
const loading = ref(false)
|
const loading = ref(false);
|
||||||
|
|
||||||
// 清理函数集合
|
// 清理函数集合
|
||||||
let cleanupFns = []
|
let cleanupFns = [];
|
||||||
|
|
||||||
function setupListeners() {
|
function setupListeners() {
|
||||||
|
|
||||||
// 清理旧监听器
|
// 清理旧监听器
|
||||||
cleanupFns.forEach((fn) => fn())
|
cleanupFns.forEach((fn) => fn());
|
||||||
cleanupFns = []
|
cleanupFns = [];
|
||||||
|
|
||||||
cleanupFns.push(
|
cleanupFns.push(
|
||||||
window.api.onStatusChange((data) => {
|
window.api.onStatusChange((data) => {
|
||||||
const srv = servers.value.find((s) => s.id === data.id)
|
const srv = servers.value.find((s) => s.id === data.id);
|
||||||
if (srv) {
|
if (srv) {
|
||||||
srv.status = data.status
|
srv.status = data.status;
|
||||||
if (data.status === 'disconnected') {
|
if (data.status === 'disconnected') {
|
||||||
srv.topics.forEach((t) => {
|
srv.topics.forEach((t) => {
|
||||||
t.subscribed = false
|
t.subscribed = false;
|
||||||
})
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
)
|
);
|
||||||
|
|
||||||
cleanupFns.push(
|
cleanupFns.push(
|
||||||
window.api.onMessage((data) => {
|
window.api.onMessage((data) => {
|
||||||
if (data.serverId === activeServerId.value) {
|
if (data.serverId === activeServerId.value) {
|
||||||
messages.value.unshift(data.message)
|
messages.value.unshift(data.message);
|
||||||
if (messages.value.length > 500) messages.value.pop()
|
if (messages.value.length > 500) {
|
||||||
|
messages.value.pop();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
)
|
);
|
||||||
|
|
||||||
cleanupFns.push(
|
cleanupFns.push(
|
||||||
window.api.onSubscriptionChange((data) => {
|
window.api.onSubscriptionChange((data) => {
|
||||||
if (data.serverId === activeServerId.value) {
|
if (data.serverId === activeServerId.value) {
|
||||||
const srv = servers.value.find((s) => s.id === data.serverId)
|
const srv = servers.value.find((s) => s.id === data.serverId);
|
||||||
if (srv) {
|
if (srv) {
|
||||||
const t = srv.topics.find((t) => t.topic === data.topic || t.id === data.topicId)
|
const t = srv.topics.find((t) => t.topic === data.topic || t.id === data.topicId);
|
||||||
if (t) {
|
if (t) {
|
||||||
t.subscribed = data.subscribed
|
t.subscribed = data.subscribed;
|
||||||
if (data.topic && data.topic !== t.topic) {
|
if (data.topic && data.topic !== t.topic) {
|
||||||
t.topic = data.topic
|
t.topic = data.topic;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
)
|
);
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function useMqttStore() {
|
export function useMqttStore() {
|
||||||
const activeServer = computed(
|
|
||||||
|
let activeServer = computed(
|
||||||
() => servers.value.find((s) => s.id === activeServerId.value) || null
|
() => servers.value.find((s) => s.id === activeServerId.value) || null
|
||||||
)
|
);
|
||||||
|
|
||||||
const connectedServers = computed(() => servers.value.filter((s) => s.status === 'connected'))
|
let connectedServers = computed(() => servers.value.filter((s) => s.status === 'connected'));
|
||||||
|
|
||||||
const totalTopics = computed(() =>
|
let totalTopics = computed(() =>
|
||||||
servers.value.reduce((sum, s) => sum + (s.topics?.length || 0), 0)
|
servers.value.reduce((sum, s) => sum + (s.topics?.length || 0), 0)
|
||||||
)
|
);
|
||||||
|
|
||||||
const subscribedTopics = computed(() =>
|
let subscribedTopics = computed(() =>
|
||||||
servers.value.reduce((sum, s) => sum + (s.topics?.filter((t) => t.subscribed)?.length || 0), 0)
|
servers.value.reduce(
|
||||||
)
|
(sum, s) => sum + (s.topics?.filter((t) => t.subscribed)?.length || 0),
|
||||||
|
0
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
const totalMessages = computed(() => messages.value.length)
|
let totalMessages = computed(() => messages.value.length);
|
||||||
|
|
||||||
// ==================== 初始化 ====================
|
// ==================== 初始化 ====================
|
||||||
async function init() {
|
async function init() {
|
||||||
loading.value = true
|
|
||||||
|
loading.value = true;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const data = await window.api.getServers()
|
|
||||||
servers.value = data
|
let data = await window.api.getServers();
|
||||||
setupListeners()
|
|
||||||
|
servers.value = data;
|
||||||
|
setupListeners();
|
||||||
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error('初始化失败:', e)
|
console.error('初始化失败:', e);
|
||||||
} finally {
|
} finally {
|
||||||
loading.value = false
|
loading.value = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ==================== 服务器管理 ====================
|
// ==================== 服务器管理 ====================
|
||||||
function setActiveServer(id) {
|
function setActiveServer(id) {
|
||||||
activeServerId.value = id
|
|
||||||
|
activeServerId.value = id;
|
||||||
|
|
||||||
// 切换时刷新消息
|
// 切换时刷新消息
|
||||||
refreshMessages()
|
refreshMessages();
|
||||||
refreshPublishHistory()
|
refreshPublishHistory();
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async function addServer(config) {
|
async function addServer(config) {
|
||||||
const server = await window.api.addServer(config)
|
|
||||||
servers.value.push(server)
|
let server = await window.api.addServer(config);
|
||||||
|
|
||||||
|
servers.value.push(server);
|
||||||
|
|
||||||
if (!activeServerId.value) {
|
if (!activeServerId.value) {
|
||||||
activeServerId.value = server.id
|
activeServerId.value = server.id;
|
||||||
}
|
}
|
||||||
return server
|
|
||||||
|
return server;
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async function updateServer(id, config) {
|
async function updateServer(id, config) {
|
||||||
const result = await window.api.updateServer(id, config)
|
|
||||||
|
let result = await window.api.updateServer(id, config);
|
||||||
|
|
||||||
if (result) {
|
if (result) {
|
||||||
const idx = servers.value.findIndex((s) => s.id === id)
|
let idx = servers.value.findIndex((s) => s.id === id);
|
||||||
if (idx !== -1) {
|
if (idx !== -1) {
|
||||||
servers.value[idx] = { ...servers.value[idx], ...result }
|
servers.value[idx] = { ...servers.value[idx], ...result };
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return result
|
|
||||||
|
return result;
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async function deleteServer(id) {
|
async function deleteServer(id) {
|
||||||
await window.api.deleteServer(id)
|
|
||||||
servers.value = servers.value.filter((s) => s.id !== id)
|
await window.api.deleteServer(id);
|
||||||
|
|
||||||
|
servers.value = servers.value.filter((s) => s.id !== id);
|
||||||
|
|
||||||
if (activeServerId.value === id) {
|
if (activeServerId.value === id) {
|
||||||
activeServerId.value = servers.value.length > 0 ? servers.value[0].id : null
|
activeServerId.value = servers.value.length > 0 ? servers.value[0].id : null;
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async function connectServer(id) {
|
async function connectServer(id) {
|
||||||
return await window.api.connect(id)
|
return await window.api.connect(id);
|
||||||
}
|
}
|
||||||
|
|
||||||
async function disconnectServer(id) {
|
async function disconnectServer(id) {
|
||||||
await window.api.disconnect(id)
|
await window.api.disconnect(id);
|
||||||
}
|
}
|
||||||
|
|
||||||
// ==================== 主题管理 ====================
|
// ==================== 主题管理 ====================
|
||||||
async function addTopic(serverId, topic, qos) {
|
async function addTopic(serverId, topic, qos) {
|
||||||
const result = await window.api.addTopic(serverId, topic, qos)
|
|
||||||
|
let result = await window.api.addTopic(serverId, topic, qos);
|
||||||
|
|
||||||
if (result) {
|
if (result) {
|
||||||
const srv = servers.value.find((s) => s.id === serverId)
|
let srv = servers.value.find((s) => s.id === serverId);
|
||||||
if (srv) {
|
if (srv) {
|
||||||
srv.topics.push({ topic, qos, subscribed: false })
|
srv.topics.push({ topic, qos, subscribed: false });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return result
|
|
||||||
|
return result;
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async function removeTopic(serverId, topic) {
|
async function removeTopic(serverId, topic) {
|
||||||
await window.api.removeTopic(serverId, topic)
|
|
||||||
const srv = servers.value.find((s) => s.id === serverId)
|
await window.api.removeTopic(serverId, topic);
|
||||||
|
|
||||||
|
let srv = servers.value.find((s) => s.id === serverId);
|
||||||
|
|
||||||
if (srv) {
|
if (srv) {
|
||||||
srv.topics = srv.topics.filter((t) => t.topic !== topic)
|
srv.topics = srv.topics.filter((t) => t.topic !== topic);
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async function subscribeTopic(serverId, topic) {
|
async function subscribeTopic(serverId, topic) {
|
||||||
return await window.api.subscribe(serverId, topic)
|
return await window.api.subscribe(serverId, topic);
|
||||||
}
|
}
|
||||||
|
|
||||||
async function unsubscribeTopic(serverId, topic) {
|
async function unsubscribeTopic(serverId, topic) {
|
||||||
return await window.api.unsubscribe(serverId, topic)
|
return await window.api.unsubscribe(serverId, topic);
|
||||||
}
|
}
|
||||||
|
|
||||||
async function subscribeAllTopics(serverId) {
|
async function subscribeAllTopics(serverId) {
|
||||||
return await window.api.subscribeAll(serverId)
|
return await window.api.subscribeAll(serverId);
|
||||||
}
|
}
|
||||||
|
|
||||||
async function updateTopic(serverId, topicId, topic, qos) {
|
async function updateTopic(serverId, topicId, topic, qos) {
|
||||||
return await window.api.updateTopic(serverId, topicId, topic, qos)
|
return await window.api.updateTopic(serverId, topicId, topic, qos);
|
||||||
}
|
}
|
||||||
|
|
||||||
// ==================== 消息发布 ====================
|
// ==================== 消息发布 ====================
|
||||||
async function publishMessage(serverId, topic, payload, opts) {
|
async function publishMessage(serverId, topic, payload, opts) {
|
||||||
const result = await window.api.publish(serverId, topic, payload, opts)
|
|
||||||
|
let result = await window.api.publish(serverId, topic, payload, opts);
|
||||||
|
|
||||||
if (result && serverId === activeServerId.value) {
|
if (result && serverId === activeServerId.value) {
|
||||||
refreshPublishHistory()
|
refreshPublishHistory();
|
||||||
}
|
}
|
||||||
return result
|
|
||||||
|
return result;
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ==================== 消息管理 ====================
|
// ==================== 消息管理 ====================
|
||||||
|
|
||||||
async function refreshMessages() {
|
async function refreshMessages() {
|
||||||
if (!activeServerId.value) return
|
if (activeServerId.value) {
|
||||||
messages.value = await window.api.getMessages(activeServerId.value)
|
messages.value = await window.api.getMessages(activeServerId.value);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function refreshPublishHistory() {
|
async function refreshPublishHistory() {
|
||||||
if (!activeServerId.value) return
|
if (activeServerId.value) {
|
||||||
publishHistory.value = await window.api.getPublishHistory(activeServerId.value)
|
publishHistory.value = await window.api.getPublishHistory(activeServerId.value);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function clearMessages() {
|
async function clearMessages() {
|
||||||
if (!activeServerId.value) return
|
if (activeServerId.value) {
|
||||||
await window.api.clearMessages(activeServerId.value)
|
await window.api.clearMessages(activeServerId.value);
|
||||||
messages.value = []
|
messages.value = [];
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function clearPublishHistory() {
|
async function clearPublishHistory() {
|
||||||
if (!activeServerId.value) return
|
if (activeServerId.value) {
|
||||||
await window.api.clearPublishHistory(activeServerId.value)
|
await window.api.clearPublishHistory(activeServerId.value);
|
||||||
publishHistory.value = []
|
publishHistory.value = [];
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
@@ -230,6 +277,7 @@ export function useMqttStore() {
|
|||||||
refreshMessages,
|
refreshMessages,
|
||||||
refreshPublishHistory,
|
refreshPublishHistory,
|
||||||
clearMessages,
|
clearMessages,
|
||||||
clearPublishHistory
|
clearPublishHistory,
|
||||||
}
|
};
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,60 +1,67 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import { useMqttStore } from '../stores/mqtt.js'
|
import { useMqttStore } from '../stores/mqtt.js';
|
||||||
import {
|
import { NGrid, NGridItem, NCard, NStatistic, NTag, NDescriptions, NDescriptionsItem, NSpace, NButton, NList, NListItem, useMessage } from 'naive-ui';
|
||||||
NGrid,
|
|
||||||
NGridItem,
|
|
||||||
NCard,
|
|
||||||
NStatistic,
|
|
||||||
NTag,
|
|
||||||
NDescriptions,
|
|
||||||
NDescriptionsItem,
|
|
||||||
NSpace,
|
|
||||||
NButton,
|
|
||||||
NList,
|
|
||||||
NListItem
|
|
||||||
} from 'naive-ui'
|
|
||||||
import { useMessage } from 'naive-ui'
|
|
||||||
|
|
||||||
const store = useMqttStore()
|
const store = useMqttStore();
|
||||||
const msg = useMessage()
|
const msg = useMessage();
|
||||||
|
|
||||||
function handleAction(srv, action) {
|
function handleAction(srv, action) {
|
||||||
if (srv.id === store.activeServerId.value) {
|
if (srv.id === store.activeServerId.value) {
|
||||||
emit('switch-tab', action)
|
emit('switch-tab', action);
|
||||||
} else {
|
} else {
|
||||||
store.setActiveServer(srv.id)
|
store.setActiveServer(srv.id);
|
||||||
emit('switch-tab', action)
|
emit('switch-tab', action);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function handleToggleConnection(srv) {
|
async function handleToggleConnection(srv) {
|
||||||
if (srv.status === 'connected') {
|
if (srv.status === 'connected') {
|
||||||
await store.disconnectServer(srv.id)
|
await store.disconnectServer(srv.id);
|
||||||
msg.info('已断开连接: ' + srv.name)
|
msg.info('已断开连接: ' + srv.name);
|
||||||
} else if (srv.status === 'disconnected' || srv.status === 'error') {
|
} else if (srv.status === 'disconnected' || srv.status === 'error') {
|
||||||
await store.connectServer(srv.id)
|
await store.connectServer(srv.id);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const emit = defineEmits(['switch-tab', 'edit-server'])
|
const emit = defineEmits(['switch-tab', 'edit-server']);
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="dashboard">
|
<div class="dashboard">
|
||||||
|
|
||||||
<!-- 统计卡片 -->
|
<!-- 统计卡片 -->
|
||||||
<n-grid :cols="4" :x-gap="12" responsive="screen">
|
<n-grid
|
||||||
|
:cols="4"
|
||||||
|
:x-gap="12"
|
||||||
|
responsive="screen"
|
||||||
|
>
|
||||||
<n-grid-item>
|
<n-grid-item>
|
||||||
<n-card size="small" :bordered="true">
|
<n-card
|
||||||
<n-statistic label="服务器总数" :value="store.servers.value.length" />
|
size="small"
|
||||||
|
:bordered="true"
|
||||||
|
>
|
||||||
|
<n-statistic
|
||||||
|
label="服务器总数"
|
||||||
|
:value="store.servers.value.length"
|
||||||
|
/>
|
||||||
</n-card>
|
</n-card>
|
||||||
</n-grid-item>
|
</n-grid-item>
|
||||||
<n-grid-item>
|
<n-grid-item>
|
||||||
<n-card size="small" :bordered="true">
|
<n-card
|
||||||
<n-statistic label="已连接" :value="store.connectedServers.value.length" />
|
size="small"
|
||||||
|
:bordered="true"
|
||||||
|
>
|
||||||
|
<n-statistic
|
||||||
|
label="已连接"
|
||||||
|
:value="store.connectedServers.value.length"
|
||||||
|
/>
|
||||||
</n-card>
|
</n-card>
|
||||||
</n-grid-item>
|
</n-grid-item>
|
||||||
<n-grid-item>
|
<n-grid-item>
|
||||||
<n-card size="small" :bordered="true">
|
<n-card
|
||||||
|
size="small"
|
||||||
|
:bordered="true"
|
||||||
|
>
|
||||||
<n-statistic
|
<n-statistic
|
||||||
label="已订阅主题"
|
label="已订阅主题"
|
||||||
:value="`${store.subscribedTopics.value}/${store.totalTopics.value}`"
|
:value="`${store.subscribedTopics.value}/${store.totalTopics.value}`"
|
||||||
@@ -62,8 +69,14 @@ const emit = defineEmits(['switch-tab', 'edit-server'])
|
|||||||
</n-card>
|
</n-card>
|
||||||
</n-grid-item>
|
</n-grid-item>
|
||||||
<n-grid-item>
|
<n-grid-item>
|
||||||
<n-card size="small" :bordered="true">
|
<n-card
|
||||||
<n-statistic label="消息总数" :value="store.messages.value.length" />
|
size="small"
|
||||||
|
:bordered="true"
|
||||||
|
>
|
||||||
|
<n-statistic
|
||||||
|
label="消息总数"
|
||||||
|
:value="store.messages.value.length"
|
||||||
|
/>
|
||||||
</n-card>
|
</n-card>
|
||||||
</n-grid-item>
|
</n-grid-item>
|
||||||
</n-grid>
|
</n-grid>
|
||||||
@@ -77,29 +90,17 @@ const emit = defineEmits(['switch-tab', 'edit-server'])
|
|||||||
>
|
>
|
||||||
<template #header-extra>
|
<template #header-extra>
|
||||||
<n-tag
|
<n-tag
|
||||||
:type="
|
:type="store.activeServer.value.status === 'connected' ? 'success' : store.activeServer.value.status === 'connecting' ? 'warning' : 'error'"
|
||||||
store.activeServer.value.status === 'connected'
|
|
||||||
? 'success'
|
|
||||||
: store.activeServer.value.status === 'connecting'
|
|
||||||
? 'warning'
|
|
||||||
: 'error'
|
|
||||||
"
|
|
||||||
size="small"
|
size="small"
|
||||||
>
|
>{{ store.activeServer.value.status === 'connected' ? '已连接' : store.activeServer.value.status === 'connecting' ? '连接中...' : '未连接' }}</n-tag>
|
||||||
{{
|
|
||||||
store.activeServer.value.status === 'connected'
|
|
||||||
? '已连接'
|
|
||||||
: store.activeServer.value.status === 'connecting'
|
|
||||||
? '连接中...'
|
|
||||||
: '未连接'
|
|
||||||
}}
|
|
||||||
</n-tag>
|
|
||||||
</template>
|
</template>
|
||||||
<n-descriptions :column="2" size="small" bordered>
|
<n-descriptions
|
||||||
|
:column="2"
|
||||||
|
size="small"
|
||||||
|
bordered
|
||||||
|
>
|
||||||
<n-descriptions-item label="地址">
|
<n-descriptions-item label="地址">
|
||||||
{{ store.activeServer.value.protocol }}://{{ store.activeServer.value.host }}:{{
|
{{ store.activeServer.value.protocol }}://{{ store.activeServer.value.host }}:{{ store.activeServer.value.port }}
|
||||||
store.activeServer.value.port
|
|
||||||
}}
|
|
||||||
</n-descriptions-item>
|
</n-descriptions-item>
|
||||||
<n-descriptions-item label="客户端 ID">
|
<n-descriptions-item label="客户端 ID">
|
||||||
{{ store.activeServer.value.clientId }}
|
{{ store.activeServer.value.clientId }}
|
||||||
@@ -116,52 +117,50 @@ const emit = defineEmits(['switch-tab', 'edit-server'])
|
|||||||
size="small"
|
size="small"
|
||||||
:type="store.activeServer.value.status === 'connected' ? 'error' : 'success'"
|
:type="store.activeServer.value.status === 'connected' ? 'error' : 'success'"
|
||||||
@click="handleToggleConnection(store.activeServer.value)"
|
@click="handleToggleConnection(store.activeServer.value)"
|
||||||
>
|
>{{ store.activeServer.value.status === 'connected' ? '断开连接' : '连接服务器' }}</n-button>
|
||||||
{{ store.activeServer.value.status === 'connected' ? '断开连接' : '连接服务器' }}
|
<n-button
|
||||||
</n-button>
|
size="small"
|
||||||
<n-button size="small" @click="$emit('edit-server', store.activeServer.value)"
|
@click="$emit('edit-server', store.activeServer.value)"
|
||||||
>编辑配置</n-button
|
>编辑配置</n-button>
|
||||||
>
|
<n-button
|
||||||
<n-button size="small" @click="handleAction(store.activeServer.value, 'subscribe')"
|
size="small"
|
||||||
>管理订阅</n-button
|
@click="handleAction(store.activeServer.value, 'subscribe')"
|
||||||
>
|
>管理订阅</n-button>
|
||||||
<n-button size="small" @click="handleAction(store.activeServer.value, 'publish')"
|
<n-button
|
||||||
>发布消息</n-button
|
size="small"
|
||||||
>
|
@click="handleAction(store.activeServer.value, 'publish')"
|
||||||
<n-button size="small" @click="handleAction(store.activeServer.value, 'messages')"
|
>发布消息</n-button>
|
||||||
>查看消息</n-button
|
<n-button
|
||||||
>
|
size="small"
|
||||||
|
@click="handleAction(store.activeServer.value, 'messages')"
|
||||||
|
>查看消息</n-button>
|
||||||
</n-space>
|
</n-space>
|
||||||
</n-card>
|
</n-card>
|
||||||
|
|
||||||
<!-- 所有服务器状态 -->
|
<!-- 所有服务器状态 -->
|
||||||
<n-card title="所有服务器状态" size="small" style="margin-top: 12px">
|
<n-card
|
||||||
|
title="所有服务器状态"
|
||||||
|
size="small"
|
||||||
|
style="margin-top: 12px"
|
||||||
|
>
|
||||||
<n-list>
|
<n-list>
|
||||||
<n-list-item v-for="srv in store.servers.value" :key="srv.id">
|
<n-list-item
|
||||||
|
v-for="srv in store.servers.value"
|
||||||
|
:key="srv.id"
|
||||||
|
>
|
||||||
<template #prefix>
|
<template #prefix>
|
||||||
<span class="status-dot" :class="srv.status" />
|
<span
|
||||||
|
class="status-dot"
|
||||||
|
:class="srv.status"
|
||||||
|
/>
|
||||||
</template>
|
</template>
|
||||||
<n-space align="center">
|
<n-space align="center">
|
||||||
<span style="font-weight: 500">{{ srv.name }}</span>
|
<span style="font-weight: 500">{{ srv.name }}</span>
|
||||||
<n-tag
|
<n-tag
|
||||||
:bordered="false"
|
:bordered="false"
|
||||||
size="small"
|
size="small"
|
||||||
:type="
|
:type="srv.status === 'connected' ? 'success' : srv.status === 'connecting' ? 'warning' : 'default'"
|
||||||
srv.status === 'connected'
|
>{{ srv.status === 'connected' ? '已连接' : srv.status === 'connecting' ? '连接中...' : '未连接' }}</n-tag>
|
||||||
? 'success'
|
|
||||||
: srv.status === 'connecting'
|
|
||||||
? 'warning'
|
|
||||||
: 'default'
|
|
||||||
"
|
|
||||||
>
|
|
||||||
{{
|
|
||||||
srv.status === 'connected'
|
|
||||||
? '已连接'
|
|
||||||
: srv.status === 'connecting'
|
|
||||||
? '连接中...'
|
|
||||||
: '未连接'
|
|
||||||
}}
|
|
||||||
</n-tag>
|
|
||||||
</n-space>
|
</n-space>
|
||||||
<template #suffix>
|
<template #suffix>
|
||||||
<n-button
|
<n-button
|
||||||
@@ -169,15 +168,12 @@ const emit = defineEmits(['switch-tab', 'edit-server'])
|
|||||||
:type="srv.status === 'connected' ? 'error' : 'success'"
|
:type="srv.status === 'connected' ? 'error' : 'success'"
|
||||||
:disabled="srv.status === 'connecting'"
|
:disabled="srv.status === 'connecting'"
|
||||||
@click="handleToggleConnection(srv)"
|
@click="handleToggleConnection(srv)"
|
||||||
>
|
>{{ srv.status === 'connected' ? '断开' : srv.status === 'connecting' ? '...' : '连接' }}</n-button>
|
||||||
{{
|
|
||||||
srv.status === 'connected' ? '断开' : srv.status === 'connecting' ? '...' : '连接'
|
|
||||||
}}
|
|
||||||
</n-button>
|
|
||||||
</template>
|
</template>
|
||||||
</n-list-item>
|
</n-list-item>
|
||||||
</n-list>
|
</n-list>
|
||||||
</n-card>
|
</n-card>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@@ -196,13 +192,16 @@ const emit = defineEmits(['switch-tab', 'edit-server'])
|
|||||||
.status-dot.connected {
|
.status-dot.connected {
|
||||||
background: #18a058;
|
background: #18a058;
|
||||||
}
|
}
|
||||||
|
|
||||||
.status-dot.connecting {
|
.status-dot.connecting {
|
||||||
background: #f0a020;
|
background: #f0a020;
|
||||||
animation: pulse 1s infinite;
|
animation: pulse 1s infinite;
|
||||||
}
|
}
|
||||||
|
|
||||||
.status-dot.disconnected {
|
.status-dot.disconnected {
|
||||||
background: #999;
|
background: #999;
|
||||||
}
|
}
|
||||||
|
|
||||||
.status-dot.error {
|
.status-dot.error {
|
||||||
background: #d03050;
|
background: #d03050;
|
||||||
}
|
}
|
||||||
@@ -212,6 +211,7 @@ const emit = defineEmits(['switch-tab', 'edit-server'])
|
|||||||
100% {
|
100% {
|
||||||
opacity: 1;
|
opacity: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
50% {
|
50% {
|
||||||
opacity: 0.3;
|
opacity: 0.3;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,59 +1,76 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import { ref, computed } from 'vue'
|
import { ref, computed } from 'vue';
|
||||||
import { useMqttStore } from '../stores/mqtt.js'
|
import { useMqttStore } from '../stores/mqtt.js';
|
||||||
import { NCard, NSpace, NInput, NButton, NEmpty, useMessage } from 'naive-ui'
|
import { NCard, NSpace, NInput, NButton, NEmpty, useMessage } from 'naive-ui';
|
||||||
|
|
||||||
const store = useMqttStore()
|
const store = useMqttStore();
|
||||||
const message = useMessage()
|
const message = useMessage();
|
||||||
|
|
||||||
const filter = ref('')
|
const filter = ref('');
|
||||||
|
|
||||||
async function copyTopic(topic) {
|
async function copyTopic(topic) {
|
||||||
try {
|
try {
|
||||||
await navigator.clipboard.writeText(topic)
|
await navigator.clipboard.writeText(topic);
|
||||||
message.success('主题已复制')
|
message.success('主题已复制');
|
||||||
} catch {
|
} catch {
|
||||||
message.error('复制失败')
|
message.error('复制失败');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const filteredMessages = computed(() => {
|
const filteredMessages = computed(() => {
|
||||||
if (!filter.value.trim()) return store.messages.value
|
|
||||||
const f = filter.value.trim().toLowerCase()
|
if (!filter.value.trim()) {
|
||||||
return store.messages.value.filter((m) => m.topic.toLowerCase().includes(f))
|
return store.messages.value;
|
||||||
|
}
|
||||||
|
|
||||||
|
let f = filter.value.trim().toLowerCase();
|
||||||
|
|
||||||
|
return store.messages.value.filter((m) => {
|
||||||
|
return m.topic.toLowerCase().includes(f);
|
||||||
|
});
|
||||||
|
|
||||||
})
|
})
|
||||||
|
|
||||||
async function handleClear() {
|
async function handleClear() {
|
||||||
await store.clearMessages()
|
await store.clearMessages();
|
||||||
}
|
}
|
||||||
|
|
||||||
function formatPayload(payload) {
|
function formatPayload(payload) {
|
||||||
try {
|
try {
|
||||||
return JSON.stringify(JSON.parse(payload), null, 2)
|
return JSON.stringify(JSON.parse(payload), null, 2);
|
||||||
} catch {
|
} catch {
|
||||||
return payload
|
return payload;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div v-if="store.activeServer.value" class="messages-view">
|
|
||||||
|
<!-- 内容区域 -->
|
||||||
|
<div
|
||||||
|
v-if="store.activeServer.value"
|
||||||
|
class="messages-view"
|
||||||
|
>
|
||||||
<n-card
|
<n-card
|
||||||
size="small"
|
size="small"
|
||||||
:content-style="{
|
:content-style="{
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
flexDirection: 'column',
|
flexDirection: 'column',
|
||||||
minHeight: 0,
|
minHeight: 0,
|
||||||
overflow: 'hidden'
|
overflow: 'hidden',
|
||||||
}"
|
}"
|
||||||
>
|
>
|
||||||
<template #header>
|
<template #header>
|
||||||
<n-space align="center" justify="space-between" style="width: 100%">
|
<n-space
|
||||||
|
align="center"
|
||||||
|
justify="space-between"
|
||||||
|
style="width: 100%"
|
||||||
|
>
|
||||||
<div>
|
<div>
|
||||||
<span>消息监控</span>
|
<span>消息监控</span>
|
||||||
<span style="font-size: 12px; opacity: 0.55; margin-left: 8px"
|
<span
|
||||||
>实时查看接收和发送的 MQTT 消息</span
|
style="font-size: 12px; opacity: 0.55; margin-left: 8px"
|
||||||
>
|
>实时查看接收和发送的 MQTT 消息</span>
|
||||||
</div>
|
</div>
|
||||||
<n-space>
|
<n-space>
|
||||||
<n-input
|
<n-input
|
||||||
@@ -63,31 +80,60 @@ function formatPayload(payload) {
|
|||||||
style="width: 180px"
|
style="width: 180px"
|
||||||
clearable
|
clearable
|
||||||
/>
|
/>
|
||||||
<n-button size="small" @click="handleClear">清空</n-button>
|
<n-button
|
||||||
|
size="small"
|
||||||
|
@click="handleClear"
|
||||||
|
>清空消息</n-button>
|
||||||
</n-space>
|
</n-space>
|
||||||
</n-space>
|
</n-space>
|
||||||
</template>
|
</template>
|
||||||
<n-empty v-if="filteredMessages.length === 0" description="等待消息...">
|
<n-empty
|
||||||
|
v-if="filteredMessages.length === 0"
|
||||||
|
description="等待消息..."
|
||||||
|
>
|
||||||
<template #icon>
|
<template #icon>
|
||||||
<span style="font-size: 32px; opacity: 0.4">☰</span>
|
<span style="font-size: 32px; opacity: 0.4">☰</span>
|
||||||
</template>
|
</template>
|
||||||
</n-empty>
|
</n-empty>
|
||||||
<div v-else ref="logRef" class="msg-log">
|
<div
|
||||||
<div v-for="m in filteredMessages" :key="m.id" class="msg-entry">
|
v-else
|
||||||
|
ref="logRef"
|
||||||
|
class="msg-log"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
v-for="m in filteredMessages"
|
||||||
|
:key="m.id"
|
||||||
|
class="msg-entry"
|
||||||
|
>
|
||||||
<div class="msg-header">
|
<div class="msg-header">
|
||||||
<span class="msg-time">{{ m.time }}</span>
|
<span class="msg-time">{{ m.time }}</span>
|
||||||
<span class="msg-dir" :class="m.direction">{{
|
<span
|
||||||
m.direction === 'pub' ? 'PUB' : 'SUB'
|
class="msg-dir"
|
||||||
}}</span>
|
:class="m.direction"
|
||||||
<span class="msg-topic" :title="m.topic" @click="copyTopic(m.topic)">{{ m.topic }}</span>
|
>{{ m.direction === 'pub' ? 'PUB' : 'SUB' }}</span>
|
||||||
<span class="msg-meta">QoS{{ m.qos }}{{ m.retain ? ' R' : '' }}</span>
|
<span
|
||||||
|
class="msg-topic"
|
||||||
|
:title="m.topic"
|
||||||
|
@click="copyTopic(m.topic)"
|
||||||
|
>{{ m.topic }}</span>
|
||||||
|
<span
|
||||||
|
class="msg-meta"
|
||||||
|
>QoS{{ m.qos }}{{ m.retain ? ' R' : '' }}</span>
|
||||||
</div>
|
</div>
|
||||||
<pre class="msg-payload">{{ formatPayload(m.payload) }}</pre>
|
<pre
|
||||||
|
class="msg-payload"
|
||||||
|
>{{ formatPayload(m.payload) }}</pre>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</n-card>
|
</n-card>
|
||||||
</div>
|
</div>
|
||||||
<n-empty v-else description="请先在左侧选择一个 MQTT 服务器" />
|
|
||||||
|
<!-- 空状态 -->
|
||||||
|
<n-empty
|
||||||
|
v-else
|
||||||
|
description="请先在左侧选择一个 MQTT 服务器"
|
||||||
|
/>
|
||||||
|
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
|
|||||||
@@ -1,191 +1,278 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import { ref, watch } from 'vue'
|
import { ref, watch } from 'vue';
|
||||||
import { useMqttStore } from '../stores/mqtt.js'
|
import { useMqttStore } from '../stores/mqtt.js';
|
||||||
import {
|
import { NCard, NSpace, NFormItem, NInput, NSelect, NSwitch, NButton, NEmpty, NTabs, NTabPane, useMessage } from 'naive-ui';
|
||||||
NCard,
|
|
||||||
NSpace,
|
|
||||||
NFormItem,
|
|
||||||
NInput,
|
|
||||||
NSelect,
|
|
||||||
NSwitch,
|
|
||||||
NButton,
|
|
||||||
NEmpty,
|
|
||||||
NTabs,
|
|
||||||
NTabPane,
|
|
||||||
useMessage
|
|
||||||
} from 'naive-ui'
|
|
||||||
|
|
||||||
const store = useMqttStore()
|
const store = useMqttStore();
|
||||||
const msg = useMessage()
|
const msg = useMessage();
|
||||||
|
|
||||||
const topic = ref('')
|
const topic = ref('');
|
||||||
const payload = ref('')
|
const payload = ref('');
|
||||||
const qos = ref(1)
|
const qos = ref(1);
|
||||||
const retain = ref(false)
|
const retain = ref(false);
|
||||||
const activePublishTab = ref('publish')
|
const activePublishTab = ref('publish');
|
||||||
const formStorageKey = 'mqtt-client.publish-form'
|
const formStorageKey = 'mqtt-client.publish-form';
|
||||||
const publishTabStorageKey = 'mqtt-client.publish-active-tab'
|
const publishTabStorageKey = 'mqtt-client.publish-active-tab';
|
||||||
|
|
||||||
let formReady = false
|
let formReady = false;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const savedForm = JSON.parse(localStorage.getItem(formStorageKey) || '{}')
|
|
||||||
if (typeof savedForm.topic === 'string') topic.value = savedForm.topic
|
let savedForm = JSON.parse(localStorage.getItem(formStorageKey) || '{}');
|
||||||
if (typeof savedForm.payload === 'string') payload.value = savedForm.payload
|
|
||||||
if ([0, 1, 2].includes(savedForm.qos)) qos.value = savedForm.qos
|
if (typeof savedForm.topic === 'string') {
|
||||||
if (typeof savedForm.retain === 'boolean') retain.value = savedForm.retain
|
topic.value = savedForm.topic;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof savedForm.payload === 'string') {
|
||||||
|
payload.value = savedForm.payload;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ([0, 1, 2].includes(savedForm.qos)) {
|
||||||
|
qos.value = savedForm.qos;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof savedForm.retain === 'boolean') {
|
||||||
|
retain.value = savedForm.retain;
|
||||||
|
}
|
||||||
|
|
||||||
} catch {
|
} catch {
|
||||||
localStorage.removeItem(formStorageKey)
|
localStorage.removeItem(formStorageKey);
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const savedTab = localStorage.getItem(publishTabStorageKey)
|
|
||||||
if (savedTab === 'publish' || savedTab === 'history') activePublishTab.value = savedTab
|
let savedTab = localStorage.getItem(publishTabStorageKey);
|
||||||
|
|
||||||
|
if (savedTab === 'publish' || savedTab === 'history') {
|
||||||
|
activePublishTab.value = savedTab;
|
||||||
|
}
|
||||||
|
|
||||||
} catch {
|
} catch {
|
||||||
localStorage.removeItem(publishTabStorageKey)
|
localStorage.removeItem(publishTabStorageKey);
|
||||||
}
|
}
|
||||||
|
|
||||||
watch([topic, payload, qos, retain], () => {
|
watch([topic, payload, qos, retain], () => {
|
||||||
if (!formReady) return
|
|
||||||
|
if (!formReady) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
localStorage.setItem(
|
localStorage.setItem(
|
||||||
formStorageKey,
|
formStorageKey,
|
||||||
JSON.stringify({
|
JSON.stringify({
|
||||||
topic: topic.value,
|
topic: topic.value,
|
||||||
payload: payload.value,
|
payload: payload.value,
|
||||||
qos: qos.value,
|
qos: qos.value,
|
||||||
retain: retain.value
|
retain: retain.value,
|
||||||
})
|
})
|
||||||
)
|
);
|
||||||
})
|
|
||||||
|
});
|
||||||
|
|
||||||
watch(activePublishTab, (tab) => {
|
watch(activePublishTab, (tab) => {
|
||||||
localStorage.setItem(publishTabStorageKey, tab)
|
localStorage.setItem(publishTabStorageKey, tab);
|
||||||
})
|
});
|
||||||
|
|
||||||
formReady = true
|
formReady = true;
|
||||||
|
|
||||||
const qosOptions = [
|
const qosOptions = [
|
||||||
{ label: 'QoS 0 - 最多一次', value: 0 },
|
{ label: 'QoS 0 - 最多一次', value: 0 },
|
||||||
{ label: 'QoS 1 - 至少一次', value: 1 },
|
{ label: 'QoS 1 - 至少一次', value: 1 },
|
||||||
{ label: 'QoS 2 - 恰好一次', value: 2 }
|
{ label: 'QoS 2 - 恰好一次', value: 2 },
|
||||||
]
|
];
|
||||||
|
|
||||||
async function handlePublish() {
|
async function handlePublish() {
|
||||||
const srv = store.activeServer.value
|
|
||||||
if (!srv) return
|
let srv = store.activeServer.value;
|
||||||
|
|
||||||
|
if (!srv) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (srv.status !== 'connected') {
|
if (srv.status !== 'connected') {
|
||||||
msg.warning('请先连接服务器')
|
msg.warning('请先连接服务器');
|
||||||
return
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!topic.value.trim()) {
|
if (!topic.value.trim()) {
|
||||||
msg.warning('请输入目标主题')
|
msg.warning('请输入目标主题');
|
||||||
return
|
return;
|
||||||
}
|
}
|
||||||
const result = await store.publishMessage(srv.id, topic.value.trim(), payload.value, {
|
|
||||||
|
let result = await store.publishMessage(srv.id, topic.value.trim(), payload.value, {
|
||||||
qos: qos.value,
|
qos: qos.value,
|
||||||
retain: retain.value
|
retain: retain.value,
|
||||||
})
|
});
|
||||||
|
|
||||||
if (result) {
|
if (result) {
|
||||||
msg.success('消息已发布到: ' + topic.value)
|
msg.success('消息已发布到: ' + topic.value);
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleFormatJson() {
|
function handleFormatJson() {
|
||||||
try {
|
try {
|
||||||
const obj = JSON.parse(payload.value)
|
|
||||||
payload.value = JSON.stringify(obj, null, 2)
|
let obj = JSON.parse(payload.value);
|
||||||
msg.info('JSON 已格式化')
|
|
||||||
|
payload.value = JSON.stringify(obj, null, 2);
|
||||||
|
msg.info('JSON 已格式化');
|
||||||
|
|
||||||
} catch {
|
} catch {
|
||||||
msg.warning('无效的 JSON 格式')
|
msg.warning('无效的 JSON 格式');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function handleClearHistory() {
|
async function handleClearHistory() {
|
||||||
await store.clearPublishHistory()
|
await store.clearPublishHistory();
|
||||||
}
|
}
|
||||||
|
|
||||||
async function copyTopic(topic) {
|
async function copyTopic(topic) {
|
||||||
try {
|
try {
|
||||||
await navigator.clipboard.writeText(topic)
|
await navigator.clipboard.writeText(topic);
|
||||||
msg.success('主题已复制')
|
msg.success('主题已复制');
|
||||||
} catch {
|
} catch {
|
||||||
msg.error('复制失败')
|
msg.error('复制失败');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div v-if="store.activeServer.value" class="publish-view">
|
|
||||||
<n-tabs v-model:value="activePublishTab" type="line" size="small">
|
<!-- 内容区域 -->
|
||||||
<n-tab-pane name="publish" tab="发布消息">
|
<div
|
||||||
|
v-if="store.activeServer.value"
|
||||||
|
class="publish-view"
|
||||||
|
>
|
||||||
|
<n-tabs
|
||||||
|
v-model:value="activePublishTab"
|
||||||
|
type="line"
|
||||||
|
size="small"
|
||||||
|
>
|
||||||
|
<n-tab-pane
|
||||||
|
name="publish"
|
||||||
|
tab="发布消息"
|
||||||
|
>
|
||||||
<n-card
|
<n-card
|
||||||
size="small"
|
size="small"
|
||||||
:content-style="{
|
:content-style="{
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
flexDirection: 'column',
|
flexDirection: 'column',
|
||||||
minHeight: 0,
|
minHeight: 0,
|
||||||
overflow: 'hidden'
|
overflow: 'hidden',
|
||||||
}"
|
}"
|
||||||
>
|
>
|
||||||
<div class="publish-form">
|
<div class="publish-form">
|
||||||
<n-form-item class="topic-field" label="目标主题 (Topic)">
|
<n-form-item
|
||||||
<n-input v-model:value="topic" placeholder="例如: sensor/device01/temperature" />
|
class="topic-field"
|
||||||
|
label="目标主题 (Topic)"
|
||||||
|
>
|
||||||
|
<n-input
|
||||||
|
v-model:value="topic"
|
||||||
|
placeholder="例如: sensor/device01/temperature"
|
||||||
|
/>
|
||||||
</n-form-item>
|
</n-form-item>
|
||||||
<n-space align="center">
|
<n-space align="center">
|
||||||
<n-form-item label="QoS">
|
<n-form-item label="QoS">
|
||||||
<n-select v-model:value="qos" :options="qosOptions" style="width: 160px" />
|
<n-select
|
||||||
|
v-model:value="qos"
|
||||||
|
:options="qosOptions"
|
||||||
|
style="width: 160px"
|
||||||
|
/>
|
||||||
</n-form-item>
|
</n-form-item>
|
||||||
<n-form-item label="保留消息">
|
<n-form-item label="保留消息">
|
||||||
<n-switch v-model:value="retain" />
|
<n-switch v-model:value="retain" />
|
||||||
</n-form-item>
|
</n-form-item>
|
||||||
</n-space>
|
</n-space>
|
||||||
<n-form-item class="payload-field" label="消息内容 (Payload)">
|
<n-form-item
|
||||||
|
class="payload-field"
|
||||||
|
label="消息内容 (Payload)"
|
||||||
|
>
|
||||||
<n-input
|
<n-input
|
||||||
v-model:value="payload"
|
v-model:value="payload"
|
||||||
type="textarea"
|
type="textarea"
|
||||||
:resizable="false"
|
:resizable="false"
|
||||||
:rows="1"
|
:rows="1"
|
||||||
placeholder='{"temperature": 25.6, "humidity": 68.3}'
|
|
||||||
/>
|
/>
|
||||||
</n-form-item>
|
</n-form-item>
|
||||||
<n-space>
|
<n-space>
|
||||||
<n-button type="primary" @click="handlePublish">发布消息</n-button>
|
<n-button
|
||||||
<n-button @click="handleFormatJson">格式化 JSON</n-button>
|
type="primary"
|
||||||
|
@click="handlePublish"
|
||||||
|
>发布消息</n-button>
|
||||||
|
<n-button
|
||||||
|
@click="handleFormatJson"
|
||||||
|
>格式化 JSON</n-button>
|
||||||
</n-space>
|
</n-space>
|
||||||
</div>
|
</div>
|
||||||
</n-card>
|
</n-card>
|
||||||
</n-tab-pane>
|
</n-tab-pane>
|
||||||
|
|
||||||
<n-tab-pane name="history" tab="发布历史">
|
<n-tab-pane
|
||||||
|
name="history"
|
||||||
|
tab="发布历史"
|
||||||
|
>
|
||||||
<n-card
|
<n-card
|
||||||
size="small"
|
size="small"
|
||||||
:content-style="{
|
:content-style="{
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
flexDirection: 'column',
|
flexDirection: 'column',
|
||||||
minHeight: 0,
|
minHeight: 0,
|
||||||
overflow: 'hidden'
|
overflow: 'hidden',
|
||||||
}"
|
}"
|
||||||
>
|
>
|
||||||
<n-space style="margin-bottom: 12px">
|
<n-space style="margin-bottom: 12px">
|
||||||
<n-button size="small" @click="handleClearHistory">清空</n-button>
|
<n-button
|
||||||
|
size="small"
|
||||||
|
@click="handleClearHistory"
|
||||||
|
>清空历史</n-button>
|
||||||
</n-space>
|
</n-space>
|
||||||
<n-empty v-if="store.publishHistory.value.length === 0" description="暂无发布记录" />
|
<n-empty
|
||||||
<div v-else class="msg-log">
|
v-if="store.publishHistory.value.length === 0"
|
||||||
<div v-for="m in store.publishHistory.value" :key="m.id || m.time" class="msg-entry">
|
description="暂无发布记录"
|
||||||
|
/>
|
||||||
|
<div
|
||||||
|
v-else
|
||||||
|
class="msg-log"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
v-for="m in store.publishHistory.value"
|
||||||
|
:key="m.id || m.time"
|
||||||
|
class="msg-entry"
|
||||||
|
>
|
||||||
<div class="msg-header">
|
<div class="msg-header">
|
||||||
<span class="msg-time">{{ m.time }}</span>
|
<span
|
||||||
<span class="msg-dir pub">PUB</span>
|
class="msg-time"
|
||||||
<span class="msg-topic" :title="m.topic" @click="copyTopic(m.topic)">{{ m.topic }}</span>
|
>{{ m.time }}</span>
|
||||||
<span class="msg-meta">QoS{{ m.qos }}{{ m.retain ? ' R' : '' }}</span>
|
<span
|
||||||
|
class="msg-dir pub"
|
||||||
|
>PUB</span>
|
||||||
|
<span
|
||||||
|
class="msg-topic"
|
||||||
|
:title="m.topic"
|
||||||
|
@click="copyTopic(m.topic)"
|
||||||
|
>{{ m.topic }}</span>
|
||||||
|
<span
|
||||||
|
class="msg-meta"
|
||||||
|
>QoS{{ m.qos }}{{ m.retain ? ' R' : '' }}</span>
|
||||||
</div>
|
</div>
|
||||||
<pre class="msg-payload">{{ m.payload }}</pre>
|
<pre
|
||||||
|
class="msg-payload"
|
||||||
|
>{{ m.payload }}</pre>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</n-card>
|
</n-card>
|
||||||
</n-tab-pane>
|
</n-tab-pane>
|
||||||
</n-tabs>
|
</n-tabs>
|
||||||
</div>
|
</div>
|
||||||
<n-empty v-else description="请先在左侧选择一个 MQTT 服务器" />
|
|
||||||
|
<!-- 空状态 -->
|
||||||
|
<n-empty
|
||||||
|
v-else
|
||||||
|
description="请先在左侧选择一个 MQTT 服务器"
|
||||||
|
/>
|
||||||
|
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
@@ -314,14 +401,17 @@ async function copyTopic(topic) {
|
|||||||
color: #999;
|
color: #999;
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.msg-dir {
|
.msg-dir {
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
width: 28px;
|
width: 28px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.msg-dir.pub {
|
.msg-dir.pub {
|
||||||
color: #2080f0;
|
color: #2080f0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.msg-topic {
|
.msg-topic {
|
||||||
color: #d25a00;
|
color: #d25a00;
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
@@ -335,6 +425,7 @@ async function copyTopic(topic) {
|
|||||||
.msg-topic:hover {
|
.msg-topic:hover {
|
||||||
text-decoration: underline;
|
text-decoration: underline;
|
||||||
}
|
}
|
||||||
|
|
||||||
.msg-payload {
|
.msg-payload {
|
||||||
margin: 6px 0 0;
|
margin: 6px 0 0;
|
||||||
padding: 8px 10px;
|
padding: 8px 10px;
|
||||||
@@ -345,6 +436,7 @@ async function copyTopic(topic) {
|
|||||||
white-space: pre-wrap;
|
white-space: pre-wrap;
|
||||||
overflow-wrap: anywhere;
|
overflow-wrap: anywhere;
|
||||||
}
|
}
|
||||||
|
|
||||||
.msg-meta {
|
.msg-meta {
|
||||||
color: #999;
|
color: #999;
|
||||||
font-size: 11px;
|
font-size: 11px;
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import { ref, watch } from 'vue'
|
import { ref, watch } from 'vue';
|
||||||
import { useMqttStore } from '../stores/mqtt.js'
|
import { useMqttStore } from '../stores/mqtt.js';
|
||||||
import {
|
import {
|
||||||
NCard,
|
NCard,
|
||||||
NSpace,
|
NSpace,
|
||||||
@@ -13,154 +13,191 @@ import {
|
|||||||
NListItem,
|
NListItem,
|
||||||
NTag,
|
NTag,
|
||||||
NEllipsis,
|
NEllipsis,
|
||||||
useMessage
|
useMessage,
|
||||||
} from 'naive-ui'
|
} from 'naive-ui';
|
||||||
|
|
||||||
const store = useMqttStore()
|
const store = useMqttStore();
|
||||||
const msg = useMessage()
|
const msg = useMessage();
|
||||||
|
|
||||||
const topicInput = ref('')
|
const topicInput = ref('');
|
||||||
const qos = ref(2)
|
const qos = ref(2);
|
||||||
const editingTopicId = ref(null)
|
const editingTopicId = ref(null);
|
||||||
const editingTopic = ref('')
|
const editingTopic = ref('');
|
||||||
const editingQos = ref(2)
|
const editingQos = ref(2);
|
||||||
const qosOptions = [
|
const qosOptions = [
|
||||||
{ label: 'QoS 0 - 最多一次', value: 0 },
|
{ label: 'QoS 0 - 最多一次', value: 0 },
|
||||||
{ label: 'QoS 1 - 至少一次', value: 1 },
|
{ label: 'QoS 1 - 至少一次', value: 1 },
|
||||||
{ label: 'QoS 2 - 恰好一次', value: 2 }
|
{ label: 'QoS 2 - 恰好一次', value: 2 },
|
||||||
]
|
];
|
||||||
|
|
||||||
const formStorageKey = 'mqtt-client.subscribe-form'
|
const formStorageKey = 'mqtt-client.subscribe-form';
|
||||||
let formReady = false
|
let formReady = false;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const savedForm = JSON.parse(localStorage.getItem(formStorageKey) || '{}')
|
const savedForm = JSON.parse(localStorage.getItem(formStorageKey) || '{}');
|
||||||
if (typeof savedForm.topicInput === 'string') topicInput.value = savedForm.topicInput
|
if (typeof savedForm.topicInput === 'string') {
|
||||||
if ([0, 1, 2].includes(savedForm.qos)) qos.value = savedForm.qos
|
topicInput.value = savedForm.topicInput;
|
||||||
|
}
|
||||||
|
if ([0, 1, 2].includes(savedForm.qos)) {
|
||||||
|
qos.value = savedForm.qos;
|
||||||
|
}
|
||||||
} catch {
|
} catch {
|
||||||
localStorage.removeItem(formStorageKey)
|
localStorage.removeItem(formStorageKey);
|
||||||
}
|
}
|
||||||
|
|
||||||
watch([topicInput, qos], () => {
|
watch([topicInput, qos], () => {
|
||||||
if (!formReady) return
|
if (!formReady) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
localStorage.setItem(
|
localStorage.setItem(
|
||||||
formStorageKey,
|
formStorageKey,
|
||||||
JSON.stringify({ topicInput: topicInput.value, qos: qos.value })
|
JSON.stringify({ topicInput: topicInput.value, qos: qos.value })
|
||||||
)
|
);
|
||||||
})
|
});
|
||||||
|
|
||||||
formReady = true
|
formReady = true;
|
||||||
|
|
||||||
async function handleAddTopic() {
|
async function handleAddTopic() {
|
||||||
const topic = topicInput.value.trim()
|
const topic = topicInput.value.trim();
|
||||||
if (!topic) {
|
if (!topic) {
|
||||||
msg.warning('请输入主题名称')
|
msg.warning('请输入主题名称');
|
||||||
return
|
return;
|
||||||
|
}
|
||||||
|
const srv = store.activeServer.value;
|
||||||
|
if (!srv) {
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
const srv = store.activeServer.value
|
|
||||||
if (!srv) return
|
|
||||||
if (srv.topics && srv.topics.find((t) => t.topic === topic)) {
|
if (srv.topics && srv.topics.find((t) => t.topic === topic)) {
|
||||||
msg.warning('该主题已存在')
|
msg.warning('该主题已存在');
|
||||||
return
|
return;
|
||||||
}
|
}
|
||||||
const result = await store.addTopic(srv.id, topic, qos.value)
|
const result = await store.addTopic(srv.id, topic, qos.value);
|
||||||
if (result) {
|
if (result) {
|
||||||
msg.success('主题已添加' + (srv.status === 'connected' ? '并已订阅' : ''))
|
msg.success('主题已添加' + (srv.status === 'connected' ? '并已订阅' : ''));
|
||||||
topicInput.value = ''
|
topicInput.value = '';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function handleRemoveTopic(topic) {
|
async function handleRemoveTopic(topic) {
|
||||||
const srv = store.activeServer.value
|
const srv = store.activeServer.value;
|
||||||
if (!srv) return
|
if (!srv) {
|
||||||
await store.removeTopic(srv.id, topic)
|
return;
|
||||||
msg.info('主题已移除')
|
}
|
||||||
|
await store.removeTopic(srv.id, topic);
|
||||||
|
msg.info('主题已移除');
|
||||||
}
|
}
|
||||||
|
|
||||||
async function handleToggleSub(topic) {
|
async function handleToggleSub(topic) {
|
||||||
const srv = store.activeServer.value
|
const srv = store.activeServer.value;
|
||||||
if (!srv) return
|
if (!srv) {
|
||||||
if (srv.status !== 'connected') {
|
return;
|
||||||
msg.warning('请先连接服务器')
|
}
|
||||||
return
|
if (srv.status !== 'connected') {
|
||||||
|
msg.warning('请先连接服务器');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const t = srv.topics.find((t) => t.topic === topic);
|
||||||
|
if (!t) {
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
const t = srv.topics.find((t) => t.topic === topic)
|
|
||||||
if (!t) return
|
|
||||||
if (t.subscribed) {
|
if (t.subscribed) {
|
||||||
await store.unsubscribeTopic(srv.id, topic)
|
await store.unsubscribeTopic(srv.id, topic);
|
||||||
msg.info('已取消订阅: ' + topic)
|
msg.info('已取消订阅: ' + topic);
|
||||||
} else {
|
} else {
|
||||||
await store.subscribeTopic(srv.id, topic)
|
await store.subscribeTopic(srv.id, topic);
|
||||||
msg.info('已订阅: ' + topic)
|
msg.info('已订阅: ' + topic);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function handleSubscribeAll() {
|
async function handleSubscribeAll() {
|
||||||
const srv = store.activeServer.value
|
const srv = store.activeServer.value;
|
||||||
if (!srv) return
|
if (!srv) {
|
||||||
if (srv.status !== 'connected') {
|
return;
|
||||||
msg.warning('请先连接服务器')
|
|
||||||
return
|
|
||||||
}
|
}
|
||||||
await store.subscribeAllTopics(srv.id)
|
if (srv.status !== 'connected') {
|
||||||
msg.success('已订阅全部主题')
|
msg.warning('请先连接服务器');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
await store.subscribeAllTopics(srv.id);
|
||||||
|
msg.success('已订阅全部主题');
|
||||||
}
|
}
|
||||||
|
|
||||||
function startEdit(t) {
|
function startEdit(t) {
|
||||||
editingTopicId.value = t.id || t.topic
|
editingTopicId.value = t.id || t.topic;
|
||||||
editingTopic.value = t.topic
|
editingTopic.value = t.topic;
|
||||||
editingQos.value = t.qos ?? 2
|
editingQos.value = t.qos ?? 2;
|
||||||
}
|
}
|
||||||
|
|
||||||
function cancelEdit() {
|
function cancelEdit() {
|
||||||
editingTopicId.value = null
|
editingTopicId.value = null;
|
||||||
editingTopic.value = ''
|
editingTopic.value = '';
|
||||||
editingQos.value = 2
|
editingQos.value = 2;
|
||||||
}
|
}
|
||||||
|
|
||||||
async function handleEditSave(t) {
|
async function handleEditSave(t) {
|
||||||
const srv = store.activeServer.value
|
const srv = store.activeServer.value;
|
||||||
if (!srv) return
|
if (!srv) {
|
||||||
const topicName = editingTopic.value.trim()
|
return;
|
||||||
|
}
|
||||||
|
const topicName = editingTopic.value.trim();
|
||||||
if (!topicName) {
|
if (!topicName) {
|
||||||
msg.warning('请输入主题名称')
|
msg.warning('请输入主题名称');
|
||||||
return
|
return;
|
||||||
}
|
}
|
||||||
if (
|
if (
|
||||||
srv.topics.find(
|
srv.topics.find(
|
||||||
(item) => item.topic === topicName && (item.id || item.topic) !== editingTopicId.value
|
(item) => item.topic === topicName && (item.id || item.topic) !== editingTopicId.value
|
||||||
)
|
)
|
||||||
) {
|
) {
|
||||||
msg.warning('该主题已存在')
|
msg.warning('该主题已存在');
|
||||||
return
|
return;
|
||||||
}
|
}
|
||||||
const topicId = t.id || t.topic
|
const topicId = t.id || t.topic;
|
||||||
const result = await store.updateTopic(srv.id, topicId, topicName, editingQos.value)
|
const result = await store.updateTopic(srv.id, topicId, topicName, editingQos.value);
|
||||||
if (result) {
|
if (result) {
|
||||||
msg.success('主题已更新')
|
msg.success('主题已更新');
|
||||||
cancelEdit()
|
cancelEdit();
|
||||||
} else {
|
} else {
|
||||||
msg.error('主题更新失败')
|
msg.error('主题更新失败');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div v-if="store.activeServer.value" class="subscribe-view">
|
<div
|
||||||
|
v-if="store.activeServer.value"
|
||||||
|
class="subscribe-view"
|
||||||
|
>
|
||||||
<!-- 添加主题 -->
|
<!-- 添加主题 -->
|
||||||
<n-card title="添加订阅主题" size="small">
|
<n-card
|
||||||
|
title="添加订阅主题"
|
||||||
|
size="small"
|
||||||
|
>
|
||||||
<div class="topic-form">
|
<div class="topic-form">
|
||||||
<n-form-item class="topic-field" label="主题 (Topic)">
|
<n-form-item
|
||||||
|
class="topic-field"
|
||||||
|
label="主题 (Topic)"
|
||||||
|
>
|
||||||
<n-input
|
<n-input
|
||||||
v-model:value="topicInput"
|
v-model:value="topicInput"
|
||||||
placeholder="例如: sensor/+/temperature"
|
placeholder="例如: sensor/+/temperature"
|
||||||
@keydown.enter="handleAddTopic"
|
@keydown.enter="handleAddTopic"
|
||||||
/>
|
/>
|
||||||
</n-form-item>
|
</n-form-item>
|
||||||
<n-form-item class="qos-field" label="QoS">
|
<n-form-item
|
||||||
<n-select v-model:value="qos" :options="qosOptions" />
|
class="qos-field"
|
||||||
|
label="QoS"
|
||||||
|
>
|
||||||
|
<n-select
|
||||||
|
v-model:value="qos"
|
||||||
|
:options="qosOptions"
|
||||||
|
/>
|
||||||
</n-form-item>
|
</n-form-item>
|
||||||
<n-button class="topic-submit" type="primary" @click="handleAddTopic">
|
<n-button
|
||||||
|
class="topic-submit"
|
||||||
|
type="primary"
|
||||||
|
@click="handleAddTopic"
|
||||||
|
>
|
||||||
+ 添加订阅
|
+ 添加订阅
|
||||||
</n-button>
|
</n-button>
|
||||||
</div>
|
</div>
|
||||||
@@ -175,29 +212,46 @@ async function handleEditSave(t) {
|
|||||||
display: 'flex',
|
display: 'flex',
|
||||||
flexDirection: 'column',
|
flexDirection: 'column',
|
||||||
minHeight: 0,
|
minHeight: 0,
|
||||||
overflow: 'hidden'
|
overflow: 'hidden',
|
||||||
}"
|
}"
|
||||||
>
|
>
|
||||||
<template #header-extra>
|
<template #header-extra>
|
||||||
<n-button size="small" @click="handleSubscribeAll">全部订阅</n-button>
|
<n-button
|
||||||
|
size="small"
|
||||||
|
@click="handleSubscribeAll"
|
||||||
|
>
|
||||||
|
全部订阅
|
||||||
|
</n-button>
|
||||||
</template>
|
</template>
|
||||||
<n-empty
|
<n-empty
|
||||||
v-if="!store.activeServer.value.topics || store.activeServer.value.topics.length === 0"
|
v-if="!store.activeServer.value.topics || store.activeServer.value.topics.length === 0"
|
||||||
description="暂无订阅主题,请在上方添加"
|
description="暂无订阅主题,请在上方添加"
|
||||||
/>
|
/>
|
||||||
<div v-else class="topic-list-scroll">
|
<div
|
||||||
|
v-else
|
||||||
|
class="topic-list-scroll"
|
||||||
|
>
|
||||||
<n-list>
|
<n-list>
|
||||||
<n-list-item
|
<n-list-item
|
||||||
v-for="t in store.activeServer.value.topics"
|
v-for="t in store.activeServer.value.topics"
|
||||||
:key="t.id || t.topic"
|
:key="t.id || t.topic"
|
||||||
class="topic-item"
|
class="topic-item"
|
||||||
>
|
>
|
||||||
<template v-if="editingTopicId !== (t.id || t.topic)" #prefix>
|
<template
|
||||||
<n-tag :type="t.subscribed ? 'success' : 'default'" size="small">
|
v-if="editingTopicId !== (t.id || t.topic)"
|
||||||
|
#prefix
|
||||||
|
>
|
||||||
|
<n-tag
|
||||||
|
:type="t.subscribed ? 'success' : 'default'"
|
||||||
|
size="small"
|
||||||
|
>
|
||||||
QoS {{ t.qos }}
|
QoS {{ t.qos }}
|
||||||
</n-tag>
|
</n-tag>
|
||||||
</template>
|
</template>
|
||||||
<div v-if="editingTopicId === (t.id || t.topic)" class="topic-edit-row">
|
<div
|
||||||
|
v-if="editingTopicId === (t.id || t.topic)"
|
||||||
|
class="topic-edit-row"
|
||||||
|
>
|
||||||
<n-input
|
<n-input
|
||||||
v-model:value="editingTopic"
|
v-model:value="editingTopic"
|
||||||
size="small"
|
size="small"
|
||||||
@@ -210,12 +264,29 @@ async function handleEditSave(t) {
|
|||||||
:options="qosOptions"
|
:options="qosOptions"
|
||||||
style="width: 160px; flex-shrink: 0"
|
style="width: 160px; flex-shrink: 0"
|
||||||
/>
|
/>
|
||||||
<n-space :size="4" :wrap="false">
|
<n-space
|
||||||
<n-button size="tiny" type="primary" @click="handleEditSave(t)">保存</n-button>
|
:size="4"
|
||||||
<n-button size="tiny" @click="cancelEdit">取消</n-button>
|
:wrap="false"
|
||||||
|
>
|
||||||
|
<n-button
|
||||||
|
size="tiny"
|
||||||
|
type="primary"
|
||||||
|
@click="handleEditSave(t)"
|
||||||
|
>
|
||||||
|
保存
|
||||||
|
</n-button>
|
||||||
|
<n-button
|
||||||
|
size="tiny"
|
||||||
|
@click="cancelEdit"
|
||||||
|
>
|
||||||
|
取消
|
||||||
|
</n-button>
|
||||||
</n-space>
|
</n-space>
|
||||||
</div>
|
</div>
|
||||||
<div v-else class="topic-name">
|
<div
|
||||||
|
v-else
|
||||||
|
class="topic-name"
|
||||||
|
>
|
||||||
<n-ellipsis>{{ t.topic }}</n-ellipsis>
|
<n-ellipsis>{{ t.topic }}</n-ellipsis>
|
||||||
</div>
|
</div>
|
||||||
<template #suffix>
|
<template #suffix>
|
||||||
@@ -225,7 +296,12 @@ async function handleEditSave(t) {
|
|||||||
:size="4"
|
:size="4"
|
||||||
:wrap="false"
|
:wrap="false"
|
||||||
>
|
>
|
||||||
<n-button size="tiny" @click="startEdit(t)">编辑</n-button>
|
<n-button
|
||||||
|
size="tiny"
|
||||||
|
@click="startEdit(t)"
|
||||||
|
>
|
||||||
|
编辑
|
||||||
|
</n-button>
|
||||||
<n-button
|
<n-button
|
||||||
size="tiny"
|
size="tiny"
|
||||||
:type="t.subscribed ? 'error' : 'success'"
|
:type="t.subscribed ? 'error' : 'success'"
|
||||||
@@ -233,7 +309,12 @@ async function handleEditSave(t) {
|
|||||||
>
|
>
|
||||||
{{ t.subscribed ? '取消订阅' : '订阅' }}
|
{{ t.subscribed ? '取消订阅' : '订阅' }}
|
||||||
</n-button>
|
</n-button>
|
||||||
<n-button size="tiny" type="error" text @click="handleRemoveTopic(t.topic)">
|
<n-button
|
||||||
|
size="tiny"
|
||||||
|
type="error"
|
||||||
|
text
|
||||||
|
@click.stop="handleRemoveTopic(t.topic)"
|
||||||
|
>
|
||||||
✕
|
✕
|
||||||
</n-button>
|
</n-button>
|
||||||
</n-space>
|
</n-space>
|
||||||
@@ -243,7 +324,10 @@ async function handleEditSave(t) {
|
|||||||
</div>
|
</div>
|
||||||
</n-card>
|
</n-card>
|
||||||
</div>
|
</div>
|
||||||
<n-empty v-else description="请先在左侧选择一个 MQTT 服务器" />
|
<n-empty
|
||||||
|
v-else
|
||||||
|
description="请先在左侧选择一个 MQTT 服务器"
|
||||||
|
/>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
|
|||||||
Reference in New Issue
Block a user