feat: 增加订阅主题备注功能

This commit is contained in:
2026-07-20 22:03:03 +08:00
parent cd2f926ba7
commit 7eb62ca37f
6 changed files with 127 additions and 25 deletions
+4 -4
View File
@@ -90,8 +90,8 @@ export function registerIpcHandlers() {
// ==================== 主题管理 ==================== // ==================== 主题管理 ====================
ipcMain.handle('mqtt:add-topic', (_, serverId, topic, qos) => { ipcMain.handle('mqtt:add-topic', (_, serverId, topic, qos, comment) => {
return mqttManager.addTopic(serverId, topic, qos); return mqttManager.addTopic(serverId, topic, qos, comment);
}); });
ipcMain.handle('mqtt:remove-topic', (_, serverId, topic) => { ipcMain.handle('mqtt:remove-topic', (_, serverId, topic) => {
@@ -110,8 +110,8 @@ export function registerIpcHandlers() {
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, comment) => {
return mqttManager.updateTopic(serverId, topicId, topic, qos); return mqttManager.updateTopic(serverId, topicId, topic, qos, comment);
}); });
// ==================== 消息发布 ==================== // ==================== 消息发布 ====================
+12 -5
View File
@@ -82,6 +82,7 @@ class MqttManager {
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),
comment: t.comment ?? '',
subscribed: false, subscribed: false,
})); }));
this.servers.set(s.id, s); this.servers.set(s.id, s);
@@ -154,8 +155,8 @@ class MqttManager {
reconnectInterval: 5000, reconnectInterval: 5000,
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, comment: '', subscribed: false },
{ id: 'topic_default_002', topic: 'device/status', qos: 2, subscribed: false }, { id: 'topic_default_002', topic: 'device/status', qos: 2, comment: '', subscribed: false },
], ],
}, },
]; ];
@@ -272,6 +273,7 @@ class MqttManager {
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),
topic: t.topic, topic: t.topic,
qos: t.qos ?? 1, qos: t.qos ?? 1,
comment: t.comment ?? '',
subscribed: false, subscribed: false,
}; };
} }
@@ -361,6 +363,7 @@ class MqttManager {
id: t.id, id: t.id,
topic: t.topic, topic: t.topic,
qos: t.qos, qos: t.qos,
comment: t.comment ?? '',
}; };
} }
@@ -575,9 +578,10 @@ class MqttManager {
* @param {string} serverId - 服务器 id * @param {string} serverId - 服务器 id
* @param {string} topic - 主题名称 * @param {string} topic - 主题名称
* @param {MqttQoS} qos - QoS 等级 * @param {MqttQoS} qos - QoS 等级
* @param {string} comment - 主题备注
* @returns {MqttTopic | null} 添加后的主题对象或 null * @returns {MqttTopic | null} 添加后的主题对象或 null
*/ */
addTopic(serverId, topic, qos = 1) { addTopic(serverId, topic, qos = 1, comment = '') {
let server = this.servers.get(serverId); let server = this.servers.get(serverId);
if (!server) { if (!server) {
return null; return null;
@@ -588,7 +592,7 @@ class MqttManager {
let topicId = 'topic_' + Date.now() + '_' + Math.random().toString(36).slice(2, 6); let topicId = 'topic_' + Date.now() + '_' + Math.random().toString(36).slice(2, 6);
let newTopic = { id: topicId, topic, qos, subscribed: false }; let newTopic = { id: topicId, topic, qos, comment, subscribed: false };
server.topics.push(newTopic); server.topics.push(newTopic);
this.saveData(); this.saveData();
@@ -686,9 +690,10 @@ class MqttManager {
* @param {string} topicId - 主题 id * @param {string} topicId - 主题 id
* @param {string} newTopic - 新的主题名称 * @param {string} newTopic - 新的主题名称
* @param {number} newQos - 新的 QoS 等级 * @param {number} newQos - 新的 QoS 等级
* @param {string} comment - 新的主题备注
* @returns {boolean} 是否更新成功 * @returns {boolean} 是否更新成功
*/ */
updateTopic(serverId, topicId, newTopic, newQos) { updateTopic(serverId, topicId, newTopic, newQos, comment = '') {
let server = this.servers.get(serverId); let server = this.servers.get(serverId);
@@ -721,6 +726,7 @@ class MqttManager {
t.topic = newTopic; t.topic = newTopic;
t.qos = newQos; t.qos = newQos;
t.comment = comment;
// 如果已连接且此前已订阅,重新订阅新主题 // 如果已连接且此前已订阅,重新订阅新主题
if (server.status === 'connected' && wasSubscribed) { if (server.status === 'connected' && wasSubscribed) {
@@ -733,6 +739,7 @@ class MqttManager {
topic: newTopic, topic: newTopic,
subscribed: t.subscribed, subscribed: t.subscribed,
topicId, topicId,
comment: t.comment,
}); });
return true; return true;
+4 -3
View File
@@ -20,13 +20,14 @@ const api = {
disconnect: (id) => ipcRenderer.invoke('mqtt:disconnect', id), disconnect: (id) => ipcRenderer.invoke('mqtt:disconnect', id),
// 主题管理 // 主题管理
addTopic: (serverId, topic, qos) => ipcRenderer.invoke('mqtt:add-topic', serverId, topic, qos), addTopic: (serverId, topic, qos, comment) =>
ipcRenderer.invoke('mqtt:add-topic', serverId, topic, qos, comment),
removeTopic: (serverId, topic) => ipcRenderer.invoke('mqtt:remove-topic', serverId, topic), removeTopic: (serverId, topic) => ipcRenderer.invoke('mqtt:remove-topic', serverId, topic),
subscribe: (serverId, topic) => ipcRenderer.invoke('mqtt:subscribe', serverId, topic), subscribe: (serverId, topic) => ipcRenderer.invoke('mqtt:subscribe', serverId, topic),
unsubscribe: (serverId, topic) => ipcRenderer.invoke('mqtt:unsubscribe', serverId, topic), unsubscribe: (serverId, topic) => ipcRenderer.invoke('mqtt:unsubscribe', serverId, topic),
subscribeAll: (serverId) => ipcRenderer.invoke('mqtt:subscribe-all', serverId), subscribeAll: (serverId) => ipcRenderer.invoke('mqtt:subscribe-all', serverId),
updateTopic: (serverId, topicId, topic, qos) => updateTopic: (serverId, topicId, topic, qos, comment) =>
ipcRenderer.invoke('mqtt:update-topic', serverId, topicId, topic, qos), ipcRenderer.invoke('mqtt:update-topic', serverId, topicId, topic, qos, comment),
// 消息发布 // 消息发布
publish: (serverId, topic, payload, opts) => publish: (serverId, topic, payload, opts) =>
+12 -6
View File
@@ -33,6 +33,7 @@ import { ref, computed } from 'vue';
* @property {string} serverId 所属服务器 id * @property {string} serverId 所属服务器 id
* @property {string} topic 主题名称 * @property {string} topic 主题名称
* @property {string} [topicId] 主题 id * @property {string} [topicId] 主题 id
* @property {string} [comment] 主题备注
* @property {boolean} subscribed 是否已订阅 * @property {boolean} subscribed 是否已订阅
*/ */
@@ -57,12 +58,12 @@ import { ref, computed } from 'vue';
* @property {(serverList: ServerConfig[]) => Promise<MqttServer[]>} importServers 导入服务器配置 * @property {(serverList: ServerConfig[]) => Promise<MqttServer[]>} importServers 导入服务器配置
* @property {(id: string) => Promise<boolean>} connectServer 连接服务器 * @property {(id: string) => Promise<boolean>} connectServer 连接服务器
* @property {(id: string) => Promise<void>} disconnectServer 断开服务器 * @property {(id: string) => Promise<void>} disconnectServer 断开服务器
* @property {(serverId: string, topic: string, qos: MqttQoS) => Promise<MqttTopic | null>} addTopic 添加主题 * @property {(serverId: string, topic: string, qos: MqttQoS, comment: string) => Promise<MqttTopic | null>} addTopic 添加主题
* @property {(serverId: string, topic: string) => Promise<void>} removeTopic 移除主题 * @property {(serverId: string, topic: string) => Promise<void>} removeTopic 移除主题
* @property {(serverId: string, topic: string) => Promise<boolean>} subscribeTopic 订阅主题 * @property {(serverId: string, topic: string) => Promise<boolean>} subscribeTopic 订阅主题
* @property {(serverId: string, topic: string) => Promise<boolean>} unsubscribeTopic 取消订阅主题 * @property {(serverId: string, topic: string) => Promise<boolean>} unsubscribeTopic 取消订阅主题
* @property {(serverId: string) => Promise<boolean>} subscribeAllTopics 批量订阅所有主题 * @property {(serverId: string) => Promise<boolean>} subscribeAllTopics 批量订阅所有主题
* @property {(serverId: string, topicId: string, topic: string, qos: MqttQoS) => Promise<boolean>} updateTopic 更新主题 * @property {(serverId: string, topicId: string, topic: string, qos: MqttQoS, comment: string) => Promise<boolean>} updateTopic 更新主题
* @property {(serverId: string, topic: string, payload: string, opts: PublishOptions) => Promise<boolean>} publishMessage 发布消息 * @property {(serverId: string, topic: string, payload: string, opts: PublishOptions) => Promise<boolean>} publishMessage 发布消息
* @property {() => Promise<void>} refreshMessages 刷新消息列表 * @property {() => Promise<void>} refreshMessages 刷新消息列表
* @property {() => Promise<void>} refreshPublishHistory 刷新发布历史 * @property {() => Promise<void>} refreshPublishHistory 刷新发布历史
@@ -181,6 +182,9 @@ function setupListeners() {
if (t) { if (t) {
t.subscribed = data.subscribed; t.subscribed = data.subscribed;
if (typeof data.comment === 'string') {
t.comment = data.comment;
}
if (data.topic && data.topic !== t.topic) { if (data.topic && data.topic !== t.topic) {
t.topic = data.topic; t.topic = data.topic;
sortTopics(srv); sortTopics(srv);
@@ -400,11 +404,12 @@ export function useMqttStore() {
* @param {string} serverId - 服务器 id * @param {string} serverId - 服务器 id
* @param {string} topic - 主题名称 * @param {string} topic - 主题名称
* @param {MqttQoS} qos - QoS 等级 * @param {MqttQoS} qos - QoS 等级
* @param {string} comment - 主题备注
* @returns {Promise<MqttTopic | null>} 添加后的主题对象或 null * @returns {Promise<MqttTopic | null>} 添加后的主题对象或 null
*/ */
async function addTopic(serverId, topic, qos) { async function addTopic(serverId, topic, qos, comment) {
let result = await window.api.addTopic(serverId, topic, qos); let result = await window.api.addTopic(serverId, topic, qos, comment);
if (result) { if (result) {
let srv = servers.value.find((s) => s.id === serverId); let srv = servers.value.find((s) => s.id === serverId);
@@ -472,10 +477,11 @@ export function useMqttStore() {
* @param {string} topicId - 主题 id * @param {string} topicId - 主题 id
* @param {string} topic - 新的主题名称 * @param {string} topic - 新的主题名称
* @param {MqttQoS} qos - 新的 QoS 等级 * @param {MqttQoS} qos - 新的 QoS 等级
* @param {string} comment - 新的主题备注
* @returns {Promise<boolean>} 是否更新成功 * @returns {Promise<boolean>} 是否更新成功
*/ */
async function updateTopic(serverId, topicId, topic, qos) { async function updateTopic(serverId, topicId, topic, qos, comment) {
return await window.api.updateTopic(serverId, topicId, topic, qos); return await window.api.updateTopic(serverId, topicId, topic, qos, comment);
} }
// ==================== 消息发布 ==================== // ==================== 消息发布 ====================
+91 -7
View File
@@ -1,7 +1,7 @@
<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 { NCard, NSpace, NFormItem, NInput, NSelect, NButton, NEmpty, NList, NListItem, NTag, NEllipsis, useMessage } from 'naive-ui'; import { NCard, NSpace, NFormItem, NInput, NSelect, NButton, NEmpty, NList, NListItem, NTag, useMessage } from 'naive-ui';
// 全局状态与消息 API // 全局状态与消息 API
const store = useMqttStore(); const store = useMqttStore();
@@ -10,11 +10,13 @@ const msg = useMessage();
// 主题输入与 QoS 选择 // 主题输入与 QoS 选择
const topicInput = ref(''); const topicInput = ref('');
const qos = ref(2); const qos = ref(2);
const comment = ref('');
// 编辑状态 // 编辑状态
const editingTopicId = ref(null); const editingTopicId = ref(null);
const editingTopic = ref(''); const editingTopic = ref('');
const editingQos = ref(2); const editingQos = ref(2);
const editingComment = ref('');
// QoS 下拉选项 // QoS 下拉选项
const qosOptions = [ const qosOptions = [
@@ -84,11 +86,12 @@ async function handleAddTopic() {
return; return;
} }
let result = await store.addTopic(srv.id, topic, qos.value); let result = await store.addTopic(srv.id, topic, qos.value, comment.value.trim());
if (result) { if (result) {
msg.success('主题已添加' + (srv.status === 'connected' ? '并已订阅' : '')); msg.success('主题已添加' + (srv.status === 'connected' ? '并已订阅' : ''));
topicInput.value = ''; topicInput.value = '';
comment.value = '';
} }
} }
@@ -173,6 +176,7 @@ 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;
editingComment.value = t.comment ?? '';
} }
/** /**
@@ -182,6 +186,7 @@ function cancelEdit() {
editingTopicId.value = null; editingTopicId.value = null;
editingTopic.value = ''; editingTopic.value = '';
editingQos.value = 2; editingQos.value = 2;
editingComment.value = '';
} }
/** /**
@@ -213,7 +218,13 @@ async function handleEditSave(t) {
} }
let topicId = t.id || t.topic; let topicId = t.id || t.topic;
let result = await store.updateTopic(srv.id, topicId, topicName, editingQos.value); let result = await store.updateTopic(
srv.id,
topicId,
topicName,
editingQos.value,
editingComment.value.trim()
);
if (result) { if (result) {
msg.success('主题已更新'); msg.success('主题已更新');
@@ -258,6 +269,16 @@ async function handleEditSave(t) {
:options="qosOptions" :options="qosOptions"
/> />
</n-form-item> </n-form-item>
<n-form-item
class="comment-field"
label="备注"
>
<n-input
v-model:value="comment"
placeholder="可选备注"
@keydown.enter="handleAddTopic"
/>
</n-form-item>
<n-button <n-button
class="topic-submit" class="topic-submit"
type="primary" type="primary"
@@ -329,6 +350,11 @@ async function handleEditSave(t) {
:options="qosOptions" :options="qosOptions"
style="width: 160px; flex-shrink: 0" style="width: 160px; flex-shrink: 0"
/> />
<n-input
v-model:value="editingComment"
size="small"
placeholder="备注"
/>
<n-space <n-space
:size="4" :size="4"
:wrap="false" :wrap="false"
@@ -352,7 +378,20 @@ async function handleEditSave(t) {
v-else v-else
class="topic-name" class="topic-name"
> >
<n-ellipsis>{{ t.topic }}</n-ellipsis> <span
class="topic-label"
:title="t.topic"
>{{ t.topic }}</span>
<div
v-if="t.comment"
class="topic-comment"
>
<span class="topic-comment-prefix">备注</span>
<span
class="topic-comment-text"
:title="t.comment"
>{{ t.comment }}</span>
</div>
</div> </div>
<template #suffix> <template #suffix>
<n-space <n-space
@@ -443,12 +482,18 @@ async function handleEditSave(t) {
width: 160px; width: 160px;
} }
.comment-field {
flex: 1 1 180px;
width: auto;
}
.topic-submit { .topic-submit {
flex: 0 0 auto; flex: 0 0 auto;
} }
.topic-field :deep(.n-form-item-blank), .topic-field :deep(.n-form-item-blank),
.qos-field :deep(.n-form-item-blank) { .qos-field :deep(.n-form-item-blank),
.comment-field :deep(.n-form-item-blank) {
width: 100%; width: 100%;
} }
@@ -460,12 +505,51 @@ async function handleEditSave(t) {
} }
.topic-name { .topic-name {
min-width: 0; display: flex;
flex: 1; flex: 1;
font-family: monospace; flex-direction: column;
align-items: stretch;
min-width: 0;
gap: 5px;
font-size: 13px; font-size: 13px;
} }
.topic-label {
min-width: 0;
overflow: hidden;
font-family: monospace;
text-overflow: ellipsis;
white-space: nowrap;
}
.topic-comment {
display: flex;
align-items: center;
min-width: 0;
max-width: 100%;
padding: 2px 6px;
gap: 5px;
border: 1px solid rgba(14, 165, 160, 0.16);
border-radius: 4px;
background: rgba(20, 184, 166, 0.06);
color: var(--n-text-color-3);
font-size: 12px;
line-height: 18px;
}
.topic-comment-prefix {
flex-shrink: 0;
color: #0f766e;
}
.topic-comment-text {
flex: 1;
min-width: 0;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.topic-actions { .topic-actions {
flex-shrink: 0; flex-shrink: 0;
} }
+4
View File
@@ -17,6 +17,8 @@ export interface MqttTopic {
topic: string; topic: string;
/** 订阅 QoS 等级 */ /** 订阅 QoS 等级 */
qos: MqttQoS; qos: MqttQoS;
/** 主题备注,空字符串表示未备注 */
comment: string;
/** 当前是否已订阅 */ /** 当前是否已订阅 */
subscribed: boolean; subscribed: boolean;
} }
@@ -29,6 +31,8 @@ export interface TopicConfig {
topic: string; topic: string;
/** 订阅 QoS 等级,默认 1 */ /** 订阅 QoS 等级,默认 1 */
qos?: MqttQoS; qos?: MqttQoS;
/** 主题备注,默认空字符串 */
comment?: string;
} }
/** MQTT 服务器配置(不含运行时状态) */ /** MQTT 服务器配置(不含运行时状态) */