Compare commits
4 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| f2b7af5824 | |||
| 0f89e142e9 | |||
| 9723495f12 | |||
| 4aaf584451 |
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "frost-mqtt-client",
|
"name": "frost-mqtt-client",
|
||||||
"version": "1.0.0",
|
"version": "1.0.1",
|
||||||
"description": "一个使用 AI 开发的多功能 MQTT 桌面客户端,基于 Electron + Vue 3 + Naive UI 构建。",
|
"description": "一个使用 AI 开发的多功能 MQTT 桌面客户端,基于 Electron + Vue 3 + Naive UI 构建。",
|
||||||
"main": "./out/main/index.js",
|
"main": "./out/main/index.js",
|
||||||
"author": "Frost-ZX",
|
"author": "Frost-ZX",
|
||||||
|
|||||||
@@ -77,6 +77,17 @@ export function registerIpcHandlers() {
|
|||||||
return mqttManager.importServers(serverList);
|
return mqttManager.importServers(serverList);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// ==================== 快速发送 ====================
|
||||||
|
|
||||||
|
ipcMain.handle('mqtt:get-quick-send-list', () => {
|
||||||
|
return mqttManager.getQuickSendList();
|
||||||
|
});
|
||||||
|
|
||||||
|
ipcMain.handle('mqtt:save-quick-send-list', (_, list) => {
|
||||||
|
mqttManager.saveQuickSendList(list);
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
|
||||||
// ==================== 连接管理 ====================
|
// ==================== 连接管理 ====================
|
||||||
|
|
||||||
ipcMain.handle('mqtt:connect', (_, id) => {
|
ipcMain.handle('mqtt:connect', (_, id) => {
|
||||||
|
|||||||
@@ -35,6 +35,7 @@ class MqttManager {
|
|||||||
this.listeners = new Set(); // IPC callback functions
|
this.listeners = new Set(); // IPC callback functions
|
||||||
this.msgIdCounter = 0; // 用于生成唯一消息 id
|
this.msgIdCounter = 0; // 用于生成唯一消息 id
|
||||||
this.dataPath = join(app.getPath('userData'), 'frost-mqtt-data.json');
|
this.dataPath = join(app.getPath('userData'), 'frost-mqtt-data.json');
|
||||||
|
this.quickSendList = [];
|
||||||
this.loadData();
|
this.loadData();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -101,6 +102,12 @@ class MqttManager {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (Array.isArray(data.quickSendList)) {
|
||||||
|
this.quickSendList = data.quickSendList.filter((item) => {
|
||||||
|
return item && typeof item.id === 'string' && typeof item.topic === 'string';
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
} else {
|
} else {
|
||||||
this.initDefaultServers();
|
this.initDefaultServers();
|
||||||
}
|
}
|
||||||
@@ -124,6 +131,7 @@ class MqttManager {
|
|||||||
})),
|
})),
|
||||||
messages: Object.fromEntries(this.messages),
|
messages: Object.fromEntries(this.messages),
|
||||||
publishHistory: Object.fromEntries(this.publishHistory),
|
publishHistory: Object.fromEntries(this.publishHistory),
|
||||||
|
quickSendList: this.quickSendList,
|
||||||
};
|
};
|
||||||
|
|
||||||
writeFileSync(this.dataPath, JSON.stringify(data, null, 2), 'utf-8');
|
writeFileSync(this.dataPath, JSON.stringify(data, null, 2), 'utf-8');
|
||||||
@@ -409,6 +417,30 @@ class MqttManager {
|
|||||||
return this.getServers();
|
return this.getServers();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ==================== 快速发送 ====================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取快速发送列表
|
||||||
|
* @returns {QuickSendItem[]} 快速发送项数组
|
||||||
|
*/
|
||||||
|
getQuickSendList() {
|
||||||
|
return this.quickSendList;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 保存快速发送列表
|
||||||
|
* @param {QuickSendItem[]} list - 快速发送项数组
|
||||||
|
*/
|
||||||
|
saveQuickSendList(list) {
|
||||||
|
if (!Array.isArray(list)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.quickSendList = list.filter((item) => {
|
||||||
|
return item && typeof item.id === 'string' && typeof item.topic === 'string';
|
||||||
|
});
|
||||||
|
this.saveData();
|
||||||
|
}
|
||||||
|
|
||||||
// ==================== 连接管理 ====================
|
// ==================== 连接管理 ====================
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -15,6 +15,10 @@ const api = {
|
|||||||
exportServers: () => ipcRenderer.invoke('mqtt:export-servers'),
|
exportServers: () => ipcRenderer.invoke('mqtt:export-servers'),
|
||||||
importServers: (serverList) => ipcRenderer.invoke('mqtt:import-servers', serverList),
|
importServers: (serverList) => ipcRenderer.invoke('mqtt:import-servers', serverList),
|
||||||
|
|
||||||
|
// 快速发送
|
||||||
|
getQuickSendList: () => ipcRenderer.invoke('mqtt:get-quick-send-list'),
|
||||||
|
saveQuickSendList: (list) => ipcRenderer.invoke('mqtt:save-quick-send-list', list),
|
||||||
|
|
||||||
// 连接管理
|
// 连接管理
|
||||||
connect: (id) => ipcRenderer.invoke('mqtt:connect', id),
|
connect: (id) => ipcRenderer.invoke('mqtt:connect', id),
|
||||||
disconnect: (id) => ipcRenderer.invoke('mqtt:disconnect', id),
|
disconnect: (id) => ipcRenderer.invoke('mqtt:disconnect', id),
|
||||||
|
|||||||
@@ -8,13 +8,14 @@ const store = useMqttStore();
|
|||||||
const msg = useMessage();
|
const msg = useMessage();
|
||||||
const dialog = useDialog();
|
const dialog = useDialog();
|
||||||
|
|
||||||
// localStorage 持久化 key
|
// 旧版 localStorage key,用于数据迁移
|
||||||
const storageKey = 'mqtt-client.quick-send-list';
|
const oldStorageKey = 'mqtt-client.quick-send-list';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @typedef {Object} QuickSendItem
|
* @typedef {Object} QuickSendItem
|
||||||
* @property {string} id
|
* @property {string} id
|
||||||
* @property {string} topic
|
* @property {string} topic
|
||||||
|
* @property {string} comment
|
||||||
* @property {string} payload
|
* @property {string} payload
|
||||||
* @property {0|1|2} qos
|
* @property {0|1|2} qos
|
||||||
* @property {boolean} retain
|
* @property {boolean} retain
|
||||||
@@ -24,6 +25,9 @@ const storageKey = 'mqtt-client.quick-send-list';
|
|||||||
/** @type {import('vue').Ref<QuickSendItem[]>} */
|
/** @type {import('vue').Ref<QuickSendItem[]>} */
|
||||||
const quickList = ref([]);
|
const quickList = ref([]);
|
||||||
|
|
||||||
|
// 列表是否已从本地文件加载完成
|
||||||
|
const quickListLoaded = ref(false);
|
||||||
|
|
||||||
// 表单弹窗显隐
|
// 表单弹窗显隐
|
||||||
const showModal = ref(false);
|
const showModal = ref(false);
|
||||||
const isEdit = ref(false);
|
const isEdit = ref(false);
|
||||||
@@ -31,30 +35,38 @@ const isEdit = ref(false);
|
|||||||
// 表单数据
|
// 表单数据
|
||||||
const formId = ref('');
|
const formId = ref('');
|
||||||
const formTopic = ref('');
|
const formTopic = ref('');
|
||||||
|
const formComment = ref('');
|
||||||
const formPayload = ref('');
|
const formPayload = ref('');
|
||||||
const formQos = ref(1);
|
const formQos = ref(1);
|
||||||
const formRetain = ref(false);
|
const formRetain = ref(false);
|
||||||
|
|
||||||
// 从 localStorage 恢复列表
|
// 从旧版 localStorage 迁移数据
|
||||||
try {
|
function migrateFromLocalStorage() {
|
||||||
|
try {
|
||||||
let saved = JSON.parse(localStorage.getItem(storageKey) || '[]');
|
let saved = JSON.parse(localStorage.getItem(oldStorageKey) || '[]');
|
||||||
|
if (Array.isArray(saved) && saved.length > 0) {
|
||||||
if (Array.isArray(saved)) {
|
|
||||||
quickList.value = saved.filter((item) => {
|
quickList.value = saved.filter((item) => {
|
||||||
return item && typeof item.id === 'string' && typeof item.topic === 'string';
|
return item && typeof item.id === 'string' && typeof item.topic === 'string';
|
||||||
});
|
});
|
||||||
|
window.api.saveQuickSendList(JSON.parse(JSON.stringify(quickList.value)));
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
// 忽略旧数据解析错误
|
||||||
|
} finally {
|
||||||
|
localStorage.removeItem(oldStorageKey);
|
||||||
}
|
}
|
||||||
|
|
||||||
} catch {
|
|
||||||
localStorage.removeItem(storageKey);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 列表变化时持久化
|
// 列表变化时持久化到本地文件
|
||||||
watch(
|
watch(
|
||||||
quickList,
|
quickList,
|
||||||
() => {
|
() => {
|
||||||
localStorage.setItem(storageKey, JSON.stringify(quickList.value));
|
if (!quickListLoaded.value) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
window.api.saveQuickSendList(JSON.parse(JSON.stringify(quickList.value))).catch((e) => {
|
||||||
|
console.error('保存快速发送列表失败:', e);
|
||||||
|
});
|
||||||
},
|
},
|
||||||
{ deep: true }
|
{ deep: true }
|
||||||
);
|
);
|
||||||
@@ -111,6 +123,7 @@ function handleAdd() {
|
|||||||
isEdit.value = false;
|
isEdit.value = false;
|
||||||
formId.value = '';
|
formId.value = '';
|
||||||
formTopic.value = '';
|
formTopic.value = '';
|
||||||
|
formComment.value = '';
|
||||||
formPayload.value = '';
|
formPayload.value = '';
|
||||||
formQos.value = 1;
|
formQos.value = 1;
|
||||||
formRetain.value = false;
|
formRetain.value = false;
|
||||||
@@ -125,6 +138,7 @@ function handleEdit(item) {
|
|||||||
isEdit.value = true;
|
isEdit.value = true;
|
||||||
formId.value = item.id;
|
formId.value = item.id;
|
||||||
formTopic.value = item.topic;
|
formTopic.value = item.topic;
|
||||||
|
formComment.value = item.comment || '';
|
||||||
formPayload.value = item.payload;
|
formPayload.value = item.payload;
|
||||||
formQos.value = item.qos;
|
formQos.value = item.qos;
|
||||||
formRetain.value = item.retain;
|
formRetain.value = item.retain;
|
||||||
@@ -143,6 +157,7 @@ function handleSave() {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let comment = formComment.value.trim();
|
||||||
let payload = formPayload.value;
|
let payload = formPayload.value;
|
||||||
let qos = formQos.value;
|
let qos = formQos.value;
|
||||||
let retain = formRetain.value;
|
let retain = formRetain.value;
|
||||||
@@ -155,6 +170,7 @@ function handleSave() {
|
|||||||
quickList.value[idx] = {
|
quickList.value[idx] = {
|
||||||
...quickList.value[idx],
|
...quickList.value[idx],
|
||||||
topic,
|
topic,
|
||||||
|
comment,
|
||||||
payload,
|
payload,
|
||||||
qos,
|
qos,
|
||||||
retain,
|
retain,
|
||||||
@@ -166,6 +182,7 @@ function handleSave() {
|
|||||||
quickList.value.push({
|
quickList.value.push({
|
||||||
id: generateId(),
|
id: generateId(),
|
||||||
topic,
|
topic,
|
||||||
|
comment,
|
||||||
payload,
|
payload,
|
||||||
qos,
|
qos,
|
||||||
retain,
|
retain,
|
||||||
@@ -461,7 +478,23 @@ function updateColumnCount(width) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(async () => {
|
||||||
|
try {
|
||||||
|
let list = await window.api.getQuickSendList();
|
||||||
|
if (Array.isArray(list)) {
|
||||||
|
quickList.value = list.filter((item) => {
|
||||||
|
return item && typeof item.id === 'string' && typeof item.topic === 'string';
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (quickList.value.length === 0) {
|
||||||
|
migrateFromLocalStorage();
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.error('加载快速发送列表失败:', e);
|
||||||
|
} finally {
|
||||||
|
quickListLoaded.value = true;
|
||||||
|
}
|
||||||
|
|
||||||
resizeObserver = new ResizeObserver((entries) => {
|
resizeObserver = new ResizeObserver((entries) => {
|
||||||
for (let entry of entries) {
|
for (let entry of entries) {
|
||||||
updateColumnCount(entry.contentRect.width);
|
updateColumnCount(entry.contentRect.width);
|
||||||
@@ -582,6 +615,11 @@ onUnmounted(() => {
|
|||||||
>删除</n-button>
|
>删除</n-button>
|
||||||
</n-space>
|
</n-space>
|
||||||
</div>
|
</div>
|
||||||
|
<div
|
||||||
|
v-if="item.comment"
|
||||||
|
class="quick-comment"
|
||||||
|
:title="item.comment"
|
||||||
|
>{{ item.comment }}</div>
|
||||||
<pre
|
<pre
|
||||||
v-if="item.payload"
|
v-if="item.payload"
|
||||||
class="quick-payload"
|
class="quick-payload"
|
||||||
@@ -616,6 +654,12 @@ onUnmounted(() => {
|
|||||||
placeholder="例如: sensor/device01/temperature"
|
placeholder="例如: sensor/device01/temperature"
|
||||||
/>
|
/>
|
||||||
</n-form-item>
|
</n-form-item>
|
||||||
|
<n-form-item label="备注">
|
||||||
|
<n-input
|
||||||
|
v-model:value="formComment"
|
||||||
|
placeholder="可选,用于标识该快速消息的用途"
|
||||||
|
/>
|
||||||
|
</n-form-item>
|
||||||
<n-grid
|
<n-grid
|
||||||
cols="2"
|
cols="2"
|
||||||
x-gap="12"
|
x-gap="12"
|
||||||
@@ -692,21 +736,21 @@ onUnmounted(() => {
|
|||||||
min-height: 0;
|
min-height: 0;
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: 1fr;
|
grid-template-columns: minmax(0, 1fr);
|
||||||
gap: 12px;
|
gap: 12px;
|
||||||
padding-right: 4px;
|
padding-right: 4px;
|
||||||
align-content: start;
|
align-content: start;
|
||||||
|
|
||||||
&.columns-1 {
|
&.columns-1 {
|
||||||
grid-template-columns: 1fr;
|
grid-template-columns: minmax(0, 1fr);
|
||||||
}
|
}
|
||||||
|
|
||||||
&.columns-2 {
|
&.columns-2 {
|
||||||
grid-template-columns: repeat(2, 1fr);
|
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||||
}
|
}
|
||||||
|
|
||||||
&.columns-3 {
|
&.columns-3 {
|
||||||
grid-template-columns: repeat(3, 1fr);
|
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -801,6 +845,15 @@ onUnmounted(() => {
|
|||||||
font-family: monospace;
|
font-family: monospace;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.quick-comment {
|
||||||
|
margin-top: 8px;
|
||||||
|
color: var(--frost-color-text-secondary);
|
||||||
|
font-size: 12px;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
.quick-payload {
|
.quick-payload {
|
||||||
margin: 10px 0 0;
|
margin: 10px 0 0;
|
||||||
padding: 8px 10px;
|
padding: 8px 10px;
|
||||||
|
|||||||
@@ -1,5 +1,16 @@
|
|||||||
# CHANGELOG
|
# CHANGELOG
|
||||||
|
|
||||||
|
## [1.0.1] - 2026-07-21
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- 快速发送列表持久化到本地文件 `frost-mqtt-data.json`。
|
||||||
|
- 快速消息支持备注。
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
- 解决“快速发送”模块中,主题名称较长时,外层卡片缩小到一定程度后不能再随外层容器缩小而缩小的问题。
|
||||||
|
|
||||||
## [1.0.0] - 2026-07-21
|
## [1.0.0] - 2026-07-21
|
||||||
|
|
||||||
- 初始版本发布。
|
- 初始版本发布。
|
||||||
|
|||||||
Reference in New Issue
Block a user