feat: 阿里云 MQTT 连接预设、连接分组与 MQTT 3.1.1 支持

- 后端引入 rumqttc v4 客户端,支持 MQTT 3.1.1 连接/发布/订阅,协议版本随连接下发
- 连接表单:阿里云预设管理(分组、导入导出、去抖自动保存),退出云模式需确认,mqtts 时端口跟随 8883
- 连接列表:分组列就地编辑(输入即创建),空分组提示优化
- 导入按内容去重并提示跳过数量,导出提示含明文密钥
- 调试日志密码脱敏;修复多处取消弹窗的 unhandled rejection
This commit is contained in:
2026-09-04 01:45:19 +08:00
parent 8a2c63e575
commit 0152a10ee6
10 changed files with 1626 additions and 345 deletions
+125
View File
@@ -0,0 +1,125 @@
import { ref, watch } from 'vue'
import type { AliyunPreset } from '../types/mqtt'
import { defaultAliyunPreset } from '../types/mqtt'
const STORAGE_KEY = 'mqttx_aliyun_presets'
function loadPresets(): AliyunPreset[] {
try {
const raw = localStorage.getItem(STORAGE_KEY)
if (!raw) return []
const parsed = JSON.parse(raw) as AliyunPreset[]
return parsed.map((p) => ({
...defaultAliyunPreset(),
...p,
id: p.id || crypto.randomUUID(),
group: p.group ?? '',
}))
} catch {
return []
}
}
function savePresets(presets: AliyunPreset[]) {
try {
localStorage.setItem(STORAGE_KEY, JSON.stringify(presets))
} catch {}
}
const presets = ref<AliyunPreset[]>(loadPresets())
watch(presets, (val) => savePresets(val), { deep: true })
export function useAliyunPresets() {
function add(preset?: Partial<AliyunPreset>): AliyunPreset {
const p: AliyunPreset = { ...defaultAliyunPreset(), ...preset }
presets.value.push(p)
return p
}
function update(id: string, data: Partial<AliyunPreset>) {
const idx = presets.value.findIndex((p) => p.id === id)
if (idx !== -1) {
presets.value[idx] = { ...presets.value[idx], ...data }
}
}
function remove(id: string) {
const idx = presets.value.findIndex((p) => p.id === id)
if (idx !== -1) presets.value.splice(idx, 1)
}
function getById(id: string): AliyunPreset | undefined {
return presets.value.find((p) => p.id === id)
}
// ─── 分组 ────────────────────────────────────────────────────────────────
function getGroups(): string[] {
const set = new Set<string>()
for (const p of presets.value) {
if (p.group) set.add(p.group)
}
return Array.from(set).sort()
}
function renameGroup(oldName: string, newName: string) {
for (const p of presets.value) {
if (p.group === oldName) p.group = newName
}
}
function deleteGroup(group: string) {
for (const p of presets.value) {
if (p.group === group) p.group = ''
}
}
// ─── 导入 / 导出 ────────────────────────────────────────────────────────
function exportPresets(ids?: string[]): string {
const list = ids
? presets.value.filter((p) => ids.includes(p.id))
: presets.value
return JSON.stringify(list, null, 2)
}
/** 按 实例ID+GroupID+设备ID+AK 去重,返回导入/跳过数量 */
function importPresets(json: string): { imported: number; skipped: number } {
const parsed = JSON.parse(json) as Partial<AliyunPreset>[]
const seen = new Set(
presets.value.map((p) => `${p.instanceId}|${p.groupId}|${p.deviceId}|${p.accessKeyId}`),
)
let imported = 0
let skipped = 0
for (const item of parsed) {
const p: AliyunPreset = {
...defaultAliyunPreset(),
...item,
id: crypto.randomUUID(),
}
const key = `${p.instanceId}|${p.groupId}|${p.deviceId}|${p.accessKeyId}`
if (seen.has(key)) {
skipped++
continue
}
seen.add(key)
presets.value.push(p)
imported++
}
return { imported, skipped }
}
return {
presets,
add,
update,
remove,
getById,
getGroups,
renameGroup,
deleteGroup,
exportPresets,
importPresets,
}
}
+84 -6
View File
@@ -9,9 +9,10 @@ function loadConnections(): MqttConnection[] {
const raw = localStorage.getItem(STORAGE_KEY)
if (!raw) return []
const parsed = JSON.parse(raw) as MqttConnection[]
// Ensure each connection has runtime status fields
return parsed.map((c) => ({
...c,
protocolVersion: c.protocolVersion ?? '5.0',
group: c.group ?? '',
status: 'disconnected' as const,
lastError: '',
}))
@@ -22,17 +23,15 @@ function loadConnections(): MqttConnection[] {
function saveConnections(connections: MqttConnection[]) {
try {
// Strip runtime fields before persisting
const toSave = connections.map(({ status: _, lastError: __, ...rest }) => rest)
localStorage.setItem(STORAGE_KEY, JSON.stringify(toSave))
} catch {
// localStorage full or unavailable — silently ignore
// localStorage full or unavailable
}
}
const connections = ref<MqttConnection[]>(loadConnections())
// Auto-persist on changes
watch(
connections,
(val) => {
@@ -42,7 +41,7 @@ watch(
)
export function useConnections() {
function add() {
function add(): MqttConnection {
const conn = defaultConnection()
connections.value.push(conn)
return conn
@@ -82,6 +81,79 @@ export function useConnections() {
return connections.value.filter((c) => c.status === 'connected')
}
// ─── 分组操作 ──────────────────────────────────────────────────────────────
/** 获取所有分组名称(去重) */
function getGroups(): string[] {
const set = new Set<string>()
for (const c of connections.value) {
if (c.group) set.add(c.group)
}
return Array.from(set).sort()
}
/** 按分组获取连接列表 */
function getByGroup(group: string): MqttConnection[] {
return connections.value.filter((c) => c.group === group)
}
/** 重命名分组 */
function renameGroup(oldName: string, newName: string) {
for (const c of connections.value) {
if (c.group === oldName) {
c.group = newName
}
}
}
/** 删除分组(将组内连接移到未分组) */
function deleteGroup(group: string) {
for (const c of connections.value) {
if (c.group === group) {
c.group = ''
}
}
}
// ─── 导入 / 导出 ──────────────────────────────────────────────────────────
/** 导出为 JSON 字符串(去除运行时字段) */
function exportConnections(ids?: string[]): string {
const list = ids
? connections.value.filter((c) => ids.includes(c.id))
: connections.value
const data = list.map(({ status: _, lastError: __, ...rest }) => rest)
return JSON.stringify(data, null, 2)
}
/** 从 JSON 导入,按名称+地址+端口+ClientID 去重,返回导入/跳过数量 */
function importConnections(json: string): { imported: number; skipped: number } {
const parsed = JSON.parse(json) as Partial<MqttConnection>[]
const seen = new Set(
connections.value.map((c) => `${c.name}|${c.host}|${c.port}|${c.clientId}`),
)
let imported = 0
let skipped = 0
for (const item of parsed) {
const conn: MqttConnection = {
...defaultConnection(),
...item,
id: crypto.randomUUID(), // 始终生成新 ID 避免冲突
status: 'disconnected',
lastError: '',
}
const key = `${conn.name}|${conn.host}|${conn.port}|${conn.clientId}`
if (seen.has(key)) {
skipped++
continue
}
seen.add(key)
connections.value.push(conn)
imported++
}
return { imported, skipped }
}
return {
connections,
add,
@@ -90,5 +162,11 @@ export function useConnections() {
getById,
updateStatus,
getConnected,
getGroups,
getByGroup,
renameGroup,
deleteGroup,
exportConnections,
importConnections,
}
}
}
+4
View File
@@ -149,6 +149,10 @@ export async function mqttTestConnection(conn: MqttConnectionDto): Promise<strin
return await invoke<string>('mqtt_test_connection', { conn })
}
export async function mqttReadDebugLog(): Promise<string> {
return await invoke<string>('mqtt_read_debug_log')
}
export async function mqttGetLogDir(): Promise<string> {
return await invoke<string>('mqtt_get_log_dir')
}