Compare commits
57 Commits
5f002865f1
...
9c6db50de2
Author | SHA1 | Date |
---|---|---|
|
9c6db50de2 | |
|
fc9648ea8b | |
|
12b56e80d6 | |
|
dccf5feaa8 | |
|
94d8061436 | |
|
aaea2c50a7 | |
|
d0325e5b44 | |
|
28eb1cedbb | |
|
b06b040e5c | |
|
e96037a073 | |
|
2f4790a57c | |
|
b7590d8997 | |
|
2082f5b60c | |
|
d7d7c94f53 | |
|
dd1b9adc0d | |
|
8e0d8234e5 | |
|
6b2d195faf | |
|
aa4e83937a | |
|
85864823af | |
|
6a7ec54ce8 | |
|
ff3b8bc9c7 | |
|
545d02e57b | |
|
dc861ab4ef | |
|
70cf85aba5 | |
|
af73147bc5 | |
|
e53b0ed883 | |
|
72079d0cf9 | |
|
d5a9bee947 | |
|
1d5adbf14e | |
|
42624fb58c | |
|
4900caeda4 | |
|
c4bf06f043 | |
|
a444813a1e | |
|
d534fd4ec8 | |
|
8a84b215a7 | |
|
5b9d6f878f | |
|
3b6c48f1d0 | |
|
bc92656e20 | |
|
981b0fbfed | |
|
949ef16e7f | |
|
418a31ce6d | |
|
99b84b416d | |
|
94ae6e44ef | |
|
8d52ff6690 | |
|
4f50883e8c | |
|
6a64f861ba | |
|
021627caf8 | |
|
99fada4c85 | |
|
be07db9414 | |
|
11b53e7531 | |
|
fb833e6af7 | |
|
f0f11c0646 | |
|
a2b7026f54 | |
|
a7758ac69a | |
|
51783612bc | |
|
15c1143b25 | |
|
c2b8f6ba09 |
|
@ -0,0 +1,2 @@
|
||||||
|
node_modules
|
||||||
|
dist
|
|
@ -0,0 +1,85 @@
|
||||||
|
# wireless-esp32-tools 网页用户界面
|
||||||
|
|
||||||
|
本项目是 [wireless-esp32-tools](https://github.com/kerms/wireless-esp32-tools) 项目的网页用户界面。
|
||||||
|
|
||||||
|
`wireless-esp32-tools` 项目为多种ESP芯片提供了兼容CMSIS-DAP的无线调试工具,可将ESP芯片变为一个功能强大的无线调试器。
|
||||||
|
|
||||||
|
## 关于此网页界面
|
||||||
|
|
||||||
|
此Web应用程序提供了一个用户友好的仪表板,用于管理您的 `wireless-esp32-tools` 设备并与之交互。它采用 Vue.js 3、Vite 和 Element Plus 构建,并设计为直接托管在 ESP32 上。
|
||||||
|
|
||||||
|
通过此界面,您可以构建一个自定义的仪表板来监控目标设备并与之交互。
|
||||||
|
|
||||||
|
### 主要功能
|
||||||
|
|
||||||
|
* **Wi-Fi配置**:包括终端模式(STA)和热点模式(AP),支持静态/动态IP地址分配和DNS设置。
|
||||||
|
* **动态组件仪表板**:一个完全灵活的基于网格的面板。
|
||||||
|
* **实时数据可视化**:包括一个用于监控目标设备串行通信的UART数据查看器。
|
||||||
|
* **嵌入式优先设计**:部署为单个.html文件以减少http连接数。
|
||||||
|
* **适用于小屏幕设计**:响应式布局适配移动设备和平板电脑,具有触摸友好的控件和针对小屏幕优化的UI元素。
|
||||||
|
|
||||||
|
## 界面截图
|
||||||
|
|
||||||
|
**组件面板**
|
||||||
|

|
||||||
|
|
||||||
|
**UART数据显示**
|
||||||
|

|
||||||
|
|
||||||
|
## 使用说明
|
||||||
|
|
||||||
|
### 最终用户
|
||||||
|
|
||||||
|
1. 将您的计算机或移动设备连接到运行 `wireless-esp32-tools` 的ESP32所承载的Wi-Fi网络。
|
||||||
|
2. 打开Web浏览器并导航到ESP32设备的IP地址(例如 `http://dap.local` 或 `http://192.168.1.1`(连接AP的情况下))。
|
||||||
|
|
||||||
|
### 开发人员 (针对此Web UI)
|
||||||
|
|
||||||
|
请按照以下步骤设置项目以进行本地开发或构建以进行部署。
|
||||||
|
|
||||||
|
#### 环境准备
|
||||||
|
|
||||||
|
* [Node.js](https://nodejs.org/) (v16 或更高版本)
|
||||||
|
* [npm](https://www.npmjs.com/)
|
||||||
|
|
||||||
|
#### 本地开发
|
||||||
|
|
||||||
|
1. **安装依赖:**
|
||||||
|
```sh
|
||||||
|
npm install
|
||||||
|
```
|
||||||
|
2. **运行开发服务器:**
|
||||||
|
```sh
|
||||||
|
npm run dev
|
||||||
|
```
|
||||||
|
这将在本地启动一个服务器,通常地址为 `http://localhost:5173`。
|
||||||
|
|
||||||
|
3. **为移动/远程调试运行:**
|
||||||
|
要从同一网络上的其他设备(如手机)访问开发服务器,请使用:
|
||||||
|
```sh
|
||||||
|
npm run devh
|
||||||
|
```
|
||||||
|
这将把服务器暴露给您的本地网络(例如 `http://192.168.X.X:5173`)。
|
||||||
|
|
||||||
|
#### 构建生产版本 (部署到ESP32)
|
||||||
|
|
||||||
|
1. **构建项目:**
|
||||||
|
```sh
|
||||||
|
npm run build
|
||||||
|
```
|
||||||
|
此命令将编译和打包应用程序到 `dist/` 目录,生成 `index.html` 和 `ws.sharedworker.js`。
|
||||||
|
|
||||||
|
2. **压缩输出文件:**
|
||||||
|
进入 `dist/` 目录并使用gzip压缩构建产物。
|
||||||
|
```sh
|
||||||
|
cd dist
|
||||||
|
gzip *
|
||||||
|
```
|
||||||
|
这将生成 `index.html.gz` 和 `ws.sharedworker.js.gz`。
|
||||||
|
|
||||||
|
3. **部署到ESP32:**
|
||||||
|
将压缩后的 `.gz` 文件复制到您的 `wireless-esp32-tools` ESP-IDF项目中的相应目录(例如 `project_components/html/`),然后将固件刷入您的设备。
|
||||||
|
|
||||||
|
## 许可证
|
||||||
|
|
||||||
|
根据MIT许可证分发。更多信息请参见 `LICENCE.txt` 文件。
|
93
README.md
|
@ -1,18 +1,87 @@
|
||||||
# 允斯无线透传器的内嵌网页版上位机
|
# Web UI for wireless-esp32-tools
|
||||||
|
|
||||||
此项目使用`NPM`包管理, 需要先安装`node`工具。
|
This is the web user interface for the [wireless-esp32-tools](https://github.com/kerms/wireless-esp32-tools) project.
|
||||||
|
|
||||||
# 环境准备步骤
|
The `wireless-esp32-tools` project provides CMSIS-DAP compatible wireless debugging tools for various ESP chips, turning them into powerful wireless debug probes.
|
||||||
|
|
||||||
##### 本地测试:
|
## About This Web Interface
|
||||||
|
|
||||||
1. `npm install`
|
This web application provides a user-friendly dashboard to manage and interact with your `wireless-esp32-tools` device. It is built with Vue.js 3, Vite, and Element Plus, and is designed to be hosted directly on the ESP32.
|
||||||
2. `npm run dev`,或用 `npm run devh` 则可以用其他设备访问,如手机调试移动界面。
|
|
||||||
3. 根据显示的地址,使用浏览器打开,默认地址为`localhost:5173`, 或者其他设备访问`192.168.X.X:5173`
|
|
||||||
|
|
||||||
##### 发布至esp32:
|
From this interface, you can build a customized dashboard to monitor and interact with your target device.
|
||||||
|
|
||||||
1. `npm install`
|
### Key Features
|
||||||
2. `npm run build` -> 会在`dist/`生成`index.html`和`ws.sharedworker.js`
|
|
||||||
3. 在`dist/`里执行`gzip *` -> -> 会在`dist/`生成`index.html.gz`和`ws.sharedworker.js.gz`
|
* **Wi-Fi Configuration**: Easily configure Wi-Fi settings including station mode (STA) and access point mode (AP), with support for static/dynamic IP addressing and DNS settings.
|
||||||
4. 至此,可以使用这两个文件覆盖ESP32目录中的`project_components/html`里相对应的文件了。
|
* **Dynamic Widget Dashboard**: A fully flexible grid-based panel.
|
||||||
|
* **Real-time Data Visualization**: Includes a UART data viewer for monitoring serial communication from your target device.
|
||||||
|
* **Embedded-First Design**: Deploy in single .html file to reduce the number of http connection.
|
||||||
|
* **Designed to be usable on small screen**: Responsive layout that adapts to mobile devices and tablets, with touch-friendly controls and optimized UI elements for small displays.
|
||||||
|
|
||||||
|
## Screenshots
|
||||||
|
|
||||||
|
**Widget Panel**
|
||||||
|

|
||||||
|
|
||||||
|
**UART Data Viewer**
|
||||||
|

|
||||||
|
|
||||||
|
## Getting Started
|
||||||
|
|
||||||
|
### For End-Users
|
||||||
|
|
||||||
|
1. Connect your computer or mobile device to the Wi-Fi network hosted by the ESP32 running `wireless-esp32-tools`.
|
||||||
|
2. Open a web browser and navigate to the IP address of the ESP32 device (e.g., `http://dap.local` or `http://192.168.1.1`(when connected to the AP)).
|
||||||
|
|
||||||
|
### For Developers (of this Web UI)
|
||||||
|
|
||||||
|
Follow these steps to set up the project for local development or to build it for deployment.
|
||||||
|
|
||||||
|
#### Prerequisites
|
||||||
|
|
||||||
|
* [Node.js](https://nodejs.org/) (v16 or later)
|
||||||
|
* [npm](https://www.npmjs.com/)
|
||||||
|
|
||||||
|
#### Local Development
|
||||||
|
|
||||||
|
1. **Install dependencies:**
|
||||||
|
```sh
|
||||||
|
npm install
|
||||||
|
```
|
||||||
|
2. **Run the development server:**
|
||||||
|
```sh
|
||||||
|
npm run dev
|
||||||
|
```
|
||||||
|
This will start a local server, typically at `http://localhost:5173`.
|
||||||
|
|
||||||
|
3. **Run for mobile/remote debugging:**
|
||||||
|
To access the development server from other devices on the same network (like a mobile phone), use:
|
||||||
|
```sh
|
||||||
|
npm run devh
|
||||||
|
```
|
||||||
|
This will expose the server to your local network (e.g., `http://192.168.X.X:5173`).
|
||||||
|
|
||||||
|
#### Building for Production (Deploying to ESP32)
|
||||||
|
|
||||||
|
1. **Build the project:**
|
||||||
|
```sh
|
||||||
|
npm run build
|
||||||
|
```
|
||||||
|
This command compiles and bundles the application into the `dist/` directory, creating `index.html` and `ws.sharedworker.js`.
|
||||||
|
|
||||||
|
There is some bugs on Firefox or Vue3 when embbeding sharedWorker in single file mode. So at the time, the sharedWorker are build separately.
|
||||||
|
|
||||||
|
2. **Compress the output files:**
|
||||||
|
Navigate into the `dist/` directory and compress the build artifacts using gzip.
|
||||||
|
```sh
|
||||||
|
cd dist
|
||||||
|
gzip *
|
||||||
|
```
|
||||||
|
This will generate `index.html.gz` and `ws.sharedworker.js.gz`.
|
||||||
|
|
||||||
|
3. **Deploy to ESP32:**
|
||||||
|
Copy the compressed `.gz` files to the appropriate directory in your `wireless-esp32-tools` ESP-IDF project (e.g., `project_components/html/`) and flash the firmware to your device.
|
||||||
|
|
||||||
|
## License
|
||||||
|
|
||||||
|
Distributed under the MIT License. See `LICENCE.txt` for more information.
|
||||||
|
|
After Width: | Height: | Size: 103 KiB |
After Width: | Height: | Size: 38 KiB |
|
@ -4,7 +4,7 @@
|
||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<link id="favicon" rel="icon" href="data:,">
|
<link id="favicon" rel="icon" href="data:,">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
<title>Vite App</title>
|
<title>yunsi.studio project</title>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div id="app"></div>
|
<div id="app"></div>
|
||||||
|
|
|
@ -10,11 +10,12 @@
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@vueuse/core": "^10.9.0",
|
"@vueuse/core": "^10.9.0",
|
||||||
"ansi_up": "^6.0.2",
|
"ansi_up": "^6.0.2",
|
||||||
"element-plus": "^2.7.3",
|
"element-plus": "^2.8.1",
|
||||||
"mitt": "^3.0.1",
|
"mitt": "^3.0.1",
|
||||||
"pinia": "^2.1.7",
|
"pinia": "^2.1.7",
|
||||||
"vue": "^3.4.21",
|
"vue": "^3.5.16",
|
||||||
"vue-draggable-plus": "^0.4.1",
|
"vue-draggable-plus": "^0.4.1",
|
||||||
|
"vue-grid-layout-v3": "^3.1.2",
|
||||||
"vue-i18n": "^9.10.2",
|
"vue-i18n": "^9.10.2",
|
||||||
"vue-router": "^4.3.0",
|
"vue-router": "^4.3.0",
|
||||||
"vuetify": "^3.6.5"
|
"vuetify": "^3.6.5"
|
||||||
|
@ -77,10 +78,29 @@
|
||||||
"url": "https://github.com/sponsors/antfu"
|
"url": "https://github.com/sponsors/antfu"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@babel/helper-string-parser": {
|
||||||
|
"version": "7.27.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz",
|
||||||
|
"integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=6.9.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@babel/helper-validator-identifier": {
|
||||||
|
"version": "7.27.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.27.1.tgz",
|
||||||
|
"integrity": "sha512-D2hP9eA+Sqx1kBZgzxZh0y1trbuU+JoDkiEwqhQ36nodYqJwyEIhPSdMNd7lOm/4io72luTPWH20Yda0xOuUow==",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=6.9.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@babel/parser": {
|
"node_modules/@babel/parser": {
|
||||||
"version": "7.24.1",
|
"version": "7.27.5",
|
||||||
"resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.24.1.tgz",
|
"resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.27.5.tgz",
|
||||||
"integrity": "sha512-Zo9c7N3xdOIQrNip7Lc9wvRPzlRtovHVE4lkz8WEDr7uYh/GMQhSiIgFxGIArRHYdJE5kxtZjAf8rT0xhdLCzg==",
|
"integrity": "sha512-OsQd175SxWkGlzbny8J3K8TnnDD0N3lrIUtB92xwyRpzaenGZhxDvxN/JgU00U3CDZNj9tPuDJ5H0WS4Nt3vKg==",
|
||||||
|
"dependencies": {
|
||||||
|
"@babel/types": "^7.27.3"
|
||||||
|
},
|
||||||
"bin": {
|
"bin": {
|
||||||
"parser": "bin/babel-parser.js"
|
"parser": "bin/babel-parser.js"
|
||||||
},
|
},
|
||||||
|
@ -88,6 +108,18 @@
|
||||||
"node": ">=6.0.0"
|
"node": ">=6.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@babel/types": {
|
||||||
|
"version": "7.27.6",
|
||||||
|
"resolved": "https://registry.npmjs.org/@babel/types/-/types-7.27.6.tgz",
|
||||||
|
"integrity": "sha512-ETyHEk2VHHvl9b9jZP5IHPavHYk57EhanlRRuae9XCpb/j5bDCbPPMOBfCWhnl/7EDJz0jEMCi/RhccCE8r1+Q==",
|
||||||
|
"dependencies": {
|
||||||
|
"@babel/helper-string-parser": "^7.27.1",
|
||||||
|
"@babel/helper-validator-identifier": "^7.27.1"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=6.9.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@ctrl/tinycolor": {
|
"node_modules/@ctrl/tinycolor": {
|
||||||
"version": "3.6.1",
|
"version": "3.6.1",
|
||||||
"resolved": "https://registry.npmjs.org/@ctrl/tinycolor/-/tinycolor-3.6.1.tgz",
|
"resolved": "https://registry.npmjs.org/@ctrl/tinycolor/-/tinycolor-3.6.1.tgz",
|
||||||
|
@ -274,6 +306,102 @@
|
||||||
"integrity": "sha512-6EwiSjwWYP7pTckG6I5eyFANjPhmPjUX9JRLUSfNPC7FX7zK9gyZAfUEaECL6ALTpGX5AjnBq3C9XmVWPitNpw==",
|
"integrity": "sha512-6EwiSjwWYP7pTckG6I5eyFANjPhmPjUX9JRLUSfNPC7FX7zK9gyZAfUEaECL6ALTpGX5AjnBq3C9XmVWPitNpw==",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
|
"node_modules/@interactjs/actions": {
|
||||||
|
"version": "1.10.27",
|
||||||
|
"resolved": "https://registry.npmjs.org/@interactjs/actions/-/actions-1.10.27.tgz",
|
||||||
|
"integrity": "sha512-FCRg5KwB+stkPcAMx/Cn0fgGP6p4LyMX9S/Upcn/W+hpYme31bPi54PCqmOebzz6myTthN6zFf9jMyLOqtI/gg==",
|
||||||
|
"optionalDependencies": {
|
||||||
|
"@interactjs/interact": "1.10.27"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@interactjs/core": "1.10.27",
|
||||||
|
"@interactjs/utils": "1.10.27"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@interactjs/auto-scroll": {
|
||||||
|
"version": "1.10.27",
|
||||||
|
"resolved": "https://registry.npmjs.org/@interactjs/auto-scroll/-/auto-scroll-1.10.27.tgz",
|
||||||
|
"integrity": "sha512-zPg5TnVsZv+9Hnt4qnbxLvBMf+rIWHkoJVoSETEbLNaj90C8hIyr0pVwukSUySSgDhCgQ7np0f3pg4INLq9beQ==",
|
||||||
|
"optionalDependencies": {
|
||||||
|
"@interactjs/interact": "1.10.27"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@interactjs/utils": "1.10.27"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@interactjs/auto-start": {
|
||||||
|
"version": "1.10.27",
|
||||||
|
"resolved": "https://registry.npmjs.org/@interactjs/auto-start/-/auto-start-1.10.27.tgz",
|
||||||
|
"integrity": "sha512-ECLBO/nxmaF1knncJKIE5F7la3KKRgEkn0Cu2JTPOYj9xy/LpfYElo3wkRHsodgOqF651nR70GK2/IzPR2lO9A==",
|
||||||
|
"optionalDependencies": {
|
||||||
|
"@interactjs/interact": "1.10.27"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@interactjs/core": "1.10.27",
|
||||||
|
"@interactjs/utils": "1.10.27"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@interactjs/core": {
|
||||||
|
"version": "1.10.27",
|
||||||
|
"resolved": "https://registry.npmjs.org/@interactjs/core/-/core-1.10.27.tgz",
|
||||||
|
"integrity": "sha512-SliUr/3ZbLAdED8LokzYzWHWMdCB5Cq+UnpXuRy+BIod1j97m4IUFf/D1iIKUBBjBcucgXbz28z96WnenVCB7Q==",
|
||||||
|
"peerDependencies": {
|
||||||
|
"@interactjs/utils": "1.10.27"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@interactjs/dev-tools": {
|
||||||
|
"version": "1.10.27",
|
||||||
|
"resolved": "https://registry.npmjs.org/@interactjs/dev-tools/-/dev-tools-1.10.27.tgz",
|
||||||
|
"integrity": "sha512-YolmBwRaKH1gWbvyLeV3m5QSwtD38lOZnCBA87PCAlcd9PQAC2gb03fEPeEyD336bE20oLB8f0WZt4Wre+afiw==",
|
||||||
|
"optionalDependencies": {
|
||||||
|
"@interactjs/interact": "1.10.27",
|
||||||
|
"vue": "3"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@interactjs/modifiers": "1.10.27",
|
||||||
|
"@interactjs/utils": "1.10.27"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@interactjs/interact": {
|
||||||
|
"version": "1.10.27",
|
||||||
|
"resolved": "https://registry.npmjs.org/@interactjs/interact/-/interact-1.10.27.tgz",
|
||||||
|
"integrity": "sha512-XdH3A2UUzjEFGGJgFuJlhiz99tE8jB8xNh/DmnoMuL6uOQPxNA+sWRnzEVjG0+zY2P3/dbhEpi4Cn3FLPzydwA==",
|
||||||
|
"dependencies": {
|
||||||
|
"@interactjs/core": "1.10.27",
|
||||||
|
"@interactjs/utils": "1.10.27"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@interactjs/modifiers": {
|
||||||
|
"version": "1.10.27",
|
||||||
|
"resolved": "https://registry.npmjs.org/@interactjs/modifiers/-/modifiers-1.10.27.tgz",
|
||||||
|
"integrity": "sha512-ei/qfoQ+9/8k6WzNzdNqHI6cWkIV576N4Ap16r5CoqOWwhA6Xzj3OMHf1g0t1O4eSq2HdJsVJn3eLNfw9HsbeQ==",
|
||||||
|
"dependencies": {
|
||||||
|
"@interactjs/snappers": "1.10.27"
|
||||||
|
},
|
||||||
|
"optionalDependencies": {
|
||||||
|
"@interactjs/interact": "1.10.27"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@interactjs/core": "1.10.27",
|
||||||
|
"@interactjs/utils": "1.10.27"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@interactjs/snappers": {
|
||||||
|
"version": "1.10.27",
|
||||||
|
"resolved": "https://registry.npmjs.org/@interactjs/snappers/-/snappers-1.10.27.tgz",
|
||||||
|
"integrity": "sha512-HZLZ0XSi6HI08OmTv/HKG6AltQoaKAALLQ+KDW92utj3XSgw7oren0KsWUKPhaPg3Av7R1jFQd08s+uafqIlLw==",
|
||||||
|
"optionalDependencies": {
|
||||||
|
"@interactjs/interact": "1.10.27"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@interactjs/utils": "1.10.27"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@interactjs/utils": {
|
||||||
|
"version": "1.10.27",
|
||||||
|
"resolved": "https://registry.npmjs.org/@interactjs/utils/-/utils-1.10.27.tgz",
|
||||||
|
"integrity": "sha512-+qfLOio2OxQqg1cXSnRaCl+N8MQDQLDS9w+aOGxH8YLAhIMyt7Asxx/46//sT8orgsi16pmlBPtngPHT9s8zKw=="
|
||||||
|
},
|
||||||
"node_modules/@intlify/core-base": {
|
"node_modules/@intlify/core-base": {
|
||||||
"version": "9.10.2",
|
"version": "9.10.2",
|
||||||
"resolved": "https://registry.npmjs.org/@intlify/core-base/-/core-base-9.10.2.tgz",
|
"resolved": "https://registry.npmjs.org/@intlify/core-base/-/core-base-9.10.2.tgz",
|
||||||
|
@ -402,9 +530,9 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@jridgewell/sourcemap-codec": {
|
"node_modules/@jridgewell/sourcemap-codec": {
|
||||||
"version": "1.4.15",
|
"version": "1.5.0",
|
||||||
"resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.15.tgz",
|
"resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.0.tgz",
|
||||||
"integrity": "sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg=="
|
"integrity": "sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ=="
|
||||||
},
|
},
|
||||||
"node_modules/@jridgewell/trace-mapping": {
|
"node_modules/@jridgewell/trace-mapping": {
|
||||||
"version": "0.3.25",
|
"version": "0.3.25",
|
||||||
|
@ -979,77 +1107,85 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@volar/language-core": {
|
"node_modules/@volar/language-core": {
|
||||||
"version": "2.1.4",
|
"version": "2.4.0",
|
||||||
"resolved": "https://registry.npmjs.org/@volar/language-core/-/language-core-2.1.4.tgz",
|
"resolved": "https://registry.npmjs.org/@volar/language-core/-/language-core-2.4.0.tgz",
|
||||||
"integrity": "sha512-ROfPepDxZ5Eq+Unbx3M9QcHT7MoE9tYdbkuzLTtxG5rfkEi5RwsDPncjANMOq/gHhIIDlWgqWwS2nXWMGsuj4w==",
|
"integrity": "sha512-FTla+khE+sYK0qJP+6hwPAAUwiNHVMph4RUXpxf/FIPKUP61NFrVZorml4mjFShnueR2y9/j8/vnh09YwVdH7A==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@volar/source-map": "2.1.4"
|
"@volar/source-map": "2.4.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@volar/source-map": {
|
"node_modules/@volar/source-map": {
|
||||||
"version": "2.1.4",
|
"version": "2.4.0",
|
||||||
"resolved": "https://registry.npmjs.org/@volar/source-map/-/source-map-2.1.4.tgz",
|
"resolved": "https://registry.npmjs.org/@volar/source-map/-/source-map-2.4.0.tgz",
|
||||||
"integrity": "sha512-mCg8IiPZmHZVzqL4Owg+BzQ5ZTG1cVwATxrkrFPZpcAin97Xa3MbchxVhHtHTWTT8ER8bJh5xVjeVxsSN++FUA==",
|
"integrity": "sha512-2ceY8/NEZvN6F44TXw2qRP6AQsvCYhV2bxaBPWxV9HqIfkbRydSksTFObCF1DBDNBfKiZTS8G/4vqV6cvjdOIQ==",
|
||||||
"dev": true,
|
"dev": true
|
||||||
"dependencies": {
|
|
||||||
"muggle-string": "^0.4.0"
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
"node_modules/@volar/typescript": {
|
"node_modules/@volar/typescript": {
|
||||||
"version": "2.1.4",
|
"version": "2.4.0",
|
||||||
"resolved": "https://registry.npmjs.org/@volar/typescript/-/typescript-2.1.4.tgz",
|
"resolved": "https://registry.npmjs.org/@volar/typescript/-/typescript-2.4.0.tgz",
|
||||||
"integrity": "sha512-Mt7wOLPkomFnUfVpb5IHlPhSpD7FJAn+FHSsovePmqFNQzFLz16wrpHjAkorPiAnP0847w71NL5fIJyWbAsR8Q==",
|
"integrity": "sha512-9zx3lQWgHmVd+JRRAHUSRiEhe4TlzL7U7e6ulWXOxHH/WNYxzKwCvZD7WYWEZFdw4dHfTD9vUR0yPQO6GilCaQ==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@volar/language-core": "2.1.4",
|
"@volar/language-core": "2.4.0",
|
||||||
"path-browserify": "^1.0.1"
|
"path-browserify": "^1.0.1",
|
||||||
|
"vscode-uri": "^3.0.8"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@vue/compiler-core": {
|
"node_modules/@vue/compiler-core": {
|
||||||
"version": "3.4.21",
|
"version": "3.5.16",
|
||||||
"resolved": "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.4.21.tgz",
|
"resolved": "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.5.16.tgz",
|
||||||
"integrity": "sha512-MjXawxZf2SbZszLPYxaFCjxfibYrzr3eYbKxwpLR9EQN+oaziSu3qKVbwBERj1IFIB8OLUewxB5m/BFzi613og==",
|
"integrity": "sha512-AOQS2eaQOaaZQoL1u+2rCJIKDruNXVBZSiUD3chnUrsoX5ZTQMaCvXlWNIfxBJuU15r1o7+mpo5223KVtIhAgQ==",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@babel/parser": "^7.23.9",
|
"@babel/parser": "^7.27.2",
|
||||||
"@vue/shared": "3.4.21",
|
"@vue/shared": "3.5.16",
|
||||||
"entities": "^4.5.0",
|
"entities": "^4.5.0",
|
||||||
"estree-walker": "^2.0.2",
|
"estree-walker": "^2.0.2",
|
||||||
"source-map-js": "^1.0.2"
|
"source-map-js": "^1.2.1"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@vue/compiler-dom": {
|
"node_modules/@vue/compiler-dom": {
|
||||||
"version": "3.4.21",
|
"version": "3.5.16",
|
||||||
"resolved": "https://registry.npmjs.org/@vue/compiler-dom/-/compiler-dom-3.4.21.tgz",
|
"resolved": "https://registry.npmjs.org/@vue/compiler-dom/-/compiler-dom-3.5.16.tgz",
|
||||||
"integrity": "sha512-IZC6FKowtT1sl0CR5DpXSiEB5ayw75oT2bma1BEhV7RRR1+cfwLrxc2Z8Zq/RGFzJ8w5r9QtCOvTjQgdn0IKmA==",
|
"integrity": "sha512-SSJIhBr/teipXiXjmWOVWLnxjNGo65Oj/8wTEQz0nqwQeP75jWZ0n4sF24Zxoht1cuJoWopwj0J0exYwCJ0dCQ==",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@vue/compiler-core": "3.4.21",
|
"@vue/compiler-core": "3.5.16",
|
||||||
"@vue/shared": "3.4.21"
|
"@vue/shared": "3.5.16"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@vue/compiler-sfc": {
|
"node_modules/@vue/compiler-sfc": {
|
||||||
"version": "3.4.21",
|
"version": "3.5.16",
|
||||||
"resolved": "https://registry.npmjs.org/@vue/compiler-sfc/-/compiler-sfc-3.4.21.tgz",
|
"resolved": "https://registry.npmjs.org/@vue/compiler-sfc/-/compiler-sfc-3.5.16.tgz",
|
||||||
"integrity": "sha512-me7epoTxYlY+2CUM7hy9PCDdpMPfIwrOvAXud2Upk10g4YLv9UBW7kL798TvMeDhPthkZ0CONNrK2GoeI1ODiQ==",
|
"integrity": "sha512-rQR6VSFNpiinDy/DVUE0vHoIDUF++6p910cgcZoaAUm3POxgNOOdS/xgoll3rNdKYTYPnnbARDCZOyZ+QSe6Pw==",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@babel/parser": "^7.23.9",
|
"@babel/parser": "^7.27.2",
|
||||||
"@vue/compiler-core": "3.4.21",
|
"@vue/compiler-core": "3.5.16",
|
||||||
"@vue/compiler-dom": "3.4.21",
|
"@vue/compiler-dom": "3.5.16",
|
||||||
"@vue/compiler-ssr": "3.4.21",
|
"@vue/compiler-ssr": "3.5.16",
|
||||||
"@vue/shared": "3.4.21",
|
"@vue/shared": "3.5.16",
|
||||||
"estree-walker": "^2.0.2",
|
"estree-walker": "^2.0.2",
|
||||||
"magic-string": "^0.30.7",
|
"magic-string": "^0.30.17",
|
||||||
"postcss": "^8.4.35",
|
"postcss": "^8.5.3",
|
||||||
"source-map-js": "^1.0.2"
|
"source-map-js": "^1.2.1"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@vue/compiler-ssr": {
|
"node_modules/@vue/compiler-ssr": {
|
||||||
"version": "3.4.21",
|
"version": "3.5.16",
|
||||||
"resolved": "https://registry.npmjs.org/@vue/compiler-ssr/-/compiler-ssr-3.4.21.tgz",
|
"resolved": "https://registry.npmjs.org/@vue/compiler-ssr/-/compiler-ssr-3.5.16.tgz",
|
||||||
"integrity": "sha512-M5+9nI2lPpAsgXOGQobnIueVqc9sisBFexh5yMIMRAPYLa7+5wEJs8iqOZc1WAa9WQbx9GR2twgznU8LTIiZ4Q==",
|
"integrity": "sha512-d2V7kfxbdsjrDSGlJE7my1ZzCXViEcqN6w14DOsDrUCHEA6vbnVCpRFfrc4ryCP/lCKzX2eS1YtnLE/BuC9f/A==",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@vue/compiler-dom": "3.4.21",
|
"@vue/compiler-dom": "3.5.16",
|
||||||
"@vue/shared": "3.4.21"
|
"@vue/shared": "3.5.16"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@vue/compiler-vue2": {
|
||||||
|
"version": "2.7.16",
|
||||||
|
"resolved": "https://registry.npmjs.org/@vue/compiler-vue2/-/compiler-vue2-2.7.16.tgz",
|
||||||
|
"integrity": "sha512-qYC3Psj9S/mfu9uVi5WvNZIzq+xnXMhOwbTFKKDD7b1lhpnn71jXSFdTQ+WsIEk0ONCd7VV2IMm7ONl6tbQ86A==",
|
||||||
|
"dev": true,
|
||||||
|
"dependencies": {
|
||||||
|
"de-indent": "^1.0.2",
|
||||||
|
"he": "^1.2.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@vue/devtools-api": {
|
"node_modules/@vue/devtools-api": {
|
||||||
|
@ -1096,18 +1232,19 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@vue/language-core": {
|
"node_modules/@vue/language-core": {
|
||||||
"version": "2.0.7",
|
"version": "2.0.29",
|
||||||
"resolved": "https://registry.npmjs.org/@vue/language-core/-/language-core-2.0.7.tgz",
|
"resolved": "https://registry.npmjs.org/@vue/language-core/-/language-core-2.0.29.tgz",
|
||||||
"integrity": "sha512-Vh1yZX3XmYjn9yYLkjU8DN6L0ceBtEcapqiyclHne8guG84IaTzqtvizZB1Yfxm3h6m7EIvjerLO5fvOZO6IIQ==",
|
"integrity": "sha512-o2qz9JPjhdoVj8D2+9bDXbaI4q2uZTHQA/dbyZT4Bj1FR9viZxDJnLcKVHfxdn6wsOzRgpqIzJEEmSSvgMvDTQ==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@volar/language-core": "~2.1.3",
|
"@volar/language-core": "~2.4.0-alpha.18",
|
||||||
"@vue/compiler-dom": "^3.4.0",
|
"@vue/compiler-dom": "^3.4.0",
|
||||||
|
"@vue/compiler-vue2": "^2.7.16",
|
||||||
"@vue/shared": "^3.4.0",
|
"@vue/shared": "^3.4.0",
|
||||||
"computeds": "^0.0.1",
|
"computeds": "^0.0.1",
|
||||||
"minimatch": "^9.0.3",
|
"minimatch": "^9.0.3",
|
||||||
"path-browserify": "^1.0.1",
|
"muggle-string": "^0.4.1",
|
||||||
"vue-template-compiler": "^2.7.14"
|
"path-browserify": "^1.0.1"
|
||||||
},
|
},
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"typescript": "*"
|
"typescript": "*"
|
||||||
|
@ -1119,48 +1256,49 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@vue/reactivity": {
|
"node_modules/@vue/reactivity": {
|
||||||
"version": "3.4.21",
|
"version": "3.5.16",
|
||||||
"resolved": "https://registry.npmjs.org/@vue/reactivity/-/reactivity-3.4.21.tgz",
|
"resolved": "https://registry.npmjs.org/@vue/reactivity/-/reactivity-3.5.16.tgz",
|
||||||
"integrity": "sha512-UhenImdc0L0/4ahGCyEzc/pZNwVgcglGy9HVzJ1Bq2Mm9qXOpP8RyNTjookw/gOCUlXSEtuZ2fUg5nrHcoqJcw==",
|
"integrity": "sha512-FG5Q5ee/kxhIm1p2bykPpPwqiUBV3kFySsHEQha5BJvjXdZTUfmya7wP7zC39dFuZAcf/PD5S4Lni55vGLMhvA==",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@vue/shared": "3.4.21"
|
"@vue/shared": "3.5.16"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@vue/runtime-core": {
|
"node_modules/@vue/runtime-core": {
|
||||||
"version": "3.4.21",
|
"version": "3.5.16",
|
||||||
"resolved": "https://registry.npmjs.org/@vue/runtime-core/-/runtime-core-3.4.21.tgz",
|
"resolved": "https://registry.npmjs.org/@vue/runtime-core/-/runtime-core-3.5.16.tgz",
|
||||||
"integrity": "sha512-pQthsuYzE1XcGZznTKn73G0s14eCJcjaLvp3/DKeYWoFacD9glJoqlNBxt3W2c5S40t6CCcpPf+jG01N3ULyrA==",
|
"integrity": "sha512-bw5Ykq6+JFHYxrQa7Tjr+VSzw7Dj4ldR/udyBZbq73fCdJmyy5MPIFR9IX/M5Qs+TtTjuyUTCnmK3lWWwpAcFQ==",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@vue/reactivity": "3.4.21",
|
"@vue/reactivity": "3.5.16",
|
||||||
"@vue/shared": "3.4.21"
|
"@vue/shared": "3.5.16"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@vue/runtime-dom": {
|
"node_modules/@vue/runtime-dom": {
|
||||||
"version": "3.4.21",
|
"version": "3.5.16",
|
||||||
"resolved": "https://registry.npmjs.org/@vue/runtime-dom/-/runtime-dom-3.4.21.tgz",
|
"resolved": "https://registry.npmjs.org/@vue/runtime-dom/-/runtime-dom-3.5.16.tgz",
|
||||||
"integrity": "sha512-gvf+C9cFpevsQxbkRBS1NpU8CqxKw0ebqMvLwcGQrNpx6gqRDodqKqA+A2VZZpQ9RpK2f9yfg8VbW/EpdFUOJw==",
|
"integrity": "sha512-T1qqYJsG2xMGhImRUV9y/RseB9d0eCYZQ4CWca9ztCuiPj/XWNNN+lkNBuzVbia5z4/cgxdL28NoQCvC0Xcfww==",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@vue/runtime-core": "3.4.21",
|
"@vue/reactivity": "3.5.16",
|
||||||
"@vue/shared": "3.4.21",
|
"@vue/runtime-core": "3.5.16",
|
||||||
|
"@vue/shared": "3.5.16",
|
||||||
"csstype": "^3.1.3"
|
"csstype": "^3.1.3"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@vue/server-renderer": {
|
"node_modules/@vue/server-renderer": {
|
||||||
"version": "3.4.21",
|
"version": "3.5.16",
|
||||||
"resolved": "https://registry.npmjs.org/@vue/server-renderer/-/server-renderer-3.4.21.tgz",
|
"resolved": "https://registry.npmjs.org/@vue/server-renderer/-/server-renderer-3.5.16.tgz",
|
||||||
"integrity": "sha512-aV1gXyKSN6Rz+6kZ6kr5+Ll14YzmIbeuWe7ryJl5muJ4uwSwY/aStXTixx76TwkZFJLm1aAlA/HSWEJ4EyiMkg==",
|
"integrity": "sha512-BrX0qLiv/WugguGsnQUJiYOE0Fe5mZTwi6b7X/ybGB0vfrPH9z0gD/Y6WOR1sGCgX4gc25L1RYS5eYQKDMoNIg==",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@vue/compiler-ssr": "3.4.21",
|
"@vue/compiler-ssr": "3.5.16",
|
||||||
"@vue/shared": "3.4.21"
|
"@vue/shared": "3.5.16"
|
||||||
},
|
},
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"vue": "3.4.21"
|
"vue": "3.5.16"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@vue/shared": {
|
"node_modules/@vue/shared": {
|
||||||
"version": "3.4.21",
|
"version": "3.5.16",
|
||||||
"resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.4.21.tgz",
|
"resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.5.16.tgz",
|
||||||
"integrity": "sha512-PuJe7vDIi6VYSinuEbUIQgMIRZGgM8e4R+G+/dQTk0X1NEdvgvvgv7m+rfmDH1gZzyA1OjjoWskvHlfRNfQf3g=="
|
"integrity": "sha512-c/0fWy3Jw6Z8L9FmTyYfkpM5zklnqqa9+a6dz3DvONRKW2NEbh46BP0FHuLFSWi2TnQEtp91Z6zOWNrU6QiyPg=="
|
||||||
},
|
},
|
||||||
"node_modules/@vue/tsconfig": {
|
"node_modules/@vue/tsconfig": {
|
||||||
"version": "0.5.1",
|
"version": "0.5.1",
|
||||||
|
@ -1427,6 +1565,11 @@
|
||||||
"integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==",
|
"integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
|
"node_modules/batch-processor": {
|
||||||
|
"version": "1.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/batch-processor/-/batch-processor-1.0.0.tgz",
|
||||||
|
"integrity": "sha512-xoLQD8gmmR32MeuBHgH0Tzd5PuSZx71ZsbhVxOCRbgktZEPe4SQy7s9Z50uPp0F/f7iw2XmkHN2xkgbMfckMDA=="
|
||||||
|
},
|
||||||
"node_modules/binary-extensions": {
|
"node_modules/binary-extensions": {
|
||||||
"version": "2.3.0",
|
"version": "2.3.0",
|
||||||
"resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz",
|
"resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz",
|
||||||
|
@ -1533,9 +1676,9 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/caniuse-lite": {
|
"node_modules/caniuse-lite": {
|
||||||
"version": "1.0.30001599",
|
"version": "1.0.30001667",
|
||||||
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001599.tgz",
|
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001667.tgz",
|
||||||
"integrity": "sha512-LRAQHZ4yT1+f9LemSMeqdMpMxZcc4RMWdj4tiFe3G8tNkWK+E58g+/tzotb5cU6TbcVJLr4fySiAW7XmxQvZQA==",
|
"integrity": "sha512-7LTwJjcRkzKFmtqGsibMeuXmvFDfZq/nzIjnmgCGzKKRVzjD72selLDK1oPF/Oxzmt4fNcPvTDvGqSDG4tCALw==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"funding": [
|
"funding": [
|
||||||
{
|
{
|
||||||
|
@ -1793,12 +1936,12 @@
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
"node_modules/debug": {
|
"node_modules/debug": {
|
||||||
"version": "4.3.4",
|
"version": "4.4.0",
|
||||||
"resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz",
|
"resolved": "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz",
|
||||||
"integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==",
|
"integrity": "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==",
|
||||||
"devOptional": true,
|
"devOptional": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"ms": "2.1.2"
|
"ms": "^2.1.3"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=6.0"
|
"node": ">=6.0"
|
||||||
|
@ -1977,9 +2120,9 @@
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
"node_modules/element-plus": {
|
"node_modules/element-plus": {
|
||||||
"version": "2.7.3",
|
"version": "2.8.1",
|
||||||
"resolved": "https://registry.npmjs.org/element-plus/-/element-plus-2.7.3.tgz",
|
"resolved": "https://registry.npmjs.org/element-plus/-/element-plus-2.8.1.tgz",
|
||||||
"integrity": "sha512-OaqY1kQ2xzNyRFyge3fzM7jqMwux+464RBEqd+ybRV9xPiGxtgnj/sVK4iEbnKnzQIa9XK03DOIFzoToUhu1DA==",
|
"integrity": "sha512-p11/6w/O0+hGvPhiN3jrcgh+XG+eg5jZlLdQVYvcPHZYhhCh3J3YeZWW1JO/REPES1vevkboT6VAi+9wHA8Dsg==",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@ctrl/tinycolor": "^3.4.1",
|
"@ctrl/tinycolor": "^3.4.1",
|
||||||
"@element-plus/icons-vue": "^2.3.1",
|
"@element-plus/icons-vue": "^2.3.1",
|
||||||
|
@ -2089,6 +2232,14 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/element-resize-detector": {
|
||||||
|
"version": "1.2.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/element-resize-detector/-/element-resize-detector-1.2.4.tgz",
|
||||||
|
"integrity": "sha512-Fl5Ftk6WwXE0wqCgNoseKWndjzZlDCwuPTcoVZfCP9R3EHQF8qUtr3YUPNETegRBOKqQKPW3n4kiIWngGi8tKg==",
|
||||||
|
"dependencies": {
|
||||||
|
"batch-processor": "1.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/emoji-regex": {
|
"node_modules/emoji-regex": {
|
||||||
"version": "9.2.2",
|
"version": "9.2.2",
|
||||||
"resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz",
|
"resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz",
|
||||||
|
@ -3209,14 +3360,11 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/magic-string": {
|
"node_modules/magic-string": {
|
||||||
"version": "0.30.8",
|
"version": "0.30.17",
|
||||||
"resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.8.tgz",
|
"resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.17.tgz",
|
||||||
"integrity": "sha512-ISQTe55T2ao7XtlAStud6qwYPZjE4GK1S/BeVPus4jrq6JuOnQ00YKQC581RWhR122W7msZV263KzVeLoqidyQ==",
|
"integrity": "sha512-sNPKHvyjVf7gyjwS4xGTaW/mCnF8wnjtifKBEhxfZ7E/S8tQ0rssrwGNn6q8JH/ohItJfSQp9mBtQYuTlH5QnA==",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@jridgewell/sourcemap-codec": "^1.4.15"
|
"@jridgewell/sourcemap-codec": "^1.5.0"
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">=12"
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/mdn-data": {
|
"node_modules/mdn-data": {
|
||||||
|
@ -3249,9 +3397,9 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/micromatch": {
|
"node_modules/micromatch": {
|
||||||
"version": "4.0.7",
|
"version": "4.0.8",
|
||||||
"resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.7.tgz",
|
"resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz",
|
||||||
"integrity": "sha512-LPP/3KorzCwBxfeUuZmaR6bG2kdeHSbe0P2tY3FLRU4vYrjYz5hI4QZwV0njUx3jeuKe67YukQ1LSPZBKDqO/Q==",
|
"integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"braces": "^3.0.3",
|
"braces": "^3.0.3",
|
||||||
|
@ -3303,9 +3451,9 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/ms": {
|
"node_modules/ms": {
|
||||||
"version": "2.1.2",
|
"version": "2.1.3",
|
||||||
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
|
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
|
||||||
"integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==",
|
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
|
||||||
"devOptional": true
|
"devOptional": true
|
||||||
},
|
},
|
||||||
"node_modules/muggle-string": {
|
"node_modules/muggle-string": {
|
||||||
|
@ -3326,9 +3474,9 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/nanoid": {
|
"node_modules/nanoid": {
|
||||||
"version": "3.3.7",
|
"version": "3.3.11",
|
||||||
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.7.tgz",
|
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz",
|
||||||
"integrity": "sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g==",
|
"integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==",
|
||||||
"funding": [
|
"funding": [
|
||||||
{
|
{
|
||||||
"type": "github",
|
"type": "github",
|
||||||
|
@ -3709,9 +3857,9 @@
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
"node_modules/picocolors": {
|
"node_modules/picocolors": {
|
||||||
"version": "1.0.1",
|
"version": "1.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
|
||||||
"integrity": "sha512-anP1Z8qwhkbmu7MFP5iTt+wQKXgwzf7zTyGlcdzabySa9vd0Xt392U0rVmz9poOaBj0uHJKyyo9/upk0HrEQew=="
|
"integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA=="
|
||||||
},
|
},
|
||||||
"node_modules/picomatch": {
|
"node_modules/picomatch": {
|
||||||
"version": "2.3.1",
|
"version": "2.3.1",
|
||||||
|
@ -3817,9 +3965,9 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/postcss": {
|
"node_modules/postcss": {
|
||||||
"version": "8.4.39",
|
"version": "8.5.4",
|
||||||
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.39.tgz",
|
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.4.tgz",
|
||||||
"integrity": "sha512-0vzE+lAiG7hZl1/9I8yzKLx3aR9Xbof3fBHKunvMfOCYAtMhrsnccJY2iTURb9EZd5+pLuiNV9/c/GZJOHsgIw==",
|
"integrity": "sha512-QSa9EBe+uwlGTFmHsPKokv3B/oEMQZxfqW0QqNCyhpa6mB1afzulwn8hihglqAb2pOw+BJgNlmXQ8la2VeHB7w==",
|
||||||
"funding": [
|
"funding": [
|
||||||
{
|
{
|
||||||
"type": "opencollective",
|
"type": "opencollective",
|
||||||
|
@ -3835,9 +3983,9 @@
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"nanoid": "^3.3.7",
|
"nanoid": "^3.3.11",
|
||||||
"picocolors": "^1.0.1",
|
"picocolors": "^1.1.1",
|
||||||
"source-map-js": "^1.2.0"
|
"source-map-js": "^1.2.1"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": "^10 || ^12 || >=14"
|
"node": "^10 || ^12 || >=14"
|
||||||
|
@ -4263,9 +4411,9 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/source-map-js": {
|
"node_modules/source-map-js": {
|
||||||
"version": "1.2.0",
|
"version": "1.2.1",
|
||||||
"resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",
|
||||||
"integrity": "sha512-itJW8lvSA0TXEphiRoawsCksnlf8SyvmFzIhltqAHluXd88pkCd+cXJVHTDwdCr0IzwptSm035IHQktUu1QUMg==",
|
"integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=0.10.0"
|
"node": ">=0.10.0"
|
||||||
}
|
}
|
||||||
|
@ -5042,16 +5190,22 @@
|
||||||
"vue": ">=3.2.13"
|
"vue": ">=3.2.13"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/vscode-uri": {
|
||||||
|
"version": "3.0.8",
|
||||||
|
"resolved": "https://registry.npmjs.org/vscode-uri/-/vscode-uri-3.0.8.tgz",
|
||||||
|
"integrity": "sha512-AyFQ0EVmsOZOlAnxoFOGOq1SQDWAB7C6aqMGS23svWAllfOaxbuFvcT8D1i8z3Gyn8fraVeZNNmN6e9bxxXkKw==",
|
||||||
|
"dev": true
|
||||||
|
},
|
||||||
"node_modules/vue": {
|
"node_modules/vue": {
|
||||||
"version": "3.4.21",
|
"version": "3.5.16",
|
||||||
"resolved": "https://registry.npmjs.org/vue/-/vue-3.4.21.tgz",
|
"resolved": "https://registry.npmjs.org/vue/-/vue-3.5.16.tgz",
|
||||||
"integrity": "sha512-5hjyV/jLEIKD/jYl4cavMcnzKwjMKohureP8ejn3hhEjwhWIhWeuzL2kJAjzl/WyVsgPY56Sy4Z40C3lVshxXA==",
|
"integrity": "sha512-rjOV2ecxMd5SiAmof2xzh2WxntRcigkX/He4YFJ6WdRvVUrbt6DxC1Iujh10XLl8xCDRDtGKMeO3D+pRQ1PP9w==",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@vue/compiler-dom": "3.4.21",
|
"@vue/compiler-dom": "3.5.16",
|
||||||
"@vue/compiler-sfc": "3.4.21",
|
"@vue/compiler-sfc": "3.5.16",
|
||||||
"@vue/runtime-dom": "3.4.21",
|
"@vue/runtime-dom": "3.5.16",
|
||||||
"@vue/server-renderer": "3.4.21",
|
"@vue/server-renderer": "3.5.16",
|
||||||
"@vue/shared": "3.4.21"
|
"@vue/shared": "3.5.16"
|
||||||
},
|
},
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"typescript": "*"
|
"typescript": "*"
|
||||||
|
@ -5099,6 +5253,21 @@
|
||||||
"eslint": ">=6.0.0"
|
"eslint": ">=6.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/vue-grid-layout-v3": {
|
||||||
|
"version": "3.1.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/vue-grid-layout-v3/-/vue-grid-layout-v3-3.1.2.tgz",
|
||||||
|
"integrity": "sha512-4VB6pel2OklFNnFSUVg4wXwx/fKuKKvnhleLyscwk5ay7aEFk+HghcVKbComg+EgMvjuPzZ7hgIES3FlxuVK6w==",
|
||||||
|
"dependencies": {
|
||||||
|
"@interactjs/actions": "^1.10.27",
|
||||||
|
"@interactjs/auto-scroll": "^1.10.27",
|
||||||
|
"@interactjs/auto-start": "^1.10.27",
|
||||||
|
"@interactjs/dev-tools": "^1.10.27",
|
||||||
|
"@interactjs/interact": "^1.10.27",
|
||||||
|
"@interactjs/modifiers": "^1.10.27",
|
||||||
|
"element-resize-detector": "^1.2.4",
|
||||||
|
"mitt": "^3.0.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/vue-i18n": {
|
"node_modules/vue-i18n": {
|
||||||
"version": "9.10.2",
|
"version": "9.10.2",
|
||||||
"resolved": "https://registry.npmjs.org/vue-i18n/-/vue-i18n-9.10.2.tgz",
|
"resolved": "https://registry.npmjs.org/vue-i18n/-/vue-i18n-9.10.2.tgz",
|
||||||
|
@ -5132,31 +5301,21 @@
|
||||||
"vue": "^3.2.0"
|
"vue": "^3.2.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/vue-template-compiler": {
|
|
||||||
"version": "2.7.16",
|
|
||||||
"resolved": "https://registry.npmjs.org/vue-template-compiler/-/vue-template-compiler-2.7.16.tgz",
|
|
||||||
"integrity": "sha512-AYbUWAJHLGGQM7+cNTELw+KsOG9nl2CnSv467WobS5Cv9uk3wFcnr1Etsz2sEIHEZvw1U+o9mRlEO6QbZvUPGQ==",
|
|
||||||
"dev": true,
|
|
||||||
"dependencies": {
|
|
||||||
"de-indent": "^1.0.2",
|
|
||||||
"he": "^1.2.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/vue-tsc": {
|
"node_modules/vue-tsc": {
|
||||||
"version": "2.0.7",
|
"version": "2.0.29",
|
||||||
"resolved": "https://registry.npmjs.org/vue-tsc/-/vue-tsc-2.0.7.tgz",
|
"resolved": "https://registry.npmjs.org/vue-tsc/-/vue-tsc-2.0.29.tgz",
|
||||||
"integrity": "sha512-LYa0nInkfcDBB7y8jQ9FQ4riJTRNTdh98zK/hzt4gEpBZQmf30dPhP+odzCa+cedGz6B/guvJEd0BavZaRptjg==",
|
"integrity": "sha512-MHhsfyxO3mYShZCGYNziSbc63x7cQ5g9kvijV7dRe1TTXBRLxXyL0FnXWpUF1xII2mJ86mwYpYsUmMwkmerq7Q==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@volar/typescript": "~2.1.3",
|
"@volar/typescript": "~2.4.0-alpha.18",
|
||||||
"@vue/language-core": "2.0.7",
|
"@vue/language-core": "2.0.29",
|
||||||
"semver": "^7.5.4"
|
"semver": "^7.5.4"
|
||||||
},
|
},
|
||||||
"bin": {
|
"bin": {
|
||||||
"vue-tsc": "bin/vue-tsc.js"
|
"vue-tsc": "bin/vue-tsc.js"
|
||||||
},
|
},
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"typescript": "*"
|
"typescript": ">=5.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/vuetify": {
|
"node_modules/vuetify": {
|
||||||
|
|
21
package.json
|
@ -4,25 +4,26 @@
|
||||||
"private": true,
|
"private": true,
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": ". ./set_env.sh && vite",
|
"dev": "node scripts/gen-env.mjs",
|
||||||
"devh": ". ./set_env.sh && vite --host",
|
"devh": "node scripts/gen-env.mjs --host",
|
||||||
"build": "run-p type-check \"build-only {@}\" --",
|
"build": "run-p type-check build-only --",
|
||||||
"preview": ". ./set_env.sh && vite preview",
|
"preview": "node scripts/gen-env.mjs preview",
|
||||||
"previewh": ". ./set_env.sh && vite preview --host",
|
"previewh": "node scripts/gen-env.mjs preview --host",
|
||||||
"build-only": ". ./set_env.sh && vite build",
|
"build-only": "node scripts/gen-env.mjs build",
|
||||||
"build:dev": ". ./set_env.sh && vite build --mode development",
|
"build:dev": "NODE_ENV=development node scripts/gen-env.mjs build",
|
||||||
"type-check": "vue-tsc --build --force",
|
"type-check": "vue-tsc --build --force",
|
||||||
"lint": "eslint . --ext .vue,.js,.jsx,.cjs,.mjs,.ts,.tsx,.cts,.mts --fix --ignore-path .gitignore",
|
"lint": "eslint . --ext .vue,.js,.jsx,.ts,.tsx --fix --ignore-path .gitignore",
|
||||||
"format": "prettier --write src/"
|
"format": "prettier --write src/"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@vueuse/core": "^10.9.0",
|
"@vueuse/core": "^10.9.0",
|
||||||
"ansi_up": "^6.0.2",
|
"ansi_up": "^6.0.2",
|
||||||
"element-plus": "^2.7.3",
|
"element-plus": "^2.8.1",
|
||||||
"mitt": "^3.0.1",
|
"mitt": "^3.0.1",
|
||||||
"pinia": "^2.1.7",
|
"pinia": "^2.1.7",
|
||||||
"vue": "^3.4.21",
|
"vue": "^3.5.16",
|
||||||
"vue-draggable-plus": "^0.4.1",
|
"vue-draggable-plus": "^0.4.1",
|
||||||
|
"vue-grid-layout-v3": "^3.1.2",
|
||||||
"vue-i18n": "^9.10.2",
|
"vue-i18n": "^9.10.2",
|
||||||
"vue-router": "^4.3.0",
|
"vue-router": "^4.3.0",
|
||||||
"vuetify": "^3.6.5"
|
"vuetify": "^3.6.5"
|
||||||
|
|
|
@ -0,0 +1,25 @@
|
||||||
|
import { execSync, spawn } from 'node:child_process';
|
||||||
|
import { fileURLToPath } from 'node:url';
|
||||||
|
import { dirname, resolve } from 'node:path';
|
||||||
|
|
||||||
|
const gitTag = execSync('git describe --tags')
|
||||||
|
.toString().trim().split('-').slice(0, 2).join('-');
|
||||||
|
|
||||||
|
const lastCommit = execSync('git log -1 --format=%cd')
|
||||||
|
.toString().trim();
|
||||||
|
|
||||||
|
// run whatever came after “npm run dev …”
|
||||||
|
const argsFromNpm = process.argv.slice(2); // e.g. ["--host"]
|
||||||
|
|
||||||
|
spawn(
|
||||||
|
process.platform === 'win32' ? 'npx.cmd' : 'npx',
|
||||||
|
['vite', ...argsFromNpm],
|
||||||
|
{
|
||||||
|
stdio: 'inherit',
|
||||||
|
env: {
|
||||||
|
...process.env,
|
||||||
|
VITE_APP_GIT_TAG: gitTag,
|
||||||
|
VITE_APP_LAST_COMMIT: lastCommit
|
||||||
|
}
|
||||||
|
}
|
||||||
|
).on('exit', code => process.exit(code));
|
94
src/App.vue
|
@ -1,64 +1,94 @@
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import {useWsStore} from "@/stores/websocket";
|
import { useWsStore } from '@/stores/websocket'
|
||||||
import type {IWebsocketService} from "@/composables/websocket/websocketService";
|
import type { IWebsocketService } from '@/composables/websocket/websocketService'
|
||||||
import {getWebsocketService} from "@/composables/websocket/websocketService";
|
import { getWebsocketService } from '@/composables/websocket/websocketService'
|
||||||
import {onMounted, onUnmounted} from "vue";
|
import { onMounted, onUnmounted } from 'vue'
|
||||||
import {changeFavicon} from "@/composables/importFavicon";
|
import { changeFavicon } from '@/composables/importFavicon'
|
||||||
import {logHelloMessage} from "@/composables/logConsoleMsg";
|
import { logHelloMessage } from '@/composables/logConsoleMsg'
|
||||||
import NavBar from "@/views/navigation/NavBar.vue";
|
import NavBar from '@/views/navigation/NavBar.vue'
|
||||||
import type {ControlMsg, ServerMsg} from "@/api";
|
import type { ControlMsg, ServerMsg } from '@/api'
|
||||||
import {ControlEvent, ControlMsgType} from "@/api";
|
import { ControlEvent, ControlMsgType } from '@/api'
|
||||||
import {routeCtrlMsg, routeModuleServerMsg} from "@/router/msgRouter";
|
import { routeCtrlMsg, routeModuleServerMsg } from '@/router/msgRouter'
|
||||||
import {globalNotify} from "@/composables/notification";
|
import { globalNotify } from '@/composables/notification'
|
||||||
import {isDevMode} from "@/composables/buildMode";
|
import {
|
||||||
|
getTrialDate,
|
||||||
|
getTrialMsg,
|
||||||
|
isDevMode,
|
||||||
|
isOTAEnabled,
|
||||||
|
isTrialMode
|
||||||
|
} from '@/composables/buildMode'
|
||||||
|
import { useSystemModule } from '@/composables/useSystemModule'
|
||||||
|
import { useDataFlowModule } from '@/composables/useDataFlowModule'
|
||||||
|
import { useUpdateModule } from '@/composables/useUpdateModule'
|
||||||
|
import { ElMessageBox } from 'element-plus'
|
||||||
|
import { translate } from '@/locales'
|
||||||
|
|
||||||
const wsState = useWsStore();
|
const wsState = useWsStore()
|
||||||
|
|
||||||
const onClientCtrl = (msg: ControlMsg) => {
|
const onClientCtrl = (msg: ControlMsg) => {
|
||||||
if (isDevMode()) {
|
if (isDevMode()) {
|
||||||
console.log("App.vue:", msg);
|
console.log('App.vue:', msg)
|
||||||
}
|
}
|
||||||
if (msg.type === ControlMsgType.WS_EVENT) {
|
if (msg.type === ControlMsgType.WS_EVENT) {
|
||||||
wsState.$patch({ state: msg.data as ControlEvent })
|
wsState.$patch({ state: msg.data as ControlEvent })
|
||||||
routeCtrlMsg(msg);
|
routeCtrlMsg(msg)
|
||||||
if (msg.data === ControlEvent.CONNECTED) {
|
if (msg.data === ControlEvent.CONNECTED) {
|
||||||
globalNotify("调试器已连接", "success");
|
globalNotify(translate('common.debuggerConnected'), 'success')
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
|
||||||
|
|
||||||
const onServerMsg = (msg: ServerMsg) => {
|
const onServerMsg = (msg: ServerMsg) => {
|
||||||
if (isDevMode()) {
|
if (isDevMode()) {
|
||||||
console.log("App.vue:", msg);
|
console.log('App.vue:', msg)
|
||||||
|
}
|
||||||
|
routeModuleServerMsg(msg)
|
||||||
}
|
}
|
||||||
routeModuleServerMsg(msg);
|
|
||||||
};
|
|
||||||
|
|
||||||
let websocketService: IWebsocketService;
|
let websocketService: IWebsocketService
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
|
logHelloMessage()
|
||||||
logHelloMessage();
|
let host: string
|
||||||
let host = "";
|
|
||||||
if (isDevMode()) {
|
if (isDevMode()) {
|
||||||
host = import.meta.env.VITE_DEVICE_HOST_NAME || "dap.local";
|
host = import.meta.env.VITE_DEVICE_HOST_NAME || 'dap.local'
|
||||||
} else {
|
} else {
|
||||||
host = window.location.host
|
host = window.location.host
|
||||||
}
|
}
|
||||||
websocketService = getWebsocketService();
|
if (import.meta.env.VITE_DISABLE_CONNECTION !== 'true') {
|
||||||
websocketService.init(host, onServerMsg, onClientCtrl);
|
websocketService = getWebsocketService()
|
||||||
changeFavicon();
|
websocketService.init(host, onServerMsg, onClientCtrl)
|
||||||
});
|
websocketService.getSocketStatus()
|
||||||
|
}
|
||||||
|
changeFavicon()
|
||||||
|
|
||||||
onUnmounted(() => {
|
useSystemModule()
|
||||||
|
useDataFlowModule()
|
||||||
|
|
||||||
});
|
if (isOTAEnabled()) {
|
||||||
|
useUpdateModule()
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isTrialMode()) {
|
||||||
|
ElMessageBox.alert(getTrialMsg(), getTrialDate(), {
|
||||||
|
confirmButtonText: translate('common.ok')
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
onUnmounted(() => {})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="flex flex-col h-screen">
|
<div class="flex flex-col wt-h-100">
|
||||||
<header>
|
<header>
|
||||||
<nav-bar />
|
<nav-bar />
|
||||||
</header>
|
</header>
|
||||||
<RouterView />
|
<RouterView />
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.wt-h-100 {
|
||||||
|
height: 100vh;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
|
@ -0,0 +1,104 @@
|
||||||
|
import {type ApiJsonMsg} from '@/api'
|
||||||
|
import * as api from "@/api/index";
|
||||||
|
|
||||||
|
export enum WtDataFlowType {
|
||||||
|
NONE = 0,
|
||||||
|
SOCKET = 0x10,
|
||||||
|
WS_SERVER = 0x11,
|
||||||
|
WS_CLIENT,
|
||||||
|
WSS_SERVER,
|
||||||
|
WSS_CLIENT,
|
||||||
|
TCP_SERVER,
|
||||||
|
TCP_CLIENT,
|
||||||
|
TCP_TLS_SERVER,
|
||||||
|
TCP_TLS_CLIENT,
|
||||||
|
UDP_SERVER,
|
||||||
|
UDP_CLIENT,
|
||||||
|
PERIPHERAL = 0x80,
|
||||||
|
GPIO = 0x81,
|
||||||
|
UART = 0x82,
|
||||||
|
I2C,
|
||||||
|
I3C,
|
||||||
|
SPI,
|
||||||
|
I2S,
|
||||||
|
CAN,
|
||||||
|
RMT,
|
||||||
|
USB,
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum WtDataFlowCmd {
|
||||||
|
UNKNOWN = 0,
|
||||||
|
GET_INS_LIST = 1,
|
||||||
|
GET_CUR_INS = 2,
|
||||||
|
GET_CUR_ATTACH_LIST = 3,
|
||||||
|
GET_ATTACH_LIST = 4,
|
||||||
|
ATTACH = 5,
|
||||||
|
ATTACH_CUR_TO_RECVER = 6,
|
||||||
|
ATTACH_CUR_TO_SENDER = 7,
|
||||||
|
DETACH_SINGLE = 8,
|
||||||
|
DETACH_CUR_FROM = 9,
|
||||||
|
SET_DATA_TYPE = 10,
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IWtDataFlowJsonMsg extends ApiJsonMsg {
|
||||||
|
data_type?: 3 | 4,
|
||||||
|
ins_idx?: number,
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IPeriphInfo {
|
||||||
|
periph_num: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ISocketInfo {
|
||||||
|
foreign_port: number;
|
||||||
|
foreign_ip: string;
|
||||||
|
local_port: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface InstanceInfo {
|
||||||
|
ins_idx: number,
|
||||||
|
mod_idx: number,
|
||||||
|
mod_type: number,
|
||||||
|
port_info: ISocketInfo | IPeriphInfo;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IInstanceList extends ApiJsonMsg {
|
||||||
|
instances: InstanceInfo[],
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface AttachInfo {
|
||||||
|
attach_idx: number,
|
||||||
|
s_ins_idx: number,
|
||||||
|
r_ins_idx: number,
|
||||||
|
data_type: 3 | 4,
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IAttachList extends ApiJsonMsg {
|
||||||
|
attaches: AttachInfo[],
|
||||||
|
}
|
||||||
|
|
||||||
|
export function wt_data_flow_get_instance_list() {
|
||||||
|
const jsonMsg: IWtDataFlowJsonMsg = {
|
||||||
|
cmd: WtDataFlowCmd.GET_INS_LIST,
|
||||||
|
module: api.WtModuleID.DATA_FLOW,
|
||||||
|
}
|
||||||
|
api.sendJsonMsg(jsonMsg);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function wt_data_flow_attach_cur_to_sender(instance_index: number) {
|
||||||
|
const jsonMsg: IWtDataFlowJsonMsg = {
|
||||||
|
cmd: WtDataFlowCmd.ATTACH_CUR_TO_SENDER,
|
||||||
|
module: api.WtModuleID.DATA_FLOW,
|
||||||
|
data_type: 3,
|
||||||
|
ins_idx: instance_index,
|
||||||
|
}
|
||||||
|
api.sendJsonMsg(jsonMsg);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function wt_data_flow_get_attach_list() {
|
||||||
|
const jsonMsg: IWtDataFlowJsonMsg = {
|
||||||
|
cmd: WtDataFlowCmd.GET_ATTACH_LIST,
|
||||||
|
module: api.WtModuleID.DATA_FLOW,
|
||||||
|
}
|
||||||
|
api.sendJsonMsg(jsonMsg);
|
||||||
|
}
|
|
@ -0,0 +1,61 @@
|
||||||
|
import {type ApiJsonMsg, sendJsonMsg, WtModuleID} from '@/api'
|
||||||
|
|
||||||
|
export enum WtOTACmd {
|
||||||
|
WT_OTA_GET_UPDATE_INFO = 1, /* total_size, ver */
|
||||||
|
WT_OTA_DO_UPDATE = 2, /* returns OK, chunk of remaining bytes and total length -> wt_event_manager */
|
||||||
|
WT_OTA_GET_PROGRESS = 3, /* returns chunk of remaining bytes and total length */
|
||||||
|
WT_OTA_DO_URL_UPDATE = 4, /* force update { url: "https://" } */
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum WtOTAProgressStatus {
|
||||||
|
OK = "OK",
|
||||||
|
IDLE = "IDLE",
|
||||||
|
IN_PROGRESS = "IN_PROGRESS",
|
||||||
|
FAILED = "FAILED",
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IOTAProgress extends ApiJsonMsg {
|
||||||
|
progress: number;
|
||||||
|
total_size: number;
|
||||||
|
status: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IOTAFmInfo extends ApiJsonMsg {
|
||||||
|
fm_size: number;
|
||||||
|
fm_ver: string;
|
||||||
|
upd_date: string;
|
||||||
|
upd_note: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function wt_ota_get_update_info() {
|
||||||
|
const msg: ApiJsonMsg = {
|
||||||
|
module: WtModuleID.OTA,
|
||||||
|
cmd: WtOTACmd.WT_OTA_GET_UPDATE_INFO,
|
||||||
|
};
|
||||||
|
sendJsonMsg(msg);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function wt_ota_do_update() {
|
||||||
|
const msg: ApiJsonMsg = {
|
||||||
|
module: WtModuleID.OTA,
|
||||||
|
cmd: WtOTACmd.WT_OTA_DO_UPDATE,
|
||||||
|
};
|
||||||
|
sendJsonMsg(msg);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function wt_ota_get_progress() {
|
||||||
|
const msg: ApiJsonMsg = {
|
||||||
|
module: WtModuleID.OTA,
|
||||||
|
cmd: WtOTACmd.WT_OTA_GET_PROGRESS,
|
||||||
|
};
|
||||||
|
sendJsonMsg(msg);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function wt_ota_do_url_update(url: string) {
|
||||||
|
const msg: ApiJsonMsg & {url: string} = {
|
||||||
|
module: WtModuleID.OTA,
|
||||||
|
cmd: WtOTACmd.WT_OTA_DO_URL_UPDATE,
|
||||||
|
url: url,
|
||||||
|
};
|
||||||
|
sendJsonMsg(msg);
|
||||||
|
}
|
|
@ -0,0 +1,45 @@
|
||||||
|
import {type ApiJsonMsg, sendJsonMsg, WtModuleID} from '@/api'
|
||||||
|
|
||||||
|
export enum WtSytemCmd {
|
||||||
|
WT_SYS_GET_FM_INFO = 1,
|
||||||
|
WT_SYS_REBOOT = 2,
|
||||||
|
WT_SYS_GET_SYS_INFO = 3,
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ISysFmInfo extends ApiJsonMsg {
|
||||||
|
fm_ver: string;
|
||||||
|
upd_date: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ISysHwInfo extends ApiJsonMsg {
|
||||||
|
hw_ver: string;
|
||||||
|
mf_date: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ISysInfo {
|
||||||
|
sn: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function wt_sys_get_fm_info() {
|
||||||
|
const msg: ApiJsonMsg = {
|
||||||
|
module: WtModuleID.SYSTEM,
|
||||||
|
cmd: WtSytemCmd.WT_SYS_GET_FM_INFO,
|
||||||
|
};
|
||||||
|
sendJsonMsg(msg);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function wt_sys_reboot() {
|
||||||
|
const msg: ApiJsonMsg = {
|
||||||
|
module: WtModuleID.SYSTEM,
|
||||||
|
cmd: WtSytemCmd.WT_SYS_REBOOT,
|
||||||
|
};
|
||||||
|
sendJsonMsg(msg);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function wt_sys_get_sys_info() {
|
||||||
|
const msg: ApiJsonMsg = {
|
||||||
|
module: WtModuleID.SYSTEM,
|
||||||
|
cmd: WtSytemCmd.WT_SYS_GET_SYS_INFO,
|
||||||
|
};
|
||||||
|
sendJsonMsg(msg);
|
||||||
|
}
|
|
@ -0,0 +1,110 @@
|
||||||
|
import type {ApiBinaryMsg} from "@/api/binDataDef";
|
||||||
|
import {WtDataType} from "@/api/binDataDef";
|
||||||
|
import {type ApiJsonMsg, sendBinMsg, sendJsonMsg, WtModuleID} from "@/api/index";
|
||||||
|
|
||||||
|
export enum WtUartCmd {
|
||||||
|
UNKNOWN = 0,
|
||||||
|
|
||||||
|
/* UART PERIPHERAL */
|
||||||
|
GET_AVAILABLE_NUMS = 1,
|
||||||
|
GET_BAUD = 4,
|
||||||
|
SET_BAUD = 5,
|
||||||
|
GET_CONFIG = 6, /* data bits, parity and stop bits */
|
||||||
|
SET_CONFIG = 7,
|
||||||
|
GET_FLOW_CTRL, /* flow control function RTS/CTS*/
|
||||||
|
SET_FLOW_CTRL,
|
||||||
|
GET_PINS_NUM, /* not implemented change pinout function */
|
||||||
|
SET_PINS_NUM, /* not implemented */
|
||||||
|
GET_MODE, /* not implemented UART/RS485/IrDA */
|
||||||
|
SET_MODE, /* not implemented UART/RS485/IrDA */
|
||||||
|
|
||||||
|
GET_STATUS = 20, /* is uart enabled and other information */
|
||||||
|
SET_STATUS, /* set specific uart port disable */
|
||||||
|
GET_DATA_TYPE = 22, // 0x03 or 0x04
|
||||||
|
SET_DATA_TYPE = 23, // 0x03 or 0x04
|
||||||
|
|
||||||
|
GET_DEFAULT_NUM = 24,
|
||||||
|
}
|
||||||
|
|
||||||
|
enum ANSI_ESCAPE_CODE {
|
||||||
|
REFRESH_WINDOW = '\x1b[7t',
|
||||||
|
CLEAR_WINDOW = '\x1b[2J'
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IUartConfig {
|
||||||
|
data_bits: 5 | 6 | 7 | 8;
|
||||||
|
parity : 0 | 1 | 2;
|
||||||
|
stop_bits: 1 | 15 | 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IUartMsgConfig extends ApiJsonMsg, IUartConfig {
|
||||||
|
sub_mod: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IUartMsgBaud extends ApiJsonMsg {
|
||||||
|
sub_mod: number;
|
||||||
|
baud: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IUartMsgNum extends ApiJsonMsg {
|
||||||
|
num: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function uart_send_msg(payload: Uint8Array, sub_mod: number) {
|
||||||
|
/* hard code uart num for now */
|
||||||
|
const msg: ApiBinaryMsg = {
|
||||||
|
sub_mod: sub_mod,
|
||||||
|
data_type: WtDataType.RAW,
|
||||||
|
module: WtModuleID.UART,
|
||||||
|
payload: payload,
|
||||||
|
}
|
||||||
|
sendBinMsg(msg);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function uart_get_baud(uart_num: number) {
|
||||||
|
const cmd = {
|
||||||
|
cmd: WtUartCmd.GET_BAUD,
|
||||||
|
module: WtModuleID.UART,
|
||||||
|
sub_mod: uart_num,
|
||||||
|
}
|
||||||
|
sendJsonMsg(cmd);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function uart_set_baud(baud: number, uart_num: number) {
|
||||||
|
const cmd: IUartMsgBaud = {
|
||||||
|
cmd: WtUartCmd.SET_BAUD,
|
||||||
|
module: WtModuleID.UART,
|
||||||
|
baud: baud,
|
||||||
|
sub_mod: uart_num,
|
||||||
|
}
|
||||||
|
sendJsonMsg(cmd);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function uart_get_config(uart_num: number) {
|
||||||
|
const cmd = {
|
||||||
|
cmd: WtUartCmd.GET_CONFIG,
|
||||||
|
module: WtModuleID.UART,
|
||||||
|
sub_mod: uart_num,
|
||||||
|
}
|
||||||
|
sendJsonMsg(cmd);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function uart_set_config(uart_config: IUartConfig, uart_num: number) {
|
||||||
|
const cmd: IUartMsgConfig = {
|
||||||
|
cmd: WtUartCmd.SET_CONFIG,
|
||||||
|
module: WtModuleID.UART,
|
||||||
|
sub_mod: uart_num,
|
||||||
|
data_bits: uart_config.data_bits,
|
||||||
|
parity: uart_config.parity,
|
||||||
|
stop_bits: uart_config.stop_bits,
|
||||||
|
}
|
||||||
|
sendJsonMsg(cmd);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function uart_get_default_num() {
|
||||||
|
const cmd = {
|
||||||
|
cmd: WtUartCmd.GET_DEFAULT_NUM,
|
||||||
|
module: WtModuleID.UART,
|
||||||
|
}
|
||||||
|
sendJsonMsg(cmd);
|
||||||
|
}
|
|
@ -31,9 +31,11 @@ export interface ServerMsg {
|
||||||
}
|
}
|
||||||
|
|
||||||
export enum WtModuleID {
|
export enum WtModuleID {
|
||||||
|
SYSTEM = 0,
|
||||||
WIFI = 1,
|
WIFI = 1,
|
||||||
DATA_FLOW = 2,
|
DATA_FLOW = 2,
|
||||||
UART = 4,
|
UART = 4,
|
||||||
|
OTA = 5,
|
||||||
}
|
}
|
||||||
|
|
||||||
export function sendJsonMsg(apiJsonMsg: ApiJsonMsg) {
|
export function sendJsonMsg(apiJsonMsg: ApiJsonMsg) {
|
||||||
|
|
|
@ -0,0 +1 @@
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 -960 960 960" fill="#5f6368"><path d="M480-360 280-560h400L480-360Z"/></svg>
|
After Width: | Height: | Size: 127 B |
|
@ -0,0 +1 @@
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 -960 960 960" fill="#5f6368"><path d="m280-400 200-200 200 200H280Z"/></svg>
|
After Width: | Height: | Size: 127 B |
|
@ -0,0 +1 @@
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 -960 960 960" width="24px" fill="#1f1f1f"><path d="m256-200-56-56 224-224-224-224 56-56 224 224 224-224 56 56-224 224 224 224-56 56-224-224-224 224Z"/></svg>
|
After Width: | Height: | Size: 222 B |
|
@ -0,0 +1 @@
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 -960 960 960" width="24px" fill="#1f1f1f"><path d="M240-80q-33 0-56.5-23.5T160-160v-400q0-33 23.5-56.5T240-640h40v-80q0-83 58.5-141.5T480-920q83 0 141.5 58.5T680-720v80h40q33 0 56.5 23.5T800-560v400q0 33-23.5 56.5T720-80H240Zm0-80h480v-400H240v400Zm240-120q33 0 56.5-23.5T560-360q0-33-23.5-56.5T480-440q-33 0-56.5 23.5T400-360q0 33 23.5 56.5T480-280ZM360-640h240v-80q0-50-35-85t-85-35q-50 0-85 35t-35 85v80ZM240-160v-400 400Z"/></svg>
|
After Width: | Height: | Size: 499 B |
|
@ -0,0 +1 @@
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 -960 960 960" width="24px" fill="#1f1f1f"><path d="M240-160h480v-400H240v400Zm240-120q33 0 56.5-23.5T560-360q0-33-23.5-56.5T480-440q-33 0-56.5 23.5T400-360q0 33 23.5 56.5T480-280ZM240-160v-400 400Zm0 80q-33 0-56.5-23.5T160-160v-400q0-33 23.5-56.5T240-640h280v-80q0-83 58.5-141.5T720-920q83 0 141.5 58.5T920-720h-80q0-50-35-85t-85-35q-50 0-85 35t-35 85v80h120q33 0 56.5 23.5T800-560v400q0 33-23.5 56.5T720-80H240Z"/></svg>
|
After Width: | Height: | Size: 486 B |
|
@ -0,0 +1 @@
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 -960 960 960" width="24px" fill="#1f1f1f"><path d="M320-200v-560l440 280-440 280Zm80-280Zm0 134 210-134-210-134v268Z"/></svg>
|
After Width: | Height: | Size: 190 B |
|
@ -0,0 +1 @@
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 -960 960 960" width="24px" fill="#1f1f1f"><path d="M280-80 120-240l160-160 56 58-62 62h406v-160h80v240H274l62 62-56 58Zm-80-440v-240h486l-62-62 56-58 160 160-160 160-56-58 62-62H280v160h-80Z"/></svg>
|
After Width: | Height: | Size: 264 B |
|
@ -0,0 +1 @@
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 -960 960 960" width="24px" fill="#1f1f1f"><path d="M240-320h320v-80H240v80Zm400 0h80v-80h-80v80ZM240-480h80v-80h-80v80Zm160 0h320v-80H400v80ZM160-160q-33 0-56.5-23.5T80-240v-480q0-33 23.5-56.5T160-800h640q33 0 56.5 23.5T880-720v480q0 33-23.5 56.5T800-160H160Zm0-80h640v-480H160v480Zm0 0v-480 480Z"/></svg>
|
After Width: | Height: | Size: 370 B |
|
@ -0,0 +1 @@
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M0 0h24v24H0z" fill="none"/><path d="M12.87 15.07l-2.54-2.51.03-.03c1.74-1.94 2.98-4.17 3.71-6.53H17V4h-7V2H8v2H1v1.99h11.17C11.5 7.92 10.44 9.75 9 11.35 8.07 10.32 7.3 9.19 6.69 8h-2c.73 1.63 1.73 3.17 2.98 4.56l-5.09 5.02L4 19l5-5 3.11 3.11.76-2.04zM18.5 10h-2L12 22h2l1.12-3h4.75L21 22h2l-4.5-12zm-2.62 7l1.62-4.33L19.12 17h-3.24z"/></svg>
|
After Width: | Height: | Size: 411 B |
|
@ -0,0 +1 @@
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 -960 960 960" fill="#5f6368"><path d="M280-120q-33 0-56.5-23.5T200-200v-520h-40v-80h200v-40h240v40h200v80h-40v520q0 33-23.5 56.5T680-120H280Zm400-600H280v520h400v-520ZM360-280h80v-360h-80v360Zm160 0h80v-360h-80v360ZM280-720v520-520Z"/></svg>
|
After Width: | Height: | Size: 292 B |
|
@ -1,4 +1,17 @@
|
||||||
export const toServer = new BroadcastChannel("toServer");
|
// Define a fallback mock class only if BroadcastChannel is undefined
|
||||||
export const toClient = new BroadcastChannel("toClient");
|
const BC: typeof BroadcastChannel = typeof BroadcastChannel !== 'undefined'
|
||||||
export const toWebsocketCtrl = new BroadcastChannel("toWebsocketCtrl");
|
? BroadcastChannel
|
||||||
export const toClientCtrl = new BroadcastChannel("toClientCtrl");
|
: class {
|
||||||
|
constructor(name: string) {
|
||||||
|
// no-op
|
||||||
|
}
|
||||||
|
postMessage(_: any) {}
|
||||||
|
close() {}
|
||||||
|
addEventListener(_: string, __: any) {}
|
||||||
|
removeEventListener(_: string, __: any) {}
|
||||||
|
} as unknown as typeof BroadcastChannel;
|
||||||
|
|
||||||
|
export const toServer = new BC("toServer");
|
||||||
|
export const toClient = new BC("toClient");
|
||||||
|
export const toWebsocketCtrl = new BC("toWebsocketCtrl");
|
||||||
|
export const toClientCtrl = new BC("toClientCtrl");
|
|
@ -1,3 +1,19 @@
|
||||||
export function isDevMode() {
|
export function isDevMode() {
|
||||||
return import.meta.env.VITE_APP_MODE === 'dev';
|
return import.meta.env.VITE_APP_MODE === 'dev';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function isOTAEnabled() {
|
||||||
|
return import.meta.env.VITE_ENABLE_OTA === 'true' || false;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isTrialMode() {
|
||||||
|
return import.meta.env.VITE_TRIAL_MODE === "true" || false;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getTrialDate() {
|
||||||
|
return import.meta.env.VITE_TRIAL_DATE || "1970-01-01";
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getTrialMsg() {
|
||||||
|
return import.meta.env.VITE_TRIAL_MSG || "感谢您试用允斯开放固件,若您喜欢,欢迎关注我的B站或者加入允斯群,新项目和更新都会在第一时间在这里发布. 使用愉快^_^";
|
||||||
|
}
|
||||||
|
|
|
@ -2,7 +2,7 @@ import {ElMessage, ElNotification} from "element-plus";
|
||||||
|
|
||||||
type NotificationType = 'error' | 'warning' | 'info' | 'success' ;
|
type NotificationType = 'error' | 'warning' | 'info' | 'success' ;
|
||||||
|
|
||||||
export function globalNotify(msg: string, type: NotificationType) {
|
export function globalNotify(msg: string, type: NotificationType = "info") {
|
||||||
ElMessage({
|
ElMessage({
|
||||||
message: msg,
|
message: msg,
|
||||||
grouping: true,
|
grouping: true,
|
||||||
|
@ -13,7 +13,7 @@ export function globalNotify(msg: string, type: NotificationType) {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
export function globalNotifyRightSide(msg: string, type: NotificationType) {
|
export function globalNotifyRightSide(msg: string, type: NotificationType = "info") {
|
||||||
ElNotification({
|
ElNotification({
|
||||||
message: msg,
|
message: msg,
|
||||||
type: type,
|
type: type,
|
||||||
|
|
|
@ -0,0 +1,176 @@
|
||||||
|
/**
|
||||||
|
* @file Composable for managing a task scheduler that executes callbacks at specified intervals.
|
||||||
|
* This scheduler avoids time drift and handles race conditions from stale timers.
|
||||||
|
*/
|
||||||
|
import { ref, onUnmounted } from 'vue'
|
||||||
|
import { isDevMode } from './buildMode'
|
||||||
|
|
||||||
|
interface ScheduledTask {
|
||||||
|
id: string
|
||||||
|
intervalMS: number
|
||||||
|
nextExecutionTime: number
|
||||||
|
executeCallback: () => Promise<void> | void
|
||||||
|
oneTime: boolean
|
||||||
|
version: number
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Module-level state (Singleton pattern) ---
|
||||||
|
|
||||||
|
/** A sorted array of tasks to be executed. The task at index 0 is always the next one. */
|
||||||
|
const scheduledTasks = ref<ScheduledTask[]>([])
|
||||||
|
/** Tracks the latest version for a given task ID to prevent stale timers from running. */
|
||||||
|
const taskVersions = new Map<string, number>()
|
||||||
|
/** The ID of the currently active `setTimeout` instance. */
|
||||||
|
let currentTimerId: number | null = null
|
||||||
|
/** A reactive flag indicating if the scheduler has any pending tasks. */
|
||||||
|
export const isSchedulerRunning = ref(false)
|
||||||
|
/** A lock to prevent concurrent task executions */
|
||||||
|
let isTaskExecuting = false
|
||||||
|
|
||||||
|
export function useCommandLoopManager() {
|
||||||
|
// --- Private Scheduler Core ---
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Executes the task at the front of the queue and reschedules it if it's recurring.
|
||||||
|
*/
|
||||||
|
const executeNextTask = async () => {
|
||||||
|
// If a task is already running, wait before trying to execute the next one.
|
||||||
|
if (isTaskExecuting) {
|
||||||
|
if (isDevMode()) console.log('[Scheduler] Delaying task execution: another task is already running.')
|
||||||
|
// This is a simple back-off strategy.
|
||||||
|
currentTimerId = window.setTimeout(executeNextTask, 50)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (scheduledTasks.value.length === 0) {
|
||||||
|
isSchedulerRunning.value = false
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const taskToExecute = scheduledTasks.value.shift()!
|
||||||
|
|
||||||
|
// Stale check
|
||||||
|
const latestVersion = taskVersions.get(taskToExecute.id)
|
||||||
|
if (taskToExecute.version !== latestVersion) {
|
||||||
|
if (isDevMode()) console.log(`[Scheduler] Discarding stale task '${taskToExecute.id}' v${taskToExecute.version}.`)
|
||||||
|
scheduleNextExecution() // The queue has changed, so recalculate.
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Acquire the lock and execute the callback.
|
||||||
|
try {
|
||||||
|
isTaskExecuting = true
|
||||||
|
if (isDevMode()) console.log(`[Scheduler] Executing task '${taskToExecute.id}' v${taskToExecute.version}`)
|
||||||
|
await taskToExecute.executeCallback()
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`[Scheduler] Error in task '${taskToExecute.id}':`, error)
|
||||||
|
} finally {
|
||||||
|
isTaskExecuting = false // ALWAYS release the lock
|
||||||
|
}
|
||||||
|
|
||||||
|
// If it's a recurring task, reschedule it.
|
||||||
|
if (!taskToExecute.oneTime) {
|
||||||
|
taskToExecute.nextExecutionTime = Date.now() + taskToExecute.intervalMS
|
||||||
|
// Re-insert the task and re-sort the queue.
|
||||||
|
scheduledTasks.value.push(taskToExecute)
|
||||||
|
scheduledTasks.value.sort((a, b) => a.nextExecutionTime - b.nextExecutionTime)
|
||||||
|
} else {
|
||||||
|
taskVersions.delete(taskToExecute.id)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set the timer for the next task in the queue.
|
||||||
|
scheduleNextExecution()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets a single master timer for the next task in the queue.
|
||||||
|
*/
|
||||||
|
const scheduleNextExecution = () => {
|
||||||
|
if (currentTimerId !== null) {
|
||||||
|
window.clearTimeout(currentTimerId)
|
||||||
|
currentTimerId = null
|
||||||
|
}
|
||||||
|
|
||||||
|
if (scheduledTasks.value.length === 0) {
|
||||||
|
isSchedulerRunning.value = false
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const nextTask = scheduledTasks.value[0]
|
||||||
|
const timeout = Math.max(0, nextTask.nextExecutionTime - Date.now())
|
||||||
|
|
||||||
|
currentTimerId = window.setTimeout(executeNextTask, timeout)
|
||||||
|
isSchedulerRunning.value = true
|
||||||
|
|
||||||
|
if (isDevMode()) {
|
||||||
|
console.log(`[Scheduler] Next task '${nextTask.id}' v${nextTask.version} scheduled in ${timeout}ms.`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Public API ---
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Registers a new task, updates an existing one, or removes a task.
|
||||||
|
* This is the single entry point for all scheduling changes.
|
||||||
|
* @param id A unique identifier for the task.
|
||||||
|
* @param intervalMS The interval in milliseconds. Pass 0 or an invalid value to unregister.
|
||||||
|
* @param executeCallback The function to execute.
|
||||||
|
* @param oneTime If true, the task runs once and is not rescheduled.
|
||||||
|
*/
|
||||||
|
const registerLoop = (
|
||||||
|
id: string,
|
||||||
|
intervalMS: number | string,
|
||||||
|
executeCallback: () => Promise<void> | void,
|
||||||
|
oneTime = false,
|
||||||
|
) => {
|
||||||
|
scheduledTasks.value = scheduledTasks.value.filter((task) => task.id !== id)
|
||||||
|
|
||||||
|
const interval = typeof intervalMS === 'string' ? parseInt(intervalMS, 10) : intervalMS
|
||||||
|
if (!interval || isNaN(interval) || interval <= 0) {
|
||||||
|
if (isDevMode()) console.log(`[Scheduler] Unregistered task '${id}'.`)
|
||||||
|
taskVersions.delete(id)
|
||||||
|
scheduleNextExecution()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const newVersion = (taskVersions.get(id) || 0) + 1
|
||||||
|
taskVersions.set(id, newVersion)
|
||||||
|
|
||||||
|
const newTask: ScheduledTask = {
|
||||||
|
id,
|
||||||
|
intervalMS: interval,
|
||||||
|
nextExecutionTime: Date.now() + interval,
|
||||||
|
executeCallback,
|
||||||
|
oneTime,
|
||||||
|
version: newVersion,
|
||||||
|
}
|
||||||
|
|
||||||
|
scheduledTasks.value.push(newTask)
|
||||||
|
scheduledTasks.value.sort((a, b) => a.nextExecutionTime - b.nextExecutionTime)
|
||||||
|
|
||||||
|
if (isDevMode()) {
|
||||||
|
console.log(`[Scheduler] Registered task '${id}' v${newVersion} with interval ${interval}ms.`)
|
||||||
|
}
|
||||||
|
|
||||||
|
scheduleNextExecution()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A convenience helper to explicitly remove a task from the scheduler.
|
||||||
|
* @param id The identifier of the task to remove.
|
||||||
|
*/
|
||||||
|
const unregisterLoop = (id: string) => {
|
||||||
|
registerLoop(id, -1, () => {})
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Lifecycle Hook ---
|
||||||
|
|
||||||
|
onUnmounted(() => {
|
||||||
|
if (currentTimerId !== null) window.clearTimeout(currentTimerId)
|
||||||
|
scheduledTasks.value = []
|
||||||
|
taskVersions.clear()
|
||||||
|
isSchedulerRunning.value = false
|
||||||
|
})
|
||||||
|
|
||||||
|
return { registerLoop, unregisterLoop, isRunning: isSchedulerRunning }
|
||||||
|
}
|
|
@ -0,0 +1,44 @@
|
||||||
|
import {registerModule} from "@/router/msgRouter";
|
||||||
|
import {type ApiJsonMsg, type ControlMsg, ControlMsgType, WtModuleID} from "@/api";
|
||||||
|
import {isDevMode} from "@/composables/buildMode";
|
||||||
|
import {useDataFlowStore} from "@/stores/useDataFlowStore";
|
||||||
|
import {type IInstanceList, WtDataFlowCmd} from "@/api/apiDataFlow";
|
||||||
|
|
||||||
|
|
||||||
|
export function useDataFlowModule() {
|
||||||
|
const dfStore = useDataFlowStore()
|
||||||
|
|
||||||
|
function onClientCtrl(msg: ControlMsg) {
|
||||||
|
if (msg.type !== ControlMsgType.WS_EVENT) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function onClientMsg(msg: ApiJsonMsg) {
|
||||||
|
switch (msg.cmd as WtDataFlowCmd) {
|
||||||
|
case WtDataFlowCmd.GET_INS_LIST: {
|
||||||
|
const insList = msg as IInstanceList;
|
||||||
|
dfStore.instanceList = insList.instances;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case WtDataFlowCmd.GET_ATTACH_LIST: {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
if (isDevMode()) {
|
||||||
|
console.log(msg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
registerModule(WtModuleID.DATA_FLOW, {
|
||||||
|
ctrlCallback: onClientCtrl,
|
||||||
|
serverJsonMsgCallback: onClientMsg,
|
||||||
|
serverBinMsgCallback: () => {},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,83 @@
|
||||||
|
import { useDataViewerStore } from '@/stores/dataViewerStore'
|
||||||
|
import { watch } from 'vue'
|
||||||
|
import type { IDataBuf } from '@/stores/dataViewerStore'
|
||||||
|
import { isDevMode } from '@/composables/buildMode'
|
||||||
|
|
||||||
|
function decodeUtf8(u8Arr: Uint8Array) {
|
||||||
|
try {
|
||||||
|
const decoder = new TextDecoder()
|
||||||
|
const decodedText = decoder.decode(u8Arr) // Attempt to decode
|
||||||
|
return decodedText.replace(/\uFFFD/g, '') // Remove all <20> characters
|
||||||
|
} catch (error) {
|
||||||
|
return ''
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useSequentialUart() {
|
||||||
|
const dataViewerStore = useDataViewerStore() as any
|
||||||
|
|
||||||
|
async function sendCommand(command: string): Promise<string> {
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
let responseBuffer = ''
|
||||||
|
let responseTimeout: number | null = null
|
||||||
|
const RESPONSE_WAIT_TIME = 10 // Wait 500ms after last response before resolving
|
||||||
|
|
||||||
|
// Index to track which messages we've already processed
|
||||||
|
let lastProcessedIndex = dataViewerStore.dataFiltered.length - 1
|
||||||
|
|
||||||
|
const stopWatch = watch(
|
||||||
|
() => dataViewerStore.dataFiltered,
|
||||||
|
(newBuf: IDataBuf[]) => {
|
||||||
|
if (isDevMode()) {
|
||||||
|
console.log('watch data', newBuf)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Process only new messages
|
||||||
|
for (let i = lastProcessedIndex + 1; i < newBuf.length; i++) {
|
||||||
|
const message = newBuf[i]
|
||||||
|
if (message.isRX) {
|
||||||
|
// Add to response buffer
|
||||||
|
responseBuffer += decodeUtf8(message.data)
|
||||||
|
|
||||||
|
// Reset timeout to wait for more responses
|
||||||
|
if (responseTimeout) {
|
||||||
|
clearTimeout(responseTimeout)
|
||||||
|
}
|
||||||
|
|
||||||
|
responseTimeout = window.setTimeout(() => {
|
||||||
|
stopWatch()
|
||||||
|
resolve(responseBuffer)
|
||||||
|
}, RESPONSE_WAIT_TIME)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update last processed index
|
||||||
|
lastProcessedIndex = newBuf.length - 1
|
||||||
|
},
|
||||||
|
{ deep: true }
|
||||||
|
)
|
||||||
|
|
||||||
|
dataViewerStore.addString(command, false, true)
|
||||||
|
|
||||||
|
// Set a maximum timeout in case no response is received
|
||||||
|
const maxTimeout = window.setTimeout(() => {
|
||||||
|
stopWatch()
|
||||||
|
resolve(responseBuffer)
|
||||||
|
}, 1000) // 5 seconds maximum wait time
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
async function sendCommands(commands: string[]): Promise<string[]> {
|
||||||
|
const responses: string[] = []
|
||||||
|
for (const command of commands) {
|
||||||
|
const response = await sendCommand(command)
|
||||||
|
responses.push(response)
|
||||||
|
}
|
||||||
|
if (isDevMode()) {
|
||||||
|
console.log('sendCommands', responses)
|
||||||
|
}
|
||||||
|
return responses
|
||||||
|
}
|
||||||
|
|
||||||
|
return { sendCommands }
|
||||||
|
}
|
|
@ -0,0 +1,54 @@
|
||||||
|
import {useSystemStore} from "@/stores/useSystemStore";
|
||||||
|
import {registerModule} from "@/router/msgRouter";
|
||||||
|
import {type ApiJsonMsg, ControlEvent, type ControlMsg, ControlMsgType, WtModuleID} from "@/api";
|
||||||
|
import {type ISysFmInfo, type ISysInfo, wt_sys_get_fm_info, wt_sys_get_sys_info, WtSytemCmd} from "@/api/apiSystem";
|
||||||
|
import {isDevMode} from "@/composables/buildMode";
|
||||||
|
|
||||||
|
|
||||||
|
export function useSystemModule() {
|
||||||
|
const sysStore = useSystemStore()
|
||||||
|
|
||||||
|
function onClientCtrl(msg: ControlMsg) {
|
||||||
|
if (msg.type !== ControlMsgType.WS_EVENT) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (msg.data === ControlEvent.CONNECTED) {
|
||||||
|
wt_sys_get_fm_info();
|
||||||
|
wt_sys_get_sys_info();
|
||||||
|
sysStore.rebootInProgress = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function onClientMsg(msg: ApiJsonMsg) {
|
||||||
|
switch (msg.cmd as WtSytemCmd) {
|
||||||
|
case WtSytemCmd.WT_SYS_REBOOT:
|
||||||
|
sysStore.rebootInProgress = true;
|
||||||
|
break;
|
||||||
|
case WtSytemCmd.WT_SYS_GET_FM_INFO: {
|
||||||
|
const fm_info = msg as ISysFmInfo;
|
||||||
|
sysStore.curFmInfo.date = fm_info.upd_date;
|
||||||
|
sysStore.curFmInfo.ver = fm_info.fm_ver;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case WtSytemCmd.WT_SYS_GET_SYS_INFO: {
|
||||||
|
const sysInfo: ISysInfo = msg as ISysInfo & ApiJsonMsg;
|
||||||
|
Object.assign(sysStore.sysInfo, sysInfo);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (isDevMode()) {
|
||||||
|
console.log(msg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
registerModule(WtModuleID.SYSTEM, {
|
||||||
|
ctrlCallback: onClientCtrl,
|
||||||
|
serverJsonMsgCallback: onClientMsg,
|
||||||
|
serverBinMsgCallback: () => {},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,81 @@
|
||||||
|
import type { IUartMsgConfig, IUartMsgNum } from '@/api/apiUart'
|
||||||
|
import type { IUartMsgBaud } from '@/api/apiUart'
|
||||||
|
import { uart_get_baud, uart_get_config, WtUartCmd } from '@/api/apiUart'
|
||||||
|
import { useUartStore } from '@/stores/useUartStore'
|
||||||
|
import { registerModule } from '@/router/msgRouter'
|
||||||
|
import * as api from '@/api'
|
||||||
|
import { isDevMode } from './buildMode'
|
||||||
|
import { useDataViewerStore } from '@/stores/dataViewerStore'
|
||||||
|
import type { ApiBinaryMsg } from '@/api/binDataDef'
|
||||||
|
import { ControlEvent } from '@/api'
|
||||||
|
import * as df from '@/api/apiDataFlow'
|
||||||
|
import { uart_get_default_num } from '@/api/apiUart'
|
||||||
|
|
||||||
|
export function useUartModule() {
|
||||||
|
const dataViewerStore = useDataViewerStore()
|
||||||
|
const uartStore = useUartStore()
|
||||||
|
|
||||||
|
function updateUartData() {
|
||||||
|
/* TODO: hard code for the moment, 0 is UART instance id (can be changed in the future) */
|
||||||
|
uart_get_default_num()
|
||||||
|
df.wt_data_flow_attach_cur_to_sender(0)
|
||||||
|
}
|
||||||
|
|
||||||
|
const onUartJsonMsg = (msg: api.ApiJsonMsg) => {
|
||||||
|
switch (msg.cmd as WtUartCmd) {
|
||||||
|
case WtUartCmd.GET_BAUD:
|
||||||
|
case WtUartCmd.SET_BAUD: {
|
||||||
|
const uartMsg = msg as IUartMsgBaud
|
||||||
|
if (uartMsg.baud) {
|
||||||
|
dataViewerStore.setUartBaud(uartMsg.baud)
|
||||||
|
}
|
||||||
|
break
|
||||||
|
}
|
||||||
|
case WtUartCmd.GET_CONFIG:
|
||||||
|
case WtUartCmd.SET_CONFIG: {
|
||||||
|
const uartMsg = msg as IUartMsgConfig
|
||||||
|
dataViewerStore.uartConfig.data_bits = uartMsg.data_bits
|
||||||
|
dataViewerStore.uartConfig.stop_bits = uartMsg.stop_bits
|
||||||
|
dataViewerStore.uartConfig.parity = uartMsg.parity
|
||||||
|
break
|
||||||
|
}
|
||||||
|
case WtUartCmd.GET_DEFAULT_NUM:
|
||||||
|
uartStore.uartNum = (msg as IUartMsgNum).num
|
||||||
|
uart_get_baud(uartStore.uartNum)
|
||||||
|
uart_get_config(uartStore.uartNum)
|
||||||
|
break
|
||||||
|
default:
|
||||||
|
if (isDevMode()) {
|
||||||
|
console.log('uart not treated', msg)
|
||||||
|
}
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const onUartBinaryMsg = (msg: ApiBinaryMsg) => {
|
||||||
|
if (isDevMode()) {
|
||||||
|
console.log('uart', msg)
|
||||||
|
}
|
||||||
|
|
||||||
|
dataViewerStore.addSegment(new Uint8Array(msg.payload), true)
|
||||||
|
}
|
||||||
|
|
||||||
|
const onClientCtrl = (msg: api.ControlMsg) => {
|
||||||
|
if (msg.type !== api.ControlMsgType.WS_EVENT) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (msg.data === ControlEvent.DISCONNECTED) {
|
||||||
|
dataViewerStore.acceptIncomingData = false
|
||||||
|
} else if (msg.data === ControlEvent.CONNECTED) {
|
||||||
|
updateUartData()
|
||||||
|
dataViewerStore.acceptIncomingData = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
registerModule(api.WtModuleID.UART, {
|
||||||
|
ctrlCallback: onClientCtrl,
|
||||||
|
serverJsonMsgCallback: onUartJsonMsg,
|
||||||
|
serverBinMsgCallback: onUartBinaryMsg
|
||||||
|
})
|
||||||
|
}
|
|
@ -0,0 +1,99 @@
|
||||||
|
import {registerModule} from "@/router/msgRouter";
|
||||||
|
import {type ApiJsonMsg, ControlEvent, type ControlMsg, ControlMsgType, WtModuleID} from "@/api";
|
||||||
|
import {useUpdateStore} from "@/stores/useUpdateStore";
|
||||||
|
import {
|
||||||
|
type IOTAFmInfo,
|
||||||
|
type IOTAProgress,
|
||||||
|
WtOTACmd,
|
||||||
|
WtOTAProgressStatus,
|
||||||
|
wt_ota_get_progress,
|
||||||
|
wt_ota_get_update_info,
|
||||||
|
} from "@/api/apiOTA";
|
||||||
|
import {isDevMode} from "@/composables/buildMode";
|
||||||
|
import {useSystemStore} from "@/stores/useSystemStore";
|
||||||
|
|
||||||
|
export function useUpdateModule() {
|
||||||
|
const updateStore = useUpdateStore()
|
||||||
|
const sysStore = useSystemStore()
|
||||||
|
|
||||||
|
function onClientCtrl(msg: ControlMsg) {
|
||||||
|
if (msg.type !== ControlMsgType.WS_EVENT) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (msg.data === ControlEvent.CONNECTED) {
|
||||||
|
wt_ota_get_update_info();
|
||||||
|
wt_ota_get_progress();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function onClientMsg(msg: ApiJsonMsg) {
|
||||||
|
switch (msg.cmd as WtOTACmd) {
|
||||||
|
case WtOTACmd.WT_OTA_GET_UPDATE_INFO: {
|
||||||
|
const info = msg as IOTAFmInfo;
|
||||||
|
Object.assign(updateStore.newFmInfo, info);
|
||||||
|
if (updateStore.newFmInfo.fm_ver !== sysStore.curFmInfo.ver && updateStore.newFmInfo.fm_ver[0] !== '-'
|
||||||
|
&& (updateStore.updateStatus === 'IDLE' || updateStore.updateStatus === 'FAILED')) {
|
||||||
|
updateStore.canUpdate = true;
|
||||||
|
} else {
|
||||||
|
updateStore.canUpdate = false;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case WtOTACmd.WT_OTA_DO_UPDATE:
|
||||||
|
break;
|
||||||
|
case WtOTACmd.WT_OTA_GET_PROGRESS: {
|
||||||
|
const progress = msg as IOTAProgress;
|
||||||
|
updateStore.updateStatus = progress.status;
|
||||||
|
if (progress.total_size !== 0) {
|
||||||
|
updateStore.updateProgress = (progress.progress / progress.total_size) * 100;
|
||||||
|
} else {
|
||||||
|
updateStore.updateProgress = 0;
|
||||||
|
}
|
||||||
|
if (progress.status === WtOTAProgressStatus.IDLE) {
|
||||||
|
if (updateStore.newFmInfo.fm_ver !== sysStore.curFmInfo.ver && updateStore.newFmInfo.fm_ver[0] !== '-') {
|
||||||
|
updateStore.canUpdate = true;
|
||||||
|
} else {
|
||||||
|
updateStore.canUpdate = false;
|
||||||
|
}
|
||||||
|
updateStore.clearProgressInterval();
|
||||||
|
updateStore.progressBarStatus = '';
|
||||||
|
} else if (progress.status === WtOTAProgressStatus.FAILED) {
|
||||||
|
if (updateStore.newFmInfo.fm_ver !== sysStore.curFmInfo.ver && updateStore.newFmInfo.fm_ver[0] !== '-') {
|
||||||
|
updateStore.canUpdate = true;
|
||||||
|
} else {
|
||||||
|
updateStore.canUpdate = false;
|
||||||
|
}
|
||||||
|
updateStore.clearProgressInterval();
|
||||||
|
updateStore.progressBarStatus = 'exception';
|
||||||
|
} else if (progress.status === WtOTAProgressStatus.IN_PROGRESS) {
|
||||||
|
updateStore.setProgressInterval();
|
||||||
|
updateStore.progressBarStatus = '';
|
||||||
|
updateStore.canUpdate = false;
|
||||||
|
} else if (progress.status === WtOTAProgressStatus.OK) {
|
||||||
|
updateStore.clearProgressInterval();
|
||||||
|
updateStore.canUpdate = false;
|
||||||
|
updateStore.progressBarStatus = 'success';
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isDevMode()) {
|
||||||
|
console.log(msg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
registerModule(WtModuleID.OTA, {
|
||||||
|
ctrlCallback: onClientCtrl,
|
||||||
|
serverJsonMsgCallback: onClientMsg,
|
||||||
|
serverBinMsgCallback: () => {
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -2,7 +2,7 @@ import MyWorker from '@/composables/websocket/ws.sharedworker?sharedworker'
|
||||||
import {WebsocketWrapper} from "@/composables/websocket/websocketWrapper";
|
import {WebsocketWrapper} from "@/composables/websocket/websocketWrapper";
|
||||||
import {toClient, toClientCtrl, toServer} from "@/composables/broadcastChannelDef";
|
import {toClient, toClientCtrl, toServer} from "@/composables/broadcastChannelDef";
|
||||||
import type {ControlMsg, ServerMsg} from "@/api";
|
import type {ControlMsg, ServerMsg} from "@/api";
|
||||||
import {ControlEvent, ControlMsgType} from "@/api";
|
import {ControlMsgType} from "@/api";
|
||||||
import {isDevMode} from "@/composables/buildMode";
|
import {isDevMode} from "@/composables/buildMode";
|
||||||
|
|
||||||
export interface IWebsocketService {
|
export interface IWebsocketService {
|
||||||
|
@ -14,6 +14,7 @@ export interface IWebsocketService {
|
||||||
deinit(): void;
|
deinit(): void;
|
||||||
|
|
||||||
send(msg: ServerMsg): void;
|
send(msg: ServerMsg): void;
|
||||||
|
getSocketStatus(): void;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -82,6 +83,10 @@ class WebsocketShared implements IWebsocketService{
|
||||||
|
|
||||||
this.ctrlCallback(ev.data);
|
this.ctrlCallback(ev.data);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getSocketStatus() {
|
||||||
|
this.worker.port.postMessage({type: ControlMsgType.WS_GET_STATE} as ControlMsg)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class WebsocketClassic implements IWebsocketService{
|
class WebsocketClassic implements IWebsocketService{
|
||||||
|
@ -115,10 +120,14 @@ class WebsocketClassic implements IWebsocketService{
|
||||||
send(msg: ServerMsg): void {
|
send(msg: ServerMsg): void {
|
||||||
this.socket.send(msg);
|
this.socket.send(msg);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getSocketStatus(): void {
|
||||||
|
this.socket.getSocketStatus();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getWebsocketService(): IWebsocketService {
|
export function getWebsocketService(): IWebsocketService {
|
||||||
if (typeof SharedWorker !== 'undefined') {
|
if (typeof SharedWorker !== 'undefined' && typeof localStorage !== 'undefined') {
|
||||||
return WebsocketShared.getInstance();
|
return WebsocketShared.getInstance();
|
||||||
} else {
|
} else {
|
||||||
return WebsocketClassic.getInstance();
|
return WebsocketClassic.getInstance();
|
||||||
|
|
|
@ -1,5 +1,4 @@
|
||||||
|
import type {ControlMsg, ServerMsg} from "@/api";
|
||||||
import type {ApiJsonMsg, ControlMsg, ServerMsg} from "@/api";
|
|
||||||
import {ControlEvent, ControlMsgType} from "@/api";
|
import {ControlEvent, ControlMsgType} from "@/api";
|
||||||
import {isDevMode} from "@/composables/buildMode";
|
import {isDevMode} from "@/composables/buildMode";
|
||||||
|
|
||||||
|
@ -9,6 +8,8 @@ interface IWebsocket {
|
||||||
close(): void;
|
close(): void;
|
||||||
|
|
||||||
send(msg: ServerMsg): void;
|
send(msg: ServerMsg): void;
|
||||||
|
|
||||||
|
getSocketStatus(): void;
|
||||||
}
|
}
|
||||||
|
|
||||||
class WebsocketDummy implements IWebsocket {
|
class WebsocketDummy implements IWebsocket {
|
||||||
|
@ -20,6 +21,9 @@ class WebsocketDummy implements IWebsocket {
|
||||||
send(msg: ServerMsg) {
|
send(msg: ServerMsg) {
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getSocketStatus(): void {
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class OneTimeWebsocket implements IWebsocket {
|
class OneTimeWebsocket implements IWebsocket {
|
||||||
|
@ -61,6 +65,8 @@ class OneTimeWebsocket implements IWebsocket {
|
||||||
console.log("No heart beat, break connection");
|
console.log("No heart beat, break connection");
|
||||||
this.close();
|
this.close();
|
||||||
this.clear();
|
this.clear();
|
||||||
|
// } else if (this.socket.readyState === this.socket.CONNECTING) {
|
||||||
|
// this.close();
|
||||||
}
|
}
|
||||||
if (isDevMode()) {
|
if (isDevMode()) {
|
||||||
console.log("interval: ", this.heartBeatTimeCount, "state: ", this.socket.readyState);
|
console.log("interval: ", this.heartBeatTimeCount, "state: ", this.socket.readyState);
|
||||||
|
@ -159,6 +165,26 @@ class OneTimeWebsocket implements IWebsocket {
|
||||||
this.ctrlCallback(msg);
|
this.ctrlCallback(msg);
|
||||||
this.closeCallback();
|
this.closeCallback();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getSocketStatus() {
|
||||||
|
let type: ControlEvent;
|
||||||
|
switch (this.socket.readyState) {
|
||||||
|
case WebSocket.CONNECTING:
|
||||||
|
type = ControlEvent.CONNECTING;
|
||||||
|
break;
|
||||||
|
case WebSocket.OPEN:
|
||||||
|
type = ControlEvent.CONNECTED;
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
type = ControlEvent.DISCONNECTED;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
const msg: ControlMsg = {
|
||||||
|
type: ControlMsgType.WS_EVENT,
|
||||||
|
data: type,
|
||||||
|
};
|
||||||
|
this.ctrlCallback(msg);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export class WebsocketWrapper {
|
export class WebsocketWrapper {
|
||||||
|
@ -219,4 +245,8 @@ export class WebsocketWrapper {
|
||||||
send(msg: ServerMsg) {
|
send(msg: ServerMsg) {
|
||||||
this.socket.send(msg)
|
this.socket.send(msg)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getSocketStatus() {
|
||||||
|
this.socket.getSocketStatus();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,12 +1,11 @@
|
||||||
import type {ControlMsg, ServerMsg} from "@/api";
|
import type {ControlMsg, ServerMsg} from "@/api";
|
||||||
|
import {ControlEvent, ControlMsgType} from "@/api";
|
||||||
declare const self: SharedWorkerGlobalScope;
|
|
||||||
|
|
||||||
import {WebsocketWrapper} from "@/composables/websocket/websocketWrapper";
|
import {WebsocketWrapper} from "@/composables/websocket/websocketWrapper";
|
||||||
import {toClient, toClientCtrl, toServer} from "@/composables/broadcastChannelDef";
|
import {toClient, toClientCtrl, toServer} from "@/composables/broadcastChannelDef";
|
||||||
import {ControlEvent, ControlMsgType} from "@/api";
|
|
||||||
import {isDevMode} from "@/composables/buildMode";
|
import {isDevMode} from "@/composables/buildMode";
|
||||||
|
|
||||||
|
declare const self: SharedWorkerGlobalScope;
|
||||||
|
|
||||||
const websocket = new WebsocketWrapper();
|
const websocket = new WebsocketWrapper();
|
||||||
let host = "";
|
let host = "";
|
||||||
|
|
||||||
|
@ -30,6 +29,8 @@ self.onconnect = function(event) {
|
||||||
host = e.data.data;
|
host = e.data.data;
|
||||||
websocket.init(host, msgBroadcast, ctrlBroadcast);
|
websocket.init(host, msgBroadcast, ctrlBroadcast);
|
||||||
}
|
}
|
||||||
|
} else if (e.data.type === ControlMsgType.WS_GET_STATE) {
|
||||||
|
websocket.getSocketStatus();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
const msg: ControlMsg = {
|
const msg: ControlMsg = {
|
||||||
|
|
45
src/i18n.ts
|
@ -1,19 +1,52 @@
|
||||||
import {createI18n} from 'vue-i18n';
|
import {createI18n} from 'vue-i18n';
|
||||||
import zh from '@/locales/zh'
|
import zh from '@/locales/zh'
|
||||||
import en from '@/locales/en'
|
import en from '@/locales/en'
|
||||||
|
import fr from '@/locales/fr'
|
||||||
|
|
||||||
// const locale = localStorage.getItem('lang') || 'zh';
|
const userLanguage = navigator.language || 'en';
|
||||||
export const locale = 'zh';
|
|
||||||
|
// Get the language code (e.g., 'en' from 'en-US')
|
||||||
|
export const locale = userLanguage.split('-')[0];
|
||||||
|
const messages = {
|
||||||
|
zh,
|
||||||
|
en,
|
||||||
|
fr,
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
type Locale = keyof typeof messages;
|
||||||
|
|
||||||
|
export const availableLanguages = Object.keys(messages);
|
||||||
|
|
||||||
|
// export const locale = 'zh';
|
||||||
|
console.log(userLanguage, locale, availableLanguages)
|
||||||
|
|
||||||
const i18n = createI18n({
|
const i18n = createI18n({
|
||||||
globalInjection: true,
|
globalInjection: true,
|
||||||
legacy: false,
|
legacy: false,
|
||||||
locale: locale,
|
locale: locale,
|
||||||
fallbackLocale: 'zh',
|
fallbackLocale: 'zh',
|
||||||
messages: {
|
messages: messages
|
||||||
zh,
|
|
||||||
// en,
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
export function getFlagFromLang(lang: string) {
|
||||||
|
if (lang === 'zh') {
|
||||||
|
return '🇨🇳';
|
||||||
|
} else if (lang === 'en') {
|
||||||
|
return '🇺🇸';
|
||||||
|
} else if (lang === 'fr') {
|
||||||
|
return '🇫🇷';
|
||||||
|
}
|
||||||
|
return '🏳️';
|
||||||
|
}
|
||||||
|
|
||||||
|
export function setLang(lang: string): void {
|
||||||
|
if (availableLanguages.includes(lang)) {
|
||||||
|
i18n.global.locale.value = lang as Locale;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getLang() {
|
||||||
|
return i18n.global.locale;
|
||||||
|
}
|
||||||
|
|
||||||
export default i18n;
|
export default i18n;
|
||||||
|
|
|
@ -1,3 +1,235 @@
|
||||||
export default {
|
export default {
|
||||||
disconnected: "disconnected"
|
emoji: {
|
||||||
|
flag: "🇺🇸",
|
||||||
|
},
|
||||||
|
disconnected: "Disconnected",
|
||||||
|
connected: "Connected",
|
||||||
|
connecting: "Connecting",
|
||||||
|
use: "use",
|
||||||
|
author: "author",
|
||||||
|
studioYunSi: "Yunsi Studio",
|
||||||
|
authorEmail: "Author email",
|
||||||
|
TencentQQGroup: "QQ Group",
|
||||||
|
Discord: "Discord",
|
||||||
|
BiliBili: "BiliBili",
|
||||||
|
|
||||||
|
suggestion: "suggestion",
|
||||||
|
feature: "feature",
|
||||||
|
version: "Version",
|
||||||
|
releaseTime: "Release Time",
|
||||||
|
credit: "Credit",
|
||||||
|
aboutWebHost: "About the Web Host Application",
|
||||||
|
aboutDebugger: "About the Debugger",
|
||||||
|
officialWebsite: "Official Website",
|
||||||
|
email: "Email",
|
||||||
|
note: "Note",
|
||||||
|
welcomeMessage: "Welcome to reach out anytime",
|
||||||
|
serialNumber: "Serial Number",
|
||||||
|
|
||||||
|
ws: {
|
||||||
|
disconnected: "Disconnected",
|
||||||
|
connected: "Connected",
|
||||||
|
connecting: "Connecting",
|
||||||
|
},
|
||||||
|
|
||||||
|
page: {
|
||||||
|
home: "Home",
|
||||||
|
wifi: "Wi-Fi",
|
||||||
|
about: "About",
|
||||||
|
uart: "Uart",
|
||||||
|
widget: "Widget",
|
||||||
|
feedback: "Feedback",
|
||||||
|
close: "Close",
|
||||||
|
update: "Update",
|
||||||
|
fullscreen: "Fullscreen",
|
||||||
|
windowed: "Windowed"
|
||||||
|
},
|
||||||
|
|
||||||
|
uart: {
|
||||||
|
port: "Port",
|
||||||
|
startCommunication: "Start Communication",
|
||||||
|
stopCommunication: "Stop Communication",
|
||||||
|
commonlyUsed: "Common",
|
||||||
|
baudrate: "Baud Rate",
|
||||||
|
customBaud: "Custom Baud",
|
||||||
|
use: "Use",
|
||||||
|
actual: "Actual",
|
||||||
|
dataBits: "Data Bits",
|
||||||
|
stopBits: "Stop Bits",
|
||||||
|
parity: "Parity",
|
||||||
|
parityNone: "None",
|
||||||
|
parityOdd: "Odd",
|
||||||
|
parityEven: "Even",
|
||||||
|
flowControl: "Flow Control",
|
||||||
|
send: "Send",
|
||||||
|
clear: "Clear",
|
||||||
|
clearTooltip: "Only clears the display area, can be restored with refresh.",
|
||||||
|
updateTooltip: "Sync with cache + filter",
|
||||||
|
autoUpdateTooltip: "Only stop refreshing the display area; the background continues to receive data.",
|
||||||
|
receive: "Receive",
|
||||||
|
|
||||||
|
displayOptions: "Display Options",
|
||||||
|
display: "Display",
|
||||||
|
show: "Show",
|
||||||
|
text: "Text",
|
||||||
|
timestamp: "Timestamp",
|
||||||
|
enable: "Enable",
|
||||||
|
lineWrap: "Line Wrap",
|
||||||
|
highlight: "Highlight",
|
||||||
|
|
||||||
|
frameBreakStrategy: "Frame Break Strategy",
|
||||||
|
priority: "Priority",
|
||||||
|
rule: "Rule",
|
||||||
|
ruleTips:
|
||||||
|
"<p>Timeout=-1: Disable timeout frame break</p>" +
|
||||||
|
"<p>Timeout=0: Immediate break, any received data is considered complete</p>" +
|
||||||
|
"<p>Match after break: Typical \\n scenario</p>" +
|
||||||
|
"<p>Match before break: For scenarios with special frame headers</p>" +
|
||||||
|
"<p>Fixed byte frame break: Useful for large data transfer, e.g., break frame every 1024 bytes for easy data viewing</p>",
|
||||||
|
value: "Value",
|
||||||
|
timeout: "Timeout",
|
||||||
|
match: "Match",
|
||||||
|
byte: "Byte",
|
||||||
|
begin: "b",
|
||||||
|
end: "b",
|
||||||
|
|
||||||
|
other: "Other",
|
||||||
|
decodeAnsiEscapeCodes: "Decode ANSI Escape Codes",
|
||||||
|
ansiTooltips:
|
||||||
|
"<p>ANSI escape codes have many uses for terminals and text, such as changing text colors, among other effects.</p>\n" +
|
||||||
|
"<p>\n Learn more ->\n <a target=\"_blank\" href=\"https://en.wikipedia.org/wiki/ANSI_escape_code\">" +
|
||||||
|
"https://en.wikipedia.org/wiki/ANSI_escape_code\n </a></p>",
|
||||||
|
filter: "Filter",
|
||||||
|
textAndEscape: "Text with \\n\\x support",
|
||||||
|
autoUpdateNewData: "Auto-refresh new data",
|
||||||
|
updateFrequency: "Data Display Update Interval (ms)",
|
||||||
|
updateFrequencyTooltip: "Increasing the interval can reduce CPU usage.",
|
||||||
|
|
||||||
|
addHeader: "Add Header",
|
||||||
|
addFooter: "Add Footer",
|
||||||
|
|
||||||
|
passthrough: "Passthrough",
|
||||||
|
proxy: "Proxy",
|
||||||
|
serverPort: "Server Port",
|
||||||
|
connectedClient: "Connected Client",
|
||||||
|
refresh: "Refresh",
|
||||||
|
interface: "Interface",
|
||||||
|
noClientConnected: "No Client Connected",
|
||||||
|
|
||||||
|
import: "Import",
|
||||||
|
export: "Export",
|
||||||
|
reset: "Reset",
|
||||||
|
resetTooltip: "Takes effect after refreshing the page.",
|
||||||
|
saveToLocal: "Save to Local",
|
||||||
|
saveToLocalTooltip: "If multiple pages exist, they will overwrite each other.",
|
||||||
|
add: "Add",
|
||||||
|
edit: "Edit",
|
||||||
|
drag: "Drag",
|
||||||
|
ipChangeAlert: "Changing the IP address will cause the configuration to be lost.",
|
||||||
|
|
||||||
|
layout: "Layout",
|
||||||
|
landscape: "Landscape",
|
||||||
|
portrait: "Portrait",
|
||||||
|
responsive: "Responsive",
|
||||||
|
configPannel: "Config",
|
||||||
|
displayPannel: "Display",
|
||||||
|
macroPannel: "Quick Send",
|
||||||
|
autoScrollToBottom: "Auto Scroll",
|
||||||
|
clearScreen: "Clear",
|
||||||
|
autoUpdate: "Auto Update",
|
||||||
|
tempDisplayTooltip: "Data that does not meet the frame-break rules (e.g., not timed out) is temporarily displayed in real-time in this area. If it exceeds 8192 bytes, it will automatically break frames.",
|
||||||
|
loopSend: "Loop Send",
|
||||||
|
loopSendTooltip: "The actual frequency is affected by the interface refresh rate. For more accuracy, you can try turning off 'auto-refresh'.",
|
||||||
|
sendFormat: "Send Format",
|
||||||
|
cachedFrame: "Cached",
|
||||||
|
format: "Format",
|
||||||
|
},
|
||||||
|
|
||||||
|
wifi: {
|
||||||
|
settings: "Settings",
|
||||||
|
setFailed: "Settings failed to set",
|
||||||
|
setSuccess: "Settings saved",
|
||||||
|
connection: "Connection",
|
||||||
|
scanning: "Scanning",
|
||||||
|
scan: "Scan",
|
||||||
|
scanDone: "Scan done",
|
||||||
|
warnWifiName: "Enter Wi-Fi Name",
|
||||||
|
password: "Password",
|
||||||
|
connectInfoHTML: "Changing Wi-Fi will disconnect this interface from the passthrough device if not connected through its hotspot.",
|
||||||
|
connect: "Connect",
|
||||||
|
mode: "Mode",
|
||||||
|
save: "Save",
|
||||||
|
station: "Station",
|
||||||
|
intelligent: "Smart",
|
||||||
|
APOnly: "Hotspot Only",
|
||||||
|
disconnected: "Disconnected",
|
||||||
|
modeTipsHtml: "<p>\n" +
|
||||||
|
"<el-textsize=\"small\">Smart Mode:</el-text>\n" +
|
||||||
|
"After connecting to Wi-Fi, the hotspot will turn off automatically after 30 seconds if no device is connected. It will turn on after 5 seconds if disconnected from AP.\n" +
|
||||||
|
"</p>\n" +
|
||||||
|
"<p>\n" +
|
||||||
|
"<el-textsize=\"small\">Coexistence Mode:</el-text>\n" +
|
||||||
|
"Convenient but impacts stability and increases power consumption.\n" +
|
||||||
|
"</p>\n" +
|
||||||
|
"<p>\n" +
|
||||||
|
"<el-textsize=\"small\">Hotspot-Only Mode Drawback:</el-text>\n" +
|
||||||
|
"No network connection.\n" +
|
||||||
|
"</p>",
|
||||||
|
enabled: "Enabled",
|
||||||
|
disabled: "Disabled",
|
||||||
|
|
||||||
|
stationInfo: "Terminal (STA)",
|
||||||
|
hotspotInfo: "Hotspot (AP)",
|
||||||
|
signalStrength: "Signal Strength",
|
||||||
|
gateway: "Gateway",
|
||||||
|
netmask: "Netmask",
|
||||||
|
primaryDNS: "Primary DNS",
|
||||||
|
backupDNS: "Backup DNS",
|
||||||
|
IPmode: "IP Allocation Mode",
|
||||||
|
DNSmode: "DNS Mode",
|
||||||
|
internalAddress: "Internal Address",
|
||||||
|
|
||||||
|
autoIP: "Automatic (DHCP)",
|
||||||
|
staticIP: "Static IP",
|
||||||
|
autoDNS: "Automatic (Use Gateway)",
|
||||||
|
staticDNS: "Static DNS",
|
||||||
|
APauto_STA: "Smart Hotspot + Persistent Terminal (AP+STA)",
|
||||||
|
APonly: "Hotspot Only (AP)",
|
||||||
|
AP_STA: "Persistent Hotspot + Persistent Terminal (AP+STA)",
|
||||||
|
|
||||||
|
connectionSuccess: "Connection Successful",
|
||||||
|
enterAPName: "Entre the AP name",
|
||||||
|
debuggerNotConnected: "Debugger not connected",
|
||||||
|
},
|
||||||
|
|
||||||
|
widget: {
|
||||||
|
editGrid: 'Edit Grid',
|
||||||
|
editCell: 'Edit Cell',
|
||||||
|
loopWidget: 'Loop Widget',
|
||||||
|
loopWidgetDesc: 'Container for command sequences.',
|
||||||
|
addGrid: 'Add to Grid',
|
||||||
|
dataViewer: 'Data Viewer',
|
||||||
|
dataViewerDesc: 'Displays raw text data from UART.',
|
||||||
|
exportSettings: 'Export Settings',
|
||||||
|
importSettings: 'Import Settings',
|
||||||
|
resetToDefault: 'Reset to Default',
|
||||||
|
gridItemName: 'Widget name',
|
||||||
|
dropHere: 'Drop here',
|
||||||
|
run: 'Run',
|
||||||
|
loop: 'Loop',
|
||||||
|
delay: 'Delay',
|
||||||
|
addCommand: 'Add Command',
|
||||||
|
loopInterval: 'Loop Interval',
|
||||||
|
uartViewOnce: 'UART View Widget can only be added once.'
|
||||||
|
},
|
||||||
|
|
||||||
|
common: {
|
||||||
|
debuggerConnected: 'Debugger connected',
|
||||||
|
ok: 'OK'
|
||||||
|
},
|
||||||
|
|
||||||
|
navbar: {
|
||||||
|
navigationSidebar: 'Navigation Sidebar',
|
||||||
|
getSomeFries: "Let's go to the dock and get some fries",
|
||||||
|
},
|
||||||
};
|
};
|
|
@ -0,0 +1,236 @@
|
||||||
|
export default {
|
||||||
|
emoji: {
|
||||||
|
flag: "🇫🇷",
|
||||||
|
},
|
||||||
|
disconnected: "Déconnecté",
|
||||||
|
connected: "Connecté",
|
||||||
|
connecting: "Connexion..",
|
||||||
|
use: "utiliser",
|
||||||
|
author: "Auteur",
|
||||||
|
studioYunSi: "Studio Yunsi",
|
||||||
|
authorEmail: "Email de l'auteur",
|
||||||
|
TencentQQGroup: "Groupe QQ",
|
||||||
|
Discord: "Discord",
|
||||||
|
BiliBili: "BiliBili",
|
||||||
|
|
||||||
|
suggestion: "suggestion",
|
||||||
|
feature: "fonctionnalité",
|
||||||
|
version: "Version",
|
||||||
|
releaseTime: "Date de Publication",
|
||||||
|
credit: "Remerciements",
|
||||||
|
aboutWebHost: "À propos de l'Hôte Web",
|
||||||
|
aboutDebugger: "À propos du Débogueur",
|
||||||
|
officialWebsite: "Site Officiel",
|
||||||
|
email: "E-mail",
|
||||||
|
note: "Remarque",
|
||||||
|
welcomeMessage: "N'hésitez pas à venir nous solliciter.",
|
||||||
|
serialNumber: "Numéro de série",
|
||||||
|
|
||||||
|
ws: {
|
||||||
|
disconnected: "Déconnecté",
|
||||||
|
connected: "Connecté",
|
||||||
|
connecting: "Connexion..",
|
||||||
|
},
|
||||||
|
|
||||||
|
page: {
|
||||||
|
home: "Accueil",
|
||||||
|
wifi: "Wi-Fi",
|
||||||
|
about: "À propos",
|
||||||
|
uart: "Uart",
|
||||||
|
widget: "Widget",
|
||||||
|
feedback: "Feedback",
|
||||||
|
close: "Fermer",
|
||||||
|
update: "Mise à jour",
|
||||||
|
fullscreen: "Plein écran",
|
||||||
|
windowed: "Fenêtré",
|
||||||
|
},
|
||||||
|
|
||||||
|
uart: {
|
||||||
|
port: "Port",
|
||||||
|
startCommunication: "Démarrer la communication",
|
||||||
|
stopCommunication: "Arrêter la communication",
|
||||||
|
commonlyUsed: "Fréquemment utilisé",
|
||||||
|
baudrate: "Taux de Baud",
|
||||||
|
customBaud: "Baud",
|
||||||
|
use: "Utiliser",
|
||||||
|
actual: "Actuel",
|
||||||
|
dataBits: "Bits de Données",
|
||||||
|
stopBits: "Bits d'Arrêt",
|
||||||
|
parity: "Parité",
|
||||||
|
parityNone: "Aucune",
|
||||||
|
parityOdd: "Impair(Odd)",
|
||||||
|
parityEven: "Pair(Even)",
|
||||||
|
flowControl: "Contrôle de Flux",
|
||||||
|
send: "Envoyer",
|
||||||
|
clear: "Effacer",
|
||||||
|
clearTooltip: "Ne supprime que la zone d'affichage, peut être restaurée en actualisant.",
|
||||||
|
updateTooltip: "Synchroniser avec le cache + filtrer",
|
||||||
|
autoUpdateTooltip: "Arrête uniquement le rafraîchissement de la zone d'affichage ; l'arrière-plan continue de recevoir des données.",
|
||||||
|
receive: "Recevoir",
|
||||||
|
|
||||||
|
displayOptions: "Options d'Affichage",
|
||||||
|
display: "Affichage",
|
||||||
|
show: "Afficher",
|
||||||
|
text: "Texte",
|
||||||
|
timestamp: "Horodatage",
|
||||||
|
enable: "Activer",
|
||||||
|
lineWrap: "Retour à la Ligne",
|
||||||
|
highlight: "Surligner",
|
||||||
|
|
||||||
|
frameBreakStrategy: "Stratégie de Coupure de Trame",
|
||||||
|
priority: "Priorité",
|
||||||
|
rule: "Règle",
|
||||||
|
ruleTips:
|
||||||
|
"<p>Délai d'expiration=-1 : Désactiver la coupure de trame par délai d'expiration</p>" +
|
||||||
|
"<p>Délai d'expiration=0 : Coupure immédiate, toutes données reçues sont considérées complètes</p>" +
|
||||||
|
"<p>Match après coupure : Scénario typique \\n</p>" +
|
||||||
|
"<p>Match avant coupure : Pour des scénarios avec en-têtes de trame spécifiques</p>" +
|
||||||
|
"<p>Coupure de trame par octets fixes : Utile pour le transfert de grandes quantités de données, par exemple, couper la trame tous les 1024 octets pour faciliter la visualisation des données</p>",
|
||||||
|
value: "Valeur",
|
||||||
|
timeout: "Timeout",
|
||||||
|
match: "Match",
|
||||||
|
byte: "Byte",
|
||||||
|
begin: "b",
|
||||||
|
end: "b",
|
||||||
|
|
||||||
|
other: "Autres",
|
||||||
|
decodeAnsiEscapeCodes: "Décode Échappement ANSI",
|
||||||
|
ansiTooltips:
|
||||||
|
"<p>Les codes d'échappement ANSI ont de nombreuses utilisations pour les terminaux et le texte, comme changer les couleurs du texte, entre autres effets.</p>" +
|
||||||
|
"<p>\n En savoir plus ->\n " +
|
||||||
|
"<a target=\"_blank\" href=\"https://en.wikipedia.org/wiki/ANSI_escape_code\">\n" +
|
||||||
|
"https://en.wikipedia.org/wiki/ANSI_escape_code\n </a>\n</p>",
|
||||||
|
filter: "Filtrer",
|
||||||
|
textAndEscape: "Texte;supporte\\n\\x",
|
||||||
|
autoUpdateNewData: "Auto-update nouvelles données",
|
||||||
|
updateFrequency: "Délais rafraîchissement des Données (ms)",
|
||||||
|
updateFrequencyTooltip: "Augmenter l'intervalle peut réduire l'utilisation des ressources CPU.",
|
||||||
|
|
||||||
|
addHeader: "Ajouter un En-tête",
|
||||||
|
addFooter: "Ajouter un Pied de page",
|
||||||
|
|
||||||
|
passthrough: "Transmission",
|
||||||
|
proxy: "Proxy",
|
||||||
|
serverPort: "Port Serveur",
|
||||||
|
connectedClient: "Client Connecté",
|
||||||
|
refresh: "Rafraîchir",
|
||||||
|
interface: "Interface",
|
||||||
|
noClientConnected: "Aucun Client Connecté",
|
||||||
|
|
||||||
|
import: "Importer",
|
||||||
|
export: "Exporter",
|
||||||
|
reset: "Réinitialiser",
|
||||||
|
resetTooltip: "Prend effet après le rafraîchissement de la page.",
|
||||||
|
saveToLocal: "Enregistrer Localement",
|
||||||
|
saveToLocalTooltip: "S'il existe plusieurs pages, elles se chevaucheront mutuellement.",
|
||||||
|
add: "Ajouter",
|
||||||
|
edit: "Éditer",
|
||||||
|
drag: "Glisser",
|
||||||
|
ipChangeAlert: "Le changement d'adresse IP entraînera la perte de la configuration.",
|
||||||
|
|
||||||
|
layout: "Disposition",
|
||||||
|
landscape: "Paysage",
|
||||||
|
portrait: "Portrait",
|
||||||
|
responsive: "Résponsive",
|
||||||
|
configPannel: "Configuration",
|
||||||
|
displayPannel: "Données",
|
||||||
|
macroPannel: "Envoie Rapide",
|
||||||
|
autoScrollToBottom: "Auto Scroll",
|
||||||
|
clearScreen: "Effacer",
|
||||||
|
autoUpdate: "Auto Update",
|
||||||
|
tempDisplayTooltip: "es données qui ne respectent pas les règles de rupture de trame (par exemple : non expirées) s'affichent temporairement en temps réel dans cette zone. Au-delà de 8192 octets, une rupture de trame est automatique.",
|
||||||
|
loopSend: "Envoi en Boucle",
|
||||||
|
loopSendTooltip: "La fréquence réelle est influencée par le taux de rafraîchissement de l'interface. Pour plus de précision, vous pouvez essayer de désactiver 'l'actualisation automatique'.",
|
||||||
|
sendFormat: "Format d'Envoi",
|
||||||
|
cachedFrame: "Cache",
|
||||||
|
format: "Format",
|
||||||
|
},
|
||||||
|
|
||||||
|
wifi: {
|
||||||
|
settings: "Paramètres",
|
||||||
|
setFailed: "Echec d'enregistrement de paramètres",
|
||||||
|
setSuccess: "Paramètres enregistrés",
|
||||||
|
connection: "Connexion",
|
||||||
|
scanning: "Recherche en cours",
|
||||||
|
scan: "Rechercher",
|
||||||
|
scanDone: "Fin recherche de Wi-Fi",
|
||||||
|
warnWifiName: "Entrez le nom du Wi-Fi",
|
||||||
|
password: "Mot de passe",
|
||||||
|
connectInfoHTML: "Changer de Wi-Fi déconnectera cette interface du dispositif de transmission s'il ne passe pas par le point d'accès.",
|
||||||
|
connect: "Connecter",
|
||||||
|
mode: "Mode",
|
||||||
|
save: "Enregistrer",
|
||||||
|
station: "Station",
|
||||||
|
intelligent: "Intelligent",
|
||||||
|
APOnly: "Point d'accès uniquement",
|
||||||
|
disconnected: "Déconnecté",
|
||||||
|
modeTipsHtml: "<p>\n" +
|
||||||
|
"<el-textsize=\"small\">Mode intelligent :</el-text>\n" +
|
||||||
|
"Après la connexion au Wi-Fi, le point d'accès s'éteindra automatiquement après 30 secondes si aucun appareil n'est connecté. Il s'allumera après 5 secondes si la connexion AP est perdue.\n" +
|
||||||
|
"</p>\n" +
|
||||||
|
"<p>\n" +
|
||||||
|
"<el-textsize=\"small\">Mode coexistence :</el-text>\n" +
|
||||||
|
"Pratique mais réduit la stabilité et augmente la consommation d'énergie.\n" +
|
||||||
|
"</p>\n" +
|
||||||
|
"<p>\n" +
|
||||||
|
"<el-textsize=\"small\">Inconvénient du mode point d'accès seul :</el-text>\n" +
|
||||||
|
"Pas de connexion réseau.\n" +
|
||||||
|
"</p>",
|
||||||
|
enabled: "Activé",
|
||||||
|
disabled: "Désactivé",
|
||||||
|
|
||||||
|
stationInfo: "Terminal(STA)",
|
||||||
|
hotspotInfo: "Point d'Accès(AP)",
|
||||||
|
signalStrength: "Puissance du Signal",
|
||||||
|
gateway: "Passerelle",
|
||||||
|
netmask: "Masque de Sous-réseau",
|
||||||
|
primaryDNS: "DNS Primaire",
|
||||||
|
backupDNS: "DNS Secondaire",
|
||||||
|
IPmode: "Mode d'Attribution IP",
|
||||||
|
DNSmode: "Mode DNS",
|
||||||
|
internalAddress: "Adresse Interne",
|
||||||
|
|
||||||
|
autoIP: "Automatique (DHCP)",
|
||||||
|
staticIP: "IP Statique",
|
||||||
|
autoDNS: "Automatique (gateway)",
|
||||||
|
staticDNS: "DNS Statique",
|
||||||
|
APauto_STA: "Point d'Accès Intelligent + Terminal Permanent (AP+STA)",
|
||||||
|
APonly: "Point d'Accès Seul (AP)",
|
||||||
|
AP_STA: "Point d'Accès Permanent + Terminal Permanent (AP+STA)",
|
||||||
|
|
||||||
|
connectionSuccess: "Connexion Réussie",
|
||||||
|
enterAPName: "Entrez le nom du AP",
|
||||||
|
debuggerNotConnected: "Debugger non connecté",
|
||||||
|
},
|
||||||
|
|
||||||
|
widget: {
|
||||||
|
editGrid: 'Modifier la grille',
|
||||||
|
editCell: 'Modifier les cellules',
|
||||||
|
loopWidget: 'Widget Boucle',
|
||||||
|
loopWidgetDesc: 'Conteneur pour les séquences de commandes.',
|
||||||
|
addGrid: 'Ajouter à la grille',
|
||||||
|
dataViewer: 'Visualiseur de données',
|
||||||
|
dataViewerDesc: "Affiche les données texte brutes de l'UART.",
|
||||||
|
exportSettings: 'Exporter les paramètres',
|
||||||
|
importSettings: 'Importer les paramètres',
|
||||||
|
resetToDefault: 'Réinitialiser par défaut',
|
||||||
|
gridItemName: "Nom",
|
||||||
|
dropHere: 'Déposer ici',
|
||||||
|
run: 'Exécuter',
|
||||||
|
loop: 'Boucle',
|
||||||
|
delay: 'Délai',
|
||||||
|
addCommand: 'Ajouter une commande',
|
||||||
|
loopInterval: 'Intervalle de répétition',
|
||||||
|
uartViewOnce: "Le widget de vue UART ne peut être ajouté qu'une seule fois."
|
||||||
|
},
|
||||||
|
|
||||||
|
common: {
|
||||||
|
debuggerConnected: 'Débogueur connecté',
|
||||||
|
ok: 'OK'
|
||||||
|
},
|
||||||
|
|
||||||
|
navbar: {
|
||||||
|
navigationSidebar: 'Barre latérale de navigation',
|
||||||
|
getSomeFries: 'Allons au quai prendre des frites',
|
||||||
|
},
|
||||||
|
};
|
|
@ -7,8 +7,8 @@ type NestedKeyOf<ObjectType extends object> = {
|
||||||
: `${Key}`
|
: `${Key}`
|
||||||
}[keyof ObjectType & (string | number)];
|
}[keyof ObjectType & (string | number)];
|
||||||
|
|
||||||
type TranslationKeys = NestedKeyOf<typeof zh>;
|
export type TranslationKeys = NestedKeyOf<typeof zh>;
|
||||||
|
|
||||||
export function translate<K extends TranslationKeys>(key: K | string): string {
|
export function translate(key: TranslationKeys | string): string {
|
||||||
return i18n.global.t(key.toLowerCase());
|
return i18n.global.t(key);
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,7 +1,30 @@
|
||||||
export default {
|
export default {
|
||||||
|
emoji: {
|
||||||
|
flag: "🇨🇳",
|
||||||
|
},
|
||||||
disconnected: "未连接",
|
disconnected: "未连接",
|
||||||
connected: "已连接",
|
connected: "已连接",
|
||||||
connecting: "连接中",
|
connecting: "连接中",
|
||||||
|
use: "使用",
|
||||||
|
author: "作者",
|
||||||
|
studioYunSi: "允斯工作室",
|
||||||
|
authorEmail: "作者邮箱",
|
||||||
|
TencentQQGroup: "QQ群",
|
||||||
|
Discord: "Discord",
|
||||||
|
BiliBili: "哔哩哔哩",
|
||||||
|
|
||||||
|
suggestion: "建议",
|
||||||
|
feature: "需求",
|
||||||
|
version: "版本",
|
||||||
|
releaseTime: "发布时间",
|
||||||
|
credit: "鸣谢",
|
||||||
|
aboutWebHost: "关于网页版上位机",
|
||||||
|
aboutDebugger: "关于调试器",
|
||||||
|
officialWebsite: "官网",
|
||||||
|
email: "邮箱",
|
||||||
|
note: "备注",
|
||||||
|
welcomeMessage: "欢迎来打扰啊~",
|
||||||
|
serialNumber: "序列号",
|
||||||
|
|
||||||
ws: {
|
ws: {
|
||||||
disconnected: "未连接",
|
disconnected: "未连接",
|
||||||
|
@ -13,8 +36,204 @@ export default {
|
||||||
home: "主页",
|
home: "主页",
|
||||||
wifi: "Wi-Fi",
|
wifi: "Wi-Fi",
|
||||||
about: "关于",
|
about: "关于",
|
||||||
uart: "UART透传",
|
uart: "UART",
|
||||||
|
widget: "组件",
|
||||||
feedback: "反馈",
|
feedback: "反馈",
|
||||||
close: "关闭",
|
close: "关闭",
|
||||||
|
update: "更新",
|
||||||
|
fullscreen: "全屏",
|
||||||
|
windowed: "窗口",
|
||||||
|
},
|
||||||
|
|
||||||
|
uart: {
|
||||||
|
port: "接口",
|
||||||
|
startCommunication: "开始数据收发",
|
||||||
|
stopCommunication: "停止数据收发",
|
||||||
|
commonlyUsed: "常用",
|
||||||
|
baudrate: "波特率",
|
||||||
|
customBaud: "自定义波特率",
|
||||||
|
use: "使用",
|
||||||
|
actual: "实际",
|
||||||
|
dataBits: "数据位",
|
||||||
|
stopBits: "停止位",
|
||||||
|
parity: "校验位",
|
||||||
|
parityNone: "无(None)",
|
||||||
|
parityOdd: "奇(Odd)",
|
||||||
|
parityEven: "偶(Even)",
|
||||||
|
flowControl: "流控制",
|
||||||
|
send: "发送",
|
||||||
|
clear: "清空",
|
||||||
|
clearTooltip: "仅清除显示区域,可用刷新恢复",
|
||||||
|
updateTooltip: "与缓存同步+过滤",
|
||||||
|
autoUpdateTooltip: "仅停止刷新显示区,后台继续接收数据",
|
||||||
|
receive: "接收",
|
||||||
|
|
||||||
|
displayOptions: "显示选项",
|
||||||
|
display: "显示框",
|
||||||
|
show: "显示",
|
||||||
|
text: "文本",
|
||||||
|
timestamp: "时间戳",
|
||||||
|
enable: "启用",
|
||||||
|
lineWrap: "换行",
|
||||||
|
highlight: "高亮",
|
||||||
|
|
||||||
|
frameBreakStrategy: "断帧策略",
|
||||||
|
priority: "优先级",
|
||||||
|
rule: "规则",
|
||||||
|
ruleTips:
|
||||||
|
"<p>超时=-1: 禁用超时断帧</p>" +
|
||||||
|
"<p>超时=0: 当机立断,收到任何数据都视为完整数据</p>" +
|
||||||
|
"<p>匹配断后:典型\\n的场景</p>" +
|
||||||
|
"<p>匹配断前:用于有特殊帧头的场景</p>" +
|
||||||
|
"<p>固定字节断帧:传输大量数据,比如可以每隔1024字节断帧,方便查看数据</p>",
|
||||||
|
value: "值",
|
||||||
|
timeout: "超时",
|
||||||
|
match: "匹配",
|
||||||
|
byte: "字节",
|
||||||
|
begin: "断",
|
||||||
|
end: "断",
|
||||||
|
|
||||||
|
other: "其他",
|
||||||
|
decodeAnsiEscapeCodes: "解码ANSI转义码",
|
||||||
|
ansiTooltips:
|
||||||
|
"<p>ANSI转义码对终端和文本有很多作用,比如改变文本颜色等。</p>\n" +
|
||||||
|
"<p>\n" +
|
||||||
|
" 简单了解->\n" +
|
||||||
|
" <a target=\"_blank\" href=\"https://yunsi.studio/wireless-debugger/docs/uart-webhost/ansi-escape-code\">\n" +
|
||||||
|
" https://yunsi.studio/wireless-debugger/docs/uart-webhost/ansi-escape-code\n" +
|
||||||
|
" </a>\n" +
|
||||||
|
"</p>",
|
||||||
|
filter: "过滤",
|
||||||
|
textAndEscape: "文本,支持\\n\\x",
|
||||||
|
autoUpdateNewData: "新数据自动刷新",
|
||||||
|
updateFrequency: "数据显示刷新间隔(ms)",
|
||||||
|
updateFrequencyTooltip: "提高间隔可减少CPU资源的使用",
|
||||||
|
|
||||||
|
addHeader: "增加帧头",
|
||||||
|
addFooter: "增加帧尾",
|
||||||
|
|
||||||
|
passthrough: "透传",
|
||||||
|
proxy: "透传",
|
||||||
|
serverPort: "服务器端口",
|
||||||
|
connectedClient: "已连接的客户端",
|
||||||
|
refresh: "刷新",
|
||||||
|
interface: "接口",
|
||||||
|
noClientConnected: "无客户端连接",
|
||||||
|
|
||||||
|
import: "导入",
|
||||||
|
export: "导出",
|
||||||
|
reset: "重置",
|
||||||
|
resetTooltip: "刷新页面后生效",
|
||||||
|
saveToLocal: "保存到本地",
|
||||||
|
saveToLocalTooltip: "若存在多个页面,会相互覆盖",
|
||||||
|
add: "添加",
|
||||||
|
edit: "编辑",
|
||||||
|
drag: "拖拽",
|
||||||
|
ipChangeAlert: "IP地址改变会导致配置丢失",
|
||||||
|
|
||||||
|
layout: "布局",
|
||||||
|
landscape: "横/行",
|
||||||
|
portrait: "竖/列",
|
||||||
|
responsive: "自适应",
|
||||||
|
configPannel: "设置窗",
|
||||||
|
displayPannel: "数据窗",
|
||||||
|
macroPannel: "快捷窗",
|
||||||
|
autoScrollToBottom: "自动滚动到底部",
|
||||||
|
clearScreen: "清屏",
|
||||||
|
autoUpdate: "自动刷新",
|
||||||
|
tempDisplayTooltip: "未满足断帧规则的数据(如:未超时),暂时实时显示在此区域。超过8192字节,自动断帧;",
|
||||||
|
loopSend: "循环发送",
|
||||||
|
loopSendTooltip: "实际频率受界面刷新率影响,如需要更精确,可以尝试关闭'自动刷新'",
|
||||||
|
sendFormat: "发送格式",
|
||||||
|
cachedFrame: "缓存帧数",
|
||||||
|
format: "格式化",
|
||||||
|
},
|
||||||
|
|
||||||
|
wifi: {
|
||||||
|
settings: "配置",
|
||||||
|
setFailed: "设置失败",
|
||||||
|
setSuccess: "配置成功",
|
||||||
|
connection: "连接",
|
||||||
|
scanning: "扫描中",
|
||||||
|
scan: "扫描",
|
||||||
|
scanDone: "扫描成功",
|
||||||
|
warnWifiName: "请输入WIFI名",
|
||||||
|
password: "密码",
|
||||||
|
connectInfoHTML: "如果不是通过透传器的热点连接,更换Wi-Fi将导致此界面与透传器断开连接。",
|
||||||
|
connect: "连接",
|
||||||
|
mode: "模式",
|
||||||
|
save: "保存",
|
||||||
|
station: "终端",
|
||||||
|
intelligent: "智能",
|
||||||
|
APOnly: "仅开启热点",
|
||||||
|
disconnected: "未连接",
|
||||||
|
modeTipsHtml: "<p>\n" +
|
||||||
|
"<el-textsize=\"small\">智能模式:</el-text>\n" +
|
||||||
|
"成功连接至Wi-Fi后,如果此设备的热点未被其他设备连接,将在30秒后自动关闭热点;如果此设备与AP断开连接,将在5秒后自动开启热点\n" +
|
||||||
|
"</p>\n" +
|
||||||
|
"<p>\n" +
|
||||||
|
"<el-textsize=\"small\">热点+终端共存模式:</el-text>\n" +
|
||||||
|
"方便使用,但是影响稳定性,增加功耗\n" +
|
||||||
|
"</p>\n" +
|
||||||
|
"<p>\n" +
|
||||||
|
"<el-textsize=\"small\">单热点模式缺点:</el-text>\n" +
|
||||||
|
"无网络\n" +
|
||||||
|
"</p>",
|
||||||
|
enabled: "已开启",
|
||||||
|
disabled: "未开启",
|
||||||
|
|
||||||
|
stationInfo: "终端(STA)",
|
||||||
|
hotspotInfo: "自发热点(AP)",
|
||||||
|
signalStrength: "信号强度",
|
||||||
|
gateway: "网关",
|
||||||
|
netmask: "掩码",
|
||||||
|
primaryDNS: "首选DNS",
|
||||||
|
backupDNS: "备用DNS",
|
||||||
|
IPmode: "IP分配模式",
|
||||||
|
DNSmode: "DNS模式",
|
||||||
|
internalAddress: "内网地址",
|
||||||
|
|
||||||
|
autoIP: "自动 (DHCP)",
|
||||||
|
staticIP: "静态IP",
|
||||||
|
autoDNS: "自动 (使用网关)",
|
||||||
|
staticDNS: "静态DNS",
|
||||||
|
APauto_STA: "智能热点+常开终端 (AP+STA)",
|
||||||
|
APonly: "仅开启热点 (AP)",
|
||||||
|
AP_STA: "常开热点+常开终端 (AP+STA)",
|
||||||
|
|
||||||
|
connectionSuccess: "连接成功",
|
||||||
|
enterAPName: "请输入AP名",
|
||||||
|
debuggerNotConnected: "调试器未连接",
|
||||||
|
},
|
||||||
|
|
||||||
|
widget: {
|
||||||
|
editGrid: '编辑网格',
|
||||||
|
editCell: '编辑单元',
|
||||||
|
loopWidget: '循环小部件',
|
||||||
|
loopWidgetDesc: '用于命令序列的容器。',
|
||||||
|
addGrid: '添加到网格',
|
||||||
|
dataViewer: '数据显示器',
|
||||||
|
dataViewerDesc: '显示来自UART的原始文本数据。',
|
||||||
|
exportSettings: '导出设置',
|
||||||
|
importSettings: '导入设置',
|
||||||
|
resetToDefault: '重置',
|
||||||
|
gridItemName: '组件名称',
|
||||||
|
dropHere: '在此处放置',
|
||||||
|
run: '运行',
|
||||||
|
loop: '循环',
|
||||||
|
delay: '延迟',
|
||||||
|
addCommand: '添加命令',
|
||||||
|
loopInterval: '循环间隔',
|
||||||
|
uartViewOnce: 'UART视图组件只能添加一次。'
|
||||||
|
},
|
||||||
|
|
||||||
|
common: {
|
||||||
|
debuggerConnected: '调试器已连接',
|
||||||
|
ok: '好的'
|
||||||
|
},
|
||||||
|
|
||||||
|
navbar: {
|
||||||
|
navigationSidebar: '导航侧栏',
|
||||||
|
getSomeFries: '走,去码头整点薯条',
|
||||||
},
|
},
|
||||||
}
|
}
|
|
@ -4,6 +4,8 @@ import '@/assets/page.css'
|
||||||
import '@/assets/navigation.css'
|
import '@/assets/navigation.css'
|
||||||
import 'element-plus/dist/index.css';
|
import 'element-plus/dist/index.css';
|
||||||
|
|
||||||
|
import 'vuetify/styles'
|
||||||
|
import { createVuetify } from 'vuetify'
|
||||||
|
|
||||||
import { createApp } from 'vue'
|
import { createApp } from 'vue'
|
||||||
import { createPinia } from 'pinia'
|
import { createPinia } from 'pinia'
|
||||||
|
@ -17,5 +19,6 @@ const app = createApp(App)
|
||||||
app.use(createPinia())
|
app.use(createPinia())
|
||||||
app.use(i18n);
|
app.use(i18n);
|
||||||
app.use(router)
|
app.use(router)
|
||||||
|
app.use(createVuetify())
|
||||||
|
|
||||||
app.mount('#app')
|
app.mount('#app')
|
||||||
|
|
|
@ -1,54 +1,101 @@
|
||||||
import {createRouter, createWebHistory} from 'vue-router'
|
import {createRouter, createWebHashHistory, type RouteLocationNormalizedLoaded} from 'vue-router'
|
||||||
import Home from '@/views/Home.vue'
|
|
||||||
import Wifi from '@/views/Wifi.vue'
|
import Wifi from '@/views/Wifi.vue'
|
||||||
import Feedback from '@/views/Feedback.vue'
|
import Feedback from '@/views/Feedback.vue'
|
||||||
import About from '@/views/About.vue'
|
import About from '@/views/About.vue'
|
||||||
import Uart from '@/views/Uart.vue'
|
import Uart from '@/views/Uart.vue'
|
||||||
import Page404 from '@/views/404.vue'
|
import Page404 from '@/views/404.vue'
|
||||||
|
import Update from '@/views/Update.vue'
|
||||||
|
import WidgetPannel from '@/views/WidgetPannel.vue'
|
||||||
import {translate} from "@/locales";
|
import {translate} from "@/locales";
|
||||||
|
import {isOTAEnabled} from "@/composables/buildMode";
|
||||||
|
import {reactive, watch} from "vue";
|
||||||
|
import {getLang} from "@/i18n";
|
||||||
|
|
||||||
|
const languageState = reactive({
|
||||||
|
lang: getLang()
|
||||||
|
});
|
||||||
|
|
||||||
|
interface AppRouteMeta {
|
||||||
|
title?: string;
|
||||||
|
titleKey?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const updateMetaTitles = () => {
|
||||||
|
router.getRoutes().forEach(route => {
|
||||||
|
const meta = route.meta as AppRouteMeta;
|
||||||
|
if (meta.titleKey) {
|
||||||
|
meta.title = translate(meta.titleKey);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
function updateDocumentTitle(route: RouteLocationNormalizedLoaded) {
|
||||||
|
const meta = route.meta as AppRouteMeta;
|
||||||
|
document.title = typeof route.meta.title === 'string'
|
||||||
|
? `${translate(meta.titleKey || "")} | ${translate('studioYunSi')}`
|
||||||
|
: '允斯调试器';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Watch for language changes to update the titles dynamically
|
||||||
|
watch(() => languageState.lang, () => {
|
||||||
|
// Recompute all route meta titles
|
||||||
|
updateMetaTitles();
|
||||||
|
updateDocumentTitle(router.currentRoute.value);
|
||||||
|
}, {deep: true});
|
||||||
|
|
||||||
const router = createRouter({
|
const router = createRouter({
|
||||||
history: createWebHistory(import.meta.env.BASE_URL),
|
history: createWebHashHistory(import.meta.env.BASE_URL),
|
||||||
routes: [
|
routes: [
|
||||||
{
|
{
|
||||||
path: '/',
|
path: '/',
|
||||||
name: 'home',
|
name: 'home',
|
||||||
meta: {title: translate("page.home")},
|
meta: { titleKey: 'page.home' },
|
||||||
// component: Wifi
|
redirect: () => '/uart',
|
||||||
redirect: () => '/wifi',
|
|
||||||
}, {
|
}, {
|
||||||
path: '/home:ext(.*)',
|
path: '/home:ext(.*)',
|
||||||
meta: {title: translate("page.home")},
|
meta: { titleKey: 'page.home' },
|
||||||
redirect: () => '/',
|
redirect: () => '/',
|
||||||
}, {
|
}, {
|
||||||
path: '/wifi:ext(.*)',
|
path: '/wifi:ext(.*)',
|
||||||
meta: {title: translate('page.wifi')},
|
meta: { titleKey: 'page.wifi' },
|
||||||
component: Wifi,
|
component: Wifi,
|
||||||
}, {
|
}, {
|
||||||
path: '/about:ext(.*)',
|
path: '/about:ext(.*)',
|
||||||
meta: {title: translate('page.about')},
|
meta: { titleKey: 'page.about' },
|
||||||
component: About,
|
component: About,
|
||||||
}, {
|
}, {
|
||||||
path: '/uart:ext(.*)',
|
path: '/uart:ext(.*)',
|
||||||
meta: {title: translate('page.uart')},
|
meta: { titleKey: 'page.uart' },
|
||||||
component: Uart,
|
component: Uart,
|
||||||
|
}, {
|
||||||
|
path: '/widget:ext(.*)',
|
||||||
|
meta: { titleKey: 'page.widget' },
|
||||||
|
component: WidgetPannel,
|
||||||
}, {
|
}, {
|
||||||
path: '/feedback:ext(.*)',
|
path: '/feedback:ext(.*)',
|
||||||
meta: {title: translate('page.feedback')},
|
meta: { titleKey: 'page.feedback' },
|
||||||
name: 'feedback',
|
name: 'feedback',
|
||||||
component: Feedback,
|
component: Feedback,
|
||||||
}, {
|
}, {
|
||||||
path: '/:catchAll(.*)', // This will match all paths that aren't matched by above routes
|
path: '/update:ext(.*)',
|
||||||
|
meta: { titleKey: 'page.update' },
|
||||||
|
name: 'update',
|
||||||
|
component: isOTAEnabled() ? Update : Page404,
|
||||||
|
}, {
|
||||||
|
path: '/:catchAll(.*)', // Catch-all route for 404
|
||||||
name: 'NotFound',
|
name: 'NotFound',
|
||||||
component: Page404,
|
component: Page404,
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// Update document title dynamically
|
||||||
router.beforeEach((to, from, next) => {
|
router.beforeEach((to, from, next) => {
|
||||||
document.title = typeof to.meta.title === 'string' ? to.meta.title + " | 允斯工作室" : '允斯调试器';
|
updateDocumentTitle(to);
|
||||||
next();
|
next();
|
||||||
});
|
});
|
||||||
|
|
||||||
export default router
|
// Initialize titles on load
|
||||||
|
updateMetaTitles();
|
||||||
|
|
||||||
|
export default router;
|
|
@ -12,6 +12,9 @@ const moduleMap = new Map<number, IModuleCallback>();
|
||||||
|
|
||||||
export function registerModule(moduleId: number, moduleCallback: IModuleCallback): boolean {
|
export function registerModule(moduleId: number, moduleCallback: IModuleCallback): boolean {
|
||||||
if (moduleMap.has(moduleId)) {
|
if (moduleMap.has(moduleId)) {
|
||||||
|
if (isDevMode()) {
|
||||||
|
console.log("module ", moduleId, "already registered");
|
||||||
|
}
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,11 @@
|
||||||
|
import {defineStore} from "pinia";
|
||||||
|
import {type Ref, ref} from "vue";
|
||||||
|
import type {InstanceInfo} from "@/api/apiDataFlow";
|
||||||
|
|
||||||
|
export const useDataFlowStore = defineStore('data_flow', () => {
|
||||||
|
const instanceList: Ref<InstanceInfo[]> = ref([]);
|
||||||
|
|
||||||
|
return {
|
||||||
|
instanceList,
|
||||||
|
}
|
||||||
|
});
|
|
@ -0,0 +1,28 @@
|
||||||
|
import {defineStore} from "pinia";
|
||||||
|
import {ref} from "vue";
|
||||||
|
|
||||||
|
export const useSystemStore = defineStore('system', () => {
|
||||||
|
|
||||||
|
const curFmInfo = ref({
|
||||||
|
ver: "-",
|
||||||
|
date: "-",
|
||||||
|
});
|
||||||
|
|
||||||
|
const hwInfo = ref({
|
||||||
|
ver: "-",
|
||||||
|
date: "-",
|
||||||
|
})
|
||||||
|
|
||||||
|
const sys_info = ref({
|
||||||
|
sn: "-",
|
||||||
|
});
|
||||||
|
|
||||||
|
const rebootInProgress = ref(false);
|
||||||
|
|
||||||
|
return {
|
||||||
|
curFmInfo,
|
||||||
|
hwInfo,
|
||||||
|
sysInfo: sys_info,
|
||||||
|
rebootInProgress,
|
||||||
|
}
|
||||||
|
});
|
|
@ -0,0 +1,8 @@
|
||||||
|
import { ref } from 'vue'
|
||||||
|
import { defineStore } from 'pinia'
|
||||||
|
|
||||||
|
export const useUartStore = defineStore('uart', () => {
|
||||||
|
const uartNum = ref(1);
|
||||||
|
|
||||||
|
return { uartNum }
|
||||||
|
})
|
|
@ -0,0 +1,45 @@
|
||||||
|
import { ref } from 'vue'
|
||||||
|
import { defineStore } from 'pinia'
|
||||||
|
import {wt_ota_get_progress} from "@/api/apiOTA";
|
||||||
|
|
||||||
|
export const useUpdateStore = defineStore('update', () => {
|
||||||
|
const canUpdate = ref(false);
|
||||||
|
const updateProgress = ref(0);
|
||||||
|
const updateStatus = ref('');
|
||||||
|
|
||||||
|
const progressBarStatus = ref('');
|
||||||
|
|
||||||
|
let progressIntervalID = -1;
|
||||||
|
|
||||||
|
const newFmInfo = ref({
|
||||||
|
fm_size: 0,
|
||||||
|
fm_ver: "-",
|
||||||
|
upd_date: "-",
|
||||||
|
upd_note: "-",
|
||||||
|
})
|
||||||
|
|
||||||
|
function setProgressInterval() {
|
||||||
|
if (progressIntervalID < 0) {
|
||||||
|
progressIntervalID = setInterval(() => {
|
||||||
|
wt_ota_get_progress();
|
||||||
|
}, 1000);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function clearProgressInterval() {
|
||||||
|
if (progressIntervalID >= 0) {
|
||||||
|
clearInterval(progressIntervalID);
|
||||||
|
progressIntervalID = -1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
canUpdate,
|
||||||
|
updateProgress,
|
||||||
|
updateStatus,
|
||||||
|
progressBarStatus,
|
||||||
|
newFmInfo,
|
||||||
|
setProgressInterval,
|
||||||
|
clearProgressInterval,
|
||||||
|
}
|
||||||
|
})
|
|
@ -0,0 +1,424 @@
|
||||||
|
import { computed, markRaw, ref, toRaw, watch } from 'vue'
|
||||||
|
import { defineStore } from 'pinia'
|
||||||
|
import type { WidgetItem } from '@/types/grid'
|
||||||
|
import { globalNotify } from '@/composables/notification'
|
||||||
|
import { ElMessageBox } from 'element-plus'
|
||||||
|
import UartAtCommand from '@/views/widgets/uartAtCommand.vue'
|
||||||
|
import WidgetLoop from '@/views/widgets/widgetLoop.vue'
|
||||||
|
import textDataViewer from '@/views/text-data-viewer/textDataViewer.vue'
|
||||||
|
|
||||||
|
const componentMap: { [key: string]: any } = {
|
||||||
|
WidgetLoop,
|
||||||
|
textDataViewer,
|
||||||
|
UartAtCommand
|
||||||
|
}
|
||||||
|
|
||||||
|
const getComponentName = (component: any): string | null => {
|
||||||
|
const rawComponent = toRaw(component)
|
||||||
|
for (const name in componentMap) {
|
||||||
|
if (componentMap[name] === rawComponent) {
|
||||||
|
return name
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
const getDefaultLayout = (): WidgetItem[] => [
|
||||||
|
{
|
||||||
|
x: 0,
|
||||||
|
y: 0,
|
||||||
|
w: 10,
|
||||||
|
h: 10,
|
||||||
|
i: 0,
|
||||||
|
name: 'Widget A',
|
||||||
|
static: false,
|
||||||
|
widget: markRaw(WidgetLoop),
|
||||||
|
widgetProps: [
|
||||||
|
{
|
||||||
|
id: 1,
|
||||||
|
componentType: markRaw(UartAtCommand),
|
||||||
|
props: { label: 'Device ID', command: 'AT+ID?', response: 'ID:xxxx' }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 2,
|
||||||
|
componentType: markRaw(UartAtCommand),
|
||||||
|
props: { label: 'Version', command: 'AT+VER?', response: 'V1.0.0' }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 3,
|
||||||
|
componentType: markRaw(UartAtCommand),
|
||||||
|
props: { label: 'Reset', command: 'AT+RESET', response: 'OK' }
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
x: 10,
|
||||||
|
y: 0,
|
||||||
|
w: 10,
|
||||||
|
h: 10,
|
||||||
|
i: 1,
|
||||||
|
name: 'Widget B',
|
||||||
|
static: false,
|
||||||
|
widget: markRaw(WidgetLoop),
|
||||||
|
widgetProps: [
|
||||||
|
{
|
||||||
|
id: 1,
|
||||||
|
componentType: markRaw(UartAtCommand),
|
||||||
|
props: {
|
||||||
|
label: 'Scan WiFi',
|
||||||
|
command: 'AT+WSCANasdfasdfasdf',
|
||||||
|
response: 'SCAN OKasd fsdaf asdf asdf asdf asdf '
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 2,
|
||||||
|
componentType: markRaw(UartAtCommand),
|
||||||
|
props: {
|
||||||
|
label: 'Connect WiFi',
|
||||||
|
command: 'AT+WCONN=ssid,pwd',
|
||||||
|
response: 'CONN OK'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
x: 0,
|
||||||
|
y: 10,
|
||||||
|
w: 10,
|
||||||
|
h: 10,
|
||||||
|
i: 2,
|
||||||
|
name: 'Widget C',
|
||||||
|
static: false,
|
||||||
|
widget: markRaw(WidgetLoop),
|
||||||
|
widgetProps: [
|
||||||
|
{
|
||||||
|
id: 1,
|
||||||
|
componentType: markRaw(UartAtCommand),
|
||||||
|
props: {
|
||||||
|
label: 'Ping Test',
|
||||||
|
command: 'AT+PING=google.com',
|
||||||
|
response: 'PING OK'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
x: 10,
|
||||||
|
y: 10,
|
||||||
|
w: 10,
|
||||||
|
h: 10,
|
||||||
|
i: 3,
|
||||||
|
name: 'Widget D',
|
||||||
|
static: false,
|
||||||
|
widget: markRaw(textDataViewer),
|
||||||
|
widgetProps: []
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
const throttle = (fn: Function, wait: number) => {
|
||||||
|
let inThrottle: boolean, lastFn: number, lastTime: number
|
||||||
|
return function (this: any, ...args: any[]) {
|
||||||
|
const context = this
|
||||||
|
if (!inThrottle) {
|
||||||
|
fn.apply(context, args)
|
||||||
|
lastTime = Date.now()
|
||||||
|
inThrottle = true
|
||||||
|
} else {
|
||||||
|
clearTimeout(lastFn)
|
||||||
|
lastFn = window.setTimeout(() => {
|
||||||
|
if (Date.now() - lastTime >= wait) {
|
||||||
|
fn.apply(context, args)
|
||||||
|
lastTime = Date.now()
|
||||||
|
}
|
||||||
|
}, Math.max(wait - (Date.now() - lastTime), 0))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const useWidgetStore = defineStore('widget', () => {
|
||||||
|
const layout = ref<WidgetItem[]>(getDefaultLayout())
|
||||||
|
|
||||||
|
const editCell = ref(false)
|
||||||
|
const editGrid = ref(false)
|
||||||
|
const showOptions = ref(true)
|
||||||
|
|
||||||
|
const isUartViewAdded = computed(() =>
|
||||||
|
layout.value.some((item) => toRaw(item.widget) === textDataViewer)
|
||||||
|
)
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => editGrid.value,
|
||||||
|
(newValue) => {
|
||||||
|
if (newValue) {
|
||||||
|
editCell.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => editCell.value,
|
||||||
|
(newValue) => {
|
||||||
|
if (newValue) {
|
||||||
|
editGrid.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
const getNextId = (): number => {
|
||||||
|
const numericIds = layout.value.map((item) => Number(item.i)).filter((id) => !isNaN(id))
|
||||||
|
if (numericIds.length === 0) {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
return Math.max(...numericIds) + 1
|
||||||
|
}
|
||||||
|
|
||||||
|
const addLoopWidget = () => {
|
||||||
|
const nextId = getNextId()
|
||||||
|
let y = 0
|
||||||
|
if (layout.value.length > 0) {
|
||||||
|
y = Math.max(...layout.value.map((item) => item.y + item.h))
|
||||||
|
}
|
||||||
|
const newWidget: WidgetItem = {
|
||||||
|
x: 0,
|
||||||
|
y: y,
|
||||||
|
w: 10,
|
||||||
|
h: 5,
|
||||||
|
i: nextId,
|
||||||
|
name: `New Loop Widget`,
|
||||||
|
static: false,
|
||||||
|
widget: markRaw(WidgetLoop),
|
||||||
|
widgetProps: []
|
||||||
|
}
|
||||||
|
layout.value.push(newWidget)
|
||||||
|
}
|
||||||
|
|
||||||
|
const addUartViewWidget = () => {
|
||||||
|
if (isUartViewAdded.value) {
|
||||||
|
globalNotify('UART View Widget can only be added once.', 'warning')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const nextId = getNextId()
|
||||||
|
let y = 0
|
||||||
|
if (layout.value.length > 0) {
|
||||||
|
y = Math.max(...layout.value.map((item) => item.y + item.h))
|
||||||
|
}
|
||||||
|
|
||||||
|
const newWidget: WidgetItem = {
|
||||||
|
x: 0,
|
||||||
|
y: y,
|
||||||
|
w: 10,
|
||||||
|
h: 10,
|
||||||
|
i: nextId,
|
||||||
|
name: 'UART Data Viewer',
|
||||||
|
static: false,
|
||||||
|
widget: markRaw(textDataViewer),
|
||||||
|
widgetProps: []
|
||||||
|
}
|
||||||
|
layout.value.push(newWidget)
|
||||||
|
}
|
||||||
|
|
||||||
|
const deleteWidget = (index: number) => {
|
||||||
|
layout.value.splice(index, 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
const resetToDefault = () => {
|
||||||
|
ElMessageBox.confirm(
|
||||||
|
'This will reset your layout to the default settings. Are you sure?',
|
||||||
|
'Warning',
|
||||||
|
{
|
||||||
|
confirmButtonText: 'OK',
|
||||||
|
cancelButtonText: 'Cancel',
|
||||||
|
type: 'warning'
|
||||||
|
}
|
||||||
|
)
|
||||||
|
.then(() => {
|
||||||
|
layout.value = getDefaultLayout()
|
||||||
|
globalNotify('Layout reset to default.', 'success')
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
globalNotify('Layout reset cancelled.', 'info')
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const exportSettings = () => {
|
||||||
|
try {
|
||||||
|
const layoutToSave = toRaw(layout.value).map((item) => {
|
||||||
|
const rawItem = toRaw(item)
|
||||||
|
return {
|
||||||
|
...rawItem,
|
||||||
|
widget: getComponentName(rawItem.widget),
|
||||||
|
widgetProps: rawItem.widgetProps?.map((prop: any) => {
|
||||||
|
const rawProp = toRaw(prop)
|
||||||
|
return {
|
||||||
|
...rawProp,
|
||||||
|
componentType: getComponentName(rawProp.componentType)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const dataStr = JSON.stringify(layoutToSave, null, 2)
|
||||||
|
const dataUri = 'data:application/json;charset=utf-8,' + encodeURIComponent(dataStr)
|
||||||
|
|
||||||
|
const exportFileDefaultName = 'at-command-settings.json'
|
||||||
|
|
||||||
|
const linkElement = document.createElement('a')
|
||||||
|
linkElement.setAttribute('href', dataUri)
|
||||||
|
linkElement.setAttribute('download', exportFileDefaultName)
|
||||||
|
linkElement.click()
|
||||||
|
globalNotify('Settings exported successfully.', 'success')
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to export settings:', error)
|
||||||
|
globalNotify('Failed to export settings.', 'error')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const importSettings = () => {
|
||||||
|
const input = document.createElement('input')
|
||||||
|
input.type = 'file'
|
||||||
|
input.accept = '.json'
|
||||||
|
input.onchange = (e) => {
|
||||||
|
const file = (e.target as HTMLInputElement).files?.[0]
|
||||||
|
if (!file) return
|
||||||
|
|
||||||
|
const reader = new FileReader()
|
||||||
|
reader.onload = (event) => {
|
||||||
|
try {
|
||||||
|
const fileContent = event.target?.result as string
|
||||||
|
const parsedLayout = JSON.parse(fileContent)
|
||||||
|
|
||||||
|
// Basic validation
|
||||||
|
if (!Array.isArray(parsedLayout)) {
|
||||||
|
throw new Error('Invalid format: expected an array of widgets.')
|
||||||
|
}
|
||||||
|
|
||||||
|
const newLayout = parsedLayout
|
||||||
|
.map((item: any) => {
|
||||||
|
if (item.widget && componentMap[item.widget]) {
|
||||||
|
item.widget = markRaw(componentMap[item.widget])
|
||||||
|
} else {
|
||||||
|
console.warn(`Unknown widget type "${item.widget}" during import. Skipping item.`)
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
if (item.widgetProps) {
|
||||||
|
item.widgetProps.forEach((prop: any) => {
|
||||||
|
if (prop.componentType && componentMap[prop.componentType]) {
|
||||||
|
prop.componentType = markRaw(componentMap[prop.componentType])
|
||||||
|
} else if (prop.componentType) {
|
||||||
|
console.warn(
|
||||||
|
`Unknown componentType "${prop.componentType}" for widget "${item.name}". It will be ignored.`
|
||||||
|
)
|
||||||
|
prop.componentType = null
|
||||||
|
}
|
||||||
|
})
|
||||||
|
item.widgetProps = item.widgetProps.filter((prop: any) => prop.componentType)
|
||||||
|
}
|
||||||
|
return item
|
||||||
|
})
|
||||||
|
.filter(Boolean) // remove null items
|
||||||
|
|
||||||
|
layout.value = newLayout
|
||||||
|
globalNotify('Settings imported successfully.', 'success')
|
||||||
|
} catch (error: any) {
|
||||||
|
console.error('Failed to import settings:', error)
|
||||||
|
globalNotify(`Failed to import settings: ${error.message}`, 'error')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
reader.readAsText(file)
|
||||||
|
}
|
||||||
|
input.click()
|
||||||
|
}
|
||||||
|
|
||||||
|
const saveLayoutToLocalStorage = () => {
|
||||||
|
try {
|
||||||
|
const layoutToSave = toRaw(layout.value).map((item) => {
|
||||||
|
const rawItem = toRaw(item)
|
||||||
|
return {
|
||||||
|
...rawItem,
|
||||||
|
widget: getComponentName(rawItem.widget),
|
||||||
|
widgetProps: rawItem.widgetProps?.map((prop: any) => {
|
||||||
|
const rawProp = toRaw(prop)
|
||||||
|
return {
|
||||||
|
...rawProp,
|
||||||
|
componentType: getComponentName(rawProp.componentType)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
localStorage.setItem('at-command-layout', JSON.stringify(layoutToSave))
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to save layout to localStorage:', error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const loadLayoutFromLocalStorage = () => {
|
||||||
|
const savedLayoutJSON = localStorage.getItem('at-command-layout')
|
||||||
|
if (!savedLayoutJSON) return
|
||||||
|
|
||||||
|
try {
|
||||||
|
const parsedLayout = JSON.parse(savedLayoutJSON)
|
||||||
|
|
||||||
|
if (!Array.isArray(parsedLayout)) {
|
||||||
|
throw new Error('Invalid format in localStorage: expected an array of widgets.')
|
||||||
|
}
|
||||||
|
|
||||||
|
const newLayout = parsedLayout
|
||||||
|
.map((item: any) => {
|
||||||
|
if (item.widget && componentMap[item.widget]) {
|
||||||
|
item.widget = markRaw(componentMap[item.widget])
|
||||||
|
} else {
|
||||||
|
console.warn(`Unknown widget type "${item.widget}" from localStorage. Skipping item.`)
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
if (item.widgetProps) {
|
||||||
|
item.widgetProps.forEach((prop: any) => {
|
||||||
|
if (prop.componentType && componentMap[prop.componentType]) {
|
||||||
|
prop.componentType = markRaw(componentMap[prop.componentType])
|
||||||
|
} else if (prop.componentType) {
|
||||||
|
console.warn(
|
||||||
|
`Unknown componentType "${prop.componentType}" for widget "${item.name}" from localStorage. It will be ignored.`
|
||||||
|
)
|
||||||
|
prop.componentType = null
|
||||||
|
}
|
||||||
|
})
|
||||||
|
item.widgetProps = item.widgetProps.filter((prop: any) => prop.componentType)
|
||||||
|
}
|
||||||
|
return item
|
||||||
|
})
|
||||||
|
.filter(Boolean) // remove null items
|
||||||
|
|
||||||
|
layout.value = newLayout
|
||||||
|
globalNotify('Settings restored from last session.', 'success')
|
||||||
|
} catch (error: any) {
|
||||||
|
console.error('Failed to load layout from localStorage:', error)
|
||||||
|
globalNotify(`Failed to load settings from localStorage: ${error.message}`, 'error')
|
||||||
|
localStorage.removeItem('at-command-layout')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
watch(
|
||||||
|
layout,
|
||||||
|
throttle(() => saveLayoutToLocalStorage(), 1000),
|
||||||
|
{ deep: true }
|
||||||
|
)
|
||||||
|
|
||||||
|
loadLayoutFromLocalStorage()
|
||||||
|
|
||||||
|
return {
|
||||||
|
layout,
|
||||||
|
editGrid,
|
||||||
|
editCell,
|
||||||
|
showOptions,
|
||||||
|
isUartViewAdded,
|
||||||
|
addLoopWidget,
|
||||||
|
addUartViewWidget,
|
||||||
|
deleteWidget,
|
||||||
|
resetToDefault,
|
||||||
|
exportSettings,
|
||||||
|
importSettings,
|
||||||
|
getNextId
|
||||||
|
}
|
||||||
|
})
|
|
@ -0,0 +1,26 @@
|
||||||
|
import type { Component } from 'vue'
|
||||||
|
|
||||||
|
export interface UartCommandData {
|
||||||
|
command: string
|
||||||
|
label: string
|
||||||
|
response: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface DraggableComponent<T = Record<string, any>> {
|
||||||
|
id: number
|
||||||
|
componentType: Component | string
|
||||||
|
props: T
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface WidgetItem {
|
||||||
|
x: number
|
||||||
|
y: number
|
||||||
|
w: number
|
||||||
|
h: number
|
||||||
|
i: number
|
||||||
|
name: string
|
||||||
|
static: boolean
|
||||||
|
widget: Component | string
|
||||||
|
widgetIconName?: string
|
||||||
|
widgetProps: DraggableComponent[]
|
||||||
|
}
|
|
@ -1,22 +1,25 @@
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
|
import {useSystemStore} from "@/stores/useSystemStore";
|
||||||
|
import {translate} from "@/locales";
|
||||||
|
|
||||||
const version = import.meta.env.VITE_APP_GIT_TAG || "v0.0.0";
|
const version = import.meta.env.VITE_APP_GIT_TAG || "v0.0.0";
|
||||||
const compileTime = import.meta.env.VITE_APP_LAST_COMMIT || "1970-00-00";
|
const compileTime = import.meta.env.VITE_APP_LAST_COMMIT || "1970-00-00";
|
||||||
|
const sysStore = useSystemStore();
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="text-layout">
|
<div class="text-layout">
|
||||||
<el-divider></el-divider>
|
<el-divider></el-divider>
|
||||||
<el-divider>关于</el-divider>
|
<el-divider>{{ translate('page.about') }}</el-divider>
|
||||||
<el-divider></el-divider>
|
<el-divider></el-divider>
|
||||||
<el-collapse>
|
<el-collapse>
|
||||||
<el-collapse-item title="关于网页版上位机">
|
<el-collapse-item :title="translate('aboutWebHost')">
|
||||||
<el-descriptions border :column="1" class="mt-5 description-style">
|
<el-descriptions border :column="1" class="mt-5 description-style">
|
||||||
<el-descriptions-item label="版本">{{ version }}</el-descriptions-item>
|
<el-descriptions-item :label="translate('version')">{{ version }}</el-descriptions-item>
|
||||||
<el-descriptions-item label="发布时间">{{ compileTime }}</el-descriptions-item>
|
<el-descriptions-item :label="translate('releaseTime')">{{ compileTime }}</el-descriptions-item>
|
||||||
<el-descriptions-item label="许可证">MIT</el-descriptions-item>
|
|
||||||
</el-descriptions>
|
</el-descriptions>
|
||||||
|
|
||||||
<el-descriptions title="鸣谢" border :column="1" class="mt-5 description-style">
|
<el-descriptions :title="translate('credit')" border :column="1" class="mt-5 description-style">
|
||||||
<el-descriptions-item label="vuejs"><a target="_blank" href="https://github.com/vuejs/vue/blob/main/LICENSE">MIT</a>
|
<el-descriptions-item label="vuejs"><a target="_blank" href="https://github.com/vuejs/vue/blob/main/LICENSE">MIT</a>
|
||||||
</el-descriptions-item>
|
</el-descriptions-item>
|
||||||
<el-descriptions-item label="typescript"><a
|
<el-descriptions-item label="typescript"><a
|
||||||
|
@ -40,13 +43,15 @@ const compileTime = import.meta.env.VITE_APP_LAST_COMMIT || "1970-00-00";
|
||||||
</el-descriptions-item>
|
</el-descriptions-item>
|
||||||
</el-descriptions>
|
</el-descriptions>
|
||||||
</el-collapse-item>
|
</el-collapse-item>
|
||||||
<el-collapse-item title="关于下位机">
|
<el-collapse-item :title="translate('aboutDebugger')">
|
||||||
<el-descriptions border :column="1" class="mt-5 description-style">
|
<el-descriptions border :column="1" class="mt-5 description-style">
|
||||||
<el-descriptions-item label="官网"><a target="_blank" href="https://yunsi.studio/wireless-proxy">允斯工作室</a></el-descriptions-item>
|
<el-descriptions-item :label="translate('officialWebsite')"><a target="_blank" href="https://yunsi.studio/wireless-debugger">https://yunsi.studio/wireless-debugger</a></el-descriptions-item>
|
||||||
<el-descriptions-item label="版本">-</el-descriptions-item>
|
<el-descriptions-item :label="translate('version')">{{ sysStore.curFmInfo.ver }}</el-descriptions-item>
|
||||||
|
<el-descriptions-item :label="translate('releaseTime')">{{ sysStore.curFmInfo.date }}</el-descriptions-item>
|
||||||
|
<el-descriptions-item :label="translate('serialNumber')">{{ sysStore.sysInfo.sn }}</el-descriptions-item>
|
||||||
</el-descriptions>
|
</el-descriptions>
|
||||||
|
|
||||||
<el-descriptions title="鸣谢" border :column="1" class="mt-5 description-style">
|
<el-descriptions :title="translate('credit')" border :column="1" class="mt-5 description-style">
|
||||||
<el-descriptions-item label="windowsair"><a target="_blank" href="https://github.com/windowsair/wireless-esp8266-dap">wireless-esp8266-dap</a>
|
<el-descriptions-item label="windowsair"><a target="_blank" href="https://github.com/windowsair/wireless-esp8266-dap">wireless-esp8266-dap</a>
|
||||||
</el-descriptions-item>
|
</el-descriptions-item>
|
||||||
</el-descriptions>
|
</el-descriptions>
|
||||||
|
@ -54,22 +59,22 @@ const compileTime = import.meta.env.VITE_APP_LAST_COMMIT || "1970-00-00";
|
||||||
</el-collapse>
|
</el-collapse>
|
||||||
|
|
||||||
|
|
||||||
<el-descriptions title="作者:空空(kerms)" border :column="1" class="mt-5 description-style">
|
<el-descriptions :title="translate('author') + ' :空空(kerms)'" border :column="1" class="mt-5 description-style">
|
||||||
<el-descriptions-item label="官网"><a target="_blank" href="https://yunsi.studio/">允斯工作室(https://yunsi.studio/)</a></el-descriptions-item>
|
<el-descriptions-item :label="translate('officialWebsite')"><a target="_blank" href="https://yunsi.studio/">https://yunsi.studio/</a></el-descriptions-item>
|
||||||
<el-descriptions-item label="github"><a target="_blank" href="https://github.com/kerms">https://github.com/kerms</a>
|
<el-descriptions-item label="github"><a target="_blank" href="https://github.com/kerms">https://github.com/kerms</a>
|
||||||
</el-descriptions-item>
|
</el-descriptions-item>
|
||||||
<el-descriptions-item label="邮箱">kerms@niazo.org</el-descriptions-item>
|
<el-descriptions-item :label="translate('email')">kerms@niazo.org</el-descriptions-item>
|
||||||
<el-descriptions-item label="BiliBili"><a target="_blank" href="https://space.bilibili.com/3461571571353885">3461571571353885</a>
|
<el-descriptions-item label="BiliBili"><a target="_blank" href="https://space.bilibili.com/3461571571353885">3461571571353885</a>
|
||||||
</el-descriptions-item>
|
</el-descriptions-item>
|
||||||
<el-descriptions-item label="QQ群">642246000</el-descriptions-item>
|
<el-descriptions-item :label="translate('TencentQQGroup')">642246000</el-descriptions-item>
|
||||||
<el-descriptions-item label="备注">欢迎大家来打扰啊~</el-descriptions-item>
|
<el-descriptions-item :label="translate('note')">{{ translate('welcomeMessage') }}</el-descriptions-item>
|
||||||
</el-descriptions>
|
</el-descriptions>
|
||||||
</div>
|
</div>
|
||||||
<el-divider></el-divider>
|
<el-divider></el-divider>
|
||||||
|
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped lang="postcss">
|
||||||
.description-style :deep(.el-descriptions__label) {
|
.description-style :deep(.el-descriptions__label) {
|
||||||
@apply w-32
|
@apply w-32
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,12 +2,13 @@
|
||||||
|
|
||||||
<div class="text-layout">
|
<div class="text-layout">
|
||||||
<el-divider></el-divider>
|
<el-divider></el-divider>
|
||||||
<el-divider>反馈</el-divider>
|
<el-divider>{{ translate('page.feedback') }}</el-divider>
|
||||||
<el-divider></el-divider>
|
<el-divider></el-divider>
|
||||||
|
|
||||||
<el-descriptions title="反馈/建议/需要新功能" border :column="1">
|
<el-descriptions :title="translate('page.feedback') + '/' + translate('suggestion') + '/' + translate('feature')" border :column="1">
|
||||||
<el-descriptions-item label="QQ群">642246000</el-descriptions-item>
|
<el-descriptions-item :label="translate('TencentQQGroup')">642246000</el-descriptions-item>
|
||||||
<el-descriptions-item label="作者邮箱">kerms@niazo.org</el-descriptions-item>
|
<el-descriptions-item :label="translate('authorEmail')">kerms@niazo.org</el-descriptions-item>
|
||||||
|
<!-- TODO: add discord + BiliBili / instagram ? -->
|
||||||
</el-descriptions>
|
</el-descriptions>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
@ -16,5 +17,5 @@
|
||||||
|
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
|
import {translate} from "@/locales";
|
||||||
</script>
|
</script>
|
||||||
|
|
|
@ -1,9 +1,480 @@
|
||||||
|
<template>
|
||||||
|
<div class="button-m-0 messages-container flex flex-grow overflow-hidden" :class="{'flex-col': store.winLayoutMode ==='col'}">
|
||||||
|
<div v-show="store.winLeft.show" ref="win1Ref" class="bg-gray-50 flex-shrink-0 overflow-auto"
|
||||||
|
:class="{
|
||||||
|
'max-w-60': store.winLayoutMode==='row', 'xl:max-w-80': store.winLayoutMode==='row',
|
||||||
|
'min-w-60': store.winLayoutMode==='row', 'xl:min-w-80': store.winLayoutMode==='row'
|
||||||
|
}"
|
||||||
|
>
|
||||||
|
<text-data-config></text-data-config>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-show="store.winLeft.show && (winDataView.show || store.winRight.show)" ref="firstWinResizeRef"></div>
|
||||||
|
|
||||||
|
<div v-show="winDataView.show" class="flex flex-col flex-grow overflow-hidden p-2">
|
||||||
|
<textDataViewer :showDataConfig="store.winLeft.show"></textDataViewer>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-show="winDataView.show && store.winRight.show" ref="thirdWinResizeRef"></div>
|
||||||
|
|
||||||
|
<div v-show="store.winRight.show" ref="win2Ref" :class="{
|
||||||
|
'max-w-80': store.winLayoutMode==='row', 'xl:max-w-96': store.winLayoutMode==='row',
|
||||||
|
'min-w-80': store.winLayoutMode==='row', 'xl:min-w-96': store.winLayoutMode==='row'
|
||||||
|
}"
|
||||||
|
class="bg-gray-50 flex flex-col flex-shrink-0 min-h-32 overflow-auto p-2">
|
||||||
|
<TextDataMacro @winSizeRefresh="handleWinSizeRefresh"></TextDataMacro>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<teleport to="#page-spec-slot">
|
||||||
|
<div>
|
||||||
|
<el-popover
|
||||||
|
placement="bottom"
|
||||||
|
trigger="click"
|
||||||
|
:hide-after="0"
|
||||||
|
transition="none"
|
||||||
|
>
|
||||||
|
<div class="button-m-0 flex flex-col space-y-2">
|
||||||
|
<div class="custom-style flex justify-center">
|
||||||
|
<el-segmented v-model="store.winLayoutMode" :options="layoutOptions" size="small"/>
|
||||||
|
</div>
|
||||||
|
<el-checkbox v-model="store.winAutoLayout" border size="small"
|
||||||
|
:disabled="store.winLayoutMode==='col'">
|
||||||
|
{{ $t('uart.responsive') }}
|
||||||
|
</el-checkbox>
|
||||||
|
<el-checkbox v-model="store.winLeft.show" border size="small" :disabled="store.winAutoLayout">
|
||||||
|
{{ $t("uart.configPannel") }}
|
||||||
|
</el-checkbox>
|
||||||
|
<el-checkbox v-model="winDataView.show" border size="small" :disabled="store.winAutoLayout">
|
||||||
|
{{ $t('uart.displayPannel') }}
|
||||||
|
</el-checkbox>
|
||||||
|
<el-checkbox v-model="store.winRight.show" border size="small" :disabled="store.winAutoLayout">
|
||||||
|
{{ $t('uart.macroPannel') }}
|
||||||
|
</el-checkbox>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<template #reference>
|
||||||
|
<el-button class="min-h-full" type="primary" :size="layoutConf.isMedium ? 'small' : 'default'">
|
||||||
|
{{ $t('uart.layout') }}
|
||||||
|
</el-button>
|
||||||
|
</template>
|
||||||
|
</el-popover>
|
||||||
|
</div>
|
||||||
|
<div class="mx-1"></div>
|
||||||
|
</teleport>
|
||||||
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
|
import {computed, onMounted, onUnmounted, reactive, type Ref, ref, type UnwrapRef, watch} from "vue";
|
||||||
|
import {breakpointsTailwind, useBreakpoints} from '@vueuse/core'
|
||||||
|
import {useDataViewerStore} from '@/stores/dataViewerStore';
|
||||||
|
import * as api from '@/api';
|
||||||
|
import {ControlEvent} from '@/api';
|
||||||
|
import {
|
||||||
|
type IUartMsgBaud,
|
||||||
|
type IUartMsgConfig,
|
||||||
|
type IUartMsgNum,
|
||||||
|
uart_get_baud,
|
||||||
|
uart_get_config,
|
||||||
|
uart_get_default_num,
|
||||||
|
WtUartCmd
|
||||||
|
} from '@/api/apiUart';
|
||||||
|
|
||||||
|
/* TODO: use https://antoniandre.github.io/splitpanes/ */
|
||||||
|
|
||||||
|
import { type ApiBinaryMsg } from '@/api/binDataDef'
|
||||||
|
import * as df from '@/api/apiDataFlow'
|
||||||
|
import textDataViewer from '@/views/text-data-viewer/textDataViewer.vue'
|
||||||
|
import textDataConfig from '@/views/text-data-viewer/textDataConfig.vue'
|
||||||
|
import { registerModule } from '@/router/msgRouter'
|
||||||
|
import { isDevMode } from '@/composables/buildMode'
|
||||||
|
import { useWsStore } from '@/stores/websocket'
|
||||||
|
import { useUartStore } from '@/stores/useUartStore'
|
||||||
|
import TextDataMacro from '@/views/text-data-viewer/textDataMacro.vue'
|
||||||
|
import { translate } from '@/locales'
|
||||||
|
import { useUartModule } from '@/composables/useUartModule'
|
||||||
|
|
||||||
|
|
||||||
|
const store = useDataViewerStore()
|
||||||
|
const wsStore = useWsStore()
|
||||||
|
const uartStore = useUartStore()
|
||||||
|
|
||||||
|
const firstWinResizeRef = ref(document.body);
|
||||||
|
const thirdWinResizeRef = ref(document.body);
|
||||||
|
const win1Ref = ref(document.body);
|
||||||
|
const win2Ref = ref(document.body);
|
||||||
|
|
||||||
|
const breakpoints = useBreakpoints(breakpointsTailwind)
|
||||||
|
|
||||||
|
const layoutConf = reactive({
|
||||||
|
isSmall: breakpoints.smaller("sm"),
|
||||||
|
isMedium: breakpoints.smaller("lg"),
|
||||||
|
});
|
||||||
|
|
||||||
|
const layoutOptions = computed(() => [{
|
||||||
|
label: translate("uart.landscape"),
|
||||||
|
value: 'row'
|
||||||
|
}, {
|
||||||
|
label: translate("uart.portrait"),
|
||||||
|
value: 'col'
|
||||||
|
}]);
|
||||||
|
|
||||||
|
interface WinProperty {
|
||||||
|
show: boolean;
|
||||||
|
width: string;
|
||||||
|
height: string;
|
||||||
|
borderSize: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
const winDataView = reactive({
|
||||||
|
show: true,
|
||||||
|
})
|
||||||
|
|
||||||
|
const ctx = reactive({
|
||||||
|
curResizeTarget: "none",
|
||||||
|
curHeightOffset: 0,
|
||||||
|
});
|
||||||
|
|
||||||
|
function updateCursor(i: HTMLElement) {
|
||||||
|
if (store.winLayoutMode === 'row') {
|
||||||
|
i.style.cursor = "col-resize";
|
||||||
|
} else {
|
||||||
|
i.style.cursor = "row-resize";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateWin(r: Ref<HTMLElement>, p: UnwrapRef<WinProperty>) {
|
||||||
|
if (store.winLayoutMode === 'row') {
|
||||||
|
r.value.style.minHeight = "";
|
||||||
|
r.value.style.maxHeight = ""
|
||||||
|
if (winDataView.show) {
|
||||||
|
r.value.style.minWidth = p.width;
|
||||||
|
r.value.style.maxWidth = p.width;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
r.value.style.minWidth = ""
|
||||||
|
r.value.style.maxWidth = ""
|
||||||
|
if (winDataView.show) {
|
||||||
|
r.value.style.minHeight = p.height;
|
||||||
|
r.value.style.maxHeight = p.height;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateCursors() {
|
||||||
|
updateCursor(firstWinResizeRef.value);
|
||||||
|
updateCursor(thirdWinResizeRef.value);
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateResizer() {
|
||||||
|
updateCursors();
|
||||||
|
updateWin(win1Ref, store.winLeft);
|
||||||
|
updateWin(win2Ref, store.winRight);
|
||||||
|
}
|
||||||
|
|
||||||
|
function mouseResize(e: MouseEvent) {
|
||||||
|
const curTarget = e.target as HTMLElement
|
||||||
|
|
||||||
|
if (store.winLayoutMode === 'row') {
|
||||||
|
let f = e.clientX;
|
||||||
|
if (ctx.curResizeTarget === "first") {
|
||||||
|
win1Ref.value.style.minWidth = f + "px";
|
||||||
|
win1Ref.value.style.maxWidth = f + "px";
|
||||||
|
} else {
|
||||||
|
if (isDevMode()) {
|
||||||
|
console.log("Row clientX", e.clientX, "clientY", e.clientY,
|
||||||
|
"layerX", e.layerX, "layerY", e.layerY, "offsetX", e.offsetX, "offsetY", e.offsetY,
|
||||||
|
"pageX", e.pageX, "pageY", e.pageY, win2Ref.value.clientHeight);
|
||||||
|
}
|
||||||
|
win2Ref.value.style.minWidth = document.body.scrollWidth - f - store.winRight.borderSize + "px";
|
||||||
|
win2Ref.value.style.maxWidth = document.body.scrollWidth - f - store.winRight.borderSize + "px";
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
/* col mode */
|
||||||
|
let f = e.clientY;
|
||||||
|
if (ctx.curResizeTarget === "first") {
|
||||||
|
win1Ref.value.style.minHeight = f - ctx.curHeightOffset + "px";
|
||||||
|
win1Ref.value.style.maxHeight = f - ctx.curHeightOffset + "px";
|
||||||
|
} else {
|
||||||
|
if (isDevMode()) {
|
||||||
|
console.log("Col clientX", e.clientX, "clientY", e.clientY,
|
||||||
|
"layerX", e.layerX, "layerY", e.layerY, "offsetX", e.offsetX, "offsetY", e.offsetY,
|
||||||
|
"pageX", e.pageX, "pageY", e.pageY, curTarget.offsetWidth, ctx.curHeightOffset);
|
||||||
|
}
|
||||||
|
win2Ref.value.style.minHeight = ctx.curHeightOffset - f + "px";
|
||||||
|
win2Ref.value.style.maxHeight = ctx.curHeightOffset - f + "px";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function touchResize(e: TouchEvent) {
|
||||||
|
let t = e.touches[0];
|
||||||
|
let f: number;
|
||||||
|
|
||||||
|
if (store.winLayoutMode === 'row') {
|
||||||
|
f = t.clientX;
|
||||||
|
if (ctx.curResizeTarget === "first") {
|
||||||
|
win1Ref.value.style.minWidth = f + "px";
|
||||||
|
win1Ref.value.style.maxWidth = f + "px";
|
||||||
|
} else {
|
||||||
|
win2Ref.value.style.minWidth = document.body.scrollWidth - f - store.winRight.borderSize + "px";
|
||||||
|
win2Ref.value.style.maxWidth = document.body.scrollWidth - f - store.winRight.borderSize + "px";
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
/* column layout mode */
|
||||||
|
f = t.clientY;
|
||||||
|
if (ctx.curResizeTarget === "first") {
|
||||||
|
/* setting window */
|
||||||
|
win1Ref.value.style.minHeight = f - ctx.curHeightOffset + "px";
|
||||||
|
win1Ref.value.style.maxHeight = f - ctx.curHeightOffset + "px";
|
||||||
|
} else {
|
||||||
|
/* quick access window */
|
||||||
|
win2Ref.value.style.minHeight = document.body.scrollHeight - f - store.winRight.borderSize + "px";
|
||||||
|
win2Ref.value.style.maxHeight = document.body.scrollHeight - f - store.winRight.borderSize + "px";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function startResize(event: Event) {
|
||||||
|
// Normalize touch and mouse events
|
||||||
|
if (event.type.includes('touch')) {
|
||||||
|
ctx.curHeightOffset = (event as TouchEvent).touches[0].clientY;
|
||||||
|
} else {
|
||||||
|
ctx.curHeightOffset = (event as MouseEvent).clientY;
|
||||||
|
}
|
||||||
|
|
||||||
|
const divRef = event.target;
|
||||||
|
|
||||||
|
if (divRef === firstWinResizeRef.value) {
|
||||||
|
ctx.curResizeTarget = "first";
|
||||||
|
ctx.curHeightOffset -= win1Ref.value.clientHeight;
|
||||||
|
// ctx.curOffset = win1Ref.value.clientHeight;
|
||||||
|
} else if (divRef === thirdWinResizeRef.value) {
|
||||||
|
ctx.curResizeTarget = "third";
|
||||||
|
ctx.curHeightOffset += win2Ref.value.clientHeight;
|
||||||
|
}
|
||||||
|
|
||||||
|
win1Ref.value.style.transition = 'initial';
|
||||||
|
win2Ref.value.style.transition = 'initial';
|
||||||
|
document.addEventListener("mousemove", mouseResize, false);
|
||||||
|
document.addEventListener("touchmove", touchResize, false);
|
||||||
|
store.winAutoLayout = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
function stopResize() {
|
||||||
|
if (win1Ref.value) {
|
||||||
|
win1Ref.value.style.transition = '';
|
||||||
|
if (store.winLayoutMode === "row") {
|
||||||
|
store.winLeft.width = win1Ref.value.style.minWidth;
|
||||||
|
} else {
|
||||||
|
store.winLeft.height = win1Ref.value.style.minHeight;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (win2Ref.value) {
|
||||||
|
win2Ref.value.style.transition = '';
|
||||||
|
if (store.winLayoutMode === "row") {
|
||||||
|
store.winRight.width = win2Ref.value.style.minWidth;
|
||||||
|
} else {
|
||||||
|
store.winRight.height = win2Ref.value.style.minHeight;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
document.body.style.cursor = '';
|
||||||
|
document.removeEventListener("mousemove", mouseResize, false);
|
||||||
|
document.removeEventListener("touchmove", touchResize, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
watch(() => store.winLayoutMode, (value) => {
|
||||||
|
updateResizer();
|
||||||
|
if (value === "col") {
|
||||||
|
store.winAutoLayout = false;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
watch([
|
||||||
|
() => layoutConf.isSmall,
|
||||||
|
() => store.winAutoLayout
|
||||||
|
], (value) => {
|
||||||
|
if (store.winAutoLayout) {
|
||||||
|
store.winRight.show = !value[0];
|
||||||
|
win1Ref.value.style.minWidth = "";
|
||||||
|
win1Ref.value.style.maxWidth = "";
|
||||||
|
}
|
||||||
|
}, {
|
||||||
|
immediate: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
watch([
|
||||||
|
() => layoutConf.isMedium,
|
||||||
|
() => store.winAutoLayout
|
||||||
|
], (value) => {
|
||||||
|
if (store.winAutoLayout) {
|
||||||
|
store.winLeft.show = !value[0];
|
||||||
|
win1Ref.value.style.minWidth = "";
|
||||||
|
win1Ref.value.style.maxWidth = "";
|
||||||
|
win2Ref.value.style.minWidth = "";
|
||||||
|
win2Ref.value.style.maxWidth = "";
|
||||||
|
winDataView.show = true;
|
||||||
|
}
|
||||||
|
}, {
|
||||||
|
immediate: true
|
||||||
|
});
|
||||||
|
|
||||||
|
watch(() => winDataView.show, value => {
|
||||||
|
if (!value) {
|
||||||
|
win1Ref.value.style.minWidth = "";
|
||||||
|
win1Ref.value.style.maxWidth = "";
|
||||||
|
win1Ref.value.style.maxHeight = "";
|
||||||
|
win1Ref.value.style.maxHeight = "";
|
||||||
|
|
||||||
|
win2Ref.value.style.minWidth = "";
|
||||||
|
win2Ref.value.style.maxWidth = "";
|
||||||
|
win2Ref.value.style.maxHeight = "";
|
||||||
|
win2Ref.value.style.maxHeight = "";
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
watch(() => store.winRight.show, value => {
|
||||||
|
if (!value && !winDataView.show) {
|
||||||
|
win1Ref.value.style.maxHeight = "";
|
||||||
|
win1Ref.value.style.maxHeight = "";
|
||||||
|
win1Ref.value.style.maxWidth = "";
|
||||||
|
win1Ref.value.style.maxWidth = "";
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const onUartJsonMsg = (msg: api.ApiJsonMsg) => {
|
||||||
|
switch (msg.cmd as WtUartCmd) {
|
||||||
|
case WtUartCmd.GET_BAUD:
|
||||||
|
case WtUartCmd.SET_BAUD:{
|
||||||
|
const uartMsg = msg as IUartMsgBaud;
|
||||||
|
if (uartMsg.baud) {
|
||||||
|
store.setUartBaud(uartMsg.baud)
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case WtUartCmd.GET_CONFIG:
|
||||||
|
case WtUartCmd.SET_CONFIG:{
|
||||||
|
const uartMsg = msg as IUartMsgConfig;
|
||||||
|
store.uartConfig.data_bits = uartMsg.data_bits;
|
||||||
|
store.uartConfig.stop_bits = uartMsg.stop_bits;
|
||||||
|
store.uartConfig.parity = uartMsg.parity;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case WtUartCmd.GET_DEFAULT_NUM:
|
||||||
|
uartStore.uartNum = (msg as IUartMsgNum).num;
|
||||||
|
uart_get_baud(uartStore.uartNum);
|
||||||
|
uart_get_config(uartStore.uartNum);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
if (isDevMode()) {
|
||||||
|
console.log("uart not treated", msg);
|
||||||
|
}
|
||||||
|
break
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const onUartBinaryMsg = (msg: ApiBinaryMsg) => {
|
||||||
|
if (isDevMode()) {
|
||||||
|
console.log("uart", msg);
|
||||||
|
}
|
||||||
|
|
||||||
|
store.addSegment(new Uint8Array(msg.payload), true);
|
||||||
|
};
|
||||||
|
|
||||||
|
const onClientCtrl = (msg: api.ControlMsg) => {
|
||||||
|
if (msg.type !== api.ControlMsgType.WS_EVENT) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (msg.data === ControlEvent.DISCONNECTED) {
|
||||||
|
store.acceptIncomingData = false;
|
||||||
|
} else if (msg.data === ControlEvent.CONNECTED) {
|
||||||
|
updateUartData();
|
||||||
|
store.acceptIncomingData = true;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
function updateUartData() {
|
||||||
|
/* TODO: hard code for the moment, 0 is UART instance id (can be changed in the future) */
|
||||||
|
uart_get_default_num();
|
||||||
|
df.wt_data_flow_attach_cur_to_sender(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleWinSizeRefresh() {
|
||||||
|
if (!store.winAutoLayout) {
|
||||||
|
if (win1Ref.value) {
|
||||||
|
if (store.winLayoutMode === "row") {
|
||||||
|
win1Ref.value.style.minWidth = store.winLeft.width;
|
||||||
|
} else {
|
||||||
|
win1Ref.value.style.minHeight = store.winLeft.height;
|
||||||
|
win1Ref.value.style.maxHeight = store.winRight.height;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (win2Ref.value) {
|
||||||
|
if (store.winLayoutMode === "row") {
|
||||||
|
win2Ref.value.style.minWidth = store.winRight.width;
|
||||||
|
} else {
|
||||||
|
win2Ref.value.style.minHeight = store.winRight.height;
|
||||||
|
win2Ref.value.style.maxHeight = store.winRight.height;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
useUartModule()
|
||||||
|
|
||||||
|
firstWinResizeRef.value.style.borderWidth = store.winLeft.borderSize + "px";
|
||||||
|
thirdWinResizeRef.value.style.borderWidth = store.winRight.borderSize + "px";
|
||||||
|
updateCursors()
|
||||||
|
|
||||||
|
if (firstWinResizeRef.value) {
|
||||||
|
firstWinResizeRef.value.addEventListener("mousedown", startResize, false);
|
||||||
|
firstWinResizeRef.value.addEventListener("touchstart", startResize, false);
|
||||||
|
}
|
||||||
|
if (thirdWinResizeRef.value) {
|
||||||
|
thirdWinResizeRef.value.addEventListener("mousedown", startResize, false);
|
||||||
|
thirdWinResizeRef.value.addEventListener("touchstart", startResize, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
document.addEventListener("mouseup", stopResize, false);
|
||||||
|
document.addEventListener("touchend", stopResize, false);
|
||||||
|
updateUartData();
|
||||||
|
store.acceptIncomingData = wsStore.state === ControlEvent.CONNECTED;
|
||||||
|
handleWinSizeRefresh()
|
||||||
|
});
|
||||||
|
|
||||||
|
onUnmounted(() => {
|
||||||
|
if (firstWinResizeRef.value) {
|
||||||
|
firstWinResizeRef.value.removeEventListener("mousedown", startResize, false);
|
||||||
|
firstWinResizeRef.value.removeEventListener("touchstart", startResize, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (thirdWinResizeRef.value) {
|
||||||
|
thirdWinResizeRef.value.removeEventListener("mousedown", startResize, false);
|
||||||
|
thirdWinResizeRef.value.removeEventListener("touchstart", startResize, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
document.removeEventListener("mouseup", stopResize, false);
|
||||||
|
document.removeEventListener("touchend", stopResize, false);
|
||||||
|
});
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<style scoped>
|
||||||
<div class="text-layout">
|
.button-m-0 :deep(.el-button + .el-button) {
|
||||||
<h2 class="page-title opacity-10">尽请期待</h2>
|
margin-left: 0;
|
||||||
</div>
|
}
|
||||||
</template>
|
|
||||||
|
.custom-style .el-segmented {
|
||||||
|
--el-segmented-item-selected-color: var(--el-text-color-primary);
|
||||||
|
--el-segmented-item-selected-bg-color: var(--el-color-primary);
|
||||||
|
--el-border-radius-base: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.button-m-0 :deep(.el-checkbox) {
|
||||||
|
margin-right: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
</style>
|
||||||
|
|
|
@ -0,0 +1,108 @@
|
||||||
|
<template>
|
||||||
|
<div class="text-layout description-style">
|
||||||
|
<h1 class="page-title">
|
||||||
|
固件更新
|
||||||
|
</h1>
|
||||||
|
<p class="text-center">(需联网)</p>
|
||||||
|
<el-divider></el-divider>
|
||||||
|
|
||||||
|
<el-descriptions title="当前版本" border :column="1">
|
||||||
|
<el-descriptions-item label="硬件版本">{{ sysStore.hwInfo.ver }}</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="固件版本">{{ sysStore.curFmInfo.ver }}</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="固件日期">{{ sysStore.curFmInfo.date }}</el-descriptions-item>
|
||||||
|
</el-descriptions>
|
||||||
|
|
||||||
|
<el-divider></el-divider>
|
||||||
|
|
||||||
|
<el-descriptions title="最新版本" border :column="1">
|
||||||
|
<template #extra>
|
||||||
|
<div class="flex">
|
||||||
|
<el-tooltip placement="top" effect="light">
|
||||||
|
<template #content>
|
||||||
|
<p>2秒延迟后重启</p>
|
||||||
|
</template>
|
||||||
|
<el-button @click="doReboot" type="warning" :disabled="sysStore.rebootInProgress">
|
||||||
|
重启{{ sysStore.rebootInProgress ? '中' : ''}}
|
||||||
|
</el-button>
|
||||||
|
</el-tooltip>
|
||||||
|
|
||||||
|
<el-button @click="doUpdate" type="primary" :disabled="!updateStore.canUpdate">
|
||||||
|
更新
|
||||||
|
</el-button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</template>
|
||||||
|
<el-descriptions-item label="固件版本">{{ updateStore.newFmInfo.fm_ver }}</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="更新日期">{{ updateStore.newFmInfo.upd_date }}</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="固件大小">{{ updateStore.newFmInfo.fm_size }}</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="更新进度">
|
||||||
|
<el-alert v-if="updateStore.updateStatus === 'OK'" title="更新已完成,重启后,刷新网页生效" type="success" show-icon :closable="false" />
|
||||||
|
<el-progress v-else :percentage="updateStore.updateProgress" :format="format" :status="updateStore.progressBarStatus"/>
|
||||||
|
</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="更新内容">
|
||||||
|
<pre>{{ updateStore.newFmInfo.upd_note }}</pre>
|
||||||
|
</el-descriptions-item>
|
||||||
|
</el-descriptions>
|
||||||
|
|
||||||
|
<el-divider @click="showHidden = !showHidden">底部</el-divider>
|
||||||
|
<div v-if="showHidden">
|
||||||
|
<p>直链更新(仅用于测试,请勿使用)</p>
|
||||||
|
<el-input placeholder="https://..." v-model="directLinkUpdate"></el-input>
|
||||||
|
<el-button type="primary" @click="doDirectLinkUpdate">更新</el-button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import {onMounted, onUnmounted, ref} from "vue";
|
||||||
|
import {
|
||||||
|
wt_ota_do_update, wt_ota_do_url_update,
|
||||||
|
wt_ota_get_progress, wt_ota_get_update_info,
|
||||||
|
} from "@/api/apiOTA";
|
||||||
|
import {useSystemStore} from "@/stores/useSystemStore";
|
||||||
|
import {wt_sys_reboot} from "@/api/apiSystem";
|
||||||
|
import {useUpdateStore} from "@/stores/useUpdateStore";
|
||||||
|
|
||||||
|
const sysStore = useSystemStore();
|
||||||
|
const updateStore = useUpdateStore();
|
||||||
|
const showHidden = ref(false)
|
||||||
|
|
||||||
|
const directLinkUpdate = ref("");
|
||||||
|
|
||||||
|
const format = (percentage: number) => (percentage.toFixed(2) + '%')
|
||||||
|
|
||||||
|
function doUpdate() {
|
||||||
|
wt_ota_do_update();
|
||||||
|
updateStore.setProgressInterval();
|
||||||
|
}
|
||||||
|
|
||||||
|
function doReboot() {
|
||||||
|
wt_sys_reboot();
|
||||||
|
}
|
||||||
|
|
||||||
|
function doDirectLinkUpdate() {
|
||||||
|
if (directLinkUpdate.value.length === 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
updateStore.setProgressInterval();
|
||||||
|
wt_ota_do_url_update(directLinkUpdate.value);
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
wt_ota_get_update_info();
|
||||||
|
wt_ota_get_progress();
|
||||||
|
});
|
||||||
|
|
||||||
|
onUnmounted(() => {
|
||||||
|
updateStore.clearProgressInterval();
|
||||||
|
});
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.description-style :deep(.el-descriptions__label) {
|
||||||
|
@apply w-32
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -0,0 +1,345 @@
|
||||||
|
<template>
|
||||||
|
<div class="flex flex-col h-screen">
|
||||||
|
<div v-show="widgetStore.showOptions" class="flex h-32 overflow-y-auto m-2">
|
||||||
|
<div class="flex gap-4">
|
||||||
|
<div class="flex flex-col gap-2 border-r pr-4">
|
||||||
|
<el-checkbox v-model="widgetStore.editGrid" border class="w-full">{{
|
||||||
|
translate('widget.editGrid')
|
||||||
|
}}</el-checkbox>
|
||||||
|
<el-checkbox v-model="widgetStore.editCell" border class="w-full">{{
|
||||||
|
translate('widget.editCell')
|
||||||
|
}}</el-checkbox>
|
||||||
|
</div>
|
||||||
|
<div class="flex flex-col items-center">
|
||||||
|
<div
|
||||||
|
class="w-40 h-24 bg-gray-200 border-2 rounded-md p-2 flex flex-col justify-center items-center text-center cursor-move"
|
||||||
|
draggable="true"
|
||||||
|
@dragstart="dragStart('loop', $event)"
|
||||||
|
@drag="drag"
|
||||||
|
@dragend="dragEnd"
|
||||||
|
>
|
||||||
|
<span class="font-bold">{{ translate('widget.loopWidget') }}</span>
|
||||||
|
<p class="text-xs">{{ translate('widget.loopWidgetDesc') }}</p>
|
||||||
|
</div>
|
||||||
|
<el-button @click="widgetStore.addLoopWidget" size="small" class="w-full mt-1">{{
|
||||||
|
translate('widget.addGrid')
|
||||||
|
}}</el-button>
|
||||||
|
</div>
|
||||||
|
<div class="flex flex-col items-center">
|
||||||
|
<div
|
||||||
|
class="w-40 h-24 bg-gray-200 border-2 rounded-md p-2 flex flex-col justify-center items-center text-center"
|
||||||
|
:class="{
|
||||||
|
'cursor-move': !widgetStore.isUartViewAdded,
|
||||||
|
'cursor-not-allowed opacity-50': widgetStore.isUartViewAdded
|
||||||
|
}"
|
||||||
|
:draggable="!widgetStore.isUartViewAdded"
|
||||||
|
@dragstart="dragStart('uart', $event)"
|
||||||
|
@drag="drag"
|
||||||
|
@dragend="dragEnd"
|
||||||
|
>
|
||||||
|
<span class="font-bold">{{ translate('widget.dataViewer') }}</span>
|
||||||
|
<p class="text-xs">{{ translate('widget.dataViewerDesc') }}</p>
|
||||||
|
</div>
|
||||||
|
<el-button
|
||||||
|
@click="widgetStore.addUartViewWidget"
|
||||||
|
size="small"
|
||||||
|
class="w-full mt-1"
|
||||||
|
:disabled="widgetStore.isUartViewAdded"
|
||||||
|
>{{ translate('widget.addGrid') }}</el-button
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
<div class="flex flex-col items-start gap-2 border-l pl-4">
|
||||||
|
<div>
|
||||||
|
<el-button @click="widgetStore.exportSettings" size="small">{{
|
||||||
|
translate('widget.exportSettings')
|
||||||
|
}}</el-button>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<el-button @click="widgetStore.importSettings" size="small">{{
|
||||||
|
translate('widget.importSettings')
|
||||||
|
}}</el-button>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<el-button type="danger" @click="widgetStore.resetToDefault" size="small">{{
|
||||||
|
translate('widget.resetToDefault')
|
||||||
|
}}</el-button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div ref="gridWrapper" class="flex-1 flex flex-col w-full min-h-0">
|
||||||
|
<div class="flex-1 bg-gray-100 overflow-y-auto min-h-0">
|
||||||
|
<GridLayout
|
||||||
|
ref="gridlayout"
|
||||||
|
v-model:layout="widgetStore.layout"
|
||||||
|
:col-num="20"
|
||||||
|
:row-height="30"
|
||||||
|
:is-draggable="widgetStore.editGrid"
|
||||||
|
:is-resizable="widgetStore.editGrid"
|
||||||
|
:auto-size="false"
|
||||||
|
:compact-type="null"
|
||||||
|
:vertical-compact="false"
|
||||||
|
>
|
||||||
|
<grid-item
|
||||||
|
v-for="(item, index) in widgetStore.layout"
|
||||||
|
:key="item.i"
|
||||||
|
v-bind="item"
|
||||||
|
class="rounded-md flex flex-col text-xs p-1"
|
||||||
|
:class="{
|
||||||
|
'bg-blue-300': String(item.i) !== dropId,
|
||||||
|
'bg-gray-400 opacity-50': String(item.i) === dropId
|
||||||
|
}"
|
||||||
|
>
|
||||||
|
<template v-if="String(item.i) !== dropId">
|
||||||
|
<div class="flex justify-between pb-0.5">
|
||||||
|
<InlineSvg :name="getWidgetIconName(item)" width="20"></InlineSvg>
|
||||||
|
<el-button
|
||||||
|
v-show="widgetStore.editGrid"
|
||||||
|
type="danger"
|
||||||
|
size="small"
|
||||||
|
class="self-center px-1"
|
||||||
|
@click="widgetStore.deleteWidget(index)"
|
||||||
|
>
|
||||||
|
<InlineSvg name="close" width="20"></InlineSvg>
|
||||||
|
</el-button>
|
||||||
|
<div :id="`tp-widget-before-${item.i}`"></div>
|
||||||
|
<div v-if="widgetStore.editGrid" class="w-full">
|
||||||
|
<el-input
|
||||||
|
v-model="item.name"
|
||||||
|
size="small"
|
||||||
|
:placeholder="translate('widget.gridItemName')"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
v-else-if="item.name"
|
||||||
|
class="truncate font-bold self-center text-center text-sm w-full"
|
||||||
|
>
|
||||||
|
{{ item.name }}
|
||||||
|
</div>
|
||||||
|
<p v-else></p>
|
||||||
|
<!-- empty space to align the check tag -->
|
||||||
|
<el-check-tag
|
||||||
|
v-if="widgetStore.editGrid"
|
||||||
|
:checked="item.static"
|
||||||
|
type="danger"
|
||||||
|
class="self-center px-1"
|
||||||
|
@click="item.static = !item.static"
|
||||||
|
>
|
||||||
|
<InlineSvg v-show="item.static" name="lock" width="20"></InlineSvg>
|
||||||
|
<InlineSvg v-show="!item.static" name="lock_open" width="20"></InlineSvg>
|
||||||
|
</el-check-tag>
|
||||||
|
<div v-show="!widgetStore.editGrid" :id="`tp-widget-${item.i}`"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="bg-white overflow-y-auto flex flex-col flex-grow">
|
||||||
|
<component
|
||||||
|
:is="item.widget"
|
||||||
|
v-model="widgetStore.layout[index]"
|
||||||
|
:editCell="widgetStore.editCell"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<div v-else class="flex justify-center items-center h-full">
|
||||||
|
<p class="font-bold text-white">{{ translate('widget.dropHere') }}</p>
|
||||||
|
</div>
|
||||||
|
</grid-item>
|
||||||
|
</GridLayout>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<teleport to="#nav-right-slot">
|
||||||
|
<ElCheckTag
|
||||||
|
:checked="widgetStore.showOptions"
|
||||||
|
type="primary"
|
||||||
|
@click="widgetStore.showOptions = !widgetStore.showOptions"
|
||||||
|
>{{ translate('widget.editGrid') }}</ElCheckTag
|
||||||
|
>
|
||||||
|
</teleport>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { markRaw, ref, onMounted, onBeforeUnmount } from 'vue'
|
||||||
|
import { GridLayout, GridItem } from 'vue-grid-layout-v3'
|
||||||
|
import { ElInput, ElCheckbox, ElCheckTag, ElButton } from 'element-plus'
|
||||||
|
import UartAtCommand from './widgets/uartAtCommand.vue'
|
||||||
|
import WidgetLoop from './widgets/widgetLoop.vue'
|
||||||
|
import textDataViewer from '@/views/text-data-viewer/textDataViewer.vue'
|
||||||
|
import { useUartModule } from '@/composables/useUartModule'
|
||||||
|
import { globalNotify } from '@/composables/notification'
|
||||||
|
import { useWidgetStore } from '@/stores/useWidgetStore'
|
||||||
|
import type { WidgetItem } from '@/types/grid'
|
||||||
|
import { translate } from '@/locales'
|
||||||
|
|
||||||
|
const widgetStore = useWidgetStore()
|
||||||
|
|
||||||
|
const gridlayout = ref<InstanceType<typeof GridLayout> | null>(null)
|
||||||
|
const gridWrapper = ref<HTMLElement | null>(null)
|
||||||
|
|
||||||
|
const getWidgetIconName = (item: WidgetItem) => {
|
||||||
|
if (typeof item.widget === 'object' && item.widget !== null && 'widgetIconName' in item.widget) {
|
||||||
|
return (item.widget as any).widgetIconName
|
||||||
|
}
|
||||||
|
return 'default-icon' // or some other default
|
||||||
|
}
|
||||||
|
|
||||||
|
const throttle = (fn: Function, wait: number) => {
|
||||||
|
let inThrottle: boolean, lastFn: number, lastTime: number
|
||||||
|
return function (this: any, ...args: any[]) {
|
||||||
|
const context = this
|
||||||
|
if (!inThrottle) {
|
||||||
|
fn.apply(context, args)
|
||||||
|
lastTime = Date.now()
|
||||||
|
inThrottle = true
|
||||||
|
} else {
|
||||||
|
clearTimeout(lastFn)
|
||||||
|
lastFn = window.setTimeout(() => {
|
||||||
|
if (Date.now() - lastTime >= wait) {
|
||||||
|
fn.apply(context, args)
|
||||||
|
lastTime = Date.now()
|
||||||
|
}
|
||||||
|
}, Math.max(wait - (Date.now() - lastTime), 0))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const mouseAt = { x: -1, y: -1 }
|
||||||
|
function syncMousePosition(event: MouseEvent) {
|
||||||
|
mouseAt.x = event.clientX
|
||||||
|
mouseAt.y = event.clientY
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
useUartModule()
|
||||||
|
document.addEventListener('dragover', syncMousePosition)
|
||||||
|
})
|
||||||
|
|
||||||
|
onBeforeUnmount(() => {
|
||||||
|
document.removeEventListener('dragover', syncMousePosition)
|
||||||
|
})
|
||||||
|
|
||||||
|
const dropId = 'drop-placeholder'
|
||||||
|
const dragging = ref<{ type: string; w: number; h: number } | null>(null)
|
||||||
|
|
||||||
|
function dragStart(itemType: string, event: DragEvent) {
|
||||||
|
if (itemType === 'loop') {
|
||||||
|
dragging.value = { type: 'loop', w: 10, h: 5 }
|
||||||
|
} else if (itemType === 'uart') {
|
||||||
|
dragging.value = { type: 'uart', w: 10, h: 10 }
|
||||||
|
}
|
||||||
|
event.dataTransfer?.setData('text/plain', itemType)
|
||||||
|
}
|
||||||
|
|
||||||
|
const drag = throttle(() => {
|
||||||
|
if (!dragging.value || !gridWrapper.value || !gridlayout.value) return
|
||||||
|
|
||||||
|
const parentRect = gridWrapper.value.getBoundingClientRect()
|
||||||
|
const mouseInGrid =
|
||||||
|
mouseAt.x > parentRect.left &&
|
||||||
|
mouseAt.x < parentRect.right &&
|
||||||
|
mouseAt.y > parentRect.top &&
|
||||||
|
mouseAt.y < parentRect.bottom
|
||||||
|
|
||||||
|
const placeholderIndex = widgetStore.layout.findIndex((item) => String(item.i) === dropId)
|
||||||
|
|
||||||
|
if (mouseInGrid) {
|
||||||
|
const gLayout = gridlayout.value as any
|
||||||
|
const colNum = 20
|
||||||
|
const rowHeight = 30
|
||||||
|
const margin = [10, 10]
|
||||||
|
const scrollContainer = gLayout.$el.parentElement
|
||||||
|
if (!scrollContainer) return
|
||||||
|
const colWidth =
|
||||||
|
(gLayout.$el.offsetWidth -
|
||||||
|
margin[0] * (colNum - 1) -
|
||||||
|
(gLayout.containerPadding?.[0] || margin[0]) * 2) /
|
||||||
|
colNum
|
||||||
|
|
||||||
|
const xInGrid = mouseAt.x - parentRect.left + scrollContainer.scrollLeft
|
||||||
|
const yInGrid = mouseAt.y - parentRect.top + scrollContainer.scrollTop
|
||||||
|
|
||||||
|
let gridX = Math.round(xInGrid / (colWidth + margin[0]) - dragging.value.w / 2)
|
||||||
|
let gridY = Math.round(yInGrid / (rowHeight + margin[1]) - dragging.value.h / 2)
|
||||||
|
|
||||||
|
gridX = Math.max(0, Math.min(gridX, colNum - dragging.value.w))
|
||||||
|
gridY = Math.max(0, gridY)
|
||||||
|
|
||||||
|
if (placeholderIndex === -1) {
|
||||||
|
widgetStore.layout.push({
|
||||||
|
x: gridX,
|
||||||
|
y: gridY,
|
||||||
|
w: dragging.value.w,
|
||||||
|
h: dragging.value.h,
|
||||||
|
i: dropId
|
||||||
|
} as any)
|
||||||
|
} else {
|
||||||
|
widgetStore.layout[placeholderIndex].x = gridX
|
||||||
|
widgetStore.layout[placeholderIndex].y = gridY
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (placeholderIndex !== -1) {
|
||||||
|
widgetStore.layout.splice(placeholderIndex, 1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, 50)
|
||||||
|
|
||||||
|
function dragEnd() {
|
||||||
|
if (!dragging.value) return
|
||||||
|
|
||||||
|
const parentRect = gridWrapper.value?.getBoundingClientRect()
|
||||||
|
|
||||||
|
const placeholderIndex = widgetStore.layout.findIndex((item) => String(item.i) === dropId)
|
||||||
|
|
||||||
|
if (placeholderIndex === -1) {
|
||||||
|
dragging.value = null
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const placeholder = widgetStore.layout[placeholderIndex]
|
||||||
|
const mouseInGrid =
|
||||||
|
parentRect &&
|
||||||
|
mouseAt.x > parentRect.left &&
|
||||||
|
mouseAt.x < parentRect.right &&
|
||||||
|
mouseAt.y > parentRect.top &&
|
||||||
|
mouseAt.y < parentRect.bottom
|
||||||
|
|
||||||
|
if (mouseInGrid) {
|
||||||
|
if (dragging.value.type === 'uart') {
|
||||||
|
const uartViewExists = widgetStore.layout.some(
|
||||||
|
(item) => item.widget === textDataViewer && String(item.i) !== dropId
|
||||||
|
)
|
||||||
|
if (uartViewExists) {
|
||||||
|
globalNotify(translate('widget.uartViewOnce'), 'warning')
|
||||||
|
widgetStore.layout.splice(placeholderIndex, 1) // remove placeholder
|
||||||
|
dragging.value = null
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const newWidget = {
|
||||||
|
x: placeholder.x,
|
||||||
|
y: placeholder.y,
|
||||||
|
w: dragging.value.w,
|
||||||
|
h: dragging.value.h,
|
||||||
|
i: widgetStore.getNextId(),
|
||||||
|
name: dragging.value.type === 'loop' ? 'New Loop Widget' : 'UART Data Viewer',
|
||||||
|
static: false,
|
||||||
|
widget: markRaw(dragging.value.type === 'loop' ? WidgetLoop : textDataViewer),
|
||||||
|
widgetProps: []
|
||||||
|
}
|
||||||
|
|
||||||
|
widgetStore.layout.splice(placeholderIndex, 1, newWidget as any)
|
||||||
|
} else {
|
||||||
|
// Not in grid, just remove placeholder
|
||||||
|
widgetStore.layout.splice(placeholderIndex, 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
dragging.value = null
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
:deep(.el-check-tag) {
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
|
@ -1,17 +1,17 @@
|
||||||
<template>
|
<template>
|
||||||
<div class="text-layout">
|
<div class="text-layout">
|
||||||
<h1 class="page-title">
|
<h1 class="page-title">
|
||||||
Wi-Fi 配置
|
Wi-Fi {{ translate('wifi.settings') }}
|
||||||
</h1>
|
</h1>
|
||||||
<el-divider></el-divider>
|
<el-divider></el-divider>
|
||||||
|
|
||||||
<h2 class="mb-4 text-xl font-bold tracking-tight md:text-2xl lg:text-3xl">连接Wi-Fi</h2>
|
<h2 class="mb-4 text-xl font-bold tracking-tight md:text-2xl lg:text-3xl">{{ translate('wifi.connection') }} Wi-Fi</h2>
|
||||||
<el-form label-width="auto" ref="formRef" :model="ssidValidateForm" class="m-auto">
|
<el-form label-width="auto" ref="formRef" :model="ssidValidateForm" class="m-auto">
|
||||||
<el-form-item
|
<el-form-item
|
||||||
label="Wi-Fi名"
|
label="Wi-Fi"
|
||||||
prop="wifiSsid"
|
prop="wifiSsid"
|
||||||
:rules="[
|
:rules="[
|
||||||
{ required: true, message: '请输入WIFI名'},
|
{ required: true, message: translate('wifi.warnWifiName')},
|
||||||
]"
|
]"
|
||||||
>
|
>
|
||||||
<div class="flex w-full">
|
<div class="flex w-full">
|
||||||
|
@ -36,7 +36,7 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
<el-form-item label="密码">
|
<el-form-item :label="translate('wifi.password')">
|
||||||
<el-input
|
<el-input
|
||||||
v-model="ssidValidateForm.password"
|
v-model="ssidValidateForm.password"
|
||||||
show-password
|
show-password
|
||||||
|
@ -46,33 +46,21 @@
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
<div class="mb-2">
|
<div class="mb-2">
|
||||||
<el-alert type="info" show-icon>
|
<el-alert type="info" show-icon>
|
||||||
如果不是通过透传器的热点连接,更换Wi-Fi将导致此界面与透传器断开连接。
|
{{ translate("wifi.connectInfoHTML")}}
|
||||||
</el-alert>
|
</el-alert>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex justify-center">
|
<div class="flex justify-center">
|
||||||
<el-button @click="onConnectClick" type="primary">连接</el-button>
|
<el-button @click="onConnectClick" type="primary">{{ translate('wifi.connect') }}</el-button>
|
||||||
</div>
|
</div>
|
||||||
</el-form>
|
</el-form>
|
||||||
|
|
||||||
<el-divider></el-divider>
|
<el-divider></el-divider>
|
||||||
<div class="flex items-center">
|
<div class="flex items-center">
|
||||||
<h5 class="text-md font-bold text-gray-800 w-32">Wi-Fi模式</h5>
|
<h5 class="text-md font-bold text-gray-800 w-32">Wi-Fi {{ translate('wifi.mode') }}</h5>
|
||||||
<div class="flex shrink-0">
|
<div class="flex shrink-0">
|
||||||
<el-tooltip effect="light">
|
<el-tooltip effect="light">
|
||||||
<template #content>
|
<template #content>
|
||||||
<p>热点+终端模式并存会影响稳定性。且保持热点开启会增加功耗。</p>
|
<div v-html="translate('wifi.modeTipsHtml')"></div>
|
||||||
<p>
|
|
||||||
<el-text size="small">智能模式:</el-text>
|
|
||||||
成功连接Wi-Fi,30秒后自动关闭热点;断开连接,5秒后自动打开热点
|
|
||||||
</p>
|
|
||||||
<p>
|
|
||||||
<el-text size="small">热点+终端共存模式:</el-text>
|
|
||||||
方便使用,但是影响稳定性
|
|
||||||
</p>
|
|
||||||
<p>
|
|
||||||
<el-text size="small">单热点模式缺点:</el-text>
|
|
||||||
无网络
|
|
||||||
</p>
|
|
||||||
</template>
|
</template>
|
||||||
<InlineSvg name="help" class="w-3.5 h-3.5 text-gray-500 cursor-help"></InlineSvg>
|
<InlineSvg name="help" class="w-3.5 h-3.5 text-gray-500 cursor-help"></InlineSvg>
|
||||||
</el-tooltip>
|
</el-tooltip>
|
||||||
|
@ -85,38 +73,41 @@
|
||||||
:label="item.label"
|
:label="item.label"
|
||||||
/>
|
/>
|
||||||
</el-select>
|
</el-select>
|
||||||
<el-button type="primary" @click="wifiChangeMode" :loading="wifiMode_loading">保存</el-button>
|
<el-button type="primary" @click="wifiChangeMode" :loading="wifiMode_loading">{{ translate('wifi.save') }}</el-button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<el-divider></el-divider>
|
<el-divider></el-divider>
|
||||||
|
|
||||||
|
|
||||||
<el-descriptions
|
<el-descriptions
|
||||||
title="Wi-Fi终端(STA)信息"
|
|
||||||
:column="1"
|
:column="1"
|
||||||
border
|
border
|
||||||
class="description-style"
|
class="description-style"
|
||||||
>
|
>
|
||||||
|
<template #title>
|
||||||
|
Wi-Fi {{ translate('wifi.stationInfo') }}
|
||||||
|
<el-tag v-if="!isConnected" type="danger">{{ translate('wifi.disconnected') }}</el-tag>
|
||||||
|
</template>
|
||||||
<template #extra>
|
<template #extra>
|
||||||
<el-switch v-model="wifiSta_On" :disabled="wsStore.state != ControlEvent.CONNECTED || !wifiAp_On"
|
<el-switch v-model="wifiSta_On" :disabled="!isConnected || !wifiAp_On"
|
||||||
active-text="已开启" inactive-text="未开启" :loading="wifiMode_loading"
|
:active-text="translate('wifi.enabled')" :inactive-text="translate('wifi.disabled')" :loading="wifiMode_loading"
|
||||||
:before-change="()=>beforeWifiModeChange('STA')"
|
:before-change="()=>beforeWifiModeChange('STA')"
|
||||||
/>
|
/>
|
||||||
</template>
|
</template>
|
||||||
<el-descriptions-item span="4">
|
<el-descriptions-item span="4">
|
||||||
<template #label>
|
<template #label>
|
||||||
<div>
|
<div>
|
||||||
信号强度
|
{{ translate('wifi.signalStrength') }}
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<template #default>
|
<template #default>
|
||||||
<p>{{ wifiStaApInfo.rssi }}</p>
|
<p> {{ wifi_rssi_to_percent(wifiStaApInfo.rssi) }} % ({{ wifiStaApInfo.rssi }} dBm)</p>
|
||||||
</template>
|
</template>
|
||||||
</el-descriptions-item>
|
</el-descriptions-item>
|
||||||
<el-descriptions-item span="4">
|
<el-descriptions-item span="4">
|
||||||
<template #label>
|
<template #label>
|
||||||
<div>
|
<div>
|
||||||
Wi-Fi名(SSID)
|
Wi-Fi(SSID)
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<p>{{ wifiStaApInfo.ssid }}</p>
|
<p>{{ wifiStaApInfo.ssid }}</p>
|
||||||
|
@ -139,14 +130,14 @@
|
||||||
</el-descriptions-item>
|
</el-descriptions-item>
|
||||||
<el-descriptions-item span="4">
|
<el-descriptions-item span="4">
|
||||||
<template #label>
|
<template #label>
|
||||||
<div>IP(内网地址)</div>
|
<div>IP({{ translate('wifi.internalAddress') }})</div>
|
||||||
</template>
|
</template>
|
||||||
<p>{{ wifiStaApInfo.ip }}</p>
|
<p>{{ wifiStaApInfo.ip }}</p>
|
||||||
</el-descriptions-item>
|
</el-descriptions-item>
|
||||||
<el-descriptions-item span="4">
|
<el-descriptions-item span="4">
|
||||||
<template #label>
|
<template #label>
|
||||||
<div>
|
<div>
|
||||||
网关
|
{{ translate('wifi.gateway') }}
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<p>{{ wifiStaApInfo.gateway }}</p>
|
<p>{{ wifiStaApInfo.gateway }}</p>
|
||||||
|
@ -154,7 +145,7 @@
|
||||||
<el-descriptions-item span="4">
|
<el-descriptions-item span="4">
|
||||||
<template #label>
|
<template #label>
|
||||||
<div>
|
<div>
|
||||||
掩码
|
{{ translate('wifi.netmask') }}
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<p>{{ wifiStaApInfo.netmask }}</p>
|
<p>{{ wifiStaApInfo.netmask }}</p>
|
||||||
|
@ -162,7 +153,7 @@
|
||||||
<el-descriptions-item span="4">
|
<el-descriptions-item span="4">
|
||||||
<template #label>
|
<template #label>
|
||||||
<div>
|
<div>
|
||||||
首选DNS
|
{{ translate('wifi.primaryDNS') }}
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<p>{{ wifiStaApInfo.dns_main }}</p>
|
<p>{{ wifiStaApInfo.dns_main }}</p>
|
||||||
|
@ -170,7 +161,7 @@
|
||||||
<el-descriptions-item span="4">
|
<el-descriptions-item span="4">
|
||||||
<template #label>
|
<template #label>
|
||||||
<div>
|
<div>
|
||||||
备用DNS
|
{{ translate('wifi.backupDNS') }}
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<p>{{ wifiStaApInfo.dns_backup }}</p>
|
<p>{{ wifiStaApInfo.dns_backup }}</p>
|
||||||
|
@ -179,10 +170,10 @@
|
||||||
<el-descriptions-item span="4">
|
<el-descriptions-item span="4">
|
||||||
<template #label>
|
<template #label>
|
||||||
<div>
|
<div>
|
||||||
IP分配模式
|
{{ translate('wifi.IPmode') }}
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<el-select v-model="wifiStaticInfo.static_ip_en" :disabled="wsStore.state != ControlEvent.CONNECTED">
|
<el-select v-model="wifiStaticInfo.static_ip_en" :disabled="!isConnected">
|
||||||
<el-option
|
<el-option
|
||||||
v-for="item in staIPModeOptions"
|
v-for="item in staIPModeOptions"
|
||||||
:key="item.key"
|
:key="item.key"
|
||||||
|
@ -193,14 +184,14 @@
|
||||||
</el-descriptions-item>
|
</el-descriptions-item>
|
||||||
<el-descriptions-item span="4" v-if="wifiStaticInfo.static_ip_en">
|
<el-descriptions-item span="4" v-if="wifiStaticInfo.static_ip_en">
|
||||||
<template #label>
|
<template #label>
|
||||||
<div>IP(内网地址)</div>
|
<div>IP({{ translate('wifi.internalAddress') }})</div>
|
||||||
</template>
|
</template>
|
||||||
<el-input v-model="wifiStaticInfo.ip"></el-input>
|
<el-input v-model="wifiStaticInfo.ip"></el-input>
|
||||||
</el-descriptions-item>
|
</el-descriptions-item>
|
||||||
<el-descriptions-item span="4" v-if="wifiStaticInfo.static_ip_en">
|
<el-descriptions-item span="4" v-if="wifiStaticInfo.static_ip_en">
|
||||||
<template #label>
|
<template #label>
|
||||||
<div>
|
<div>
|
||||||
网关
|
{{ translate('wifi.gateway') }}
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<el-input v-model="wifiStaticInfo.gateway"></el-input>
|
<el-input v-model="wifiStaticInfo.gateway"></el-input>
|
||||||
|
@ -208,7 +199,7 @@
|
||||||
<el-descriptions-item span="4" v-if="wifiStaticInfo.static_ip_en">
|
<el-descriptions-item span="4" v-if="wifiStaticInfo.static_ip_en">
|
||||||
<template #label>
|
<template #label>
|
||||||
<div>
|
<div>
|
||||||
掩码
|
{{ translate('wifi.netmask') }}
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<el-input v-model="wifiStaticInfo.netmask"></el-input>
|
<el-input v-model="wifiStaticInfo.netmask"></el-input>
|
||||||
|
@ -217,10 +208,10 @@
|
||||||
<el-descriptions-item span="4">
|
<el-descriptions-item span="4">
|
||||||
<template #label>
|
<template #label>
|
||||||
<div>
|
<div>
|
||||||
DNS模式
|
{{ translate('wifi.DNSmode') }}
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<el-select v-model="wifiStaticInfo.static_dns_en" :disabled="wsStore.state != ControlEvent.CONNECTED">
|
<el-select v-model="wifiStaticInfo.static_dns_en" :disabled="!isConnected">
|
||||||
<el-option
|
<el-option
|
||||||
v-for="item in staDNSModeOptions"
|
v-for="item in staDNSModeOptions"
|
||||||
:key="item.key"
|
:key="item.key"
|
||||||
|
@ -232,7 +223,7 @@
|
||||||
<el-descriptions-item span="4" v-if="wifiStaticInfo.static_dns_en">
|
<el-descriptions-item span="4" v-if="wifiStaticInfo.static_dns_en">
|
||||||
<template #label>
|
<template #label>
|
||||||
<div>
|
<div>
|
||||||
首选DNS
|
{{ translate('wifi.primaryDNS') }}
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<el-input v-model="wifiStaticInfo.dns_main"></el-input>
|
<el-input v-model="wifiStaticInfo.dns_main"></el-input>
|
||||||
|
@ -240,34 +231,37 @@
|
||||||
<el-descriptions-item span="4" v-if="wifiStaticInfo.static_dns_en">
|
<el-descriptions-item span="4" v-if="wifiStaticInfo.static_dns_en">
|
||||||
<template #label>
|
<template #label>
|
||||||
<div>
|
<div>
|
||||||
备用DNS
|
{{ translate('wifi.backupDNS') }}
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<el-input v-model="wifiStaticInfo.dns_backup"></el-input>
|
<el-input v-model="wifiStaticInfo.dns_backup"></el-input>
|
||||||
</el-descriptions-item>
|
</el-descriptions-item>
|
||||||
</el-descriptions>
|
</el-descriptions>
|
||||||
<div class="flex justify-center mt-4">
|
<div class="flex justify-center mt-4">
|
||||||
<el-button type="primary" :loading="wifiMode_loading" @click="wifiStaSetStaticInfo">保存</el-button>
|
<el-button type="primary" :loading="wifiMode_loading" @click="wifiStaSetStaticInfo">{{ translate('wifi.save') }}</el-button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<el-divider></el-divider>
|
<el-divider></el-divider>
|
||||||
|
|
||||||
<el-descriptions
|
<el-descriptions
|
||||||
title="Wi-Fi自发热点(AP)信息"
|
|
||||||
:column="1"
|
:column="1"
|
||||||
border
|
border
|
||||||
class="description-style"
|
class="description-style"
|
||||||
>
|
>
|
||||||
|
<template #title>
|
||||||
|
Wi-Fi {{ translate('wifi.hotspotInfo') }}
|
||||||
|
<el-tag v-if="!isConnected" type="danger">{{ translate('wifi.disconnected') }}</el-tag>
|
||||||
|
</template>
|
||||||
<template #extra>
|
<template #extra>
|
||||||
<el-switch v-model="wifiAp_On" :disabled="wsStore.state != ControlEvent.CONNECTED || !wifiSta_On"
|
<el-switch v-model="wifiAp_On" :disabled="!isConnected || !wifiSta_On"
|
||||||
:loading="wifiMode_loading" active-text="已开启" inactive-text="未开启"
|
:loading="wifiMode_loading" :active-text="translate('wifi.enabled')" :inactive-text="translate('wifi.disabled')"
|
||||||
:before-change="()=>beforeWifiModeChange('AP')"
|
:before-change="()=>beforeWifiModeChange('AP')"
|
||||||
/>
|
/>
|
||||||
</template>
|
</template>
|
||||||
<el-descriptions-item span="6">
|
<el-descriptions-item span="6">
|
||||||
<template #label>
|
<template #label>
|
||||||
<div>
|
<div>
|
||||||
Wi-Fi名(SSID)
|
Wi-Fi(SSID)
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<div class="flex">
|
<div class="flex">
|
||||||
|
@ -277,7 +271,7 @@
|
||||||
<el-descriptions-item span="6">
|
<el-descriptions-item span="6">
|
||||||
<template #label>
|
<template #label>
|
||||||
<div>
|
<div>
|
||||||
密码
|
{{ translate('wifi.password') }}
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<el-input v-model="wifiApInfo.password"></el-input>
|
<el-input v-model="wifiApInfo.password"></el-input>
|
||||||
|
@ -303,7 +297,7 @@
|
||||||
<el-descriptions-item span="4">
|
<el-descriptions-item span="4">
|
||||||
<template #label>
|
<template #label>
|
||||||
<div>
|
<div>
|
||||||
网关
|
{{ translate('wifi.gateway') }}
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
{{ wifiApInfo.gateway }}
|
{{ wifiApInfo.gateway }}
|
||||||
|
@ -312,14 +306,14 @@
|
||||||
<el-descriptions-item span="4">
|
<el-descriptions-item span="4">
|
||||||
<template #label>
|
<template #label>
|
||||||
<div>
|
<div>
|
||||||
掩码
|
{{ translate('wifi.netmask') }}
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
{{ wifiApInfo.netmask }}
|
{{ wifiApInfo.netmask }}
|
||||||
</el-descriptions-item>
|
</el-descriptions-item>
|
||||||
</el-descriptions>
|
</el-descriptions>
|
||||||
<div class="flex justify-center mt-4">
|
<div class="flex justify-center mt-4">
|
||||||
<el-button type="primary" :loading="wifiMode_loading" @click="wifiApChangeCredential">保存</el-button>
|
<el-button type="primary" :loading="wifiMode_loading" @click="wifiApChangeCredential">{{ translate('wifi.save') }}</el-button>
|
||||||
</div>
|
</div>
|
||||||
<el-divider></el-divider>
|
<el-divider></el-divider>
|
||||||
</div>
|
</div>
|
||||||
|
@ -327,7 +321,7 @@
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import {computed, onMounted, onUnmounted, reactive, ref} from "vue";
|
import {computed, type ComputedRef, onMounted, onUnmounted, reactive, ref} from "vue";
|
||||||
import {
|
import {
|
||||||
type IWifiMode,
|
type IWifiMode,
|
||||||
wifi_ap_get_info,
|
wifi_ap_get_info,
|
||||||
|
@ -355,9 +349,10 @@ import {registerModule, unregisterModule} from "@/router/msgRouter";
|
||||||
import {useWsStore} from "@/stores/websocket";
|
import {useWsStore} from "@/stores/websocket";
|
||||||
import {globalNotify, globalNotifyRightSide} from "@/composables/notification";
|
import {globalNotify, globalNotifyRightSide} from "@/composables/notification";
|
||||||
import {isDevMode} from "@/composables/buildMode";
|
import {isDevMode} from "@/composables/buildMode";
|
||||||
|
import {translate} from "@/locales";
|
||||||
|
|
||||||
const formRef = ref<FormInstance>()
|
const formRef = ref<FormInstance>()
|
||||||
let wifiListPlaceholder = ref("我的WIFI")
|
let wifiListPlaceholder = ref("MY-WIFI")
|
||||||
let ssidValidateForm = reactive({
|
let ssidValidateForm = reactive({
|
||||||
wifiSsid: "",
|
wifiSsid: "",
|
||||||
password: "",
|
password: "",
|
||||||
|
@ -370,58 +365,60 @@ let wifiAp_On = ref(false);
|
||||||
|
|
||||||
let wifiMode = ref(-1);
|
let wifiMode = ref(-1);
|
||||||
|
|
||||||
let wifiModeOptions = [
|
let wifiModeOptions = computed( () => [
|
||||||
{
|
{
|
||||||
label: "智能热点+常开终端 (AP+STA)",
|
label: translate('wifi.APauto_STA'),
|
||||||
key: WifiMode.WIFI_AP_AUTO_STA_ON,
|
key: WifiMode.WIFI_AP_AUTO_STA_ON,
|
||||||
}, {
|
}, {
|
||||||
label: "仅开启热点 (AP)",
|
label: translate('wifi.APonly'),
|
||||||
key: WifiMode.WIFI_AP_ON_STA_OFF,
|
key: WifiMode.WIFI_AP_ON_STA_OFF,
|
||||||
}, {
|
}, {
|
||||||
label: "[不推荐] 常开热点+常开终端 (AP+STA)",
|
label: translate('wifi.AP_STA'),
|
||||||
key: WifiMode.WIFI_AP_STA_ON,
|
key: WifiMode.WIFI_AP_STA_ON,
|
||||||
}, /* {
|
}, /*
|
||||||
|
{
|
||||||
value: "仅开启终端(STA)",
|
value: "仅开启终端(STA)",
|
||||||
key: 2,
|
key: 2,
|
||||||
},*/
|
},*/
|
||||||
|
])
|
||||||
]
|
|
||||||
|
|
||||||
let wsStore = useWsStore();
|
let wsStore = useWsStore();
|
||||||
|
|
||||||
const defWifiInfo: WifiInfo = {
|
const defWifiInfo: WifiInfo = {
|
||||||
cmd: 1,
|
cmd: 1,
|
||||||
module: 1,
|
module: 1,
|
||||||
gateway: "未连接",
|
gateway: "-",
|
||||||
ip: "未连接",
|
ip: "-",
|
||||||
mac: "未连接",
|
mac: "-",
|
||||||
dns_main: "未连接",
|
dns_main: "-",
|
||||||
dns_backup: "未连接",
|
dns_backup: "-",
|
||||||
rssi: 0,
|
rssi: 0,
|
||||||
netmask: "未连接",
|
netmask: "-",
|
||||||
ssid: "未连接",
|
ssid: "-",
|
||||||
password: "",
|
password: "",
|
||||||
}
|
}
|
||||||
|
|
||||||
const staIPModeOptions = [
|
const staIPModeOptions = [
|
||||||
{
|
{
|
||||||
label: "自动 (DHCP)",
|
label: translate('wifi.autoIP'),
|
||||||
key: 0,
|
key: 0,
|
||||||
}, {
|
}, {
|
||||||
label: "静态IP",
|
label: translate('wifi.staticIP'),
|
||||||
key: 1,
|
key: 1,
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
|
|
||||||
const staDNSModeOptions = [
|
const staDNSModeOptions = [
|
||||||
{
|
{
|
||||||
label: "自动 (使用网关)",
|
label: translate('wifi.autoDNS'),
|
||||||
key: 0,
|
key: 0,
|
||||||
}, {
|
}, {
|
||||||
label: "静态DNS",
|
label: translate('wifi.staticDNS'),
|
||||||
key: 1,
|
key: 1,
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
|
|
||||||
|
const isConnected = computed(() => wsStore.state === ControlEvent.CONNECTED)
|
||||||
let wifiStaApInfo = reactive<WifiInfo>({...defWifiInfo});
|
let wifiStaApInfo = reactive<WifiInfo>({...defWifiInfo});
|
||||||
let wifiApInfo = reactive<WifiInfo>({...defWifiInfo});
|
let wifiApInfo = reactive<WifiInfo>({...defWifiInfo});
|
||||||
let wifiStaticInfo = reactive<IWifiStaStaticInfo>({
|
let wifiStaticInfo = reactive<IWifiStaStaticInfo>({
|
||||||
|
@ -439,7 +436,7 @@ let scan_cb: any;
|
||||||
let connectBtnClicked = 0;
|
let connectBtnClicked = 0;
|
||||||
let options: Array<WifiScanInfo> = [];
|
let options: Array<WifiScanInfo> = [];
|
||||||
const scanText = computed(() => {
|
const scanText = computed(() => {
|
||||||
return scanning.value ? "扫描中" : "扫描";
|
return scanning.value ? translate("wifi.scanning") : translate("wifi.scan");
|
||||||
});
|
});
|
||||||
|
|
||||||
const querySearch = (queryString: string, cb: any) => {
|
const querySearch = (queryString: string, cb: any) => {
|
||||||
|
@ -463,7 +460,7 @@ const onClientMsg = (msg: ApiJsonMsg) => {
|
||||||
}
|
}
|
||||||
if (connectBtnClicked) {
|
if (connectBtnClicked) {
|
||||||
connectBtnClicked = 0;
|
connectBtnClicked = 0;
|
||||||
globalNotifyRightSide(wifiStaApInfo.ssid + " 连接成功", "success");
|
globalNotifyRightSide(wifiStaApInfo.ssid + " " + translate('wifi.connectionSuccess'), "success");
|
||||||
wifi_sta_get_static_info();
|
wifi_sta_get_static_info();
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
|
@ -487,7 +484,7 @@ const onClientMsg = (msg: ApiJsonMsg) => {
|
||||||
scan_cb(options);
|
scan_cb(options);
|
||||||
scan_cb = null;
|
scan_cb = null;
|
||||||
}
|
}
|
||||||
globalNotifyRightSide("扫描完成", "success");
|
globalNotifyRightSide(translate('wifi.scanDone'), "success");
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case WifiCmd.WIFI_API_JSON_DISCONNECT:
|
case WifiCmd.WIFI_API_JSON_DISCONNECT:
|
||||||
|
@ -504,7 +501,7 @@ const onClientMsg = (msg: ApiJsonMsg) => {
|
||||||
const modeInfo = msg as IWifiMode;
|
const modeInfo = msg as IWifiMode;
|
||||||
wifiMode_loading.value = false;
|
wifiMode_loading.value = false;
|
||||||
if (modeInfo.err !== undefined) {
|
if (modeInfo.err !== undefined) {
|
||||||
globalNotifyRightSide("设置失败", "error");
|
globalNotifyRightSide(translate('wifi.setFailed'), "error");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -532,7 +529,7 @@ const onClientMsg = (msg: ApiJsonMsg) => {
|
||||||
if (wifiCred.err !== undefined) {
|
if (wifiCred.err !== undefined) {
|
||||||
globalNotifyRightSide(wifiCred.err, "error");
|
globalNotifyRightSide(wifiCred.err, "error");
|
||||||
} else {
|
} else {
|
||||||
globalNotifyRightSide("已保存配置", "success");
|
globalNotifyRightSide(translate('wifi.setSuccess'), "success");
|
||||||
}
|
}
|
||||||
wifiMode_loading.value = false;
|
wifiMode_loading.value = false;
|
||||||
|
|
||||||
|
@ -540,7 +537,6 @@ const onClientMsg = (msg: ApiJsonMsg) => {
|
||||||
}
|
}
|
||||||
case WifiCmd.WIFI_API_JSON_STA_GET_STATIC_INFO: {
|
case WifiCmd.WIFI_API_JSON_STA_GET_STATIC_INFO: {
|
||||||
const staticInfo = msg as IWifiStaStaticInfo & ApiJsonMsg;
|
const staticInfo = msg as IWifiStaStaticInfo & ApiJsonMsg;
|
||||||
console.log("@@@", staticInfo);
|
|
||||||
Object.assign(wifiStaticInfo, staticInfo);
|
Object.assign(wifiStaticInfo, staticInfo);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
@ -574,8 +570,8 @@ const onClientCtrl = (msg: ControlMsg) => {
|
||||||
};
|
};
|
||||||
|
|
||||||
function onScanClick() {
|
function onScanClick() {
|
||||||
if (wsStore.state !== ControlEvent.CONNECTED) {
|
if (!isConnected.value) {
|
||||||
globalNotify("调试器未连接", 'error');
|
globalNotify(translate('wifi.debuggerNotConnected'), 'error');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
scanning.value = true;
|
scanning.value = true;
|
||||||
|
@ -583,8 +579,8 @@ function onScanClick() {
|
||||||
}
|
}
|
||||||
|
|
||||||
function onConnectClick() {
|
function onConnectClick() {
|
||||||
if (wsStore.state !== ControlEvent.CONNECTED) {
|
if (!isConnected.value) {
|
||||||
globalNotify("调试器未连接", 'error');
|
globalNotify(translate('wifi.debuggerNotConnected'), 'error');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (ssidValidateForm.wifiSsid !== "") {
|
if (ssidValidateForm.wifiSsid !== "") {
|
||||||
|
@ -610,9 +606,20 @@ function wifiChangeMode() {
|
||||||
wifi_set_mode(wifiMode.value);
|
wifi_set_mode(wifiMode.value);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function wifi_rssi_to_percent(rssi: number)
|
||||||
|
{
|
||||||
|
if (rssi <= -100) {
|
||||||
|
return 0;
|
||||||
|
} else if (rssi >= -50) {
|
||||||
|
return 100;
|
||||||
|
} else {
|
||||||
|
return 2 * (rssi + 100);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function wifiApChangeCredential() {
|
function wifiApChangeCredential() {
|
||||||
if (wifiApInfo.ssid === "") {
|
if (wifiApInfo.ssid === "") {
|
||||||
globalNotifyRightSide("请输入AP名称", "error");
|
globalNotifyRightSide(translate('wifi.enterAPName'), "error");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
wifiMode_loading.value = true;
|
wifiMode_loading.value = true;
|
||||||
|
@ -644,7 +651,7 @@ onUnmounted(() => {
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|
||||||
<style scoped>
|
<style scoped lang="postcss">
|
||||||
.description-style :deep(.el-descriptions__label) {
|
.description-style :deep(.el-descriptions__label) {
|
||||||
@apply w-32
|
@apply w-32
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,26 +1,22 @@
|
||||||
<template>
|
<template>
|
||||||
<nav class="relative px-2 py-0.5 sm:py-1 flex justify-between items-center border-b h-full">
|
<nav class="relative px-2 py-0.5 sm:py-1 flex justify-between items-center border-b h-full">
|
||||||
<div class="flex">
|
<div class="flex">
|
||||||
<button @click.prevent="sideMenuOpen=true" class="flex items-center hover:text-blue-600 pl-1 mx-4">
|
<button @click.prevent="sideMenuOpen=true" class="flex items-center hover:text-blue-600 pl-1 mx-2 sm:mx-4">
|
||||||
<svg class="block h-3 lg:h-4 lg:w-4 fill-current" viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg">
|
<svg class="block h-3 lg:h-4 lg:w-4 fill-current" viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg">
|
||||||
<title>导航侧栏</title>
|
<title>{{ translate('navbar.navigationSidebar') }}</title>
|
||||||
<path d="M0 3h20v2H0V3zm0 6h20v2H0V9zm0 6h20v2H0v-2z"></path>
|
<path d="M0 3h20v2H0V3zm0 6h20v2H0V9zm0 6h20v2H0v-2z"></path>
|
||||||
</svg>
|
</svg>
|
||||||
|
<el-badge v-if="updateStore.canUpdate" is-dot></el-badge>
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<router-link to="/" class="text-3xl px-4 font-bold leading-none hidden items-center sm:flex" title="走,去码头整点薯条">
|
<router-link to="/" class="text-3xl px-4 font-bold leading-none hidden items-center sm:flex" :title="translate('navbar.getSomeFries')">
|
||||||
<InlineSvg name="favicon" class="h-5 lg:h-8"></InlineSvg>
|
<InlineSvg name="favicon" class="h-5 lg:h-8"></InlineSvg>
|
||||||
</router-link>
|
</router-link>
|
||||||
|
|
||||||
|
<div id="nav-right-slot"></div>
|
||||||
|
|
||||||
<!-- <a class="text-3xl px-4 font-bold leading-none" href="/">-->
|
<div class="flex pt-0.5 sm:pt-1 ml-4 text-xs items-center sm:hidden">
|
||||||
<!-- <InlineSvg name="home" class="h-10"></InlineSvg>-->
|
<router-link :to="route.fullPath">{{ $route.meta.title }}</router-link>
|
||||||
<!-- </a>-->
|
|
||||||
<!-- <router-link to="/" class="flex items-center text-sm text-blue-600 font-bold">主页</router-link>-->
|
|
||||||
<!-- <a class="flex items-center text-sm text-blue-600 font-bold" href="/">主页6</a>-->
|
|
||||||
|
|
||||||
<div class="flex pt-0.5 sm:pt-1 ml-4 text-sm items-center sm:hidden">
|
|
||||||
<router-link :to="route.fullPath">{{ route.meta.title }}</router-link>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
@ -35,6 +31,20 @@
|
||||||
<!-- <a class="md:ml-auto md:mr-3"></a>-->
|
<!-- <a class="md:ml-auto md:mr-3"></a>-->
|
||||||
<div class="flex h-full">
|
<div class="flex h-full">
|
||||||
<div id="page-spec-slot" class="content-center h-full flex flex-row"></div>
|
<div id="page-spec-slot" class="content-center h-full flex flex-row"></div>
|
||||||
|
<div class="mr-2">
|
||||||
|
<el-select v-model="language" class="min-w-20 h-full" @change="handleLanguageChange">
|
||||||
|
<el-option value="en">🇺🇸 English</el-option>
|
||||||
|
<el-option value="zh">🇨🇳 简体中文</el-option>
|
||||||
|
<el-option value="fr">🇫🇷 Français</el-option>
|
||||||
|
<template #label>
|
||||||
|
<div class="flex">
|
||||||
|
<InlineSvg name="translate" class="w-4 mr-1"></InlineSvg>
|
||||||
|
{{ languageFlag }}
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</el-select>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="lg:hidden">
|
<div class="lg:hidden">
|
||||||
<el-button :type="wsColor" size="small" class="transition duration-1000 min-h-full">
|
<el-button :type="wsColor" size="small" class="transition duration-1000 min-h-full">
|
||||||
<InlineSvg v-show="wsColor!=='success'" name="link-off" class="mr-2" width="20"></InlineSvg>
|
<InlineSvg v-show="wsColor!=='success'" name="link-off" class="mr-2" width="20"></InlineSvg>
|
||||||
|
@ -67,8 +77,11 @@
|
||||||
|
|
||||||
<div class="flex flex-col justify-between m-4 mt-0">
|
<div class="flex flex-col justify-between m-4 mt-0">
|
||||||
<ul>
|
<ul>
|
||||||
<li v-for="(item, index) in menuItems" class="mb-1" :key="index">
|
<li v-for="(item, index) in sideBarItems" class="mb-1" :key="index">
|
||||||
<router-link @click="sideMenuOpen=false" :title="item.name" :to="item.href" :class="[sideMenuItemClass, item?.class]">{{ item.name }}</router-link>
|
<router-link @click="sideMenuOpen=false" :title="item.name" :to="item.href" :class="[sideMenuItemClass, item?.class]">
|
||||||
|
{{ item.name }}
|
||||||
|
<el-badge v-if="item?.badge?.value" is-dot></el-badge>
|
||||||
|
</router-link>
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
|
@ -77,9 +90,9 @@
|
||||||
<div>
|
<div>
|
||||||
<el-button @click="toggle">
|
<el-button @click="toggle">
|
||||||
<InlineSvg v-if="!isFullscreen" name="open-in-full" width="16px" fill="#000000"></InlineSvg>
|
<InlineSvg v-if="!isFullscreen" name="open-in-full" width="16px" fill="#000000"></InlineSvg>
|
||||||
<p v-if="!isFullscreen">全屏</p>
|
<p v-if="!isFullscreen">{{ translate('page.fullscreen') }}</p>
|
||||||
<InlineSvg v-if="isFullscreen" name="close-fullscreen" width="16px" fill="#000000"></InlineSvg>
|
<InlineSvg v-if="isFullscreen" name="close-fullscreen" width="16px" fill="#000000"></InlineSvg>
|
||||||
<p v-if="isFullscreen">缩小</p>
|
<p v-if="isFullscreen">{{ translate('page.windowed') }}</p>
|
||||||
</el-button>
|
</el-button>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
@ -105,18 +118,31 @@
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import InlineSvg from "@/components/InlineSvg.vue";
|
import InlineSvg from "@/components/InlineSvg.vue";
|
||||||
import {computed, ref} from "vue";
|
import {computed, type ComputedRef, type Ref, ref} from "vue";
|
||||||
import {useWsStore} from "@/stores/websocket";
|
import {useWsStore} from "@/stores/websocket";
|
||||||
import {translate} from "@/locales";
|
import {translate} from "@/locales";
|
||||||
import {ControlEvent} from "@/api";
|
import {ControlEvent} from "@/api";
|
||||||
import {useRoute} from "vue-router";
|
import {useRoute} from "vue-router";
|
||||||
import { useFullscreen } from '@vueuse/core'
|
import { useFullscreen } from '@vueuse/core'
|
||||||
|
import {useUpdateStore} from "@/stores/useUpdateStore";
|
||||||
|
import {isOTAEnabled} from "@/composables/buildMode";
|
||||||
|
import {getFlagFromLang, locale, setLang} from "@/i18n"
|
||||||
|
|
||||||
const wsStore = useWsStore();
|
const wsStore = useWsStore();
|
||||||
|
const updateStore = useUpdateStore();
|
||||||
const {isFullscreen, toggle} = useFullscreen();
|
const {isFullscreen, toggle} = useFullscreen();
|
||||||
const route = useRoute();
|
const route = useRoute();
|
||||||
|
const language = ref(locale);
|
||||||
|
|
||||||
const sideMenuItemClass = "block p-4 text-sm font-semibold hover:bg-blue-50 hover:text-blue-600 rounded"
|
const languageFlag = computed(() => {
|
||||||
|
return getFlagFromLang(language.value);
|
||||||
|
});
|
||||||
|
|
||||||
|
function handleLanguageChange(lang: string) {
|
||||||
|
setLang(lang);
|
||||||
|
}
|
||||||
|
|
||||||
|
const sideMenuItemClass = "block p-4 text-sm font-semibold hover:bg-blue-50 hover:text-blue-600 rounded flex"
|
||||||
const sideMenuOpen = ref(false);
|
const sideMenuOpen = ref(false);
|
||||||
const stateMenuOpen = ref(false)
|
const stateMenuOpen = ref(false)
|
||||||
|
|
||||||
|
@ -137,22 +163,40 @@ const wsColor = computed(() => {
|
||||||
});
|
});
|
||||||
|
|
||||||
const wsState = computed(() => {
|
const wsState = computed(() => {
|
||||||
return translate(wsStore.state);
|
return translate(wsStore.state.toLocaleLowerCase());
|
||||||
});
|
});
|
||||||
|
|
||||||
type Item = {
|
type Item = {
|
||||||
name: string;
|
name: string;
|
||||||
href: string;
|
href: string;
|
||||||
class?: string;
|
class?: string;
|
||||||
|
badge?: Ref<boolean>;
|
||||||
};
|
};
|
||||||
|
|
||||||
const menuItems: Item[] = ([
|
const menuItems: ComputedRef<Item[]> = computed(() => ([
|
||||||
/* {
|
{
|
||||||
name: translate("page.home"),
|
name: translate("page.uart"),
|
||||||
href: "/",
|
href: "/uart",
|
||||||
}, */{
|
}, {
|
||||||
name: translate("page.wifi"),
|
name: translate("page.wifi"),
|
||||||
href: "/wifi",
|
href: "/wifi",
|
||||||
|
}, {
|
||||||
|
name: translate("page.feedback"),
|
||||||
|
href: "/feedback",
|
||||||
|
},
|
||||||
|
]));
|
||||||
|
|
||||||
|
const sideBarItems: ComputedRef<Item[]> = computed(() => {
|
||||||
|
const items: Item[] = [
|
||||||
|
{
|
||||||
|
name: translate("page.wifi"),
|
||||||
|
href: "/wifi",
|
||||||
|
}, {
|
||||||
|
name: translate("page.uart"),
|
||||||
|
href: "/uart",
|
||||||
|
}, {
|
||||||
|
name: translate("page.widget"),
|
||||||
|
href: "/widget",
|
||||||
}, {
|
}, {
|
||||||
name: translate("page.about"),
|
name: translate("page.about"),
|
||||||
href: "/about",
|
href: "/about",
|
||||||
|
@ -160,7 +204,18 @@ const menuItems: Item[] = ([
|
||||||
name: translate("page.feedback"),
|
name: translate("page.feedback"),
|
||||||
href: "/feedback",
|
href: "/feedback",
|
||||||
},
|
},
|
||||||
]);
|
];
|
||||||
|
if (isOTAEnabled()) {
|
||||||
|
items.push({
|
||||||
|
name: translate("page.update"),
|
||||||
|
href: "/update",
|
||||||
|
badge: computed(() => updateStore.canUpdate),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return items;
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
@ -188,5 +243,9 @@ const menuItems: Item[] = ([
|
||||||
padding: 0;
|
padding: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.el-select :deep(.el-select__wrapper) {
|
||||||
|
@apply h-full;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
</style>
|
</style>
|
|
@ -0,0 +1,457 @@
|
||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<el-tabs v-model="store.configPanelTab" class="mx-2 custom-tabs fit">
|
||||||
|
<el-tab-pane name="first" class="min-h-80">
|
||||||
|
<template #label>{{ $t("uart.port") }}</template>
|
||||||
|
<div class="flex flex-col gap-2">
|
||||||
|
<el-form :size="store.winLeft.show ? '' : 'small'" label-position="left" label-width="auto">
|
||||||
|
<el-form-item
|
||||||
|
class="mb-2"
|
||||||
|
>
|
||||||
|
<template #label>{{ $t("uart.baudrate") }}</template>
|
||||||
|
<div class="flex w-full">
|
||||||
|
<el-select v-model="store.uartBaud" :teleported="false" @change="onUartBaudChange">
|
||||||
|
<template #header>
|
||||||
|
<div class="overflow-auto max-h-40">
|
||||||
|
<div class="flex gap-0">
|
||||||
|
<el-input-number
|
||||||
|
v-model="uartCustomBaud"
|
||||||
|
:placeholder="translate('uart.customBaud')"
|
||||||
|
size="small"
|
||||||
|
:controls="false"
|
||||||
|
:min="110"
|
||||||
|
class="flex-grow"
|
||||||
|
></el-input-number>
|
||||||
|
<el-button size="small" @click="onUseCustomUartBaud">{{ $t('uart.use') }}</el-button>
|
||||||
|
<!-- <el-button size="small" @click="onConfirm" class="ml-0">增加</el-button>-->
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<el-option-group :label="translate('uart.commonlyUsed')">
|
||||||
|
<el-option
|
||||||
|
v-for="item in store.predefinedUartBaudFrequent"
|
||||||
|
:key="item.baud"
|
||||||
|
:value="item.baud"
|
||||||
|
class="border-b list-none"
|
||||||
|
/>
|
||||||
|
</el-option-group>
|
||||||
|
|
||||||
|
<el-option-group :label="translate('uart.other')">
|
||||||
|
<el-option
|
||||||
|
v-for="item in store.uartBaudList"
|
||||||
|
:key="item.baud"
|
||||||
|
:value="item.baud"
|
||||||
|
class="border-b list-none"
|
||||||
|
/>
|
||||||
|
</el-option-group>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</el-select>
|
||||||
|
</div>
|
||||||
|
</el-form-item>
|
||||||
|
<p class="text-xs">{{ $t('uart.actual') }} {{ $t('uart.baudrate') }}:{{ store.uartBaudReal }}</p>
|
||||||
|
|
||||||
|
<el-form-item :label="translate('uart.dataBits')" class="mb-2">
|
||||||
|
<el-select v-model="store.uartConfig.data_bits" :teleported="false"
|
||||||
|
placeholder="Select" @change="onUartConfigChange">
|
||||||
|
<el-option
|
||||||
|
v-for="item in uartDataBitsOptions"
|
||||||
|
:key="item.key"
|
||||||
|
:value="item.key"
|
||||||
|
:label="item.label"
|
||||||
|
/>
|
||||||
|
</el-select>
|
||||||
|
</el-form-item>
|
||||||
|
|
||||||
|
<el-form-item :label="translate('uart.parity')" class="mb-2">
|
||||||
|
<el-select v-model="store.uartConfig.parity" :teleported="false"
|
||||||
|
placeholder="Select" @change="onUartConfigChange">
|
||||||
|
<el-option
|
||||||
|
v-for="item in uartParityOptions"
|
||||||
|
:key="item.key"
|
||||||
|
:value="item.key"
|
||||||
|
:label="item.label"
|
||||||
|
/>
|
||||||
|
</el-select>
|
||||||
|
</el-form-item>
|
||||||
|
|
||||||
|
<el-form-item :label="translate('uart.stopBits')">
|
||||||
|
<el-select v-model="store.uartConfig.stop_bits" :teleported="false"
|
||||||
|
placeholder="Select" @change="onUartConfigChange">
|
||||||
|
<el-option
|
||||||
|
v-for="item in uartStopBitsOptions"
|
||||||
|
:key="item.key"
|
||||||
|
:value="item.key"
|
||||||
|
:label="item.label"
|
||||||
|
/>
|
||||||
|
</el-select>
|
||||||
|
</el-form-item>
|
||||||
|
|
||||||
|
<div class="flex justify-center">
|
||||||
|
<el-button :type="store.acceptIncomingData ? 'danger': 'success'"
|
||||||
|
:disabled="wsStore.state !== ControlEvent.CONNECTED"
|
||||||
|
@click="store.acceptIncomingData = !store.acceptIncomingData"
|
||||||
|
>
|
||||||
|
{{ store.acceptIncomingData ? $t("uart.stopCommunication") : $t("uart.startCommunication") }}
|
||||||
|
</el-button>
|
||||||
|
</div>
|
||||||
|
</el-form>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
</el-tab-pane>
|
||||||
|
<!-- ///////////////////////////////////////////////////////////////// -->
|
||||||
|
<el-tab-pane name="second">
|
||||||
|
<template #label>{{ $t("uart.displayPannel") }}</template>
|
||||||
|
<div class="flex flex-col">
|
||||||
|
<el-collapse v-model="collapseActiveName">
|
||||||
|
<el-collapse-item name="1">
|
||||||
|
<template #title>
|
||||||
|
{{ $t('uart.displayOptions') }}
|
||||||
|
</template>
|
||||||
|
<template #default>
|
||||||
|
<div class="flex flex-col gap-2">
|
||||||
|
<div class="flex flex-col">
|
||||||
|
<el-checkbox border v-model="store.showText" :label="translate('uart.text')"/>
|
||||||
|
</div>
|
||||||
|
<div class="flex flex-col">
|
||||||
|
<el-checkbox border v-model="store.showHex" label="HEX"/>
|
||||||
|
</div>
|
||||||
|
<div class="flex flex-col">
|
||||||
|
<el-checkbox border v-model="store.showHexdump" label="HEXDUMP"/>
|
||||||
|
</div>
|
||||||
|
<div class="flex flex-col">
|
||||||
|
<el-checkbox border v-model="store.showTimestamp" :label="translate('uart.timestamp')"/>
|
||||||
|
</div>
|
||||||
|
<div class="flex flex-col">
|
||||||
|
<el-checkbox border v-model="store.enableLineWrap" :label="translate('uart.lineWrap')"/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<el-tag type="success">
|
||||||
|
<el-text type="success">RX HEXDUMP {{ $t("uart.highlight") }}</el-text>
|
||||||
|
<el-color-picker v-model="store.RxHexdumpColor" show-alpha :predefine="store.predefineColors"
|
||||||
|
size="small"/>
|
||||||
|
</el-tag>
|
||||||
|
|
||||||
|
<el-tag type="primary">
|
||||||
|
<el-text type="primary">TX HEXDUMP {{ $t("uart.highlight") }}</el-text>
|
||||||
|
<el-color-picker v-model="store.TxHexdumpColor" show-alpha :predefine="store.predefineColors"
|
||||||
|
size="small"/>
|
||||||
|
</el-tag>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</el-collapse-item>
|
||||||
|
|
||||||
|
<el-collapse-item name="2" :title="translate('uart.frameBreakStrategy')">
|
||||||
|
<VueDraggable v-model="store.frameBreakRules" target="tbody" handle=".sort-target"
|
||||||
|
:animation="150"
|
||||||
|
:on-move="checkMove">
|
||||||
|
<table class="w-full bg-white">
|
||||||
|
<thead>
|
||||||
|
<tr class="text-sm h-7">
|
||||||
|
<th>{{ $t('uart.priority') }}</th>
|
||||||
|
<th>
|
||||||
|
<div class="flex justify-center">
|
||||||
|
{{ translate('uart.rule' as TranslationKeys) }}
|
||||||
|
<el-tooltip placement="top" effect="light">
|
||||||
|
<template #content>
|
||||||
|
<div v-html="translate('uart.ruleTips')"></div>
|
||||||
|
</template>
|
||||||
|
<InlineSvg name="help" class="w-4 text-gray-500 cursor-help"></InlineSvg>
|
||||||
|
</el-tooltip>
|
||||||
|
</div>
|
||||||
|
</th>
|
||||||
|
<th>{{ translate('uart.value' as TranslationKeys) }}</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody class="text-xs text-center">
|
||||||
|
<tr v-for="(item, index) in store.frameBreakRules" :key="index">
|
||||||
|
<td :class="item.draggable ? 'sort-target' : ''">
|
||||||
|
{{ item.draggable ? index : 'NaN' }}
|
||||||
|
</td>
|
||||||
|
<td :class="item.draggable ? 'sort-target' : ''">
|
||||||
|
{{ translate("uart." + item.name) }}
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<div v-if="item.type === 'number'">
|
||||||
|
<el-input-number v-if="item.name === 'timeout'" v-model="store.frameBreakDelay" :min="item.min || 0" size="small" style="width: 100px"/>
|
||||||
|
<el-input-number v-else v-model="store.frameBreakSize" :min="item.min || 0" size="small" style="width: 100px"/>
|
||||||
|
</div>
|
||||||
|
<div v-else>
|
||||||
|
<el-input class="break-input" v-model="store.frameBreakSequence" :placeholder="translate('uart.textAndEscape')" size="small"
|
||||||
|
style="width: 100px">
|
||||||
|
<template #prepend>
|
||||||
|
<el-button size="small" @click="store.frameBreakAfterSequence = false">
|
||||||
|
<span
|
||||||
|
:class="store.frameBreakAfterSequence ? 'text-gray-400' : 'text-blue-400 font-bold'">
|
||||||
|
{{ translate("uart.begin") }}
|
||||||
|
</span>
|
||||||
|
</el-button>
|
||||||
|
</template>
|
||||||
|
<template #append>
|
||||||
|
<el-button size="small" @click="store.frameBreakAfterSequence = true">
|
||||||
|
<span
|
||||||
|
:class="store.frameBreakAfterSequence ? 'text-blue-400 font-bold' : 'text-gray-300'">
|
||||||
|
{{ translate("uart.end") }}
|
||||||
|
</span>
|
||||||
|
</el-button>
|
||||||
|
</template>
|
||||||
|
</el-input>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</VueDraggable>
|
||||||
|
</el-collapse-item>
|
||||||
|
|
||||||
|
<el-collapse-item name="3" :title="translate('uart.other')">
|
||||||
|
<template #default>
|
||||||
|
<div class="flex flex-col gap-2">
|
||||||
|
<el-tooltip
|
||||||
|
class="box-item"
|
||||||
|
effect="light"
|
||||||
|
placement="right-start"
|
||||||
|
>
|
||||||
|
<template #content>
|
||||||
|
<div v-html="translate('uart.ansiTooltips')"></div>
|
||||||
|
</template>
|
||||||
|
<el-checkbox border v-model="store.enableAnsiDecode">{{ translate('uart.decodeAnsiEscapeCodes') }}</el-checkbox>
|
||||||
|
</el-tooltip>
|
||||||
|
<el-input v-model="store.filterValue" :placeholder="translate('uart.textAndEscape')" clearable>
|
||||||
|
<template #prepend>
|
||||||
|
{{ translate("uart.filter") }}
|
||||||
|
</template>
|
||||||
|
</el-input>
|
||||||
|
|
||||||
|
<div class="border rounded flex flex-col">
|
||||||
|
|
||||||
|
<el-checkbox border v-model="store.dataFilterAutoUpdate">{{ translate('uart.autoUpdateNewData') }}</el-checkbox>
|
||||||
|
|
||||||
|
<el-tooltip :content="translate('uart.updateFrequencyTooltip')" placement="right" effect="light"
|
||||||
|
:show-after="500">
|
||||||
|
<div class="flex gap-4 p-2">
|
||||||
|
<el-text>{{ translate('uart.updateFrequency') }}</el-text>
|
||||||
|
<el-input-number
|
||||||
|
:step="10"
|
||||||
|
:min="10"
|
||||||
|
size="small"
|
||||||
|
v-model="store.batchUpdateTime"
|
||||||
|
>
|
||||||
|
</el-input-number>
|
||||||
|
</div>
|
||||||
|
</el-tooltip>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</template>
|
||||||
|
</el-collapse-item>
|
||||||
|
</el-collapse>
|
||||||
|
|
||||||
|
<!-- <div class="flex flex-col">-->
|
||||||
|
<!-- <el-text type="success">断帧设置</el-text>-->
|
||||||
|
<!-- <el-input v-model="store.frameBreakSequence" class="max-w-52">-->
|
||||||
|
<!-- <template #prepend>-->
|
||||||
|
<!-- 文本匹配断帧-->
|
||||||
|
<!-- </template>-->
|
||||||
|
<!-- </el-input>-->
|
||||||
|
<!-- <el-input v-model="store.frameBreakDelay" type="number" class="max-w-52">-->
|
||||||
|
<!-- <template #prepend>-->
|
||||||
|
<!-- 超时断帧-->
|
||||||
|
<!-- </template>-->
|
||||||
|
<!-- </el-input>-->
|
||||||
|
<!-- </div>-->
|
||||||
|
|
||||||
|
|
||||||
|
<!-- <div class="flex flex-col flex-wrap">-->
|
||||||
|
<!-- <el-button size="small">滚动到底</el-button>-->
|
||||||
|
|
||||||
|
|
||||||
|
<!-- <div>显示-->
|
||||||
|
<!-- <el-checkbox size="small" border>数据差异高亮</el-checkbox>-->
|
||||||
|
<!-- <el-checkbox size="small" border>TX高亮</el-checkbox>-->
|
||||||
|
<!-- <el-checkbox size="small" border>显示RX</el-checkbox>-->
|
||||||
|
<!-- <el-checkbox size="small" border>显示TX</el-checkbox>-->
|
||||||
|
<!-- <el-checkbox size="small" border>RX右对齐</el-checkbox>-->
|
||||||
|
<!-- </div>-->
|
||||||
|
|
||||||
|
<!-- <!– <div>专有协议–>-->
|
||||||
|
<!-- <!– <el-button size="small">输入格式</el-button>–>-->
|
||||||
|
<!-- <!– <el-button size="small">输出格式</el-button>–>-->
|
||||||
|
<!-- <!– </div>–>-->
|
||||||
|
<!-- </div>-->
|
||||||
|
</div>
|
||||||
|
</el-tab-pane>
|
||||||
|
|
||||||
|
<!-- ///////////////////////////////////////////////////////////// -->
|
||||||
|
<el-tab-pane :label="translate('uart.send')" name="third">
|
||||||
|
<template #label>{{ $t("uart.send") }}</template>
|
||||||
|
<div class="flex flex-col gap-2">
|
||||||
|
<el-input v-model="store.textPrefixValue" :placeholder="translate('uart.textAndEscape')" clearable>
|
||||||
|
<template #prepend>
|
||||||
|
{{ translate('uart.addHeader') }}►
|
||||||
|
</template>
|
||||||
|
</el-input>
|
||||||
|
<el-input v-model="store.textSuffixValue" :placeholder="translate('uart.textAndEscape')" clearable>
|
||||||
|
<template #append>
|
||||||
|
◄{{ translate('uart.addFooter') }}
|
||||||
|
</template>
|
||||||
|
</el-input>
|
||||||
|
</div>
|
||||||
|
</el-tab-pane>
|
||||||
|
|
||||||
|
|
||||||
|
<el-tab-pane :label="translate('uart.proxy')" name="fourth" class="min-h-80">
|
||||||
|
<template #label>{{ $t("uart.passthrough") }}</template>
|
||||||
|
<div class="flex flex-col gap-2">
|
||||||
|
<div class="border rounded bg-white p-2">
|
||||||
|
<span class="border-r px-2">TCP {{ translate('uart.serverPort') }}</span>
|
||||||
|
<span class="px-2 cursor-not-allowed">1346</span>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<p><el-button @click="refreshTCPClientList" size="small" type="primary" :plain="true">{{ translate('uart.refresh') }}</el-button> {{ translate('uart.connectedClient') }}</p>
|
||||||
|
|
||||||
|
<el-table :data="dfStore.instanceList.filter((item) => (item.port_info as ISocketInfo).local_port === 1346)" :empty-text="translate('uart.noClientConnected')">
|
||||||
|
<el-table-column label="IP" prop="port_info.foreign_ip" />
|
||||||
|
<el-table-column :label="translate('uart.port')" prop="port_info.foreign_port"/>
|
||||||
|
</el-table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</el-tab-pane>
|
||||||
|
</el-tabs>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import {VueDraggable} from 'vue-draggable-plus'
|
||||||
|
import {computed, ref} from "vue";
|
||||||
|
import {useDataViewerStore} from "@/stores/dataViewerStore";
|
||||||
|
import {useWsStore} from "@/stores/websocket";
|
||||||
|
import {globalNotify} from "@/composables/notification";
|
||||||
|
import {ControlEvent} from "@/api";
|
||||||
|
import type {MoveEvent} from "sortablejs";
|
||||||
|
import InlineSvg from "@/components/InlineSvg.vue";
|
||||||
|
import {useDataFlowStore} from "@/stores/useDataFlowStore";
|
||||||
|
import {wt_data_flow_get_instance_list, type ISocketInfo} from "@/api/apiDataFlow";
|
||||||
|
import {uart_set_baud, uart_set_config} from "@/api/apiUart";
|
||||||
|
import {useUartStore} from "@/stores/useUartStore";
|
||||||
|
import {translate, type TranslationKeys} from "@/locales";
|
||||||
|
|
||||||
|
const store = useDataViewerStore()
|
||||||
|
const uartStore = useUartStore()
|
||||||
|
const wsStore = useWsStore()
|
||||||
|
const dfStore = useDataFlowStore()
|
||||||
|
|
||||||
|
const collapseActiveName = ref(["1", "2", "3"])
|
||||||
|
|
||||||
|
const uartCustomBaud = ref(1500000)
|
||||||
|
|
||||||
|
const uartDataBitsOptions = [
|
||||||
|
{
|
||||||
|
key: 5,
|
||||||
|
label: "5 bits",
|
||||||
|
}, {
|
||||||
|
key: 6,
|
||||||
|
label: "6 bits",
|
||||||
|
}, {
|
||||||
|
key: 7,
|
||||||
|
label: "7 bits",
|
||||||
|
}, {
|
||||||
|
key: 8,
|
||||||
|
label: "8 bits",
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
const uartParityOptions = computed(() => [
|
||||||
|
{
|
||||||
|
key: 0,
|
||||||
|
label: translate("uart.parityNone"),
|
||||||
|
}, {
|
||||||
|
key: 1,
|
||||||
|
label: translate("uart.parityOdd"),
|
||||||
|
}, {
|
||||||
|
key: 2,
|
||||||
|
label: translate("uart.parityEven"),
|
||||||
|
}
|
||||||
|
]);
|
||||||
|
|
||||||
|
|
||||||
|
const uartStopBitsOptions = [
|
||||||
|
{
|
||||||
|
key: 1,
|
||||||
|
label: "1",
|
||||||
|
}, {
|
||||||
|
key: 15,
|
||||||
|
label: "1.5",
|
||||||
|
}, {
|
||||||
|
key: 2,
|
||||||
|
label: "2",
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
const onUseCustomUartBaud = () => {
|
||||||
|
if (uartCustomBaud.value) {
|
||||||
|
store.uartBaud = uartCustomBaud.value;
|
||||||
|
onUartBaudChange();
|
||||||
|
} else {
|
||||||
|
globalNotify("波特率格式错误", "warning")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function onUartBaudChange() {
|
||||||
|
uart_set_baud(store.uartBaud, uartStore.uartNum);
|
||||||
|
}
|
||||||
|
|
||||||
|
function onUartConfigChange() {
|
||||||
|
uart_set_config(store.uartConfig, uartStore.uartNum);
|
||||||
|
}
|
||||||
|
|
||||||
|
function checkMove(event: MoveEvent) {
|
||||||
|
// Find index of related element
|
||||||
|
const toIndex: number = Array.from(event.to.children).indexOf(event.related);
|
||||||
|
return !!store.frameBreakRules[toIndex].draggable;
|
||||||
|
}
|
||||||
|
|
||||||
|
function refreshTCPClientList() {
|
||||||
|
dfStore.instanceList = [];
|
||||||
|
wt_data_flow_get_instance_list();
|
||||||
|
}
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.custom-tabs {
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
.custom-tabs :deep(.el-tabs__item.is-top) {
|
||||||
|
padding: unset;
|
||||||
|
}
|
||||||
|
|
||||||
|
.custom-tabs :deep(.el-tabs__nav.is-top) {
|
||||||
|
@apply w-full flex justify-around
|
||||||
|
}
|
||||||
|
|
||||||
|
.custom-tabs :deep(.el-collapse-item__wrap) {
|
||||||
|
transition: all 0s; /* Customize the duration and easing */
|
||||||
|
}
|
||||||
|
|
||||||
|
.sortable-chosen {
|
||||||
|
background-color: var(--el-color-primary-light-9);
|
||||||
|
}
|
||||||
|
|
||||||
|
.sort-target {
|
||||||
|
cursor: move;
|
||||||
|
}
|
||||||
|
|
||||||
|
tr td {
|
||||||
|
@apply p-1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.break-input :deep(.el-input-group__prepend), .break-input :deep(.el-input-group__append) {
|
||||||
|
background-color: unset;
|
||||||
|
@apply p-0 min-w-6
|
||||||
|
}
|
||||||
|
|
||||||
|
</style>
|
|
@ -0,0 +1,182 @@
|
||||||
|
<template>
|
||||||
|
<div class="flex items-center mb-2 flex-wrap gap-2">
|
||||||
|
<el-button type="primary" @click="importSettings">{{ translate('uart.import') }}</el-button>
|
||||||
|
<el-button type="warning" @click="exportSettings">{{ translate('uart.export') }}</el-button>
|
||||||
|
|
||||||
|
<el-tooltip
|
||||||
|
effect="light"
|
||||||
|
placement="top"
|
||||||
|
:show-after="500"
|
||||||
|
>
|
||||||
|
<template #content>
|
||||||
|
<p>{{ translate('uart.resetTooltip') }}</p>
|
||||||
|
</template>
|
||||||
|
<el-button type="info" @click="resetSettings">{{ translate('uart.reset') }}</el-button>
|
||||||
|
</el-tooltip>
|
||||||
|
<el-tooltip
|
||||||
|
effect="light"
|
||||||
|
placement="top"
|
||||||
|
:show-after="500"
|
||||||
|
>
|
||||||
|
<template #content>
|
||||||
|
<p>{{ translate('uart.saveToLocalTooltip') }}</p>
|
||||||
|
</template>
|
||||||
|
<el-checkbox border v-model="store.autoSaveSettings">{{ translate('uart.saveToLocal') }}</el-checkbox>
|
||||||
|
</el-tooltip>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex items-center mb-2 flex-wrap gap-2">
|
||||||
|
<el-button type="primary" @click="() => {
|
||||||
|
store.macroData.push({
|
||||||
|
value: '',
|
||||||
|
label: translate('uart.send'),
|
||||||
|
id: store.macroId,
|
||||||
|
})
|
||||||
|
store.macroId++;
|
||||||
|
}">{{ translate('uart.add') }}
|
||||||
|
</el-button>
|
||||||
|
<el-checkbox v-model="editMode" border>{{ translate('uart.edit') }}</el-checkbox>
|
||||||
|
<el-checkbox v-model="draggableEnabled" border>{{ translate('uart.drag') }}</el-checkbox>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<el-alert v-if="store.ipChangeAlert" @close="store.ipChangeAlert=false">{{ translate('uart.ipChangeAlert') }}</el-alert>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<VueDraggable v-model="store.macroData" handle=".sort-target"
|
||||||
|
:animation="150" class="break-input">
|
||||||
|
<div v-for="(item, index) in store.macroData" :key="item.id" class="w-full text-xs flex items-center py-0.5"
|
||||||
|
:class="editMode ? 'macroButtons' : ''">
|
||||||
|
<el-tag size="large" type="success" v-if="draggableEnabled" class="sort-target mr-1">
|
||||||
|
=
|
||||||
|
</el-tag>
|
||||||
|
<el-input v-model="item.value" class="font-mono">
|
||||||
|
<template #append>
|
||||||
|
<el-input v-if="editMode" v-model="item.label"></el-input>
|
||||||
|
<el-button v-else @click="onSendClick(item.value)" type="primary">{{ item.label }}</el-button>
|
||||||
|
</template>
|
||||||
|
</el-input>
|
||||||
|
<el-link :underline="false" @click="store.macroData.splice(index, 1);">
|
||||||
|
<el-tag size="large" type="danger" v-if="editMode" class="ml-1">
|
||||||
|
x
|
||||||
|
</el-tag>
|
||||||
|
</el-link>
|
||||||
|
</div>
|
||||||
|
</VueDraggable>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import {VueDraggable} from "vue-draggable-plus";
|
||||||
|
import {onMounted, ref} from "vue";
|
||||||
|
import {globalNotify, globalNotifyRightSide} from "@/composables/notification";
|
||||||
|
import {useDataViewerStore} from "@/stores/dataViewerStore";
|
||||||
|
import {translate} from "@/locales";
|
||||||
|
|
||||||
|
const editMode = ref(false);
|
||||||
|
const draggableEnabled = ref(true);
|
||||||
|
const store = useDataViewerStore();
|
||||||
|
const emit = defineEmits(['winSizeRefresh'])
|
||||||
|
|
||||||
|
function onSendClick(val: string) {
|
||||||
|
if (!val && !store.hasAddedText) {
|
||||||
|
globalNotify("无帧头帧尾、发送框无数据发送")
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (store.acceptIncomingData) {
|
||||||
|
store.addString(val, false, true);
|
||||||
|
} else {
|
||||||
|
store.addString(val, false, true, 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function importSettings() {
|
||||||
|
const input = document.createElement('input');
|
||||||
|
input.type = 'file';
|
||||||
|
input.accept = 'application/json';
|
||||||
|
|
||||||
|
input.onchange = (event: Event) => {
|
||||||
|
const target = event.target as HTMLInputElement;
|
||||||
|
if (!target.files) return;
|
||||||
|
const file = target.files[0];
|
||||||
|
|
||||||
|
const reader = new FileReader();
|
||||||
|
reader.onload = (e: ProgressEvent<FileReader>) => {
|
||||||
|
const text = e.target?.result;
|
||||||
|
if (typeof text !== 'string') return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
store.loadSettings(text);
|
||||||
|
emit('winSizeRefresh', '');
|
||||||
|
} catch (error) {
|
||||||
|
globalNotifyRightSide('导入失败', "error");
|
||||||
|
console.log("error", error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
reader.readAsText(file);
|
||||||
|
};
|
||||||
|
|
||||||
|
input.click();
|
||||||
|
}
|
||||||
|
|
||||||
|
function exportSettings() {
|
||||||
|
let obj = {
|
||||||
|
version: "v0.1.0",
|
||||||
|
|
||||||
|
/* Macro Window */
|
||||||
|
...store.settings
|
||||||
|
};
|
||||||
|
|
||||||
|
const dataStr = JSON.stringify(obj, null, 2);
|
||||||
|
const blob = new Blob([dataStr], { type: 'application/json' });
|
||||||
|
const url = URL.createObjectURL(blob);
|
||||||
|
const link = document.createElement('a');
|
||||||
|
link.href = url;
|
||||||
|
link.download = "settingsBackup.json";
|
||||||
|
document.body.appendChild(link);
|
||||||
|
link.click();
|
||||||
|
document.body.removeChild(link);
|
||||||
|
URL.revokeObjectURL(url);
|
||||||
|
}
|
||||||
|
|
||||||
|
function resetSettings() {
|
||||||
|
localStorage.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
store.loadSettings();
|
||||||
|
});
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.sortable-chosen {
|
||||||
|
background-color: var(--el-color-primary-light-7);
|
||||||
|
}
|
||||||
|
|
||||||
|
.sort-target {
|
||||||
|
cursor: move;
|
||||||
|
}
|
||||||
|
|
||||||
|
.macroButtons :deep(.el-input-group__append) {
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.break-input :deep(.el-input-group__append) {
|
||||||
|
background-color: unset;
|
||||||
|
border-color: unset;
|
||||||
|
color: unset;
|
||||||
|
}
|
||||||
|
|
||||||
|
.break-input :deep(.el-input-group__append button.el-button) {
|
||||||
|
background-color: var(--el-color-primary-light-9);
|
||||||
|
border-color: var(--el-border-color);
|
||||||
|
color: unset;
|
||||||
|
border-radius: 0 5px 5px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.break-input :deep(.el-input-group__append button.el-button):hover {
|
||||||
|
background-color: var(--el-color-primary-light-7);
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -0,0 +1,457 @@
|
||||||
|
<template>
|
||||||
|
<div class="flex min-h-7 overflow-auto">
|
||||||
|
<el-popover
|
||||||
|
placement="bottom"
|
||||||
|
trigger="click"
|
||||||
|
:hide-after="0"
|
||||||
|
transition="none"
|
||||||
|
width="300"
|
||||||
|
>
|
||||||
|
<div v-if="showDataConfig" class="h-[40vh] overflow-auto">
|
||||||
|
<text-data-config></text-data-config>
|
||||||
|
</div>
|
||||||
|
<template #reference>
|
||||||
|
<el-link v-show="showDataConfig" type="primary">
|
||||||
|
<InlineSvg name="arrow_drop_down" class="h-6 mb-1 px-2"></InlineSvg>
|
||||||
|
</el-link>
|
||||||
|
</template>
|
||||||
|
</el-popover>
|
||||||
|
|
||||||
|
<div class="flex">
|
||||||
|
<el-checkbox size="small" v-model="store.forceToBottom" :label="translate('uart.autoScrollToBottom')" border/>
|
||||||
|
<el-tooltip
|
||||||
|
class="box-item"
|
||||||
|
effect="light"
|
||||||
|
placement="top"
|
||||||
|
>
|
||||||
|
<template #content>
|
||||||
|
<p>{{ translate('uart.clearTooltip') }}</p>
|
||||||
|
</template>
|
||||||
|
<el-button size="small" @click="store.clearFilteredBuff">
|
||||||
|
<InlineSvg class="h-5" name="trash"></InlineSvg>
|
||||||
|
{{ $t('uart.clearScreen') }} ⇩
|
||||||
|
</el-button>
|
||||||
|
</el-tooltip>
|
||||||
|
|
||||||
|
<el-tooltip
|
||||||
|
class="box-item"
|
||||||
|
effect="light"
|
||||||
|
placement="top"
|
||||||
|
>
|
||||||
|
<template #content>
|
||||||
|
<p>{{ translate('uart.clearTooltip') }}</p>
|
||||||
|
</template>
|
||||||
|
<el-button size="small" @click="store.refreshFilteredBuff">
|
||||||
|
{{ $t('page.update') }}
|
||||||
|
</el-button>
|
||||||
|
</el-tooltip>
|
||||||
|
<el-tooltip
|
||||||
|
class="box-item"
|
||||||
|
effect="light"
|
||||||
|
placement="top"
|
||||||
|
>
|
||||||
|
<template #content>
|
||||||
|
<p>{{ translate('uart.autoUpdateTooltip') }}</p>
|
||||||
|
</template>
|
||||||
|
<el-checkbox size="small" border v-model="store.dataFilterAutoUpdate">
|
||||||
|
{{ $t('uart.autoUpdate') }}
|
||||||
|
</el-checkbox>
|
||||||
|
</el-tooltip>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex flex-grow overflow-hidden border-2 rounded scroll-m-2">
|
||||||
|
<v-virtual-scroll
|
||||||
|
v-if="store.showVirtualScroll"
|
||||||
|
:items="store.dataFiltered"
|
||||||
|
id="myScrollerID"
|
||||||
|
ref="vuetifyVirtualScrollRef"
|
||||||
|
class="font-mono break-all text-sm"
|
||||||
|
:class="[store.enableLineWrap ? 'break-all' : 'text-nowrap']"
|
||||||
|
>
|
||||||
|
<template v-slot:default="{ item, }">
|
||||||
|
<div class="">
|
||||||
|
<div class="flex" :class="[store.enableLineWrap ? 'whitespace-pre-wrap' : 'whitespace-pre']">
|
||||||
|
<p class="text-nowrap text-sm text-lime-500" v-if="item.isRX" type="success" v-show="store.showTimestamp">
|
||||||
|
<span>{{ item.time }}</span>◄-RX|</p>
|
||||||
|
<p class="text-nowrap text-sm text-sky-500" v-else-if="item.type === 0" type="primary" v-show="store.showTimestamp">
|
||||||
|
<span>{{ item.time }}</span>TX-►|</p>
|
||||||
|
<p class="text-nowrap text-sm text-amber-800" v-else type="primary" v-show="store.showTimestamp">
|
||||||
|
<span>{{ item.time }}</span>NS-►|</p>
|
||||||
|
|
||||||
|
<p v-show="store.showText"
|
||||||
|
v-html="item.str"></p>
|
||||||
|
</div>
|
||||||
|
<div class="flex">
|
||||||
|
<p v-show="store.showHex" class="">{{ item.hex }}</p>
|
||||||
|
</div>
|
||||||
|
<div class="flex whitespace-pre">
|
||||||
|
<p v-show="store.showHexdump"
|
||||||
|
class="text-nowrap"
|
||||||
|
:style="{ 'background-color': item.isRX ? store.RxHexdumpColor : store.TxHexdumpColor }"
|
||||||
|
v-html="item.hexdump"
|
||||||
|
></p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</v-virtual-scroll>
|
||||||
|
<v-virtual-scroll
|
||||||
|
v-else
|
||||||
|
:items="store.dataFiltered"
|
||||||
|
id="myScrollerID"
|
||||||
|
ref="vuetifyVirtualScrollRef2"
|
||||||
|
class="font-mono break-all text-sm"
|
||||||
|
:class="[store.enableLineWrap ? 'break-all' : 'text-nowrap']"
|
||||||
|
>
|
||||||
|
<template v-slot:default="{ item, }">
|
||||||
|
<div>
|
||||||
|
<div class="flex" :class="[store.enableLineWrap ? 'whitespace-pre-wrap' : 'whitespace-pre']">
|
||||||
|
<p class="text-nowrap text-sm text-lime-500" v-if="item.isRX" type="success" v-show="store.showTimestamp">
|
||||||
|
<span>{{ item.time }}</span>◄-RX|</p>
|
||||||
|
<p class="text-nowrap text-sm text-sky-500" v-else-if="item.type === 0" type="primary" v-show="store.showTimestamp">
|
||||||
|
<span>{{ item.time }}</span>TX-►|</p>
|
||||||
|
<p class="text-nowrap text-sm text-amber-800" v-else type="primary" v-show="store.showTimestamp">
|
||||||
|
<span>{{ item.time }}</span>NS-►|</p>
|
||||||
|
<p v-show="store.showText"
|
||||||
|
v-html="item.str"></p>
|
||||||
|
</div>
|
||||||
|
<div class="flex">
|
||||||
|
<p v-show="store.showHex" class="">{{ item.hex }}</p>
|
||||||
|
</div>
|
||||||
|
<div class="flex whitespace-pre">
|
||||||
|
<p v-show="store.showHexdump"
|
||||||
|
class="text-nowrap"
|
||||||
|
:style="{ 'background-color': item.isRX ? store.RxHexdumpColor : store.TxHexdumpColor }"
|
||||||
|
v-html="item.hexdump"
|
||||||
|
></p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</v-virtual-scroll>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="shrink-0 flex h-8 mt-0.5 text-xs">
|
||||||
|
<div class="flex shrink-0">
|
||||||
|
<el-tooltip :content="translate('uart.tempDisplayTooltip')" effect="light">
|
||||||
|
<InlineSvg name="help" class="w-3.5 h-3.5 text-gray-500 cursor-help"></InlineSvg>
|
||||||
|
</el-tooltip>
|
||||||
|
<p>►</p>
|
||||||
|
</div>
|
||||||
|
<div ref="RxHexDumpRef" class="p-0.5 border-2 rounded w-full overflow-y-scroll font-mono text-nowrap">
|
||||||
|
<p v-html="store.RxRemainHexdump"></p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="shrink-0 min-h-6 flex gap-2 justify-between overflow-auto">
|
||||||
|
<div class="flex gap-2">
|
||||||
|
<el-link @click="clearSendInput">
|
||||||
|
<el-tag class="font-mono" size="small">
|
||||||
|
<div class="flex ">
|
||||||
|
<InlineSvg class="h-5" name="trash"></InlineSvg>
|
||||||
|
<span class="content-end text-xs">⇩</span>
|
||||||
|
</div>
|
||||||
|
</el-tag>
|
||||||
|
</el-link>
|
||||||
|
|
||||||
|
<el-tooltip :content="translate('uart.loopSendTooltip')" placement="right" effect="light" :show-after="1000">
|
||||||
|
<div class="flex align-center">
|
||||||
|
<el-checkbox v-model="store.enableLoopSend" class="font-mono font-bold max-h-5" size="small" border>
|
||||||
|
{{ translate('uart.loopSend') }}(ms)
|
||||||
|
</el-checkbox>
|
||||||
|
<el-input-number
|
||||||
|
v-model="store.loopSendFreq"
|
||||||
|
class="h-5"
|
||||||
|
size="small"
|
||||||
|
:step="10"
|
||||||
|
:min="1"
|
||||||
|
>
|
||||||
|
</el-input-number>
|
||||||
|
</div>
|
||||||
|
</el-tooltip>
|
||||||
|
|
||||||
|
<el-link @click="store.isSendTextFormat = !store.isSendTextFormat">
|
||||||
|
<el-tag class="font-mono font-bold" size="small">{{ translate('uart.sendFormat') }}:{{ store.isSendTextFormat ? translate("uart.text") : "HEX" }}</el-tag>
|
||||||
|
</el-link>
|
||||||
|
</div>
|
||||||
|
<div class="flex gap-2">
|
||||||
|
<el-link @click="store.clearTxCounter()">
|
||||||
|
<el-tag class="font-mono font-bold" size="small">
|
||||||
|
{{ `TX(B):${store.TxByteCount}/ ${store.TxTotalByteCount}` }}
|
||||||
|
</el-tag>
|
||||||
|
</el-link>
|
||||||
|
<el-link type="success" @click="store.clearRxCounter()">
|
||||||
|
<el-tag class="font-mono font-bold" size="small" type="success">
|
||||||
|
{{ `RX(B):${store.RxByteCount}/ ${store.RxTotalByteCount}` }}
|
||||||
|
</el-tag>
|
||||||
|
</el-link>
|
||||||
|
<div class="flex align-center">
|
||||||
|
<el-tag class="font-mono font-bold" size="small" type="info">
|
||||||
|
<el-link class="flex" @click="store.clearDataBuff" type="warning">
|
||||||
|
<InlineSvg class="h-5" name="trash"></InlineSvg>
|
||||||
|
</el-link>
|
||||||
|
<span class="align-text-bottom">{{ translate('uart.cachedFrame') }}: {{ store.dataBufLength }}/30000</span>
|
||||||
|
</el-tag>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="flex flex-row font-mono">
|
||||||
|
<el-input type="textarea" :autosize="{ minRows: 1, maxRows: 6}" v-model="store.uartInputTextBox" clearable
|
||||||
|
:placeholder="store.isSendTextFormat ?
|
||||||
|
translate('uart.textAndEscape') :
|
||||||
|
'HEX'"
|
||||||
|
@keydown="handleTextboxKeydown"
|
||||||
|
></el-input>
|
||||||
|
<el-tooltip content="Ctrl+Enter" placement="top" :auto-close="500">
|
||||||
|
<el-button type="primary"
|
||||||
|
@click="onSendClick">
|
||||||
|
{{ (store.isSendTextFormat || store.isHexStringValid) ? translate("uart.send") : translate("格式化") }}
|
||||||
|
</el-button>
|
||||||
|
</el-tooltip>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import {nextTick, onMounted, onUnmounted, ref, watch} from "vue";
|
||||||
|
import {useDataViewerStore} from "@/stores/dataViewerStore";
|
||||||
|
import InlineSvg from "@/components/InlineSvg.vue";
|
||||||
|
import TextDataConfig from "@/views/text-data-viewer/textDataConfig.vue";
|
||||||
|
import {debouncedWatch} from "@vueuse/core";
|
||||||
|
import {globalNotify} from "@/composables/notification";
|
||||||
|
import {translate} from "@/locales";
|
||||||
|
import type { DraggableComponent } from '@/types/grid'
|
||||||
|
|
||||||
|
defineOptions({
|
||||||
|
name: 'TextDataViewer',
|
||||||
|
widgetIconName: 'text-data'
|
||||||
|
})
|
||||||
|
|
||||||
|
withDefaults(defineProps<{
|
||||||
|
editCell?: boolean
|
||||||
|
showDataConfig?: boolean
|
||||||
|
}>(), {
|
||||||
|
editCell: false,
|
||||||
|
showDataConfig: true,
|
||||||
|
})
|
||||||
|
|
||||||
|
const modelValue = defineModel<DraggableComponent>({ required: false })
|
||||||
|
|
||||||
|
|
||||||
|
const count = ref(0);
|
||||||
|
const vuetifyVirtualScrollBarRef = ref(document.body);
|
||||||
|
const vuetifyVirtualScrollContainerRef = ref(document.body);
|
||||||
|
|
||||||
|
const store = useDataViewerStore();
|
||||||
|
|
||||||
|
const RxHexDumpRef = ref(document.body);
|
||||||
|
|
||||||
|
let lastScrollHeight = 0;
|
||||||
|
|
||||||
|
const mutationObserver = new MutationObserver(() => {
|
||||||
|
if (store.forceToBottom) {
|
||||||
|
lastScrollHeight = vuetifyVirtualScrollBarRef.value.scrollTop;
|
||||||
|
scrollToBottom();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
function attachScroll() {
|
||||||
|
const parent = document.getElementById('myScrollerID') || document.body;
|
||||||
|
|
||||||
|
// used to scroll to bottom
|
||||||
|
vuetifyVirtualScrollBarRef.value = parent || document.body;
|
||||||
|
|
||||||
|
// used to monitor height changes, so that one new Items are rendered, the height change -> scroll to bottom
|
||||||
|
vuetifyVirtualScrollContainerRef.value = parent.querySelector('.v-virtual-scroll__container') || document.body;
|
||||||
|
|
||||||
|
vuetifyVirtualScrollBarRef.value.onscroll = handleScroll;
|
||||||
|
|
||||||
|
if (vuetifyVirtualScrollContainerRef.value) {
|
||||||
|
const config = {childList: true, subtree: true, attributes: true};
|
||||||
|
mutationObserver.observe(vuetifyVirtualScrollBarRef.value, config)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (store.forceToBottom) {
|
||||||
|
scrollToBottom();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
attachScroll();
|
||||||
|
})
|
||||||
|
|
||||||
|
onUnmounted(() => {
|
||||||
|
mutationObserver.disconnect();
|
||||||
|
if (!store.isHexStringValid) {
|
||||||
|
store.uartInputTextBox = formatHexInput(store.uartInputTextBox);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
debouncedWatch(() => store.showVirtualScroll, () => {
|
||||||
|
lastScrollHeight = 0;
|
||||||
|
mutationObserver.disconnect();
|
||||||
|
attachScroll();
|
||||||
|
}, {debounce: 80});
|
||||||
|
|
||||||
|
|
||||||
|
function addItem(nr: number) {
|
||||||
|
let rawText = "";
|
||||||
|
|
||||||
|
let maxcount = count.value + nr;
|
||||||
|
|
||||||
|
for (; count.value < maxcount; count.value++) {
|
||||||
|
let text = "";
|
||||||
|
if ((count.value & 3) === 3) {
|
||||||
|
text = `<p class="border-4">${count.value}inputasdf<br/>${count.value}asdf <br/>${count.value}asdfasdf <br/>${count.value}asdf </p>`;
|
||||||
|
text += text;
|
||||||
|
|
||||||
|
} else if ((count.value & 2) === 2) {
|
||||||
|
text = `<p class="border-4">${count.value}inputas df2<br/>${count.value} asdf asd <br/>${count.value}fasdf asdf </p>`;
|
||||||
|
text += text;
|
||||||
|
} else if ((count.value & 1) === 1) {
|
||||||
|
text = `<p class="border-4">${count.value}inputas df2asdf asd <br/>${count.value}fasdf asdf ${count.value} </p>`;
|
||||||
|
text += text;
|
||||||
|
} else {
|
||||||
|
text = `<p class="border-4">${count.value}inputasa<br/>jdhfklasjdhfklasdhflasidfhilasdfhlasdiufhlasdkfhuasnlfcyerhfcibnkuaweghnfctiklaweuyrchnlaweirtucgnawertkcgyawertcnawelcrvnawgervcawencrgf${count.value} </p>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
rawText = count.value + "<p class=\"border-4\"> 666666666b\n6666 666\x1b[33m6666666666666666666666666</p>b\n"
|
||||||
|
const encoder = new TextEncoder();
|
||||||
|
const arr = encoder.encode(rawText);
|
||||||
|
store.addItem(arr, false, false, 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function scrollToBottom() {
|
||||||
|
nextTick(() => {
|
||||||
|
const scrollerElement = vuetifyVirtualScrollBarRef.value; // Adjust according to your setup
|
||||||
|
scrollerElement.scrollTop = scrollerElement.scrollHeight;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatHexInput(input: string) {
|
||||||
|
// Split the input string on spaces to process each segment separately
|
||||||
|
let str;
|
||||||
|
|
||||||
|
// Remove any "0x" prefix and handle uppercase conversion
|
||||||
|
str = input.replace(/^0x/i, ' ').toUpperCase();
|
||||||
|
|
||||||
|
// Remove any non-hexadecimal characters
|
||||||
|
str = str.replace(/[^0-9A-F]/gi, ' ');
|
||||||
|
|
||||||
|
let segments = str.split(/\s+/);
|
||||||
|
let output: string[] = [];
|
||||||
|
|
||||||
|
segments.forEach(segment => {
|
||||||
|
// Check if segment length is odd and needs padding
|
||||||
|
if (segment.length % 2 !== 0) {
|
||||||
|
segment = '0' + segment; // Prepend '0' to make the length even
|
||||||
|
}
|
||||||
|
|
||||||
|
// Split segment into array of two-character chunks
|
||||||
|
let chunked = [];
|
||||||
|
for (let i = 0; i < segment.length; i += 2) {
|
||||||
|
chunked.push(segment.substring(i, i + 2));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Concatenate chunked segments and add to output
|
||||||
|
output.push(chunked.join(' '));
|
||||||
|
});
|
||||||
|
|
||||||
|
// Join all processed segments with a space and return
|
||||||
|
return output.join(' ');
|
||||||
|
}
|
||||||
|
|
||||||
|
function checkHexTextValid() {
|
||||||
|
store.isHexStringValid = store.uartInputTextBox.toUpperCase() === formatHexInput(store.uartInputTextBox);
|
||||||
|
if (!store.isHexStringValid) {
|
||||||
|
store.enableLoopSend = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
watch(() => store.isSendTextFormat, (value) => {
|
||||||
|
if (!value) {
|
||||||
|
checkHexTextValid()
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
watch(() => store.uartInputTextBox, () => {
|
||||||
|
if (!store.isSendTextFormat) {
|
||||||
|
checkHexTextValid()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
/* patch scroll container does not update clear filter */
|
||||||
|
watch(() => store.filterChanged, (value) => {
|
||||||
|
if (value && store.forceToBottom) {
|
||||||
|
scrollToBottom();
|
||||||
|
}
|
||||||
|
store.filterChanged = false;
|
||||||
|
})
|
||||||
|
|
||||||
|
watch(() => store.RxRemainHexdump, value => {
|
||||||
|
if (value) {
|
||||||
|
RxHexDumpRef.value.scrollTop = RxHexDumpRef.value.scrollHeight;
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
watch(() => store.showVirtualScroll, () => {
|
||||||
|
if (store.forceToBottom) {
|
||||||
|
scrollToBottom();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const handleScroll = () => {
|
||||||
|
if (store.forceToBottom) {
|
||||||
|
if (vuetifyVirtualScrollBarRef.value.scrollTop - lastScrollHeight < 0) {
|
||||||
|
store.forceToBottom = false;
|
||||||
|
} else {
|
||||||
|
scrollToBottom();
|
||||||
|
}
|
||||||
|
} else if ((vuetifyVirtualScrollBarRef.value.scrollHeight -
|
||||||
|
vuetifyVirtualScrollBarRef.value.scrollTop) <= vuetifyVirtualScrollBarRef.value.clientHeight) {
|
||||||
|
store.forceToBottom = true;
|
||||||
|
}
|
||||||
|
lastScrollHeight = vuetifyVirtualScrollBarRef.value.scrollTop;
|
||||||
|
};
|
||||||
|
|
||||||
|
watch(() => store.forceToBottom, value => {
|
||||||
|
if (value) {
|
||||||
|
setTimeout(scrollToBottom, 0);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
function clearSendInput() {
|
||||||
|
store.uartInputTextBox = ""
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleTextboxKeydown(ev: KeyboardEvent) {
|
||||||
|
if (ev.ctrlKey && ev.key === 'Enter') {
|
||||||
|
onSendClick();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function onSendClick() {
|
||||||
|
if (!store.uartInputTextBox && !store.hasAddedText) {
|
||||||
|
globalNotify("无帧头帧尾、发送框无数据发送")
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (store.acceptIncomingData) {
|
||||||
|
if (store.isSendTextFormat) {
|
||||||
|
store.addString(store.uartInputTextBox, false, true);
|
||||||
|
} else if (!store.isHexStringValid) {
|
||||||
|
store.uartInputTextBox = formatHexInput(store.uartInputTextBox);
|
||||||
|
} else {
|
||||||
|
store.addHexString(store.uartInputTextBox, false, true);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (store.isSendTextFormat) {
|
||||||
|
store.addString(store.uartInputTextBox, false, true, 1);
|
||||||
|
} else if (!store.isHexStringValid) {
|
||||||
|
store.uartInputTextBox = formatHexInput(store.uartInputTextBox);
|
||||||
|
} else {
|
||||||
|
store.addHexString(store.uartInputTextBox, false, true, 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
</script>
|
|
@ -0,0 +1,69 @@
|
||||||
|
<template>
|
||||||
|
<div ref="rootEl" class="p-1 bg-white text-xs border-b-2">
|
||||||
|
<div v-if="props.editCell">
|
||||||
|
<div>
|
||||||
|
<div class="mb-1">
|
||||||
|
<el-input v-model="model.label" placeholder="Label" size="small">
|
||||||
|
<template #prepend>Label</template>
|
||||||
|
</el-input>
|
||||||
|
</div>
|
||||||
|
<div class="mb-1">
|
||||||
|
<el-input v-model="model.command" placeholder="UART Command" size="small">
|
||||||
|
<template #prepend>Command</template>
|
||||||
|
</el-input>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div v-else-if="width < 250" class="">
|
||||||
|
<div class="truncate flex justify-between">
|
||||||
|
<el-text class="font-bold">{{ model.label }}</el-text>
|
||||||
|
<p class="ml-1 font-mono text-gray-500 text-[10px]">{{ model.command }}</p>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<strong>R:</strong>
|
||||||
|
<el-text class="ml-1 font-mono">{{ model.response }}</el-text>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div v-else class="flex text-gray-700">
|
||||||
|
<div class="truncate min-h-8 w-32">
|
||||||
|
<p class="font-bold">{{ model.label }}</p>
|
||||||
|
<p class="font-mono text-gray-500 text-[10px]">{{ model.command }}</p>
|
||||||
|
</div>
|
||||||
|
<div class="pt-0.5">
|
||||||
|
<p class="font-mono text-sm">{{ model.response }}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { defineModel, type PropType, ref } from 'vue'
|
||||||
|
import { ElInput, ElText } from 'element-plus'
|
||||||
|
import { useElementSize } from '@vueuse/core'
|
||||||
|
|
||||||
|
interface UartCommandData {
|
||||||
|
command: string
|
||||||
|
label: string
|
||||||
|
response: string
|
||||||
|
group?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
editCell: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const model = defineModel<UartCommandData>({ required: true })
|
||||||
|
|
||||||
|
const rootEl = ref(null)
|
||||||
|
const { width } = useElementSize(rootEl)
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.el-input :deep(.el-input-group__prepend) {
|
||||||
|
padding: 0 5px;
|
||||||
|
font-size: 10px;
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -0,0 +1,233 @@
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { VueDraggable } from 'vue-draggable-plus'
|
||||||
|
import type { DraggableComponent, WidgetItem } from '../../types/grid'
|
||||||
|
import { ElButton, ElIcon } from 'element-plus'
|
||||||
|
import { markRaw, ref, watch, onMounted, onUnmounted } from 'vue'
|
||||||
|
import { debouncedWatch } from '@vueuse/core'
|
||||||
|
import type { UartCommandData } from '@/types/grid'
|
||||||
|
import UartAtCommand from '@/views/widgets/uartAtCommand.vue'
|
||||||
|
import { useWsStore } from '@/stores/websocket'
|
||||||
|
import { globalNotify } from '@/composables/notification'
|
||||||
|
import { isDevMode } from '@/composables/buildMode'
|
||||||
|
import { useSequentialUart } from '@/composables/useSequentialUart'
|
||||||
|
import { useCommandLoopManager } from '@/composables/useCommandLoopManager'
|
||||||
|
import { translate } from '@/locales'
|
||||||
|
|
||||||
|
/* ---------------- props & model ----------------------------------- */
|
||||||
|
const modelValue = defineModel<WidgetItem>({ required: true })
|
||||||
|
const editCell = defineModel<boolean>('editCell', { required: true })
|
||||||
|
|
||||||
|
defineOptions({
|
||||||
|
name: 'WidgetLoop',
|
||||||
|
widgetIconName: 'repeat'
|
||||||
|
})
|
||||||
|
|
||||||
|
const intervalMS = ref(0)
|
||||||
|
const active = ref(false)
|
||||||
|
|
||||||
|
// Get command loop manager
|
||||||
|
const commandLoopManager = useCommandLoopManager()
|
||||||
|
const { sendCommands } = useSequentialUart()
|
||||||
|
const widgetId = ref(`widget-${modelValue.value.i}`)
|
||||||
|
|
||||||
|
// This watcher handles enabling or disabling the recurring task.
|
||||||
|
watch(active, (isActive) => {
|
||||||
|
if (isActive) {
|
||||||
|
// When activated, register the loop if the interval is valid.
|
||||||
|
const intervalValue = typeof intervalMS.value === 'string' ? parseInt(intervalMS.value, 10) : intervalMS.value;
|
||||||
|
if (intervalValue > 0) {
|
||||||
|
commandLoopManager.registerLoop(widgetId.value, intervalValue, runCommands);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// When deactivated, always unregister the loop.
|
||||||
|
commandLoopManager.unregisterLoop(widgetId.value);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// This watcher handles changes to the interval, but only if the loop is active.
|
||||||
|
debouncedWatch(intervalMS, (newInterval) => {
|
||||||
|
// If the loop isn't active, do nothing. The `active` watcher handles state.
|
||||||
|
if (!active.value) {
|
||||||
|
// If the interval is cleared while inactive, ensure it's unregistered.
|
||||||
|
if (!newInterval || newInterval <= 0) {
|
||||||
|
commandLoopManager.unregisterLoop(widgetId.value);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const intervalValue = typeof newInterval === 'string' ? parseInt(newInterval, 10) : newInterval;
|
||||||
|
|
||||||
|
// The registerLoop function internally handles unregistering the old task.
|
||||||
|
// It will also handle unregistering if the new interval is invalid (e.g., 0).
|
||||||
|
commandLoopManager.registerLoop(widgetId.value, intervalValue, runCommands);
|
||||||
|
|
||||||
|
}, { debounce: 500 });
|
||||||
|
|
||||||
|
const handleAddItem = () => {
|
||||||
|
const newId =
|
||||||
|
Math.max(
|
||||||
|
0,
|
||||||
|
...modelValue.value.widgetProps.map((item) => item.id)
|
||||||
|
) + 1
|
||||||
|
const newItem: DraggableComponent<UartCommandData> = {
|
||||||
|
id: newId,
|
||||||
|
componentType: markRaw(UartAtCommand),
|
||||||
|
props: {
|
||||||
|
label: 'New Command',
|
||||||
|
command: 'AT+CMD',
|
||||||
|
response: ''
|
||||||
|
}
|
||||||
|
}
|
||||||
|
modelValue.value.widgetProps.push(newItem)
|
||||||
|
}
|
||||||
|
|
||||||
|
function deleteItem(id: number) {
|
||||||
|
const index = modelValue.value.widgetProps.findIndex((item) => item.id === id)
|
||||||
|
if (index !== -1) {
|
||||||
|
modelValue.value.widgetProps.splice(index, 1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* optional helper if you still need cloning */
|
||||||
|
function rawClone(item: DraggableComponent): DraggableComponent {
|
||||||
|
return { ...item } // already plain in parent
|
||||||
|
}
|
||||||
|
|
||||||
|
function ensureUniqueId(evt: any) {
|
||||||
|
const arr = modelValue.value.widgetProps
|
||||||
|
const moved = arr[evt.newIndex] // item that just arrived
|
||||||
|
const hasDuplicate = arr.filter(i => i.id === moved.id).length > 1
|
||||||
|
if (hasDuplicate) {
|
||||||
|
// e.g. give it the next free integer
|
||||||
|
const max = Math.max(...arr.map(i => i.id))
|
||||||
|
moved.id = max + 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const executeOnce = () => {
|
||||||
|
// Register a one-time execution with the command loop manager
|
||||||
|
// Set as highest priority by using a very small interval (1ms)
|
||||||
|
commandLoopManager.registerLoop(
|
||||||
|
`${widgetId.value}-once-${Date.now()}`, // Unique ID
|
||||||
|
1, // 1ms interval (will be executed immediately)
|
||||||
|
runCommands,
|
||||||
|
true // oneTime = true
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const runCommands = async () => {
|
||||||
|
if (useWsStore().state !== 'CONNECTED') {
|
||||||
|
globalNotify('Device not connected', 'error');
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const commandsToRun = modelValue.value.widgetProps
|
||||||
|
if (!commandsToRun || commandsToRun.length === 0) return
|
||||||
|
|
||||||
|
// Extract command strings
|
||||||
|
const commandStrings = commandsToRun.map(cmd => cmd.props.command)
|
||||||
|
|
||||||
|
if (isDevMode()) {
|
||||||
|
console.log('Running commands:', commandStrings)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Execute all commands at once
|
||||||
|
const responses = await sendCommands(commandStrings)
|
||||||
|
|
||||||
|
// Update responses in UI
|
||||||
|
commandsToRun.forEach((command, index) => {
|
||||||
|
if (index < responses.length) {
|
||||||
|
command.props.response = responses[index] || 'No response'
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Cleanup on unmount
|
||||||
|
onUnmounted(() => {
|
||||||
|
commandLoopManager.unregisterLoop(widgetId.value)
|
||||||
|
})
|
||||||
|
|
||||||
|
// Initialize on mount
|
||||||
|
onMounted(() => {
|
||||||
|
// On component mount, only register if it's explicitly set to active and has an interval.
|
||||||
|
if (active.value && intervalMS.value > 0) {
|
||||||
|
commandLoopManager.registerLoop(
|
||||||
|
widgetId.value,
|
||||||
|
intervalMS.value,
|
||||||
|
runCommands
|
||||||
|
)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="flex flex-col h-full p-1">
|
||||||
|
<VueDraggable
|
||||||
|
v-model="modelValue.widgetProps"
|
||||||
|
item-key="id"
|
||||||
|
class="flex-1 min-h-0 overflow-y-auto"
|
||||||
|
group="people"
|
||||||
|
:clone="rawClone"
|
||||||
|
:animation="100"
|
||||||
|
direction="vertical"
|
||||||
|
handle=".drag-handle"
|
||||||
|
@add="ensureUniqueId"
|
||||||
|
>
|
||||||
|
<div v-for="row in modelValue.widgetProps" :key="row.id" class="flex flex-row items-center">
|
||||||
|
<el-tag v-if="editCell" size="large" type="success" class="drag-handle cursor-move">
|
||||||
|
=
|
||||||
|
</el-tag>
|
||||||
|
<component
|
||||||
|
:is="row.componentType"
|
||||||
|
v-model:modelValue="row.props"
|
||||||
|
:editCell="editCell"
|
||||||
|
class="flex-1"
|
||||||
|
/>
|
||||||
|
<el-button
|
||||||
|
v-if="editCell"
|
||||||
|
type="danger"
|
||||||
|
size="small"
|
||||||
|
@click="deleteItem(row.id)"
|
||||||
|
circle
|
||||||
|
>
|
||||||
|
<InlineSvg name="trash" width="20"></InlineSvg>
|
||||||
|
</el-button>
|
||||||
|
</div>
|
||||||
|
</VueDraggable>
|
||||||
|
<div v-if="editCell" class="bg-gray-50 flex gap-1">
|
||||||
|
<el-button type="primary" size="small" @click="handleAddItem">{{ translate('widget.addCommand') }}</el-button>
|
||||||
|
<div>
|
||||||
|
<el-popover
|
||||||
|
placement="top-start"
|
||||||
|
trigger="hover"
|
||||||
|
:show-after="1000"
|
||||||
|
:content="translate('widget.loopInterval')"
|
||||||
|
>
|
||||||
|
<template #reference>
|
||||||
|
<el-input
|
||||||
|
v-model="intervalMS"
|
||||||
|
size="small"
|
||||||
|
type="number"
|
||||||
|
:min="0"
|
||||||
|
:max="2147483647"
|
||||||
|
>
|
||||||
|
<template #prepend>
|
||||||
|
<InlineSvg name="repeat" width="20"></InlineSvg>
|
||||||
|
</template>
|
||||||
|
</el-input>
|
||||||
|
</template>
|
||||||
|
</el-popover>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<teleport defer :to="`#tp-widget-before-${modelValue.i}`">
|
||||||
|
<el-button plain size="small" @click="active = !active" :type="active ? 'success' : 'info'">
|
||||||
|
{{ intervalMS }}ms
|
||||||
|
</el-button>
|
||||||
|
</teleport>
|
||||||
|
<teleport defer :to="`#tp-widget-${modelValue.i}`">
|
||||||
|
<el-button text bg size="small" @click="executeOnce">
|
||||||
|
<InlineSvg name="play" width="20"></InlineSvg>
|
||||||
|
</el-button>
|
||||||
|
</teleport>
|
||||||
|
</template>
|
|
@ -0,0 +1,37 @@
|
||||||
|
<!-- CommandWidget.vue -->
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { VueDraggable } from 'vue-draggable-plus'
|
||||||
|
import type { DraggableComponent } from '../../types/grid'
|
||||||
|
|
||||||
|
/* ---------------- props & model ----------------------------------- */
|
||||||
|
const modelValue = defineModel<DraggableComponent[]>({ required: true })
|
||||||
|
defineProps<{
|
||||||
|
editCell: boolean
|
||||||
|
}>()
|
||||||
|
|
||||||
|
/* optional helper if you still need cloning */
|
||||||
|
function rawClone(item: DraggableComponent): DraggableComponent {
|
||||||
|
return { ...item } // already plain in parent
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<VueDraggable
|
||||||
|
v-model="modelValue"
|
||||||
|
item-key="id"
|
||||||
|
class="h-full flex flex-col"
|
||||||
|
group="people"
|
||||||
|
:clone="rawClone"
|
||||||
|
:animation="100"
|
||||||
|
direction="vertical"
|
||||||
|
handle=".drag-handle"
|
||||||
|
>
|
||||||
|
<component
|
||||||
|
v-for="row in modelValue"
|
||||||
|
:key="row.id"
|
||||||
|
:is="row.componentType"
|
||||||
|
v-model:modelValue="row.props"
|
||||||
|
:is-editing-cell="editCell"
|
||||||
|
/>
|
||||||
|
</VueDraggable>
|
||||||
|
</template>
|
|
@ -7,7 +7,7 @@ import vue from '@vitejs/plugin-vue'
|
||||||
import svgLoader from "vite-svg-loader";
|
import svgLoader from "vite-svg-loader";
|
||||||
import cssInjectedByJsPlugin from "vite-plugin-css-injected-by-js";
|
import cssInjectedByJsPlugin from "vite-plugin-css-injected-by-js";
|
||||||
import { viteSingleFile } from 'vite-plugin-singlefile'
|
import { viteSingleFile } from 'vite-plugin-singlefile'
|
||||||
|
import vuetify from 'vite-plugin-vuetify'
|
||||||
|
|
||||||
// https://vitejs.dev/config/
|
// https://vitejs.dev/config/
|
||||||
export default ({mode}: ConfigEnv) => {
|
export default ({mode}: ConfigEnv) => {
|
||||||
|
@ -25,6 +25,7 @@ export default ({mode}: ConfigEnv) => {
|
||||||
svgLoader(),
|
svgLoader(),
|
||||||
cssInjectedByJsPlugin(),
|
cssInjectedByJsPlugin(),
|
||||||
viteSingleFile(),
|
viteSingleFile(),
|
||||||
|
vuetify(),
|
||||||
],
|
],
|
||||||
define: {},
|
define: {},
|
||||||
resolve: {
|
resolve: {
|
||||||
|
|