feat: 增加服务器信息导入、导出功能

This commit is contained in:
2026-07-19 20:00:33 +08:00
parent 2682c0a1d6
commit 21683cb790
5 changed files with 182 additions and 29 deletions

View File

@@ -58,6 +58,14 @@ export function registerIpcHandlers() {
return true;
});
ipcMain.handle('mqtt:export-servers', () => {
return mqttManager.exportServers();
});
ipcMain.handle('mqtt:import-servers', (_, serverList) => {
return mqttManager.importServers(serverList);
});
// ==================== 连接管理 ====================
ipcMain.handle('mqtt:connect', (_, id) => {

View File

@@ -164,23 +164,47 @@ class MqttManager {
return this.servers.get(id) || null;
}
getServerConfigSchema() {
return {
name: (c, e) => c.name ?? e?.name,
host: (c, e) => c.host ?? e?.host,
port: (c, e) => c.port || e?.port || 1883,
protocol: (c, e) => c.protocol || e?.protocol || 'mqtt',
clientId: (c, e) => c.clientId || e?.clientId || 'frost_' + Math.random().toString(36).slice(2, 10),
username: (c, e) => c.username ?? e?.username ?? '',
password: (c, e, opts) => c.password ?? (opts.keepExistingPassword ? e?.password : '') ?? '',
keepAlive: (c, e) => c.keepAlive || e?.keepAlive || 60,
cleanSession: (c) => c.cleanSession !== false,
connectTimeout: (c, e) => c.connectTimeout ?? e?.connectTimeout ?? 10000,
reconnect: (c) => c.reconnect !== false,
reconnectInterval: (c, e) => c.reconnectInterval ?? e?.reconnectInterval ?? 5000,
};
}
buildServerFields(config, existing = null, options = {}) {
let schema = this.getServerConfigSchema();
let fields = {};
for (let key of Object.keys(schema)) {
fields[key] = schema[key](config, existing, options);
}
return fields;
}
buildTopic(t) {
return {
id: t.id || 'topic_' + Date.now() + '_' + Math.random().toString(36).slice(2, 6),
topic: t.topic,
qos: t.qos ?? 1,
subscribed: false,
};
}
addServer(config) {
let id = 'srv_' + Date.now() + '_' + Math.random().toString(36).slice(2, 6);
let server = {
id,
name: config.name,
host: config.host,
port: config.port || 1883,
protocol: config.protocol || 'mqtt',
clientId: config.clientId || 'frost_' + Math.random().toString(36).slice(2, 10),
username: config.username || '',
password: config.password || '',
keepAlive: config.keepAlive || 60,
cleanSession: config.cleanSession !== false,
connectTimeout: config.connectTimeout ?? 10000,
reconnect: config.reconnect !== false,
reconnectInterval: config.reconnectInterval ?? 5000,
...this.buildServerFields(config),
status: 'disconnected',
topics: [],
};
@@ -202,23 +226,7 @@ class MqttManager {
return null;
}
Object.assign(server, {
name: config.name,
host: config.host,
port: config.port || 1883,
protocol: config.protocol || 'mqtt',
clientId: config.clientId || server.clientId,
username: config.username || '',
keepAlive: config.keepAlive || 60,
cleanSession: config.cleanSession !== false,
connectTimeout: config.connectTimeout ?? server.connectTimeout ?? 10000,
reconnect: config.reconnect !== false,
reconnectInterval: config.reconnectInterval ?? server.reconnectInterval ?? 5000,
});
if (config.password) {
server.password = config.password;
}
Object.assign(server, this.buildServerFields(config, server, { keepExistingPassword: true }));
this.saveData();
@@ -235,6 +243,63 @@ class MqttManager {
this.saveData();
}
exportServers() {
let schemaKeys = Object.keys(this.getServerConfigSchema());
return Array.from(this.servers.values()).map((s) => {
let exported = {};
for (let key of schemaKeys) {
exported[key] = s[key];
}
exported.topics = (s.topics || []).map((t) => this.buildTopicExport(t));
return exported;
});
}
buildTopicExport(t) {
return {
id: t.id,
topic: t.topic,
qos: t.qos,
};
}
importServers(serverList) {
if (!Array.isArray(serverList)) {
throw new Error('导入格式错误:必须是服务器配置数组');
}
// 断开所有现有连接
this.clients.forEach((client, id) => {
this.disconnect(id);
});
this.clients.clear();
this.servers.clear();
this.messages.clear();
this.publishHistory.clear();
serverList.forEach((config) => {
if (!config.name || !config.host) {
return;
}
let id = 'srv_' + Date.now() + '_' + Math.random().toString(36).slice(2, 6);
let server = {
id,
...this.buildServerFields(config),
status: 'disconnected',
topics: (config.topics || []).map((t) => this.buildTopic(t)),
};
this.servers.set(id, server);
this.messages.set(id, []);
this.publishHistory.set(id, []);
});
this.saveData();
return this.getServers();
}
// ==================== 连接管理 ====================
connect(id) {

View File

@@ -9,6 +9,8 @@ const api = {
addServer: (config) => ipcRenderer.invoke('mqtt:add-server', config),
updateServer: (id, config) => ipcRenderer.invoke('mqtt:update-server', id, config),
deleteServer: (id) => ipcRenderer.invoke('mqtt:delete-server', id),
exportServers: () => ipcRenderer.invoke('mqtt:export-servers'),
importServers: (serverList) => ipcRenderer.invoke('mqtt:import-servers', serverList),
// 连接管理
connect: (id) => ipcRenderer.invoke('mqtt:connect', id),

View File

@@ -1,4 +1,5 @@
<script setup>
import { ref } from 'vue';
import { NLayoutSider, NButton, NTag, useMessage, useDialog } from 'naive-ui';
import { useMqttStore } from '../stores/mqtt.js';
@@ -6,6 +7,7 @@ const emit = defineEmits(['add-server', 'edit-server']);
const store = useMqttStore();
const message = useMessage();
const dialog = useDialog();
const fileInput = ref(null);
async function handleConnect(server) {
if (server.status === 'connected') {
@@ -32,6 +34,40 @@ function handleDelete(server) {
},
});
}
async function handleExport() {
try {
let data = await store.exportServers();
let blob = new Blob([JSON.stringify(data, null, 2)], { type: 'application/json' });
let url = URL.createObjectURL(blob);
let a = document.createElement('a');
a.href = url;
a.download = `frost-mqtt-servers-${new Date().toISOString().slice(0, 10)}.json`;
a.click();
URL.revokeObjectURL(url);
message.success('服务器配置已导出');
} catch (e) {
message.error('导出失败: ' + (e.message || '未知错误'));
}
}
function handleImportClick() {
fileInput.value?.click();
}
async function handleImportFile(event) {
let file = event.target.files?.[0];
if (!file) return;
try {
let text = await file.text();
let data = JSON.parse(text);
await store.importServers(data);
message.success('服务器配置已导入');
} catch (e) {
message.error('导入失败: ' + (e.message || '未知错误'));
}
event.target.value = '';
}
</script>
<template>
@@ -125,6 +161,25 @@ function handleDelete(server) {
block
@click="$emit('add-server')"
> + 添加服务器</n-button>
<div class="sidebar-import-export">
<n-button
size="small"
block
@click="handleImportClick"
>导入配置</n-button>
<n-button
size="small"
block
@click="handleExport"
>导出配置</n-button>
</div>
<input
ref="fileInput"
type="file"
accept=".json,application/json"
style="display: none"
@change="handleImportFile"
>
</div>
</div>
@@ -297,4 +352,11 @@ function handleDelete(server) {
border-top: 1px solid var(--n-border-color);
flex-shrink: 0;
}
.sidebar-import-export {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 8px;
margin-top: 8px;
}
</style>

View File

@@ -161,6 +161,20 @@ export function useMqttStore() {
}
async function exportServers() {
return await window.api.exportServers();
}
async function importServers(serverList) {
let data = await window.api.importServers(serverList);
servers.value = data;
sortServers();
if (servers.value.length > 0 && !activeServerId.value) {
activeServerId.value = servers.value[0].id;
}
return data;
}
async function connectServer(id) {
return await window.api.connect(id);
}
@@ -273,6 +287,8 @@ export function useMqttStore() {
addServer,
updateServer,
deleteServer,
exportServers,
importServers,
connectServer,
disconnectServer,
addTopic,