Compare commits
38 Commits
32643c2518
..
main
| Author | SHA1 | Date | |
|---|---|---|---|
| f2b7af5824 | |||
| 0f89e142e9 | |||
| 9723495f12 | |||
| 4aaf584451 | |||
| 330a9a48fd | |||
| e233911d5e | |||
| 821b895d04 | |||
| 6a60b4daf4 | |||
| e491dd78bf | |||
| 7ec0ad36ba | |||
| 0828edf6b1 | |||
| 7eb62ca37f | |||
| cd2f926ba7 | |||
| 515fc2158b | |||
| a1106d4ce6 | |||
| b23cf4d854 | |||
| 089bfaacf7 | |||
| 21683cb790 | |||
| 2682c0a1d6 | |||
| 4b01ae9898 | |||
| ededa0dc19 | |||
| 1483712c93 | |||
| e4122162ba | |||
| 974dc89292 | |||
| 611cb0c6d7 | |||
| 455833c9d4 | |||
| 0c07a7c623 | |||
| 1bef01c0b2 | |||
| 78797dee86 | |||
| 6bd13851d9 | |||
| a62b9557ff | |||
| 1c23b85d92 | |||
| 72ee175bff | |||
| 771d8447b4 | |||
| acee68ab58 | |||
| 9c2a469e4d | |||
| 7ebb384f01 | |||
| 0bf12715c6 |
@@ -0,0 +1,108 @@
|
|||||||
|
# frost-mqtt-client
|
||||||
|
|
||||||
|
一个使用 AI 开发的多功能 MQTT 桌面客户端,基于 Electron + Vue 3 + Naive UI 构建。
|
||||||
|
|
||||||
|
## 功能特性
|
||||||
|
|
||||||
|
- **多服务器管理**:支持添加、编辑、删除多个 MQTT 服务器配置,数据持久化到本地。
|
||||||
|
- **灵活连接**:支持 `mqtt://`、`mqtts://`、`ws://`、`wss://` 多种协议,可配置用户名、密码、客户端 ID、Keep Alive、Clean Session 等。
|
||||||
|
- **主题订阅**:支持 QoS 0/1/2 多级订阅,通配符主题(如 `sensor/+/temperature`),可单个或批量订阅/取消订阅。
|
||||||
|
- **消息发布**:向指定主题发布消息,支持设置 QoS 和 Retain 标志,内置 JSON 格式化。
|
||||||
|
- **消息中心**:实时显示接收和发送的消息,支持按主题过滤、清空日志,自动 JSON 美化。
|
||||||
|
- **仪表盘概览**:展示服务器总数、连接数、订阅主题数、消息总数等关键指标,以及当前服务器状态。
|
||||||
|
- **本地持久化**:服务器配置、订阅主题、消息记录、发布历史均自动保存到本地用户数据目录。
|
||||||
|
|
||||||
|
## 技术栈
|
||||||
|
|
||||||
|
- [Electron](https://www.electronjs.org/)
|
||||||
|
- [Vue 3](https://vuejs.org/)
|
||||||
|
- [Vite](https://vitejs.dev/)
|
||||||
|
- [electron-vite](https://electron-vite.org/)
|
||||||
|
- [Naive UI](https://www.naiveui.com/)
|
||||||
|
- [MQTT.js](https://github.com/mqttjs/MQTT.js)
|
||||||
|
|
||||||
|
## 项目结构
|
||||||
|
|
||||||
|
```
|
||||||
|
frost-mqtt-client/
|
||||||
|
├── app/ # Electron 应用源码
|
||||||
|
│ ├── src/
|
||||||
|
│ │ ├── main/ # 主进程
|
||||||
|
│ │ │ ├── index.js # 应用入口
|
||||||
|
│ │ │ ├── ipc-handlers.js # IPC 通信处理
|
||||||
|
│ │ │ └── mqtt-manager.js # MQTT 连接与数据管理
|
||||||
|
│ │ ├── preload/ # 预加载脚本
|
||||||
|
│ │ └── renderer/ # 渲染进程
|
||||||
|
│ │ └── src/
|
||||||
|
│ │ ├── components/ # 公共组件
|
||||||
|
│ │ ├── stores/ # Pinia 状态管理
|
||||||
|
│ │ ├── views/ # 页面视图
|
||||||
|
│ │ └── App.vue # 根组件
|
||||||
|
│ ├── package.json
|
||||||
|
│ └── electron.vite.config.mjs
|
||||||
|
├── design/ # 设计稿页面
|
||||||
|
└── README.md
|
||||||
|
```
|
||||||
|
|
||||||
|
## 环境要求
|
||||||
|
|
||||||
|
- [Node.js](https://nodejs.org/) 18+
|
||||||
|
- [pnpm](https://pnpm.io/) 8+
|
||||||
|
|
||||||
|
## 安装依赖
|
||||||
|
|
||||||
|
```bash
|
||||||
|
$ cd app
|
||||||
|
$ pnpm install
|
||||||
|
```
|
||||||
|
|
||||||
|
## 开发运行
|
||||||
|
|
||||||
|
```bash
|
||||||
|
$ cd app
|
||||||
|
$ pnpm dev
|
||||||
|
```
|
||||||
|
|
||||||
|
## 构建打包
|
||||||
|
|
||||||
|
```bash
|
||||||
|
$ cd app
|
||||||
|
|
||||||
|
# Windows
|
||||||
|
$ pnpm build:win
|
||||||
|
|
||||||
|
# macOS
|
||||||
|
$ pnpm build:mac
|
||||||
|
|
||||||
|
# Linux
|
||||||
|
$ pnpm build:linux
|
||||||
|
```
|
||||||
|
|
||||||
|
打包后的安装包会输出到 `app/dist` 目录。
|
||||||
|
|
||||||
|
## 默认配置
|
||||||
|
|
||||||
|
首次启动时,应用会自动创建一个示例服务器:
|
||||||
|
|
||||||
|
- **名称**:EMQX 公共测试
|
||||||
|
- **地址**:`broker.emqx.io`
|
||||||
|
- **端口**:`1883`
|
||||||
|
- **协议**:`mqtt`
|
||||||
|
|
||||||
|
你也可以在左侧服务器列表中点击「添加服务器」按钮,配置自己的 MQTT Broker。
|
||||||
|
|
||||||
|
## 使用说明
|
||||||
|
|
||||||
|
1. 在左侧边栏选择或添加 MQTT 服务器。
|
||||||
|
2. 在「仪表盘」页面点击「连接服务器」。
|
||||||
|
3. 切换到「主题订阅」页面,添加并订阅感兴趣的主题。
|
||||||
|
4. 在「消息中心」页面可发布消息并实时查看收发日志。
|
||||||
|
5. 所有配置和消息记录会自动保存,下次打开即可继续使用。
|
||||||
|
|
||||||
|
## 数据存储
|
||||||
|
|
||||||
|
应用数据(服务器配置、订阅主题、消息记录等)存储在系统的用户数据目录下:
|
||||||
|
|
||||||
|
- **Windows**:`%APPDATA%\frost-mqtt-client\frost-mqtt-data.json`
|
||||||
|
- **macOS**:`~/Library/Application Support/frost-mqtt-client/frost-mqtt-data.json`
|
||||||
|
- **Linux**:`~/.config/frost-mqtt-client/frost-mqtt-data.json`
|
||||||
@@ -1,6 +0,0 @@
|
|||||||
out
|
|
||||||
dist
|
|
||||||
pnpm-lock.yaml
|
|
||||||
LICENSE.md
|
|
||||||
tsconfig.json
|
|
||||||
tsconfig.*.json
|
|
||||||
@@ -1,4 +0,0 @@
|
|||||||
singleQuote: true
|
|
||||||
semi: false
|
|
||||||
printWidth: 100
|
|
||||||
trailingComma: none
|
|
||||||
@@ -1,11 +1 @@
|
|||||||
{
|
{}
|
||||||
"[typescript]": {
|
|
||||||
"editor.defaultFormatter": "esbenp.prettier-vscode"
|
|
||||||
},
|
|
||||||
"[javascript]": {
|
|
||||||
"editor.defaultFormatter": "esbenp.prettier-vscode"
|
|
||||||
},
|
|
||||||
"[json]": {
|
|
||||||
"editor.defaultFormatter": "esbenp.prettier-vscode"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,34 +0,0 @@
|
|||||||
# frost-mqtt-client
|
|
||||||
|
|
||||||
An Electron application with Vue
|
|
||||||
|
|
||||||
## Recommended IDE Setup
|
|
||||||
|
|
||||||
- [VSCode](https://code.visualstudio.com/) + [ESLint](https://marketplace.visualstudio.com/items?itemName=dbaeumer.vscode-eslint) + [Prettier](https://marketplace.visualstudio.com/items?itemName=esbenp.prettier-vscode) + [Volar](https://marketplace.visualstudio.com/items?itemName=Vue.volar)
|
|
||||||
|
|
||||||
## Project Setup
|
|
||||||
|
|
||||||
### Install
|
|
||||||
|
|
||||||
```bash
|
|
||||||
$ pnpm install
|
|
||||||
```
|
|
||||||
|
|
||||||
### Development
|
|
||||||
|
|
||||||
```bash
|
|
||||||
$ pnpm dev
|
|
||||||
```
|
|
||||||
|
|
||||||
### Build
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# For windows
|
|
||||||
$ pnpm build:win
|
|
||||||
|
|
||||||
# For macOS
|
|
||||||
$ pnpm build:mac
|
|
||||||
|
|
||||||
# For Linux
|
|
||||||
$ pnpm build:linux
|
|
||||||
```
|
|
||||||
|
Before Width: | Height: | Size: 84 KiB After Width: | Height: | Size: 16 KiB |
|
Before Width: | Height: | Size: 121 KiB After Width: | Height: | Size: 22 KiB |
|
Before Width: | Height: | Size: 35 KiB After Width: | Height: | Size: 16 KiB |
@@ -1,5 +1,5 @@
|
|||||||
appId: com.electron.app
|
appId: top.frost-zx.frost-mqtt-client
|
||||||
productName: frost-mqtt-client
|
productName: Frost MQTT Client
|
||||||
directories:
|
directories:
|
||||||
buildResources: build
|
buildResources: build
|
||||||
files:
|
files:
|
||||||
@@ -13,10 +13,15 @@ asarUnpack:
|
|||||||
win:
|
win:
|
||||||
executableName: frost-mqtt-client
|
executableName: frost-mqtt-client
|
||||||
nsis:
|
nsis:
|
||||||
artifactName: ${name}-${version}-setup.${ext}
|
allowToChangeInstallationDirectory: true
|
||||||
|
artifactName: ${name}-${version}-Windows-${arch}-Setup.${ext}
|
||||||
|
createDesktopShortcut: always
|
||||||
|
createStartMenuShortcut: true
|
||||||
|
oneClick: false
|
||||||
|
perMachine: false
|
||||||
|
runAfterFinish: false
|
||||||
shortcutName: ${productName}
|
shortcutName: ${productName}
|
||||||
uninstallDisplayName: ${productName}
|
uninstallDisplayName: ${productName}
|
||||||
createDesktopShortcut: always
|
|
||||||
mac:
|
mac:
|
||||||
entitlementsInherit: build/entitlements.mac.plist
|
entitlementsInherit: build/entitlements.mac.plist
|
||||||
extendInfo:
|
extendInfo:
|
||||||
@@ -26,16 +31,17 @@ mac:
|
|||||||
- NSDownloadsFolderUsageDescription: Application requests access to the user's Downloads folder.
|
- NSDownloadsFolderUsageDescription: Application requests access to the user's Downloads folder.
|
||||||
notarize: false
|
notarize: false
|
||||||
dmg:
|
dmg:
|
||||||
artifactName: ${name}-${version}.${ext}
|
artifactName: ${name}-${version}-macOS-${arch}.${ext}
|
||||||
linux:
|
linux:
|
||||||
target:
|
|
||||||
- AppImage
|
|
||||||
- snap
|
|
||||||
- deb
|
|
||||||
maintainer: electronjs.org
|
|
||||||
category: Utility
|
category: Utility
|
||||||
|
artifactName: ${name}-${version}-Linux-${arch}.${ext}
|
||||||
|
maintainer: frost-zx.github.io
|
||||||
|
target:
|
||||||
|
# - AppImage
|
||||||
|
# - snap
|
||||||
|
- deb
|
||||||
appImage:
|
appImage:
|
||||||
artifactName: ${name}-${version}.${ext}
|
artifactName: ${name}-${version}-Linux-${arch}.${ext}
|
||||||
npmRebuild: false
|
npmRebuild: false
|
||||||
publish:
|
publish:
|
||||||
provider: generic
|
provider: generic
|
||||||
|
|||||||
@@ -1,10 +1,11 @@
|
|||||||
import eslintConfig from '@electron-toolkit/eslint-config'
|
import eslintConfig from '@electron-toolkit/eslint-config'
|
||||||
import eslintConfigPrettier from '@electron-toolkit/eslint-config-prettier'
|
|
||||||
import eslintPluginVue from 'eslint-plugin-vue'
|
import eslintPluginVue from 'eslint-plugin-vue'
|
||||||
import vueParser from 'vue-eslint-parser'
|
import vueParser from 'vue-eslint-parser'
|
||||||
|
|
||||||
export default [
|
export default [
|
||||||
{ ignores: ['**/node_modules', '**/dist', '**/out'] },
|
{
|
||||||
|
ignores: ['**/node_modules', '**/dist', '**/out'],
|
||||||
|
},
|
||||||
eslintConfig,
|
eslintConfig,
|
||||||
...eslintPluginVue.configs['flat/recommended'],
|
...eslintPluginVue.configs['flat/recommended'],
|
||||||
{
|
{
|
||||||
@@ -13,18 +14,19 @@ export default [
|
|||||||
parser: vueParser,
|
parser: vueParser,
|
||||||
parserOptions: {
|
parserOptions: {
|
||||||
ecmaFeatures: {
|
ecmaFeatures: {
|
||||||
jsx: true
|
jsx: true,
|
||||||
},
|
},
|
||||||
extraFileExtensions: ['.vue']
|
extraFileExtensions: ['.vue'],
|
||||||
}
|
},
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
files: ['**/*.{js,jsx,vue}'],
|
files: ['**/*.{js,jsx,vue}'],
|
||||||
rules: {
|
rules: {
|
||||||
|
'vue/multi-word-component-names': 'off',
|
||||||
|
'vue/multiline-html-element-content-newline': 'off',
|
||||||
'vue/require-default-prop': 'off',
|
'vue/require-default-prop': 'off',
|
||||||
'vue/multi-word-component-names': 'off'
|
'vue/singleline-html-element-content-newline': 'off',
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
eslintConfigPrettier
|
];
|
||||||
]
|
|
||||||
|
|||||||
@@ -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"
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -1,12 +1,11 @@
|
|||||||
{
|
{
|
||||||
"name": "frost-mqtt-client",
|
"name": "frost-mqtt-client",
|
||||||
"version": "1.0.0",
|
"version": "1.0.1",
|
||||||
"description": "An Electron application with Vue",
|
"description": "一个使用 AI 开发的多功能 MQTT 桌面客户端,基于 Electron + Vue 3 + Naive UI 构建。",
|
||||||
"main": "./out/main/index.js",
|
"main": "./out/main/index.js",
|
||||||
"author": "example.com",
|
"author": "Frost-ZX",
|
||||||
"homepage": "https://electron-vite.org",
|
"homepage": "https://frost-zx.github.io/",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"format": "prettier --write .",
|
|
||||||
"lint": "eslint --cache .",
|
"lint": "eslint --cache .",
|
||||||
"start": "electron-vite preview",
|
"start": "electron-vite preview",
|
||||||
"dev": "electron-vite dev",
|
"dev": "electron-vite dev",
|
||||||
@@ -24,15 +23,14 @@
|
|||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@electron-toolkit/eslint-config": "^2.1.0",
|
"@electron-toolkit/eslint-config": "^2.1.0",
|
||||||
"@electron-toolkit/eslint-config-prettier": "^3.0.0",
|
|
||||||
"@vitejs/plugin-vue": "^6.0.2",
|
"@vitejs/plugin-vue": "^6.0.2",
|
||||||
"electron": "^39.2.6",
|
"electron": "^39.2.6",
|
||||||
"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",
|
||||||
"prettier": "^3.7.4",
|
|
||||||
"vite": "^7.2.6",
|
"vite": "^7.2.6",
|
||||||
"vue": "^3.5.25",
|
"vue": "^3.5.25",
|
||||||
"vue-eslint-parser": "^10.2.0"
|
"vue-eslint-parser": "^10.2.0"
|
||||||
|
|||||||
@@ -21,12 +21,9 @@ importers:
|
|||||||
'@electron-toolkit/eslint-config':
|
'@electron-toolkit/eslint-config':
|
||||||
specifier: ^2.1.0
|
specifier: ^2.1.0
|
||||||
version: 2.1.0(eslint@9.39.5(jiti@2.7.0))
|
version: 2.1.0(eslint@9.39.5(jiti@2.7.0))
|
||||||
'@electron-toolkit/eslint-config-prettier':
|
|
||||||
specifier: ^3.0.0
|
|
||||||
version: 3.0.0(eslint@9.39.5(jiti@2.7.0))(prettier@3.9.5)
|
|
||||||
'@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
|
||||||
@@ -35,22 +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)
|
||||||
prettier:
|
|
||||||
specifier: ^3.7.4
|
|
||||||
version: 3.9.5
|
|
||||||
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
|
||||||
@@ -151,12 +148,6 @@ packages:
|
|||||||
peerDependencies:
|
peerDependencies:
|
||||||
vue: ^3.0.11
|
vue: ^3.0.11
|
||||||
|
|
||||||
'@electron-toolkit/eslint-config-prettier@3.0.0':
|
|
||||||
resolution: {integrity: sha512-YapmIOVkbYdHLuTa+ad1SAVtcqYL9A/SJsc7cxQokmhcwAwonGevNom37jBf9slXegcZ/Slh01I/JARG1yhNFw==}
|
|
||||||
peerDependencies:
|
|
||||||
eslint: '>= 9.0.0'
|
|
||||||
prettier: '>= 3.0.0'
|
|
||||||
|
|
||||||
'@electron-toolkit/eslint-config@2.1.0':
|
'@electron-toolkit/eslint-config@2.1.0':
|
||||||
resolution: {integrity: sha512-F/r45x5wDHs8r2RSkXwcrMYo9X4lkc/W+ZTlWzOzjXn7ncirMqVo256BCAogeNBWIxd24ePsSwmJXELiQZUZUg==}
|
resolution: {integrity: sha512-F/r45x5wDHs8r2RSkXwcrMYo9X4lkc/W+ZTlWzOzjXn7ncirMqVo256BCAogeNBWIxd24ePsSwmJXELiQZUZUg==}
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
@@ -638,10 +629,6 @@ packages:
|
|||||||
resolution: {integrity: sha512-ODOov0sGMJMf3jPonOkgGqPknTsu+DdQ7kD++gz8aI+aFMOMHFbWAA2taqXXVTdP+OTOQR/znGvSpmkeI0WTYQ==}
|
resolution: {integrity: sha512-ODOov0sGMJMf3jPonOkgGqPknTsu+DdQ7kD++gz8aI+aFMOMHFbWAA2taqXXVTdP+OTOQR/znGvSpmkeI0WTYQ==}
|
||||||
engines: {node: '>=14.18.0'}
|
engines: {node: '>=14.18.0'}
|
||||||
|
|
||||||
'@pkgr/core@0.3.6':
|
|
||||||
resolution: {integrity: sha512-SEeaJLb3qBNF/OaXnaR1NmmBbFYk1zC0ZH/52fATcRPLFg/p791YrcyFFy44Bo9sLaGuSuLp5Q6axbb/O+v/RA==}
|
|
||||||
engines: {node: ^14.18.0 || >=16.0.0}
|
|
||||||
|
|
||||||
'@rolldown/pluginutils@1.0.1':
|
'@rolldown/pluginutils@1.0.1':
|
||||||
resolution: {integrity: sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw==}
|
resolution: {integrity: sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw==}
|
||||||
|
|
||||||
@@ -1104,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==}
|
||||||
|
|
||||||
@@ -1136,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'}
|
||||||
@@ -1248,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'}
|
||||||
@@ -1285,26 +1296,6 @@ packages:
|
|||||||
resolution: {integrity: sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==}
|
resolution: {integrity: sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==}
|
||||||
engines: {node: '>=10'}
|
engines: {node: '>=10'}
|
||||||
|
|
||||||
eslint-config-prettier@10.1.8:
|
|
||||||
resolution: {integrity: sha512-82GZUjRS0p/jganf6q1rEO25VSoHH0hKPCTrgillPjdI/3bgBhAE1QzHrHTizjpRvy6pGAvKjDJtk2pF9NDq8w==}
|
|
||||||
hasBin: true
|
|
||||||
peerDependencies:
|
|
||||||
eslint: '>=7.0.0'
|
|
||||||
|
|
||||||
eslint-plugin-prettier@5.5.6:
|
|
||||||
resolution: {integrity: sha512-ifetmTcxWfz+4qRW3pH/ujdTq2jQIj59AxJMIN26K5avYgU8dxycUETQonWiW+wPrYXA0j3Try0l1CnwVQtDqQ==}
|
|
||||||
engines: {node: ^14.18.0 || >=16.0.0}
|
|
||||||
peerDependencies:
|
|
||||||
'@types/eslint': '>=8.0.0'
|
|
||||||
eslint: '>=8.0.0'
|
|
||||||
eslint-config-prettier: '>= 7.0.0 <10.0.0 || >=10.1.0'
|
|
||||||
prettier: '>=3.0.0'
|
|
||||||
peerDependenciesMeta:
|
|
||||||
'@types/eslint':
|
|
||||||
optional: true
|
|
||||||
eslint-config-prettier:
|
|
||||||
optional: true
|
|
||||||
|
|
||||||
eslint-plugin-vue@10.9.2:
|
eslint-plugin-vue@10.9.2:
|
||||||
resolution: {integrity: sha512-4g7ZP3pYcuqd7Zp0pzUKcos0W+RkjBz4EGdhJ92FcYk6v03Ti/GK5NwjgsjxHK+98eXDbHeK7VtX1az7/8doZA==}
|
resolution: {integrity: sha512-4g7ZP3pYcuqd7Zp0pzUKcos0W+RkjBz4EGdhJ92FcYk6v03Ti/GK5NwjgsjxHK+98eXDbHeK7VtX1az7/8doZA==}
|
||||||
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
|
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
|
||||||
@@ -1398,9 +1389,6 @@ packages:
|
|||||||
fast-deep-equal@3.1.3:
|
fast-deep-equal@3.1.3:
|
||||||
resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==}
|
resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==}
|
||||||
|
|
||||||
fast-diff@1.3.0:
|
|
||||||
resolution: {integrity: sha512-VxPP4NqbUjj6MaAOafWeUn2cXWLcCtljklUtZf0Ind4XQ+QPtmA0b18zZy0jIQx+ExRVCR/ZQpBmik5lXshNsw==}
|
|
||||||
|
|
||||||
fast-json-stable-stringify@2.1.0:
|
fast-json-stable-stringify@2.1.0:
|
||||||
resolution: {integrity: sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==}
|
resolution: {integrity: sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==}
|
||||||
|
|
||||||
@@ -1583,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==}
|
||||||
|
|
||||||
@@ -1621,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==}
|
||||||
|
|
||||||
@@ -1699,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'}
|
||||||
@@ -1733,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'}
|
||||||
@@ -1749,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'}
|
||||||
@@ -1800,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==}
|
||||||
|
|
||||||
@@ -1817,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'}
|
||||||
@@ -1878,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'}
|
||||||
@@ -1929,14 +1960,8 @@ packages:
|
|||||||
resolution: {integrity: sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==}
|
resolution: {integrity: sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==}
|
||||||
engines: {node: '>= 0.8.0'}
|
engines: {node: '>= 0.8.0'}
|
||||||
|
|
||||||
prettier-linter-helpers@1.0.1:
|
probe-image-size@7.3.0:
|
||||||
resolution: {integrity: sha512-SxToR7P8Y2lWmv/kTzVLC1t/GDI2WGjMwNhLLE9qtH8Q13C+aEmuRlzDst4Up4s0Wc8sF2M+J57iB3cMLqftfg==}
|
resolution: {integrity: sha512-7CaDeBwiAbh6ohXsvLbAZhO7wzsZAmaevfxe39qvCwRh8LyaZfDlBGGLU1CCTgrTLtCOdwBBhjOrIHaIIimHfQ==}
|
||||||
engines: {node: '>=6.0.0'}
|
|
||||||
|
|
||||||
prettier@3.9.5:
|
|
||||||
resolution: {integrity: sha512-/FVl766LpUfB5vXgCYOYa0MeV/441Ia99AeICQIQFTY/Nw0roZwULcXpku5i1/m5kt/baz+s4Zogspd839HSMg==}
|
|
||||||
engines: {node: '>=14'}
|
|
||||||
hasBin: true
|
|
||||||
|
|
||||||
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==}
|
||||||
@@ -1960,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==}
|
||||||
|
|
||||||
@@ -2042,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==}
|
||||||
|
|
||||||
@@ -2122,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'}
|
||||||
@@ -2148,10 +2182,6 @@ packages:
|
|||||||
resolution: {integrity: sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==}
|
resolution: {integrity: sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==}
|
||||||
engines: {node: '>=8'}
|
engines: {node: '>=8'}
|
||||||
|
|
||||||
synckit@0.11.13:
|
|
||||||
resolution: {integrity: sha512-eNRKgb3z66Yp3D2CixVujOUvXLFUTij/zVnV8KRyvFdQwpz7I5DS8UfRkTeLzb64u+dkzDSdelE24izu+zSSUg==}
|
|
||||||
engines: {node: ^14.18.0 || >=16.0.0}
|
|
||||||
|
|
||||||
tar@7.5.20:
|
tar@7.5.20:
|
||||||
resolution: {integrity: sha512-9FcyK4PA6+WbzlTM9WhQm6vB5W7cP7dUiPsv1g7YDwEQnQ1CGpK3MGlKk/ITVWMk05kHZuBhmVhiv8LZoy/PFQ==}
|
resolution: {integrity: sha512-9FcyK4PA6+WbzlTM9WhQm6vB5W7cP7dUiPsv1g7YDwEQnQ1CGpK3MGlKk/ITVWMk05kHZuBhmVhiv8LZoy/PFQ==}
|
||||||
engines: {node: '>=18'}
|
engines: {node: '>=18'}
|
||||||
@@ -2511,15 +2541,6 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
vue: 3.5.39
|
vue: 3.5.39
|
||||||
|
|
||||||
'@electron-toolkit/eslint-config-prettier@3.0.0(eslint@9.39.5(jiti@2.7.0))(prettier@3.9.5)':
|
|
||||||
dependencies:
|
|
||||||
eslint: 9.39.5(jiti@2.7.0)
|
|
||||||
eslint-config-prettier: 10.1.8(eslint@9.39.5(jiti@2.7.0))
|
|
||||||
eslint-plugin-prettier: 5.5.6(eslint-config-prettier@10.1.8(eslint@9.39.5(jiti@2.7.0)))(eslint@9.39.5(jiti@2.7.0))(prettier@3.9.5)
|
|
||||||
prettier: 3.9.5
|
|
||||||
transitivePeerDependencies:
|
|
||||||
- '@types/eslint'
|
|
||||||
|
|
||||||
'@electron-toolkit/eslint-config@2.1.0(eslint@9.39.5(jiti@2.7.0))':
|
'@electron-toolkit/eslint-config@2.1.0(eslint@9.39.5(jiti@2.7.0))':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@eslint/js': 9.39.5
|
'@eslint/js': 9.39.5
|
||||||
@@ -2911,8 +2932,6 @@ snapshots:
|
|||||||
tslib: 2.8.1
|
tslib: 2.8.1
|
||||||
webcrypto-core: 1.9.2
|
webcrypto-core: 1.9.2
|
||||||
|
|
||||||
'@pkgr/core@0.3.6': {}
|
|
||||||
|
|
||||||
'@rolldown/pluginutils@1.0.1': {}
|
'@rolldown/pluginutils@1.0.1': {}
|
||||||
|
|
||||||
'@rollup/rollup-android-arm-eabi@4.62.2':
|
'@rollup/rollup-android-arm-eabi@4.62.2':
|
||||||
@@ -3056,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':
|
||||||
@@ -3383,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:
|
||||||
@@ -3411,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
|
||||||
@@ -3518,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)
|
||||||
@@ -3526,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
|
||||||
|
|
||||||
@@ -3562,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: {}
|
||||||
@@ -3642,19 +3680,6 @@ snapshots:
|
|||||||
|
|
||||||
escape-string-regexp@4.0.0: {}
|
escape-string-regexp@4.0.0: {}
|
||||||
|
|
||||||
eslint-config-prettier@10.1.8(eslint@9.39.5(jiti@2.7.0)):
|
|
||||||
dependencies:
|
|
||||||
eslint: 9.39.5(jiti@2.7.0)
|
|
||||||
|
|
||||||
eslint-plugin-prettier@5.5.6(eslint-config-prettier@10.1.8(eslint@9.39.5(jiti@2.7.0)))(eslint@9.39.5(jiti@2.7.0))(prettier@3.9.5):
|
|
||||||
dependencies:
|
|
||||||
eslint: 9.39.5(jiti@2.7.0)
|
|
||||||
prettier: 3.9.5
|
|
||||||
prettier-linter-helpers: 1.0.1
|
|
||||||
synckit: 0.11.13
|
|
||||||
optionalDependencies:
|
|
||||||
eslint-config-prettier: 10.1.8(eslint@9.39.5(jiti@2.7.0))
|
|
||||||
|
|
||||||
eslint-plugin-vue@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))):
|
eslint-plugin-vue@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))):
|
||||||
dependencies:
|
dependencies:
|
||||||
'@eslint-community/eslint-utils': 4.9.1(eslint@9.39.5(jiti@2.7.0))
|
'@eslint-community/eslint-utils': 4.9.1(eslint@9.39.5(jiti@2.7.0))
|
||||||
@@ -3771,8 +3796,6 @@ snapshots:
|
|||||||
|
|
||||||
fast-deep-equal@3.1.3: {}
|
fast-deep-equal@3.1.3: {}
|
||||||
|
|
||||||
fast-diff@1.3.0: {}
|
|
||||||
|
|
||||||
fast-json-stable-stringify@2.1.0: {}
|
fast-json-stable-stringify@2.1.0: {}
|
||||||
|
|
||||||
fast-levenshtein@2.0.6: {}
|
fast-levenshtein@2.0.6: {}
|
||||||
@@ -3987,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: {}
|
||||||
@@ -4015,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: {}
|
||||||
@@ -4074,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
|
||||||
@@ -4105,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
|
||||||
@@ -4118,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: {}
|
||||||
@@ -4183,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):
|
||||||
@@ -4211,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
|
||||||
@@ -4283,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: {}
|
||||||
@@ -4330,11 +4406,14 @@ snapshots:
|
|||||||
|
|
||||||
prelude-ls@1.2.1: {}
|
prelude-ls@1.2.1: {}
|
||||||
|
|
||||||
prettier-linter-helpers@1.0.1:
|
probe-image-size@7.3.0:
|
||||||
dependencies:
|
dependencies:
|
||||||
fast-diff: 1.3.0
|
lodash.merge: 4.6.2
|
||||||
|
needle: 2.9.1
|
||||||
prettier@3.9.5: {}
|
stream-parser: 0.3.1
|
||||||
|
transitivePeerDependencies:
|
||||||
|
- supports-color
|
||||||
|
optional: true
|
||||||
|
|
||||||
proc-log@6.1.0: {}
|
proc-log@6.1.0: {}
|
||||||
|
|
||||||
@@ -4355,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
|
||||||
@@ -4469,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
|
||||||
@@ -4528,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
|
||||||
@@ -4558,10 +4650,6 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
has-flag: 4.0.0
|
has-flag: 4.0.0
|
||||||
|
|
||||||
synckit@0.11.13:
|
|
||||||
dependencies:
|
|
||||||
'@pkgr/core': 0.3.6
|
|
||||||
|
|
||||||
tar@7.5.20:
|
tar@7.5.20:
|
||||||
dependencies:
|
dependencies:
|
||||||
'@isaacs/fs-minipass': 4.0.1
|
'@isaacs/fs-minipass': 4.0.1
|
||||||
@@ -4649,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)
|
||||||
@@ -4661,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:
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 35 KiB After Width: | Height: | Size: 16 KiB |
@@ -0,0 +1 @@
|
|||||||
|
<svg width="304" height="303" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" overflow="hidden"><g transform="translate(-774 -198)"><path d="M775 248.334C775 220.535 797.535 198 825.334 198L1026.67 198C1054.46 198 1077 220.535 1077 248.334L1077 449.666C1077 477.465 1054.46 500 1026.67 500L825.334 500C797.535 500 775 477.465 775 449.666Z" fill="#FFFFFF" fill-rule="evenodd"/><path d="M926.5 226.5 1039.5 283 1039.5 416 926.5 472.5 813.5 416 813.5 283Z" stroke="#43D9D0" stroke-width="10.6667" stroke-linecap="round" stroke-linejoin="round" stroke-miterlimit="10" fill="none" fill-rule="evenodd"/><path d="M926.5 255.5 1013.5 299 1013.5 401 926.5 444.5 839.5 401 839.5 299Z" stroke="#43D9D0" stroke-width="10.6667" stroke-linecap="round" stroke-linejoin="round" stroke-miterlimit="10" fill="none" fill-rule="evenodd"/><path d="M869.5 321.834C869.5 315.574 874.574 310.5 880.834 310.5L888.5 310.5 888.5 310.5 917 310.5 972.167 310.5C978.426 310.5 983.5 315.574 983.5 321.834L983.5 350.167 983.5 350.167 983.5 367.167 983.5 367.167C983.5 373.426 978.426 378.5 972.167 378.5L917 378.5 889.947 398.75 888.5 378.5 880.834 378.5C874.574 378.5 869.5 373.426 869.5 367.167L869.5 367.167 869.5 350.167 869.5 350.167Z" stroke="#43D9D0" stroke-width="10.6667" stroke-linecap="round" stroke-linejoin="round" stroke-miterlimit="10" fill="none" fill-rule="evenodd"/><path d="M892.5 334.5 960.532 334.5" stroke="#43D9D0" stroke-width="10.6667" stroke-linecap="round" stroke-linejoin="round" stroke-miterlimit="10" fill="none" fill-rule="evenodd"/><path d="M892.5 355.5 930.295 355.5" stroke="#43D9D0" stroke-width="10.6667" stroke-linecap="round" stroke-linejoin="round" stroke-miterlimit="10" fill="none" fill-rule="evenodd"/><path d="M926.5 406.5 926.5 444.274" stroke="#43D9D0" stroke-width="10.6667" stroke-linecap="round" stroke-linejoin="round" stroke-miterlimit="10" fill="none" fill-rule="evenodd"/><path d="M994.5 391.5 1013.27 400.998" stroke="#43D9D0" stroke-width="10.6667" stroke-linecap="round" stroke-linejoin="round" stroke-miterlimit="10" fill="none" fill-rule="evenodd"/><path d="M0 0 15.2633 10.8059" stroke="#43D9D0" stroke-width="10.6667" stroke-linecap="round" stroke-linejoin="round" stroke-miterlimit="10" fill="none" fill-rule="evenodd" transform="matrix(-1 0 0 1 854.763 389.5)"/><path d="M0 0 0.000104987 34.0158" stroke="#43D9D0" stroke-width="10.6667" stroke-linecap="round" stroke-linejoin="round" stroke-miterlimit="10" fill="none" fill-rule="evenodd" transform="matrix(1 0 0 -1 926.5 289.516)"/></g></svg>
|
||||||
|
After Width: | Height: | Size: 2.5 KiB |
@@ -1,11 +1,17 @@
|
|||||||
import { app, shell, BrowserWindow } from 'electron'
|
import { app, shell, BrowserWindow } from 'electron';
|
||||||
import { join } from 'path'
|
import { join } from 'path';
|
||||||
import { electronApp, optimizer, is } from '@electron-toolkit/utils'
|
import { electronApp, optimizer, is } from '@electron-toolkit/utils';
|
||||||
import icon from '../../resources/icon.png?asset'
|
import { registerIpcHandlers } from './ipc-handlers.js';
|
||||||
import { registerIpcHandlers } from './ipc-handlers.js'
|
|
||||||
|
|
||||||
|
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,
|
||||||
@@ -16,62 +22,66 @@ function createWindow() {
|
|||||||
...(process.platform === 'linux' ? { icon } : {}),
|
...(process.platform === 'linux' ? { icon } : {}),
|
||||||
webPreferences: {
|
webPreferences: {
|
||||||
preload: join(__dirname, '../preload/index.js'),
|
preload: join(__dirname, '../preload/index.js'),
|
||||||
sandbox: false
|
sandbox: false,
|
||||||
}
|
},
|
||||||
})
|
});
|
||||||
|
|
||||||
|
// 窗口准备就绪后显示,避免白屏
|
||||||
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 {
|
||||||
mainWindow.loadFile(join(__dirname, '../renderer/index.html'))
|
mainWindow.loadFile(join(__dirname, '../renderer/index.html'));
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 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
|
|
||||||
electronApp.setAppUserModelId('com.electron')
|
|
||||||
|
|
||||||
// Default open or close DevTools by F12 in development
|
// 为 Windows 设置应用用户模型 ID
|
||||||
// and ignore CommandOrControl + R in production.
|
electronApp.setAppUserModelId('com.electron');
|
||||||
// see https://github.com/alex8088/electron-toolkit/tree/master/packages/utils
|
|
||||||
|
// 开发环境通过 F12 打开/关闭 DevTools
|
||||||
|
// 生产环境忽略 CommandOrControl + R 刷新
|
||||||
|
// 详见 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);
|
||||||
})
|
});
|
||||||
|
|
||||||
// 注册 IPC 处理器
|
// 注册 IPC 处理器
|
||||||
registerIpcHandlers()
|
registerIpcHandlers();
|
||||||
|
|
||||||
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();
|
||||||
})
|
}
|
||||||
})
|
});
|
||||||
|
|
||||||
// Quit when all windows are closed, except on macOS. There, it's common
|
});
|
||||||
// for applications and their menu bar to stay active until the user quits
|
|
||||||
// explicitly with Cmd + Q.
|
// 当所有窗口关闭时退出应用,macOS 除外
|
||||||
|
// 在 macOS 上,应用及其菜单栏通常会保持活动状态,直到用户通过 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.
|
// 也可以将它们放到单独的文件中,然后在这里引入
|
||||||
|
|||||||
@@ -1,18 +1,31 @@
|
|||||||
import { ipcMain, BrowserWindow } from 'electron'
|
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()) {
|
||||||
win.webContents.send(channel, data)
|
win.webContents.send(channel, data);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -20,100 +33,122 @@ export function registerIpcHandlers() {
|
|||||||
mqttManager.addListener((event, data) => {
|
mqttManager.addListener((event, data) => {
|
||||||
switch (event) {
|
switch (event) {
|
||||||
case 'status-change':
|
case 'status-change':
|
||||||
sendToRenderer('mqtt:status-change', data)
|
sendToRenderer('mqtt:status-change', data);
|
||||||
break
|
break;
|
||||||
case 'message':
|
case 'message':
|
||||||
sendToRenderer('mqtt:message', data)
|
sendToRenderer('mqtt:message', data);
|
||||||
break
|
break;
|
||||||
case 'subscription-change':
|
case 'subscription-change':
|
||||||
sendToRenderer('mqtt:subscription-change', data)
|
sendToRenderer('mqtt:subscription-change', data);
|
||||||
break
|
break;
|
||||||
case 'subscription-error':
|
case 'subscription-error':
|
||||||
sendToRenderer('mqtt:subscription-error', data)
|
sendToRenderer('mqtt:subscription-error', data);
|
||||||
break
|
break;
|
||||||
case 'publish-error':
|
case 'publish-error':
|
||||||
sendToRenderer('mqtt:publish-error', data)
|
sendToRenderer('mqtt:publish-error', data);
|
||||||
break
|
break;
|
||||||
}
|
}
|
||||||
})
|
});
|
||||||
|
|
||||||
// ==================== 服务器管理 ====================
|
// ==================== 服务器管理 ====================
|
||||||
|
|
||||||
ipcMain.handle('mqtt:get-servers', () => {
|
ipcMain.handle('mqtt:get-servers', () => {
|
||||||
return mqttManager.getServers()
|
return mqttManager.getServers();
|
||||||
})
|
});
|
||||||
|
|
||||||
ipcMain.handle('mqtt:add-server', (_, config) => {
|
ipcMain.handle('mqtt:add-server', (_, config) => {
|
||||||
return mqttManager.addServer(config)
|
return mqttManager.addServer(config);
|
||||||
})
|
});
|
||||||
|
|
||||||
ipcMain.handle('mqtt:update-server', (_, id, config) => {
|
ipcMain.handle('mqtt:update-server', (_, id, config) => {
|
||||||
return mqttManager.updateServer(id, config)
|
return mqttManager.updateServer(id, config);
|
||||||
})
|
});
|
||||||
|
|
||||||
ipcMain.handle('mqtt:delete-server', (_, id) => {
|
ipcMain.handle('mqtt:delete-server', (_, id) => {
|
||||||
mqttManager.deleteServer(id)
|
mqttManager.deleteServer(id);
|
||||||
return true
|
return true;
|
||||||
})
|
});
|
||||||
|
|
||||||
|
ipcMain.handle('mqtt:export-servers', () => {
|
||||||
|
return mqttManager.exportServers();
|
||||||
|
});
|
||||||
|
|
||||||
|
ipcMain.handle('mqtt:import-servers', (_, 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) => {
|
||||||
return mqttManager.connect(id)
|
return mqttManager.connect(id);
|
||||||
})
|
});
|
||||||
|
|
||||||
ipcMain.handle('mqtt:disconnect', (_, id) => {
|
ipcMain.handle('mqtt:disconnect', (_, id) => {
|
||||||
mqttManager.disconnect(id)
|
mqttManager.disconnect(id);
|
||||||
return true
|
return true;
|
||||||
})
|
});
|
||||||
|
|
||||||
// ==================== 主题管理 ====================
|
// ==================== 主题管理 ====================
|
||||||
ipcMain.handle('mqtt:add-topic', (_, serverId, topic, qos) => {
|
|
||||||
return mqttManager.addTopic(serverId, topic, qos)
|
ipcMain.handle('mqtt:add-topic', (_, serverId, topic, qos, comment) => {
|
||||||
})
|
return mqttManager.addTopic(serverId, topic, qos, comment);
|
||||||
|
});
|
||||||
|
|
||||||
ipcMain.handle('mqtt:remove-topic', (_, serverId, topic) => {
|
ipcMain.handle('mqtt:remove-topic', (_, serverId, topic) => {
|
||||||
return mqttManager.removeTopic(serverId, topic)
|
return mqttManager.removeTopic(serverId, topic);
|
||||||
})
|
});
|
||||||
|
|
||||||
ipcMain.handle('mqtt:subscribe', (_, serverId, topic) => {
|
ipcMain.handle('mqtt:subscribe', (_, serverId, topic) => {
|
||||||
return mqttManager.subscribe(serverId, topic)
|
return mqttManager.subscribe(serverId, topic);
|
||||||
})
|
});
|
||||||
|
|
||||||
ipcMain.handle('mqtt:unsubscribe', (_, serverId, topic) => {
|
ipcMain.handle('mqtt:unsubscribe', (_, serverId, topic) => {
|
||||||
return mqttManager.unsubscribe(serverId, topic)
|
return mqttManager.unsubscribe(serverId, topic);
|
||||||
})
|
});
|
||||||
|
|
||||||
ipcMain.handle('mqtt:subscribe-all', (_, serverId) => {
|
ipcMain.handle('mqtt:subscribe-all', (_, serverId) => {
|
||||||
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);
|
||||||
})
|
});
|
||||||
|
|
||||||
// ==================== 消息发布 ====================
|
// ==================== 消息发布 ====================
|
||||||
|
|
||||||
ipcMain.handle('mqtt:publish', (_, serverId, topic, payload, opts) => {
|
ipcMain.handle('mqtt:publish', (_, serverId, topic, payload, opts) => {
|
||||||
return mqttManager.publish(serverId, topic, payload, opts)
|
return mqttManager.publish(serverId, topic, payload, opts);
|
||||||
})
|
});
|
||||||
|
|
||||||
// ==================== 消息管理 ====================
|
// ==================== 消息管理 ====================
|
||||||
|
|
||||||
ipcMain.handle('mqtt:get-messages', (_, serverId) => {
|
ipcMain.handle('mqtt:get-messages', (_, serverId) => {
|
||||||
return mqttManager.getMessages(serverId)
|
return mqttManager.getMessages(serverId);
|
||||||
})
|
});
|
||||||
|
|
||||||
ipcMain.handle('mqtt:get-publish-history', (_, serverId) => {
|
ipcMain.handle('mqtt:get-publish-history', (_, serverId) => {
|
||||||
return mqttManager.getPublishHistory(serverId)
|
return mqttManager.getPublishHistory(serverId);
|
||||||
})
|
});
|
||||||
|
|
||||||
ipcMain.handle('mqtt:clear-messages', (_, serverId) => {
|
ipcMain.handle('mqtt:clear-messages', (_, serverId) => {
|
||||||
mqttManager.clearMessages(serverId)
|
mqttManager.clearMessages(serverId);
|
||||||
return true
|
return true;
|
||||||
})
|
});
|
||||||
|
|
||||||
ipcMain.handle('mqtt:clear-publish-history', (_, serverId) => {
|
ipcMain.handle('mqtt:clear-publish-history', (_, serverId) => {
|
||||||
mqttManager.clearPublishHistory(serverId)
|
mqttManager.clearPublishHistory(serverId);
|
||||||
return true
|
return true;
|
||||||
})
|
});
|
||||||
|
|
||||||
// 兼容旧的 ping 测试
|
|
||||||
ipcMain.on('ping', () => console.log('pong'))
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,26 +1,37 @@
|
|||||||
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 = {
|
||||||
|
|
||||||
// 服务器管理
|
// 服务器管理
|
||||||
getServers: () => ipcRenderer.invoke('mqtt:get-servers'),
|
getServers: () => ipcRenderer.invoke('mqtt:get-servers'),
|
||||||
addServer: (config) => ipcRenderer.invoke('mqtt:add-server', config),
|
addServer: (config) => ipcRenderer.invoke('mqtt:add-server', config),
|
||||||
updateServer: (id, config) => ipcRenderer.invoke('mqtt:update-server', id, config),
|
updateServer: (id, config) => ipcRenderer.invoke('mqtt:update-server', id, config),
|
||||||
deleteServer: (id) => ipcRenderer.invoke('mqtt:delete-server', id),
|
deleteServer: (id) => ipcRenderer.invoke('mqtt:delete-server', id),
|
||||||
|
exportServers: () => ipcRenderer.invoke('mqtt:export-servers'),
|
||||||
|
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) =>
|
||||||
@@ -34,43 +45,42 @@ const api = {
|
|||||||
|
|
||||||
// 事件监听
|
// 事件监听
|
||||||
onStatusChange: (callback) => {
|
onStatusChange: (callback) => {
|
||||||
const handler = (_, data) => callback(data)
|
const handler = (_, data) => callback(data);
|
||||||
ipcRenderer.on('mqtt:status-change', handler)
|
ipcRenderer.on('mqtt:status-change', handler);
|
||||||
return () => ipcRenderer.removeListener('mqtt:status-change', handler)
|
return () => ipcRenderer.removeListener('mqtt:status-change', handler);
|
||||||
},
|
},
|
||||||
onMessage: (callback) => {
|
onMessage: (callback) => {
|
||||||
const handler = (_, data) => callback(data)
|
const handler = (_, data) => callback(data);
|
||||||
ipcRenderer.on('mqtt:message', handler)
|
ipcRenderer.on('mqtt:message', handler);
|
||||||
return () => ipcRenderer.removeListener('mqtt:message', handler)
|
return () => ipcRenderer.removeListener('mqtt:message', handler);
|
||||||
},
|
},
|
||||||
onSubscriptionChange: (callback) => {
|
onSubscriptionChange: (callback) => {
|
||||||
const handler = (_, data) => callback(data)
|
const handler = (_, data) => callback(data);
|
||||||
ipcRenderer.on('mqtt:subscription-change', handler)
|
ipcRenderer.on('mqtt:subscription-change', handler);
|
||||||
return () => ipcRenderer.removeListener('mqtt:subscription-change', handler)
|
return () => ipcRenderer.removeListener('mqtt:subscription-change', handler);
|
||||||
},
|
},
|
||||||
onSubscriptionError: (callback) => {
|
onSubscriptionError: (callback) => {
|
||||||
const handler = (_, data) => callback(data)
|
const handler = (_, data) => callback(data);
|
||||||
ipcRenderer.on('mqtt:subscription-error', handler)
|
ipcRenderer.on('mqtt:subscription-error', handler);
|
||||||
return () => ipcRenderer.removeListener('mqtt:subscription-error', handler)
|
return () => ipcRenderer.removeListener('mqtt:subscription-error', handler);
|
||||||
},
|
},
|
||||||
onPublishError: (callback) => {
|
onPublishError: (callback) => {
|
||||||
const handler = (_, data) => callback(data)
|
const handler = (_, data) => callback(data);
|
||||||
ipcRenderer.on('mqtt:publish-error', handler)
|
ipcRenderer.on('mqtt:publish-error', handler);
|
||||||
return () => ipcRenderer.removeListener('mqtt:publish-error', handler)
|
return () => ipcRenderer.removeListener('mqtt:publish-error', handler);
|
||||||
}
|
},
|
||||||
}
|
|
||||||
|
|
||||||
// Use `contextBridge` APIs to expose Electron APIs to
|
};
|
||||||
// renderer only if context isolation is enabled, otherwise
|
|
||||||
// just add to the DOM global.
|
// 根据是否启用上下文隔离,使用 contextBridge 暴露 API 或挂载到 window
|
||||||
if (process.contextIsolated) {
|
if (process.contextIsolated) {
|
||||||
try {
|
try {
|
||||||
contextBridge.exposeInMainWorld('electron', electronAPI)
|
contextBridge.exposeInMainWorld('electron', electronAPI);
|
||||||
contextBridge.exposeInMainWorld('api', api)
|
contextBridge.exposeInMainWorld('api', api);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(error)
|
console.error(error);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
window.electron = electronAPI
|
window.electron = electronAPI;
|
||||||
window.api = api
|
window.api = api;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,14 +1,14 @@
|
|||||||
<!doctype html>
|
<!DOCTYPE html>
|
||||||
<html>
|
<html lang="zh-CN">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8" />
|
<meta charset="UTF-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
<title>Frost MQTT Client</title>
|
<title>Frost MQTT Client</title>
|
||||||
<meta
|
<meta
|
||||||
http-equiv="Content-Security-Policy"
|
http-equiv="Content-Security-Policy"
|
||||||
content="default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data:; connect-src 'self' ws: wss: mqtt: mqtts:"
|
content="default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data:; connect-src 'self' ws: wss: mqtt: mqtts:"
|
||||||
/>
|
/>
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
<body>
|
<body>
|
||||||
<div id="app"></div>
|
<div id="app"></div>
|
||||||
<script type="module" src="/src/main.js"></script>
|
<script type="module" src="/src/main.js"></script>
|
||||||
|
|||||||
@@ -1,74 +1,143 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import { ref, onMounted } from 'vue'
|
import { ref, onMounted } from 'vue';
|
||||||
import {
|
import { NLayout, NMessageProvider, NDialogProvider, NConfigProvider, NTabs, NTabPane, zhCN, dateZhCN } from 'naive-ui';
|
||||||
NLayout,
|
import { useMqttStore } from './stores/mqtt.js';
|
||||||
NMessageProvider,
|
|
||||||
NDialogProvider,
|
|
||||||
NConfigProvider,
|
|
||||||
NTabs,
|
|
||||||
NTabPane,
|
|
||||||
zhCN,
|
|
||||||
dateZhCN
|
|
||||||
} from 'naive-ui'
|
|
||||||
import Sidebar from './components/Sidebar.vue'
|
|
||||||
import ServerModal from './components/ServerModal.vue'
|
|
||||||
import Dashboard from './views/Dashboard.vue'
|
|
||||||
import Subscribe from './views/Subscribe.vue'
|
|
||||||
import Publish from './views/Publish.vue'
|
|
||||||
import Messages from './views/Messages.vue'
|
|
||||||
import { useMqttStore } from './stores/mqtt.js'
|
|
||||||
|
|
||||||
const store = useMqttStore()
|
import Sidebar from './components/Sidebar.vue';
|
||||||
|
import ServerModal from './components/ServerModal.vue';
|
||||||
|
import Dashboard from './views/Dashboard.vue';
|
||||||
|
import Subscribe from './views/Subscribe.vue';
|
||||||
|
import Publish from './views/Publish.vue';
|
||||||
|
import Messages from './views/Messages.vue';
|
||||||
|
import QuickSend from './views/QuickSend.vue';
|
||||||
|
|
||||||
const activeTab = ref('dashboard')
|
// 全局状态
|
||||||
const showServerModal = ref(false)
|
const store = useMqttStore();
|
||||||
const editServerData = ref(null)
|
|
||||||
|
|
||||||
|
// 主题覆盖配置
|
||||||
|
const themeOverrides = {
|
||||||
|
common: {
|
||||||
|
primaryColor: '#0ea5a0',
|
||||||
|
primaryColorHover: '#0b8c87',
|
||||||
|
primaryColorPressed: '#0b8c87',
|
||||||
|
primaryColorSuppl: '#0ea5a0',
|
||||||
|
successColor: '#22c55e',
|
||||||
|
successColorHover: '#16a34a',
|
||||||
|
successColorPressed: '#15803d',
|
||||||
|
successColorSuppl: '#22c55e',
|
||||||
|
warningColor: '#f59e0b',
|
||||||
|
warningColorHover: '#d97706',
|
||||||
|
warningColorPressed: '#b45309',
|
||||||
|
warningColorSuppl: '#f59e0b',
|
||||||
|
errorColor: '#ef4444',
|
||||||
|
errorColorHover: '#dc2626',
|
||||||
|
errorColorPressed: '#b91c1c',
|
||||||
|
errorColorSuppl: '#ef4444',
|
||||||
|
infoColor: '#3b82f6',
|
||||||
|
infoColorHover: '#2563eb',
|
||||||
|
infoColorPressed: '#1d4ed8',
|
||||||
|
infoColorSuppl: '#3b82f6',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
// 当前激活的标签页
|
||||||
|
const activeTab = ref('dashboard');
|
||||||
|
// 是否显示服务器编辑弹窗
|
||||||
|
const showServerModal = ref(false);
|
||||||
|
// 当前编辑的服务器数据,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;
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<n-config-provider :locale="zhCN" :date-locale="dateZhCN" :theme="null">
|
<n-config-provider
|
||||||
|
:locale="zhCN"
|
||||||
|
:date-locale="dateZhCN"
|
||||||
|
:theme="null"
|
||||||
|
:theme-overrides="themeOverrides"
|
||||||
|
>
|
||||||
<n-message-provider placement="bottom">
|
<n-message-provider placement="bottom">
|
||||||
<n-dialog-provider>
|
<n-dialog-provider>
|
||||||
<n-layout has-sider position="absolute">
|
<n-layout
|
||||||
|
has-sider
|
||||||
|
position="absolute"
|
||||||
|
>
|
||||||
|
|
||||||
<!-- 侧边栏 -->
|
<!-- 侧边栏 -->
|
||||||
<Sidebar @add-server="handleAddServer" @edit-server="handleEditServer" />
|
<Sidebar
|
||||||
|
@add-server="handleAddServer"
|
||||||
|
@edit-server="handleEditServer"
|
||||||
|
/>
|
||||||
|
|
||||||
<!-- 主内容区 -->
|
<!-- 主内容区 -->
|
||||||
<n-layout content-class="main-layout-content">
|
<n-layout content-class="main-layout-content">
|
||||||
<n-tabs v-model:value="activeTab" type="line" size="medium" style="padding: 0 16px">
|
<n-tabs
|
||||||
<n-tab-pane name="dashboard" tab="仪表盘">
|
v-model:value="activeTab"
|
||||||
|
type="line"
|
||||||
|
size="medium"
|
||||||
|
style="padding: 0 16px"
|
||||||
|
>
|
||||||
|
<n-tab-pane
|
||||||
|
name="dashboard"
|
||||||
|
tab="仪表盘"
|
||||||
|
>
|
||||||
<div class="tab-content dashboard-content">
|
<div class="tab-content dashboard-content">
|
||||||
<Dashboard @switch-tab="handleSwitchTab" @edit-server="handleEditServer" />
|
<div class="dashboard-scroll-wrapper">
|
||||||
|
<Dashboard
|
||||||
|
@switch-tab="handleSwitchTab"
|
||||||
|
@edit-server="handleEditServer"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</n-tab-pane>
|
</n-tab-pane>
|
||||||
<n-tab-pane name="subscribe" tab="主题订阅">
|
<n-tab-pane
|
||||||
|
name="subscribe"
|
||||||
|
tab="主题订阅"
|
||||||
|
>
|
||||||
<div class="tab-content">
|
<div class="tab-content">
|
||||||
<Subscribe />
|
<Subscribe />
|
||||||
</div>
|
</div>
|
||||||
</n-tab-pane>
|
</n-tab-pane>
|
||||||
<n-tab-pane name="messages" tab="消息中心">
|
<n-tab-pane
|
||||||
|
name="messages"
|
||||||
|
tab="消息中心"
|
||||||
|
>
|
||||||
<div class="tab-content">
|
<div class="tab-content">
|
||||||
<div class="message-center-layout">
|
<div class="message-center-layout">
|
||||||
<section class="publish-panel">
|
<section class="publish-panel">
|
||||||
@@ -80,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>
|
||||||
|
|
||||||
@@ -88,16 +165,46 @@ function handleSwitchTab(tab) {
|
|||||||
:edit-server="editServerData"
|
:edit-server="editServerData"
|
||||||
@saved="handleServerSaved"
|
@saved="handleServerSaved"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
</n-layout>
|
</n-layout>
|
||||||
</n-dialog-provider>
|
</n-dialog-provider>
|
||||||
</n-message-provider>
|
</n-message-provider>
|
||||||
</n-config-provider>
|
</n-config-provider>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style>
|
<style lang="less">
|
||||||
html,
|
:root {
|
||||||
body,
|
--frost-primary-color: #0ea5a0;
|
||||||
#app {
|
--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 {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
@@ -110,12 +217,20 @@ body,
|
|||||||
|
|
||||||
.tab-content {
|
.tab-content {
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
padding: 12px 0;
|
padding: 0 0 16px 0;
|
||||||
height: calc(100vh - 50px);
|
height: calc(100vh - 50px);
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
|
|
||||||
|
&.dashboard-content {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.dashboard-content {
|
.dashboard-scroll-wrapper {
|
||||||
|
flex: 1;
|
||||||
|
min-height: 0;
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
overscroll-behavior: contain;
|
overscroll-behavior: contain;
|
||||||
}
|
}
|
||||||
@@ -126,31 +241,57 @@ body,
|
|||||||
gap: 12px;
|
gap: 12px;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
}
|
|
||||||
|
|
||||||
.publish-panel,
|
.publish-panel,
|
||||||
.messages-panel {
|
.messages-panel {
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
min-height: 0;
|
min-height: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.publish-panel {
|
.publish-panel {
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
min-height: 0;
|
min-height: 0;
|
||||||
padding-right: 4px;
|
overflow: hidden;
|
||||||
overflow: hidden;
|
}
|
||||||
}
|
|
||||||
|
|
||||||
.messages-panel {
|
.messages-panel {
|
||||||
height: 100%;
|
height: 100%;
|
||||||
min-height: 0;
|
min-height: 0;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (max-width: 900px) {
|
@media (max-width: 900px) {
|
||||||
.message-center-layout {
|
|
||||||
grid-template-columns: 1fr;
|
grid-template-columns: 1fr;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* 全局滚动条样式 */
|
||||||
|
* {
|
||||||
|
scrollbar-width: thin;
|
||||||
|
scrollbar-color: rgba(144, 147, 153, 0.5) transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
::-webkit-scrollbar {
|
||||||
|
width: 8px;
|
||||||
|
height: 8px;
|
||||||
|
|
||||||
|
&-track {
|
||||||
|
background-color: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
&-thumb {
|
||||||
|
background-color: rgba(144, 147, 153, 0.5);
|
||||||
|
border-radius: 4px;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background-color: rgba(144, 147, 153, 0.8);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&-corner {
|
||||||
|
display: none;
|
||||||
|
background-color: transparent;
|
||||||
|
}
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -1,67 +0,0 @@
|
|||||||
:root {
|
|
||||||
--ev-c-white: #ffffff;
|
|
||||||
--ev-c-white-soft: #f8f8f8;
|
|
||||||
--ev-c-white-mute: #f2f2f2;
|
|
||||||
|
|
||||||
--ev-c-black: #1b1b1f;
|
|
||||||
--ev-c-black-soft: #222222;
|
|
||||||
--ev-c-black-mute: #282828;
|
|
||||||
|
|
||||||
--ev-c-gray-1: #515c67;
|
|
||||||
--ev-c-gray-2: #414853;
|
|
||||||
--ev-c-gray-3: #32363f;
|
|
||||||
|
|
||||||
--ev-c-text-1: rgba(255, 255, 245, 0.86);
|
|
||||||
--ev-c-text-2: rgba(235, 235, 245, 0.6);
|
|
||||||
--ev-c-text-3: rgba(235, 235, 245, 0.38);
|
|
||||||
|
|
||||||
--ev-button-alt-border: transparent;
|
|
||||||
--ev-button-alt-text: var(--ev-c-text-1);
|
|
||||||
--ev-button-alt-bg: var(--ev-c-gray-3);
|
|
||||||
--ev-button-alt-hover-border: transparent;
|
|
||||||
--ev-button-alt-hover-text: var(--ev-c-text-1);
|
|
||||||
--ev-button-alt-hover-bg: var(--ev-c-gray-2);
|
|
||||||
}
|
|
||||||
|
|
||||||
:root {
|
|
||||||
--color-background: var(--ev-c-black);
|
|
||||||
--color-background-soft: var(--ev-c-black-soft);
|
|
||||||
--color-background-mute: var(--ev-c-black-mute);
|
|
||||||
|
|
||||||
--color-text: var(--ev-c-text-1);
|
|
||||||
}
|
|
||||||
|
|
||||||
*,
|
|
||||||
*::before,
|
|
||||||
*::after {
|
|
||||||
box-sizing: border-box;
|
|
||||||
margin: 0;
|
|
||||||
font-weight: normal;
|
|
||||||
}
|
|
||||||
|
|
||||||
ul {
|
|
||||||
list-style: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
body {
|
|
||||||
min-height: 100vh;
|
|
||||||
color: var(--color-text);
|
|
||||||
background: var(--color-background);
|
|
||||||
line-height: 1.6;
|
|
||||||
font-family:
|
|
||||||
Inter,
|
|
||||||
-apple-system,
|
|
||||||
BlinkMacSystemFont,
|
|
||||||
'Segoe UI',
|
|
||||||
Roboto,
|
|
||||||
Oxygen,
|
|
||||||
Ubuntu,
|
|
||||||
Cantarell,
|
|
||||||
'Fira Sans',
|
|
||||||
'Droid Sans',
|
|
||||||
'Helvetica Neue',
|
|
||||||
sans-serif;
|
|
||||||
text-rendering: optimizeLegibility;
|
|
||||||
-webkit-font-smoothing: antialiased;
|
|
||||||
-moz-osx-font-smoothing: grayscale;
|
|
||||||
}
|
|
||||||
@@ -1,10 +0,0 @@
|
|||||||
<svg viewBox="0 0 128 128" fill="none" xmlns="http://www.w3.org/2000/svg">
|
|
||||||
<circle cx="64" cy="64" r="64" fill="#2F3242"/>
|
|
||||||
<ellipse cx="63.9835" cy="23.2036" rx="4.48794" ry="4.495" stroke="#A2ECFB" stroke-width="3.6" stroke-linecap="round"/>
|
|
||||||
<path d="M51.3954 39.5028C52.3733 39.6812 53.3108 39.033 53.4892 38.055C53.6676 37.0771 53.0194 36.1396 52.0414 35.9612L51.3954 39.5028ZM28.6153 43.5751L30.1748 44.4741L30.1748 44.4741L28.6153 43.5751ZM28.9393 60.9358C29.4332 61.7985 30.5329 62.0976 31.3957 61.6037C32.2585 61.1098 32.5575 60.0101 32.0636 59.1473L28.9393 60.9358ZM37.6935 66.7457C37.025 66.01 35.8866 65.9554 35.1508 66.6239C34.415 67.2924 34.3605 68.4308 35.029 69.1666L37.6935 66.7457ZM53.7489 81.7014L52.8478 83.2597L53.7489 81.7014ZM96.9206 89.515C97.7416 88.9544 97.9526 87.8344 97.3919 87.0135C96.8313 86.1925 95.7113 85.9815 94.8904 86.5422L96.9206 89.515ZM52.0414 35.9612C46.4712 34.9451 41.2848 34.8966 36.9738 35.9376C32.6548 36.9806 29.0841 39.1576 27.0559 42.6762L30.1748 44.4741C31.5693 42.0549 34.1448 40.3243 37.8188 39.4371C41.5009 38.5479 46.1547 38.5468 51.3954 39.5028L52.0414 35.9612ZM27.0559 42.6762C24.043 47.9029 25.2781 54.5399 28.9393 60.9358L32.0636 59.1473C28.6579 53.1977 28.1088 48.0581 30.1748 44.4741L27.0559 42.6762ZM35.029 69.1666C39.6385 74.24 45.7158 79.1355 52.8478 83.2597L54.6499 80.1432C47.8081 76.1868 42.0298 71.5185 37.6935 66.7457L35.029 69.1666ZM52.8478 83.2597C61.344 88.1726 70.0465 91.2445 77.7351 92.3608C85.359 93.4677 92.2744 92.6881 96.9206 89.515L94.8904 86.5422C91.3255 88.9767 85.4902 89.849 78.2524 88.7982C71.0793 87.7567 62.809 84.8612 54.6499 80.1432L52.8478 83.2597ZM105.359 84.9077C105.359 81.4337 102.546 78.6127 99.071 78.6127V82.2127C100.553 82.2127 101.759 83.4166 101.759 84.9077H105.359ZM99.071 78.6127C95.5956 78.6127 92.7831 81.4337 92.7831 84.9077H96.3831C96.3831 83.4166 97.5892 82.2127 99.071 82.2127V78.6127ZM92.7831 84.9077C92.7831 88.3817 95.5956 91.2027 99.071 91.2027V87.6027C97.5892 87.6027 96.3831 86.3988 96.3831 84.9077H92.7831ZM99.071 91.2027C102.546 91.2027 105.359 88.3817 105.359 84.9077H101.759C101.759 86.3988 100.553 87.6027 99.071 87.6027V91.2027Z" fill="#A2ECFB"/>
|
|
||||||
<path d="M91.4873 65.382C90.8456 66.1412 90.9409 67.2769 91.7002 67.9186C92.4594 68.5603 93.5951 68.465 94.2368 67.7058L91.4873 65.382ZM99.3169 43.6354L97.7574 44.5344L99.3169 43.6354ZM84.507 35.2412C83.513 35.2282 82.6967 36.0236 82.6838 37.0176C82.6708 38.0116 83.4661 38.8279 84.4602 38.8409L84.507 35.2412ZM74.9407 39.8801C75.9127 39.6716 76.5315 38.7145 76.323 37.7425C76.1144 36.7706 75.1573 36.1517 74.1854 36.3603L74.9407 39.8801ZM53.7836 46.3728L54.6847 47.931L53.7836 46.3728ZM25.5491 80.9047C25.6932 81.8883 26.6074 82.5688 27.5911 82.4247C28.5747 82.2806 29.2552 81.3664 29.1111 80.3828L25.5491 80.9047ZM94.2368 67.7058C97.8838 63.3907 100.505 58.927 101.752 54.678C103.001 50.4213 102.9 46.2472 100.876 42.7365L97.7574 44.5344C99.1494 46.9491 99.3603 50.0419 98.2974 53.6644C97.2323 57.2945 94.9184 61.3223 91.4873 65.382L94.2368 67.7058ZM100.876 42.7365C97.9119 37.5938 91.7082 35.335 84.507 35.2412L84.4602 38.8409C91.1328 38.9278 95.7262 41.0106 97.7574 44.5344L100.876 42.7365ZM74.1854 36.3603C67.4362 37.8086 60.0878 40.648 52.8826 44.8146L54.6847 47.931C61.5972 43.9338 68.5948 41.2419 74.9407 39.8801L74.1854 36.3603ZM52.8826 44.8146C44.1366 49.872 36.9669 56.0954 32.1491 62.3927C27.3774 68.63 24.7148 75.2115 25.5491 80.9047L29.1111 80.3828C28.4839 76.1026 30.4747 70.5062 35.0084 64.5802C39.496 58.7143 46.2839 52.7889 54.6847 47.931L52.8826 44.8146Z" fill="#A2ECFB"/>
|
|
||||||
<path d="M49.0825 87.2295C48.7478 86.2934 47.7176 85.8059 46.7816 86.1406C45.8455 86.4753 45.358 87.5055 45.6927 88.4416L49.0825 87.2295ZM78.5635 96.4256C79.075 95.5732 78.7988 94.4675 77.9464 93.9559C77.0941 93.4443 75.9884 93.7205 75.4768 94.5729L78.5635 96.4256ZM79.5703 85.1795C79.2738 86.1284 79.8027 87.1379 80.7516 87.4344C81.7004 87.7308 82.71 87.2019 83.0064 86.2531L79.5703 85.1795ZM84.3832 64.0673H82.5832H84.3832ZM69.156 22.5301C68.2477 22.1261 67.1838 22.535 66.7799 23.4433C66.3759 24.3517 66.7848 25.4155 67.6931 25.8194L69.156 22.5301ZM45.6927 88.4416C47.5994 93.7741 50.1496 98.2905 53.2032 101.505C56.2623 104.724 59.9279 106.731 63.9835 106.731V103.131C61.1984 103.131 58.4165 101.765 55.8131 99.0249C53.2042 96.279 50.8768 92.2477 49.0825 87.2295L45.6927 88.4416ZM63.9835 106.731C69.8694 106.731 74.8921 102.542 78.5635 96.4256L75.4768 94.5729C72.0781 100.235 68.0122 103.131 63.9835 103.131V106.731ZM83.0064 86.2531C85.0269 79.7864 86.1832 72.1831 86.1832 64.0673H82.5832C82.5832 71.8536 81.4723 79.0919 79.5703 85.1795L83.0064 86.2531ZM86.1832 64.0673C86.1832 54.1144 84.4439 44.922 81.4961 37.6502C78.5748 30.4436 74.3436 24.8371 69.156 22.5301L67.6931 25.8194C71.6364 27.5731 75.3846 32.1564 78.1598 39.0026C80.9086 45.7836 82.5832 54.507 82.5832 64.0673H86.1832Z" fill="#A2ECFB"/>
|
|
||||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M103.559 84.9077C103.559 82.4252 101.55 80.4127 99.071 80.4127C96.5924 80.4127 94.5831 82.4252 94.5831 84.9077C94.5831 87.3902 96.5924 89.4027 99.071 89.4027C101.55 89.4027 103.559 87.3902 103.559 84.9077V84.9077Z" stroke="#A2ECFB" stroke-width="3.6" stroke-linecap="round"/>
|
|
||||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M28.8143 89.4027C31.2929 89.4027 33.3023 87.3902 33.3023 84.9077C33.3023 82.4252 31.2929 80.4127 28.8143 80.4127C26.3357 80.4127 24.3264 82.4252 24.3264 84.9077C24.3264 87.3902 26.3357 89.4027 28.8143 89.4027V89.4027V89.4027Z" stroke="#A2ECFB" stroke-width="3.6" stroke-linecap="round"/>
|
|
||||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M64.8501 68.0857C62.6341 68.5652 60.451 67.1547 59.9713 64.9353C59.4934 62.7159 60.9007 60.5293 63.1167 60.0489C65.3326 59.5693 67.5157 60.9798 67.9954 63.1992C68.4742 65.4186 67.066 67.6052 64.8501 68.0857Z" fill="#A2ECFB"/>
|
|
||||||
</svg>
|
|
||||||
|
Before Width: | Height: | Size: 5.7 KiB |
@@ -1,163 +0,0 @@
|
|||||||
@import './base.css';
|
|
||||||
|
|
||||||
body {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
overflow: hidden;
|
|
||||||
background-image: url('./wavy-lines.svg');
|
|
||||||
background-size: cover;
|
|
||||||
user-select: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
code {
|
|
||||||
font-weight: 600;
|
|
||||||
padding: 3px 5px;
|
|
||||||
border-radius: 2px;
|
|
||||||
background-color: var(--color-background-mute);
|
|
||||||
font-family:
|
|
||||||
ui-monospace,
|
|
||||||
SFMono-Regular,
|
|
||||||
SF Mono,
|
|
||||||
Menlo,
|
|
||||||
Consolas,
|
|
||||||
Liberation Mono,
|
|
||||||
monospace;
|
|
||||||
font-size: 85%;
|
|
||||||
}
|
|
||||||
|
|
||||||
#app {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
flex-direction: column;
|
|
||||||
margin-bottom: 80px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.logo {
|
|
||||||
margin-bottom: 20px;
|
|
||||||
-webkit-user-drag: none;
|
|
||||||
height: 128px;
|
|
||||||
width: 128px;
|
|
||||||
will-change: filter;
|
|
||||||
transition: filter 300ms;
|
|
||||||
}
|
|
||||||
|
|
||||||
.logo:hover {
|
|
||||||
filter: drop-shadow(0 0 1.2em #6988e6aa);
|
|
||||||
}
|
|
||||||
|
|
||||||
.creator {
|
|
||||||
font-size: 14px;
|
|
||||||
line-height: 16px;
|
|
||||||
color: var(--ev-c-text-2);
|
|
||||||
font-weight: 600;
|
|
||||||
margin-bottom: 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.text {
|
|
||||||
font-size: 28px;
|
|
||||||
color: var(--ev-c-text-1);
|
|
||||||
font-weight: 700;
|
|
||||||
line-height: 32px;
|
|
||||||
text-align: center;
|
|
||||||
margin: 0 10px;
|
|
||||||
padding: 16px 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.tip {
|
|
||||||
font-size: 16px;
|
|
||||||
line-height: 24px;
|
|
||||||
color: var(--ev-c-text-2);
|
|
||||||
font-weight: 600;
|
|
||||||
}
|
|
||||||
|
|
||||||
.vue {
|
|
||||||
background: -webkit-linear-gradient(315deg, #42d392 25%, #647eff);
|
|
||||||
background-clip: text;
|
|
||||||
-webkit-background-clip: text;
|
|
||||||
-webkit-text-fill-color: transparent;
|
|
||||||
font-weight: 700;
|
|
||||||
}
|
|
||||||
|
|
||||||
.actions {
|
|
||||||
display: flex;
|
|
||||||
padding-top: 32px;
|
|
||||||
margin: -6px;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
justify-content: flex-start;
|
|
||||||
}
|
|
||||||
|
|
||||||
.action {
|
|
||||||
flex-shrink: 0;
|
|
||||||
padding: 6px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.action a {
|
|
||||||
cursor: pointer;
|
|
||||||
text-decoration: none;
|
|
||||||
display: inline-block;
|
|
||||||
border: 1px solid transparent;
|
|
||||||
text-align: center;
|
|
||||||
font-weight: 600;
|
|
||||||
white-space: nowrap;
|
|
||||||
border-radius: 20px;
|
|
||||||
padding: 0 20px;
|
|
||||||
line-height: 38px;
|
|
||||||
font-size: 14px;
|
|
||||||
border-color: var(--ev-button-alt-border);
|
|
||||||
color: var(--ev-button-alt-text);
|
|
||||||
background-color: var(--ev-button-alt-bg);
|
|
||||||
}
|
|
||||||
|
|
||||||
.action a:hover {
|
|
||||||
border-color: var(--ev-button-alt-hover-border);
|
|
||||||
color: var(--ev-button-alt-hover-text);
|
|
||||||
background-color: var(--ev-button-alt-hover-bg);
|
|
||||||
}
|
|
||||||
|
|
||||||
.versions {
|
|
||||||
position: absolute;
|
|
||||||
bottom: 30px;
|
|
||||||
margin: 0 auto;
|
|
||||||
padding: 15px 0;
|
|
||||||
font-family: 'Menlo', 'Lucida Console', monospace;
|
|
||||||
display: inline-flex;
|
|
||||||
overflow: hidden;
|
|
||||||
align-items: center;
|
|
||||||
border-radius: 22px;
|
|
||||||
background-color: #202127;
|
|
||||||
backdrop-filter: blur(24px);
|
|
||||||
}
|
|
||||||
|
|
||||||
.versions li {
|
|
||||||
display: block;
|
|
||||||
float: left;
|
|
||||||
border-right: 1px solid var(--ev-c-gray-1);
|
|
||||||
padding: 0 20px;
|
|
||||||
font-size: 14px;
|
|
||||||
line-height: 14px;
|
|
||||||
opacity: 0.8;
|
|
||||||
&:last-child {
|
|
||||||
border: none;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (max-width: 720px) {
|
|
||||||
.text {
|
|
||||||
font-size: 20px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (max-width: 620px) {
|
|
||||||
.versions {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (max-width: 350px) {
|
|
||||||
.tip,
|
|
||||||
.actions {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,25 +0,0 @@
|
|||||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1422 800" opacity="0.3">
|
|
||||||
<defs>
|
|
||||||
<linearGradient x1="50%" y1="0%" x2="50%" y2="100%" id="oooscillate-grad">
|
|
||||||
<stop stop-color="hsl(206, 75%, 49%)" stop-opacity="1" offset="0%"></stop>
|
|
||||||
<stop stop-color="hsl(331, 90%, 56%)" stop-opacity="1" offset="100%"></stop>
|
|
||||||
</linearGradient>
|
|
||||||
</defs>
|
|
||||||
<g stroke-width="1" stroke="url(#oooscillate-grad)" fill="none" stroke-linecap="round">
|
|
||||||
<path d="M 0 448 Q 355.5 -100 711 400 Q 1066.5 900 1422 448" opacity="0.05"></path>
|
|
||||||
<path d="M 0 420 Q 355.5 -100 711 400 Q 1066.5 900 1422 420" opacity="0.11"></path>
|
|
||||||
<path d="M 0 392 Q 355.5 -100 711 400 Q 1066.5 900 1422 392" opacity="0.18"></path>
|
|
||||||
<path d="M 0 364 Q 355.5 -100 711 400 Q 1066.5 900 1422 364" opacity="0.24"></path>
|
|
||||||
<path d="M 0 336 Q 355.5 -100 711 400 Q 1066.5 900 1422 336" opacity="0.30"></path>
|
|
||||||
<path d="M 0 308 Q 355.5 -100 711 400 Q 1066.5 900 1422 308" opacity="0.37"></path>
|
|
||||||
<path d="M 0 280 Q 355.5 -100 711 400 Q 1066.5 900 1422 280" opacity="0.43"></path>
|
|
||||||
<path d="M 0 252 Q 355.5 -100 711 400 Q 1066.5 900 1422 252" opacity="0.49"></path>
|
|
||||||
<path d="M 0 224 Q 355.5 -100 711 400 Q 1066.5 900 1422 224" opacity="0.56"></path>
|
|
||||||
<path d="M 0 196 Q 355.5 -100 711 400 Q 1066.5 900 1422 196" opacity="0.62"></path>
|
|
||||||
<path d="M 0 168 Q 355.5 -100 711 400 Q 1066.5 900 1422 168" opacity="0.68"></path>
|
|
||||||
<path d="M 0 140 Q 355.5 -100 711 400 Q 1066.5 900 1422 140" opacity="0.75"></path>
|
|
||||||
<path d="M 0 112 Q 355.5 -100 711 400 Q 1066.5 900 1422 112" opacity="0.81"></path>
|
|
||||||
<path d="M 0 84 Q 355.5 -100 711 400 Q 1066.5 900 1422 84" opacity="0.87"></path>
|
|
||||||
<path d="M 0 56 Q 355.5 -100 711 400 Q 1066.5 900 1422 56" opacity="0.94"></path>
|
|
||||||
</g>
|
|
||||||
</svg>
|
|
||||||
|
Before Width: | Height: | Size: 1.7 KiB |
@@ -1,31 +1,22 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import { ref, watch } from 'vue'
|
import { ref, watch } from 'vue';
|
||||||
import {
|
import { NModal, NForm, NGrid, NFormItemGi, NInput, NInputNumber, NSelect, NSwitch, NButton, useMessage } from 'naive-ui';
|
||||||
NModal,
|
import { useMqttStore } from '../stores/mqtt.js';
|
||||||
NForm,
|
|
||||||
NFormItem,
|
|
||||||
NGrid,
|
|
||||||
NFormItemGi,
|
|
||||||
NInput,
|
|
||||||
NInputNumber,
|
|
||||||
NSelect,
|
|
||||||
NSwitch,
|
|
||||||
NButton,
|
|
||||||
useMessage
|
|
||||||
} from 'naive-ui'
|
|
||||||
import { useMqttStore } from '../stores/mqtt.js'
|
|
||||||
|
|
||||||
|
// 弹窗显示状态与编辑数据
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
show: Boolean,
|
show: Boolean,
|
||||||
editServer: Object
|
editServer: Object,
|
||||||
})
|
});
|
||||||
|
|
||||||
const emit = defineEmits(['update:show', 'saved'])
|
// 更新 show、保存完成事件
|
||||||
|
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: '',
|
||||||
host: '',
|
host: '',
|
||||||
@@ -35,30 +26,39 @@ const form = ref({
|
|||||||
username: '',
|
username: '',
|
||||||
password: '',
|
password: '',
|
||||||
keepAlive: 60,
|
keepAlive: 60,
|
||||||
cleanSession: true
|
cleanSession: true,
|
||||||
})
|
connectTimeout: 10000,
|
||||||
|
reconnect: true,
|
||||||
|
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' },
|
||||||
{ label: 'ws:// (WebSocket)', value: 'ws' },
|
{ label: 'ws:// (WebSocket)', value: 'ws' },
|
||||||
{ 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) => {
|
||||||
if (val) {
|
if (val) {
|
||||||
if (props.editServer) {
|
if (props.editServer) {
|
||||||
isEdit.value = true
|
isEdit.value = true;
|
||||||
form.value = {
|
form.value = {
|
||||||
name: props.editServer.name,
|
name: props.editServer.name,
|
||||||
host: props.editServer.host,
|
host: props.editServer.host,
|
||||||
@@ -66,12 +66,15 @@ watch(
|
|||||||
protocol: props.editServer.protocol || 'mqtt',
|
protocol: props.editServer.protocol || 'mqtt',
|
||||||
clientId: props.editServer.clientId || '',
|
clientId: props.editServer.clientId || '',
|
||||||
username: props.editServer.username || '',
|
username: props.editServer.username || '',
|
||||||
password: '',
|
password: props.editServer.password || '',
|
||||||
keepAlive: props.editServer.keepAlive || 60,
|
keepAlive: props.editServer.keepAlive || 60,
|
||||||
cleanSession: props.editServer.cleanSession !== false
|
cleanSession: props.editServer.cleanSession !== false,
|
||||||
}
|
connectTimeout: props.editServer.connectTimeout ?? 10000,
|
||||||
|
reconnect: props.editServer.reconnect !== false,
|
||||||
|
reconnectInterval: props.editServer.reconnectInterval ?? 5000,
|
||||||
|
};
|
||||||
} else {
|
} else {
|
||||||
isEdit.value = false
|
isEdit.value = false;
|
||||||
form.value = {
|
form.value = {
|
||||||
name: '',
|
name: '',
|
||||||
host: '',
|
host: '',
|
||||||
@@ -81,61 +84,107 @@ watch(
|
|||||||
username: '',
|
username: '',
|
||||||
password: '',
|
password: '',
|
||||||
keepAlive: 60,
|
keepAlive: 60,
|
||||||
cleanSession: true
|
cleanSession: true,
|
||||||
}
|
connectTimeout: 10000,
|
||||||
|
reconnect: true,
|
||||||
|
reconnectInterval: 5000,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
)
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 保存服务器配置
|
||||||
|
*/
|
||||||
async function handleSave() {
|
async function handleSave() {
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await formRef.value?.validate()
|
await formRef.value?.validate();
|
||||||
} catch {
|
} catch {
|
||||||
return
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
|
||||||
if (isEdit.value && props.editServer) {
|
if (isEdit.value && props.editServer) {
|
||||||
await store.updateServer(props.editServer.id, { ...form.value })
|
await store.updateServer(props.editServer.id, { ...form.value });
|
||||||
msg.success('服务器配置已更新')
|
msg.success('服务器配置已更新');
|
||||||
} else {
|
} else {
|
||||||
await store.addServer({ ...form.value })
|
await store.addServer({ ...form.value });
|
||||||
msg.success('服务器已添加')
|
msg.success('服务器已添加');
|
||||||
}
|
}
|
||||||
emit('saved')
|
|
||||||
emit('update:show', false)
|
emit('saved');
|
||||||
|
emit('update:show', false);
|
||||||
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
msg.error('操作失败: ' + (e.message || '未知错误'))
|
msg.error('操作失败: ' + (e.message || '未知错误'));
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<n-modal
|
<n-modal
|
||||||
:show="show"
|
|
||||||
:mask-closable="false"
|
:mask-closable="false"
|
||||||
preset="card"
|
:show="show"
|
||||||
:title="isEdit ? '编辑 MQTT 服务器' : '添加 MQTT 服务器'"
|
:title="isEdit ? '编辑 MQTT 服务器' : '添加 MQTT 服务器'"
|
||||||
|
preset="card"
|
||||||
style="width: 520px; max-width: 90vw"
|
style="width: 520px; max-width: 90vw"
|
||||||
@update:show="$emit('update:show', $event)"
|
@update:show="$emit('update:show', $event)"
|
||||||
>
|
>
|
||||||
<n-form ref="formRef" :model="form" :rules="rules" label-placement="top" size="medium">
|
<n-form
|
||||||
<n-grid :cols="2" :x-gap="12">
|
ref="formRef"
|
||||||
<n-form-item-gi label="服务器名称" path="name">
|
:model="form"
|
||||||
<n-input v-model:value="form.name" placeholder="例如: 本地开发服务器" />
|
:rules="rules"
|
||||||
|
label-placement="top"
|
||||||
|
size="medium"
|
||||||
|
>
|
||||||
|
<n-grid
|
||||||
|
:cols="2"
|
||||||
|
:x-gap="12"
|
||||||
|
>
|
||||||
|
<n-form-item-gi
|
||||||
|
label="服务器名称"
|
||||||
|
path="name"
|
||||||
|
>
|
||||||
|
<n-input
|
||||||
|
v-model:value="form.name"
|
||||||
|
placeholder="例如: 本地开发服务器"
|
||||||
|
/>
|
||||||
</n-form-item-gi>
|
</n-form-item-gi>
|
||||||
<n-form-item-gi label="主机地址" path="host">
|
<n-form-item-gi
|
||||||
<n-input v-model:value="form.host" placeholder="例如: broker.emqx.io" />
|
label="主机地址"
|
||||||
|
path="host"
|
||||||
|
>
|
||||||
|
<n-input
|
||||||
|
v-model:value="form.host"
|
||||||
|
placeholder="例如: broker.emqx.io"
|
||||||
|
/>
|
||||||
</n-form-item-gi>
|
</n-form-item-gi>
|
||||||
<n-form-item-gi label="端口" path="port">
|
<n-form-item-gi
|
||||||
<n-input-number v-model:value="form.port" :min="1" :max="65535" style="width: 100%" />
|
label="端口"
|
||||||
|
path="port"
|
||||||
|
>
|
||||||
|
<n-input-number
|
||||||
|
v-model:value="form.port"
|
||||||
|
:min="1"
|
||||||
|
:max="65535"
|
||||||
|
style="width: 100%"
|
||||||
|
/>
|
||||||
</n-form-item-gi>
|
</n-form-item-gi>
|
||||||
<n-form-item-gi label="协议">
|
<n-form-item-gi label="协议">
|
||||||
<n-select v-model:value="form.protocol" :options="protocolOptions" />
|
<n-select
|
||||||
|
v-model:value="form.protocol"
|
||||||
|
:options="protocolOptions"
|
||||||
|
/>
|
||||||
</n-form-item-gi>
|
</n-form-item-gi>
|
||||||
<n-form-item-gi label="客户端 ID">
|
<n-form-item-gi label="客户端 ID">
|
||||||
<n-input v-model:value="form.clientId" placeholder="自动生成" />
|
<n-input
|
||||||
|
v-model:value="form.clientId"
|
||||||
|
placeholder="自动生成"
|
||||||
|
/>
|
||||||
</n-form-item-gi>
|
</n-form-item-gi>
|
||||||
<n-form-item-gi label="Keep Alive (秒)">
|
<n-form-item-gi label="Keep Alive (秒)">
|
||||||
<n-input-number
|
<n-input-number
|
||||||
@@ -146,20 +195,66 @@ async function handleSave() {
|
|||||||
/>
|
/>
|
||||||
</n-form-item-gi>
|
</n-form-item-gi>
|
||||||
<n-form-item-gi label="用户名">
|
<n-form-item-gi label="用户名">
|
||||||
<n-input v-model:value="form.username" placeholder="可选" />
|
<n-input
|
||||||
|
v-model:value="form.username"
|
||||||
|
placeholder="可选"
|
||||||
|
/>
|
||||||
</n-form-item-gi>
|
</n-form-item-gi>
|
||||||
<n-form-item-gi label="密码">
|
<n-form-item-gi label="密码">
|
||||||
<n-input v-model:value="form.password" type="password" placeholder="可选" />
|
<n-input
|
||||||
|
v-model:value="form.password"
|
||||||
|
type="password"
|
||||||
|
show-password-on="click"
|
||||||
|
placeholder="可选"
|
||||||
|
/>
|
||||||
|
</n-form-item-gi>
|
||||||
|
</n-grid>
|
||||||
|
<n-grid
|
||||||
|
:cols="2"
|
||||||
|
:x-gap="12"
|
||||||
|
>
|
||||||
|
<n-form-item-gi label="Clean Session">
|
||||||
|
<n-switch v-model:value="form.cleanSession" />
|
||||||
|
</n-form-item-gi>
|
||||||
|
<n-form-item-gi label="自动重连">
|
||||||
|
<n-switch v-model:value="form.reconnect" />
|
||||||
|
</n-form-item-gi>
|
||||||
|
<n-form-item-gi
|
||||||
|
label="连接超时 (毫秒)"
|
||||||
|
path="connectTimeout"
|
||||||
|
>
|
||||||
|
<n-input-number
|
||||||
|
v-model:value="form.connectTimeout"
|
||||||
|
:min="1000"
|
||||||
|
:max="120000"
|
||||||
|
:step="1000"
|
||||||
|
style="width: 100%"
|
||||||
|
/>
|
||||||
|
</n-form-item-gi>
|
||||||
|
<n-form-item-gi
|
||||||
|
label="重连间隔 (毫秒)"
|
||||||
|
path="reconnectInterval"
|
||||||
|
>
|
||||||
|
<n-input-number
|
||||||
|
v-model:value="form.reconnectInterval"
|
||||||
|
:min="1000"
|
||||||
|
:max="300000"
|
||||||
|
:step="1000"
|
||||||
|
:disabled="!form.reconnect"
|
||||||
|
style="width: 100%"
|
||||||
|
/>
|
||||||
</n-form-item-gi>
|
</n-form-item-gi>
|
||||||
</n-grid>
|
</n-grid>
|
||||||
<n-form-item label="Clean Session">
|
|
||||||
<n-switch v-model:value="form.cleanSession" />
|
|
||||||
</n-form-item>
|
|
||||||
</n-form>
|
</n-form>
|
||||||
<template #footer>
|
<template #footer>
|
||||||
<div style="display: flex; justify-content: flex-end; gap: 8px">
|
<div style="display: flex; justify-content: flex-end; gap: 8px">
|
||||||
<n-button @click="$emit('update:show', false)">取消</n-button>
|
<n-button
|
||||||
<n-button type="primary" @click="handleSave">{{ isEdit ? '更新' : '保存' }}</n-button>
|
@click="$emit('update:show', false)"
|
||||||
|
>取消</n-button>
|
||||||
|
<n-button
|
||||||
|
type="primary"
|
||||||
|
@click="handleSave"
|
||||||
|
>{{ isEdit ? '更新' : '保存' }}</n-button>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
</n-modal>
|
</n-modal>
|
||||||
|
|||||||
@@ -1,25 +1,44 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import { NLayoutSider, NButton, NTag, useMessage, useDialog } from 'naive-ui'
|
import { ref } from 'vue';
|
||||||
import { useMqttStore } from '../stores/mqtt.js'
|
import { NLayoutSider, NButton, NTag, useMessage, useDialog } from 'naive-ui';
|
||||||
|
import { useMqttStore } from '../stores/mqtt.js';
|
||||||
|
|
||||||
const emit = defineEmits(['add-server', 'edit-server'])
|
// 组件事件定义
|
||||||
const store = useMqttStore()
|
const emit = defineEmits(['add-server', 'edit-server']);
|
||||||
const message = useMessage()
|
|
||||||
const dialog = useDialog()
|
|
||||||
|
|
||||||
|
// 全局状态与消息/对话框 API
|
||||||
|
const store = useMqttStore();
|
||||||
|
const message = useMessage();
|
||||||
|
const dialog = useDialog();
|
||||||
|
|
||||||
|
// 文件输入控件引用,用于导入配置
|
||||||
|
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);
|
||||||
message.info('已断开连接: ' + server.name)
|
message.info('已断开连接: ' + server.name);
|
||||||
} else {
|
} else {
|
||||||
await store.connectServer(server.id)
|
await store.connectServer(server.id);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 编辑服务器
|
||||||
|
* @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: '确认删除',
|
||||||
@@ -27,10 +46,54 @@ function handleDelete(server) {
|
|||||||
positiveText: '删除',
|
positiveText: '删除',
|
||||||
negativeText: '取消',
|
negativeText: '取消',
|
||||||
onPositiveClick: async () => {
|
onPositiveClick: async () => {
|
||||||
await store.deleteServer(server.id)
|
await store.deleteServer(server.id);
|
||||||
message.info('服务器已删除')
|
message.info('服务器已删除');
|
||||||
}
|
},
|
||||||
})
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 导出服务器配置到本地 JSON 文件
|
||||||
|
*/
|
||||||
|
async function handleExport() {
|
||||||
|
try {
|
||||||
|
let data = await store.exportServers();
|
||||||
|
let blob = new Blob([JSON.stringify(data, null, 2)], { type: 'application/json' });
|
||||||
|
let url = URL.createObjectURL(blob);
|
||||||
|
let a = document.createElement('a');
|
||||||
|
a.href = url;
|
||||||
|
a.download = `frost-mqtt-servers-${new Date().toISOString().slice(0, 10)}.json`;
|
||||||
|
a.click();
|
||||||
|
URL.revokeObjectURL(url);
|
||||||
|
message.success('服务器配置已导出');
|
||||||
|
} catch (e) {
|
||||||
|
message.error('导出失败: ' + (e.message || '未知错误'));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 触发文件导入点击
|
||||||
|
*/
|
||||||
|
function handleImportClick() {
|
||||||
|
fileInput.value?.click();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 处理导入的 JSON 配置文件
|
||||||
|
* @param {Event} event - 文件选择事件
|
||||||
|
*/
|
||||||
|
async function handleImportFile(event) {
|
||||||
|
let file = event.target.files?.[0];
|
||||||
|
if (!file) return;
|
||||||
|
try {
|
||||||
|
let text = await file.text();
|
||||||
|
let data = JSON.parse(text);
|
||||||
|
await store.importServers(data);
|
||||||
|
message.success('服务器配置已导入');
|
||||||
|
} catch (e) {
|
||||||
|
message.error('导入失败: ' + (e.message || '未知错误'));
|
||||||
|
}
|
||||||
|
event.target.value = '';
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
@@ -43,17 +106,23 @@ function handleDelete(server) {
|
|||||||
show-trigger="bar"
|
show-trigger="bar"
|
||||||
:native-scrollbar="true"
|
:native-scrollbar="true"
|
||||||
content-class="sidebar-scroll-container"
|
content-class="sidebar-scroll-container"
|
||||||
:content-style="{ height: '100%', overflow: 'hidden' }"
|
:content-style="{
|
||||||
|
height: '100%',
|
||||||
|
overflow: 'hidden',
|
||||||
|
}"
|
||||||
>
|
>
|
||||||
<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>
|
||||||
<div class="sidebar-title">Frost MQTT Client</div>
|
<div class="sidebar-title">Frost MQTT Client</div>
|
||||||
<div class="sidebar-subtitle">多服务器 MQTT 客户端</div>
|
<div class="sidebar-subtitle">多功能 MQTT 客户端</div>
|
||||||
</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"
|
||||||
@@ -65,12 +134,21 @@ function handleDelete(server) {
|
|||||||
<div class="server-item-top">
|
<div class="server-item-top">
|
||||||
<span class="server-name">{{ srv.name }}</span>
|
<span class="server-name">{{ srv.name }}</span>
|
||||||
<div class="server-actions">
|
<div class="server-actions">
|
||||||
<n-button text size="tiny" @click.stop="handleEdit(srv)">
|
<n-button
|
||||||
|
text
|
||||||
|
size="tiny"
|
||||||
|
@click.stop="handleEdit(srv)"
|
||||||
|
>
|
||||||
<template #icon>
|
<template #icon>
|
||||||
<span style="font-size: 14px">✎</span>
|
<span style="font-size: 14px">✎</span>
|
||||||
</template>
|
</template>
|
||||||
</n-button>
|
</n-button>
|
||||||
<n-button text size="tiny" type="error" @click.stop="handleDelete(srv)">
|
<n-button
|
||||||
|
text
|
||||||
|
size="tiny"
|
||||||
|
type="error"
|
||||||
|
@click.stop="handleDelete(srv)"
|
||||||
|
>
|
||||||
<template #icon>
|
<template #icon>
|
||||||
<span style="font-size: 14px">✕</span>
|
<span style="font-size: 14px">✕</span>
|
||||||
</template>
|
</template>
|
||||||
@@ -81,60 +159,64 @@ function handleDelete(server) {
|
|||||||
<n-tag
|
<n-tag
|
||||||
:bordered="false"
|
:bordered="false"
|
||||||
size="small"
|
size="small"
|
||||||
:type="
|
:type="srv.status === 'connected' ? 'success' : srv.status === 'connecting' ? 'warning' : 'default'"
|
||||||
srv.status === 'connected'
|
|
||||||
? 'success'
|
|
||||||
: srv.status === 'connecting'
|
|
||||||
? 'warning'
|
|
||||||
: 'default'
|
|
||||||
"
|
|
||||||
>
|
>
|
||||||
<template #icon>
|
<template #icon>
|
||||||
<span class="status-dot" :class="srv.status" />
|
<span
|
||||||
|
class="status-dot"
|
||||||
|
:class="srv.status"
|
||||||
|
/>
|
||||||
</template>
|
</template>
|
||||||
{{ srv.protocol }}://{{ srv.host }}:{{ srv.port }}
|
{{ srv.protocol }}://{{ srv.host }}:{{ srv.port }}
|
||||||
</n-tag>
|
</n-tag>
|
||||||
</div>
|
</div>
|
||||||
<div class="server-bottom">
|
<div class="server-bottom">
|
||||||
<span class="server-status-text">
|
<span
|
||||||
{{
|
class="server-status-text"
|
||||||
srv.status === 'connected'
|
>{{ srv.status === 'connected' ? '已连接' : srv.status === 'connecting' ? '连接中...' : srv.status === 'error' ? '连接失败' : '未连接' }}</span>
|
||||||
? '已连接'
|
|
||||||
: srv.status === 'connecting'
|
|
||||||
? '连接中...'
|
|
||||||
: srv.status === 'error'
|
|
||||||
? '连接失败'
|
|
||||||
: '未连接'
|
|
||||||
}}
|
|
||||||
</span>
|
|
||||||
<n-button
|
<n-button
|
||||||
size="tiny"
|
size="tiny"
|
||||||
:type="
|
:type="srv.status === 'connected' ? 'error' : srv.status === 'connecting' ? 'warning' : 'success'"
|
||||||
srv.status === 'connected'
|
|
||||||
? 'error'
|
|
||||||
: srv.status === 'connecting'
|
|
||||||
? 'warning'
|
|
||||||
: 'success'
|
|
||||||
"
|
|
||||||
:disabled="srv.status === 'connecting'"
|
:disabled="srv.status === 'connecting'"
|
||||||
@click.stop="handleConnect(srv)"
|
@click.stop="handleConnect(srv)"
|
||||||
>
|
>{{ srv.status === 'connected' ? '断开' : srv.status === 'connecting' ? '...' : '连接' }}</n-button>
|
||||||
{{
|
|
||||||
srv.status === 'connected' ? '断开' : srv.status === 'connecting' ? '...' : '连接'
|
|
||||||
}}
|
|
||||||
</n-button>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- 侧边栏底部操作按钮 -->
|
||||||
<div class="sidebar-footer">
|
<div class="sidebar-footer">
|
||||||
<n-button type="primary" block @click="$emit('add-server')"> + 添加服务器 </n-button>
|
<n-button
|
||||||
|
type="primary"
|
||||||
|
block
|
||||||
|
@click="$emit('add-server')"
|
||||||
|
> + 添加服务器</n-button>
|
||||||
|
<div class="sidebar-import-export">
|
||||||
|
<n-button
|
||||||
|
size="small"
|
||||||
|
block
|
||||||
|
@click="handleImportClick"
|
||||||
|
>导入配置</n-button>
|
||||||
|
<n-button
|
||||||
|
size="small"
|
||||||
|
block
|
||||||
|
@click="handleExport"
|
||||||
|
>导出配置</n-button>
|
||||||
|
</div>
|
||||||
|
<input
|
||||||
|
ref="fileInput"
|
||||||
|
type="file"
|
||||||
|
accept=".json,application/json"
|
||||||
|
style="display: none"
|
||||||
|
@change="handleImportFile"
|
||||||
|
>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</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;
|
||||||
@@ -160,7 +242,7 @@ function handleDelete(server) {
|
|||||||
.sidebar-logo {
|
.sidebar-logo {
|
||||||
width: 32px;
|
width: 32px;
|
||||||
height: 32px;
|
height: 32px;
|
||||||
background: #18a058;
|
background: var(--frost-primary-color);
|
||||||
border-radius: 6px;
|
border-radius: 6px;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
@@ -196,15 +278,19 @@ function handleDelete(server) {
|
|||||||
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-item.active {
|
.server-actions {
|
||||||
background: var(--n-color-pressed);
|
opacity: 1;
|
||||||
border-color: #18a058;
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&.active {
|
||||||
|
background: var(--n-color-pressed);
|
||||||
|
border-color: var(--frost-primary-color);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.server-item-top {
|
.server-item-top {
|
||||||
@@ -228,12 +314,19 @@ function handleDelete(server) {
|
|||||||
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;
|
||||||
|
|
||||||
|
:deep(.n-tag) {
|
||||||
|
max-width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.n-tag__content) {
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.server-bottom {
|
.server-bottom {
|
||||||
@@ -254,20 +347,23 @@ function handleDelete(server) {
|
|||||||
height: 6px;
|
height: 6px;
|
||||||
border-radius: 50%;
|
border-radius: 50%;
|
||||||
margin-right: 2px;
|
margin-right: 2px;
|
||||||
}
|
|
||||||
|
|
||||||
.status-dot.connected {
|
&.connected {
|
||||||
background: #18a058;
|
background: var(--frost-success-color);
|
||||||
}
|
}
|
||||||
.status-dot.connecting {
|
|
||||||
background: #f0a020;
|
&.connecting {
|
||||||
animation: pulse 1s infinite;
|
background: var(--frost-warning-color);
|
||||||
}
|
animation: pulse 1s infinite;
|
||||||
.status-dot.disconnected {
|
}
|
||||||
background: #999;
|
|
||||||
}
|
&.disconnected {
|
||||||
.status-dot.error {
|
background: var(--frost-color-text-tertiary);
|
||||||
background: #d03050;
|
}
|
||||||
|
|
||||||
|
&.error {
|
||||||
|
background: var(--frost-error-color);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@keyframes pulse {
|
@keyframes pulse {
|
||||||
@@ -275,6 +371,7 @@ function handleDelete(server) {
|
|||||||
100% {
|
100% {
|
||||||
opacity: 1;
|
opacity: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
50% {
|
50% {
|
||||||
opacity: 0.3;
|
opacity: 0.3;
|
||||||
}
|
}
|
||||||
@@ -285,4 +382,11 @@ function handleDelete(server) {
|
|||||||
border-top: 1px solid var(--n-border-color);
|
border-top: 1px solid var(--n-border-color);
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.sidebar-import-export {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 1fr 1fr;
|
||||||
|
gap: 8px;
|
||||||
|
margin-top: 8px;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -1,13 +0,0 @@
|
|||||||
<script setup>
|
|
||||||
import { reactive } from 'vue'
|
|
||||||
|
|
||||||
const versions = reactive({ ...window.electron.process.versions })
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<ul class="versions">
|
|
||||||
<li class="electron-version">Electron v{{ versions.electron }}</li>
|
|
||||||
<li class="chrome-version">Chromium v{{ versions.chrome }}</li>
|
|
||||||
<li class="node-version">Node v{{ versions.node }}</li>
|
|
||||||
</ul>
|
|
||||||
</template>
|
|
||||||
@@ -1,5 +1,10 @@
|
|||||||
import { createApp } from 'vue'
|
import { createApp } from 'vue';
|
||||||
import App from './App.vue'
|
|
||||||
|
|
||||||
const app = createApp(App)
|
import App from './App.vue';
|
||||||
app.mount('#app')
|
|
||||||
|
/**
|
||||||
|
* 创建并挂载 Vue 应用
|
||||||
|
*/
|
||||||
|
const app = createApp(App);
|
||||||
|
|
||||||
|
app.mount('#app');
|
||||||
|
|||||||
@@ -1,205 +1,553 @@
|
|||||||
import { ref, computed } from 'vue'
|
import { ref, computed } from 'vue';
|
||||||
|
|
||||||
// 全局状态(单例)
|
// ==================== 类型定义 ====================
|
||||||
const servers = ref([])
|
|
||||||
const activeServerId = ref(null)
|
|
||||||
const messages = ref([])
|
|
||||||
const publishHistory = ref([])
|
|
||||||
const loading = ref(false)
|
|
||||||
|
|
||||||
// 清理函数集合
|
/**
|
||||||
let cleanupFns = []
|
* 从公共类型定义文件引入 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
|
||||||
|
*/
|
||||||
|
|
||||||
function setupListeners() {
|
/**
|
||||||
// 清理旧监听器
|
* @typedef {Object} StatusChangeData 服务器状态变更事件数据
|
||||||
cleanupFns.forEach((fn) => fn())
|
* @property {string} id 服务器 id
|
||||||
cleanupFns = []
|
* @property {MqttServerStatus} status 新状态
|
||||||
|
* @property {string} [error] 错误信息(状态为 error 时存在)
|
||||||
|
*/
|
||||||
|
|
||||||
cleanupFns.push(
|
/**
|
||||||
window.api.onStatusChange((data) => {
|
* @typedef {Object} MessageEventData 消息接收事件数据
|
||||||
const srv = servers.value.find((s) => s.id === data.id)
|
* @property {string} serverId 所属服务器 id
|
||||||
if (srv) {
|
* @property {MqttMessage} message 消息对象
|
||||||
srv.status = data.status
|
*/
|
||||||
if (data.status === 'disconnected') {
|
|
||||||
srv.topics.forEach((t) => {
|
|
||||||
t.subscribed = false
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
)
|
|
||||||
|
|
||||||
cleanupFns.push(
|
/**
|
||||||
window.api.onMessage((data) => {
|
* @typedef {Object} SubscriptionChangeData 订阅状态变更事件数据
|
||||||
if (data.serverId === activeServerId.value) {
|
* @property {string} serverId 所属服务器 id
|
||||||
messages.value.unshift(data.message)
|
* @property {string} topic 主题名称
|
||||||
if (messages.value.length > 500) messages.value.pop()
|
* @property {string} [topicId] 主题 id
|
||||||
}
|
* @property {string} [comment] 主题备注
|
||||||
})
|
* @property {boolean} subscribed 是否已订阅
|
||||||
)
|
*/
|
||||||
|
|
||||||
cleanupFns.push(
|
/**
|
||||||
window.api.onSubscriptionChange((data) => {
|
* @typedef {Object} MqttStore
|
||||||
if (data.serverId === activeServerId.value) {
|
* @property {import('vue').Ref<MqttServer[]>} servers 所有服务器列表
|
||||||
const srv = servers.value.find((s) => s.id === data.serverId)
|
* @property {import('vue').Ref<string | null>} activeServerId 当前活动服务器 ID
|
||||||
if (srv) {
|
* @property {import('vue').ComputedRef<MqttServer | null>} activeServer 当前活动服务器对象
|
||||||
const t = srv.topics.find((t) => t.topic === data.topic || t.id === data.topicId)
|
* @property {import('vue').Ref<MqttMessage[]>} messages 当前活动服务器消息列表
|
||||||
if (t) {
|
* @property {import('vue').Ref<PublishRecord[]>} publishHistory 当前活动服务器发布历史
|
||||||
t.subscribed = data.subscribed
|
* @property {import('vue').Ref<boolean>} loading 全局加载状态
|
||||||
if (data.topic && data.topic !== t.topic) {
|
* @property {import('vue').ComputedRef<MqttServer[]>} connectedServers 已连接服务器列表
|
||||||
t.topic = data.topic
|
* @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([]);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 当前在界面中选中的服务器 ID
|
||||||
|
* @type {import('vue').Ref<string | null>}
|
||||||
|
*/
|
||||||
|
const activeServerId = ref(null);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 当前活动服务器的消息列表(包含订阅接收与本地发布的消息)
|
||||||
|
* @type {import('vue').Ref<MqttMessage[]>}
|
||||||
|
*/
|
||||||
|
const messages = ref([]);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 当前活动服务器的发布历史记录
|
||||||
|
* @type {import('vue').Ref<PublishRecord[]>}
|
||||||
|
*/
|
||||||
|
const publishHistory = ref([]);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Store 初始化或异步操作时的全局加载状态
|
||||||
|
* @type {import('vue').Ref<boolean>}
|
||||||
|
*/
|
||||||
|
const loading = ref(false);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* IPC 监听器清理函数集合
|
||||||
|
* @type {Array<() => void>}
|
||||||
|
*/
|
||||||
|
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() {
|
||||||
|
|
||||||
|
// 清理旧监听器
|
||||||
|
cleanupFns.forEach((fn) => fn());
|
||||||
|
cleanupFns = [];
|
||||||
|
|
||||||
|
cleanupFns.push(
|
||||||
|
window.api.onStatusChange((/** @type {StatusChangeData} */ data) => {
|
||||||
|
|
||||||
|
let srv = servers.value.find((s) => {
|
||||||
|
return s.id === data.id;
|
||||||
|
});
|
||||||
|
|
||||||
|
if (srv) {
|
||||||
|
|
||||||
|
srv.status = data.status;
|
||||||
|
|
||||||
|
if (data.status === 'disconnected') {
|
||||||
|
srv.topics.forEach((t) => {
|
||||||
|
t.subscribed = false;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
cleanupFns.push(
|
||||||
|
window.api.onMessage((/** @type {MessageEventData} */ data) => {
|
||||||
|
if (data.serverId === activeServerId.value) {
|
||||||
|
|
||||||
|
messages.value.unshift(data.message);
|
||||||
|
|
||||||
|
if (messages.value.length > 500) {
|
||||||
|
messages.value.pop();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
cleanupFns.push(
|
||||||
|
window.api.onSubscriptionChange((/** @type {SubscriptionChangeData} */ data) => {
|
||||||
|
if (data.serverId === activeServerId.value) {
|
||||||
|
|
||||||
|
let srv = servers.value.find((s) => {
|
||||||
|
return s.id === data.serverId;
|
||||||
|
});
|
||||||
|
|
||||||
|
if (srv) {
|
||||||
|
|
||||||
|
let t = srv.topics.find((t) => {
|
||||||
|
return t.topic === data.topic || t.id === data.topicId;
|
||||||
|
});
|
||||||
|
|
||||||
|
if (t) {
|
||||||
|
t.subscribed = data.subscribed;
|
||||||
|
if (typeof data.comment === 'string') {
|
||||||
|
t.comment = data.comment;
|
||||||
|
}
|
||||||
|
if (data.topic && data.topic !== t.topic) {
|
||||||
|
t.topic = data.topic;
|
||||||
|
sortTopics(srv);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* MQTT 全局状态管理 Store
|
||||||
|
* @returns {MqttStore} 包含状态、计算属性和方法的对象
|
||||||
|
*/
|
||||||
export function useMqttStore() {
|
export function useMqttStore() {
|
||||||
const activeServer = computed(
|
|
||||||
|
/**
|
||||||
|
* 当前选中的服务器
|
||||||
|
* @type {import('vue').ComputedRef<MqttServer | null>}
|
||||||
|
*/
|
||||||
|
let activeServer = computed(
|
||||||
() => servers.value.find((s) => s.id === activeServerId.value) || null
|
() => servers.value.find((s) => s.id === activeServerId.value) || null
|
||||||
)
|
);
|
||||||
|
|
||||||
const connectedServers = computed(() => servers.value.filter((s) => s.status === 'connected'))
|
/**
|
||||||
|
* 已连接的服务器列表
|
||||||
|
* @type {import('vue').ComputedRef<MqttServer[]>}
|
||||||
|
*/
|
||||||
|
let connectedServers = computed(() => servers.value.filter((s) => s.status === 'connected'));
|
||||||
|
|
||||||
const totalTopics = computed(() =>
|
/**
|
||||||
|
* 所有服务器主题总数
|
||||||
|
* @type {import('vue').ComputedRef<number>}
|
||||||
|
*/
|
||||||
|
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)
|
||||||
)
|
);
|
||||||
|
|
||||||
const subscribedTopics = computed(() =>
|
/**
|
||||||
servers.value.reduce((sum, s) => sum + (s.topics?.filter((t) => t.subscribed)?.length || 0), 0)
|
* 已订阅主题总数
|
||||||
)
|
* @type {import('vue').ComputedRef<number>}
|
||||||
|
*/
|
||||||
|
let subscribedTopics = computed(() =>
|
||||||
|
servers.value.reduce(
|
||||||
|
(sum, s) => sum + (s.topics?.filter((t) => t.subscribed)?.length || 0),
|
||||||
|
0
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
const totalMessages = computed(() => messages.value.length)
|
/**
|
||||||
|
* 当前活动服务器消息总数
|
||||||
|
* @type {import('vue').ComputedRef<number>}
|
||||||
|
*/
|
||||||
|
let totalMessages = computed(() => messages.value.length);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 按服务器名称对服务器列表排序(中文环境)
|
||||||
|
* @returns {void}
|
||||||
|
*/
|
||||||
|
function sortServers() {
|
||||||
|
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;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const data = await window.api.getServers()
|
|
||||||
servers.value = data
|
let data = await window.api.getServers();
|
||||||
if (data.length > 0 && !activeServerId.value) {
|
|
||||||
activeServerId.value = data[0].id
|
servers.value = data;
|
||||||
}
|
sortServers();
|
||||||
setupListeners()
|
servers.value.forEach((srv) => sortTopics(srv));
|
||||||
|
setupListeners();
|
||||||
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error('初始化失败:', e)
|
console.error('初始化失败:', e);
|
||||||
} finally {
|
} finally {
|
||||||
loading.value = false
|
loading.value = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ==================== 服务器管理 ====================
|
// ==================== 服务器管理 ====================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置当前活动服务器并刷新相关数据
|
||||||
|
* @param {string|null} id - 服务器 id
|
||||||
|
*/
|
||||||
function setActiveServer(id) {
|
function setActiveServer(id) {
|
||||||
activeServerId.value = id
|
|
||||||
|
activeServerId.value = id;
|
||||||
|
|
||||||
// 切换时刷新消息
|
// 切换时刷新消息
|
||||||
refreshMessages()
|
refreshMessages();
|
||||||
refreshPublishHistory()
|
refreshPublishHistory();
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 添加服务器
|
||||||
|
* @param {ServerConfig} config - 服务器配置
|
||||||
|
* @returns {Promise<MqttServer>} 添加后的服务器对象
|
||||||
|
*/
|
||||||
async function addServer(config) {
|
async function addServer(config) {
|
||||||
const server = await window.api.addServer(config)
|
|
||||||
servers.value.push(server)
|
let server = await window.api.addServer(config);
|
||||||
|
|
||||||
|
servers.value.push(server);
|
||||||
|
sortServers();
|
||||||
|
|
||||||
if (!activeServerId.value) {
|
if (!activeServerId.value) {
|
||||||
activeServerId.value = server.id
|
activeServerId.value = server.id;
|
||||||
}
|
}
|
||||||
return server
|
|
||||||
|
return server;
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 更新服务器配置
|
||||||
|
* @param {string} id - 服务器 id
|
||||||
|
* @param {ServerConfig} config - 新的服务器配置
|
||||||
|
* @returns {Promise<MqttServer | null>} 更新后的服务器对象或 null
|
||||||
|
*/
|
||||||
async function updateServer(id, config) {
|
async function updateServer(id, config) {
|
||||||
const result = await window.api.updateServer(id, config)
|
|
||||||
|
let result = await window.api.updateServer(id, config);
|
||||||
|
|
||||||
if (result) {
|
if (result) {
|
||||||
const idx = servers.value.findIndex((s) => s.id === id)
|
let idx = servers.value.findIndex((s) => s.id === id);
|
||||||
if (idx !== -1) {
|
if (idx !== -1) {
|
||||||
servers.value[idx] = { ...servers.value[idx], ...result }
|
servers.value[idx] = { ...servers.value[idx], ...result };
|
||||||
}
|
}
|
||||||
|
sortServers();
|
||||||
}
|
}
|
||||||
return result
|
|
||||||
|
return result;
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 删除服务器
|
||||||
|
* @param {string} id - 服务器 id
|
||||||
|
* @returns {Promise<void>}
|
||||||
|
*/
|
||||||
async function deleteServer(id) {
|
async function deleteServer(id) {
|
||||||
await window.api.deleteServer(id)
|
|
||||||
servers.value = servers.value.filter((s) => s.id !== id)
|
await window.api.deleteServer(id);
|
||||||
|
|
||||||
|
servers.value = servers.value.filter((s) => s.id !== id);
|
||||||
|
sortServers();
|
||||||
|
|
||||||
if (activeServerId.value === id) {
|
if (activeServerId.value === id) {
|
||||||
activeServerId.value = servers.value.length > 0 ? servers.value[0].id : null
|
activeServerId.value = servers.value.length > 0 ? servers.value[0].id : null;
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 导出所有服务器配置
|
||||||
|
* @returns {Promise<ServerConfig[]>} 服务器配置数组
|
||||||
|
*/
|
||||||
|
async function exportServers() {
|
||||||
|
return await window.api.exportServers();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 导入服务器配置列表
|
||||||
|
* @param {ServerConfig[]} serverList - 服务器配置数组
|
||||||
|
* @returns {Promise<MqttServer[]>} 导入后的服务器列表
|
||||||
|
*/
|
||||||
|
async function importServers(serverList) {
|
||||||
|
let data = await window.api.importServers(serverList);
|
||||||
|
servers.value = data;
|
||||||
|
sortServers();
|
||||||
|
servers.value.forEach((srv) => sortTopics(srv));
|
||||||
|
if (servers.value.length > 0 && !activeServerId.value) {
|
||||||
|
activeServerId.value = servers.value[0].id;
|
||||||
|
}
|
||||||
|
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) {
|
|
||||||
const 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) {
|
||||||
const srv = servers.value.find((s) => s.id === serverId)
|
let srv = servers.value.find((s) => s.id === serverId);
|
||||||
if (srv) {
|
if (srv) {
|
||||||
srv.topics.push({ topic, qos, subscribed: false })
|
srv.topics.push(result);
|
||||||
|
sortTopics(srv);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return result
|
|
||||||
|
return result;
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 移除指定服务器的主题
|
||||||
|
* @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)
|
|
||||||
const srv = servers.value.find((s) => s.id === serverId)
|
await window.api.removeTopic(serverId, topic);
|
||||||
|
|
||||||
|
let srv = servers.value.find((s) => s.id === serverId);
|
||||||
|
|
||||||
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) {
|
||||||
const result = await window.api.publish(serverId, topic, payload, opts)
|
|
||||||
|
let result = await window.api.publish(serverId, topic, payload, opts);
|
||||||
|
|
||||||
if (result && serverId === activeServerId.value) {
|
if (result && serverId === activeServerId.value) {
|
||||||
refreshPublishHistory()
|
refreshPublishHistory();
|
||||||
}
|
}
|
||||||
return result
|
|
||||||
|
return result;
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ==================== 消息管理 ====================
|
// ==================== 消息管理 ====================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 刷新当前活动服务器的消息列表
|
||||||
|
* @returns {Promise<void>}
|
||||||
|
*/
|
||||||
async function refreshMessages() {
|
async function refreshMessages() {
|
||||||
if (!activeServerId.value) return
|
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) return
|
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) return
|
if (activeServerId.value) {
|
||||||
await window.api.clearMessages(activeServerId.value)
|
await window.api.clearMessages(activeServerId.value);
|
||||||
messages.value = []
|
messages.value = [];
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 清空当前活动服务器的发布历史
|
||||||
|
* @returns {Promise<void>}
|
||||||
|
*/
|
||||||
async function clearPublishHistory() {
|
async function clearPublishHistory() {
|
||||||
if (!activeServerId.value) return
|
if (activeServerId.value) {
|
||||||
await window.api.clearPublishHistory(activeServerId.value)
|
await window.api.clearPublishHistory(activeServerId.value);
|
||||||
publishHistory.value = []
|
publishHistory.value = [];
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
@@ -221,6 +569,8 @@ export function useMqttStore() {
|
|||||||
addServer,
|
addServer,
|
||||||
updateServer,
|
updateServer,
|
||||||
deleteServer,
|
deleteServer,
|
||||||
|
exportServers,
|
||||||
|
importServers,
|
||||||
connectServer,
|
connectServer,
|
||||||
disconnectServer,
|
disconnectServer,
|
||||||
addTopic,
|
addTopic,
|
||||||
@@ -233,6 +583,7 @@ export function useMqttStore() {
|
|||||||
refreshMessages,
|
refreshMessages,
|
||||||
refreshPublishHistory,
|
refreshPublishHistory,
|
||||||
clearMessages,
|
clearMessages,
|
||||||
clearPublishHistory
|
clearPublishHistory,
|
||||||
}
|
};
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,60 +1,78 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import { useMqttStore } from '../stores/mqtt.js'
|
import { useMqttStore } from '../stores/mqtt.js';
|
||||||
import {
|
import { NGrid, NGridItem, NCard, NStatistic, NTag, NDescriptions, NDescriptionsItem, NSpace, NButton, NList, NListItem, useMessage } from 'naive-ui';
|
||||||
NGrid,
|
|
||||||
NGridItem,
|
|
||||||
NCard,
|
|
||||||
NStatistic,
|
|
||||||
NTag,
|
|
||||||
NDescriptions,
|
|
||||||
NDescriptionsItem,
|
|
||||||
NSpace,
|
|
||||||
NButton,
|
|
||||||
NList,
|
|
||||||
NListItem
|
|
||||||
} from 'naive-ui'
|
|
||||||
import { useMessage } from 'naive-ui'
|
|
||||||
|
|
||||||
const store = useMqttStore()
|
// 全局状态与消息 API
|
||||||
const msg = useMessage()
|
const store = useMqttStore();
|
||||||
|
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);
|
||||||
} else {
|
} else {
|
||||||
store.setActiveServer(srv.id)
|
store.setActiveServer(srv.id);
|
||||||
emit('switch-tab', action)
|
emit('switch-tab', 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);
|
||||||
msg.info('已断开连接: ' + srv.name)
|
msg.info('已断开连接: ' + srv.name);
|
||||||
} else if (srv.status === 'disconnected' || srv.status === 'error') {
|
} else if (srv.status === 'disconnected' || srv.status === 'error') {
|
||||||
await store.connectServer(srv.id)
|
await store.connectServer(srv.id);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const emit = defineEmits(['switch-tab', 'edit-server'])
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="dashboard">
|
<div class="dashboard">
|
||||||
|
|
||||||
<!-- 统计卡片 -->
|
<!-- 统计卡片 -->
|
||||||
<n-grid :cols="4" :x-gap="12" responsive="screen">
|
<n-grid
|
||||||
|
:cols="4"
|
||||||
|
:x-gap="12"
|
||||||
|
responsive="screen"
|
||||||
|
>
|
||||||
<n-grid-item>
|
<n-grid-item>
|
||||||
<n-card size="small" :bordered="true">
|
<n-card
|
||||||
<n-statistic label="服务器总数" :value="store.servers.value.length" />
|
size="small"
|
||||||
|
:bordered="true"
|
||||||
|
>
|
||||||
|
<n-statistic
|
||||||
|
label="服务器总数"
|
||||||
|
:value="store.servers.value.length"
|
||||||
|
/>
|
||||||
</n-card>
|
</n-card>
|
||||||
</n-grid-item>
|
</n-grid-item>
|
||||||
<n-grid-item>
|
<n-grid-item>
|
||||||
<n-card size="small" :bordered="true">
|
<n-card
|
||||||
<n-statistic label="已连接" :value="store.connectedServers.value.length" />
|
size="small"
|
||||||
|
:bordered="true"
|
||||||
|
>
|
||||||
|
<n-statistic
|
||||||
|
label="已连接"
|
||||||
|
:value="store.connectedServers.value.length"
|
||||||
|
/>
|
||||||
</n-card>
|
</n-card>
|
||||||
</n-grid-item>
|
</n-grid-item>
|
||||||
<n-grid-item>
|
<n-grid-item>
|
||||||
<n-card size="small" :bordered="true">
|
<n-card
|
||||||
|
size="small"
|
||||||
|
:bordered="true"
|
||||||
|
>
|
||||||
<n-statistic
|
<n-statistic
|
||||||
label="已订阅主题"
|
label="已订阅主题"
|
||||||
:value="`${store.subscribedTopics.value}/${store.totalTopics.value}`"
|
:value="`${store.subscribedTopics.value}/${store.totalTopics.value}`"
|
||||||
@@ -62,8 +80,14 @@ const emit = defineEmits(['switch-tab', 'edit-server'])
|
|||||||
</n-card>
|
</n-card>
|
||||||
</n-grid-item>
|
</n-grid-item>
|
||||||
<n-grid-item>
|
<n-grid-item>
|
||||||
<n-card size="small" :bordered="true">
|
<n-card
|
||||||
<n-statistic label="消息总数" :value="store.messages.value.length" />
|
size="small"
|
||||||
|
:bordered="true"
|
||||||
|
>
|
||||||
|
<n-statistic
|
||||||
|
label="消息总数"
|
||||||
|
:value="store.messages.value.length"
|
||||||
|
/>
|
||||||
</n-card>
|
</n-card>
|
||||||
</n-grid-item>
|
</n-grid-item>
|
||||||
</n-grid>
|
</n-grid>
|
||||||
@@ -77,29 +101,18 @@ const emit = defineEmits(['switch-tab', 'edit-server'])
|
|||||||
>
|
>
|
||||||
<template #header-extra>
|
<template #header-extra>
|
||||||
<n-tag
|
<n-tag
|
||||||
:type="
|
:type="store.activeServer.value.status === 'connected' ? 'success' : store.activeServer.value.status === 'connecting' ? 'warning' : 'error'"
|
||||||
store.activeServer.value.status === 'connected'
|
|
||||||
? 'success'
|
|
||||||
: store.activeServer.value.status === 'connecting'
|
|
||||||
? 'warning'
|
|
||||||
: 'error'
|
|
||||||
"
|
|
||||||
size="small"
|
size="small"
|
||||||
>
|
>{{ store.activeServer.value.status === 'connected' ? '已连接' : store.activeServer.value.status === 'connecting' ? '连接中...' : '未连接' }}</n-tag>
|
||||||
{{
|
|
||||||
store.activeServer.value.status === 'connected'
|
|
||||||
? '已连接'
|
|
||||||
: store.activeServer.value.status === 'connecting'
|
|
||||||
? '连接中...'
|
|
||||||
: '未连接'
|
|
||||||
}}
|
|
||||||
</n-tag>
|
|
||||||
</template>
|
</template>
|
||||||
<n-descriptions :column="2" size="small" bordered>
|
<n-descriptions
|
||||||
|
class="server-descriptions"
|
||||||
|
:column="2"
|
||||||
|
size="small"
|
||||||
|
bordered
|
||||||
|
>
|
||||||
<n-descriptions-item label="地址">
|
<n-descriptions-item label="地址">
|
||||||
{{ store.activeServer.value.protocol }}://{{ store.activeServer.value.host }}:{{
|
{{ store.activeServer.value.protocol }}://{{ store.activeServer.value.host }}:{{ store.activeServer.value.port }}
|
||||||
store.activeServer.value.port
|
|
||||||
}}
|
|
||||||
</n-descriptions-item>
|
</n-descriptions-item>
|
||||||
<n-descriptions-item label="客户端 ID">
|
<n-descriptions-item label="客户端 ID">
|
||||||
{{ store.activeServer.value.clientId }}
|
{{ store.activeServer.value.clientId }}
|
||||||
@@ -116,52 +129,50 @@ const emit = defineEmits(['switch-tab', 'edit-server'])
|
|||||||
size="small"
|
size="small"
|
||||||
:type="store.activeServer.value.status === 'connected' ? 'error' : 'success'"
|
:type="store.activeServer.value.status === 'connected' ? 'error' : 'success'"
|
||||||
@click="handleToggleConnection(store.activeServer.value)"
|
@click="handleToggleConnection(store.activeServer.value)"
|
||||||
>
|
>{{ store.activeServer.value.status === 'connected' ? '断开连接' : '连接服务器' }}</n-button>
|
||||||
{{ store.activeServer.value.status === 'connected' ? '断开连接' : '连接服务器' }}
|
<n-button
|
||||||
</n-button>
|
size="small"
|
||||||
<n-button size="small" @click="$emit('edit-server', store.activeServer.value)"
|
@click="$emit('edit-server', store.activeServer.value)"
|
||||||
>编辑配置</n-button
|
>编辑配置</n-button>
|
||||||
>
|
<n-button
|
||||||
<n-button size="small" @click="handleAction(store.activeServer.value, 'subscribe')"
|
size="small"
|
||||||
>管理订阅</n-button
|
@click="handleAction(store.activeServer.value, 'subscribe')"
|
||||||
>
|
>管理订阅</n-button>
|
||||||
<n-button size="small" @click="handleAction(store.activeServer.value, 'publish')"
|
<n-button
|
||||||
>发布消息</n-button
|
size="small"
|
||||||
>
|
@click="handleAction(store.activeServer.value, 'publish')"
|
||||||
<n-button size="small" @click="handleAction(store.activeServer.value, 'messages')"
|
>发布消息</n-button>
|
||||||
>查看消息</n-button
|
<n-button
|
||||||
>
|
size="small"
|
||||||
|
@click="handleAction(store.activeServer.value, 'messages')"
|
||||||
|
>查看消息</n-button>
|
||||||
</n-space>
|
</n-space>
|
||||||
</n-card>
|
</n-card>
|
||||||
|
|
||||||
<!-- 所有服务器状态 -->
|
<!-- 所有服务器状态 -->
|
||||||
<n-card title="所有服务器状态" size="small" style="margin-top: 12px">
|
<n-card
|
||||||
|
title="所有服务器状态"
|
||||||
|
size="small"
|
||||||
|
style="margin-top: 12px"
|
||||||
|
>
|
||||||
<n-list>
|
<n-list>
|
||||||
<n-list-item v-for="srv in store.servers.value" :key="srv.id">
|
<n-list-item
|
||||||
|
v-for="srv in store.servers.value"
|
||||||
|
:key="srv.id"
|
||||||
|
>
|
||||||
<template #prefix>
|
<template #prefix>
|
||||||
<span class="status-dot" :class="srv.status" />
|
<span
|
||||||
|
class="status-dot"
|
||||||
|
:class="srv.status"
|
||||||
|
/>
|
||||||
</template>
|
</template>
|
||||||
<n-space align="center">
|
<n-space align="center">
|
||||||
<span style="font-weight: 500">{{ srv.name }}</span>
|
<span style="font-weight: 500">{{ srv.name }}</span>
|
||||||
<n-tag
|
<n-tag
|
||||||
:bordered="false"
|
:bordered="false"
|
||||||
size="small"
|
size="small"
|
||||||
:type="
|
:type="srv.status === 'connected' ? 'success' : srv.status === 'connecting' ? 'warning' : 'default'"
|
||||||
srv.status === 'connected'
|
>{{ srv.status === 'connected' ? '已连接' : srv.status === 'connecting' ? '连接中...' : '未连接' }}</n-tag>
|
||||||
? 'success'
|
|
||||||
: srv.status === 'connecting'
|
|
||||||
? 'warning'
|
|
||||||
: 'default'
|
|
||||||
"
|
|
||||||
>
|
|
||||||
{{
|
|
||||||
srv.status === 'connected'
|
|
||||||
? '已连接'
|
|
||||||
: srv.status === 'connecting'
|
|
||||||
? '连接中...'
|
|
||||||
: '未连接'
|
|
||||||
}}
|
|
||||||
</n-tag>
|
|
||||||
</n-space>
|
</n-space>
|
||||||
<template #suffix>
|
<template #suffix>
|
||||||
<n-button
|
<n-button
|
||||||
@@ -169,42 +180,60 @@ const emit = defineEmits(['switch-tab', 'edit-server'])
|
|||||||
:type="srv.status === 'connected' ? 'error' : 'success'"
|
:type="srv.status === 'connected' ? 'error' : 'success'"
|
||||||
:disabled="srv.status === 'connecting'"
|
:disabled="srv.status === 'connecting'"
|
||||||
@click="handleToggleConnection(srv)"
|
@click="handleToggleConnection(srv)"
|
||||||
>
|
>{{ srv.status === 'connected' ? '断开' : srv.status === 'connecting' ? '...' : '连接' }}</n-button>
|
||||||
{{
|
|
||||||
srv.status === 'connected' ? '断开' : srv.status === 'connecting' ? '...' : '连接'
|
|
||||||
}}
|
|
||||||
</n-button>
|
|
||||||
</template>
|
</template>
|
||||||
</n-list-item>
|
</n-list-item>
|
||||||
</n-list>
|
</n-list>
|
||||||
</n-card>
|
</n-card>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped lang="less">
|
||||||
.dashboard {
|
.dashboard {
|
||||||
max-width: 900px;
|
max-width: 900px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.server-descriptions {
|
||||||
|
:deep(.n-descriptions-table) {
|
||||||
|
table-layout: fixed;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.n-descriptions-table-header),
|
||||||
|
:deep(.n-descriptions-table-content) {
|
||||||
|
width: 25%;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.n-descriptions-table-content) {
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
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%;
|
||||||
}
|
|
||||||
|
|
||||||
.status-dot.connected {
|
&.connected {
|
||||||
background: #18a058;
|
background: var(--frost-success-color);
|
||||||
}
|
}
|
||||||
.status-dot.connecting {
|
|
||||||
background: #f0a020;
|
&.connecting {
|
||||||
animation: pulse 1s infinite;
|
background: var(--frost-warning-color);
|
||||||
}
|
animation: pulse 1s infinite;
|
||||||
.status-dot.disconnected {
|
}
|
||||||
background: #999;
|
|
||||||
}
|
&.disconnected {
|
||||||
.status-dot.error {
|
background: var(--frost-color-text-tertiary);
|
||||||
background: #d03050;
|
}
|
||||||
|
|
||||||
|
&.error {
|
||||||
|
background: var(--frost-error-color);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@keyframes pulse {
|
@keyframes pulse {
|
||||||
@@ -212,6 +241,7 @@ const emit = defineEmits(['switch-tab', 'edit-server'])
|
|||||||
100% {
|
100% {
|
||||||
opacity: 1;
|
opacity: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
50% {
|
50% {
|
||||||
opacity: 0.3;
|
opacity: 0.3;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,108 +1,229 @@
|
|||||||
<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 } from 'naive-ui'
|
import { NCard, NSelect, NButton, NEmpty, useMessage } from 'naive-ui';
|
||||||
|
|
||||||
const store = useMqttStore()
|
// 全局状态与消息 API
|
||||||
|
const store = useMqttStore();
|
||||||
|
const message = useMessage();
|
||||||
|
|
||||||
const filter = ref('')
|
// 已选中的过滤主题列表
|
||||||
|
const filter = ref([]);
|
||||||
|
|
||||||
const filteredMessages = computed(() => {
|
/**
|
||||||
if (!filter.value.trim()) return store.messages.value
|
* 复制主题到剪贴板
|
||||||
const f = filter.value.trim().toLowerCase()
|
* @param {string} topic - 主题名称
|
||||||
return store.messages.value.filter((m) => m.topic.toLowerCase().includes(f))
|
*/
|
||||||
})
|
async function copyTopic(topic) {
|
||||||
|
try {
|
||||||
async function handleClear() {
|
await navigator.clipboard.writeText(topic);
|
||||||
await store.clearMessages()
|
message.success('主题已复制');
|
||||||
|
} catch {
|
||||||
|
message.error('复制失败');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 可过滤的主题选项(来自已配置主题和已接收消息)
|
||||||
|
* @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(() => {
|
||||||
|
|
||||||
|
if (!filter.value || filter.value.length === 0) {
|
||||||
|
return store.messages.value;
|
||||||
|
}
|
||||||
|
|
||||||
|
return store.messages.value.filter((m) =>
|
||||||
|
filter.value.some((f) => topicMatchesFilter(f, m.topic))
|
||||||
|
);
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 清空当前活动服务器的消息列表
|
||||||
|
*/
|
||||||
|
async function handleClear() {
|
||||||
|
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);
|
||||||
} catch {
|
} catch {
|
||||||
return payload
|
return payload;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div v-if="store.activeServer.value" class="messages-view">
|
|
||||||
|
<!-- 内容区域 -->
|
||||||
|
<div
|
||||||
|
v-if="store.activeServer.value"
|
||||||
|
class="messages-view"
|
||||||
|
>
|
||||||
<n-card
|
<n-card
|
||||||
size="small"
|
size="small"
|
||||||
:content-style="{
|
:content-style="{
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
flexDirection: 'column',
|
flexDirection: 'column',
|
||||||
minHeight: 0,
|
minHeight: 0,
|
||||||
overflow: 'hidden'
|
overflow: 'hidden',
|
||||||
}"
|
}"
|
||||||
>
|
>
|
||||||
<template #header>
|
<template #header>
|
||||||
<n-space align="center" justify="space-between" style="width: 100%">
|
<div
|
||||||
|
style="display: flex; flex-direction: column; gap: 8px; width: 100%"
|
||||||
|
>
|
||||||
<div>
|
<div>
|
||||||
<span style="font-weight: 700">消息监控</span>
|
<span>消息监控</span>
|
||||||
<span style="font-size: 12px; opacity: 0.55; margin-left: 8px"
|
<span
|
||||||
>实时查看接收和发送的 MQTT 消息</span
|
style="font-size: 12px; opacity: 0.55; margin-left: 8px"
|
||||||
>
|
>实时查看接收和发送的 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 size="small" @click="handleClear">清空</n-button>
|
<n-button
|
||||||
</n-space>
|
size="small"
|
||||||
</n-space>
|
@click="handleClear"
|
||||||
|
>清空消息</n-button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<n-empty v-if="filteredMessages.length === 0" description="等待消息...">
|
<n-empty
|
||||||
|
v-if="filteredMessages.length === 0"
|
||||||
|
description="等待消息..."
|
||||||
|
>
|
||||||
<template #icon>
|
<template #icon>
|
||||||
<span style="font-size: 32px; opacity: 0.4">☰</span>
|
<span style="font-size: 32px; opacity: 0.4">☰</span>
|
||||||
</template>
|
</template>
|
||||||
</n-empty>
|
</n-empty>
|
||||||
<div v-else ref="logRef" class="msg-log">
|
<div
|
||||||
<div v-for="m in filteredMessages" :key="m.id" class="msg-entry">
|
v-else
|
||||||
|
ref="logRef"
|
||||||
|
class="msg-log"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
v-for="m in filteredMessages"
|
||||||
|
:key="m.id"
|
||||||
|
class="msg-entry"
|
||||||
|
>
|
||||||
<div class="msg-header">
|
<div class="msg-header">
|
||||||
<span class="msg-time">{{ m.time }}</span>
|
<span class="msg-time">{{ m.time }}</span>
|
||||||
<span class="msg-dir" :class="m.direction">{{
|
<span
|
||||||
m.direction === 'pub' ? 'PUB' : 'SUB'
|
class="msg-dir"
|
||||||
}}</span>
|
:class="m.direction"
|
||||||
<span class="msg-topic" :title="m.topic">{{ m.topic }}</span>
|
>{{ m.direction === 'pub' ? 'PUB' : 'SUB' }}</span>
|
||||||
<span class="msg-meta">QoS{{ m.qos }}{{ m.retain ? ' R' : '' }}</span>
|
<span
|
||||||
|
class="msg-topic"
|
||||||
|
:title="m.topic"
|
||||||
|
@click="copyTopic(m.topic)"
|
||||||
|
>{{ m.topic }}</span>
|
||||||
|
<span
|
||||||
|
class="msg-meta"
|
||||||
|
>QoS{{ m.qos }}{{ m.retain ? ' R' : '' }}</span>
|
||||||
</div>
|
</div>
|
||||||
<pre class="msg-payload">{{ formatPayload(m.payload) }}</pre>
|
<pre
|
||||||
|
class="msg-payload"
|
||||||
|
>{{ formatPayload(m.payload) }}</pre>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</n-card>
|
</n-card>
|
||||||
</div>
|
</div>
|
||||||
<n-empty v-else description="请先在左侧选择一个 MQTT 服务器" />
|
|
||||||
|
<!-- 空状态 -->
|
||||||
|
<n-empty
|
||||||
|
v-else
|
||||||
|
description="请先在左侧选择一个 MQTT 服务器"
|
||||||
|
/>
|
||||||
|
|
||||||
</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: #1e1e2e;
|
background: var(--frost-color-bg-light);
|
||||||
border-radius: 6px;
|
border-radius: 6px;
|
||||||
padding: 12px;
|
padding: 12px;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
@@ -113,12 +234,12 @@ function formatPayload(payload) {
|
|||||||
font-family: monospace;
|
font-family: monospace;
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
line-height: 1.7;
|
line-height: 1.7;
|
||||||
color: #cdd6f4;
|
color: #333;
|
||||||
}
|
}
|
||||||
|
|
||||||
.msg-entry {
|
.msg-entry {
|
||||||
padding: 8px 0;
|
padding: 8px 0;
|
||||||
border-bottom: 1px solid rgba(255, 255, 255, 0.08);
|
border-bottom: 1px solid var(--frost-shadow-black-6);
|
||||||
}
|
}
|
||||||
|
|
||||||
.msg-header {
|
.msg-header {
|
||||||
@@ -129,40 +250,51 @@ function formatPayload(payload) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.msg-time {
|
.msg-time {
|
||||||
color: #6c7086;
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
|
&.sub {
|
||||||
|
color: var(--frost-success-color);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
.msg-dir.pub {
|
|
||||||
color: #89b4fa;
|
|
||||||
}
|
|
||||||
.msg-dir.sub {
|
|
||||||
color: #a6e3a1;
|
|
||||||
}
|
|
||||||
.msg-topic {
|
.msg-topic {
|
||||||
color: #f9e2af;
|
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;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
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(255, 255, 255, 0.05);
|
background: var(--frost-shadow-black-4);
|
||||||
color: #cdd6f4;
|
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: #6c7086;
|
color: var(--frost-color-text-tertiary);
|
||||||
font-size: 11px;
|
font-size: 11px;
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,185 +1,305 @@
|
|||||||
<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, NSwitch, NButton, NEmpty, NTabs, NTabPane, useMessage } from 'naive-ui';
|
||||||
NCard,
|
|
||||||
NSpace,
|
|
||||||
NFormItem,
|
|
||||||
NInput,
|
|
||||||
NSelect,
|
|
||||||
NSwitch,
|
|
||||||
NButton,
|
|
||||||
NEmpty,
|
|
||||||
NTabs,
|
|
||||||
NTabPane,
|
|
||||||
useMessage
|
|
||||||
} from 'naive-ui'
|
|
||||||
|
|
||||||
const store = useMqttStore()
|
// 全局状态与消息 API
|
||||||
const msg = useMessage()
|
const store = useMqttStore();
|
||||||
|
const msg = useMessage();
|
||||||
|
|
||||||
const topic = ref('')
|
// 发布表单状态
|
||||||
const payload = ref('')
|
const topic = ref('');
|
||||||
const qos = ref(1)
|
const payload = ref('');
|
||||||
const retain = ref(false)
|
const qos = ref(1);
|
||||||
const activePublishTab = ref('publish')
|
const retain = ref(false);
|
||||||
const formStorageKey = 'mqtt-client.publish-form'
|
|
||||||
const publishTabStorageKey = 'mqtt-client.publish-active-tab'
|
|
||||||
|
|
||||||
let formReady = false
|
// 当前激活的标签页
|
||||||
|
const activePublishTab = ref('publish');
|
||||||
|
|
||||||
|
// localStorage 持久化 key
|
||||||
|
const formStorageKey = 'mqtt-client.publish-form';
|
||||||
|
const publishTabStorageKey = 'mqtt-client.publish-active-tab';
|
||||||
|
|
||||||
|
let formReady = false;
|
||||||
|
|
||||||
|
// 从 localStorage 恢复发布表单
|
||||||
try {
|
try {
|
||||||
const savedForm = JSON.parse(localStorage.getItem(formStorageKey) || '{}')
|
|
||||||
if (typeof savedForm.topic === 'string') topic.value = savedForm.topic
|
let savedForm = JSON.parse(localStorage.getItem(formStorageKey) || '{}');
|
||||||
if (typeof savedForm.payload === 'string') payload.value = savedForm.payload
|
|
||||||
if ([0, 1, 2].includes(savedForm.qos)) qos.value = savedForm.qos
|
if (typeof savedForm.topic === 'string') {
|
||||||
if (typeof savedForm.retain === 'boolean') retain.value = savedForm.retain
|
topic.value = savedForm.topic;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof savedForm.payload === 'string') {
|
||||||
|
payload.value = savedForm.payload;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ([0, 1, 2].includes(savedForm.qos)) {
|
||||||
|
qos.value = savedForm.qos;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof savedForm.retain === 'boolean') {
|
||||||
|
retain.value = savedForm.retain;
|
||||||
|
}
|
||||||
|
|
||||||
} catch {
|
} catch {
|
||||||
localStorage.removeItem(formStorageKey)
|
localStorage.removeItem(formStorageKey);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 从 localStorage 恢复当前标签页
|
||||||
try {
|
try {
|
||||||
const savedTab = localStorage.getItem(publishTabStorageKey)
|
|
||||||
if (savedTab === 'publish' || savedTab === 'history') activePublishTab.value = savedTab
|
let savedTab = localStorage.getItem(publishTabStorageKey);
|
||||||
|
|
||||||
|
if (savedTab === 'publish' || savedTab === 'history') {
|
||||||
|
activePublishTab.value = savedTab;
|
||||||
|
}
|
||||||
|
|
||||||
} catch {
|
} catch {
|
||||||
localStorage.removeItem(publishTabStorageKey)
|
localStorage.removeItem(publishTabStorageKey);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 表单变化时持久化
|
||||||
watch([topic, payload, qos, retain], () => {
|
watch([topic, payload, qos, retain], () => {
|
||||||
if (!formReady) return
|
|
||||||
|
if (!formReady) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
localStorage.setItem(
|
localStorage.setItem(
|
||||||
formStorageKey,
|
formStorageKey,
|
||||||
JSON.stringify({
|
JSON.stringify({
|
||||||
topic: topic.value,
|
topic: topic.value,
|
||||||
payload: payload.value,
|
payload: payload.value,
|
||||||
qos: qos.value,
|
qos: qos.value,
|
||||||
retain: retain.value
|
retain: retain.value,
|
||||||
})
|
})
|
||||||
)
|
);
|
||||||
})
|
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
// 标签页变化时持久化
|
||||||
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() {
|
||||||
const srv = store.activeServer.value
|
|
||||||
if (!srv) return
|
let srv = store.activeServer.value;
|
||||||
|
|
||||||
|
if (!srv) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (srv.status !== 'connected') {
|
if (srv.status !== 'connected') {
|
||||||
msg.warning('请先连接服务器')
|
msg.warning('请先连接服务器');
|
||||||
return
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!topic.value.trim()) {
|
if (!topic.value.trim()) {
|
||||||
msg.warning('请输入目标主题')
|
msg.warning('请输入目标主题');
|
||||||
return
|
return;
|
||||||
}
|
}
|
||||||
const result = await store.publishMessage(srv.id, topic.value.trim(), payload.value, {
|
|
||||||
|
let result = await store.publishMessage(srv.id, topic.value.trim(), payload.value, {
|
||||||
qos: qos.value,
|
qos: qos.value,
|
||||||
retain: retain.value
|
retain: retain.value,
|
||||||
})
|
});
|
||||||
|
|
||||||
if (result) {
|
if (result) {
|
||||||
msg.success('消息已发布到: ' + topic.value)
|
msg.success('消息已发布到: ' + topic.value);
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 格式化 Payload 为 JSON
|
||||||
|
*/
|
||||||
function handleFormatJson() {
|
function handleFormatJson() {
|
||||||
try {
|
try {
|
||||||
const obj = JSON.parse(payload.value)
|
|
||||||
payload.value = JSON.stringify(obj, null, 2)
|
let obj = JSON.parse(payload.value);
|
||||||
msg.info('JSON 已格式化')
|
|
||||||
|
payload.value = JSON.stringify(obj, null, 2);
|
||||||
|
msg.info('JSON 已格式化');
|
||||||
|
|
||||||
} catch {
|
} catch {
|
||||||
msg.warning('无效的 JSON 格式')
|
msg.warning('无效的 JSON 格式');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 清空发布历史
|
||||||
|
*/
|
||||||
async function handleClearHistory() {
|
async function handleClearHistory() {
|
||||||
await store.clearPublishHistory()
|
await store.clearPublishHistory();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 复制主题到剪贴板
|
||||||
|
* @param {string} topic - 主题名称
|
||||||
|
*/
|
||||||
|
async function copyTopic(topic) {
|
||||||
|
try {
|
||||||
|
await navigator.clipboard.writeText(topic);
|
||||||
|
msg.success('主题已复制');
|
||||||
|
} catch {
|
||||||
|
msg.error('复制失败');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div v-if="store.activeServer.value" class="publish-view">
|
|
||||||
<n-tabs v-model:value="activePublishTab" type="line" size="small">
|
<!-- 内容区域 -->
|
||||||
<n-tab-pane name="publish" tab="发布消息">
|
<div
|
||||||
|
v-if="store.activeServer.value"
|
||||||
|
class="publish-view"
|
||||||
|
>
|
||||||
|
<n-tabs
|
||||||
|
v-model:value="activePublishTab"
|
||||||
|
type="line"
|
||||||
|
size="small"
|
||||||
|
>
|
||||||
|
<n-tab-pane
|
||||||
|
name="publish"
|
||||||
|
tab="发布消息"
|
||||||
|
>
|
||||||
<n-card
|
<n-card
|
||||||
size="small"
|
size="small"
|
||||||
:content-style="{
|
:content-style="{
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
flexDirection: 'column',
|
flexDirection: 'column',
|
||||||
minHeight: 0,
|
minHeight: 0,
|
||||||
overflow: 'hidden'
|
overflow: 'hidden',
|
||||||
}"
|
}"
|
||||||
>
|
>
|
||||||
<div class="publish-form">
|
<div class="publish-form">
|
||||||
<n-form-item class="topic-field" label="目标主题 (Topic)">
|
<n-form-item
|
||||||
<n-input v-model:value="topic" placeholder="例如: sensor/device01/temperature" />
|
class="topic-field"
|
||||||
|
label="目标主题 (Topic)"
|
||||||
|
>
|
||||||
|
<n-input
|
||||||
|
v-model:value="topic"
|
||||||
|
placeholder="例如: sensor/device01/temperature"
|
||||||
|
/>
|
||||||
</n-form-item>
|
</n-form-item>
|
||||||
<n-space align="center">
|
<n-space align="center">
|
||||||
<n-form-item label="QoS">
|
<n-form-item label="QoS">
|
||||||
<n-select v-model:value="qos" :options="qosOptions" style="width: 160px" />
|
<n-select
|
||||||
|
v-model:value="qos"
|
||||||
|
:options="qosOptions"
|
||||||
|
style="width: 160px"
|
||||||
|
/>
|
||||||
</n-form-item>
|
</n-form-item>
|
||||||
<n-form-item label="保留消息">
|
<n-form-item label="保留消息">
|
||||||
<n-switch v-model:value="retain" />
|
<n-switch v-model:value="retain" />
|
||||||
</n-form-item>
|
</n-form-item>
|
||||||
</n-space>
|
</n-space>
|
||||||
<n-form-item class="payload-field" label="消息内容 (Payload)">
|
<n-form-item
|
||||||
|
class="payload-field"
|
||||||
|
label="消息内容 (Payload)"
|
||||||
|
>
|
||||||
<n-input
|
<n-input
|
||||||
v-model:value="payload"
|
v-model:value="payload"
|
||||||
type="textarea"
|
type="textarea"
|
||||||
:resizable="false"
|
:resizable="false"
|
||||||
:rows="1"
|
:rows="1"
|
||||||
placeholder='{"temperature": 25.6, "humidity": 68.3}'
|
|
||||||
/>
|
/>
|
||||||
</n-form-item>
|
</n-form-item>
|
||||||
<n-space>
|
<n-space>
|
||||||
<n-button type="primary" @click="handlePublish">发布消息</n-button>
|
<n-button
|
||||||
<n-button @click="handleFormatJson">格式化 JSON</n-button>
|
type="primary"
|
||||||
|
@click="handlePublish"
|
||||||
|
>发布消息</n-button>
|
||||||
|
<n-button
|
||||||
|
@click="handleFormatJson"
|
||||||
|
>格式化 JSON</n-button>
|
||||||
</n-space>
|
</n-space>
|
||||||
</div>
|
</div>
|
||||||
</n-card>
|
</n-card>
|
||||||
</n-tab-pane>
|
</n-tab-pane>
|
||||||
|
|
||||||
<n-tab-pane name="history" tab="发布历史">
|
<n-tab-pane
|
||||||
|
name="history"
|
||||||
|
tab="发布历史"
|
||||||
|
>
|
||||||
<n-card
|
<n-card
|
||||||
size="small"
|
size="small"
|
||||||
:content-style="{
|
:content-style="{
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
flexDirection: 'column',
|
flexDirection: 'column',
|
||||||
minHeight: 0,
|
minHeight: 0,
|
||||||
overflow: 'hidden'
|
overflow: 'hidden',
|
||||||
}"
|
}"
|
||||||
>
|
>
|
||||||
<n-space style="margin-bottom: 12px">
|
<n-space style="margin-bottom: 12px">
|
||||||
<n-button size="small" @click="handleClearHistory">清空</n-button>
|
<n-button
|
||||||
|
size="small"
|
||||||
|
@click="handleClearHistory"
|
||||||
|
>清空历史</n-button>
|
||||||
</n-space>
|
</n-space>
|
||||||
<n-empty v-if="store.publishHistory.value.length === 0" description="暂无发布记录" />
|
<n-empty
|
||||||
<div v-else class="msg-log">
|
v-if="store.publishHistory.value.length === 0"
|
||||||
<div v-for="m in store.publishHistory.value" :key="m.id || m.time" class="msg-entry">
|
description="暂无发布记录"
|
||||||
|
/>
|
||||||
|
<div
|
||||||
|
v-else
|
||||||
|
class="msg-log"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
v-for="m in store.publishHistory.value"
|
||||||
|
:key="m.id || m.time"
|
||||||
|
class="msg-entry"
|
||||||
|
>
|
||||||
<div class="msg-header">
|
<div class="msg-header">
|
||||||
<span class="msg-time">{{ m.time }}</span>
|
<span
|
||||||
<span class="msg-dir pub">PUB</span>
|
class="msg-time"
|
||||||
<span class="msg-topic" :title="m.topic">{{ m.topic }}</span>
|
>{{ m.time }}</span>
|
||||||
<span class="msg-meta">QoS{{ m.qos }}{{ m.retain ? ' R' : '' }}</span>
|
<span
|
||||||
|
class="msg-dir pub"
|
||||||
|
>PUB</span>
|
||||||
|
<span
|
||||||
|
class="msg-topic"
|
||||||
|
:title="m.topic"
|
||||||
|
@click="copyTopic(m.topic)"
|
||||||
|
>{{ m.topic }}</span>
|
||||||
|
<span
|
||||||
|
class="msg-meta"
|
||||||
|
>QoS{{ m.qos }}{{ m.retain ? ' R' : '' }}</span>
|
||||||
</div>
|
</div>
|
||||||
<pre class="msg-payload">{{ m.payload }}</pre>
|
<pre
|
||||||
|
class="msg-payload"
|
||||||
|
>{{ m.payload }}</pre>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</n-card>
|
</n-card>
|
||||||
</n-tab-pane>
|
</n-tab-pane>
|
||||||
</n-tabs>
|
</n-tabs>
|
||||||
</div>
|
</div>
|
||||||
<n-empty v-else description="请先在左侧选择一个 MQTT 服务器" />
|
|
||||||
|
<!-- 空状态 -->
|
||||||
|
<n-empty
|
||||||
|
v-else
|
||||||
|
description="请先在左侧选择一个 MQTT 服务器"
|
||||||
|
/>
|
||||||
|
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped lang="less">
|
||||||
.publish-view {
|
.publish-view {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
@@ -187,61 +307,61 @@ async function handleClearHistory() {
|
|||||||
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 {
|
||||||
@@ -249,34 +369,34 @@ async function handleClearHistory() {
|
|||||||
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: #1e1e2e;
|
background: var(--frost-color-bg-light);
|
||||||
border-radius: 6px;
|
border-radius: 6px;
|
||||||
padding: 12px;
|
padding: 12px;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
@@ -286,12 +406,12 @@ async function handleClearHistory() {
|
|||||||
font-family: monospace;
|
font-family: monospace;
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
line-height: 1.7;
|
line-height: 1.7;
|
||||||
color: #cdd6f4;
|
color: #333;
|
||||||
}
|
}
|
||||||
|
|
||||||
.msg-entry {
|
.msg-entry {
|
||||||
padding: 8px 0;
|
padding: 8px 0;
|
||||||
border-bottom: 1px solid rgba(255, 255, 255, 0.08);
|
border-bottom: 1px solid var(--frost-shadow-black-6);
|
||||||
}
|
}
|
||||||
|
|
||||||
.msg-header {
|
.msg-header {
|
||||||
@@ -302,37 +422,47 @@ async function handleClearHistory() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.msg-time {
|
.msg-time {
|
||||||
color: #6c7086;
|
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: #89b4fa;
|
|
||||||
}
|
|
||||||
.msg-topic {
|
.msg-topic {
|
||||||
color: #f9e2af;
|
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;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
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(255, 255, 255, 0.05);
|
background: var(--frost-shadow-black-4);
|
||||||
color: #cdd6f4;
|
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: #6c7086;
|
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,168 +1,289 @@
|
|||||||
<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'
|
|
||||||
|
|
||||||
const store = useMqttStore()
|
// 全局状态与消息 API
|
||||||
const msg = useMessage()
|
const store = useMqttStore();
|
||||||
|
const msg = useMessage();
|
||||||
|
|
||||||
const topicInput = ref('')
|
// 主题输入与 QoS 选择
|
||||||
const qos = ref(2)
|
const topicInput = ref('');
|
||||||
const editingTopicId = ref(null)
|
const qos = ref(2);
|
||||||
const editingTopic = ref('')
|
const comment = ref('');
|
||||||
const editingQos = ref(2)
|
|
||||||
|
// 编辑状态
|
||||||
|
const editingTopicId = ref(null);
|
||||||
|
const editingTopic = ref('');
|
||||||
|
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 },
|
||||||
]
|
];
|
||||||
|
|
||||||
const formStorageKey = 'mqtt-client.subscribe-form'
|
// 表单本地存储 key
|
||||||
let formReady = false
|
const formStorageKey = 'mqtt-client.subscribe-form';
|
||||||
|
|
||||||
|
let formReady = false;
|
||||||
|
|
||||||
|
// 从 localStorage 恢复表单输入
|
||||||
try {
|
try {
|
||||||
const savedForm = JSON.parse(localStorage.getItem(formStorageKey) || '{}')
|
|
||||||
if (typeof savedForm.topicInput === 'string') topicInput.value = savedForm.topicInput
|
let savedForm = JSON.parse(localStorage.getItem(formStorageKey) || '{}');
|
||||||
if ([0, 1, 2].includes(savedForm.qos)) qos.value = savedForm.qos
|
|
||||||
|
if (typeof savedForm.topicInput === 'string') {
|
||||||
|
topicInput.value = savedForm.topicInput;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ([0, 1, 2].includes(savedForm.qos)) {
|
||||||
|
qos.value = savedForm.qos;
|
||||||
|
}
|
||||||
|
|
||||||
} catch {
|
} catch {
|
||||||
localStorage.removeItem(formStorageKey)
|
localStorage.removeItem(formStorageKey);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 表单变化时持久化到 localStorage
|
||||||
watch([topicInput, qos], () => {
|
watch([topicInput, qos], () => {
|
||||||
if (!formReady) return
|
|
||||||
|
if (!formReady) {
|
||||||
|
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
|
|
||||||
if (!srv) return
|
let srv = store.activeServer.value;
|
||||||
|
|
||||||
|
if (!srv) {
|
||||||
|
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
|
|
||||||
if (!srv) return
|
let srv = store.activeServer.value;
|
||||||
await store.removeTopic(srv.id, topic)
|
|
||||||
msg.info('主题已移除')
|
if (!srv) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await store.removeTopic(srv.id, topic);
|
||||||
|
|
||||||
|
msg.info('主题已移除');
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 切换单个主题订阅状态
|
||||||
|
* @param {string} topic - 主题名称
|
||||||
|
*/
|
||||||
async function handleToggleSub(topic) {
|
async function handleToggleSub(topic) {
|
||||||
const srv = store.activeServer.value
|
|
||||||
if (!srv) return
|
let srv = store.activeServer.value;
|
||||||
if (srv.status !== 'connected') {
|
|
||||||
msg.warning('请先连接服务器')
|
if (!srv) {
|
||||||
return
|
return;
|
||||||
}
|
}
|
||||||
const t = srv.topics.find((t) => t.topic === topic)
|
|
||||||
if (!t) return
|
if (srv.status !== 'connected') {
|
||||||
|
msg.warning('请先连接服务器');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let t = srv.topics.find((t) => t.topic === topic);
|
||||||
|
|
||||||
|
if (!t) {
|
||||||
|
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);
|
||||||
} else {
|
} else {
|
||||||
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
|
|
||||||
if (!srv) return
|
let srv = store.activeServer.value;
|
||||||
|
|
||||||
|
if (!srv) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (srv.status !== 'connected') {
|
if (srv.status !== 'connected') {
|
||||||
msg.warning('请先连接服务器')
|
msg.warning('请先连接服务器');
|
||||||
return
|
return;
|
||||||
}
|
}
|
||||||
await store.subscribeAllTopics(srv.id)
|
|
||||||
msg.success('已订阅全部主题')
|
await store.subscribeAllTopics(srv.id);
|
||||||
|
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
|
|
||||||
if (!srv) return
|
let srv = store.activeServer.value;
|
||||||
const topicName = editingTopic.value.trim()
|
|
||||||
if (!topicName) {
|
if (!srv) {
|
||||||
msg.warning('请输入主题名称')
|
return;
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let topicName = editingTopic.value.trim();
|
||||||
|
|
||||||
|
if (!topicName) {
|
||||||
|
msg.warning('请输入主题名称');
|
||||||
|
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
|
||||||
)
|
)
|
||||||
) {
|
) {
|
||||||
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 v-if="store.activeServer.value" class="subscribe-view">
|
|
||||||
|
<!-- 内容区域 -->
|
||||||
|
<div
|
||||||
|
v-if="store.activeServer.value"
|
||||||
|
class="subscribe-view"
|
||||||
|
>
|
||||||
|
|
||||||
<!-- 添加主题 -->
|
<!-- 添加主题 -->
|
||||||
<n-card title="添加订阅主题" size="small">
|
<n-card
|
||||||
|
title="添加订阅主题"
|
||||||
|
size="small"
|
||||||
|
>
|
||||||
<div class="topic-form">
|
<div class="topic-form">
|
||||||
<n-form-item class="topic-field" label="主题 (Topic)">
|
<n-form-item
|
||||||
|
class="topic-field"
|
||||||
|
label="主题 (Topic)"
|
||||||
|
>
|
||||||
<n-input
|
<n-input
|
||||||
v-model:value="topicInput"
|
v-model:value="topicInput"
|
||||||
placeholder="例如: sensor/+/temperature"
|
placeholder="例如: sensor/+/temperature"
|
||||||
@keydown.enter="handleAddTopic"
|
@keydown.enter="handleAddTopic"
|
||||||
/>
|
/>
|
||||||
</n-form-item>
|
</n-form-item>
|
||||||
<n-form-item class="qos-field" label="QoS">
|
<n-form-item
|
||||||
<n-select v-model:value="qos" :options="qosOptions" />
|
class="comment-field"
|
||||||
|
label="备注"
|
||||||
|
>
|
||||||
|
<n-input
|
||||||
|
v-model:value="comment"
|
||||||
|
placeholder="可选备注"
|
||||||
|
@keydown.enter="handleAddTopic"
|
||||||
|
/>
|
||||||
</n-form-item>
|
</n-form-item>
|
||||||
<n-button class="topic-submit" type="primary" @click="handleAddTopic">
|
<n-form-item
|
||||||
+ 添加订阅
|
class="qos-field"
|
||||||
</n-button>
|
label="QoS"
|
||||||
|
>
|
||||||
|
<n-select
|
||||||
|
v-model:value="qos"
|
||||||
|
:options="qosOptions"
|
||||||
|
/>
|
||||||
|
</n-form-item>
|
||||||
|
<n-button
|
||||||
|
class="topic-submit"
|
||||||
|
type="primary"
|
||||||
|
@click="handleAddTopic"
|
||||||
|
>+ 添加订阅</n-button>
|
||||||
</div>
|
</div>
|
||||||
</n-card>
|
</n-card>
|
||||||
|
|
||||||
@@ -175,78 +296,137 @@ async function handleEditSave(t) {
|
|||||||
display: 'flex',
|
display: 'flex',
|
||||||
flexDirection: 'column',
|
flexDirection: 'column',
|
||||||
minHeight: 0,
|
minHeight: 0,
|
||||||
overflow: 'hidden'
|
overflow: 'hidden',
|
||||||
}"
|
}"
|
||||||
>
|
>
|
||||||
<template #header-extra>
|
<template #header-extra>
|
||||||
<n-button size="small" @click="handleSubscribeAll">全部订阅</n-button>
|
<n-button
|
||||||
|
size="small"
|
||||||
|
@click="handleSubscribeAll"
|
||||||
|
>全部订阅</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"
|
||||||
description="暂无订阅主题,请在上方添加"
|
description="暂无订阅主题,请在上方添加"
|
||||||
/>
|
/>
|
||||||
<div v-else class="topic-list-scroll">
|
<div
|
||||||
|
v-else
|
||||||
|
class="topic-list-scroll"
|
||||||
|
>
|
||||||
<n-list>
|
<n-list>
|
||||||
<n-list-item
|
<n-list-item
|
||||||
v-for="t in store.activeServer.value.topics"
|
v-for="t in store.activeServer.value.topics"
|
||||||
:key="t.id || t.topic"
|
:key="t.id || t.topic"
|
||||||
class="topic-item"
|
class="topic-item"
|
||||||
>
|
>
|
||||||
<template v-if="editingTopicId !== (t.id || t.topic)" #prefix>
|
<template #prefix>
|
||||||
<n-tag :type="t.subscribed ? 'success' : 'default'" size="small">
|
<n-tag
|
||||||
QoS {{ t.qos }}
|
class="topic-qos-tag"
|
||||||
</n-tag>
|
:type="t.subscribed ? 'success' : 'default'"
|
||||||
|
size="small"
|
||||||
|
>QoS {{ t.qos }}</n-tag>
|
||||||
</template>
|
</template>
|
||||||
<div v-if="editingTopicId === (t.id || t.topic)" class="topic-edit-row">
|
<div class="topic-content">
|
||||||
<n-input
|
<div
|
||||||
v-model:value="editingTopic"
|
class="topic-name"
|
||||||
size="small"
|
:class="{ 'is-hidden': editingTopicId === (t.id || t.topic) }"
|
||||||
placeholder="主题名称"
|
>
|
||||||
@keydown.enter="handleEditSave(t)"
|
<span
|
||||||
/>
|
class="topic-label"
|
||||||
<n-select
|
:title="t.topic"
|
||||||
v-model:value="editingQos"
|
>{{ t.topic }}</span>
|
||||||
size="small"
|
<div class="topic-comment">
|
||||||
:options="qosOptions"
|
<span class="topic-comment-prefix">备注</span>
|
||||||
style="width: 160px; flex-shrink: 0"
|
<span
|
||||||
/>
|
class="topic-comment-text"
|
||||||
<n-space :size="4" :wrap="false">
|
:title="t.comment"
|
||||||
<n-button size="tiny" type="primary" @click="handleEditSave(t)">保存</n-button>
|
>{{ t.comment }}</span>
|
||||||
<n-button size="tiny" @click="cancelEdit">取消</n-button>
|
</div>
|
||||||
</n-space>
|
</div>
|
||||||
</div>
|
<div
|
||||||
<div v-else class="topic-name">
|
class="topic-edit-row"
|
||||||
<n-ellipsis>{{ t.topic }}</n-ellipsis>
|
:class="{ 'is-visible': editingTopicId === (t.id || t.topic) }"
|
||||||
|
>
|
||||||
|
<n-input
|
||||||
|
v-model:value="editingTopic"
|
||||||
|
class="topic-edit-topic"
|
||||||
|
size="small"
|
||||||
|
placeholder="主题名称"
|
||||||
|
@keydown.enter="handleEditSave(t)"
|
||||||
|
/>
|
||||||
|
<n-input
|
||||||
|
v-model:value="editingComment"
|
||||||
|
class="topic-edit-comment"
|
||||||
|
size="small"
|
||||||
|
placeholder="备注"
|
||||||
|
/>
|
||||||
|
<n-select
|
||||||
|
v-model:value="editingQos"
|
||||||
|
class="topic-edit-qos"
|
||||||
|
size="small"
|
||||||
|
:options="qosOptions"
|
||||||
|
/>
|
||||||
|
<n-space
|
||||||
|
class="topic-edit-actions"
|
||||||
|
:size="4"
|
||||||
|
:wrap="false"
|
||||||
|
>
|
||||||
|
<n-button
|
||||||
|
size="tiny"
|
||||||
|
type="primary"
|
||||||
|
@click="handleEditSave(t)"
|
||||||
|
>保存</n-button>
|
||||||
|
<n-button
|
||||||
|
size="tiny"
|
||||||
|
@click="cancelEdit"
|
||||||
|
>取消</n-button>
|
||||||
|
</n-space>
|
||||||
|
</div>
|
||||||
</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 size="tiny" @click="startEdit(t)">编辑</n-button>
|
|
||||||
<n-button
|
<n-button
|
||||||
size="tiny"
|
size="tiny"
|
||||||
:type="t.subscribed ? 'error' : 'success'"
|
@click="startEdit(t)"
|
||||||
|
>编辑</n-button>
|
||||||
|
<n-button
|
||||||
|
class="topic-action-sub"
|
||||||
|
size="tiny"
|
||||||
|
:type="t.subscribed ? 'success' : 'primary'"
|
||||||
@click="handleToggleSub(t.topic)"
|
@click="handleToggleSub(t.topic)"
|
||||||
>
|
>
|
||||||
{{ t.subscribed ? '取消订阅' : '订阅' }}
|
{{ t.subscribed ? '已订阅' : '未订阅' }}
|
||||||
</n-button>
|
|
||||||
<n-button size="tiny" type="error" text @click="handleRemoveTopic(t.topic)">
|
|
||||||
✕
|
|
||||||
</n-button>
|
</n-button>
|
||||||
|
<n-button
|
||||||
|
class="topic-action-remove"
|
||||||
|
size="tiny"
|
||||||
|
type="error"
|
||||||
|
text
|
||||||
|
@click.stop="handleRemoveTopic(t.topic)"
|
||||||
|
>✕</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 v-else description="请先在左侧选择一个 MQTT 服务器" />
|
|
||||||
|
<!-- 空状态 -->
|
||||||
|
<n-empty
|
||||||
|
v-else
|
||||||
|
description="请先在左侧选择一个 MQTT 服务器"
|
||||||
|
/>
|
||||||
|
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped lang="less">
|
||||||
.subscribe-view {
|
.subscribe-view {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
@@ -290,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,
|
||||||
width: 100%;
|
.comment-field {
|
||||||
|
:deep(.n-form-item-blank) {
|
||||||
|
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;
|
||||||
|
align-items: center;
|
||||||
|
height: 100%;
|
||||||
|
min-width: 0;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.topic-content {
|
||||||
|
position: relative;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
flex: 1;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
gap: 12px;
|
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) {
|
||||||
|
|||||||
@@ -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';
|
||||||
|
}
|
||||||
@@ -813,7 +813,7 @@
|
|||||||
<div class="sidebar-logo">F</div>
|
<div class="sidebar-logo">F</div>
|
||||||
<div>
|
<div>
|
||||||
<div class="sidebar-title">Frost MQTT Client</div>
|
<div class="sidebar-title">Frost MQTT Client</div>
|
||||||
<div class="sidebar-subtitle">多服务器管理客户端</div>
|
<div class="sidebar-subtitle">多功能 MQTT 客户端</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -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`,并修复实际报告的问题。
|
||||||