Compare commits
16 Commits
089bfaacf7
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| f2b7af5824 | |||
| 0f89e142e9 | |||
| 9723495f12 | |||
| 4aaf584451 | |||
| 330a9a48fd | |||
| e233911d5e | |||
| 821b895d04 | |||
| 6a60b4daf4 | |||
| e491dd78bf | |||
| 7ec0ad36ba | |||
| 0828edf6b1 | |||
| 7eb62ca37f | |||
| cd2f926ba7 | |||
| 515fc2158b | |||
| a1106d4ce6 | |||
| b23cf4d854 |
@@ -0,0 +1,24 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"target": "ES2022",
|
||||||
|
"module": "ESNext",
|
||||||
|
"moduleResolution": "bundler",
|
||||||
|
"allowJs": true,
|
||||||
|
"checkJs": false,
|
||||||
|
"noEmit": true,
|
||||||
|
"strict": false,
|
||||||
|
"baseUrl": ".",
|
||||||
|
"paths": {
|
||||||
|
"@types/*": ["types/*"]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"include": [
|
||||||
|
"src/**/*",
|
||||||
|
"types/**/*"
|
||||||
|
],
|
||||||
|
"exclude": [
|
||||||
|
"node_modules",
|
||||||
|
"dist",
|
||||||
|
"out"
|
||||||
|
]
|
||||||
|
}
|
||||||
+2
-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",
|
||||||
@@ -28,6 +28,7 @@
|
|||||||
"electron-builder": "^26.0.12",
|
"electron-builder": "^26.0.12",
|
||||||
"electron-vite": "^5.0.0",
|
"electron-vite": "^5.0.0",
|
||||||
"eslint": "^9.39.1",
|
"eslint": "^9.39.1",
|
||||||
|
"less": "^4.7.0",
|
||||||
"eslint-plugin-vue": "^10.6.2",
|
"eslint-plugin-vue": "^10.6.2",
|
||||||
"naive-ui": "^2.44.1",
|
"naive-ui": "^2.44.1",
|
||||||
"vite": "^7.2.6",
|
"vite": "^7.2.6",
|
||||||
|
|||||||
Generated
+185
-8
@@ -23,7 +23,7 @@ importers:
|
|||||||
version: 2.1.0(eslint@9.39.5(jiti@2.7.0))
|
version: 2.1.0(eslint@9.39.5(jiti@2.7.0))
|
||||||
'@vitejs/plugin-vue':
|
'@vitejs/plugin-vue':
|
||||||
specifier: ^6.0.2
|
specifier: ^6.0.2
|
||||||
version: 6.0.8(vite@7.3.6(@types/node@26.1.1)(jiti@2.7.0))(vue@3.5.39)
|
version: 6.0.8(vite@7.3.6(@types/node@26.1.1)(jiti@2.7.0)(less@4.7.0))(vue@3.5.39)
|
||||||
electron:
|
electron:
|
||||||
specifier: ^39.2.6
|
specifier: ^39.2.6
|
||||||
version: 39.8.10
|
version: 39.8.10
|
||||||
@@ -32,19 +32,22 @@ importers:
|
|||||||
version: 26.15.3(electron-builder-squirrel-windows@26.15.3)
|
version: 26.15.3(electron-builder-squirrel-windows@26.15.3)
|
||||||
electron-vite:
|
electron-vite:
|
||||||
specifier: ^5.0.0
|
specifier: ^5.0.0
|
||||||
version: 5.0.0(vite@7.3.6(@types/node@26.1.1)(jiti@2.7.0))
|
version: 5.0.0(vite@7.3.6(@types/node@26.1.1)(jiti@2.7.0)(less@4.7.0))
|
||||||
eslint:
|
eslint:
|
||||||
specifier: ^9.39.1
|
specifier: ^9.39.1
|
||||||
version: 9.39.5(jiti@2.7.0)
|
version: 9.39.5(jiti@2.7.0)
|
||||||
eslint-plugin-vue:
|
eslint-plugin-vue:
|
||||||
specifier: ^10.6.2
|
specifier: ^10.6.2
|
||||||
version: 10.9.2(eslint@9.39.5(jiti@2.7.0))(vue-eslint-parser@10.4.1(eslint@9.39.5(jiti@2.7.0)))
|
version: 10.9.2(eslint@9.39.5(jiti@2.7.0))(vue-eslint-parser@10.4.1(eslint@9.39.5(jiti@2.7.0)))
|
||||||
|
less:
|
||||||
|
specifier: ^4.7.0
|
||||||
|
version: 4.7.0
|
||||||
naive-ui:
|
naive-ui:
|
||||||
specifier: ^2.44.1
|
specifier: ^2.44.1
|
||||||
version: 2.44.1(vue@3.5.39)
|
version: 2.44.1(vue@3.5.39)
|
||||||
vite:
|
vite:
|
||||||
specifier: ^7.2.6
|
specifier: ^7.2.6
|
||||||
version: 7.3.6(@types/node@26.1.1)(jiti@2.7.0)
|
version: 7.3.6(@types/node@26.1.1)(jiti@2.7.0)(less@4.7.0)
|
||||||
vue:
|
vue:
|
||||||
specifier: ^3.5.25
|
specifier: ^3.5.25
|
||||||
version: 3.5.39
|
version: 3.5.39
|
||||||
@@ -1088,6 +1091,10 @@ packages:
|
|||||||
convert-source-map@2.0.0:
|
convert-source-map@2.0.0:
|
||||||
resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==}
|
resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==}
|
||||||
|
|
||||||
|
copy-anything@3.0.5:
|
||||||
|
resolution: {integrity: sha512-yCEafptTtb4bk7GLEQoM8KVJpxAfdBJYaXyzQEgQQQgYrZiDp8SJmGKlYza6CYjEDNstAdNdKA3UuoULlEbS6w==}
|
||||||
|
engines: {node: '>=12.13'}
|
||||||
|
|
||||||
core-util-is@1.0.3:
|
core-util-is@1.0.3:
|
||||||
resolution: {integrity: sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==}
|
resolution: {integrity: sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==}
|
||||||
|
|
||||||
@@ -1120,6 +1127,22 @@ packages:
|
|||||||
date-fns@4.4.0:
|
date-fns@4.4.0:
|
||||||
resolution: {integrity: sha512-+1UMbeh68lH1SegH83CGWwpb6OHHbpSgr3+s5Eww5M4CAgswBpoWS0AjTOfEJ33HiYKz1hdj/KTFprzXHmq/6w==}
|
resolution: {integrity: sha512-+1UMbeh68lH1SegH83CGWwpb6OHHbpSgr3+s5Eww5M4CAgswBpoWS0AjTOfEJ33HiYKz1hdj/KTFprzXHmq/6w==}
|
||||||
|
|
||||||
|
debug@2.6.9:
|
||||||
|
resolution: {integrity: sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==}
|
||||||
|
peerDependencies:
|
||||||
|
supports-color: '*'
|
||||||
|
peerDependenciesMeta:
|
||||||
|
supports-color:
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
debug@3.2.7:
|
||||||
|
resolution: {integrity: sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==}
|
||||||
|
peerDependencies:
|
||||||
|
supports-color: '*'
|
||||||
|
peerDependenciesMeta:
|
||||||
|
supports-color:
|
||||||
|
optional: true
|
||||||
|
|
||||||
debug@4.4.3:
|
debug@4.4.3:
|
||||||
resolution: {integrity: sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==}
|
resolution: {integrity: sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==}
|
||||||
engines: {node: '>=6.0'}
|
engines: {node: '>=6.0'}
|
||||||
@@ -1232,6 +1255,10 @@ packages:
|
|||||||
err-code@2.0.3:
|
err-code@2.0.3:
|
||||||
resolution: {integrity: sha512-2bmlRpNKBxT/CRmPOlyISQpNj+qSeYvcym/uT0Jx2bMOlKLtSy1ZmLuVxSEKKyor/N5yhvp/ZiG1oE3DEYMSFA==}
|
resolution: {integrity: sha512-2bmlRpNKBxT/CRmPOlyISQpNj+qSeYvcym/uT0Jx2bMOlKLtSy1ZmLuVxSEKKyor/N5yhvp/ZiG1oE3DEYMSFA==}
|
||||||
|
|
||||||
|
errno@0.1.8:
|
||||||
|
resolution: {integrity: sha512-dJ6oBr5SQ1VSd9qkk7ByRgb/1SH4JZjCHSW/mr63/QcXO9zLVxvJ6Oy13nio03rxpSnVDDjFor75SjVeZWPW/A==}
|
||||||
|
hasBin: true
|
||||||
|
|
||||||
es-define-property@1.0.1:
|
es-define-property@1.0.1:
|
||||||
resolution: {integrity: sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==}
|
resolution: {integrity: sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==}
|
||||||
engines: {node: '>= 0.4'}
|
engines: {node: '>= 0.4'}
|
||||||
@@ -1544,6 +1571,14 @@ packages:
|
|||||||
resolution: {integrity: sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==}
|
resolution: {integrity: sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==}
|
||||||
engines: {node: '>= 14'}
|
engines: {node: '>= 14'}
|
||||||
|
|
||||||
|
iconv-lite@0.4.24:
|
||||||
|
resolution: {integrity: sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==}
|
||||||
|
engines: {node: '>=0.10.0'}
|
||||||
|
|
||||||
|
iconv-lite@0.6.3:
|
||||||
|
resolution: {integrity: sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==}
|
||||||
|
engines: {node: '>=0.10.0'}
|
||||||
|
|
||||||
ieee754@1.2.1:
|
ieee754@1.2.1:
|
||||||
resolution: {integrity: sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==}
|
resolution: {integrity: sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==}
|
||||||
|
|
||||||
@@ -1582,6 +1617,10 @@ packages:
|
|||||||
resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==}
|
resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==}
|
||||||
engines: {node: '>=0.10.0'}
|
engines: {node: '>=0.10.0'}
|
||||||
|
|
||||||
|
is-what@4.1.16:
|
||||||
|
resolution: {integrity: sha512-ZhMwEosbFJkA0YhFnNDgTM4ZxDRsS6HqTo7qsZM08fehyRYIYa0yHu5R6mgo1n/8MgaPBXiPimPD77baVFYg+A==}
|
||||||
|
engines: {node: '>=12.13'}
|
||||||
|
|
||||||
isarray@1.0.0:
|
isarray@1.0.0:
|
||||||
resolution: {integrity: sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==}
|
resolution: {integrity: sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==}
|
||||||
|
|
||||||
@@ -1660,6 +1699,11 @@ packages:
|
|||||||
lazy-val@1.0.5:
|
lazy-val@1.0.5:
|
||||||
resolution: {integrity: sha512-0/BnGCCfyUMkBpeDgWihanIAF9JmZhHBgUhEqzvf+adhNGLoP6TaiI5oF8oyb3I45P+PcnrqihSf01M0l0G5+Q==}
|
resolution: {integrity: sha512-0/BnGCCfyUMkBpeDgWihanIAF9JmZhHBgUhEqzvf+adhNGLoP6TaiI5oF8oyb3I45P+PcnrqihSf01M0l0G5+Q==}
|
||||||
|
|
||||||
|
less@4.7.0:
|
||||||
|
resolution: {integrity: sha512-i7dAlT3+boO0mMh1G4cex0vz1lLAScmBbikm1VEDNv+cy0ore1CUo2UtX8m3N9QLE5WYDr4ISbiCRzHNGyFkrA==}
|
||||||
|
engines: {node: '>=18'}
|
||||||
|
hasBin: true
|
||||||
|
|
||||||
levn@0.4.1:
|
levn@0.4.1:
|
||||||
resolution: {integrity: sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==}
|
resolution: {integrity: sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==}
|
||||||
engines: {node: '>= 0.8.0'}
|
engines: {node: '>= 0.8.0'}
|
||||||
@@ -1694,6 +1738,10 @@ packages:
|
|||||||
magic-string@0.30.21:
|
magic-string@0.30.21:
|
||||||
resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==}
|
resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==}
|
||||||
|
|
||||||
|
make-dir@5.1.0:
|
||||||
|
resolution: {integrity: sha512-IfpFq6UM39dUNiphpA6uDezNx/AvWyhwfICWPR3t1VspkgkMZrL+Rk1RbN1bx+aeNYwOrqGJgEgV3yotk+ZUVw==}
|
||||||
|
engines: {node: '>=18'}
|
||||||
|
|
||||||
matcher@3.0.0:
|
matcher@3.0.0:
|
||||||
resolution: {integrity: sha512-OkeDaAZ/bQCxeFAozM55PKcKU0yJMPGifLwV4Qgjitu+5MoAfSQN4lsLJeXZ1b8w0x+/Emda6MZgXS1jvsapng==}
|
resolution: {integrity: sha512-OkeDaAZ/bQCxeFAozM55PKcKU0yJMPGifLwV4Qgjitu+5MoAfSQN4lsLJeXZ1b8w0x+/Emda6MZgXS1jvsapng==}
|
||||||
engines: {node: '>=10'}
|
engines: {node: '>=10'}
|
||||||
@@ -1710,6 +1758,11 @@ packages:
|
|||||||
resolution: {integrity: sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==}
|
resolution: {integrity: sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==}
|
||||||
engines: {node: '>= 0.6'}
|
engines: {node: '>= 0.6'}
|
||||||
|
|
||||||
|
mime@1.6.0:
|
||||||
|
resolution: {integrity: sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==}
|
||||||
|
engines: {node: '>=4'}
|
||||||
|
hasBin: true
|
||||||
|
|
||||||
mime@2.6.0:
|
mime@2.6.0:
|
||||||
resolution: {integrity: sha512-USPkMeET31rOMiarsBNIHZKLGgvKc/LrjofAnBlOttf5ajRvqiRA8QsenbcooctK6d6Ts6aqZXBA+XbkKthiQg==}
|
resolution: {integrity: sha512-USPkMeET31rOMiarsBNIHZKLGgvKc/LrjofAnBlOttf5ajRvqiRA8QsenbcooctK6d6Ts6aqZXBA+XbkKthiQg==}
|
||||||
engines: {node: '>=4.0.0'}
|
engines: {node: '>=4.0.0'}
|
||||||
@@ -1761,6 +1814,9 @@ packages:
|
|||||||
engines: {node: '>=16.0.0'}
|
engines: {node: '>=16.0.0'}
|
||||||
hasBin: true
|
hasBin: true
|
||||||
|
|
||||||
|
ms@2.0.0:
|
||||||
|
resolution: {integrity: sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==}
|
||||||
|
|
||||||
ms@2.1.3:
|
ms@2.1.3:
|
||||||
resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==}
|
resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==}
|
||||||
|
|
||||||
@@ -1778,6 +1834,16 @@ packages:
|
|||||||
natural-compare@1.4.0:
|
natural-compare@1.4.0:
|
||||||
resolution: {integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==}
|
resolution: {integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==}
|
||||||
|
|
||||||
|
needle@2.9.1:
|
||||||
|
resolution: {integrity: sha512-6R9fqJ5Zcmf+uYaFgdIHmLwNldn5HbK8L5ybn7Uz+ylX/rnOsSp1AHcvQSrCaFN+qNM1wpymHqD7mVasEOlHGQ==}
|
||||||
|
engines: {node: '>= 4.4.x'}
|
||||||
|
hasBin: true
|
||||||
|
|
||||||
|
needle@3.5.0:
|
||||||
|
resolution: {integrity: sha512-jaQyPKKk2YokHrEg+vFDYxXIHTCBgiZwSHOoVx/8V3GIBS8/VN6NdVRmg8q1ERtPkMvmOvebsgga4sAj5hls/w==}
|
||||||
|
engines: {node: '>= 4.4.x'}
|
||||||
|
hasBin: true
|
||||||
|
|
||||||
node-abi@4.33.0:
|
node-abi@4.33.0:
|
||||||
resolution: {integrity: sha512-vLBWCKb+7LWsX+TbfzWOkw0W81m377tyx3hOweBTjO43CXZnRGS1/JPWs20fr0PgZyDXk6ROYrylsEycK8raDA==}
|
resolution: {integrity: sha512-vLBWCKb+7LWsX+TbfzWOkw0W81m377tyx3hOweBTjO43CXZnRGS1/JPWs20fr0PgZyDXk6ROYrylsEycK8raDA==}
|
||||||
engines: {node: '>=22.12.0'}
|
engines: {node: '>=22.12.0'}
|
||||||
@@ -1839,6 +1905,10 @@ packages:
|
|||||||
resolution: {integrity: sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==}
|
resolution: {integrity: sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==}
|
||||||
engines: {node: '>=6'}
|
engines: {node: '>=6'}
|
||||||
|
|
||||||
|
parse-node-version@1.0.1:
|
||||||
|
resolution: {integrity: sha512-3YHlOa/JgH6Mnpr05jP9eDG254US9ek25LyIxZlDItp2iJtwyaXQb57lBYLdT3MowkUFYEV2XXNAYIPlESvJlA==}
|
||||||
|
engines: {node: '>= 0.10'}
|
||||||
|
|
||||||
path-exists@4.0.0:
|
path-exists@4.0.0:
|
||||||
resolution: {integrity: sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==}
|
resolution: {integrity: sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==}
|
||||||
engines: {node: '>=8'}
|
engines: {node: '>=8'}
|
||||||
@@ -1890,6 +1960,9 @@ packages:
|
|||||||
resolution: {integrity: sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==}
|
resolution: {integrity: sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==}
|
||||||
engines: {node: '>= 0.8.0'}
|
engines: {node: '>= 0.8.0'}
|
||||||
|
|
||||||
|
probe-image-size@7.3.0:
|
||||||
|
resolution: {integrity: sha512-7CaDeBwiAbh6ohXsvLbAZhO7wzsZAmaevfxe39qvCwRh8LyaZfDlBGGLU1CCTgrTLtCOdwBBhjOrIHaIIimHfQ==}
|
||||||
|
|
||||||
proc-log@6.1.0:
|
proc-log@6.1.0:
|
||||||
resolution: {integrity: sha512-iG+GYldRf2BQ0UDUAd6JQ/RwzaQy6mXmsk/IzlYyal4A4SNFw54MeH4/tLkF4I5WoWG9SQwuqWzS99jaFQHBuQ==}
|
resolution: {integrity: sha512-iG+GYldRf2BQ0UDUAd6JQ/RwzaQy6mXmsk/IzlYyal4A4SNFw54MeH4/tLkF4I5WoWG9SQwuqWzS99jaFQHBuQ==}
|
||||||
engines: {node: ^20.17.0 || >=22.9.0}
|
engines: {node: ^20.17.0 || >=22.9.0}
|
||||||
@@ -1912,6 +1985,9 @@ packages:
|
|||||||
proper-lockfile@4.1.2:
|
proper-lockfile@4.1.2:
|
||||||
resolution: {integrity: sha512-TjNPblN4BwAWMXU8s9AEz4JmQxnD1NNL7bNOY/AKUzyamc379FWASUhc/K1pL2noVb+XmZKLL68cjzLsiOAMaA==}
|
resolution: {integrity: sha512-TjNPblN4BwAWMXU8s9AEz4JmQxnD1NNL7bNOY/AKUzyamc379FWASUhc/K1pL2noVb+XmZKLL68cjzLsiOAMaA==}
|
||||||
|
|
||||||
|
prr@1.0.1:
|
||||||
|
resolution: {integrity: sha512-yPw4Sng1gWghHQWj0B3ZggWUm4qVbPwPFcRG8KyxiU7J2OHFSoEHKS+EZ3fv5l1t9CyCiop6l/ZYeWbrgoQejw==}
|
||||||
|
|
||||||
pump@3.0.4:
|
pump@3.0.4:
|
||||||
resolution: {integrity: sha512-VS7sjc6KR7e1ukRFhQSY5LM2uBWAUPiOPa/A3mkKmiMwSmRFUITt0xuj+/lesgnCv+dPIEYlkzrcyXgquIHMcA==}
|
resolution: {integrity: sha512-VS7sjc6KR7e1ukRFhQSY5LM2uBWAUPiOPa/A3mkKmiMwSmRFUITt0xuj+/lesgnCv+dPIEYlkzrcyXgquIHMcA==}
|
||||||
|
|
||||||
@@ -1994,6 +2070,9 @@ packages:
|
|||||||
safe-buffer@5.2.1:
|
safe-buffer@5.2.1:
|
||||||
resolution: {integrity: sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==}
|
resolution: {integrity: sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==}
|
||||||
|
|
||||||
|
safer-buffer@2.1.2:
|
||||||
|
resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==}
|
||||||
|
|
||||||
sanitize-filename@1.6.4:
|
sanitize-filename@1.6.4:
|
||||||
resolution: {integrity: sha512-9ZyI08PsvdQl2r/bBIGubpVdR3RR9sY6RDiWFPreA21C/EFlQhmgo20UZlNjZMMZNubusLhAQozkA0Od5J21Eg==}
|
resolution: {integrity: sha512-9ZyI08PsvdQl2r/bBIGubpVdR3RR9sY6RDiWFPreA21C/EFlQhmgo20UZlNjZMMZNubusLhAQozkA0Od5J21Eg==}
|
||||||
|
|
||||||
@@ -2074,6 +2153,9 @@ packages:
|
|||||||
resolution: {integrity: sha512-jH9EhtKIjuXZ2cWxmXS8ZP80XyC3iasQxMDV8jzhNJpfDb7VbQLVW4Wvsxz9QZvzV+G4YoSfBUVKDOyxLzi/sg==}
|
resolution: {integrity: sha512-jH9EhtKIjuXZ2cWxmXS8ZP80XyC3iasQxMDV8jzhNJpfDb7VbQLVW4Wvsxz9QZvzV+G4YoSfBUVKDOyxLzi/sg==}
|
||||||
engines: {node: '>= 6'}
|
engines: {node: '>= 6'}
|
||||||
|
|
||||||
|
stream-parser@0.3.1:
|
||||||
|
resolution: {integrity: sha512-bJ/HgKq41nlKvlhccD5kaCr/P+Hu0wPNKPJOH7en+YrJu/9EgqUF+88w5Jb6KNcjOFMhfX4B2asfeAtIGuHObQ==}
|
||||||
|
|
||||||
string-width@4.2.3:
|
string-width@4.2.3:
|
||||||
resolution: {integrity: sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==}
|
resolution: {integrity: sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==}
|
||||||
engines: {node: '>=8'}
|
engines: {node: '>=8'}
|
||||||
@@ -2993,10 +3075,10 @@ snapshots:
|
|||||||
'@types/node': 22.20.1
|
'@types/node': 22.20.1
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
'@vitejs/plugin-vue@6.0.8(vite@7.3.6(@types/node@26.1.1)(jiti@2.7.0))(vue@3.5.39)':
|
'@vitejs/plugin-vue@6.0.8(vite@7.3.6(@types/node@26.1.1)(jiti@2.7.0)(less@4.7.0))(vue@3.5.39)':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@rolldown/pluginutils': 1.0.1
|
'@rolldown/pluginutils': 1.0.1
|
||||||
vite: 7.3.6(@types/node@26.1.1)(jiti@2.7.0)
|
vite: 7.3.6(@types/node@26.1.1)(jiti@2.7.0)(less@4.7.0)
|
||||||
vue: 3.5.39
|
vue: 3.5.39
|
||||||
|
|
||||||
'@vue/compiler-core@3.5.39':
|
'@vue/compiler-core@3.5.39':
|
||||||
@@ -3320,6 +3402,10 @@ snapshots:
|
|||||||
|
|
||||||
convert-source-map@2.0.0: {}
|
convert-source-map@2.0.0: {}
|
||||||
|
|
||||||
|
copy-anything@3.0.5:
|
||||||
|
dependencies:
|
||||||
|
is-what: 4.1.16
|
||||||
|
|
||||||
core-util-is@1.0.3: {}
|
core-util-is@1.0.3: {}
|
||||||
|
|
||||||
cross-dirname@0.1.0:
|
cross-dirname@0.1.0:
|
||||||
@@ -3348,6 +3434,16 @@ snapshots:
|
|||||||
|
|
||||||
date-fns@4.4.0: {}
|
date-fns@4.4.0: {}
|
||||||
|
|
||||||
|
debug@2.6.9:
|
||||||
|
dependencies:
|
||||||
|
ms: 2.0.0
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
debug@3.2.7:
|
||||||
|
dependencies:
|
||||||
|
ms: 2.1.3
|
||||||
|
optional: true
|
||||||
|
|
||||||
debug@4.4.3:
|
debug@4.4.3:
|
||||||
dependencies:
|
dependencies:
|
||||||
ms: 2.1.3
|
ms: 2.1.3
|
||||||
@@ -3455,7 +3551,7 @@ snapshots:
|
|||||||
|
|
||||||
electron-to-chromium@1.5.392: {}
|
electron-to-chromium@1.5.392: {}
|
||||||
|
|
||||||
electron-vite@5.0.0(vite@7.3.6(@types/node@26.1.1)(jiti@2.7.0)):
|
electron-vite@5.0.0(vite@7.3.6(@types/node@26.1.1)(jiti@2.7.0)(less@4.7.0)):
|
||||||
dependencies:
|
dependencies:
|
||||||
'@babel/core': 7.29.7
|
'@babel/core': 7.29.7
|
||||||
'@babel/plugin-transform-arrow-functions': 7.29.7(@babel/core@7.29.7)
|
'@babel/plugin-transform-arrow-functions': 7.29.7(@babel/core@7.29.7)
|
||||||
@@ -3463,7 +3559,7 @@ snapshots:
|
|||||||
esbuild: 0.25.12
|
esbuild: 0.25.12
|
||||||
magic-string: 0.30.21
|
magic-string: 0.30.21
|
||||||
picocolors: 1.1.1
|
picocolors: 1.1.1
|
||||||
vite: 7.3.6(@types/node@26.1.1)(jiti@2.7.0)
|
vite: 7.3.6(@types/node@26.1.1)(jiti@2.7.0)(less@4.7.0)
|
||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- supports-color
|
- supports-color
|
||||||
|
|
||||||
@@ -3499,6 +3595,11 @@ snapshots:
|
|||||||
|
|
||||||
err-code@2.0.3: {}
|
err-code@2.0.3: {}
|
||||||
|
|
||||||
|
errno@0.1.8:
|
||||||
|
dependencies:
|
||||||
|
prr: 1.0.1
|
||||||
|
optional: true
|
||||||
|
|
||||||
es-define-property@1.0.1: {}
|
es-define-property@1.0.1: {}
|
||||||
|
|
||||||
es-errors@1.3.0: {}
|
es-errors@1.3.0: {}
|
||||||
@@ -3909,6 +4010,16 @@ snapshots:
|
|||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- supports-color
|
- supports-color
|
||||||
|
|
||||||
|
iconv-lite@0.4.24:
|
||||||
|
dependencies:
|
||||||
|
safer-buffer: 2.1.2
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
iconv-lite@0.6.3:
|
||||||
|
dependencies:
|
||||||
|
safer-buffer: 2.1.2
|
||||||
|
optional: true
|
||||||
|
|
||||||
ieee754@1.2.1: {}
|
ieee754@1.2.1: {}
|
||||||
|
|
||||||
ignore@5.3.2: {}
|
ignore@5.3.2: {}
|
||||||
@@ -3937,6 +4048,8 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
is-extglob: 2.1.1
|
is-extglob: 2.1.1
|
||||||
|
|
||||||
|
is-what@4.1.16: {}
|
||||||
|
|
||||||
isarray@1.0.0: {}
|
isarray@1.0.0: {}
|
||||||
|
|
||||||
isbinaryfile@4.0.10: {}
|
isbinaryfile@4.0.10: {}
|
||||||
@@ -3996,6 +4109,21 @@ snapshots:
|
|||||||
|
|
||||||
lazy-val@1.0.5: {}
|
lazy-val@1.0.5: {}
|
||||||
|
|
||||||
|
less@4.7.0:
|
||||||
|
dependencies:
|
||||||
|
copy-anything: 3.0.5
|
||||||
|
parse-node-version: 1.0.1
|
||||||
|
optionalDependencies:
|
||||||
|
errno: 0.1.8
|
||||||
|
graceful-fs: 4.2.11
|
||||||
|
make-dir: 5.1.0
|
||||||
|
mime: 1.6.0
|
||||||
|
needle: 3.5.0
|
||||||
|
probe-image-size: 7.3.0
|
||||||
|
source-map: 0.6.1
|
||||||
|
transitivePeerDependencies:
|
||||||
|
- supports-color
|
||||||
|
|
||||||
levn@0.4.1:
|
levn@0.4.1:
|
||||||
dependencies:
|
dependencies:
|
||||||
prelude-ls: 1.2.1
|
prelude-ls: 1.2.1
|
||||||
@@ -4027,6 +4155,9 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
'@jridgewell/sourcemap-codec': 1.5.5
|
'@jridgewell/sourcemap-codec': 1.5.5
|
||||||
|
|
||||||
|
make-dir@5.1.0:
|
||||||
|
optional: true
|
||||||
|
|
||||||
matcher@3.0.0:
|
matcher@3.0.0:
|
||||||
dependencies:
|
dependencies:
|
||||||
escape-string-regexp: 4.0.0
|
escape-string-regexp: 4.0.0
|
||||||
@@ -4040,6 +4171,9 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
mime-db: 1.52.0
|
mime-db: 1.52.0
|
||||||
|
|
||||||
|
mime@1.6.0:
|
||||||
|
optional: true
|
||||||
|
|
||||||
mime@2.6.0: {}
|
mime@2.6.0: {}
|
||||||
|
|
||||||
mimic-response@1.0.1: {}
|
mimic-response@1.0.1: {}
|
||||||
@@ -4105,6 +4239,9 @@ snapshots:
|
|||||||
- supports-color
|
- supports-color
|
||||||
- utf-8-validate
|
- utf-8-validate
|
||||||
|
|
||||||
|
ms@2.0.0:
|
||||||
|
optional: true
|
||||||
|
|
||||||
ms@2.1.3: {}
|
ms@2.1.3: {}
|
||||||
|
|
||||||
naive-ui@2.44.1(vue@3.5.39):
|
naive-ui@2.44.1(vue@3.5.39):
|
||||||
@@ -4133,6 +4270,21 @@ snapshots:
|
|||||||
|
|
||||||
natural-compare@1.4.0: {}
|
natural-compare@1.4.0: {}
|
||||||
|
|
||||||
|
needle@2.9.1:
|
||||||
|
dependencies:
|
||||||
|
debug: 3.2.7
|
||||||
|
iconv-lite: 0.4.24
|
||||||
|
sax: 1.6.0
|
||||||
|
transitivePeerDependencies:
|
||||||
|
- supports-color
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
needle@3.5.0:
|
||||||
|
dependencies:
|
||||||
|
iconv-lite: 0.6.3
|
||||||
|
sax: 1.6.0
|
||||||
|
optional: true
|
||||||
|
|
||||||
node-abi@4.33.0:
|
node-abi@4.33.0:
|
||||||
dependencies:
|
dependencies:
|
||||||
semver: 7.7.4
|
semver: 7.7.4
|
||||||
@@ -4205,6 +4357,8 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
callsites: 3.1.0
|
callsites: 3.1.0
|
||||||
|
|
||||||
|
parse-node-version@1.0.1: {}
|
||||||
|
|
||||||
path-exists@4.0.0: {}
|
path-exists@4.0.0: {}
|
||||||
|
|
||||||
path-is-absolute@1.0.1: {}
|
path-is-absolute@1.0.1: {}
|
||||||
@@ -4252,6 +4406,15 @@ snapshots:
|
|||||||
|
|
||||||
prelude-ls@1.2.1: {}
|
prelude-ls@1.2.1: {}
|
||||||
|
|
||||||
|
probe-image-size@7.3.0:
|
||||||
|
dependencies:
|
||||||
|
lodash.merge: 4.6.2
|
||||||
|
needle: 2.9.1
|
||||||
|
stream-parser: 0.3.1
|
||||||
|
transitivePeerDependencies:
|
||||||
|
- supports-color
|
||||||
|
optional: true
|
||||||
|
|
||||||
proc-log@6.1.0: {}
|
proc-log@6.1.0: {}
|
||||||
|
|
||||||
process-nextick-args@2.0.1: {}
|
process-nextick-args@2.0.1: {}
|
||||||
@@ -4271,6 +4434,9 @@ snapshots:
|
|||||||
retry: 0.12.0
|
retry: 0.12.0
|
||||||
signal-exit: 3.0.7
|
signal-exit: 3.0.7
|
||||||
|
|
||||||
|
prr@1.0.1:
|
||||||
|
optional: true
|
||||||
|
|
||||||
pump@3.0.4:
|
pump@3.0.4:
|
||||||
dependencies:
|
dependencies:
|
||||||
end-of-stream: 1.4.5
|
end-of-stream: 1.4.5
|
||||||
@@ -4385,6 +4551,9 @@ snapshots:
|
|||||||
|
|
||||||
safe-buffer@5.2.1: {}
|
safe-buffer@5.2.1: {}
|
||||||
|
|
||||||
|
safer-buffer@2.1.2:
|
||||||
|
optional: true
|
||||||
|
|
||||||
sanitize-filename@1.6.4:
|
sanitize-filename@1.6.4:
|
||||||
dependencies:
|
dependencies:
|
||||||
truncate-utf8-bytes: 1.0.2
|
truncate-utf8-bytes: 1.0.2
|
||||||
@@ -4444,6 +4613,13 @@ snapshots:
|
|||||||
|
|
||||||
stat-mode@1.0.0: {}
|
stat-mode@1.0.0: {}
|
||||||
|
|
||||||
|
stream-parser@0.3.1:
|
||||||
|
dependencies:
|
||||||
|
debug: 2.6.9
|
||||||
|
transitivePeerDependencies:
|
||||||
|
- supports-color
|
||||||
|
optional: true
|
||||||
|
|
||||||
string-width@4.2.3:
|
string-width@4.2.3:
|
||||||
dependencies:
|
dependencies:
|
||||||
emoji-regex: 8.0.0
|
emoji-regex: 8.0.0
|
||||||
@@ -4561,7 +4737,7 @@ snapshots:
|
|||||||
evtd: 0.2.4
|
evtd: 0.2.4
|
||||||
vue: 3.5.39
|
vue: 3.5.39
|
||||||
|
|
||||||
vite@7.3.6(@types/node@26.1.1)(jiti@2.7.0):
|
vite@7.3.6(@types/node@26.1.1)(jiti@2.7.0)(less@4.7.0):
|
||||||
dependencies:
|
dependencies:
|
||||||
esbuild: 0.28.1
|
esbuild: 0.28.1
|
||||||
fdir: 6.5.0(picomatch@4.0.5)
|
fdir: 6.5.0(picomatch@4.0.5)
|
||||||
@@ -4573,6 +4749,7 @@ snapshots:
|
|||||||
'@types/node': 26.1.1
|
'@types/node': 26.1.1
|
||||||
fsevents: 2.3.3
|
fsevents: 2.3.3
|
||||||
jiti: 2.7.0
|
jiti: 2.7.0
|
||||||
|
less: 4.7.0
|
||||||
|
|
||||||
vooks@0.2.12(vue@3.5.39):
|
vooks@0.2.12(vue@3.5.39):
|
||||||
dependencies:
|
dependencies:
|
||||||
|
|||||||
+20
-17
@@ -5,9 +5,13 @@ import { registerIpcHandlers } from './ipc-handlers.js';
|
|||||||
|
|
||||||
import icon from '../../resources/icon.png?asset';
|
import icon from '../../resources/icon.png?asset';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 创建主窗口
|
||||||
|
* @returns {BrowserWindow} 主窗口实例
|
||||||
|
*/
|
||||||
function createWindow() {
|
function createWindow() {
|
||||||
|
|
||||||
// Create the browser window.
|
// 创建浏览器窗口
|
||||||
const mainWindow = new BrowserWindow({
|
const mainWindow = new BrowserWindow({
|
||||||
width: 1200,
|
width: 1200,
|
||||||
height: 800,
|
height: 800,
|
||||||
@@ -22,17 +26,19 @@ function createWindow() {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// 窗口准备就绪后显示,避免白屏
|
||||||
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.
|
// 基于 electron-vite cli 的热更新
|
||||||
// Load the remote URL for development or the local html file for production.
|
// 开发环境加载远程 URL,生产环境加载本地 HTML 文件
|
||||||
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 {
|
||||||
@@ -41,17 +47,16 @@ function createWindow() {
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// This method will be called when Electron has finished
|
// 当 Electron 完成初始化并准备好创建浏览器窗口时调用此方法
|
||||||
// initialization and is ready to create browser windows.
|
// 某些 API 只能在此事件触发后使用
|
||||||
// Some APIs can only be used after this event occurs.
|
|
||||||
app.whenReady().then(() => {
|
app.whenReady().then(() => {
|
||||||
|
|
||||||
// Set app user model id for windows
|
// 为 Windows 设置应用用户模型 ID
|
||||||
electronApp.setAppUserModelId('com.electron');
|
electronApp.setAppUserModelId('com.electron');
|
||||||
|
|
||||||
// Default open or close DevTools by F12 in development
|
// 开发环境通过 F12 打开/关闭 DevTools
|
||||||
// and ignore CommandOrControl + R in production.
|
// 生产环境忽略 CommandOrControl + R 刷新
|
||||||
// see https://github.com/alex8088/electron-toolkit/tree/master/packages/utils
|
// 详见 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);
|
||||||
});
|
});
|
||||||
@@ -62,8 +67,7 @@ app.whenReady().then(() => {
|
|||||||
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
|
// 在 macOS 上,当点击应用图标且没有其他窗口打开时,通常会重新创建一个窗口
|
||||||
// dock icon is clicked and there are no other windows open.
|
|
||||||
if (BrowserWindow.getAllWindows().length === 0) {
|
if (BrowserWindow.getAllWindows().length === 0) {
|
||||||
createWindow();
|
createWindow();
|
||||||
}
|
}
|
||||||
@@ -71,14 +75,13 @@ app.whenReady().then(() => {
|
|||||||
|
|
||||||
});
|
});
|
||||||
|
|
||||||
// Quit when all windows are closed, except on macOS. There, it's common
|
// 当所有窗口关闭时退出应用,macOS 除外
|
||||||
// for applications and their menu bar to stay active until the user quits
|
// 在 macOS 上,应用及其菜单栏通常会保持活动状态,直到用户通过 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
|
// 在这个文件中,你可以包含应用特定的主进程代码
|
||||||
// code. You can also put them in separate files and require them here.
|
// 也可以将它们放到单独的文件中,然后在这里引入
|
||||||
|
|||||||
@@ -2,15 +2,26 @@ import { ipcMain, BrowserWindow } from 'electron';
|
|||||||
|
|
||||||
import mqttManager from './mqtt-manager.js';
|
import mqttManager from './mqtt-manager.js';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 注册所有 IPC 处理器
|
||||||
|
* 将渲染进程的请求转发给 MQTT 管理器,并把 MQTT 事件推送给渲染进程
|
||||||
|
*/
|
||||||
export function registerIpcHandlers() {
|
export function registerIpcHandlers() {
|
||||||
|
|
||||||
// 获取主窗口(用于推送事件)
|
/**
|
||||||
|
* 获取当前所有窗口中的第一个主窗口
|
||||||
|
* @returns {BrowserWindow|null} 主窗口实例或 null
|
||||||
|
*/
|
||||||
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;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 向渲染进程发送事件
|
/**
|
||||||
|
* 向渲染进程发送事件
|
||||||
|
* @param {string} channel - IPC 通道名称
|
||||||
|
* @param {*} data - 要发送的数据
|
||||||
|
*/
|
||||||
function sendToRenderer(channel, data) {
|
function sendToRenderer(channel, data) {
|
||||||
const win = getMainWindow();
|
const win = getMainWindow();
|
||||||
if (win && !win.isDestroyed()) {
|
if (win && !win.isDestroyed()) {
|
||||||
@@ -66,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) => {
|
||||||
@@ -79,8 +101,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) => {
|
||||||
@@ -99,8 +121,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);
|
||||||
});
|
});
|
||||||
|
|
||||||
// ==================== 消息发布 ====================
|
// ==================== 消息发布 ====================
|
||||||
|
|||||||
@@ -4,6 +4,27 @@ import { join } from 'path';
|
|||||||
|
|
||||||
import mqtt from 'mqtt';
|
import mqtt from 'mqtt';
|
||||||
|
|
||||||
|
// ==================== 类型定义 ====================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 从公共类型定义文件引入 MQTT 相关类型
|
||||||
|
* @typedef {import('@types/jsdoc').MqttQoS} MqttQoS
|
||||||
|
* @typedef {import('@types/jsdoc').MqttServerStatus} MqttServerStatus
|
||||||
|
* @typedef {import('@types/jsdoc').MqttTopic} MqttTopic
|
||||||
|
* @typedef {import('@types/jsdoc').TopicConfig} TopicConfig
|
||||||
|
* @typedef {import('@types/jsdoc').ServerConfig} ServerConfig
|
||||||
|
* @typedef {import('@types/jsdoc').MqttServer} MqttServer
|
||||||
|
* @typedef {import('@types/jsdoc').PublishOptions} PublishOptions
|
||||||
|
* @typedef {import('@types/jsdoc').MqttMessage} MqttMessage
|
||||||
|
* @typedef {import('@types/jsdoc').PublishRecord} PublishRecord
|
||||||
|
*/
|
||||||
|
|
||||||
|
// ==================== 类定义 ====================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* MQTT 管理器
|
||||||
|
* 负责 MQTT 服务器配置持久化、连接管理、主题订阅/发布、消息存储等核心逻辑
|
||||||
|
*/
|
||||||
class MqttManager {
|
class MqttManager {
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
@@ -14,9 +35,14 @@ 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();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 生成唯一消息 id
|
||||||
|
* @returns {string} 消息唯一标识
|
||||||
|
*/
|
||||||
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;
|
||||||
@@ -24,6 +50,10 @@ class MqttManager {
|
|||||||
|
|
||||||
// ==================== 数据持久化 ====================
|
// ==================== 数据持久化 ====================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取用户数据目录,若不存在则创建
|
||||||
|
* @returns {string} 用户数据目录路径
|
||||||
|
*/
|
||||||
getDataDir() {
|
getDataDir() {
|
||||||
|
|
||||||
let dir = app.getPath('userData');
|
let dir = app.getPath('userData');
|
||||||
@@ -36,6 +66,10 @@ class MqttManager {
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 从本地文件加载服务器、消息和发布历史数据
|
||||||
|
* 文件不存在或解析失败时初始化默认服务器
|
||||||
|
*/
|
||||||
loadData() {
|
loadData() {
|
||||||
try {
|
try {
|
||||||
if (existsSync(this.dataPath)) {
|
if (existsSync(this.dataPath)) {
|
||||||
@@ -49,6 +83,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);
|
||||||
@@ -67,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();
|
||||||
}
|
}
|
||||||
@@ -76,6 +117,9 @@ class MqttManager {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 将当前服务器、消息和发布历史保存到本地文件
|
||||||
|
*/
|
||||||
saveData() {
|
saveData() {
|
||||||
try {
|
try {
|
||||||
|
|
||||||
@@ -87,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');
|
||||||
@@ -96,6 +141,9 @@ class MqttManager {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 初始化默认示例服务器
|
||||||
|
*/
|
||||||
initDefaultServers() {
|
initDefaultServers() {
|
||||||
|
|
||||||
let defaults = [
|
let defaults = [
|
||||||
@@ -115,8 +163,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 },
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
@@ -133,14 +181,27 @@ class MqttManager {
|
|||||||
|
|
||||||
// ==================== 监听器 ====================
|
// ==================== 监听器 ====================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 添加 MQTT 事件监听器
|
||||||
|
* @param {Function} callback - 回调函数,接收 (event, data) 参数
|
||||||
|
*/
|
||||||
addListener(callback) {
|
addListener(callback) {
|
||||||
this.listeners.add(callback);
|
this.listeners.add(callback);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 移除 MQTT 事件监听器
|
||||||
|
* @param {Function} callback - 要移除的回调函数
|
||||||
|
*/
|
||||||
removeListener(callback) {
|
removeListener(callback) {
|
||||||
this.listeners.delete(callback);
|
this.listeners.delete(callback);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 通知所有监听器
|
||||||
|
* @param {string} event - 事件名称
|
||||||
|
* @param {*} data - 事件数据
|
||||||
|
*/
|
||||||
notify(event, data) {
|
notify(event, data) {
|
||||||
this.listeners.forEach((cb) => {
|
this.listeners.forEach((cb) => {
|
||||||
try {
|
try {
|
||||||
@@ -153,6 +214,10 @@ class MqttManager {
|
|||||||
|
|
||||||
// ==================== 服务器管理 ====================
|
// ==================== 服务器管理 ====================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取所有服务器列表
|
||||||
|
* @returns {MqttServer[]} 服务器配置数组
|
||||||
|
*/
|
||||||
getServers() {
|
getServers() {
|
||||||
return Array.from(this.servers.values()).map((s) => ({
|
return Array.from(this.servers.values()).map((s) => ({
|
||||||
...s,
|
...s,
|
||||||
@@ -160,10 +225,19 @@ class MqttManager {
|
|||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 根据 id 获取服务器配置
|
||||||
|
* @param {string} id - 服务器 id
|
||||||
|
* @returns {MqttServer | null} 服务器配置或 null
|
||||||
|
*/
|
||||||
getServer(id) {
|
getServer(id) {
|
||||||
return this.servers.get(id) || null;
|
return this.servers.get(id) || null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取服务器配置字段 schema
|
||||||
|
* @returns {Record<string, Function>} 字段名到处理函数的映射
|
||||||
|
*/
|
||||||
getServerConfigSchema() {
|
getServerConfigSchema() {
|
||||||
return {
|
return {
|
||||||
name: (c, e) => c.name ?? e?.name,
|
name: (c, e) => c.name ?? e?.name,
|
||||||
@@ -181,6 +255,13 @@ class MqttManager {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 根据配置构建服务器字段
|
||||||
|
* @param {ServerConfig} config - 新配置
|
||||||
|
* @param {MqttServer | null} existing - 已有服务器配置
|
||||||
|
* @param {{ keepExistingPassword?: boolean }} options - 可选项
|
||||||
|
* @returns {ServerConfig} 处理后的字段对象
|
||||||
|
*/
|
||||||
buildServerFields(config, existing = null, options = {}) {
|
buildServerFields(config, existing = null, options = {}) {
|
||||||
let schema = this.getServerConfigSchema();
|
let schema = this.getServerConfigSchema();
|
||||||
let fields = {};
|
let fields = {};
|
||||||
@@ -190,15 +271,26 @@ class MqttManager {
|
|||||||
return fields;
|
return fields;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 构建主题对象
|
||||||
|
* @param {TopicConfig} t - 原始主题数据
|
||||||
|
* @returns {MqttTopic} 标准化后的主题对象
|
||||||
|
*/
|
||||||
buildTopic(t) {
|
buildTopic(t) {
|
||||||
return {
|
return {
|
||||||
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,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 添加服务器
|
||||||
|
* @param {ServerConfig} config - 服务器配置
|
||||||
|
* @returns {MqttServer} 创建后的服务器对象
|
||||||
|
*/
|
||||||
addServer(config) {
|
addServer(config) {
|
||||||
|
|
||||||
let id = 'srv_' + Date.now() + '_' + Math.random().toString(36).slice(2, 6);
|
let id = 'srv_' + Date.now() + '_' + Math.random().toString(36).slice(2, 6);
|
||||||
@@ -218,6 +310,12 @@ class MqttManager {
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 更新服务器配置
|
||||||
|
* @param {string} id - 服务器 id
|
||||||
|
* @param {ServerConfig} config - 新的服务器配置
|
||||||
|
* @returns {MqttServer | null} 更新后的服务器对象或 null
|
||||||
|
*/
|
||||||
updateServer(id, config) {
|
updateServer(id, config) {
|
||||||
|
|
||||||
let server = this.servers.get(id);
|
let server = this.servers.get(id);
|
||||||
@@ -234,6 +332,10 @@ class MqttManager {
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 删除服务器,并断开其连接
|
||||||
|
* @param {string} id - 服务器 id
|
||||||
|
*/
|
||||||
deleteServer(id) {
|
deleteServer(id) {
|
||||||
// 先断开连接
|
// 先断开连接
|
||||||
this.disconnect(id);
|
this.disconnect(id);
|
||||||
@@ -243,6 +345,10 @@ class MqttManager {
|
|||||||
this.saveData();
|
this.saveData();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 导出所有服务器配置(不含运行时状态)
|
||||||
|
* @returns {ServerConfig[]} 服务器配置数组
|
||||||
|
*/
|
||||||
exportServers() {
|
exportServers() {
|
||||||
let schemaKeys = Object.keys(this.getServerConfigSchema());
|
let schemaKeys = Object.keys(this.getServerConfigSchema());
|
||||||
return Array.from(this.servers.values()).map((s) => {
|
return Array.from(this.servers.values()).map((s) => {
|
||||||
@@ -255,14 +361,25 @@ class MqttManager {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 构建导出用的主题对象
|
||||||
|
* @param {MqttTopic} t - 主题对象
|
||||||
|
* @returns {TopicConfig} 导出主题对象
|
||||||
|
*/
|
||||||
buildTopicExport(t) {
|
buildTopicExport(t) {
|
||||||
return {
|
return {
|
||||||
id: t.id,
|
id: t.id,
|
||||||
topic: t.topic,
|
topic: t.topic,
|
||||||
qos: t.qos,
|
qos: t.qos,
|
||||||
|
comment: t.comment ?? '',
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 导入服务器配置列表,会覆盖现有配置
|
||||||
|
* @param {ServerConfig[]} serverList - 服务器配置数组
|
||||||
|
* @returns {MqttServer[]} 导入后的服务器列表
|
||||||
|
*/
|
||||||
importServers(serverList) {
|
importServers(serverList) {
|
||||||
if (!Array.isArray(serverList)) {
|
if (!Array.isArray(serverList)) {
|
||||||
throw new Error('导入格式错误:必须是服务器配置数组');
|
throw new Error('导入格式错误:必须是服务器配置数组');
|
||||||
@@ -300,8 +417,37 @@ 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();
|
||||||
|
}
|
||||||
|
|
||||||
// ==================== 连接管理 ====================
|
// ==================== 连接管理 ====================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 连接指定服务器
|
||||||
|
* @param {string} id - 服务器 id
|
||||||
|
* @returns {boolean} 是否成功发起连接
|
||||||
|
*/
|
||||||
connect(id) {
|
connect(id) {
|
||||||
|
|
||||||
let server = this.servers.get(id);
|
let server = this.servers.get(id);
|
||||||
@@ -399,6 +545,10 @@ class MqttManager {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 断开指定服务器的连接
|
||||||
|
* @param {string} id - 服务器 id
|
||||||
|
*/
|
||||||
disconnect(id) {
|
disconnect(id) {
|
||||||
|
|
||||||
let client = this.clients.get(id);
|
let client = this.clients.get(id);
|
||||||
@@ -421,6 +571,14 @@ class MqttManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ==================== 订阅管理 ====================
|
// ==================== 订阅管理 ====================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 执行订阅操作(内部方法)
|
||||||
|
* @param {string} serverId - 服务器 id
|
||||||
|
* @param {string} topic - 主题名称
|
||||||
|
* @param {PublishOptions} opts - 订阅选项
|
||||||
|
* @returns {boolean} 是否发起订阅
|
||||||
|
*/
|
||||||
_doSubscribe(serverId, topic, opts) {
|
_doSubscribe(serverId, topic, opts) {
|
||||||
|
|
||||||
let client = this.clients.get(serverId);
|
let client = this.clients.get(serverId);
|
||||||
@@ -447,7 +605,15 @@ class MqttManager {
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
addTopic(serverId, topic, qos = 1) {
|
/**
|
||||||
|
* 为主题列表添加新主题
|
||||||
|
* @param {string} serverId - 服务器 id
|
||||||
|
* @param {string} topic - 主题名称
|
||||||
|
* @param {MqttQoS} qos - QoS 等级
|
||||||
|
* @param {string} comment - 主题备注
|
||||||
|
* @returns {MqttTopic | null} 添加后的主题对象或 null
|
||||||
|
*/
|
||||||
|
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;
|
||||||
@@ -458,7 +624,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();
|
||||||
@@ -472,6 +638,12 @@ class MqttManager {
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 移除主题并取消订阅
|
||||||
|
* @param {string} serverId - 服务器 id
|
||||||
|
* @param {string} topic - 主题名称
|
||||||
|
* @returns {boolean} 是否移除成功
|
||||||
|
*/
|
||||||
removeTopic(serverId, topic) {
|
removeTopic(serverId, topic) {
|
||||||
|
|
||||||
let server = this.servers.get(serverId);
|
let server = this.servers.get(serverId);
|
||||||
@@ -491,6 +663,12 @@ class MqttManager {
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 订阅指定主题
|
||||||
|
* @param {string} serverId - 服务器 id
|
||||||
|
* @param {string} topic - 主题名称
|
||||||
|
* @returns {boolean} 是否发起订阅
|
||||||
|
*/
|
||||||
subscribe(serverId, topic) {
|
subscribe(serverId, topic) {
|
||||||
|
|
||||||
let server = this.servers.get(serverId);
|
let server = this.servers.get(serverId);
|
||||||
@@ -509,6 +687,12 @@ class MqttManager {
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 取消订阅指定主题
|
||||||
|
* @param {string} serverId - 服务器 id
|
||||||
|
* @param {string} topic - 主题名称
|
||||||
|
* @returns {boolean} 是否发起取消订阅
|
||||||
|
*/
|
||||||
unsubscribe(serverId, topic) {
|
unsubscribe(serverId, topic) {
|
||||||
|
|
||||||
let client = this.clients.get(serverId);
|
let client = this.clients.get(serverId);
|
||||||
@@ -532,7 +716,16 @@ class MqttManager {
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
updateTopic(serverId, topicId, newTopic, newQos) {
|
/**
|
||||||
|
* 更新主题信息,必要时取消旧主题并重新订阅新主题
|
||||||
|
* @param {string} serverId - 服务器 id
|
||||||
|
* @param {string} topicId - 主题 id
|
||||||
|
* @param {string} newTopic - 新的主题名称
|
||||||
|
* @param {number} newQos - 新的 QoS 等级
|
||||||
|
* @param {string} comment - 新的主题备注
|
||||||
|
* @returns {boolean} 是否更新成功
|
||||||
|
*/
|
||||||
|
updateTopic(serverId, topicId, newTopic, newQos, comment = '') {
|
||||||
|
|
||||||
let server = this.servers.get(serverId);
|
let server = this.servers.get(serverId);
|
||||||
|
|
||||||
@@ -565,6 +758,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) {
|
||||||
@@ -577,12 +771,18 @@ class MqttManager {
|
|||||||
topic: newTopic,
|
topic: newTopic,
|
||||||
subscribed: t.subscribed,
|
subscribed: t.subscribed,
|
||||||
topicId,
|
topicId,
|
||||||
|
comment: t.comment,
|
||||||
});
|
});
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 批量订阅服务器下所有未订阅的主题
|
||||||
|
* @param {string} serverId - 服务器 id
|
||||||
|
* @returns {boolean} 是否发起批量订阅
|
||||||
|
*/
|
||||||
subscribeAll(serverId) {
|
subscribeAll(serverId) {
|
||||||
|
|
||||||
let client = this.clients.get(serverId);
|
let client = this.clients.get(serverId);
|
||||||
@@ -625,6 +825,14 @@ class MqttManager {
|
|||||||
|
|
||||||
// ==================== 消息发布 ====================
|
// ==================== 消息发布 ====================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 向指定主题发布消息
|
||||||
|
* @param {string} serverId - 服务器 id
|
||||||
|
* @param {string} topic - 目标主题
|
||||||
|
* @param {string} payload - 消息内容
|
||||||
|
* @param {PublishOptions} opts - 发布选项
|
||||||
|
* @returns {boolean} 是否发起发布
|
||||||
|
*/
|
||||||
publish(serverId, topic, payload, opts = {}) {
|
publish(serverId, topic, payload, opts = {}) {
|
||||||
|
|
||||||
let client = this.clients.get(serverId);
|
let client = this.clients.get(serverId);
|
||||||
@@ -700,19 +908,37 @@ class MqttManager {
|
|||||||
|
|
||||||
// ==================== 消息管理 ====================
|
// ==================== 消息管理 ====================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取指定服务器的消息列表
|
||||||
|
* @param {string} serverId - 服务器 id
|
||||||
|
* @returns {MqttMessage[]} 消息数组
|
||||||
|
*/
|
||||||
getMessages(serverId) {
|
getMessages(serverId) {
|
||||||
return this.messages.get(serverId) || [];
|
return this.messages.get(serverId) || [];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取指定服务器的发布历史
|
||||||
|
* @param {string} serverId - 服务器 id
|
||||||
|
* @returns {PublishRecord[]} 发布历史数组
|
||||||
|
*/
|
||||||
getPublishHistory(serverId) {
|
getPublishHistory(serverId) {
|
||||||
return this.publishHistory.get(serverId) || [];
|
return this.publishHistory.get(serverId) || [];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 清空指定服务器的消息列表
|
||||||
|
* @param {string} serverId - 服务器 id
|
||||||
|
*/
|
||||||
clearMessages(serverId) {
|
clearMessages(serverId) {
|
||||||
this.messages.set(serverId, []);
|
this.messages.set(serverId, []);
|
||||||
this.saveData();
|
this.saveData();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 清空指定服务器的发布历史
|
||||||
|
* @param {string} serverId - 服务器 id
|
||||||
|
*/
|
||||||
clearPublishHistory(serverId) {
|
clearPublishHistory(serverId) {
|
||||||
this.publishHistory.set(serverId, []);
|
this.publishHistory.set(serverId, []);
|
||||||
this.saveData();
|
this.saveData();
|
||||||
|
|||||||
@@ -1,7 +1,10 @@
|
|||||||
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
|
/**
|
||||||
|
* 渲染进程可调用的自定义 API
|
||||||
|
* 封装了所有与主进程通信的 IPC 调用及事件监听
|
||||||
|
*/
|
||||||
const api = {
|
const api = {
|
||||||
|
|
||||||
// 服务器管理
|
// 服务器管理
|
||||||
@@ -12,18 +15,23 @@ 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),
|
||||||
|
|
||||||
// 主题管理
|
// 主题管理
|
||||||
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) =>
|
||||||
@@ -64,9 +72,7 @@ const api = {
|
|||||||
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// Use `contextBridge` APIs to expose Electron APIs to
|
// 根据是否启用上下文隔离,使用 contextBridge 暴露 API 或挂载到 window
|
||||||
// renderer only if context isolation is enabled, otherwise
|
|
||||||
// just add to the DOM global.
|
|
||||||
if (process.contextIsolated) {
|
if (process.contextIsolated) {
|
||||||
try {
|
try {
|
||||||
contextBridge.exposeInMainWorld('electron', electronAPI);
|
contextBridge.exposeInMainWorld('electron', electronAPI);
|
||||||
|
|||||||
@@ -9,9 +9,12 @@ import Dashboard from './views/Dashboard.vue';
|
|||||||
import Subscribe from './views/Subscribe.vue';
|
import Subscribe from './views/Subscribe.vue';
|
||||||
import Publish from './views/Publish.vue';
|
import Publish from './views/Publish.vue';
|
||||||
import Messages from './views/Messages.vue';
|
import Messages from './views/Messages.vue';
|
||||||
|
import QuickSend from './views/QuickSend.vue';
|
||||||
|
|
||||||
|
// 全局状态
|
||||||
const store = useMqttStore();
|
const store = useMqttStore();
|
||||||
|
|
||||||
|
// 主题覆盖配置
|
||||||
const themeOverrides = {
|
const themeOverrides = {
|
||||||
common: {
|
common: {
|
||||||
primaryColor: '#0ea5a0',
|
primaryColor: '#0ea5a0',
|
||||||
@@ -37,28 +40,46 @@ const themeOverrides = {
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// 当前激活的标签页
|
||||||
const activeTab = ref('dashboard');
|
const activeTab = ref('dashboard');
|
||||||
|
// 是否显示服务器编辑弹窗
|
||||||
const showServerModal = ref(false);
|
const showServerModal = ref(false);
|
||||||
|
// 当前编辑的服务器数据,null 表示新增
|
||||||
const editServerData = ref(null);
|
const editServerData = ref(null);
|
||||||
|
|
||||||
|
// 组件挂载时初始化 Store
|
||||||
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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 打开编辑服务器弹窗
|
||||||
|
* @param {import('@types/jsdoc').MqttServer} server - 要编辑的服务器对象
|
||||||
|
*/
|
||||||
function handleEditServer(server) {
|
function handleEditServer(server) {
|
||||||
editServerData.value = server;
|
editServerData.value = server;
|
||||||
showServerModal.value = true;
|
showServerModal.value = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 服务器保存后的回调
|
||||||
|
*/
|
||||||
function handleServerSaved() {
|
function handleServerSaved() {
|
||||||
// 刷新数据
|
// 刷新数据
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 切换标签页
|
||||||
|
* @param {string} tab - 标签页名称
|
||||||
|
*/
|
||||||
function handleSwitchTab(tab) {
|
function handleSwitchTab(tab) {
|
||||||
activeTab.value = tab;
|
activeTab.value = tab;
|
||||||
}
|
}
|
||||||
@@ -128,6 +149,14 @@ function handleSwitchTab(tab) {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</n-tab-pane>
|
</n-tab-pane>
|
||||||
|
<n-tab-pane
|
||||||
|
name="quick-send"
|
||||||
|
tab="快速发送"
|
||||||
|
>
|
||||||
|
<div class="tab-content">
|
||||||
|
<QuickSend />
|
||||||
|
</div>
|
||||||
|
</n-tab-pane>
|
||||||
</n-tabs>
|
</n-tabs>
|
||||||
</n-layout>
|
</n-layout>
|
||||||
|
|
||||||
@@ -143,7 +172,38 @@ function handleSwitchTab(tab) {
|
|||||||
</n-config-provider>
|
</n-config-provider>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style>
|
<style lang="less">
|
||||||
|
:root {
|
||||||
|
--frost-primary-color: #0ea5a0;
|
||||||
|
--frost-primary-color-hover: #0b8c87;
|
||||||
|
--frost-primary-color-pressed: #0b8c87;
|
||||||
|
--frost-primary-color-suppl: #0ea5a0;
|
||||||
|
--frost-success-color: #22c55e;
|
||||||
|
--frost-success-color-hover: #16a34a;
|
||||||
|
--frost-success-color-pressed: #15803d;
|
||||||
|
--frost-success-color-suppl: #22c55e;
|
||||||
|
--frost-warning-color: #f59e0b;
|
||||||
|
--frost-warning-color-hover: #d97706;
|
||||||
|
--frost-warning-color-pressed: #b45309;
|
||||||
|
--frost-warning-color-suppl: #f59e0b;
|
||||||
|
--frost-error-color: #ef4444;
|
||||||
|
--frost-error-color-hover: #dc2626;
|
||||||
|
--frost-error-color-pressed: #b91c1c;
|
||||||
|
--frost-error-color-suppl: #ef4444;
|
||||||
|
--frost-info-color: #3b82f6;
|
||||||
|
--frost-info-color-hover: #2563eb;
|
||||||
|
--frost-info-color-pressed: #1d4ed8;
|
||||||
|
--frost-info-color-suppl: #3b82f6;
|
||||||
|
--frost-color-text-tertiary: #999;
|
||||||
|
--frost-color-topic: #d25a00;
|
||||||
|
--frost-color-bg-light: #f5f7fa;
|
||||||
|
--frost-color-bg-item: #fafafa;
|
||||||
|
--frost-shadow-black-4: rgba(0, 0, 0, 0.04);
|
||||||
|
--frost-shadow-black-6: rgba(0, 0, 0, 0.06);
|
||||||
|
--frost-shadow-black-8: rgba(0, 0, 0, 0.08);
|
||||||
|
--frost-shadow-black-12: rgba(0, 0, 0, 0.12);
|
||||||
|
}
|
||||||
|
|
||||||
html, body, #app {
|
html, body, #app {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
@@ -160,13 +220,13 @@ html, body, #app {
|
|||||||
padding: 0 0 16px 0;
|
padding: 0 0 16px 0;
|
||||||
height: calc(100vh - 50px);
|
height: calc(100vh - 50px);
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
}
|
|
||||||
|
|
||||||
.dashboard-content {
|
&.dashboard-content {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.dashboard-scroll-wrapper {
|
.dashboard-scroll-wrapper {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
@@ -181,7 +241,6 @@ html, body, #app {
|
|||||||
gap: 12px;
|
gap: 12px;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
}
|
|
||||||
|
|
||||||
.publish-panel,
|
.publish-panel,
|
||||||
.messages-panel {
|
.messages-panel {
|
||||||
@@ -203,7 +262,6 @@ html, body, #app {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@media (max-width: 900px) {
|
@media (max-width: 900px) {
|
||||||
.message-center-layout {
|
|
||||||
grid-template-columns: 1fr;
|
grid-template-columns: 1fr;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -213,22 +271,27 @@ html, body, #app {
|
|||||||
scrollbar-width: thin;
|
scrollbar-width: thin;
|
||||||
scrollbar-color: rgba(144, 147, 153, 0.5) transparent;
|
scrollbar-color: rgba(144, 147, 153, 0.5) transparent;
|
||||||
}
|
}
|
||||||
|
|
||||||
::-webkit-scrollbar {
|
::-webkit-scrollbar {
|
||||||
width: 8px;
|
width: 8px;
|
||||||
height: 8px;
|
height: 8px;
|
||||||
}
|
|
||||||
::-webkit-scrollbar-track {
|
&-track {
|
||||||
background-color: transparent;
|
background-color: transparent;
|
||||||
}
|
}
|
||||||
::-webkit-scrollbar-thumb {
|
|
||||||
|
&-thumb {
|
||||||
background-color: rgba(144, 147, 153, 0.5);
|
background-color: rgba(144, 147, 153, 0.5);
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
}
|
|
||||||
::-webkit-scrollbar-thumb:hover {
|
&:hover {
|
||||||
background-color: rgba(144, 147, 153, 0.8);
|
background-color: rgba(144, 147, 153, 0.8);
|
||||||
}
|
}
|
||||||
::-webkit-scrollbar-corner {
|
}
|
||||||
|
|
||||||
|
&-corner {
|
||||||
display: none;
|
display: none;
|
||||||
background-color: transparent;
|
background-color: transparent;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -3,16 +3,19 @@ import { ref, watch } from 'vue';
|
|||||||
import { NModal, NForm, NGrid, NFormItemGi, NInput, NInputNumber, NSelect, NSwitch, NButton, useMessage } from 'naive-ui';
|
import { NModal, NForm, NGrid, NFormItemGi, NInput, NInputNumber, NSelect, NSwitch, NButton, useMessage } from 'naive-ui';
|
||||||
import { useMqttStore } from '../stores/mqtt.js';
|
import { useMqttStore } from '../stores/mqtt.js';
|
||||||
|
|
||||||
|
// 弹窗显示状态与编辑数据
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
show: Boolean,
|
show: Boolean,
|
||||||
editServer: Object,
|
editServer: Object,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// 更新 show、保存完成事件
|
||||||
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: '',
|
||||||
@@ -29,12 +32,14 @@ const form = ref({
|
|||||||
reconnectInterval: 5000,
|
reconnectInterval: 5000,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// 表单校验规则
|
||||||
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' },
|
||||||
@@ -42,8 +47,12 @@ const protocolOptions = [
|
|||||||
{ 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) => {
|
||||||
@@ -85,6 +94,9 @@ watch(
|
|||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 保存服务器配置
|
||||||
|
*/
|
||||||
async function handleSave() {
|
async function handleSave() {
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
|||||||
@@ -3,12 +3,21 @@ import { ref } from 'vue';
|
|||||||
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']);
|
||||||
|
|
||||||
|
// 全局状态与消息/对话框 API
|
||||||
const store = useMqttStore();
|
const store = useMqttStore();
|
||||||
const message = useMessage();
|
const message = useMessage();
|
||||||
const dialog = useDialog();
|
const dialog = useDialog();
|
||||||
|
|
||||||
|
// 文件输入控件引用,用于导入配置
|
||||||
const fileInput = ref(null);
|
const fileInput = ref(null);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 切换服务器连接状态
|
||||||
|
* @param {import('@types/jsdoc').MqttServer} server - 服务器对象
|
||||||
|
*/
|
||||||
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);
|
||||||
@@ -18,10 +27,18 @@ async function handleConnect(server) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 编辑服务器
|
||||||
|
* @param {import('@types/jsdoc').MqttServer} server - 服务器对象
|
||||||
|
*/
|
||||||
function handleEdit(server) {
|
function handleEdit(server) {
|
||||||
emit('edit-server', server);
|
emit('edit-server', server);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 删除服务器,弹出确认对话框
|
||||||
|
* @param {import('@types/jsdoc').MqttServer} server - 服务器对象
|
||||||
|
*/
|
||||||
function handleDelete(server) {
|
function handleDelete(server) {
|
||||||
dialog.warning({
|
dialog.warning({
|
||||||
title: '确认删除',
|
title: '确认删除',
|
||||||
@@ -35,6 +52,9 @@ function handleDelete(server) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 导出服务器配置到本地 JSON 文件
|
||||||
|
*/
|
||||||
async function handleExport() {
|
async function handleExport() {
|
||||||
try {
|
try {
|
||||||
let data = await store.exportServers();
|
let data = await store.exportServers();
|
||||||
@@ -51,10 +71,17 @@ async function handleExport() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 触发文件导入点击
|
||||||
|
*/
|
||||||
function handleImportClick() {
|
function handleImportClick() {
|
||||||
fileInput.value?.click();
|
fileInput.value?.click();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 处理导入的 JSON 配置文件
|
||||||
|
* @param {Event} event - 文件选择事件
|
||||||
|
*/
|
||||||
async function handleImportFile(event) {
|
async function handleImportFile(event) {
|
||||||
let file = event.target.files?.[0];
|
let file = event.target.files?.[0];
|
||||||
if (!file) return;
|
if (!file) return;
|
||||||
@@ -86,6 +113,7 @@ async function handleImportFile(event) {
|
|||||||
>
|
>
|
||||||
<div class="sidebar-container">
|
<div class="sidebar-container">
|
||||||
|
|
||||||
|
<!-- 侧边栏头部 Logo 与标题 -->
|
||||||
<div class="sidebar-header">
|
<div class="sidebar-header">
|
||||||
<div class="sidebar-logo">F</div>
|
<div class="sidebar-logo">F</div>
|
||||||
<div>
|
<div>
|
||||||
@@ -94,6 +122,7 @@ async function handleImportFile(event) {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- 服务器列表 -->
|
||||||
<div class="server-list">
|
<div class="server-list">
|
||||||
<div
|
<div
|
||||||
v-for="srv in store.servers.value"
|
v-for="srv in store.servers.value"
|
||||||
@@ -155,6 +184,7 @@ async function handleImportFile(event) {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- 侧边栏底部操作按钮 -->
|
||||||
<div class="sidebar-footer">
|
<div class="sidebar-footer">
|
||||||
<n-button
|
<n-button
|
||||||
type="primary"
|
type="primary"
|
||||||
@@ -186,7 +216,7 @@ async function handleImportFile(event) {
|
|||||||
</n-layout-sider>
|
</n-layout-sider>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped lang="less">
|
||||||
:deep(.sidebar-scroll-container) {
|
:deep(.sidebar-scroll-container) {
|
||||||
height: 100%;
|
height: 100%;
|
||||||
overflow: hidden !important;
|
overflow: hidden !important;
|
||||||
@@ -212,7 +242,7 @@ async function handleImportFile(event) {
|
|||||||
.sidebar-logo {
|
.sidebar-logo {
|
||||||
width: 32px;
|
width: 32px;
|
||||||
height: 32px;
|
height: 32px;
|
||||||
background: #0ea5a0;
|
background: var(--frost-primary-color);
|
||||||
border-radius: 6px;
|
border-radius: 6px;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
@@ -248,15 +278,19 @@ async function handleImportFile(event) {
|
|||||||
margin-bottom: 4px;
|
margin-bottom: 4px;
|
||||||
border: 1px solid transparent;
|
border: 1px solid transparent;
|
||||||
transition: all 0.15s;
|
transition: all 0.15s;
|
||||||
}
|
|
||||||
|
|
||||||
.server-item:hover {
|
&:hover {
|
||||||
background: var(--n-color-hover);
|
background: var(--n-color-hover);
|
||||||
|
|
||||||
|
.server-actions {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.server-item.active {
|
&.active {
|
||||||
background: var(--n-color-pressed);
|
background: var(--n-color-pressed);
|
||||||
border-color: #0ea5a0;
|
border-color: var(--frost-primary-color);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.server-item-top {
|
.server-item-top {
|
||||||
@@ -280,24 +314,20 @@ async function handleImportFile(event) {
|
|||||||
transition: opacity 0.15s;
|
transition: opacity 0.15s;
|
||||||
}
|
}
|
||||||
|
|
||||||
.server-item:hover .server-actions {
|
|
||||||
opacity: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
.server-url {
|
.server-url {
|
||||||
margin-top: 4px;
|
margin-top: 4px;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
}
|
|
||||||
|
|
||||||
.server-url :deep(.n-tag) {
|
:deep(.n-tag) {
|
||||||
max-width: 100%;
|
max-width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.server-url :deep(.n-tag__content) {
|
:deep(.n-tag__content) {
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
text-overflow: ellipsis;
|
text-overflow: ellipsis;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.server-bottom {
|
.server-bottom {
|
||||||
display: flex;
|
display: flex;
|
||||||
@@ -317,23 +347,23 @@ async function handleImportFile(event) {
|
|||||||
height: 6px;
|
height: 6px;
|
||||||
border-radius: 50%;
|
border-radius: 50%;
|
||||||
margin-right: 2px;
|
margin-right: 2px;
|
||||||
|
|
||||||
|
&.connected {
|
||||||
|
background: var(--frost-success-color);
|
||||||
}
|
}
|
||||||
|
|
||||||
.status-dot.connected {
|
&.connecting {
|
||||||
background: #22c55e;
|
background: var(--frost-warning-color);
|
||||||
}
|
|
||||||
|
|
||||||
.status-dot.connecting {
|
|
||||||
background: #f59e0b;
|
|
||||||
animation: pulse 1s infinite;
|
animation: pulse 1s infinite;
|
||||||
}
|
}
|
||||||
|
|
||||||
.status-dot.disconnected {
|
&.disconnected {
|
||||||
background: #999;
|
background: var(--frost-color-text-tertiary);
|
||||||
}
|
}
|
||||||
|
|
||||||
.status-dot.error {
|
&.error {
|
||||||
background: #ef4444;
|
background: var(--frost-error-color);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@keyframes pulse {
|
@keyframes pulse {
|
||||||
|
|||||||
@@ -2,6 +2,9 @@ import { createApp } from 'vue';
|
|||||||
|
|
||||||
import App from './App.vue';
|
import App from './App.vue';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 创建并挂载 Vue 应用
|
||||||
|
*/
|
||||||
const app = createApp(App);
|
const app = createApp(App);
|
||||||
|
|
||||||
app.mount('#app');
|
app.mount('#app');
|
||||||
|
|||||||
@@ -1,15 +1,129 @@
|
|||||||
import { ref, computed } from 'vue';
|
import { ref, computed } from 'vue';
|
||||||
|
|
||||||
// 全局状态(单例)
|
// ==================== 类型定义 ====================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 从公共类型定义文件引入 MQTT 相关类型
|
||||||
|
* @typedef {import('@types/jsdoc').MqttQoS} MqttQoS
|
||||||
|
* @typedef {import('@types/jsdoc').MqttServerStatus} MqttServerStatus
|
||||||
|
* @typedef {import('@types/jsdoc').MqttTopic} MqttTopic
|
||||||
|
* @typedef {import('@types/jsdoc').TopicConfig} TopicConfig
|
||||||
|
* @typedef {import('@types/jsdoc').ServerConfig} ServerConfig
|
||||||
|
* @typedef {import('@types/jsdoc').MqttServer} MqttServer
|
||||||
|
* @typedef {import('@types/jsdoc').PublishOptions} PublishOptions
|
||||||
|
* @typedef {import('@types/jsdoc').MqttMessage} MqttMessage
|
||||||
|
* @typedef {import('@types/jsdoc').PublishRecord} PublishRecord
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @typedef {Object} StatusChangeData 服务器状态变更事件数据
|
||||||
|
* @property {string} id 服务器 id
|
||||||
|
* @property {MqttServerStatus} status 新状态
|
||||||
|
* @property {string} [error] 错误信息(状态为 error 时存在)
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @typedef {Object} MessageEventData 消息接收事件数据
|
||||||
|
* @property {string} serverId 所属服务器 id
|
||||||
|
* @property {MqttMessage} message 消息对象
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @typedef {Object} SubscriptionChangeData 订阅状态变更事件数据
|
||||||
|
* @property {string} serverId 所属服务器 id
|
||||||
|
* @property {string} topic 主题名称
|
||||||
|
* @property {string} [topicId] 主题 id
|
||||||
|
* @property {string} [comment] 主题备注
|
||||||
|
* @property {boolean} subscribed 是否已订阅
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @typedef {Object} MqttStore
|
||||||
|
* @property {import('vue').Ref<MqttServer[]>} servers 所有服务器列表
|
||||||
|
* @property {import('vue').Ref<string | null>} activeServerId 当前活动服务器 ID
|
||||||
|
* @property {import('vue').ComputedRef<MqttServer | null>} activeServer 当前活动服务器对象
|
||||||
|
* @property {import('vue').Ref<MqttMessage[]>} messages 当前活动服务器消息列表
|
||||||
|
* @property {import('vue').Ref<PublishRecord[]>} publishHistory 当前活动服务器发布历史
|
||||||
|
* @property {import('vue').Ref<boolean>} loading 全局加载状态
|
||||||
|
* @property {import('vue').ComputedRef<MqttServer[]>} connectedServers 已连接服务器列表
|
||||||
|
* @property {import('vue').ComputedRef<number>} totalTopics 所有服务器主题总数
|
||||||
|
* @property {import('vue').ComputedRef<number>} subscribedTopics 已订阅主题总数
|
||||||
|
* @property {import('vue').ComputedRef<number>} totalMessages 当前活动服务器消息总数
|
||||||
|
* @property {() => Promise<void>} init 初始化 Store
|
||||||
|
* @property {(id: string | null) => void} setActiveServer 设置当前活动服务器
|
||||||
|
* @property {(config: ServerConfig) => Promise<MqttServer>} addServer 添加服务器
|
||||||
|
* @property {(id: string, config: ServerConfig) => Promise<MqttServer | null>} updateServer 更新服务器
|
||||||
|
* @property {(id: string) => Promise<void>} deleteServer 删除服务器
|
||||||
|
* @property {() => Promise<ServerConfig[]>} exportServers 导出服务器配置
|
||||||
|
* @property {(serverList: ServerConfig[]) => Promise<MqttServer[]>} importServers 导入服务器配置
|
||||||
|
* @property {(id: string) => Promise<boolean>} connectServer 连接服务器
|
||||||
|
* @property {(id: string) => Promise<void>} disconnectServer 断开服务器
|
||||||
|
* @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<boolean>} subscribeTopic 订阅主题
|
||||||
|
* @property {(serverId: string, topic: string) => Promise<boolean>} unsubscribeTopic 取消订阅主题
|
||||||
|
* @property {(serverId: string) => Promise<boolean>} subscribeAllTopics 批量订阅所有主题
|
||||||
|
* @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 {() => Promise<void>} refreshMessages 刷新消息列表
|
||||||
|
* @property {() => Promise<void>} refreshPublishHistory 刷新发布历史
|
||||||
|
* @property {() => Promise<void>} clearMessages 清空消息列表
|
||||||
|
* @property {() => Promise<void>} clearPublishHistory 清空发布历史
|
||||||
|
*/
|
||||||
|
|
||||||
|
// ==================== 全局状态(单例) ====================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 所有已配置的服务器列表,按名称排序
|
||||||
|
* @type {import('vue').Ref<MqttServer[]>}
|
||||||
|
*/
|
||||||
const servers = ref([]);
|
const servers = ref([]);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 当前在界面中选中的服务器 ID
|
||||||
|
* @type {import('vue').Ref<string | null>}
|
||||||
|
*/
|
||||||
const activeServerId = ref(null);
|
const activeServerId = ref(null);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 当前活动服务器的消息列表(包含订阅接收与本地发布的消息)
|
||||||
|
* @type {import('vue').Ref<MqttMessage[]>}
|
||||||
|
*/
|
||||||
const messages = ref([]);
|
const messages = ref([]);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 当前活动服务器的发布历史记录
|
||||||
|
* @type {import('vue').Ref<PublishRecord[]>}
|
||||||
|
*/
|
||||||
const publishHistory = ref([]);
|
const publishHistory = ref([]);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Store 初始化或异步操作时的全局加载状态
|
||||||
|
* @type {import('vue').Ref<boolean>}
|
||||||
|
*/
|
||||||
const loading = ref(false);
|
const loading = ref(false);
|
||||||
|
|
||||||
// 清理函数集合
|
/**
|
||||||
|
* IPC 监听器清理函数集合
|
||||||
|
* @type {Array<() => void>}
|
||||||
|
*/
|
||||||
let cleanupFns = [];
|
let cleanupFns = [];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 对指定服务器的主题列表按主题名称升序排序
|
||||||
|
* @param {MqttServer} server - 服务器对象
|
||||||
|
* @returns {void}
|
||||||
|
*/
|
||||||
|
function sortTopics(server) {
|
||||||
|
if (server && server.topics) {
|
||||||
|
server.topics.sort((a, b) => a.topic.localeCompare(b.topic));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置主进程事件监听器
|
||||||
|
* 包括状态变化、消息接收、订阅变化等事件
|
||||||
|
*/
|
||||||
function setupListeners() {
|
function setupListeners() {
|
||||||
|
|
||||||
// 清理旧监听器
|
// 清理旧监听器
|
||||||
@@ -17,61 +131,106 @@ function setupListeners() {
|
|||||||
cleanupFns = [];
|
cleanupFns = [];
|
||||||
|
|
||||||
cleanupFns.push(
|
cleanupFns.push(
|
||||||
window.api.onStatusChange((data) => {
|
window.api.onStatusChange((/** @type {StatusChangeData} */ data) => {
|
||||||
const srv = servers.value.find((s) => s.id === data.id);
|
|
||||||
|
let srv = servers.value.find((s) => {
|
||||||
|
return 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((/** @type {MessageEventData} */ 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) {
|
if (messages.value.length > 500) {
|
||||||
messages.value.pop();
|
messages.value.pop();
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
cleanupFns.push(
|
cleanupFns.push(
|
||||||
window.api.onSubscriptionChange((data) => {
|
window.api.onSubscriptionChange((/** @type {SubscriptionChangeData} */ data) => {
|
||||||
if (data.serverId === activeServerId.value) {
|
if (data.serverId === activeServerId.value) {
|
||||||
const srv = servers.value.find((s) => s.id === data.serverId);
|
|
||||||
|
let srv = servers.value.find((s) => {
|
||||||
|
return s.id === data.serverId;
|
||||||
|
});
|
||||||
|
|
||||||
if (srv) {
|
if (srv) {
|
||||||
const t = srv.topics.find((t) => t.topic === data.topic || t.id === data.topicId);
|
|
||||||
|
let t = srv.topics.find((t) => {
|
||||||
|
return t.topic === data.topic || t.id === data.topicId;
|
||||||
|
});
|
||||||
|
|
||||||
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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* MQTT 全局状态管理 Store
|
||||||
|
* @returns {MqttStore} 包含状态、计算属性和方法的对象
|
||||||
|
*/
|
||||||
export function useMqttStore() {
|
export function useMqttStore() {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 当前选中的服务器
|
||||||
|
* @type {import('vue').ComputedRef<MqttServer | null>}
|
||||||
|
*/
|
||||||
let activeServer = computed(
|
let activeServer = computed(
|
||||||
() => servers.value.find((s) => s.id === activeServerId.value) || null
|
() => servers.value.find((s) => s.id === activeServerId.value) || null
|
||||||
);
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 已连接的服务器列表
|
||||||
|
* @type {import('vue').ComputedRef<MqttServer[]>}
|
||||||
|
*/
|
||||||
let connectedServers = computed(() => servers.value.filter((s) => s.status === 'connected'));
|
let connectedServers = computed(() => servers.value.filter((s) => s.status === 'connected'));
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 所有服务器主题总数
|
||||||
|
* @type {import('vue').ComputedRef<number>}
|
||||||
|
*/
|
||||||
let 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)
|
||||||
);
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 已订阅主题总数
|
||||||
|
* @type {import('vue').ComputedRef<number>}
|
||||||
|
*/
|
||||||
let subscribedTopics = computed(() =>
|
let subscribedTopics = computed(() =>
|
||||||
servers.value.reduce(
|
servers.value.reduce(
|
||||||
(sum, s) => sum + (s.topics?.filter((t) => t.subscribed)?.length || 0),
|
(sum, s) => sum + (s.topics?.filter((t) => t.subscribed)?.length || 0),
|
||||||
@@ -79,13 +238,26 @@ export function useMqttStore() {
|
|||||||
)
|
)
|
||||||
);
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 当前活动服务器消息总数
|
||||||
|
* @type {import('vue').ComputedRef<number>}
|
||||||
|
*/
|
||||||
let totalMessages = computed(() => messages.value.length);
|
let totalMessages = computed(() => messages.value.length);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 按服务器名称对服务器列表排序(中文环境)
|
||||||
|
* @returns {void}
|
||||||
|
*/
|
||||||
function sortServers() {
|
function sortServers() {
|
||||||
servers.value.sort((a, b) => a.name.localeCompare(b.name, 'zh-CN'));
|
servers.value.sort((a, b) => a.name.localeCompare(b.name, 'zh-CN'));
|
||||||
}
|
}
|
||||||
|
|
||||||
// ==================== 初始化 ====================
|
// ==================== 初始化 ====================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 初始化 Store,从主进程加载服务器列表并设置监听
|
||||||
|
* @returns {Promise<void>}
|
||||||
|
*/
|
||||||
async function init() {
|
async function init() {
|
||||||
|
|
||||||
loading.value = true;
|
loading.value = true;
|
||||||
@@ -96,6 +268,7 @@ export function useMqttStore() {
|
|||||||
|
|
||||||
servers.value = data;
|
servers.value = data;
|
||||||
sortServers();
|
sortServers();
|
||||||
|
servers.value.forEach((srv) => sortTopics(srv));
|
||||||
setupListeners();
|
setupListeners();
|
||||||
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
@@ -107,6 +280,11 @@ export function useMqttStore() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ==================== 服务器管理 ====================
|
// ==================== 服务器管理 ====================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置当前活动服务器并刷新相关数据
|
||||||
|
* @param {string|null} id - 服务器 id
|
||||||
|
*/
|
||||||
function setActiveServer(id) {
|
function setActiveServer(id) {
|
||||||
|
|
||||||
activeServerId.value = id;
|
activeServerId.value = id;
|
||||||
@@ -117,6 +295,11 @@ export function useMqttStore() {
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 添加服务器
|
||||||
|
* @param {ServerConfig} config - 服务器配置
|
||||||
|
* @returns {Promise<MqttServer>} 添加后的服务器对象
|
||||||
|
*/
|
||||||
async function addServer(config) {
|
async function addServer(config) {
|
||||||
|
|
||||||
let server = await window.api.addServer(config);
|
let server = await window.api.addServer(config);
|
||||||
@@ -132,6 +315,12 @@ export function useMqttStore() {
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 更新服务器配置
|
||||||
|
* @param {string} id - 服务器 id
|
||||||
|
* @param {ServerConfig} config - 新的服务器配置
|
||||||
|
* @returns {Promise<MqttServer | null>} 更新后的服务器对象或 null
|
||||||
|
*/
|
||||||
async function updateServer(id, config) {
|
async function updateServer(id, config) {
|
||||||
|
|
||||||
let result = await window.api.updateServer(id, config);
|
let result = await window.api.updateServer(id, config);
|
||||||
@@ -148,6 +337,11 @@ export function useMqttStore() {
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 删除服务器
|
||||||
|
* @param {string} id - 服务器 id
|
||||||
|
* @returns {Promise<void>}
|
||||||
|
*/
|
||||||
async function deleteServer(id) {
|
async function deleteServer(id) {
|
||||||
|
|
||||||
await window.api.deleteServer(id);
|
await window.api.deleteServer(id);
|
||||||
@@ -161,37 +355,67 @@ export function useMqttStore() {
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 导出所有服务器配置
|
||||||
|
* @returns {Promise<ServerConfig[]>} 服务器配置数组
|
||||||
|
*/
|
||||||
async function exportServers() {
|
async function exportServers() {
|
||||||
return await window.api.exportServers();
|
return await window.api.exportServers();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 导入服务器配置列表
|
||||||
|
* @param {ServerConfig[]} serverList - 服务器配置数组
|
||||||
|
* @returns {Promise<MqttServer[]>} 导入后的服务器列表
|
||||||
|
*/
|
||||||
async function importServers(serverList) {
|
async function importServers(serverList) {
|
||||||
let data = await window.api.importServers(serverList);
|
let data = await window.api.importServers(serverList);
|
||||||
servers.value = data;
|
servers.value = data;
|
||||||
sortServers();
|
sortServers();
|
||||||
|
servers.value.forEach((srv) => sortTopics(srv));
|
||||||
if (servers.value.length > 0 && !activeServerId.value) {
|
if (servers.value.length > 0 && !activeServerId.value) {
|
||||||
activeServerId.value = servers.value[0].id;
|
activeServerId.value = servers.value[0].id;
|
||||||
}
|
}
|
||||||
return data;
|
return data;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 连接指定服务器
|
||||||
|
* @param {string} id - 服务器 id
|
||||||
|
* @returns {Promise<boolean>} 是否成功发起连接
|
||||||
|
*/
|
||||||
async function connectServer(id) {
|
async function connectServer(id) {
|
||||||
return await window.api.connect(id);
|
return await window.api.connect(id);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 断开指定服务器
|
||||||
|
* @param {string} id - 服务器 id
|
||||||
|
* @returns {Promise<void>}
|
||||||
|
*/
|
||||||
async function disconnectServer(id) {
|
async function disconnectServer(id) {
|
||||||
await window.api.disconnect(id);
|
await window.api.disconnect(id);
|
||||||
}
|
}
|
||||||
|
|
||||||
// ==================== 主题管理 ====================
|
// ==================== 主题管理 ====================
|
||||||
async function addTopic(serverId, topic, qos) {
|
|
||||||
|
|
||||||
let result = await window.api.addTopic(serverId, topic, qos);
|
/**
|
||||||
|
* 为指定服务器添加主题
|
||||||
|
* @param {string} serverId - 服务器 id
|
||||||
|
* @param {string} topic - 主题名称
|
||||||
|
* @param {MqttQoS} qos - QoS 等级
|
||||||
|
* @param {string} comment - 主题备注
|
||||||
|
* @returns {Promise<MqttTopic | null>} 添加后的主题对象或 null
|
||||||
|
*/
|
||||||
|
async function addTopic(serverId, topic, qos, comment) {
|
||||||
|
|
||||||
|
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);
|
||||||
if (srv) {
|
if (srv) {
|
||||||
srv.topics.push(result);
|
srv.topics.push(result);
|
||||||
|
sortTopics(srv);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -199,6 +423,12 @@ export function useMqttStore() {
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 移除指定服务器的主题
|
||||||
|
* @param {string} serverId - 服务器 id
|
||||||
|
* @param {string} topic - 主题名称
|
||||||
|
* @returns {Promise<void>}
|
||||||
|
*/
|
||||||
async function removeTopic(serverId, topic) {
|
async function removeTopic(serverId, topic) {
|
||||||
|
|
||||||
await window.api.removeTopic(serverId, topic);
|
await window.api.removeTopic(serverId, topic);
|
||||||
@@ -207,27 +437,63 @@ export function useMqttStore() {
|
|||||||
|
|
||||||
if (srv) {
|
if (srv) {
|
||||||
srv.topics = srv.topics.filter((t) => t.topic !== topic);
|
srv.topics = srv.topics.filter((t) => t.topic !== topic);
|
||||||
|
sortTopics(srv);
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 订阅指定主题
|
||||||
|
* @param {string} serverId - 服务器 id
|
||||||
|
* @param {string} topic - 主题名称
|
||||||
|
* @returns {Promise<boolean>} 是否发起订阅
|
||||||
|
*/
|
||||||
async function subscribeTopic(serverId, topic) {
|
async function subscribeTopic(serverId, topic) {
|
||||||
return await window.api.subscribe(serverId, topic);
|
return await window.api.subscribe(serverId, topic);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 取消订阅指定主题
|
||||||
|
* @param {string} serverId - 服务器 id
|
||||||
|
* @param {string} topic - 主题名称
|
||||||
|
* @returns {Promise<boolean>} 是否发起取消订阅
|
||||||
|
*/
|
||||||
async function unsubscribeTopic(serverId, topic) {
|
async function unsubscribeTopic(serverId, topic) {
|
||||||
return await window.api.unsubscribe(serverId, topic);
|
return await window.api.unsubscribe(serverId, topic);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 批量订阅指定服务器下所有未订阅主题
|
||||||
|
* @param {string} serverId - 服务器 id
|
||||||
|
* @returns {Promise<boolean>} 是否发起批量订阅
|
||||||
|
*/
|
||||||
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) {
|
/**
|
||||||
return await window.api.updateTopic(serverId, topicId, topic, qos);
|
* 更新指定服务器的主题信息
|
||||||
|
* @param {string} serverId - 服务器 id
|
||||||
|
* @param {string} topicId - 主题 id
|
||||||
|
* @param {string} topic - 新的主题名称
|
||||||
|
* @param {MqttQoS} qos - 新的 QoS 等级
|
||||||
|
* @param {string} comment - 新的主题备注
|
||||||
|
* @returns {Promise<boolean>} 是否更新成功
|
||||||
|
*/
|
||||||
|
async function updateTopic(serverId, topicId, topic, qos, comment) {
|
||||||
|
return await window.api.updateTopic(serverId, topicId, topic, qos, comment);
|
||||||
}
|
}
|
||||||
|
|
||||||
// ==================== 消息发布 ====================
|
// ==================== 消息发布 ====================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 发布消息到指定主题
|
||||||
|
* @param {string} serverId - 服务器 id
|
||||||
|
* @param {string} topic - 目标主题
|
||||||
|
* @param {string} payload - 消息内容
|
||||||
|
* @param {PublishOptions} opts - 发布选项
|
||||||
|
* @returns {Promise<boolean>} 是否发起发布
|
||||||
|
*/
|
||||||
async function publishMessage(serverId, topic, payload, opts) {
|
async function publishMessage(serverId, topic, payload, opts) {
|
||||||
|
|
||||||
let result = await window.api.publish(serverId, topic, payload, opts);
|
let result = await window.api.publish(serverId, topic, payload, opts);
|
||||||
@@ -242,18 +508,30 @@ export function useMqttStore() {
|
|||||||
|
|
||||||
// ==================== 消息管理 ====================
|
// ==================== 消息管理 ====================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 刷新当前活动服务器的消息列表
|
||||||
|
* @returns {Promise<void>}
|
||||||
|
*/
|
||||||
async function refreshMessages() {
|
async function refreshMessages() {
|
||||||
if (activeServerId.value) {
|
if (activeServerId.value) {
|
||||||
messages.value = await window.api.getMessages(activeServerId.value);
|
messages.value = await window.api.getMessages(activeServerId.value);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 刷新当前活动服务器的发布历史
|
||||||
|
* @returns {Promise<void>}
|
||||||
|
*/
|
||||||
async function refreshPublishHistory() {
|
async function refreshPublishHistory() {
|
||||||
if (activeServerId.value) {
|
if (activeServerId.value) {
|
||||||
publishHistory.value = await window.api.getPublishHistory(activeServerId.value);
|
publishHistory.value = await window.api.getPublishHistory(activeServerId.value);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 清空当前活动服务器的消息列表
|
||||||
|
* @returns {Promise<void>}
|
||||||
|
*/
|
||||||
async function clearMessages() {
|
async function clearMessages() {
|
||||||
if (activeServerId.value) {
|
if (activeServerId.value) {
|
||||||
await window.api.clearMessages(activeServerId.value);
|
await window.api.clearMessages(activeServerId.value);
|
||||||
@@ -261,6 +539,10 @@ export function useMqttStore() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 清空当前活动服务器的发布历史
|
||||||
|
* @returns {Promise<void>}
|
||||||
|
*/
|
||||||
async function clearPublishHistory() {
|
async function clearPublishHistory() {
|
||||||
if (activeServerId.value) {
|
if (activeServerId.value) {
|
||||||
await window.api.clearPublishHistory(activeServerId.value);
|
await window.api.clearPublishHistory(activeServerId.value);
|
||||||
|
|||||||
@@ -2,9 +2,18 @@
|
|||||||
import { useMqttStore } from '../stores/mqtt.js';
|
import { useMqttStore } from '../stores/mqtt.js';
|
||||||
import { NGrid, NGridItem, NCard, NStatistic, NTag, NDescriptions, NDescriptionsItem, NSpace, NButton, NList, NListItem, useMessage } from 'naive-ui';
|
import { NGrid, NGridItem, NCard, NStatistic, NTag, NDescriptions, NDescriptionsItem, NSpace, NButton, NList, NListItem, useMessage } from 'naive-ui';
|
||||||
|
|
||||||
|
// 全局状态与消息 API
|
||||||
const store = useMqttStore();
|
const store = useMqttStore();
|
||||||
const msg = useMessage();
|
const msg = useMessage();
|
||||||
|
|
||||||
|
// 组件事件定义
|
||||||
|
const emit = defineEmits(['switch-tab', 'edit-server']);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 处理快捷操作:切换标签页并选中服务器
|
||||||
|
* @param {import('@types/jsdoc').MqttServer} srv - 服务器对象
|
||||||
|
* @param {string} action - 目标标签页名称
|
||||||
|
*/
|
||||||
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);
|
||||||
@@ -14,6 +23,10 @@ function handleAction(srv, action) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 切换当前服务器连接状态
|
||||||
|
* @param {import('@types/jsdoc').MqttServer} srv - 服务器对象
|
||||||
|
*/
|
||||||
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);
|
||||||
@@ -22,8 +35,6 @@ async function handleToggleConnection(srv) {
|
|||||||
await store.connectServer(srv.id);
|
await store.connectServer(srv.id);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const emit = defineEmits(['switch-tab', 'edit-server']);
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
@@ -178,49 +189,51 @@ const emit = defineEmits(['switch-tab', 'edit-server']);
|
|||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped lang="less">
|
||||||
.dashboard {
|
.dashboard {
|
||||||
max-width: 900px;
|
max-width: 900px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.server-descriptions :deep(.n-descriptions-table) {
|
.server-descriptions {
|
||||||
|
:deep(.n-descriptions-table) {
|
||||||
table-layout: fixed;
|
table-layout: fixed;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.server-descriptions :deep(.n-descriptions-table-header),
|
:deep(.n-descriptions-table-header),
|
||||||
.server-descriptions :deep(.n-descriptions-table-content) {
|
:deep(.n-descriptions-table-content) {
|
||||||
width: 25%;
|
width: 25%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.server-descriptions :deep(.n-descriptions-table-content) {
|
:deep(.n-descriptions-table-content) {
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
text-overflow: ellipsis;
|
text-overflow: ellipsis;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.status-dot {
|
.status-dot {
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
width: 8px;
|
width: 8px;
|
||||||
height: 8px;
|
height: 8px;
|
||||||
border-radius: 50%;
|
border-radius: 50%;
|
||||||
|
|
||||||
|
&.connected {
|
||||||
|
background: var(--frost-success-color);
|
||||||
}
|
}
|
||||||
|
|
||||||
.status-dot.connected {
|
&.connecting {
|
||||||
background: #22c55e;
|
background: var(--frost-warning-color);
|
||||||
}
|
|
||||||
|
|
||||||
.status-dot.connecting {
|
|
||||||
background: #f59e0b;
|
|
||||||
animation: pulse 1s infinite;
|
animation: pulse 1s infinite;
|
||||||
}
|
}
|
||||||
|
|
||||||
.status-dot.disconnected {
|
&.disconnected {
|
||||||
background: #999;
|
background: var(--frost-color-text-tertiary);
|
||||||
}
|
}
|
||||||
|
|
||||||
.status-dot.error {
|
&.error {
|
||||||
background: #ef4444;
|
background: var(--frost-error-color);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@keyframes pulse {
|
@keyframes pulse {
|
||||||
|
|||||||
@@ -1,13 +1,19 @@
|
|||||||
<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, NSelect, NButton, NEmpty, useMessage } from 'naive-ui';
|
||||||
|
|
||||||
|
// 全局状态与消息 API
|
||||||
const store = useMqttStore();
|
const store = useMqttStore();
|
||||||
const message = useMessage();
|
const message = useMessage();
|
||||||
|
|
||||||
const filter = ref('');
|
// 已选中的过滤主题列表
|
||||||
|
const filter = ref([]);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 复制主题到剪贴板
|
||||||
|
* @param {string} topic - 主题名称
|
||||||
|
*/
|
||||||
async function copyTopic(topic) {
|
async function copyTopic(topic) {
|
||||||
try {
|
try {
|
||||||
await navigator.clipboard.writeText(topic);
|
await navigator.clipboard.writeText(topic);
|
||||||
@@ -17,24 +23,79 @@ async function copyTopic(topic) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 可过滤的主题选项(来自已配置主题和已接收消息)
|
||||||
|
* @type {import('vue').ComputedRef<Array<{label: string, value: string}>>}
|
||||||
|
*/
|
||||||
|
const topicOptions = computed(() => {
|
||||||
|
|
||||||
|
let server = store.activeServer.value;
|
||||||
|
let serverTopics = server?.topics?.map((t) => t.topic) || [];
|
||||||
|
let messageTopics = store.messages.value.map((m) => m.topic);
|
||||||
|
let all = [...new Set([...serverTopics, ...messageTopics])];
|
||||||
|
|
||||||
|
all.sort((a, b) => a.localeCompare(b));
|
||||||
|
|
||||||
|
return all.map((topic) => ({ label: topic, value: topic }));
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 判断消息主题是否匹配指定的过滤主题(支持 + 单层通配符与 # 多层通配符)
|
||||||
|
* @param {string} filterTopic - 过滤主题,可包含 + 或 #
|
||||||
|
* @param {string} messageTopic - 消息主题
|
||||||
|
* @returns {boolean} 是否匹配
|
||||||
|
*/
|
||||||
|
function topicMatchesFilter(filterTopic, messageTopic) {
|
||||||
|
|
||||||
|
if (filterTopic === '#') {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (filterTopic.endsWith('/#')) {
|
||||||
|
let prefix = filterTopic.slice(0, -2);
|
||||||
|
return messageTopic === prefix || messageTopic.startsWith(prefix + '/');
|
||||||
|
}
|
||||||
|
|
||||||
|
let filterParts = filterTopic.split('/');
|
||||||
|
let messageParts = messageTopic.split('/');
|
||||||
|
|
||||||
|
if (filterParts.length !== messageParts.length) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return filterParts.every((part, index) => part === '+' || part === messageParts[index]);
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 根据选中主题筛选后的消息列表(支持 + / # 通配符)
|
||||||
|
* @type {import('vue').ComputedRef<Array<Object>>}
|
||||||
|
*/
|
||||||
const filteredMessages = computed(() => {
|
const filteredMessages = computed(() => {
|
||||||
|
|
||||||
if (!filter.value.trim()) {
|
if (!filter.value || filter.value.length === 0) {
|
||||||
return store.messages.value;
|
return store.messages.value;
|
||||||
}
|
}
|
||||||
|
|
||||||
let f = filter.value.trim().toLowerCase();
|
return store.messages.value.filter((m) =>
|
||||||
|
filter.value.some((f) => topicMatchesFilter(f, m.topic))
|
||||||
|
);
|
||||||
|
|
||||||
return store.messages.value.filter((m) => {
|
|
||||||
return m.topic.toLowerCase().includes(f);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
})
|
/**
|
||||||
|
* 清空当前活动服务器的消息列表
|
||||||
|
*/
|
||||||
async function handleClear() {
|
async function handleClear() {
|
||||||
await store.clearMessages();
|
await store.clearMessages();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 格式化消息 Payload,若可解析为 JSON 则格式化输出
|
||||||
|
* @param {string} payload - 消息内容
|
||||||
|
* @returns {string} 格式化后的内容
|
||||||
|
*/
|
||||||
function formatPayload(payload) {
|
function formatPayload(payload) {
|
||||||
try {
|
try {
|
||||||
return JSON.stringify(JSON.parse(payload), null, 2);
|
return JSON.stringify(JSON.parse(payload), null, 2);
|
||||||
@@ -61,10 +122,8 @@ function formatPayload(payload) {
|
|||||||
}"
|
}"
|
||||||
>
|
>
|
||||||
<template #header>
|
<template #header>
|
||||||
<n-space
|
<div
|
||||||
align="center"
|
style="display: flex; flex-direction: column; gap: 8px; width: 100%"
|
||||||
justify="space-between"
|
|
||||||
style="width: 100%"
|
|
||||||
>
|
>
|
||||||
<div>
|
<div>
|
||||||
<span>消息监控</span>
|
<span>消息监控</span>
|
||||||
@@ -72,20 +131,26 @@ function formatPayload(payload) {
|
|||||||
style="font-size: 12px; opacity: 0.55; margin-left: 8px"
|
style="font-size: 12px; opacity: 0.55; margin-left: 8px"
|
||||||
>实时查看接收和发送的 MQTT 消息</span>
|
>实时查看接收和发送的 MQTT 消息</span>
|
||||||
</div>
|
</div>
|
||||||
<n-space>
|
<div
|
||||||
<n-input
|
style="display: flex; align-items: center; gap: 8px; width: 100%"
|
||||||
|
>
|
||||||
|
<n-select
|
||||||
v-model:value="filter"
|
v-model:value="filter"
|
||||||
size="small"
|
size="small"
|
||||||
placeholder="过滤主题..."
|
placeholder="过滤主题..."
|
||||||
style="width: 180px"
|
style="flex: 1 1 auto; max-width: 480px; min-width: 0"
|
||||||
|
multiple
|
||||||
|
filterable
|
||||||
clearable
|
clearable
|
||||||
|
max-tag-count="responsive"
|
||||||
|
:options="topicOptions"
|
||||||
/>
|
/>
|
||||||
<n-button
|
<n-button
|
||||||
size="small"
|
size="small"
|
||||||
@click="handleClear"
|
@click="handleClear"
|
||||||
>清空消息</n-button>
|
>清空消息</n-button>
|
||||||
</n-space>
|
</div>
|
||||||
</n-space>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<n-empty
|
<n-empty
|
||||||
v-if="filteredMessages.length === 0"
|
v-if="filteredMessages.length === 0"
|
||||||
@@ -136,29 +201,29 @@ function formatPayload(payload) {
|
|||||||
|
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped lang="less">
|
||||||
.messages-view {
|
.messages-view {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
min-height: 0;
|
min-height: 0;
|
||||||
}
|
|
||||||
|
|
||||||
.messages-view :deep(.n-card) {
|
:deep(.n-card) {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
min-height: 0;
|
min-height: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.messages-view :deep(.n-card__content) {
|
:deep(.n-card__content) {
|
||||||
flex: 1 1 auto;
|
flex: 1 1 auto;
|
||||||
min-height: 0;
|
min-height: 0;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.msg-log {
|
.msg-log {
|
||||||
background: #f5f7fa;
|
background: var(--frost-color-bg-light);
|
||||||
border-radius: 6px;
|
border-radius: 6px;
|
||||||
padding: 12px;
|
padding: 12px;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
@@ -174,7 +239,7 @@ function formatPayload(payload) {
|
|||||||
|
|
||||||
.msg-entry {
|
.msg-entry {
|
||||||
padding: 8px 0;
|
padding: 8px 0;
|
||||||
border-bottom: 1px solid rgba(0, 0, 0, 0.06);
|
border-bottom: 1px solid var(--frost-shadow-black-6);
|
||||||
}
|
}
|
||||||
|
|
||||||
.msg-header {
|
.msg-header {
|
||||||
@@ -185,45 +250,51 @@ function formatPayload(payload) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.msg-time {
|
.msg-time {
|
||||||
color: #999;
|
color: var(--frost-color-text-tertiary);
|
||||||
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;
|
||||||
|
|
||||||
|
&.pub {
|
||||||
|
color: var(--frost-info-color);
|
||||||
}
|
}
|
||||||
.msg-dir.pub {
|
|
||||||
color: #3b82f6;
|
&.sub {
|
||||||
|
color: var(--frost-success-color);
|
||||||
}
|
}
|
||||||
.msg-dir.sub {
|
|
||||||
color: #22c55e;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.msg-topic {
|
.msg-topic {
|
||||||
color: #d25a00;
|
color: var(--frost-color-topic);
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
flex: 1;
|
flex: 1;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
text-overflow: ellipsis;
|
text-overflow: ellipsis;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
|
||||||
|
|
||||||
.msg-topic:hover {
|
&: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;
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
background: rgba(0, 0, 0, 0.04);
|
background: var(--frost-shadow-black-4);
|
||||||
color: #333;
|
color: #333;
|
||||||
font: inherit;
|
font: inherit;
|
||||||
white-space: pre-wrap;
|
white-space: pre-wrap;
|
||||||
overflow-wrap: anywhere;
|
overflow-wrap: anywhere;
|
||||||
}
|
}
|
||||||
|
|
||||||
.msg-meta {
|
.msg-meta {
|
||||||
color: #999;
|
color: var(--frost-color-text-tertiary);
|
||||||
font-size: 11px;
|
font-size: 11px;
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,19 +3,26 @@ import { ref, watch } from 'vue';
|
|||||||
import { useMqttStore } from '../stores/mqtt.js';
|
import { useMqttStore } from '../stores/mqtt.js';
|
||||||
import { NCard, NSpace, NFormItem, NInput, NSelect, NSwitch, NButton, NEmpty, NTabs, NTabPane, useMessage } from 'naive-ui';
|
import { NCard, NSpace, NFormItem, NInput, NSelect, NSwitch, NButton, NEmpty, NTabs, NTabPane, useMessage } from 'naive-ui';
|
||||||
|
|
||||||
|
// 全局状态与消息 API
|
||||||
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');
|
||||||
|
|
||||||
|
// localStorage 持久化 key
|
||||||
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;
|
||||||
|
|
||||||
|
// 从 localStorage 恢复发布表单
|
||||||
try {
|
try {
|
||||||
|
|
||||||
let savedForm = JSON.parse(localStorage.getItem(formStorageKey) || '{}');
|
let savedForm = JSON.parse(localStorage.getItem(formStorageKey) || '{}');
|
||||||
@@ -40,6 +47,7 @@ try {
|
|||||||
localStorage.removeItem(formStorageKey);
|
localStorage.removeItem(formStorageKey);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 从 localStorage 恢复当前标签页
|
||||||
try {
|
try {
|
||||||
|
|
||||||
let savedTab = localStorage.getItem(publishTabStorageKey);
|
let savedTab = localStorage.getItem(publishTabStorageKey);
|
||||||
@@ -52,6 +60,7 @@ try {
|
|||||||
localStorage.removeItem(publishTabStorageKey);
|
localStorage.removeItem(publishTabStorageKey);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 表单变化时持久化
|
||||||
watch([topic, payload, qos, retain], () => {
|
watch([topic, payload, qos, retain], () => {
|
||||||
|
|
||||||
if (!formReady) {
|
if (!formReady) {
|
||||||
@@ -70,18 +79,23 @@ watch([topic, payload, qos, retain], () => {
|
|||||||
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// 标签页变化时持久化
|
||||||
watch(activePublishTab, (tab) => {
|
watch(activePublishTab, (tab) => {
|
||||||
localStorage.setItem(publishTabStorageKey, tab);
|
localStorage.setItem(publishTabStorageKey, tab);
|
||||||
});
|
});
|
||||||
|
|
||||||
formReady = true;
|
formReady = true;
|
||||||
|
|
||||||
|
// QoS 下拉选项
|
||||||
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() {
|
||||||
|
|
||||||
let srv = store.activeServer.value;
|
let srv = store.activeServer.value;
|
||||||
@@ -111,6 +125,9 @@ async function handlePublish() {
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 格式化 Payload 为 JSON
|
||||||
|
*/
|
||||||
function handleFormatJson() {
|
function handleFormatJson() {
|
||||||
try {
|
try {
|
||||||
|
|
||||||
@@ -124,10 +141,17 @@ function handleFormatJson() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 清空发布历史
|
||||||
|
*/
|
||||||
async function handleClearHistory() {
|
async function handleClearHistory() {
|
||||||
await store.clearPublishHistory();
|
await store.clearPublishHistory();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 复制主题到剪贴板
|
||||||
|
* @param {string} topic - 主题名称
|
||||||
|
*/
|
||||||
async function copyTopic(topic) {
|
async function copyTopic(topic) {
|
||||||
try {
|
try {
|
||||||
await navigator.clipboard.writeText(topic);
|
await navigator.clipboard.writeText(topic);
|
||||||
@@ -275,7 +299,7 @@ async function copyTopic(topic) {
|
|||||||
|
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped lang="less">
|
||||||
.publish-view {
|
.publish-view {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
@@ -283,96 +307,96 @@ async function copyTopic(topic) {
|
|||||||
height: 100%;
|
height: 100%;
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
min-height: 0;
|
min-height: 0;
|
||||||
}
|
|
||||||
|
|
||||||
.publish-view :deep(.n-tabs) {
|
:deep(.n-tabs) {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
min-height: 0;
|
min-height: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.publish-view :deep(.n-tabs-nav) {
|
:deep(.n-tabs-nav) {
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.publish-view :deep(.n-tabs-pane-wrapper) {
|
:deep(.n-tabs-pane-wrapper) {
|
||||||
flex: 1 1 auto;
|
flex: 1 1 auto;
|
||||||
min-height: 0;
|
min-height: 0;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
.publish-view :deep(.n-tabs-tab-panels),
|
:deep(.n-tabs-tab-panels),
|
||||||
.publish-view :deep(.n-tabs-pane-wrapper) {
|
:deep(.n-tabs-pane-wrapper) {
|
||||||
height: 100%;
|
height: 100%;
|
||||||
min-height: 0;
|
min-height: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.publish-view :deep(.n-tab-pane) {
|
:deep(.n-tab-pane) {
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
min-height: 0;
|
min-height: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.publish-view :deep(.n-tab-pane > .n-card) {
|
:deep(.n-tab-pane > .n-card) {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
min-height: 0;
|
min-height: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.publish-view :deep(.n-card__content) {
|
:deep(.n-card__content) {
|
||||||
flex: 1 1 auto;
|
flex: 1 1 auto;
|
||||||
min-height: 0;
|
min-height: 0;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
}
|
|
||||||
|
|
||||||
.publish-view :deep(.n-card__content > div) {
|
> div {
|
||||||
height: 100%;
|
height: 100%;
|
||||||
min-height: 0;
|
min-height: 0;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.topic-field {
|
.topic-field {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
|
||||||
|
|
||||||
.topic-field :deep(.n-form-item-blank) {
|
:deep(.n-form-item-blank) {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.publish-form {
|
.publish-form {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
min-height: 0;
|
min-height: 0;
|
||||||
}
|
|
||||||
|
|
||||||
.publish-form .payload-field {
|
.payload-field {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
min-height: 0;
|
min-height: 0;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
}
|
|
||||||
|
|
||||||
.publish-form .payload-field :deep(.n-form-item-blank) {
|
:deep(.n-form-item-blank) {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
min-height: 0;
|
min-height: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.publish-form .payload-field :deep(.n-input) {
|
:deep(.n-input) {
|
||||||
height: 100%;
|
height: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.publish-form .payload-field :deep(.n-input__textarea-el),
|
:deep(.n-input__textarea-el),
|
||||||
.publish-form .payload-field :deep(textarea) {
|
:deep(textarea) {
|
||||||
resize: none !important;
|
resize: none !important;
|
||||||
height: 100% !important;
|
height: 100% !important;
|
||||||
min-height: 0 !important;
|
min-height: 0 !important;
|
||||||
font-family: monospace;
|
font-family: monospace;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.msg-log {
|
.msg-log {
|
||||||
background: #f5f7fa;
|
background: var(--frost-color-bg-light);
|
||||||
border-radius: 6px;
|
border-radius: 6px;
|
||||||
padding: 12px;
|
padding: 12px;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
@@ -387,7 +411,7 @@ async function copyTopic(topic) {
|
|||||||
|
|
||||||
.msg-entry {
|
.msg-entry {
|
||||||
padding: 8px 0;
|
padding: 8px 0;
|
||||||
border-bottom: 1px solid rgba(0, 0, 0, 0.06);
|
border-bottom: 1px solid var(--frost-shadow-black-6);
|
||||||
}
|
}
|
||||||
|
|
||||||
.msg-header {
|
.msg-header {
|
||||||
@@ -398,7 +422,7 @@ async function copyTopic(topic) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.msg-time {
|
.msg-time {
|
||||||
color: #999;
|
color: var(--frost-color-text-tertiary);
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -406,31 +430,31 @@ async function copyTopic(topic) {
|
|||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
width: 28px;
|
width: 28px;
|
||||||
}
|
|
||||||
|
|
||||||
.msg-dir.pub {
|
&.pub {
|
||||||
color: #3b82f6;
|
color: var(--frost-info-color);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.msg-topic {
|
.msg-topic {
|
||||||
color: #d25a00;
|
color: var(--frost-color-topic);
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
flex: 1;
|
flex: 1;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
text-overflow: ellipsis;
|
text-overflow: ellipsis;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
|
||||||
|
|
||||||
.msg-topic:hover {
|
&: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;
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
background: rgba(0, 0, 0, 0.04);
|
background: var(--frost-shadow-black-4);
|
||||||
color: #333;
|
color: #333;
|
||||||
font: inherit;
|
font: inherit;
|
||||||
white-space: pre-wrap;
|
white-space: pre-wrap;
|
||||||
@@ -438,7 +462,7 @@ async function copyTopic(topic) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.msg-meta {
|
.msg-meta {
|
||||||
color: #999;
|
color: var(--frost-color-text-tertiary);
|
||||||
font-size: 11px;
|
font-size: 11px;
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,896 @@
|
|||||||
|
<script setup>
|
||||||
|
import { ref, computed, watch, onMounted, onUnmounted } from 'vue';
|
||||||
|
import { useMqttStore } from '../stores/mqtt.js';
|
||||||
|
import { NCard, NSpace, NFormItem, NInput, NSelect, NSwitch, NButton, NEmpty, NModal, NForm, NGrid, NGridItem, NCheckbox, useMessage, useDialog } from 'naive-ui';
|
||||||
|
|
||||||
|
// 全局状态与消息 API
|
||||||
|
const store = useMqttStore();
|
||||||
|
const msg = useMessage();
|
||||||
|
const dialog = useDialog();
|
||||||
|
|
||||||
|
// 旧版 localStorage key,用于数据迁移
|
||||||
|
const oldStorageKey = 'mqtt-client.quick-send-list';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @typedef {Object} QuickSendItem
|
||||||
|
* @property {string} id
|
||||||
|
* @property {string} topic
|
||||||
|
* @property {string} comment
|
||||||
|
* @property {string} payload
|
||||||
|
* @property {0|1|2} qos
|
||||||
|
* @property {boolean} retain
|
||||||
|
*/
|
||||||
|
|
||||||
|
// 快速发送列表
|
||||||
|
/** @type {import('vue').Ref<QuickSendItem[]>} */
|
||||||
|
const quickList = ref([]);
|
||||||
|
|
||||||
|
// 列表是否已从本地文件加载完成
|
||||||
|
const quickListLoaded = ref(false);
|
||||||
|
|
||||||
|
// 表单弹窗显隐
|
||||||
|
const showModal = ref(false);
|
||||||
|
const isEdit = ref(false);
|
||||||
|
|
||||||
|
// 表单数据
|
||||||
|
const formId = ref('');
|
||||||
|
const formTopic = ref('');
|
||||||
|
const formComment = ref('');
|
||||||
|
const formPayload = ref('');
|
||||||
|
const formQos = ref(1);
|
||||||
|
const formRetain = ref(false);
|
||||||
|
|
||||||
|
// 从旧版 localStorage 迁移数据
|
||||||
|
function migrateFromLocalStorage() {
|
||||||
|
try {
|
||||||
|
let saved = JSON.parse(localStorage.getItem(oldStorageKey) || '[]');
|
||||||
|
if (Array.isArray(saved) && saved.length > 0) {
|
||||||
|
quickList.value = saved.filter((item) => {
|
||||||
|
return item && typeof item.id === 'string' && typeof item.topic === 'string';
|
||||||
|
});
|
||||||
|
window.api.saveQuickSendList(JSON.parse(JSON.stringify(quickList.value)));
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
// 忽略旧数据解析错误
|
||||||
|
} finally {
|
||||||
|
localStorage.removeItem(oldStorageKey);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 列表变化时持久化到本地文件
|
||||||
|
watch(
|
||||||
|
quickList,
|
||||||
|
() => {
|
||||||
|
if (!quickListLoaded.value) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
window.api.saveQuickSendList(JSON.parse(JSON.stringify(quickList.value))).catch((e) => {
|
||||||
|
console.error('保存快速发送列表失败:', e);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
{ deep: true }
|
||||||
|
);
|
||||||
|
|
||||||
|
// QoS 下拉选项
|
||||||
|
const qosOptions = [
|
||||||
|
{ label: 'QoS 0 - 最多一次', value: 0 },
|
||||||
|
{ label: 'QoS 1 - 至少一次', value: 1 },
|
||||||
|
{ label: 'QoS 2 - 恰好一次', value: 2 },
|
||||||
|
];
|
||||||
|
|
||||||
|
// 当前是否有选中的服务器
|
||||||
|
const hasActiveServer = computed(() => Boolean(store.activeServer.value));
|
||||||
|
|
||||||
|
// 拖拽排序状态
|
||||||
|
/** @type {import('vue').Ref<number>} */
|
||||||
|
const dragIndex = ref(-1);
|
||||||
|
/** @type {import('vue').Ref<QuickSendItem | null>} */
|
||||||
|
const dragItem = ref(null);
|
||||||
|
|
||||||
|
// 列表容器引用与响应式布局
|
||||||
|
/** @type {import('vue').Ref<HTMLElement | null>} */
|
||||||
|
const containerRef = ref(null);
|
||||||
|
const columnCount = ref(1);
|
||||||
|
/** @type {ResizeObserver | null} */
|
||||||
|
let resizeObserver = null;
|
||||||
|
|
||||||
|
// 批量操作状态
|
||||||
|
/** @type {import('vue').Ref<string[]>} */
|
||||||
|
const selectedIds = ref([]);
|
||||||
|
/** @type {import('vue').Ref<HTMLInputElement | null>} */
|
||||||
|
const fileInput = ref(null);
|
||||||
|
|
||||||
|
// 是否已全选
|
||||||
|
const isAllSelected = computed(() => {
|
||||||
|
return quickList.value.length > 0 && selectedIds.value.length === quickList.value.length;
|
||||||
|
});
|
||||||
|
|
||||||
|
// 是否有选中项
|
||||||
|
const hasSelected = computed(() => selectedIds.value.length > 0);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 生成唯一 id
|
||||||
|
* @returns {string}
|
||||||
|
*/
|
||||||
|
function generateId() {
|
||||||
|
return Date.now().toString(36) + Math.random().toString(36).slice(2, 8);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 打开新增弹窗
|
||||||
|
*/
|
||||||
|
function handleAdd() {
|
||||||
|
isEdit.value = false;
|
||||||
|
formId.value = '';
|
||||||
|
formTopic.value = '';
|
||||||
|
formComment.value = '';
|
||||||
|
formPayload.value = '';
|
||||||
|
formQos.value = 1;
|
||||||
|
formRetain.value = false;
|
||||||
|
showModal.value = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 打开编辑弹窗
|
||||||
|
* @param {QuickSendItem} item
|
||||||
|
*/
|
||||||
|
function handleEdit(item) {
|
||||||
|
isEdit.value = true;
|
||||||
|
formId.value = item.id;
|
||||||
|
formTopic.value = item.topic;
|
||||||
|
formComment.value = item.comment || '';
|
||||||
|
formPayload.value = item.payload;
|
||||||
|
formQos.value = item.qos;
|
||||||
|
formRetain.value = item.retain;
|
||||||
|
showModal.value = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 保存快速发送项
|
||||||
|
*/
|
||||||
|
function handleSave() {
|
||||||
|
|
||||||
|
let topic = formTopic.value.trim();
|
||||||
|
|
||||||
|
if (!topic) {
|
||||||
|
msg.warning('请输入目标主题');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let comment = formComment.value.trim();
|
||||||
|
let payload = formPayload.value;
|
||||||
|
let qos = formQos.value;
|
||||||
|
let retain = formRetain.value;
|
||||||
|
|
||||||
|
if (isEdit.value) {
|
||||||
|
|
||||||
|
let idx = quickList.value.findIndex((item) => item.id === formId.value);
|
||||||
|
|
||||||
|
if (idx !== -1) {
|
||||||
|
quickList.value[idx] = {
|
||||||
|
...quickList.value[idx],
|
||||||
|
topic,
|
||||||
|
comment,
|
||||||
|
payload,
|
||||||
|
qos,
|
||||||
|
retain,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
} else {
|
||||||
|
|
||||||
|
quickList.value.push({
|
||||||
|
id: generateId(),
|
||||||
|
topic,
|
||||||
|
comment,
|
||||||
|
payload,
|
||||||
|
qos,
|
||||||
|
retain,
|
||||||
|
});
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
showModal.value = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 删除快速发送项
|
||||||
|
* @param {QuickSendItem} item
|
||||||
|
*/
|
||||||
|
function handleDelete(item) {
|
||||||
|
|
||||||
|
dialog.warning({
|
||||||
|
title: '确认删除',
|
||||||
|
content: `确定要删除主题 "${item.topic}" 的快速消息吗?`,
|
||||||
|
positiveText: '删除',
|
||||||
|
negativeText: '取消',
|
||||||
|
onPositiveClick: () => {
|
||||||
|
quickList.value = quickList.value.filter((i) => i.id !== item.id);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 开始拖拽
|
||||||
|
* @param {DragEvent} e
|
||||||
|
* @param {QuickSendItem} item
|
||||||
|
* @param {number} index
|
||||||
|
*/
|
||||||
|
function handleDragStart(e, item, index) {
|
||||||
|
dragIndex.value = index;
|
||||||
|
dragItem.value = item;
|
||||||
|
if (e.dataTransfer) {
|
||||||
|
e.dataTransfer.effectAllowed = 'move';
|
||||||
|
e.dataTransfer.setData('text/plain', item.id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 拖拽经过目标项
|
||||||
|
* @param {DragEvent} e
|
||||||
|
* @param {number} index
|
||||||
|
*/
|
||||||
|
function handleDragOver(e, index) {
|
||||||
|
e.preventDefault();
|
||||||
|
if (e.dataTransfer) {
|
||||||
|
e.dataTransfer.dropEffect = 'move';
|
||||||
|
}
|
||||||
|
if (dragIndex.value === -1 || dragIndex.value === index) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 放置拖拽项
|
||||||
|
* @param {DragEvent} e
|
||||||
|
* @param {number} index
|
||||||
|
*/
|
||||||
|
function handleDrop(e, index) {
|
||||||
|
e.preventDefault();
|
||||||
|
if (dragIndex.value === -1 || dragIndex.value === index) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let item = quickList.value[dragIndex.value];
|
||||||
|
|
||||||
|
quickList.value.splice(dragIndex.value, 1);
|
||||||
|
quickList.value.splice(index, 0, item);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 拖拽结束
|
||||||
|
*/
|
||||||
|
function handleDragEnd() {
|
||||||
|
dragIndex.value = -1;
|
||||||
|
dragItem.value = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 发送消息到当前服务器
|
||||||
|
* @param {QuickSendItem} item
|
||||||
|
*/
|
||||||
|
async function handleSend(item) {
|
||||||
|
|
||||||
|
let srv = store.activeServer.value;
|
||||||
|
|
||||||
|
if (!srv) {
|
||||||
|
msg.warning('请先在左侧选择一个 MQTT 服务器');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (srv.status !== 'connected') {
|
||||||
|
msg.warning('请先连接服务器');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let result = await store.publishMessage(srv.id, item.topic, item.payload, {
|
||||||
|
qos: item.qos,
|
||||||
|
retain: item.retain,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (result) {
|
||||||
|
msg.success('消息已发布到: ' + item.topic);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 复制主题到剪贴板
|
||||||
|
* @param {string} topic
|
||||||
|
*/
|
||||||
|
async function copyTopic(topic) {
|
||||||
|
try {
|
||||||
|
await navigator.clipboard.writeText(topic);
|
||||||
|
msg.success('主题已复制');
|
||||||
|
} catch {
|
||||||
|
msg.error('复制失败');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 格式化消息内容为 JSON
|
||||||
|
*/
|
||||||
|
function handleFormatJson() {
|
||||||
|
try {
|
||||||
|
|
||||||
|
let obj = JSON.parse(formPayload.value);
|
||||||
|
|
||||||
|
formPayload.value = JSON.stringify(obj, null, 2);
|
||||||
|
msg.info('JSON 已格式化');
|
||||||
|
|
||||||
|
} catch {
|
||||||
|
msg.warning('无效的 JSON 格式');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 选中或取消选中单个快速消息
|
||||||
|
* @param {string} id
|
||||||
|
* @param {boolean} checked
|
||||||
|
*/
|
||||||
|
function handleSelectItem(id, checked) {
|
||||||
|
if (checked) {
|
||||||
|
selectedIds.value.push(id);
|
||||||
|
} else {
|
||||||
|
selectedIds.value = selectedIds.value.filter((i) => i !== id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 切换全选状态
|
||||||
|
*/
|
||||||
|
function handleToggleSelectAll() {
|
||||||
|
if (isAllSelected.value) {
|
||||||
|
selectedIds.value = [];
|
||||||
|
} else {
|
||||||
|
selectedIds.value = quickList.value.map((item) => item.id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 批量删除选中的快速消息
|
||||||
|
*/
|
||||||
|
function handleBatchDelete() {
|
||||||
|
|
||||||
|
if (!hasSelected.value) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
dialog.warning({
|
||||||
|
title: '确认批量删除',
|
||||||
|
content: `确定要删除选中的 ${selectedIds.value.length} 条快速消息吗?`,
|
||||||
|
positiveText: '删除',
|
||||||
|
negativeText: '取消',
|
||||||
|
onPositiveClick: () => {
|
||||||
|
quickList.value = quickList.value.filter((item) => !selectedIds.value.includes(item.id));
|
||||||
|
selectedIds.value = [];
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 导出快速消息列表为 JSON 文件
|
||||||
|
*/
|
||||||
|
function handleExport() {
|
||||||
|
|
||||||
|
let data = quickList.value;
|
||||||
|
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-quick-send-${new Date().toISOString().slice(0, 10)}.json`;
|
||||||
|
a.click();
|
||||||
|
|
||||||
|
URL.revokeObjectURL(url);
|
||||||
|
msg.success('快速消息已导出');
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 触发文件导入点击
|
||||||
|
*/
|
||||||
|
function handleImportClick() {
|
||||||
|
fileInput.value?.click();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 处理导入的 JSON 文件
|
||||||
|
* @param {Event} event
|
||||||
|
*/
|
||||||
|
function handleImportFile(event) {
|
||||||
|
|
||||||
|
let file = event.target.files?.[0];
|
||||||
|
|
||||||
|
if (!file) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let reader = new FileReader();
|
||||||
|
|
||||||
|
reader.onload = (e) => {
|
||||||
|
try {
|
||||||
|
|
||||||
|
let data = JSON.parse(String(e.target?.result || ''));
|
||||||
|
|
||||||
|
if (!Array.isArray(data)) {
|
||||||
|
msg.error('导入失败: 文件内容不是数组');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let validItems = data.filter((item) => {
|
||||||
|
return item && typeof item.id === 'string' && typeof item.topic === 'string';
|
||||||
|
});
|
||||||
|
|
||||||
|
if (validItems.length === 0) {
|
||||||
|
msg.warning('未找到有效的快速消息');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
dialog.create({
|
||||||
|
title: '选择导入方式',
|
||||||
|
content: `检测到 ${validItems.length} 条快速消息,请选择导入方式:`,
|
||||||
|
positiveText: '追加',
|
||||||
|
negativeText: '覆盖',
|
||||||
|
class: 'quick-send-dialog',
|
||||||
|
onPositiveClick: () => {
|
||||||
|
let existingIds = new Set(quickList.value.map((item) => item.id));
|
||||||
|
let itemsToAdd = validItems.map((item) => {
|
||||||
|
if (existingIds.has(item.id)) {
|
||||||
|
return { ...item, id: generateId() };
|
||||||
|
}
|
||||||
|
return item;
|
||||||
|
});
|
||||||
|
quickList.value.push(...itemsToAdd);
|
||||||
|
selectedIds.value = [];
|
||||||
|
msg.success(`已追加 ${itemsToAdd.length} 条快速消息`);
|
||||||
|
},
|
||||||
|
onNegativeClick: () => {
|
||||||
|
quickList.value = validItems;
|
||||||
|
selectedIds.value = [];
|
||||||
|
msg.success(`已覆盖为导入的 ${validItems.length} 条快速消息`);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
} catch (err) {
|
||||||
|
msg.error('导入失败: ' + (err.message || '未知错误'));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
reader.onerror = () => {
|
||||||
|
msg.error('读取文件失败');
|
||||||
|
};
|
||||||
|
|
||||||
|
reader.readAsText(file);
|
||||||
|
event.target.value = '';
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateColumnCount(width) {
|
||||||
|
if (width >= 1520) {
|
||||||
|
columnCount.value = 3;
|
||||||
|
} else if (width >= 720) {
|
||||||
|
columnCount.value = 2;
|
||||||
|
} else {
|
||||||
|
columnCount.value = 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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) => {
|
||||||
|
for (let entry of entries) {
|
||||||
|
updateColumnCount(entry.contentRect.width);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
if (containerRef.value) {
|
||||||
|
resizeObserver.observe(containerRef.value);
|
||||||
|
updateColumnCount(containerRef.value.clientWidth);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
onUnmounted(() => {
|
||||||
|
if (resizeObserver) {
|
||||||
|
resizeObserver.disconnect();
|
||||||
|
resizeObserver = null;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
|
||||||
|
<div
|
||||||
|
ref="containerRef"
|
||||||
|
class="quick-send-view"
|
||||||
|
>
|
||||||
|
<n-card
|
||||||
|
size="small"
|
||||||
|
:content-style="{
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: 'column',
|
||||||
|
minHeight: 0,
|
||||||
|
overflow: 'hidden',
|
||||||
|
}"
|
||||||
|
>
|
||||||
|
<n-space
|
||||||
|
style="margin-bottom: 12px"
|
||||||
|
align="center"
|
||||||
|
>
|
||||||
|
<n-button
|
||||||
|
type="primary"
|
||||||
|
@click="handleAdd"
|
||||||
|
>新增快速消息</n-button>
|
||||||
|
<n-button
|
||||||
|
size="small"
|
||||||
|
:disabled="quickList.length === 0"
|
||||||
|
@click="handleToggleSelectAll"
|
||||||
|
>{{ isAllSelected ? '取消全选' : '全选' }}</n-button>
|
||||||
|
<n-button
|
||||||
|
size="small"
|
||||||
|
type="error"
|
||||||
|
ghost
|
||||||
|
:disabled="!hasSelected"
|
||||||
|
@click="handleBatchDelete"
|
||||||
|
>批量删除</n-button>
|
||||||
|
<n-button
|
||||||
|
size="small"
|
||||||
|
@click="handleImportClick"
|
||||||
|
>导入</n-button>
|
||||||
|
<n-button
|
||||||
|
size="small"
|
||||||
|
@click="handleExport"
|
||||||
|
>导出</n-button>
|
||||||
|
</n-space>
|
||||||
|
|
||||||
|
<n-empty
|
||||||
|
v-if="quickList.length === 0"
|
||||||
|
description="暂无快速发送消息,点击上方按钮添加"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div
|
||||||
|
v-else
|
||||||
|
class="quick-list"
|
||||||
|
:class="`columns-${columnCount}`"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
v-for="(item, index) in quickList"
|
||||||
|
:key="item.id"
|
||||||
|
class="quick-item"
|
||||||
|
:class="{ dragging: dragIndex === index }"
|
||||||
|
draggable="true"
|
||||||
|
@dragstart="handleDragStart($event, item, index)"
|
||||||
|
@dragover.prevent="handleDragOver($event, index)"
|
||||||
|
@drop.prevent="handleDrop($event, index)"
|
||||||
|
@dragend="handleDragEnd"
|
||||||
|
>
|
||||||
|
<div class="quick-header">
|
||||||
|
<n-checkbox
|
||||||
|
class="quick-checkbox"
|
||||||
|
:checked="selectedIds.includes(item.id)"
|
||||||
|
@update:checked="(checked) => handleSelectItem(item.id, checked)"
|
||||||
|
@click.stop
|
||||||
|
/>
|
||||||
|
<div class="quick-topic-wrap">
|
||||||
|
<span class="quick-dir pub">PUB</span>
|
||||||
|
<span
|
||||||
|
class="quick-topic"
|
||||||
|
:title="item.topic"
|
||||||
|
@click="copyTopic(item.topic)"
|
||||||
|
>{{ item.topic }}</span>
|
||||||
|
<span class="quick-meta">QoS{{ item.qos }}{{ item.retain ? ' R' : '' }}</span>
|
||||||
|
</div>
|
||||||
|
<n-space>
|
||||||
|
<n-button
|
||||||
|
size="tiny"
|
||||||
|
:disabled="!hasActiveServer"
|
||||||
|
type="primary"
|
||||||
|
@click="handleSend(item)"
|
||||||
|
>发送</n-button>
|
||||||
|
<n-button
|
||||||
|
size="tiny"
|
||||||
|
@click="handleEdit(item)"
|
||||||
|
>编辑</n-button>
|
||||||
|
<n-button
|
||||||
|
size="tiny"
|
||||||
|
type="error"
|
||||||
|
ghost
|
||||||
|
@click="handleDelete(item)"
|
||||||
|
>删除</n-button>
|
||||||
|
</n-space>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
v-if="item.comment"
|
||||||
|
class="quick-comment"
|
||||||
|
:title="item.comment"
|
||||||
|
>{{ item.comment }}</div>
|
||||||
|
<pre
|
||||||
|
v-if="item.payload"
|
||||||
|
class="quick-payload"
|
||||||
|
>{{ item.payload }}</pre>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<input
|
||||||
|
ref="fileInput"
|
||||||
|
type="file"
|
||||||
|
accept=".json,application/json"
|
||||||
|
style="display: none"
|
||||||
|
@change="handleImportFile"
|
||||||
|
>
|
||||||
|
</n-card>
|
||||||
|
|
||||||
|
<n-modal
|
||||||
|
v-model:show="showModal"
|
||||||
|
class="quick-send-modal"
|
||||||
|
:title="isEdit ? '编辑快速消息' : '新增快速消息'"
|
||||||
|
preset="card"
|
||||||
|
style="width: 520px; max-width: 90vw"
|
||||||
|
:bordered="false"
|
||||||
|
:segmented="{ content: true }"
|
||||||
|
>
|
||||||
|
<n-form
|
||||||
|
label-placement="top"
|
||||||
|
:show-feedback="false"
|
||||||
|
>
|
||||||
|
<n-form-item label="目标主题">
|
||||||
|
<n-input
|
||||||
|
v-model:value="formTopic"
|
||||||
|
placeholder="例如: sensor/device01/temperature"
|
||||||
|
/>
|
||||||
|
</n-form-item>
|
||||||
|
<n-form-item label="备注">
|
||||||
|
<n-input
|
||||||
|
v-model:value="formComment"
|
||||||
|
placeholder="可选,用于标识该快速消息的用途"
|
||||||
|
/>
|
||||||
|
</n-form-item>
|
||||||
|
<n-grid
|
||||||
|
cols="2"
|
||||||
|
x-gap="12"
|
||||||
|
>
|
||||||
|
<n-grid-item>
|
||||||
|
<n-form-item label="QoS">
|
||||||
|
<n-select
|
||||||
|
v-model:value="formQos"
|
||||||
|
:options="qosOptions"
|
||||||
|
/>
|
||||||
|
</n-form-item>
|
||||||
|
</n-grid-item>
|
||||||
|
<n-grid-item>
|
||||||
|
<n-form-item label="保留消息">
|
||||||
|
<n-switch v-model:value="formRetain" />
|
||||||
|
</n-form-item>
|
||||||
|
</n-grid-item>
|
||||||
|
</n-grid>
|
||||||
|
<n-form-item label="消息内容">
|
||||||
|
<div class="payload-field">
|
||||||
|
<n-input
|
||||||
|
v-model:value="formPayload"
|
||||||
|
type="textarea"
|
||||||
|
:resizable="false"
|
||||||
|
:rows="6"
|
||||||
|
placeholder="输入要发送的消息内容"
|
||||||
|
/>
|
||||||
|
<n-button
|
||||||
|
size="small"
|
||||||
|
@click="handleFormatJson"
|
||||||
|
>格式化 JSON</n-button>
|
||||||
|
</div>
|
||||||
|
</n-form-item>
|
||||||
|
</n-form>
|
||||||
|
<template #footer>
|
||||||
|
<n-space justify="end">
|
||||||
|
<n-button @click="showModal = false">取消</n-button>
|
||||||
|
<n-button
|
||||||
|
type="primary"
|
||||||
|
@click="handleSave"
|
||||||
|
>保存</n-button>
|
||||||
|
</n-space>
|
||||||
|
</template>
|
||||||
|
</n-modal>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped lang="less">
|
||||||
|
.quick-send-view {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
min-width: 0;
|
||||||
|
min-height: 0;
|
||||||
|
|
||||||
|
:deep(.n-card) {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
height: 100%;
|
||||||
|
min-height: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.n-card__content) {
|
||||||
|
flex: 1 1 auto;
|
||||||
|
min-height: 0;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.quick-list {
|
||||||
|
flex: 1;
|
||||||
|
min-height: 0;
|
||||||
|
overflow-y: auto;
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: minmax(0, 1fr);
|
||||||
|
gap: 12px;
|
||||||
|
padding-right: 4px;
|
||||||
|
align-content: start;
|
||||||
|
|
||||||
|
&.columns-1 {
|
||||||
|
grid-template-columns: minmax(0, 1fr);
|
||||||
|
}
|
||||||
|
|
||||||
|
&.columns-2 {
|
||||||
|
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||||
|
}
|
||||||
|
|
||||||
|
&.columns-3 {
|
||||||
|
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.payload-field {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 8px;
|
||||||
|
width: 100%;
|
||||||
|
|
||||||
|
:deep(.n-input) {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.n-button {
|
||||||
|
align-self: flex-end;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.quick-item {
|
||||||
|
border: 1px solid var(--frost-shadow-black-8);
|
||||||
|
border-radius: 6px;
|
||||||
|
padding: 12px;
|
||||||
|
background: var(--frost-color-bg-item);
|
||||||
|
cursor: grab;
|
||||||
|
transition: box-shadow 0.2s, opacity 0.2s;
|
||||||
|
|
||||||
|
&.dragging {
|
||||||
|
opacity: 0.6;
|
||||||
|
box-shadow: 0 4px 12px var(--frost-shadow-black-12);
|
||||||
|
cursor: grabbing;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.quick-header {
|
||||||
|
display: flex;
|
||||||
|
align-items: flex-start;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 8px;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.quick-checkbox {
|
||||||
|
flex-shrink: 0;
|
||||||
|
margin-top: 2px;
|
||||||
|
|
||||||
|
:deep(.n-checkbox-box) {
|
||||||
|
width: 16px;
|
||||||
|
height: 16px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.quick-topic-wrap {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
min-width: 0;
|
||||||
|
flex: 1;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.quick-dir {
|
||||||
|
flex-shrink: 0;
|
||||||
|
font-weight: 700;
|
||||||
|
width: 28px;
|
||||||
|
font-size: 12px;
|
||||||
|
|
||||||
|
&.pub {
|
||||||
|
color: var(--frost-info-color);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.quick-topic {
|
||||||
|
color: var(--frost-color-topic);
|
||||||
|
min-width: 0;
|
||||||
|
flex: 1;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
cursor: pointer;
|
||||||
|
font-family: monospace;
|
||||||
|
font-size: 13px;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.quick-meta {
|
||||||
|
color: var(--frost-color-text-tertiary);
|
||||||
|
font-size: 11px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
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 {
|
||||||
|
margin: 10px 0 0;
|
||||||
|
padding: 8px 10px;
|
||||||
|
border-radius: 4px;
|
||||||
|
background: var(--frost-shadow-black-4);
|
||||||
|
color: #333;
|
||||||
|
font: inherit;
|
||||||
|
white-space: pre-wrap;
|
||||||
|
overflow-wrap: anywhere;
|
||||||
|
font-family: monospace;
|
||||||
|
font-size: 12px;
|
||||||
|
line-height: 1.6;
|
||||||
|
max-height: 120px;
|
||||||
|
overflow-y: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.quick-send-modal {
|
||||||
|
.n-form-item {
|
||||||
|
margin-bottom: 20px;
|
||||||
|
|
||||||
|
&__label {
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:last-child {
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.n-grid {
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.quick-send-dialog {
|
||||||
|
:deep(.n-dialog__icon) {
|
||||||
|
color: var(--frost-primary-color);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -1,105 +1,142 @@
|
|||||||
<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, NButton, NEmpty, NList, NListItem, NTag, useMessage } from 'naive-ui';
|
||||||
NCard,
|
|
||||||
NSpace,
|
|
||||||
NFormItem,
|
|
||||||
NInput,
|
|
||||||
NSelect,
|
|
||||||
NButton,
|
|
||||||
NEmpty,
|
|
||||||
NList,
|
|
||||||
NListItem,
|
|
||||||
NTag,
|
|
||||||
NEllipsis,
|
|
||||||
useMessage,
|
|
||||||
} from 'naive-ui';
|
|
||||||
|
|
||||||
|
// 全局状态与消息 API
|
||||||
const store = useMqttStore();
|
const store = useMqttStore();
|
||||||
const msg = useMessage();
|
const msg = useMessage();
|
||||||
|
|
||||||
|
// 主题输入与 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 下拉选项
|
||||||
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 },
|
||||||
];
|
];
|
||||||
|
|
||||||
|
// 表单本地存储 key
|
||||||
const formStorageKey = 'mqtt-client.subscribe-form';
|
const formStorageKey = 'mqtt-client.subscribe-form';
|
||||||
|
|
||||||
let formReady = false;
|
let formReady = false;
|
||||||
|
|
||||||
|
// 从 localStorage 恢复表单输入
|
||||||
try {
|
try {
|
||||||
const savedForm = JSON.parse(localStorage.getItem(formStorageKey) || '{}');
|
|
||||||
|
let savedForm = JSON.parse(localStorage.getItem(formStorageKey) || '{}');
|
||||||
|
|
||||||
if (typeof savedForm.topicInput === 'string') {
|
if (typeof savedForm.topicInput === 'string') {
|
||||||
topicInput.value = savedForm.topicInput;
|
topicInput.value = savedForm.topicInput;
|
||||||
}
|
}
|
||||||
|
|
||||||
if ([0, 1, 2].includes(savedForm.qos)) {
|
if ([0, 1, 2].includes(savedForm.qos)) {
|
||||||
qos.value = savedForm.qos;
|
qos.value = savedForm.qos;
|
||||||
}
|
}
|
||||||
|
|
||||||
} catch {
|
} catch {
|
||||||
localStorage.removeItem(formStorageKey);
|
localStorage.removeItem(formStorageKey);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 表单变化时持久化到 localStorage
|
||||||
watch([topicInput, qos], () => {
|
watch([topicInput, qos], () => {
|
||||||
|
|
||||||
if (!formReady) {
|
if (!formReady) {
|
||||||
return;
|
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();
|
|
||||||
|
let topic = topicInput.value.trim();
|
||||||
|
|
||||||
if (!topic) {
|
if (!topic) {
|
||||||
msg.warning('请输入主题名称');
|
msg.warning('请输入主题名称');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const srv = store.activeServer.value;
|
|
||||||
|
let srv = store.activeServer.value;
|
||||||
|
|
||||||
if (!srv) {
|
if (!srv) {
|
||||||
return;
|
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);
|
|
||||||
|
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 = '';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 移除主题
|
||||||
|
* @param {string} topic - 主题名称
|
||||||
|
*/
|
||||||
async function handleRemoveTopic(topic) {
|
async function handleRemoveTopic(topic) {
|
||||||
const srv = store.activeServer.value;
|
|
||||||
|
let srv = store.activeServer.value;
|
||||||
|
|
||||||
if (!srv) {
|
if (!srv) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
await store.removeTopic(srv.id, topic);
|
|
||||||
msg.info('主题已移除');
|
|
||||||
}
|
|
||||||
|
|
||||||
|
await store.removeTopic(srv.id, topic);
|
||||||
|
|
||||||
|
msg.info('主题已移除');
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 切换单个主题订阅状态
|
||||||
|
* @param {string} topic - 主题名称
|
||||||
|
*/
|
||||||
async function handleToggleSub(topic) {
|
async function handleToggleSub(topic) {
|
||||||
const srv = store.activeServer.value;
|
|
||||||
|
let srv = store.activeServer.value;
|
||||||
|
|
||||||
if (!srv) {
|
if (!srv) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (srv.status !== 'connected') {
|
if (srv.status !== 'connected') {
|
||||||
msg.warning('请先连接服务器');
|
msg.warning('请先连接服务器');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const t = srv.topics.find((t) => t.topic === topic);
|
|
||||||
|
let t = srv.topics.find((t) => t.topic === topic);
|
||||||
|
|
||||||
if (!t) {
|
if (!t) {
|
||||||
return;
|
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);
|
||||||
@@ -107,43 +144,70 @@ async function handleToggleSub(topic) {
|
|||||||
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;
|
|
||||||
|
let srv = store.activeServer.value;
|
||||||
|
|
||||||
if (!srv) {
|
if (!srv) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (srv.status !== 'connected') {
|
if (srv.status !== 'connected') {
|
||||||
msg.warning('请先连接服务器');
|
msg.warning('请先连接服务器');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
await store.subscribeAllTopics(srv.id);
|
await store.subscribeAllTopics(srv.id);
|
||||||
msg.success('已订阅全部主题');
|
msg.success('已订阅全部主题');
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 进入主题编辑状态
|
||||||
|
* @param {import('@types/jsdoc').MqttTopic} t - 主题对象
|
||||||
|
*/
|
||||||
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;
|
||||||
|
editingComment.value = t.comment ?? '';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 取消主题编辑
|
||||||
|
*/
|
||||||
function cancelEdit() {
|
function cancelEdit() {
|
||||||
editingTopicId.value = null;
|
editingTopicId.value = null;
|
||||||
editingTopic.value = '';
|
editingTopic.value = '';
|
||||||
editingQos.value = 2;
|
editingQos.value = 2;
|
||||||
|
editingComment.value = '';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 保存主题编辑
|
||||||
|
* @param {import('@types/jsdoc').MqttTopic} t - 主题对象
|
||||||
|
*/
|
||||||
async function handleEditSave(t) {
|
async function handleEditSave(t) {
|
||||||
const srv = store.activeServer.value;
|
|
||||||
|
let srv = store.activeServer.value;
|
||||||
|
|
||||||
if (!srv) {
|
if (!srv) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const topicName = editingTopic.value.trim();
|
|
||||||
|
let 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
|
||||||
@@ -152,22 +216,34 @@ async function handleEditSave(t) {
|
|||||||
msg.warning('该主题已存在');
|
msg.warning('该主题已存在');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const topicId = t.id || t.topic;
|
|
||||||
const result = await store.updateTopic(srv.id, topicId, topicName, editingQos.value);
|
let topicId = t.id || t.topic;
|
||||||
|
let result = await store.updateTopic(
|
||||||
|
srv.id,
|
||||||
|
topicId,
|
||||||
|
topicName,
|
||||||
|
editingQos.value,
|
||||||
|
editingComment.value.trim()
|
||||||
|
);
|
||||||
|
|
||||||
if (result) {
|
if (result) {
|
||||||
msg.success('主题已更新');
|
msg.success('主题已更新');
|
||||||
cancelEdit();
|
cancelEdit();
|
||||||
} else {
|
} else {
|
||||||
msg.error('主题更新失败');
|
msg.error('主题更新失败');
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
|
|
||||||
|
<!-- 内容区域 -->
|
||||||
<div
|
<div
|
||||||
v-if="store.activeServer.value"
|
v-if="store.activeServer.value"
|
||||||
class="subscribe-view"
|
class="subscribe-view"
|
||||||
>
|
>
|
||||||
|
|
||||||
<!-- 添加主题 -->
|
<!-- 添加主题 -->
|
||||||
<n-card
|
<n-card
|
||||||
title="添加订阅主题"
|
title="添加订阅主题"
|
||||||
@@ -184,6 +260,16 @@ async function handleEditSave(t) {
|
|||||||
@keydown.enter="handleAddTopic"
|
@keydown.enter="handleAddTopic"
|
||||||
/>
|
/>
|
||||||
</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-form-item
|
<n-form-item
|
||||||
class="qos-field"
|
class="qos-field"
|
||||||
label="QoS"
|
label="QoS"
|
||||||
@@ -197,9 +283,7 @@ async function handleEditSave(t) {
|
|||||||
class="topic-submit"
|
class="topic-submit"
|
||||||
type="primary"
|
type="primary"
|
||||||
@click="handleAddTopic"
|
@click="handleAddTopic"
|
||||||
>
|
>+ 添加订阅</n-button>
|
||||||
+ 添加订阅
|
|
||||||
</n-button>
|
|
||||||
</div>
|
</div>
|
||||||
</n-card>
|
</n-card>
|
||||||
|
|
||||||
@@ -219,9 +303,7 @@ async function handleEditSave(t) {
|
|||||||
<n-button
|
<n-button
|
||||||
size="small"
|
size="small"
|
||||||
@click="handleSubscribeAll"
|
@click="handleSubscribeAll"
|
||||||
>
|
>全部订阅</n-button>
|
||||||
全部订阅
|
|
||||||
</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"
|
||||||
@@ -237,34 +319,55 @@ async function handleEditSave(t) {
|
|||||||
:key="t.id || t.topic"
|
:key="t.id || t.topic"
|
||||||
class="topic-item"
|
class="topic-item"
|
||||||
>
|
>
|
||||||
<template
|
<template #prefix>
|
||||||
v-if="editingTopicId !== (t.id || t.topic)"
|
|
||||||
#prefix
|
|
||||||
>
|
|
||||||
<n-tag
|
<n-tag
|
||||||
|
class="topic-qos-tag"
|
||||||
:type="t.subscribed ? 'success' : 'default'"
|
:type="t.subscribed ? 'success' : 'default'"
|
||||||
size="small"
|
size="small"
|
||||||
>
|
>QoS {{ t.qos }}</n-tag>
|
||||||
QoS {{ t.qos }}
|
|
||||||
</n-tag>
|
|
||||||
</template>
|
</template>
|
||||||
|
<div class="topic-content">
|
||||||
|
<div
|
||||||
|
class="topic-name"
|
||||||
|
:class="{ 'is-hidden': editingTopicId === (t.id || t.topic) }"
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
class="topic-label"
|
||||||
|
:title="t.topic"
|
||||||
|
>{{ t.topic }}</span>
|
||||||
|
<div class="topic-comment">
|
||||||
|
<span class="topic-comment-prefix">备注</span>
|
||||||
|
<span
|
||||||
|
class="topic-comment-text"
|
||||||
|
:title="t.comment"
|
||||||
|
>{{ t.comment }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<div
|
<div
|
||||||
v-if="editingTopicId === (t.id || t.topic)"
|
|
||||||
class="topic-edit-row"
|
class="topic-edit-row"
|
||||||
|
:class="{ 'is-visible': editingTopicId === (t.id || t.topic) }"
|
||||||
>
|
>
|
||||||
<n-input
|
<n-input
|
||||||
v-model:value="editingTopic"
|
v-model:value="editingTopic"
|
||||||
|
class="topic-edit-topic"
|
||||||
size="small"
|
size="small"
|
||||||
placeholder="主题名称"
|
placeholder="主题名称"
|
||||||
@keydown.enter="handleEditSave(t)"
|
@keydown.enter="handleEditSave(t)"
|
||||||
/>
|
/>
|
||||||
|
<n-input
|
||||||
|
v-model:value="editingComment"
|
||||||
|
class="topic-edit-comment"
|
||||||
|
size="small"
|
||||||
|
placeholder="备注"
|
||||||
|
/>
|
||||||
<n-select
|
<n-select
|
||||||
v-model:value="editingQos"
|
v-model:value="editingQos"
|
||||||
|
class="topic-edit-qos"
|
||||||
size="small"
|
size="small"
|
||||||
:options="qosOptions"
|
:options="qosOptions"
|
||||||
style="width: 160px; flex-shrink: 0"
|
|
||||||
/>
|
/>
|
||||||
<n-space
|
<n-space
|
||||||
|
class="topic-edit-actions"
|
||||||
:size="4"
|
:size="4"
|
||||||
:wrap="false"
|
:wrap="false"
|
||||||
>
|
>
|
||||||
@@ -272,65 +375,58 @@ async function handleEditSave(t) {
|
|||||||
size="tiny"
|
size="tiny"
|
||||||
type="primary"
|
type="primary"
|
||||||
@click="handleEditSave(t)"
|
@click="handleEditSave(t)"
|
||||||
>
|
>保存</n-button>
|
||||||
保存
|
|
||||||
</n-button>
|
|
||||||
<n-button
|
<n-button
|
||||||
size="tiny"
|
size="tiny"
|
||||||
@click="cancelEdit"
|
@click="cancelEdit"
|
||||||
>
|
>取消</n-button>
|
||||||
取消
|
|
||||||
</n-button>
|
|
||||||
</n-space>
|
</n-space>
|
||||||
</div>
|
</div>
|
||||||
<div
|
|
||||||
v-else
|
|
||||||
class="topic-name"
|
|
||||||
>
|
|
||||||
<n-ellipsis>{{ t.topic }}</n-ellipsis>
|
|
||||||
</div>
|
</div>
|
||||||
<template #suffix>
|
<template #suffix>
|
||||||
<n-space
|
<n-space
|
||||||
v-if="editingTopicId !== (t.id || t.topic)"
|
|
||||||
class="topic-actions"
|
class="topic-actions"
|
||||||
|
:class="{ 'is-hidden': editingTopicId === (t.id || t.topic) }"
|
||||||
:size="4"
|
:size="4"
|
||||||
:wrap="false"
|
:wrap="false"
|
||||||
>
|
>
|
||||||
<n-button
|
<n-button
|
||||||
size="tiny"
|
size="tiny"
|
||||||
@click="startEdit(t)"
|
@click="startEdit(t)"
|
||||||
>
|
>编辑</n-button>
|
||||||
编辑
|
|
||||||
</n-button>
|
|
||||||
<n-button
|
<n-button
|
||||||
|
class="topic-action-sub"
|
||||||
size="tiny"
|
size="tiny"
|
||||||
:type="t.subscribed ? 'error' : 'success'"
|
:type="t.subscribed ? 'success' : 'primary'"
|
||||||
@click="handleToggleSub(t.topic)"
|
@click="handleToggleSub(t.topic)"
|
||||||
>
|
>
|
||||||
{{ t.subscribed ? '取消订阅' : '订阅' }}
|
{{ t.subscribed ? '已订阅' : '未订阅' }}
|
||||||
</n-button>
|
</n-button>
|
||||||
<n-button
|
<n-button
|
||||||
|
class="topic-action-remove"
|
||||||
size="tiny"
|
size="tiny"
|
||||||
type="error"
|
type="error"
|
||||||
text
|
text
|
||||||
@click.stop="handleRemoveTopic(t.topic)"
|
@click.stop="handleRemoveTopic(t.topic)"
|
||||||
>
|
>✕</n-button>
|
||||||
✕
|
|
||||||
</n-button>
|
|
||||||
</n-space>
|
</n-space>
|
||||||
</template>
|
</template>
|
||||||
</n-list-item>
|
</n-list-item>
|
||||||
</n-list>
|
</n-list>
|
||||||
</div>
|
</div>
|
||||||
</n-card>
|
</n-card>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- 空状态 -->
|
||||||
<n-empty
|
<n-empty
|
||||||
v-else
|
v-else
|
||||||
description="请先在左侧选择一个 MQTT 服务器"
|
description="请先在左侧选择一个 MQTT 服务器"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped lang="less">
|
||||||
.subscribe-view {
|
.subscribe-view {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
@@ -374,45 +470,168 @@ 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,
|
||||||
.qos-field :deep(.n-form-item-blank) {
|
.qos-field,
|
||||||
|
.comment-field {
|
||||||
|
:deep(.n-form-item-blank) {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.topic-item :deep(.n-list-item__main) {
|
.topic-item {
|
||||||
|
box-sizing: border-box;
|
||||||
|
height: 56px;
|
||||||
|
min-height: 56px;
|
||||||
|
padding-top: 0 !important;
|
||||||
|
padding-bottom: 0 !important;
|
||||||
|
|
||||||
|
:deep(.n-list-item__main) {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
height: 100%;
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
gap: 12px;
|
gap: 12px;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.topic-content {
|
||||||
|
position: relative;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
flex: 1;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
min-width: 0;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
.topic-name {
|
.topic-name {
|
||||||
min-width: 0;
|
display: flex;
|
||||||
flex: 1;
|
flex: 1;
|
||||||
font-family: monospace;
|
flex-direction: column;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: stretch;
|
||||||
|
width: 100%;
|
||||||
|
height: 40px;
|
||||||
|
min-width: 0;
|
||||||
|
min-height: 0;
|
||||||
|
gap: 2px;
|
||||||
font-size: 13px;
|
font-size: 13px;
|
||||||
|
overflow: hidden;
|
||||||
|
visibility: visible;
|
||||||
|
|
||||||
|
&.is-hidden {
|
||||||
|
visibility: hidden;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.topic-label {
|
||||||
|
min-width: 0;
|
||||||
|
overflow: hidden;
|
||||||
|
font-family: monospace;
|
||||||
|
line-height: 18px;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.topic-comment {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
min-width: 0;
|
||||||
|
max-width: 100%;
|
||||||
|
padding: 1px 4px;
|
||||||
|
gap: 4px;
|
||||||
|
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: 11px;
|
||||||
|
line-height: 16px;
|
||||||
|
|
||||||
|
&-prefix {
|
||||||
|
flex-shrink: 0;
|
||||||
|
color: #0f766e;
|
||||||
|
}
|
||||||
|
|
||||||
|
&-text {
|
||||||
|
flex: 1;
|
||||||
|
min-width: 0;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.topic-qos-tag {
|
||||||
|
min-width: 56px;
|
||||||
|
justify-content: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
.topic-actions {
|
.topic-actions {
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
|
visibility: visible;
|
||||||
|
|
||||||
|
&.is-hidden {
|
||||||
|
visibility: hidden;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.n-button {
|
||||||
|
min-width: 40px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.topic-action-sub {
|
||||||
|
min-width: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.topic-action-remove {
|
||||||
|
min-width: 24px;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.topic-edit-row {
|
.topic-edit-row {
|
||||||
|
position: absolute;
|
||||||
|
inset: 0;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
flex-wrap: wrap;
|
flex-wrap: nowrap;
|
||||||
gap: 8px;
|
gap: 8px;
|
||||||
flex: 1;
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
min-width: 0;
|
||||||
|
overflow: hidden;
|
||||||
|
visibility: hidden;
|
||||||
|
pointer-events: none;
|
||||||
|
|
||||||
|
&.is-visible {
|
||||||
|
visibility: visible;
|
||||||
|
pointer-events: auto;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.topic-edit-topic {
|
||||||
|
flex: 1 1 auto;
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.topic-edit-row .n-input {
|
.topic-edit-comment {
|
||||||
flex: 1;
|
flex: 1 1 auto;
|
||||||
min-width: 180px;
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.topic-edit-qos {
|
||||||
|
flex: 0 0 auto;
|
||||||
|
width: 160px;
|
||||||
|
min-width: 80px;
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (max-width: 720px) {
|
@media (max-width: 720px) {
|
||||||
|
|||||||
Vendored
+118
@@ -0,0 +1,118 @@
|
|||||||
|
/**
|
||||||
|
* 公共 MQTT 类型定义
|
||||||
|
* 供 renderer 与 main 进程共享,避免在多个模块中重复定义 JSDoc @typedef
|
||||||
|
*/
|
||||||
|
|
||||||
|
/** MQTT QoS 等级 */
|
||||||
|
export type MqttQoS = 0 | 1 | 2;
|
||||||
|
|
||||||
|
/** MQTT 服务器连接状态 */
|
||||||
|
export type MqttServerStatus = 'disconnected' | 'connecting' | 'connected' | 'error';
|
||||||
|
|
||||||
|
/** MQTT 主题 */
|
||||||
|
export interface MqttTopic {
|
||||||
|
/** 主题唯一标识 */
|
||||||
|
id: string;
|
||||||
|
/** 主题名称(支持通配符) */
|
||||||
|
topic: string;
|
||||||
|
/** 订阅 QoS 等级 */
|
||||||
|
qos: MqttQoS;
|
||||||
|
/** 主题备注,空字符串表示未备注 */
|
||||||
|
comment: string;
|
||||||
|
/** 当前是否已订阅 */
|
||||||
|
subscribed: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 主题配置(用于导入/导出或表单) */
|
||||||
|
export interface TopicConfig {
|
||||||
|
/** 主题唯一标识,未提供时由后端生成 */
|
||||||
|
id?: string;
|
||||||
|
/** 主题名称(支持通配符) */
|
||||||
|
topic: string;
|
||||||
|
/** 订阅 QoS 等级,默认 1 */
|
||||||
|
qos?: MqttQoS;
|
||||||
|
/** 主题备注,默认空字符串 */
|
||||||
|
comment?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** MQTT 服务器配置(不含运行时状态) */
|
||||||
|
export interface ServerConfig {
|
||||||
|
/** 显示名称 */
|
||||||
|
name: string;
|
||||||
|
/** 主机地址 */
|
||||||
|
host: string;
|
||||||
|
/** 端口号,默认 1883 */
|
||||||
|
port?: number;
|
||||||
|
/** 连接协议,默认 'mqtt' */
|
||||||
|
protocol?: 'mqtt' | 'mqtts' | 'ws' | 'wss';
|
||||||
|
/** 客户端 ID,默认随机生成 */
|
||||||
|
clientId?: string;
|
||||||
|
/** 用户名,默认空字符串 */
|
||||||
|
username?: string;
|
||||||
|
/** 密码,默认空字符串 */
|
||||||
|
password?: string;
|
||||||
|
/** 心跳间隔(秒),默认 60 */
|
||||||
|
keepAlive?: number;
|
||||||
|
/** 是否清理会话,默认 true */
|
||||||
|
cleanSession?: boolean;
|
||||||
|
/** 连接超时(毫秒),默认 10000 */
|
||||||
|
connectTimeout?: number;
|
||||||
|
/** 是否自动重连,默认 true */
|
||||||
|
reconnect?: boolean;
|
||||||
|
/** 重连间隔(毫秒),默认 5000 */
|
||||||
|
reconnectInterval?: number;
|
||||||
|
/** 初始主题列表 */
|
||||||
|
topics?: TopicConfig[];
|
||||||
|
}
|
||||||
|
|
||||||
|
/** MQTT 服务器运行时对象 */
|
||||||
|
export interface MqttServer extends ServerConfig {
|
||||||
|
/** 服务器唯一标识 */
|
||||||
|
id: string;
|
||||||
|
/** 当前连接状态 */
|
||||||
|
status: MqttServerStatus;
|
||||||
|
/** 该服务器下的主题列表 */
|
||||||
|
topics: MqttTopic[];
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 发布/订阅选项 */
|
||||||
|
export interface PublishOptions {
|
||||||
|
/** QoS 等级,默认 1 */
|
||||||
|
qos?: MqttQoS;
|
||||||
|
/** 是否保留消息,默认 false */
|
||||||
|
retain?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** MQTT 消息(订阅接收或本地发布) */
|
||||||
|
export interface MqttMessage {
|
||||||
|
/** 消息唯一标识 */
|
||||||
|
id: string;
|
||||||
|
/** 接收/发送时间(格式为 HH:mm:ss.SSS) */
|
||||||
|
time: string;
|
||||||
|
/** 消息主题 */
|
||||||
|
topic: string;
|
||||||
|
/** 消息负载内容 */
|
||||||
|
payload: string;
|
||||||
|
/** 消息 QoS 等级 */
|
||||||
|
qos: MqttQoS;
|
||||||
|
/** 是否为保留消息 */
|
||||||
|
retain: boolean;
|
||||||
|
/** 消息方向:pub 表示发布,sub 表示订阅 */
|
||||||
|
direction: 'pub' | 'sub';
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 发布历史记录 */
|
||||||
|
export interface PublishRecord {
|
||||||
|
/** 发布时间(格式为 HH:mm:ss.SSS) */
|
||||||
|
time: string;
|
||||||
|
/** 目标主题 */
|
||||||
|
topic: string;
|
||||||
|
/** 发布内容 */
|
||||||
|
payload: string;
|
||||||
|
/** 发布 QoS 等级 */
|
||||||
|
qos: MqttQoS;
|
||||||
|
/** 是否保留消息 */
|
||||||
|
retain: boolean;
|
||||||
|
/** 固定为 pub */
|
||||||
|
direction: 'pub';
|
||||||
|
}
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
# CHANGELOG
|
||||||
|
|
||||||
|
## [1.0.1] - 2026-07-21
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- 快速发送列表持久化到本地文件 `frost-mqtt-data.json`。
|
||||||
|
- 快速消息支持备注。
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
- 解决“快速发送”模块中,主题名称较长时,外层卡片缩小到一定程度后不能再随外层容器缩小而缩小的问题。
|
||||||
|
|
||||||
|
## [1.0.0] - 2026-07-21
|
||||||
|
|
||||||
|
- 初始版本发布。
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
# TODO
|
||||||
|
|
||||||
|
- [X] 过滤主题功能支持下拉
|
||||||
|
- [X] 订阅主题备注
|
||||||
@@ -0,0 +1,52 @@
|
|||||||
|
# 代码风格
|
||||||
|
|
||||||
|
## 基础格式
|
||||||
|
|
||||||
|
- 使用 UTF-8、LF 换行、2 个空格缩进;文件末尾保留换行,删除行尾空白。
|
||||||
|
- JavaScript、Vue SFC 使用分号结尾。
|
||||||
|
- 字符串通常使用单引号;模板属性和值使用双引号。
|
||||||
|
- 对象、数组尾项保留逗号。
|
||||||
|
- 复杂函数的函数体内常使用空行分隔初始化、分支和返回逻辑;保持与所在文件一致即可。
|
||||||
|
|
||||||
|
## JavaScript
|
||||||
|
|
||||||
|
- 使用 ES Module:`import` / `export`。
|
||||||
|
- 导入顺序通常为:框架或第三方依赖、项目内部模块、资源文件;不同分组之间空一行。
|
||||||
|
- 变量优先使用 `const`;需要重新赋值的局部变量使用 `let`;不使用 `var`。
|
||||||
|
- 函数以具名 `function` 声明为主;回调和简单映射使用箭头函数。
|
||||||
|
- 命名使用 camelCase:变量、函数、方法和参数均使用英文语义名称;布尔值常以 `is`、`show`、`has` 等开头。
|
||||||
|
- 常量或配置集合也多使用 camelCase,例如 `protocolOptions`、`themeOverrides`。
|
||||||
|
- 错误处理使用 `try...catch`,错误信息通过中文 `console.error` 或界面消息反馈。
|
||||||
|
|
||||||
|
## 注释与类型
|
||||||
|
|
||||||
|
- 注释、JSDoc 说明和面向用户的提示文案使用简体中文。
|
||||||
|
- 按职责使用 `// ==================== 模块名 ====================` 分隔较长的文件。
|
||||||
|
- 公开方法、重要状态和较复杂逻辑使用 JSDoc 描述用途、参数和返回值。
|
||||||
|
- JavaScript 项目通过 JSDoc 引入或声明类型,例如 `@typedef`、`@param`、`@returns`,公共 MQTT 类型集中引用 `types/jsdoc.d.ts`。
|
||||||
|
- 简单实现可配合单行中文注释说明意图,避免重复描述显而易见的代码。
|
||||||
|
|
||||||
|
## Vue 组件
|
||||||
|
|
||||||
|
- 单文件组件按 `<script setup>`、`<template>`、`<style>` 的顺序组织。
|
||||||
|
- 组件文件使用 PascalCase,例如 `ServerModal.vue`;组件在模板中同样使用 PascalCase。
|
||||||
|
- `script setup` 中通常依次放置:导入、Props/Emits、响应式状态、静态配置、侦听或生命周期、事件处理函数。
|
||||||
|
- 事件处理函数以 `handle` 开头,例如 `handleSave`;模板事件名采用 kebab-case,例如 `@edit-server`。
|
||||||
|
- Naive UI 组件使用 `NButton` 等 PascalCase 导入,在模板中写为 `n-button`。
|
||||||
|
- 模板属性一行较长时换行,每个属性单独一行,并保持 2 空格缩进。
|
||||||
|
- 元素标签内文本简短时,将文本与结束标签放在同一行;较长表达式或动态内容可单独成行。
|
||||||
|
- 样式使用 Less;Vue SFC 中通过 `<style lang="less">` / `<style lang="less" scoped>` 声明。
|
||||||
|
- 类名使用 kebab-case;布局和组件私有样式就近放在对应组件中。
|
||||||
|
- 善用嵌套规则、`&` 父选择器减少重复;全局主题色等公共变量在 App.vue 中通过 CSS 自定义属性定义,命名与 `themeOverrides` 对应(例如 `--frost-primary-color`)。
|
||||||
|
- 组件内直接使用 `var(--xxx)` 引用全局变量,不再单独定义 Less 变量。
|
||||||
|
|
||||||
|
## Electron 分层
|
||||||
|
|
||||||
|
- `src/main` 处理窗口、IPC 与 MQTT 等主进程逻辑。
|
||||||
|
- `src/preload` 统一封装并暴露 `window.api`,渲染进程不直接使用 Electron IPC。
|
||||||
|
- `src/renderer` 放置 Vue 页面、组件和状态管理;通过 `window.api` 调用主进程能力。
|
||||||
|
|
||||||
|
## ESLint
|
||||||
|
|
||||||
|
- 使用 `@electron-toolkit/eslint-config` 与 Vue 推荐规则作为基础。
|
||||||
|
- 提交前执行 `pnpm lint`,并修复实际报告的问题。
|
||||||
Reference in New Issue
Block a user