mirror of
https://github.com/xinnan-tech/xiaozhi-esp32-server.git
synced 2026-07-23 23:53:55 +08:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
783f17e9e8 | ||
|
|
9e305c2840 | ||
|
|
78fa5d3110 | ||
|
|
13c0a966ec | ||
|
|
d987619946 | ||
|
|
8fb9498fe6 | ||
|
|
b8f64a6693 | ||
|
|
e31bb047bf | ||
|
|
6897b813a6 | ||
|
|
83f86406b5 | ||
|
|
dde237ff0d | ||
|
|
6a4887e252 | ||
|
|
d945b94910 | ||
|
|
bc7dcf791d | ||
|
|
66c71fe3b7 | ||
|
|
16a7aeec3b | ||
|
|
94605345e5 | ||
|
|
ec9fa32c81 | ||
|
|
264aa15281 | ||
|
|
180a6ff824 | ||
|
|
5904ee1571 | ||
|
|
bc99341f85 | ||
|
|
8b9174b67e | ||
|
|
6c04a6cf69 | ||
|
|
14a63c4b97 | ||
|
|
098d13a34b | ||
|
|
488f247744 | ||
|
|
c74dbf03cd | ||
|
|
1f836c3235 | ||
|
|
bd2e2e77d5 | ||
|
|
6282ef14e8 | ||
|
|
558f23688f | ||
|
|
541f2de599 | ||
|
|
68116254cd | ||
|
|
77ff4599ea | ||
|
|
652f5a3247 | ||
|
|
9b6e57b143 | ||
|
|
bdc19256bf | ||
|
|
de5eaf4467 | ||
|
|
ca884833d1 | ||
|
|
9fc164a69d | ||
|
|
0da2da83a5 | ||
|
|
bfdfa44edd | ||
|
|
ed1cfe5eb6 | ||
|
|
b696f3f5db | ||
|
|
7a0cf5ef9a | ||
|
|
d1b7cb6a36 | ||
|
|
b21d699472 | ||
|
|
029fadd778 | ||
|
|
215d50235b | ||
|
|
000e676397 | ||
|
|
208c045d3d | ||
|
|
ee20f59a8e | ||
|
|
925cf3dabb | ||
|
|
0049d0e926 | ||
|
|
56959808a9 | ||
|
|
8667a9680e | ||
|
|
8c7d129089 | ||
|
|
a3a9b98a1d | ||
|
|
f2f66579cb | ||
|
|
1354cf2a87 | ||
|
|
15f5592429 | ||
|
|
dc69094f00 | ||
|
|
8ad3783a36 | ||
|
|
15d5dd8e92 | ||
|
|
b07edb977b | ||
|
|
1509fc2039 | ||
|
|
2877bb8faa | ||
|
|
a83410cb49 | ||
|
|
4ff9f2235b | ||
|
|
1bc611f097 | ||
|
|
4e0a3941ff | ||
|
|
4e885dac3e | ||
|
|
eafd131a22 | ||
|
|
289ce3716b | ||
|
|
2242070bdc | ||
|
|
ee83cea11c | ||
|
|
6e571a43d8 | ||
|
|
466a0245a9 | ||
|
|
d9ce5e4ce7 | ||
|
|
6b0dd291c2 | ||
|
|
4ff77d0d7e | ||
|
|
e00da5d8d2 | ||
|
|
1a525a59ab | ||
|
|
d5581c4236 | ||
|
|
4bf950574c | ||
|
|
0b9e434c4b | ||
|
|
c4b1466643 | ||
|
|
488842029b | ||
|
|
1b0ce59371 | ||
|
|
97bea89204 | ||
|
|
0892dfc601 | ||
|
|
20d66ff401 | ||
|
|
e2fda532d1 | ||
|
|
68035af7b3 | ||
|
|
ee25555f0e | ||
|
|
e528510f5c | ||
|
|
afa0527b4c | ||
|
|
dd7d23498f | ||
|
|
eaaea2b24c | ||
|
|
58d0aefae1 | ||
|
|
11c9915a70 | ||
|
|
6cf6b0d645 | ||
|
|
1980a39b73 | ||
|
|
8d3fbeeb22 | ||
|
|
2678644c2c | ||
|
|
25da9fe119 | ||
|
|
03d89d22ec | ||
|
|
100cd25ffc | ||
|
|
6284a74a6e | ||
|
|
978cd8ba8e | ||
|
|
c76f28824c | ||
|
|
703ecfae40 | ||
|
|
b15c59aa48 | ||
|
|
98a4ce701c | ||
|
|
2fc0de8f4e | ||
|
|
66fd551d68 | ||
|
|
6e4e9a002b | ||
|
|
91278b0b3d | ||
|
|
ff9b3ed1a2 | ||
|
|
0c855d4ba8 | ||
|
|
9176e9d255 | ||
|
|
2bab917b3e | ||
|
|
b80086a349 | ||
|
|
8a4fca9bd5 | ||
|
|
13288a6b3a | ||
|
|
57ed552f43 | ||
|
|
5a4d65986d | ||
|
|
427b3bb774 | ||
|
|
4912f385c6 | ||
|
|
dcb6d0feab | ||
|
|
de8c762d79 | ||
|
|
056659304a | ||
|
|
525bf18a03 | ||
|
|
e3928ffbcc | ||
|
|
7c73f060ed | ||
|
|
f0aa7df3b0 |
+7
-1
@@ -155,7 +155,13 @@ main/manager-web/node_modules
|
|||||||
main/xiaozhi-server/models/SenseVoiceSmall/model.pt
|
main/xiaozhi-server/models/SenseVoiceSmall/model.pt
|
||||||
main/xiaozhi-server/models/sherpa-onnx*
|
main/xiaozhi-server/models/sherpa-onnx*
|
||||||
my_wakeup_words.mp3
|
my_wakeup_words.mp3
|
||||||
|
!main/xiaozhi-server/config/assets/bind_code.wav
|
||||||
|
!main/xiaozhi-server/config/assets/bind_not_found.wav
|
||||||
|
!main/xiaozhi-server/config/assets/bind_code/*.wav
|
||||||
|
!main/xiaozhi-server/config/assets/max_output_size.wav
|
||||||
main/manager-api/.vscode
|
main/manager-api/.vscode
|
||||||
|
|
||||||
# Ignore webpack cache directory
|
# Ignore webpack cache directory
|
||||||
main/manager-web/.webpack_cache/
|
main/manager-web/.webpack_cache/
|
||||||
|
main/xiaozhi-server/mysql
|
||||||
|
uploadfile
|
||||||
|
|||||||
@@ -145,42 +145,24 @@ server:
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 部署方式 🚀
|
## 部署文档
|
||||||
|
|
||||||
[](./docs/Deployment.md)
|
本项目提供两种部署方式,请根据您的具体需求选择:
|
||||||
|
|
||||||
### 部署文档
|
#### 🚀 部署方式选择
|
||||||
|
|
||||||
本项目支持`最简化`安装和`全模块`安装。请根据具体需求选择安装方式。
|
| 部署方式 | 特点 | 适用场景 | Docker部署文档 | 源码部署文档 |
|
||||||
|
|---------|------|---------|---------|---------|
|
||||||
|
| **最简化安装** | 智能对话、IOT功能,数据存储在配置文件 | 低配置环境,无需数据库 | [Docker只运行Server](./docs/Deployment.md#%E6%96%B9%E5%BC%8F%E4%B8%80docker%E5%8F%AA%E8%BF%90%E8%A1%8Cserver) | [本地源码只运行Server](./docs/Deployment.md#%E6%96%B9%E5%BC%8F%E4%BA%8C%E6%9C%AC%E5%9C%B0%E6%BA%90%E7%A0%81%E5%8F%AA%E8%BF%90%E8%A1%8Cserver)|
|
||||||
|
| **全模块安装** | 智能对话、IOT、OTA、智控台,数据存储在数据库 | 完整功能体验 |[Docker运行全模块](./docs/Deployment_all.md#%E6%96%B9%E5%BC%8F%E4%B8%80docker%E8%BF%90%E8%A1%8C%E5%85%A8%E6%A8%A1%E5%9D%97) | [本地源码运行全模块](./docs/Deployment_all.md#%E6%96%B9%E5%BC%8F%E4%BA%8C%E6%9C%AC%E5%9C%B0%E6%BA%90%E7%A0%81%E8%BF%90%E8%A1%8C%E5%85%A8%E6%A8%A1%E5%9D%97) |
|
||||||
|
|
||||||
`最简化`适合在较低资源配置中运行,无需数据库。[请参考-最简化安装架构图](./docs/images/deploy1.png)
|
> 💡 提示:以下是按最新代码部署后的测试平台,有需要可烧录测试,并发为6个,每天会清空数据
|
||||||
|
|
||||||
`全模块`适合想体验完整功能,过程复杂。[请参考-全模块安装架构图](./docs/images/deploy2.png)
|
|
||||||
|
|
||||||
#### 1、 最简化安装
|
|
||||||
|
|
||||||
`最简化`安装包含:智能对话、IOT等功能,数据存储在`配置文件`。我们提供了两种安装教程,如果你决定使用最简化安装,可选择`Docker只运行Server`或`本地源码只运行Server`任意一种方式。
|
|
||||||
|
|
||||||
##### 1.1. Docker只运行Server
|
|
||||||
|
|
||||||
你可以使用编译好的docker镜像安装,[点击Docker只运行Server](./docs/Deployment.md#%E6%96%B9%E5%BC%8F%E4%B8%80docker%E5%8F%AA%E8%BF%90%E8%A1%8Cserver)。优点:减少环境配置的烦恼;缺点:版本可能有点旧,镜像文件较大。
|
|
||||||
|
|
||||||
##### 2.2. 本地源码只运行Server
|
|
||||||
|
|
||||||
你可以以本地源码的方式运行,[点击本地源码只运行Server](./docs/Deployment.md#%E6%96%B9%E5%BC%8F%E4%BA%8C%E6%9C%AC%E5%9C%B0%E6%BA%90%E7%A0%81%E5%8F%AA%E8%BF%90%E8%A1%8Cserver)。优点:可以随时更新最新代码;缺点:安装Conda环境较为复杂。
|
|
||||||
|
|
||||||
#### 2、 全模块安装
|
|
||||||
|
|
||||||
`全模块`安装包含:智能对话、IOT、OTA、智控台,数据存储在`数据库`中。我们提供了两种安装教程,如果你决定使用全模块安装,可选择`Docker运行全模块`或`本地源码运行全模块`任意一种方式。
|
|
||||||
|
|
||||||
##### 2.1. Docker运行全模块
|
|
||||||
|
|
||||||
你可以使用编译好的docker镜像安装,[点击Docker运行全模块](./docs/Deployment_all.md#%E6%96%B9%E5%BC%8F%E4%B8%80docker%E8%BF%90%E8%A1%8C%E5%85%A8%E6%A8%A1%E5%9D%97)。优点:减少环境配置的烦恼;缺点:版本可能有点旧,镜像文件较大。
|
|
||||||
|
|
||||||
##### 2.2. 本地源码运行全模块
|
|
||||||
|
|
||||||
你可以以本地源码的方式运行,[点击本地源码运行全模块](./docs/Deployment_all.md#%E6%96%B9%E5%BC%8F%E4%BA%8C%E6%9C%AC%E5%9C%B0%E6%BA%90%E7%A0%81%E8%BF%90%E8%A1%8C%E5%85%A8%E6%A8%A1%E5%9D%97)。优点:可以随时更新最新代码;缺点:安装Java、Node.js、Conda环境较为复杂。
|
|
||||||
|
|
||||||
|
```
|
||||||
|
智控台地址: https://2662r3426b.vicp.fun
|
||||||
|
OTA接口地址: https://2662r3426b.vicp.fun/xiaozhi/ota/
|
||||||
|
Websocket接口地址: wss://2662r3426b.vicp.fun/xiaozhi/v1/
|
||||||
|
```
|
||||||
---
|
---
|
||||||
|
|
||||||
## 常见问题 ❓
|
## 常见问题 ❓
|
||||||
@@ -188,54 +170,41 @@ server:
|
|||||||
如遇到问题或产品建议反馈[点这里](docs/FAQ.md)。
|
如遇到问题或产品建议反馈[点这里](docs/FAQ.md)。
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 产品生态 👬
|
|
||||||
小智是一个生态,当你使用这个产品时,也可以看看其他在这个生态圈的优秀项目
|
|
||||||
|
|
||||||
- [小智安卓客户端](https://github.com/TOM88812/xiaozhi-android-client)
|
|
||||||
|
|
||||||
一个基于xiaozhi-server的Android、IOS语音对话应用,支持实时语音交互和文字对话。现在是flutter版本,打通IOS、Android端。
|
|
||||||
- [小智电脑客户端](https://github.com/Huang-junsen/py-xiaozhi)
|
|
||||||
|
|
||||||
该项目提供了一个基于 Python 实现的小白 AI 客户端,使得在不具备实体硬件条件的情况下,依然能够体过代码体验小智 AI 的功能。主要功能包括 AI 语音交互、视觉多模态识别、IoT 设备集成、联网音乐播放、语音唤醒、自动对话模式、图形化界面、命令行模式、跨平台支持、音量控制、会话管理、加密音频传输、自动验证码处理等。
|
|
||||||
- [小智Java服务端](https://github.com/joey-zhou/xiaozhi-esp32-server-java)
|
|
||||||
|
|
||||||
小智开源后端服务 Java 版本是一个基于 Java 的开源项目,它包括前后端的服务,旨在为用户提供一个完整的后端服务解决方案。
|
|
||||||
---
|
|
||||||
## 功能清单 ✨
|
## 功能清单 ✨
|
||||||
|
|
||||||
### 已实现 ✅
|
### 已实现 ✅
|
||||||
|
|
||||||
- **通信协议**
|
| 功能模块 | 描述 |
|
||||||
基于 `xiaozhi-esp32` 协议,通过 WebSocket 实现数据交互。
|
|---------|------|
|
||||||
- **对话交互**
|
| 通信协议 | 基于 `xiaozhi-esp32` 协议,通过 WebSocket 实现数据交互 |
|
||||||
支持唤醒对话、手动对话及实时打断。长时间无对话时自动休眠
|
| 对话交互 | 支持唤醒对话、手动对话及实时打断。长时间无对话时自动休眠 |
|
||||||
- **意图识别**
|
| 意图识别 | 支持使用LLM意图识别、function call函数调用,减少硬编码意图判断 |
|
||||||
支持使用LLM意图识别、function call函数调用,减少硬编码意图判断
|
| 多语言识别 | 支持国语、粤语、英语、日语、韩语(默认使用 FunASR) |
|
||||||
- **多语言识别**
|
| LLM 模块 | 支持灵活切换 LLM 模块,默认使用 ChatGLMLLM,也可选用阿里百炼、DeepSeek、Ollama 等接口 |
|
||||||
支持国语、粤语、英语、日语、韩语(默认使用 FunASR)。
|
| TTS 模块 | 支持 EdgeTTS(默认)、火山引擎豆包 TTS 等多种 TTS 接口,满足语音合成需求 |
|
||||||
- **LLM 模块**
|
| 记忆功能 | 支持超长记忆、本地总结记忆、无记忆三种模式,满足不同场景需求 |
|
||||||
支持灵活切换 LLM 模块,默认使用 ChatGLMLLM,也可选用阿里百炼、DeepSeek、Ollama 等接口。
|
| IOT功能 | 支持管理注册设备IOT功能,支持基于对话上下文语境下的智能物联网控制 |
|
||||||
- **TTS 模块**
|
| 智控台 | 提供Web管理界面,支持智能体管理、用户管理、系统配置等功能,方便管理员和用户进行管理 |
|
||||||
支持 EdgeTTS(默认)、火山引擎豆包 TTS 等多种 TTS 接口,满足语音合成需求。
|
|
||||||
- **记忆功能**
|
|
||||||
支持超长记忆、本地总结记忆、无记忆三种模式,满足不同场景需求。
|
|
||||||
- **IOT功能**
|
|
||||||
支持管理注册设备IOT功能,支持基于对话上下文语境下的智能物联网控制。
|
|
||||||
- **智控台**
|
|
||||||
提供Web管理界面,支持智能体管理、用户管理、系统配置等功能,方便管理员和用户进行管理。
|
|
||||||
|
|
||||||
|
|
||||||
### 正在开发 🚧
|
### 正在开发 🚧
|
||||||
|
|
||||||
- 多种心情模式
|
想了解具体开发计划进度,[请点击这里](https://github.com/users/xinnan-tech/projects/3)
|
||||||
|
|
||||||
想了解具体开发进度,[请点击这里](https://github.com/users/xinnan-tech/projects/3)
|
|
||||||
|
|
||||||
如果你是一名软件开发者,这里有一份[《致开发者的公开信》](docs/contributor_open_letter.md),欢迎加入!
|
如果你是一名软件开发者,这里有一份[《致开发者的公开信》](docs/contributor_open_letter.md),欢迎加入!
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
## 产品生态 👬
|
||||||
|
小智是一个生态,当你使用这个产品时,也可以看看其他在这个生态圈的优秀项目
|
||||||
|
|
||||||
|
| 项目名称 | 项目地址 | 项目描述 |
|
||||||
|
|:---------------------|:--------|:--------|
|
||||||
|
| 小智安卓客户端 | [xiaozhi-android-client](https://github.com/TOM88812/xiaozhi-android-client) | 一个基于xiaozhi-server的Android、IOS语音对话应用,支持实时语音交互和文字对话。<br/>现在是flutter版本,打通IOS、Android端。 |
|
||||||
|
| 小智电脑客户端 | [py-xiaozhi](https://github.com/Huang-junsen/py-xiaozhi) | 该项目提供了一个基于 Python 实现的小白 AI 客户端,使得在不具备实体硬件条件的情况下,<br/>依然能够体过代码体验小智 AI 的功能。 |
|
||||||
|
| 小智Java服务端 | [xiaozhi-esp32-server-java](https://github.com/joey-zhou/xiaozhi-esp32-server-java) | 小智开源后端服务 Java 版本是一个基于 Java 的开源项目。<br/>它包括前后端的服务,旨在为用户提供一个完整的后端服务解决方案。 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## 本项目支持的平台/组件列表 📋
|
## 本项目支持的平台/组件列表 📋
|
||||||
|
|
||||||
### LLM 语言模型
|
### LLM 语言模型
|
||||||
@@ -298,10 +267,17 @@ server:
|
|||||||
|
|
||||||
## 鸣谢 🙏
|
## 鸣谢 🙏
|
||||||
|
|
||||||
- 本项目受 [百聆语音对话机器人](https://github.com/wwbin2017/bailing) 启发,并在其基础上实现。
|
| Logo | 项目/公司 | 说明 |
|
||||||
- 感谢 [十方融海](https://www.tenclass.com/) 对小智通讯协议提供的详尽文档支持。
|
|:---:|:---:|:---|
|
||||||
|
| <img src="./docs/images/logo_bailing.png" width="160"> | [百聆语音对话机器人](https://github.com/wwbin2017/bailing) | 本项目受[百聆语音对话机器人](https://github.com/wwbin2017/bailing)启发,并在其基础上实现 |
|
||||||
|
| <img src="./docs/images/logo_tenclass.png" width="160"> | [十方融海](https://www.tenclass.com/) | 感谢[十方融海](https://www.tenclass.com/)为小智生态制定了标准的通讯协议、多设备兼容性方案及高并发场景实践示范;为本项目提供了全链路技术文档支持 |
|
||||||
|
| <img src="./docs/images/logo_xuanfeng.png" width="160"> | [玄凤科技](https://github.com/Eric0308) | 感谢[玄凤科技](https://github.com/Eric0308)贡献函数调用框架、MCP通信协议及插件化调用机制的实现代码,通过标准化的指令调度体系与动态扩展能力,显著提升了前端设备(IoT)的交互效率和功能延展性 |
|
||||||
|
| <img src="./docs/images/logo_huiyuan.png" width="160"> | [汇远设计](http://ui.kwd988.net/) | 感谢[汇远设计](http://ui.kwd988.net/)为本项目提供专业视觉解决方案,用其服务超千家企业的设计实战经验,赋能本项目产品用户体验 |
|
||||||
|
| <img src="./docs/images/logo_qinren.png" width="160"> | [西安勤人信息科技](https://www.029app.com/) | 感谢[西安勤人信息科技](https://www.029app.com/)深化本项目视觉体系,确保整体设计风格在多场景应用中的一致性和扩展性 |
|
||||||
|
|
||||||
|
|
||||||
<a href="https://star-history.com/#xinnan-tech/xiaozhi-esp32-server&Date">
|
<a href="https://star-history.com/#xinnan-tech/xiaozhi-esp32-server&Date">
|
||||||
|
|
||||||
<picture>
|
<picture>
|
||||||
<source media="(prefers-color-scheme: dark)" srcset="https://api.star-history.com/svg?repos=xinnan-tech/xiaozhi-esp32-server&type=Date&theme=dark" />
|
<source media="(prefers-color-scheme: dark)" srcset="https://api.star-history.com/svg?repos=xinnan-tech/xiaozhi-esp32-server&type=Date&theme=dark" />
|
||||||
<source media="(prefers-color-scheme: light)" srcset="https://api.star-history.com/svg?repos=xinnan-tech/xiaozhi-esp32-server&type=Date" />
|
<source media="(prefers-color-scheme: light)" srcset="https://api.star-history.com/svg?repos=xinnan-tech/xiaozhi-esp32-server&type=Date" />
|
||||||
|
|||||||
+4
-1
@@ -137,7 +137,10 @@ docker logs -f xiaozhi-esp32-server
|
|||||||
```
|
```
|
||||||
docker stop xiaozhi-esp32-server
|
docker stop xiaozhi-esp32-server
|
||||||
docker rm xiaozhi-esp32-server
|
docker rm xiaozhi-esp32-server
|
||||||
|
docker stop xiaozhi-esp32-server-web
|
||||||
|
docker rm xiaozhi-esp32-server-web
|
||||||
docker rmi ghcr.nju.edu.cn/xinnan-tech/xiaozhi-esp32-server:server_latest
|
docker rmi ghcr.nju.edu.cn/xinnan-tech/xiaozhi-esp32-server:server_latest
|
||||||
|
docker rmi ghcr.nju.edu.cn/xinnan-tech/xiaozhi-esp32-server:web_latest
|
||||||
```
|
```
|
||||||
|
|
||||||
5.3、重新按docker方式部署
|
5.3、重新按docker方式部署
|
||||||
@@ -221,7 +224,7 @@ python app.py
|
|||||||
## 配置项目
|
## 配置项目
|
||||||
|
|
||||||
如果你的`xiaozhi-server`目录没有`data`,你需要创建`data`目录。
|
如果你的`xiaozhi-server`目录没有`data`,你需要创建`data`目录。
|
||||||
如果你的`data`下面没有`.config.yaml`文件,你可以把源码目录下的`config.yaml`文件复制一份,重命名为`.config.yaml`
|
如果你的`data`下面没有`.config.yaml`文件,你可以把`xiaozhi-server`目录下的`config.yaml`文件复制到`data`,并重命名为`.config.yaml`
|
||||||
|
|
||||||
修改`xiaozhi-server`下`data`目录下的`.config.yaml`文件,配置本项目必须的一个配置。
|
修改`xiaozhi-server`下`data`目录下的`.config.yaml`文件,配置本项目必须的一个配置。
|
||||||
|
|
||||||
|
|||||||
+36
-68
@@ -44,6 +44,8 @@ xiaozhi-server
|
|||||||
在页面的右侧找到名称为`RAW`按钮,在`RAW`按钮的旁边,找到下载的图标,点击下载按钮,下载`docker-compose_all.yml`文件。 把文件下载到你的
|
在页面的右侧找到名称为`RAW`按钮,在`RAW`按钮的旁边,找到下载的图标,点击下载按钮,下载`docker-compose_all.yml`文件。 把文件下载到你的
|
||||||
`xiaozhi-server`中。
|
`xiaozhi-server`中。
|
||||||
|
|
||||||
|
或者直接执行 `wget https://raw.githubusercontent.com/xinnan-tech/xiaozhi-esp32-server/refs/heads/main/main/xiaozhi-server/docker-compose_all.yml` 下载。
|
||||||
|
|
||||||
下载完后,回到本教程继续往下。
|
下载完后,回到本教程继续往下。
|
||||||
|
|
||||||
##### 1.3.2 下载 config_from_api.yaml
|
##### 1.3.2 下载 config_from_api.yaml
|
||||||
@@ -53,6 +55,8 @@ xiaozhi-server
|
|||||||
在页面的右侧找到名称为`RAW`按钮,在`RAW`按钮的旁边,找到下载的图标,点击下载按钮,下载`config_from_api.yaml`文件。 把文件下载到你的
|
在页面的右侧找到名称为`RAW`按钮,在`RAW`按钮的旁边,找到下载的图标,点击下载按钮,下载`config_from_api.yaml`文件。 把文件下载到你的
|
||||||
`xiaozhi-server`下面的`data`文件夹中,然后把`config_from_api.yaml`文件重命名为`.config.yaml`。
|
`xiaozhi-server`下面的`data`文件夹中,然后把`config_from_api.yaml`文件重命名为`.config.yaml`。
|
||||||
|
|
||||||
|
或者直接执行 `wget https://raw.githubusercontent.com/xinnan-tech/xiaozhi-esp32-server/refs/heads/main/main/xiaozhi-server/config_from_api.yaml` 下载保存。
|
||||||
|
|
||||||
下载完配置文件后,我们确认一下整个`xiaozhi-server`里面的文件如下所示:
|
下载完配置文件后,我们确认一下整个`xiaozhi-server`里面的文件如下所示:
|
||||||
|
|
||||||
```
|
```
|
||||||
@@ -67,57 +71,37 @@ xiaozhi-server
|
|||||||
|
|
||||||
如果你的文件目录结构也是上面的,就继续往下。如果不是,你就再仔细看看是不是漏操作了什么。
|
如果你的文件目录结构也是上面的,就继续往下。如果不是,你就再仔细看看是不是漏操作了什么。
|
||||||
|
|
||||||
## 2. 安装Mysql和Redis
|
## 2. 备份数据
|
||||||
|
|
||||||
### 2.1 安装Mysql数据库
|
如果你之前已经成功运行智控台,如果上面保存有你的密钥信息,请先从智控台上拷贝重要数据下来。因为升级过程中,有可能会覆盖原来的数据。
|
||||||
如果本机已经安装了MySQL,可以直接在数据库中创建名为`xiaozhi_esp32_server`的数据库。
|
|
||||||
|
|
||||||
```sql
|
|
||||||
CREATE DATABASE xiaozhi_esp32_server CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
|
|
||||||
```
|
|
||||||
|
|
||||||
如果还没有MySQL,你可以通过docker安装mysql
|
|
||||||
|
|
||||||
```
|
|
||||||
docker run --name xiaozhi-esp32-server-db -e MYSQL_ROOT_PASSWORD=123456 -p 3306:3306 -e MYSQL_DATABASE=xiaozhi_esp32_server -e MYSQL_INITDB_ARGS="--character-set-server=utf8mb4 --collation-server=utf8mb4_unicode_ci" -d mysql:latest
|
|
||||||
```
|
|
||||||
|
|
||||||
### 2.2 安装Redis
|
|
||||||
|
|
||||||
如果还没有Redis,你可以通过docker安装redis
|
|
||||||
|
|
||||||
```
|
|
||||||
docker run --name xiaozhi-esp32-server-redis -d -p 6379:6379 redis
|
|
||||||
```
|
|
||||||
|
|
||||||
## 3. 配置docker-compose_all.yml文件文件
|
|
||||||
|
|
||||||
在`xiaozhi-server`目录,打开docker-compose_all.yml,
|
|
||||||
|
|
||||||
1、修改`SPRING_DATASOURCE_DRUID_URL`里,把`192.168.1.25`修改成部署mysql的电脑局域网ip,如果是你本地,你要查看一下你本机的局域网ip是什么
|
|
||||||
|
|
||||||
2、修改`SPRING_DATA_REDIS_HOST`里,把`192.168.1.25`修改成部署redis的电脑局域网ip,如果是你本地,你要查看一下你本机的局域网ip是什么
|
|
||||||
|
|
||||||
3、确认一下mysql的用户名`SPRING_DATASOURCE_DRUID_USERNAME`和密码`SPRING_DATASOURCE_DRUID_PASSWORD`
|
|
||||||
|
|
||||||
## 4. 运行程序
|
|
||||||
|
|
||||||
确认mysql和redis是否运行正常,输入
|
|
||||||
```
|
|
||||||
docker ps
|
|
||||||
```
|
|
||||||
如果你能看到`xiaozhi-esp32-server-redis`和`xiaozhi-esp32-server-db`信息,就可以继续往下,如果看不到,说明你的`mysql`和`redis`没有安装或没有启动。需要继续回到上面的教程,看看哪一步漏了。
|
|
||||||
|
|
||||||
```
|
|
||||||
CONTAINER ID IMAGE COMMAND CREATED TATUS PORTS NAMES
|
|
||||||
xxx redis "xx" xxx xxx 0.0.0.0:6379->6379/tcp xiaozhi-esp32-server-redis
|
|
||||||
xxx mysql:latest "xx" xxx xxx 0.0.0.0:3306->3306/tcp, 33060/tcp xiaozhi-esp32-server-db
|
|
||||||
```
|
|
||||||
|
|
||||||
|
## 3. 清除历史版本镜像和容器
|
||||||
接下来打开命令行工具,使用`终端`或`命令行`工具 进入到你的`xiaozhi-server`,执行以下命令
|
接下来打开命令行工具,使用`终端`或`命令行`工具 进入到你的`xiaozhi-server`,执行以下命令
|
||||||
|
|
||||||
```
|
```
|
||||||
docker-compose -f docker-compose_all.yml up -d
|
docker compose -f docker-compose_all.yml down
|
||||||
|
|
||||||
|
docker stop xiaozhi-esp32-server
|
||||||
|
docker rm xiaozhi-esp32-server
|
||||||
|
|
||||||
|
docker stop xiaozhi-esp32-server-web
|
||||||
|
docker rm xiaozhi-esp32-server-web
|
||||||
|
|
||||||
|
docker stop xiaozhi-esp32-server-db
|
||||||
|
docker rm xiaozhi-esp32-server-db
|
||||||
|
|
||||||
|
docker stop xiaozhi-esp32-server-redis
|
||||||
|
docker rm xiaozhi-esp32-server-redis
|
||||||
|
|
||||||
|
docker rmi ghcr.nju.edu.cn/xinnan-tech/xiaozhi-esp32-server:server_latest
|
||||||
|
docker rmi ghcr.nju.edu.cn/xinnan-tech/xiaozhi-esp32-server:web_latest
|
||||||
|
```
|
||||||
|
|
||||||
|
## 4. 运行程序
|
||||||
|
执行以下命令启动新版本容器
|
||||||
|
|
||||||
|
```
|
||||||
|
docker compose -f docker-compose_all.yml up -d
|
||||||
```
|
```
|
||||||
|
|
||||||
执行完后,再执行以下命令,查看日志信息。
|
执行完后,再执行以下命令,查看日志信息。
|
||||||
@@ -150,17 +134,16 @@ http://localhost:8002/xiaozhi/doc.html
|
|||||||
|
|
||||||
```
|
```
|
||||||
manager-api:
|
manager-api:
|
||||||
url: http://127.0.0.1:8002/xiaozhi
|
url: http://127.0.0.1:8002/xiaozhi
|
||||||
secret: 你的server.secret值
|
secret: 你的server.secret值
|
||||||
```
|
```
|
||||||
1、把你刚才从`智控台`复制过来的`server.secret`的`参数值`复制到`.config.yaml`文件里的`secret`里。
|
1、把你刚才从`智控台`复制过来的`server.secret`的`参数值`复制到`.config.yaml`文件里的`secret`里。
|
||||||
|
2、注意,把`url`改成下面的`http://xiaozhi-esp32-server-web:8002/xiaozhi`
|
||||||
2、把`url`里的`127.0.0.1`改成你电脑局域网内的ip
|
|
||||||
|
|
||||||
类似这样的效果
|
类似这样的效果
|
||||||
```
|
```
|
||||||
manager-api:
|
manager-api:
|
||||||
url: http://192.168.1.25:8002/xiaozhi
|
url: http://xiaozhi-esp32-server-web:8002/xiaozhi
|
||||||
secret: 12345678-xxxx-xxxx-xxxx-123456789000
|
secret: 12345678-xxxx-xxxx-xxxx-123456789000
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -202,24 +185,6 @@ ws://你电脑局域网的ip:8000/xiaozhi/v1/
|
|||||||
接下来,你就可以开始 [编译esp32固件](firmware-build.md)了。
|
接下来,你就可以开始 [编译esp32固件](firmware-build.md)了。
|
||||||
|
|
||||||
|
|
||||||
## 6. 版本升级操作
|
|
||||||
|
|
||||||
如果后期想升级版本,先备份你的模型密钥。
|
|
||||||
|
|
||||||
进入`xiaozhi-server`目录
|
|
||||||
|
|
||||||
5.1、执行以下命令
|
|
||||||
|
|
||||||
```
|
|
||||||
docker-compose down
|
|
||||||
docker rmi ghcr.nju.edu.cn/xinnan-tech/xiaozhi-esp32-server:server_latest
|
|
||||||
docker rmi ghcr.nju.edu.cn/xinnan-tech/xiaozhi-esp32-server:web_latest
|
|
||||||
```
|
|
||||||
|
|
||||||
5.2、删掉`docker-compose_all.yaml`文件
|
|
||||||
|
|
||||||
5.3、重新按1.1开始部署
|
|
||||||
|
|
||||||
# 方式二:本地源码运行全模块
|
# 方式二:本地源码运行全模块
|
||||||
|
|
||||||
## 1.安装MySQL数据库
|
## 1.安装MySQL数据库
|
||||||
@@ -388,6 +353,9 @@ pip install -r requirements.txt
|
|||||||
|
|
||||||
`server.secret`需要说明一下,这个`参数值`很重要,作用是让我们的`Server`端连接`manager-api`。`server.secret`是每次从零部署manager模块时,会自动随机生成的密钥。
|
`server.secret`需要说明一下,这个`参数值`很重要,作用是让我们的`Server`端连接`manager-api`。`server.secret`是每次从零部署manager模块时,会自动随机生成的密钥。
|
||||||
|
|
||||||
|
如果你的`xiaozhi-server`目录没有`data`,你需要创建`data`目录。
|
||||||
|
如果你的`data`下面没有`.config.yaml`文件,你可以把`xiaozhi-server`目录下的`config_from_api.yaml`文件复制到`data`,并重命名为`.config.yaml`
|
||||||
|
|
||||||
复制`参数值`后,打开`xiaozhi-server`下的`data`目录的`.config.yaml`文件。此刻你的配置文件内容应该是这样的:
|
复制`参数值`后,打开`xiaozhi-server`下的`data`目录的`.config.yaml`文件。此刻你的配置文件内容应该是这样的:
|
||||||
|
|
||||||
```
|
```
|
||||||
|
|||||||
+12
-3
@@ -4,7 +4,16 @@
|
|||||||
先按照这个教程配置项目环境[《Windows搭建 ESP IDF 5.3.2开发环境以及编译小智》](https://icnynnzcwou8.feishu.cn/wiki/JEYDwTTALi5s2zkGlFGcDiRknXf)
|
先按照这个教程配置项目环境[《Windows搭建 ESP IDF 5.3.2开发环境以及编译小智》](https://icnynnzcwou8.feishu.cn/wiki/JEYDwTTALi5s2zkGlFGcDiRknXf)
|
||||||
|
|
||||||
## 第2步 打开配置文件
|
## 第2步 打开配置文件
|
||||||
配置好编译环境后,下载虾哥[xiaozhi-esp32](https://github.com/78/xiaozhi-esp32)项目源码,进入虾哥项目,打开`xiaozhi-esp32/main/Kconfig.projbuild`文件。
|
配置好编译环境后,下载虾哥iaozhi-esp32项目源码,
|
||||||
|
|
||||||
|
从这里下载虾哥[xiaozhi-esp32项目源码](https://github.com/78/xiaozhi-esp32/archive/refs/tags/v1.6.0.zip)。
|
||||||
|
|
||||||
|
从这里下载虾哥[xiaozhi-esp32项目源码](https://github.com/78/xiaozhi-esp32/archive/refs/tags/v1.6.0.zip)。
|
||||||
|
|
||||||
|
从这里下载虾哥[xiaozhi-esp32项目源码](https://github.com/78/xiaozhi-esp32/archive/refs/tags/v1.6.0.zip)。
|
||||||
|
|
||||||
|
下载后,解压缩包,打开`xiaozhi-esp32/main/Kconfig.projbuild`文件。
|
||||||
|
|
||||||
|
|
||||||
## 第3步 修改WEBSOCKET地址
|
## 第3步 修改WEBSOCKET地址
|
||||||
找到`WEBSOCKET_URL`的`default`的内容,把`wss://api.tenclass.net/xiaozhi/v1/`
|
找到`WEBSOCKET_URL`的`default`的内容,把`wss://api.tenclass.net/xiaozhi/v1/`
|
||||||
@@ -42,7 +51,7 @@ config WEBSOCKET_URL
|
|||||||
如果你是全模块部署本项目,就修改OTA接口,如果你只是部署了8000端口的xiaozhi-server,可以继续沿用虾哥团队的OTA接口。如果你不修改OTA接口,请直接忽略本第4步,直接看第5步
|
如果你是全模块部署本项目,就修改OTA接口,如果你只是部署了8000端口的xiaozhi-server,可以继续沿用虾哥团队的OTA接口。如果你不修改OTA接口,请直接忽略本第4步,直接看第5步
|
||||||
|
|
||||||
找到`OTA_VERSION_URL`的`default`的内容,把`https://api.tenclass.net/xiaozhi/ota/`
|
找到`OTA_VERSION_URL`的`default`的内容,把`https://api.tenclass.net/xiaozhi/ota/`
|
||||||
改成你自己的地址,例如,我的接口地址是`http://192.168.1.25:8000/xiaozhi/ota/`,就把内容改成这个。
|
改成你自己的地址,例如,我的接口地址是`http://192.168.1.25:8002/xiaozhi/ota/`,就把内容改成这个。
|
||||||
|
|
||||||
修改前:
|
修改前:
|
||||||
```
|
```
|
||||||
@@ -56,7 +65,7 @@ config OTA_VERSION_URL
|
|||||||
```
|
```
|
||||||
config OTA_VERSION_URL
|
config OTA_VERSION_URL
|
||||||
string "OTA Version URL"
|
string "OTA Version URL"
|
||||||
default "http://192.168.1.25:8000/xiaozhi/ota/"
|
default "http://192.168.1.25:8002/xiaozhi/ota/"
|
||||||
help
|
help
|
||||||
The application will access this URL to check for updates.
|
The application will access this URL to check for updates.
|
||||||
```
|
```
|
||||||
|
|||||||
Binary file not shown.
|
After Width: | Height: | Size: 35 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 29 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 14 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 6.1 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 20 KiB |
@@ -84,6 +84,21 @@ public interface Constant {
|
|||||||
*/
|
*/
|
||||||
String SERVER_SECRET = "server.secret";
|
String SERVER_SECRET = "server.secret";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* websocket地址
|
||||||
|
*/
|
||||||
|
String SERVER_WEBSOCKET = "server.websocket";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 是否允许用户注册
|
||||||
|
*/
|
||||||
|
String SERVER_ALLOW_USER_REGISTER = "server.allow_user_register";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 下发六位验证码时显示的控制面板地址
|
||||||
|
*/
|
||||||
|
String SERVER_FRONTED_URL = "server.fronted_url";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 路径分割符
|
* 路径分割符
|
||||||
*/
|
*/
|
||||||
@@ -153,4 +168,9 @@ public interface Constant {
|
|||||||
return value;
|
return value;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 版本号
|
||||||
|
*/
|
||||||
|
public static final String VERSION = "0.3.10";
|
||||||
}
|
}
|
||||||
@@ -52,4 +52,7 @@ public interface ErrorCode {
|
|||||||
int PARAM_BOOLEAN_INVALID = 10038;
|
int PARAM_BOOLEAN_INVALID = 10038;
|
||||||
int PARAM_ARRAY_INVALID = 10039;
|
int PARAM_ARRAY_INVALID = 10039;
|
||||||
int PARAM_JSON_INVALID = 10040;
|
int PARAM_JSON_INVALID = 10040;
|
||||||
|
|
||||||
|
int OTA_DEVICE_NOT_FOUND = 10041;
|
||||||
|
int OTA_DEVICE_NEED_BIND = 10042;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -75,4 +75,25 @@ public class RedisKeys {
|
|||||||
public static String getTimbreDetailsKey(String id) {
|
public static String getTimbreDetailsKey(String id) {
|
||||||
return "timbre:details:" + id;
|
return "timbre:details:" + id;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取版本号Key
|
||||||
|
*/
|
||||||
|
public static String getVersionKey() {
|
||||||
|
return "sys:version";
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* OTA固件ID的Key
|
||||||
|
*/
|
||||||
|
public static String getOtaIdKey(String uuid) {
|
||||||
|
return "ota:id:" + uuid;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* OTA固件下载次数的Key
|
||||||
|
*/
|
||||||
|
public static String getOtaDownloadCountKey(String uuid) {
|
||||||
|
return "ota:download:count:" + uuid;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,11 +1,14 @@
|
|||||||
package xiaozhi.common.redis;
|
package xiaozhi.common.redis;
|
||||||
|
|
||||||
import java.util.Collection;
|
import java.util.Collection;
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.concurrent.TimeUnit;
|
import java.util.concurrent.TimeUnit;
|
||||||
|
|
||||||
import org.springframework.data.redis.core.HashOperations;
|
import org.springframework.data.redis.core.HashOperations;
|
||||||
import org.springframework.data.redis.core.RedisTemplate;
|
import org.springframework.data.redis.core.RedisTemplate;
|
||||||
|
import org.springframework.data.redis.core.script.DefaultRedisScript;
|
||||||
import org.springframework.stereotype.Component;
|
import org.springframework.stereotype.Component;
|
||||||
|
|
||||||
import jakarta.annotation.Resource;
|
import jakarta.annotation.Resource;
|
||||||
@@ -27,7 +30,7 @@ public class RedisUtils {
|
|||||||
/**
|
/**
|
||||||
* 过期时长为1小时,单位:秒
|
* 过期时长为1小时,单位:秒
|
||||||
*/
|
*/
|
||||||
public final static long HOUR_ONE_EXPIRE = 60 * 60 * 1L;
|
public final static long HOUR_ONE_EXPIRE = (long) 60 * 60;
|
||||||
/**
|
/**
|
||||||
* 过期时长为6小时,单位:秒
|
* 过期时长为6小时,单位:秒
|
||||||
*/
|
*/
|
||||||
@@ -124,4 +127,24 @@ public class RedisUtils {
|
|||||||
public Object rightPop(String key) {
|
public Object rightPop(String key) {
|
||||||
return redisTemplate.opsForList().rightPop(key);
|
return redisTemplate.opsForList().rightPop(key);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 清空所有 Redis 数据库中的所有键
|
||||||
|
*/
|
||||||
|
public void emptyAll() {
|
||||||
|
// Lua 脚本 FLUSHALL是redis清空所有库的命令
|
||||||
|
String luaScript ="redis.call('FLUSHALL')";
|
||||||
|
|
||||||
|
// 创建 DefaultRedisScript 对象
|
||||||
|
DefaultRedisScript<Void> redisScript = new DefaultRedisScript<>();
|
||||||
|
redisScript.setScriptText(luaScript); // 设置 Lua 脚本内容
|
||||||
|
redisScript.setResultType(Void.class); // 设置返回值类型
|
||||||
|
|
||||||
|
// 执行 Lua 脚本
|
||||||
|
List<String> keys = Collections.emptyList(); // 如果脚本不依赖 key,可以传入空列表
|
||||||
|
redisTemplate.execute(redisScript, keys);
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
+1
-1
@@ -44,7 +44,7 @@ public class AgentServiceImpl extends BaseServiceImpl<AgentDao, AgentEntity> imp
|
|||||||
@Override
|
@Override
|
||||||
public PageData<AgentEntity> adminAgentList(Map<String, Object> params) {
|
public PageData<AgentEntity> adminAgentList(Map<String, Object> params) {
|
||||||
IPage<AgentEntity> page = agentDao.selectPage(
|
IPage<AgentEntity> page = agentDao.selectPage(
|
||||||
getPage(params, "sort", true),
|
getPage(params, "agent_name", true),
|
||||||
new QueryWrapper<>());
|
new QueryWrapper<>());
|
||||||
return new PageData<>(page.getRecords(), page.getTotal());
|
return new PageData<>(page.getRecords(), page.getTotal());
|
||||||
}
|
}
|
||||||
|
|||||||
+25
-23
@@ -3,6 +3,7 @@ package xiaozhi.modules.agent.service.impl;
|
|||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||||
|
import com.baomidou.mybatisplus.core.conditions.update.UpdateWrapper;
|
||||||
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
|
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
|
||||||
|
|
||||||
import xiaozhi.modules.agent.dao.AgentTemplateDao;
|
import xiaozhi.modules.agent.dao.AgentTemplateDao;
|
||||||
@@ -39,29 +40,30 @@ public class AgentTemplateServiceImpl extends ServiceImpl<AgentTemplateDao, Agen
|
|||||||
@Override
|
@Override
|
||||||
public void updateDefaultTemplateModelId(String modelType, String modelId) {
|
public void updateDefaultTemplateModelId(String modelType, String modelId) {
|
||||||
modelType = modelType.toUpperCase();
|
modelType = modelType.toUpperCase();
|
||||||
AgentTemplateEntity defaultTemplate = getDefaultTemplate();
|
|
||||||
if (defaultTemplate != null) {
|
UpdateWrapper<AgentTemplateEntity> wrapper = new UpdateWrapper<>();
|
||||||
switch (modelType) {
|
switch (modelType) {
|
||||||
case "ASR":
|
case "ASR":
|
||||||
defaultTemplate.setAsrModelId(modelId);
|
wrapper.set("asr_model_id", modelId);
|
||||||
break;
|
break;
|
||||||
case "VAD":
|
case "VAD":
|
||||||
defaultTemplate.setVadModelId(modelId);
|
wrapper.set("vad_model_id", modelId);
|
||||||
break;
|
break;
|
||||||
case "LLM":
|
case "LLM":
|
||||||
defaultTemplate.setLlmModelId(modelId);
|
wrapper.set("llm_model_id", modelId);
|
||||||
break;
|
break;
|
||||||
case "TTS":
|
case "TTS":
|
||||||
defaultTemplate.setTtsModelId(modelId);
|
wrapper.set("tts_model_id", modelId);
|
||||||
break;
|
wrapper.set("tts_voice_id", null);
|
||||||
case "Memory":
|
break;
|
||||||
defaultTemplate.setMemModelId(modelId);
|
case "MEMORY":
|
||||||
break;
|
wrapper.set("mem_model_id", modelId);
|
||||||
case "Intent":
|
break;
|
||||||
defaultTemplate.setIntentModelId(modelId);
|
case "INTENT":
|
||||||
break;
|
wrapper.set("intent_model_id", modelId);
|
||||||
}
|
break;
|
||||||
this.updateById(defaultTemplate);
|
|
||||||
}
|
}
|
||||||
|
wrapper.ge("sort", 0);
|
||||||
|
update(wrapper);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,6 +5,9 @@ import org.springframework.context.annotation.Configuration;
|
|||||||
import org.springframework.context.annotation.DependsOn;
|
import org.springframework.context.annotation.DependsOn;
|
||||||
|
|
||||||
import jakarta.annotation.PostConstruct;
|
import jakarta.annotation.PostConstruct;
|
||||||
|
import xiaozhi.common.constant.Constant;
|
||||||
|
import xiaozhi.common.redis.RedisKeys;
|
||||||
|
import xiaozhi.common.redis.RedisUtils;
|
||||||
import xiaozhi.modules.config.service.ConfigService;
|
import xiaozhi.modules.config.service.ConfigService;
|
||||||
import xiaozhi.modules.sys.service.SysParamsService;
|
import xiaozhi.modules.sys.service.SysParamsService;
|
||||||
|
|
||||||
@@ -18,8 +21,20 @@ public class SystemInitConfig {
|
|||||||
@Autowired
|
@Autowired
|
||||||
private ConfigService configService;
|
private ConfigService configService;
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private RedisUtils redisUtils;
|
||||||
|
|
||||||
@PostConstruct
|
@PostConstruct
|
||||||
public void init() {
|
public void init() {
|
||||||
|
// 检查版本号
|
||||||
|
String redisVersion = (String) redisUtils.get(RedisKeys.getVersionKey());
|
||||||
|
if (!Constant.VERSION.equals(redisVersion)) {
|
||||||
|
// 如果版本不一致,清空Redis
|
||||||
|
redisUtils.emptyAll();
|
||||||
|
// 存储新版本号
|
||||||
|
redisUtils.set(RedisKeys.getVersionKey(), Constant.VERSION);
|
||||||
|
}
|
||||||
|
|
||||||
sysParamsService.initServerSecret();
|
sysParamsService.initServerSecret();
|
||||||
configService.getConfig(false);
|
configService.getConfig(false);
|
||||||
}
|
}
|
||||||
|
|||||||
+45
-6
@@ -9,6 +9,7 @@ import org.apache.commons.lang3.StringUtils;
|
|||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
import lombok.AllArgsConstructor;
|
import lombok.AllArgsConstructor;
|
||||||
|
import xiaozhi.common.exception.ErrorCode;
|
||||||
import xiaozhi.common.exception.RenException;
|
import xiaozhi.common.exception.RenException;
|
||||||
import xiaozhi.common.redis.RedisKeys;
|
import xiaozhi.common.redis.RedisKeys;
|
||||||
import xiaozhi.common.redis.RedisUtils;
|
import xiaozhi.common.redis.RedisUtils;
|
||||||
@@ -83,8 +84,14 @@ public class ConfigServiceImpl implements ConfigService {
|
|||||||
// 根据MAC地址查找设备
|
// 根据MAC地址查找设备
|
||||||
DeviceEntity device = deviceService.getDeviceByMacAddress(macAddress);
|
DeviceEntity device = deviceService.getDeviceByMacAddress(macAddress);
|
||||||
if (device == null) {
|
if (device == null) {
|
||||||
throw new RenException("设备未找到");
|
// 如果设备,去redis里看看有没有需要连接的设备
|
||||||
|
String cachedCode = deviceService.geCodeByDeviceId(macAddress);
|
||||||
|
if (StringUtils.isNotBlank(cachedCode)) {
|
||||||
|
throw new RenException(ErrorCode.OTA_DEVICE_NEED_BIND, cachedCode);
|
||||||
|
}
|
||||||
|
throw new RenException(ErrorCode.OTA_DEVICE_NOT_FOUND, "not found device");
|
||||||
}
|
}
|
||||||
|
|
||||||
// 获取智能体信息
|
// 获取智能体信息
|
||||||
AgentEntity agent = agentService.getAgentById(device.getAgentId());
|
AgentEntity agent = agentService.getAgentById(device.getAgentId());
|
||||||
if (agent == null) {
|
if (agent == null) {
|
||||||
@@ -98,7 +105,9 @@ public class ConfigServiceImpl implements ConfigService {
|
|||||||
}
|
}
|
||||||
// 构建返回数据
|
// 构建返回数据
|
||||||
Map<String, Object> result = new HashMap<>();
|
Map<String, Object> result = new HashMap<>();
|
||||||
|
// 获取单台设备每天最多输出字数
|
||||||
|
String deviceMaxOutputSize = sysParamsService.getValue("device_max_output_size", true);
|
||||||
|
result.put("device_max_output_size", deviceMaxOutputSize);
|
||||||
// 如果客户端已实例化模型,则不返回
|
// 如果客户端已实例化模型,则不返回
|
||||||
String alreadySelectedVadModelId = (String) selectedModule.get("VAD");
|
String alreadySelectedVadModelId = (String) selectedModule.get("VAD");
|
||||||
if (alreadySelectedVadModelId != null && alreadySelectedVadModelId.equals(agent.getVadModelId())) {
|
if (alreadySelectedVadModelId != null && alreadySelectedVadModelId.equals(agent.getVadModelId())) {
|
||||||
@@ -171,7 +180,13 @@ public class ConfigServiceImpl implements ConfigService {
|
|||||||
switch (param.getValueType().toLowerCase()) {
|
switch (param.getValueType().toLowerCase()) {
|
||||||
case "number":
|
case "number":
|
||||||
try {
|
try {
|
||||||
current.put(lastKey, Double.parseDouble(value));
|
double doubleValue = Double.parseDouble(value);
|
||||||
|
// 如果数值是整数形式,则转换为Integer
|
||||||
|
if (doubleValue == (int) doubleValue) {
|
||||||
|
current.put(lastKey, (int) doubleValue);
|
||||||
|
} else {
|
||||||
|
current.put(lastKey, doubleValue);
|
||||||
|
}
|
||||||
} catch (NumberFormatException e) {
|
} catch (NumberFormatException e) {
|
||||||
current.put(lastKey, value);
|
current.put(lastKey, value);
|
||||||
}
|
}
|
||||||
@@ -231,8 +246,9 @@ public class ConfigServiceImpl implements ConfigService {
|
|||||||
boolean isCache) {
|
boolean isCache) {
|
||||||
Map<String, String> selectedModule = new HashMap<>();
|
Map<String, String> selectedModule = new HashMap<>();
|
||||||
|
|
||||||
String[] modelTypes = { "VAD", "ASR", "LLM", "TTS", "Memory", "Intent" };
|
String[] modelTypes = { "VAD", "ASR", "TTS", "Memory", "Intent", "LLM" };
|
||||||
String[] modelIds = { vadModelId, asrModelId, llmModelId, ttsModelId, memModelId, intentModelId };
|
String[] modelIds = { vadModelId, asrModelId, ttsModelId, memModelId, intentModelId, llmModelId };
|
||||||
|
String intentLLMModelId = null;
|
||||||
|
|
||||||
for (int i = 0; i < modelIds.length; i++) {
|
for (int i = 0; i < modelIds.length; i++) {
|
||||||
if (modelIds[i] == null) {
|
if (modelIds[i] == null) {
|
||||||
@@ -246,13 +262,36 @@ public class ConfigServiceImpl implements ConfigService {
|
|||||||
if ("TTS".equals(modelTypes[i]) && voice != null) {
|
if ("TTS".equals(modelTypes[i]) && voice != null) {
|
||||||
((Map<String, Object>) model.getConfigJson()).put("private_voice", voice);
|
((Map<String, Object>) model.getConfigJson()).put("private_voice", voice);
|
||||||
}
|
}
|
||||||
|
// 如果是Intent类型,且type=intent_llm,则给他添加附加模型
|
||||||
|
if ("Intent".equals(modelTypes[i])) {
|
||||||
|
Map<String, Object> map = (Map<String, Object>) model.getConfigJson();
|
||||||
|
if ("intent_llm".equals(map.get("type"))) {
|
||||||
|
intentLLMModelId = (String) map.get("llm");
|
||||||
|
if (intentLLMModelId != null && intentLLMModelId.equals(llmModelId)) {
|
||||||
|
intentLLMModelId = null;
|
||||||
|
}
|
||||||
|
} else if ("function_call".equals(map.get("type"))) {
|
||||||
|
String functionStr = (String) map.get("functions");
|
||||||
|
if (StringUtils.isNotBlank(functionStr)) {
|
||||||
|
String[] functions = functionStr.split("\\;");
|
||||||
|
map.put("functions", functions);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// 如果是LLM类型,且intentLLMModelId不为空,则添加附加模型
|
||||||
|
if ("LLM".equals(modelTypes[i]) && intentLLMModelId != null) {
|
||||||
|
ModelConfigEntity intentLLM = modelConfigService.getModelById(intentLLMModelId, isCache);
|
||||||
|
typeConfig.put(intentLLM.getId(), intentLLM.getConfigJson());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
result.put(modelTypes[i], typeConfig);
|
result.put(modelTypes[i], typeConfig);
|
||||||
|
|
||||||
selectedModule.put(modelTypes[i], model.getId());
|
selectedModule.put(modelTypes[i], model.getId());
|
||||||
}
|
}
|
||||||
|
|
||||||
result.put("selected_module", selectedModule);
|
result.put("selected_module", selectedModule);
|
||||||
if (StringUtils.isNotBlank(prompt)) {
|
if (StringUtils.isNotBlank(prompt)) {
|
||||||
prompt = prompt.replace("{{assistant_name}}", "小智");
|
prompt = prompt.replace("{{assistant_name}}", StringUtils.isBlank(assistantName) ? "小智" : assistantName);
|
||||||
}
|
}
|
||||||
result.put("prompt", prompt);
|
result.put("prompt", prompt);
|
||||||
}
|
}
|
||||||
|
|||||||
+36
-9
@@ -15,48 +15,75 @@ import org.springframework.web.bind.annotation.RestController;
|
|||||||
import com.fasterxml.jackson.annotation.JsonInclude;
|
import com.fasterxml.jackson.annotation.JsonInclude;
|
||||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||||
|
|
||||||
|
import io.swagger.v3.oas.annotations.Hidden;
|
||||||
import io.swagger.v3.oas.annotations.Operation;
|
import io.swagger.v3.oas.annotations.Operation;
|
||||||
import io.swagger.v3.oas.annotations.Parameter;
|
import io.swagger.v3.oas.annotations.Parameter;
|
||||||
import io.swagger.v3.oas.annotations.enums.ParameterIn;
|
import io.swagger.v3.oas.annotations.enums.ParameterIn;
|
||||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||||
import lombok.RequiredArgsConstructor;
|
import lombok.RequiredArgsConstructor;
|
||||||
import lombok.SneakyThrows;
|
import lombok.SneakyThrows;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import xiaozhi.common.constant.Constant;
|
||||||
import xiaozhi.modules.device.dto.DeviceReportReqDTO;
|
import xiaozhi.modules.device.dto.DeviceReportReqDTO;
|
||||||
import xiaozhi.modules.device.dto.DeviceReportRespDTO;
|
import xiaozhi.modules.device.dto.DeviceReportRespDTO;
|
||||||
|
import xiaozhi.modules.device.entity.DeviceEntity;
|
||||||
import xiaozhi.modules.device.service.DeviceService;
|
import xiaozhi.modules.device.service.DeviceService;
|
||||||
import xiaozhi.modules.device.utils.NetworkUtil;
|
import xiaozhi.modules.device.utils.NetworkUtil;
|
||||||
|
import xiaozhi.modules.sys.service.SysParamsService;
|
||||||
|
|
||||||
@Tag(name = "设备管理", description = "OTA 相关接口")
|
@Tag(name = "设备管理", description = "OTA 相关接口")
|
||||||
|
@Slf4j
|
||||||
@RestController
|
@RestController
|
||||||
@RequiredArgsConstructor
|
@RequiredArgsConstructor
|
||||||
@RequestMapping("/ota/")
|
@RequestMapping("/ota/")
|
||||||
public class OTAController {
|
public class OTAController {
|
||||||
private final DeviceService deviceService;
|
private final DeviceService deviceService;
|
||||||
|
private final SysParamsService sysParamsService;
|
||||||
|
|
||||||
@Operation(summary = "检查 OTA 版本和设备激活状态")
|
@Operation(summary = "OTA版本和设备激活状态检查")
|
||||||
@PostMapping
|
@PostMapping
|
||||||
public ResponseEntity<String> checkOTAVersion(
|
public ResponseEntity<String> checkOTAVersion(
|
||||||
@RequestBody DeviceReportReqDTO deviceReportReqDTO,
|
@RequestBody DeviceReportReqDTO deviceReportReqDTO,
|
||||||
|
|
||||||
@Parameter(name = "Device-Id", description = "设备唯一标识", required = true, in = ParameterIn.HEADER) @RequestHeader("Device-Id") String deviceId,
|
@Parameter(name = "Device-Id", description = "设备唯一标识", required = true, in = ParameterIn.HEADER) @RequestHeader("Device-Id") String deviceId,
|
||||||
|
@Parameter(name = "Client-Id", description = "客户端标识", required = false, in = ParameterIn.HEADER) @RequestHeader(value = "Client-Id", required = false) String clientId) {
|
||||||
@Parameter(name = "Client-Id", description = "客户端标识", required = true, in = ParameterIn.HEADER) @RequestHeader("Client-Id") String clientId) {
|
if (StringUtils.isBlank(deviceId)) {
|
||||||
if (StringUtils.isAnyBlank(deviceId, clientId)) {
|
|
||||||
return createResponse(DeviceReportRespDTO.createError("Device ID is required"));
|
return createResponse(DeviceReportRespDTO.createError("Device ID is required"));
|
||||||
}
|
}
|
||||||
|
if (StringUtils.isBlank(clientId)) {
|
||||||
|
clientId = deviceId;
|
||||||
|
}
|
||||||
String macAddress = deviceReportReqDTO.getMacAddress();
|
String macAddress = deviceReportReqDTO.getMacAddress();
|
||||||
boolean macAddressValid = NetworkUtil.isMacAddressValid(macAddress);
|
boolean macAddressValid = NetworkUtil.isMacAddressValid(macAddress);
|
||||||
// 设备Id和Mac地址应是一致的, 并且必须需要application字段
|
// 设备Id和Mac地址应是一致的, 并且必须需要application字段
|
||||||
if (!deviceId.equals(macAddress) || !macAddressValid || deviceReportReqDTO.getApplication() == null) {
|
if (!deviceId.equals(macAddress) || !macAddressValid || deviceReportReqDTO.getApplication() == null) {
|
||||||
return createResponse(DeviceReportRespDTO.createError("Invalid OTA request"));
|
return createResponse(DeviceReportRespDTO.createError("Invalid OTA request"));
|
||||||
}
|
}
|
||||||
return createResponse(deviceService.checkDeviceActive(macAddress, deviceId, clientId, deviceReportReqDTO));
|
return createResponse(deviceService.checkDeviceActive(macAddress, clientId, deviceReportReqDTO));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Operation(summary = "设备快速检查激活状态")
|
||||||
|
@PostMapping("activate")
|
||||||
|
public ResponseEntity<String> activateDevice(
|
||||||
|
@Parameter(name = "Device-Id", description = "设备唯一标识", required = true, in = ParameterIn.HEADER) @RequestHeader("Device-Id") String deviceId,
|
||||||
|
@Parameter(name = "Client-Id", description = "客户端标识", required = false, in = ParameterIn.HEADER) @RequestHeader(value = "Client-Id", required = false) String clientId) {
|
||||||
|
if (StringUtils.isBlank(deviceId)) {
|
||||||
|
return ResponseEntity.status(202).build();
|
||||||
|
}
|
||||||
|
DeviceEntity device = deviceService.getDeviceByMacAddress(deviceId);
|
||||||
|
if (device == null) {
|
||||||
|
return ResponseEntity.status(202).build();
|
||||||
|
}
|
||||||
|
return ResponseEntity.ok("success");
|
||||||
}
|
}
|
||||||
|
|
||||||
@Operation(summary = "获取 OTA 提示信息")
|
|
||||||
@GetMapping
|
@GetMapping
|
||||||
public ResponseEntity<String> getOTAPrompt() {
|
@Hidden
|
||||||
return createResponse(DeviceReportRespDTO.createError("请提交正确的ota参数"));
|
public ResponseEntity<String> getOTA() {
|
||||||
|
String wsUrl = sysParamsService.getValue(Constant.SERVER_WEBSOCKET, true);
|
||||||
|
if (StringUtils.isBlank(wsUrl) || wsUrl.equals("null")) {
|
||||||
|
return ResponseEntity.ok("OTA接口不正常,缺少websocket地址");
|
||||||
|
}
|
||||||
|
return ResponseEntity.ok("OTA接口运行正常,websocket集群数量:" + wsUrl.split(";").length);
|
||||||
}
|
}
|
||||||
|
|
||||||
@SneakyThrows
|
@SneakyThrows
|
||||||
|
|||||||
+291
@@ -0,0 +1,291 @@
|
|||||||
|
package xiaozhi.modules.device.controller;
|
||||||
|
|
||||||
|
import java.io.File;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.nio.file.Files;
|
||||||
|
import java.nio.file.Path;
|
||||||
|
import java.nio.file.Paths;
|
||||||
|
import java.security.MessageDigest;
|
||||||
|
import java.security.NoSuchAlgorithmException;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.UUID;
|
||||||
|
|
||||||
|
import org.apache.commons.lang3.StringUtils;
|
||||||
|
import org.apache.shiro.authz.annotation.RequiresPermissions;
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
import org.springframework.http.HttpHeaders;
|
||||||
|
import org.springframework.http.HttpStatus;
|
||||||
|
import org.springframework.http.MediaType;
|
||||||
|
import org.springframework.http.ResponseEntity;
|
||||||
|
import org.springframework.web.bind.annotation.DeleteMapping;
|
||||||
|
import org.springframework.web.bind.annotation.GetMapping;
|
||||||
|
import org.springframework.web.bind.annotation.PathVariable;
|
||||||
|
import org.springframework.web.bind.annotation.PostMapping;
|
||||||
|
import org.springframework.web.bind.annotation.PutMapping;
|
||||||
|
import org.springframework.web.bind.annotation.RequestBody;
|
||||||
|
import org.springframework.web.bind.annotation.RequestMapping;
|
||||||
|
import org.springframework.web.bind.annotation.RequestParam;
|
||||||
|
import org.springframework.web.bind.annotation.RestController;
|
||||||
|
import org.springframework.web.multipart.MultipartFile;
|
||||||
|
|
||||||
|
import io.swagger.v3.oas.annotations.Operation;
|
||||||
|
import io.swagger.v3.oas.annotations.Parameter;
|
||||||
|
import io.swagger.v3.oas.annotations.Parameters;
|
||||||
|
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import xiaozhi.common.constant.Constant;
|
||||||
|
import xiaozhi.common.page.PageData;
|
||||||
|
import xiaozhi.common.redis.RedisKeys;
|
||||||
|
import xiaozhi.common.redis.RedisUtils;
|
||||||
|
import xiaozhi.common.utils.Result;
|
||||||
|
import xiaozhi.common.validator.ValidatorUtils;
|
||||||
|
import xiaozhi.modules.device.entity.OtaEntity;
|
||||||
|
import xiaozhi.modules.device.service.OtaService;
|
||||||
|
|
||||||
|
@Tag(name = "设备管理", description = "OTA 相关接口")
|
||||||
|
@Slf4j
|
||||||
|
@RestController
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
@RequestMapping("/otaMag")
|
||||||
|
public class OTAMagController {
|
||||||
|
private static final Logger logger = LoggerFactory.getLogger(OTAController.class);
|
||||||
|
private final OtaService otaService;
|
||||||
|
private final RedisUtils redisUtils;
|
||||||
|
|
||||||
|
@GetMapping
|
||||||
|
@Operation(summary = "分页查询 OTA 固件信息")
|
||||||
|
@Parameters({
|
||||||
|
@Parameter(name = Constant.PAGE, description = "当前页码,从1开始", required = true),
|
||||||
|
@Parameter(name = Constant.LIMIT, description = "每页显示记录数", required = true)
|
||||||
|
})
|
||||||
|
@RequiresPermissions("sys:role:superAdmin")
|
||||||
|
public Result<PageData<OtaEntity>> page(@Parameter(hidden = true) @RequestParam Map<String, Object> params) {
|
||||||
|
ValidatorUtils.validateEntity(params);
|
||||||
|
PageData<OtaEntity> page = otaService.page(params);
|
||||||
|
return new Result<PageData<OtaEntity>>().ok(page);
|
||||||
|
}
|
||||||
|
|
||||||
|
@GetMapping("{id}")
|
||||||
|
@Operation(summary = "信息 OTA 固件信息")
|
||||||
|
@RequiresPermissions("sys:role:superAdmin")
|
||||||
|
public Result<OtaEntity> get(@PathVariable("id") String id) {
|
||||||
|
OtaEntity data = otaService.selectById(id);
|
||||||
|
return new Result<OtaEntity>().ok(data);
|
||||||
|
}
|
||||||
|
|
||||||
|
@PostMapping
|
||||||
|
@Operation(summary = "保存 OTA 固件信息")
|
||||||
|
@RequiresPermissions("sys:role:superAdmin")
|
||||||
|
public Result<Void> save(@RequestBody OtaEntity entity) {
|
||||||
|
if (entity == null) {
|
||||||
|
return new Result<Void>().error("固件信息不能为空");
|
||||||
|
}
|
||||||
|
if (StringUtils.isBlank(entity.getFirmwareName())) {
|
||||||
|
return new Result<Void>().error("固件名称不能为空");
|
||||||
|
}
|
||||||
|
if (StringUtils.isBlank(entity.getType())) {
|
||||||
|
return new Result<Void>().error("固件类型不能为空");
|
||||||
|
}
|
||||||
|
if (StringUtils.isBlank(entity.getVersion())) {
|
||||||
|
return new Result<Void>().error("版本号不能为空");
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
otaService.save(entity);
|
||||||
|
return new Result<Void>();
|
||||||
|
} catch (RuntimeException e) {
|
||||||
|
return new Result<Void>().error(e.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@DeleteMapping("/{id}")
|
||||||
|
@Operation(summary = "OTA 删除")
|
||||||
|
@RequiresPermissions("sys:role:superAdmin")
|
||||||
|
public Result<Void> delete(@PathVariable("id") String[] ids) {
|
||||||
|
if (ids == null || ids.length == 0) {
|
||||||
|
return new Result<Void>().error("删除的固件ID不能为空");
|
||||||
|
}
|
||||||
|
otaService.delete(ids);
|
||||||
|
return new Result<Void>();
|
||||||
|
}
|
||||||
|
|
||||||
|
@PutMapping("/{id}")
|
||||||
|
@Operation(summary = "修改 OTA 固件信息")
|
||||||
|
@RequiresPermissions("sys:role:superAdmin")
|
||||||
|
public Result<?> update(@PathVariable("id") String id, @RequestBody OtaEntity entity) {
|
||||||
|
if (entity == null) {
|
||||||
|
return new Result<>().error("固件信息不能为空");
|
||||||
|
}
|
||||||
|
entity.setId(id);
|
||||||
|
try {
|
||||||
|
otaService.update(entity);
|
||||||
|
return new Result<>();
|
||||||
|
} catch (RuntimeException e) {
|
||||||
|
return new Result<>().error(e.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@GetMapping("/getDownloadUrl/{id}")
|
||||||
|
@Operation(summary = "获取 OTA 固件下载链接")
|
||||||
|
@RequiresPermissions("sys:role:superAdmin")
|
||||||
|
public Result<String> getDownloadUrl(@PathVariable("id") String id) {
|
||||||
|
String uuid = UUID.randomUUID().toString();
|
||||||
|
redisUtils.set(RedisKeys.getOtaIdKey(uuid), id);
|
||||||
|
return new Result<String>().ok(uuid);
|
||||||
|
}
|
||||||
|
|
||||||
|
@GetMapping("/download/{uuid}")
|
||||||
|
@Operation(summary = "下载固件文件")
|
||||||
|
public ResponseEntity<byte[]> downloadFirmware(@PathVariable("uuid") String uuid) {
|
||||||
|
String id = (String) redisUtils.get(RedisKeys.getOtaIdKey(uuid));
|
||||||
|
if (StringUtils.isBlank(id)) {
|
||||||
|
return ResponseEntity.notFound().build();
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查下载次数
|
||||||
|
String downloadCountKey = RedisKeys.getOtaDownloadCountKey(uuid);
|
||||||
|
Integer downloadCount = (Integer) redisUtils.get(downloadCountKey);
|
||||||
|
if (downloadCount == null) {
|
||||||
|
downloadCount = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 如果下载次数超过3次,返回404
|
||||||
|
if (downloadCount >= 3) {
|
||||||
|
redisUtils.delete(downloadCountKey);
|
||||||
|
redisUtils.delete(RedisKeys.getOtaIdKey(uuid));
|
||||||
|
logger.warn("Download limit exceeded for UUID: {}", uuid);
|
||||||
|
return ResponseEntity.notFound().build();
|
||||||
|
}
|
||||||
|
|
||||||
|
redisUtils.set(downloadCountKey, downloadCount + 1);
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 获取固件信息
|
||||||
|
OtaEntity otaEntity = otaService.selectById(id);
|
||||||
|
if (otaEntity == null || StringUtils.isBlank(otaEntity.getFirmwarePath())) {
|
||||||
|
logger.warn("Firmware not found or path is empty for ID: {}", id);
|
||||||
|
return ResponseEntity.notFound().build();
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取文件路径 - 确保路径是绝对路径或正确的相对路径
|
||||||
|
String firmwarePath = otaEntity.getFirmwarePath();
|
||||||
|
Path path;
|
||||||
|
|
||||||
|
// 检查是否是绝对路径
|
||||||
|
if (Paths.get(firmwarePath).isAbsolute()) {
|
||||||
|
path = Paths.get(firmwarePath);
|
||||||
|
} else {
|
||||||
|
// 如果是相对路径,则从当前工作目录解析
|
||||||
|
path = Paths.get(System.getProperty("user.dir"), firmwarePath);
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.info("Attempting to download firmware for ID: {}, DB path: {}, resolved path: {}",
|
||||||
|
id, firmwarePath, path.toAbsolutePath());
|
||||||
|
|
||||||
|
if (!Files.exists(path) || !Files.isRegularFile(path)) {
|
||||||
|
// 尝试直接从firmware目录下查找文件名
|
||||||
|
String fileName = new File(firmwarePath).getName();
|
||||||
|
Path altPath = Paths.get(System.getProperty("user.dir"), "firmware", fileName);
|
||||||
|
|
||||||
|
logger.info("File not found at primary path, trying alternative path: {}", altPath.toAbsolutePath());
|
||||||
|
|
||||||
|
if (Files.exists(altPath) && Files.isRegularFile(altPath)) {
|
||||||
|
path = altPath;
|
||||||
|
} else {
|
||||||
|
logger.error("Firmware file not found at either path: {} or {}",
|
||||||
|
path.toAbsolutePath(), altPath.toAbsolutePath());
|
||||||
|
return ResponseEntity.notFound().build();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 读取文件内容
|
||||||
|
byte[] fileContent = Files.readAllBytes(path);
|
||||||
|
|
||||||
|
// 设置响应头
|
||||||
|
String originalFilename = otaEntity.getType() + "_" + otaEntity.getVersion();
|
||||||
|
if (firmwarePath.contains(".")) {
|
||||||
|
String extension = firmwarePath.substring(firmwarePath.lastIndexOf("."));
|
||||||
|
originalFilename += extension;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 清理文件名,移除不安全字符
|
||||||
|
String safeFilename = originalFilename.replaceAll("[^a-zA-Z0-9._-]", "_");
|
||||||
|
|
||||||
|
logger.info("Providing download for firmware ID: {}, filename: {}, size: {} bytes",
|
||||||
|
id, safeFilename, fileContent.length);
|
||||||
|
|
||||||
|
return ResponseEntity.ok()
|
||||||
|
.contentType(MediaType.APPLICATION_OCTET_STREAM)
|
||||||
|
.header(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=\"" + safeFilename + "\"")
|
||||||
|
.body(fileContent);
|
||||||
|
} catch (IOException e) {
|
||||||
|
logger.error("Error reading firmware file for ID: {}", id, e);
|
||||||
|
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).build();
|
||||||
|
} catch (Exception e) {
|
||||||
|
logger.error("Unexpected error during firmware download for ID: {}", id, e);
|
||||||
|
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).build();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@PostMapping("/upload")
|
||||||
|
@Operation(summary = "上传固件文件")
|
||||||
|
@RequiresPermissions("sys:role:superAdmin")
|
||||||
|
public Result<String> uploadFirmware(@RequestParam("file") MultipartFile file) {
|
||||||
|
if (file.isEmpty()) {
|
||||||
|
return new Result<String>().error("上传文件不能为空");
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查文件扩展名
|
||||||
|
String originalFilename = file.getOriginalFilename();
|
||||||
|
if (originalFilename == null) {
|
||||||
|
return new Result<String>().error("文件名不能为空");
|
||||||
|
}
|
||||||
|
|
||||||
|
String extension = originalFilename.substring(originalFilename.lastIndexOf(".")).toLowerCase();
|
||||||
|
if (!extension.equals(".bin") && !extension.equals(".apk")) {
|
||||||
|
return new Result<String>().error("只允许上传.bin和.apk格式的文件");
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 计算文件的MD5值
|
||||||
|
String md5 = calculateMD5(file);
|
||||||
|
|
||||||
|
// 设置存储路径
|
||||||
|
String uploadDir = "uploadfile";
|
||||||
|
Path uploadPath = Paths.get(uploadDir);
|
||||||
|
|
||||||
|
// 如果目录不存在,创建目录
|
||||||
|
if (!Files.exists(uploadPath)) {
|
||||||
|
Files.createDirectories(uploadPath);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 使用MD5作为文件名,固定使用.bin扩展名
|
||||||
|
String uniqueFileName = md5 + extension;
|
||||||
|
Path filePath = uploadPath.resolve(uniqueFileName);
|
||||||
|
|
||||||
|
// 检查文件是否已存在
|
||||||
|
if (Files.exists(filePath)) {
|
||||||
|
return new Result<String>().ok(filePath.toString());
|
||||||
|
}
|
||||||
|
|
||||||
|
// 保存文件
|
||||||
|
Files.copy(file.getInputStream(), filePath);
|
||||||
|
|
||||||
|
// 返回文件路径
|
||||||
|
return new Result<String>().ok(filePath.toString());
|
||||||
|
} catch (IOException | NoSuchAlgorithmException e) {
|
||||||
|
return new Result<String>().error("文件上传失败:" + e.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private String calculateMD5(MultipartFile file) throws IOException, NoSuchAlgorithmException {
|
||||||
|
MessageDigest md = MessageDigest.getInstance("MD5");
|
||||||
|
byte[] digest = md.digest(file.getBytes());
|
||||||
|
StringBuilder sb = new StringBuilder();
|
||||||
|
for (byte b : digest) {
|
||||||
|
sb.append(String.format("%02x", b));
|
||||||
|
}
|
||||||
|
return sb.toString();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
package xiaozhi.modules.device.dao;
|
||||||
|
|
||||||
|
import org.apache.ibatis.annotations.Mapper;
|
||||||
|
|
||||||
|
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||||
|
|
||||||
|
import xiaozhi.modules.device.entity.OtaEntity;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* OTA固件管理
|
||||||
|
*/
|
||||||
|
@Mapper
|
||||||
|
public interface OtaDao extends BaseMapper<OtaEntity> {
|
||||||
|
|
||||||
|
}
|
||||||
@@ -19,6 +19,9 @@ public class DeviceReportRespDTO {
|
|||||||
|
|
||||||
@Schema(description = "固件版本信息")
|
@Schema(description = "固件版本信息")
|
||||||
private Firmware firmware;
|
private Firmware firmware;
|
||||||
|
|
||||||
|
@Schema(description = "WebSocket配置")
|
||||||
|
private Websocket websocket;
|
||||||
|
|
||||||
@Getter
|
@Getter
|
||||||
@Setter
|
@Setter
|
||||||
@@ -44,6 +47,8 @@ public class DeviceReportRespDTO {
|
|||||||
@Schema(description = "激活码信息: 激活地址")
|
@Schema(description = "激活码信息: 激活地址")
|
||||||
private String message;
|
private String message;
|
||||||
|
|
||||||
|
@Schema(description = "挑战码")
|
||||||
|
private String challenge;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Getter
|
@Getter
|
||||||
@@ -58,4 +63,11 @@ public class DeviceReportRespDTO {
|
|||||||
@Schema(description = "时区偏移量,单位为分钟")
|
@Schema(description = "时区偏移量,单位为分钟")
|
||||||
private Integer timezone_offset;
|
private Integer timezone_offset;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Getter
|
||||||
|
@Setter
|
||||||
|
public static class Websocket {
|
||||||
|
@Schema(description = "WebSocket服务器地址")
|
||||||
|
private String url;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,61 @@
|
|||||||
|
package xiaozhi.modules.device.entity;
|
||||||
|
|
||||||
|
import java.util.Date;
|
||||||
|
|
||||||
|
import com.baomidou.mybatisplus.annotation.FieldFill;
|
||||||
|
import com.baomidou.mybatisplus.annotation.IdType;
|
||||||
|
import com.baomidou.mybatisplus.annotation.TableField;
|
||||||
|
import com.baomidou.mybatisplus.annotation.TableId;
|
||||||
|
import com.baomidou.mybatisplus.annotation.TableName;
|
||||||
|
|
||||||
|
import io.swagger.v3.oas.annotations.media.Schema;
|
||||||
|
import lombok.Data;
|
||||||
|
import lombok.EqualsAndHashCode;
|
||||||
|
|
||||||
|
@Data
|
||||||
|
@EqualsAndHashCode(callSuper = false)
|
||||||
|
@TableName("ai_ota")
|
||||||
|
@Schema(description = "固件信息")
|
||||||
|
public class OtaEntity {
|
||||||
|
|
||||||
|
@TableId(type = IdType.ASSIGN_UUID)
|
||||||
|
@Schema(description = "ID")
|
||||||
|
private String id;
|
||||||
|
|
||||||
|
@Schema(description = "固件名称")
|
||||||
|
private String firmwareName;
|
||||||
|
|
||||||
|
@Schema(description = "固件类型")
|
||||||
|
private String type;
|
||||||
|
|
||||||
|
@Schema(description = "版本号")
|
||||||
|
private String version;
|
||||||
|
|
||||||
|
@Schema(description = "文件大小(字节)")
|
||||||
|
private Long size;
|
||||||
|
|
||||||
|
@Schema(description = "备注/说明")
|
||||||
|
private String remark;
|
||||||
|
|
||||||
|
@Schema(description = "固件路径")
|
||||||
|
private String firmwarePath;
|
||||||
|
|
||||||
|
@Schema(description = "排序")
|
||||||
|
private Integer sort;
|
||||||
|
|
||||||
|
@Schema(description = "更新者")
|
||||||
|
@TableField(fill = FieldFill.UPDATE)
|
||||||
|
private Long updater;
|
||||||
|
|
||||||
|
@Schema(description = "更新时间")
|
||||||
|
@TableField(fill = FieldFill.UPDATE)
|
||||||
|
private Date updateDate;
|
||||||
|
|
||||||
|
@Schema(description = "创建者")
|
||||||
|
@TableField(fill = FieldFill.INSERT)
|
||||||
|
private Long creator;
|
||||||
|
|
||||||
|
@Schema(description = "创建时间")
|
||||||
|
@TableField(fill = FieldFill.INSERT)
|
||||||
|
private Date createDate;
|
||||||
|
}
|
||||||
@@ -19,7 +19,7 @@ public interface DeviceService {
|
|||||||
/**
|
/**
|
||||||
* 检查设备是否激活
|
* 检查设备是否激活
|
||||||
*/
|
*/
|
||||||
DeviceReportRespDTO checkDeviceActive(String macAddress, String deviceId, String clientId,
|
DeviceReportRespDTO checkDeviceActive(String macAddress, String clientId,
|
||||||
DeviceReportReqDTO deviceReport);
|
DeviceReportReqDTO deviceReport);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -74,4 +74,12 @@ public interface DeviceService {
|
|||||||
* @return 设备信息
|
* @return 设备信息
|
||||||
*/
|
*/
|
||||||
DeviceEntity getDeviceByMacAddress(String macAddress);
|
DeviceEntity getDeviceByMacAddress(String macAddress);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 根据设备ID获取激活码
|
||||||
|
*
|
||||||
|
* @param deviceId 设备ID
|
||||||
|
* @return 激活码
|
||||||
|
*/
|
||||||
|
String geCodeByDeviceId(String deviceId);
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
package xiaozhi.modules.device.service;
|
||||||
|
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
import xiaozhi.common.page.PageData;
|
||||||
|
import xiaozhi.common.service.BaseService;
|
||||||
|
import xiaozhi.modules.device.entity.OtaEntity;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* OTA固件管理
|
||||||
|
*/
|
||||||
|
public interface OtaService extends BaseService<OtaEntity> {
|
||||||
|
PageData<OtaEntity> page(Map<String, Object> params);
|
||||||
|
|
||||||
|
boolean save(OtaEntity entity);
|
||||||
|
|
||||||
|
void update(OtaEntity entity);
|
||||||
|
|
||||||
|
void delete(String[] ids);
|
||||||
|
}
|
||||||
+93
-43
@@ -9,7 +9,6 @@ import java.util.TimeZone;
|
|||||||
import java.util.concurrent.TimeUnit;
|
import java.util.concurrent.TimeUnit;
|
||||||
|
|
||||||
import org.apache.commons.lang3.StringUtils;
|
import org.apache.commons.lang3.StringUtils;
|
||||||
import org.springframework.beans.factory.annotation.Value;
|
|
||||||
import org.springframework.data.redis.core.RedisTemplate;
|
import org.springframework.data.redis.core.RedisTemplate;
|
||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
@@ -19,6 +18,7 @@ import com.baomidou.mybatisplus.core.conditions.update.UpdateWrapper;
|
|||||||
import com.baomidou.mybatisplus.core.metadata.IPage;
|
import com.baomidou.mybatisplus.core.metadata.IPage;
|
||||||
|
|
||||||
import cn.hutool.core.util.RandomUtil;
|
import cn.hutool.core.util.RandomUtil;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
import xiaozhi.common.constant.Constant;
|
import xiaozhi.common.constant.Constant;
|
||||||
import xiaozhi.common.exception.RenException;
|
import xiaozhi.common.exception.RenException;
|
||||||
import xiaozhi.common.page.PageData;
|
import xiaozhi.common.page.PageData;
|
||||||
@@ -34,8 +34,10 @@ import xiaozhi.modules.device.entity.DeviceEntity;
|
|||||||
import xiaozhi.modules.device.service.DeviceService;
|
import xiaozhi.modules.device.service.DeviceService;
|
||||||
import xiaozhi.modules.device.vo.UserShowDeviceListVO;
|
import xiaozhi.modules.device.vo.UserShowDeviceListVO;
|
||||||
import xiaozhi.modules.security.user.SecurityUser;
|
import xiaozhi.modules.security.user.SecurityUser;
|
||||||
|
import xiaozhi.modules.sys.service.SysParamsService;
|
||||||
import xiaozhi.modules.sys.service.SysUserUtilService;
|
import xiaozhi.modules.sys.service.SysUserUtilService;
|
||||||
|
|
||||||
|
@Slf4j
|
||||||
@Service
|
@Service
|
||||||
public class DeviceServiceImpl extends BaseServiceImpl<DeviceDao, DeviceEntity> implements DeviceService {
|
public class DeviceServiceImpl extends BaseServiceImpl<DeviceDao, DeviceEntity> implements DeviceService {
|
||||||
|
|
||||||
@@ -43,18 +45,18 @@ public class DeviceServiceImpl extends BaseServiceImpl<DeviceDao, DeviceEntity>
|
|||||||
|
|
||||||
private final SysUserUtilService sysUserUtilService;
|
private final SysUserUtilService sysUserUtilService;
|
||||||
|
|
||||||
private final String frontedUrl;
|
|
||||||
|
|
||||||
private final RedisTemplate<String, Object> redisTemplate;
|
private final RedisTemplate<String, Object> redisTemplate;
|
||||||
|
|
||||||
|
private final SysParamsService sysParamsService;
|
||||||
|
|
||||||
// 添加构造函数来初始化 deviceMapper
|
// 添加构造函数来初始化 deviceMapper
|
||||||
public DeviceServiceImpl(DeviceDao deviceDao, SysUserUtilService sysUserUtilService,
|
public DeviceServiceImpl(DeviceDao deviceDao, SysUserUtilService sysUserUtilService,
|
||||||
@Value("${app.fronted-url:http://localhost:8001}") String frontedUrl,
|
SysParamsService sysParamsService,
|
||||||
RedisTemplate<String, Object> redisTemplate) {
|
RedisTemplate<String, Object> redisTemplate) {
|
||||||
this.deviceDao = deviceDao;
|
this.deviceDao = deviceDao;
|
||||||
this.sysUserUtilService = sysUserUtilService;
|
this.sysUserUtilService = sysUserUtilService;
|
||||||
this.frontedUrl = frontedUrl;
|
|
||||||
this.redisTemplate = redisTemplate;
|
this.redisTemplate = redisTemplate;
|
||||||
|
this.sysParamsService = sysParamsService;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -120,7 +122,7 @@ public class DeviceServiceImpl extends BaseServiceImpl<DeviceDao, DeviceEntity>
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public DeviceReportRespDTO checkDeviceActive(String macAddress, String deviceId, String clientId,
|
public DeviceReportRespDTO checkDeviceActive(String macAddress, String clientId,
|
||||||
DeviceReportReqDTO deviceReport) {
|
DeviceReportReqDTO deviceReport) {
|
||||||
DeviceReportRespDTO response = new DeviceReportRespDTO();
|
DeviceReportRespDTO response = new DeviceReportRespDTO();
|
||||||
response.setServer_time(buildServerTime());
|
response.setServer_time(buildServerTime());
|
||||||
@@ -132,46 +134,33 @@ public class DeviceServiceImpl extends BaseServiceImpl<DeviceDao, DeviceEntity>
|
|||||||
firmware.setUrl("http://localhost:8002/xiaozhi/ota/download");
|
firmware.setUrl("http://localhost:8002/xiaozhi/ota/download");
|
||||||
response.setFirmware(firmware);
|
response.setFirmware(firmware);
|
||||||
|
|
||||||
DeviceEntity deviceById = getDeviceById(deviceId);
|
// 添加WebSocket配置
|
||||||
|
DeviceReportRespDTO.Websocket websocket = new DeviceReportRespDTO.Websocket();
|
||||||
|
// 从系统参数获取WebSocket URL,如果未配置则使用默认值
|
||||||
|
String wsUrl = sysParamsService.getValue(Constant.SERVER_WEBSOCKET, true);
|
||||||
|
if (StringUtils.isBlank(wsUrl) || wsUrl.equals("null")) {
|
||||||
|
log.error("WebSocket URL is not configured");
|
||||||
|
wsUrl = "ws://xiaozhi.server.com:8000/xiaozhi/v1/";
|
||||||
|
websocket.setUrl(wsUrl);
|
||||||
|
} else {
|
||||||
|
String[] wsUrls = wsUrl.split("\\;");
|
||||||
|
if (wsUrls.length > 0) {
|
||||||
|
// 随机选择一个WebSocket URL
|
||||||
|
websocket.setUrl(wsUrls[RandomUtil.randomInt(0, wsUrls.length)]);
|
||||||
|
} else {
|
||||||
|
log.error("WebSocket URL list is empty");
|
||||||
|
websocket.setUrl("ws://xiaozhi.server.com:8000/xiaozhi/v1/");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
response.setWebsocket(websocket);
|
||||||
|
|
||||||
|
DeviceEntity deviceById = getDeviceById(macAddress);
|
||||||
if (deviceById != null) { // 如果设备存在,则更新上次连接时间
|
if (deviceById != null) { // 如果设备存在,则更新上次连接时间
|
||||||
deviceById.setLastConnectedAt(new Date());
|
deviceById.setLastConnectedAt(new Date());
|
||||||
deviceDao.updateById(deviceById);
|
deviceDao.updateById(deviceById);
|
||||||
} else { // 如果设备不存在,则生成激活码
|
} else { // 如果设备不存在,则生成激活码
|
||||||
String safeDeviceId = deviceId.replace(":", "_").toLowerCase();
|
DeviceReportRespDTO.Activation code = buildActivation(macAddress, deviceReport);
|
||||||
String dataKey = String.format("ota:activation:data:%s", safeDeviceId);
|
|
||||||
|
|
||||||
Map<Object, Object> cacheMap = redisTemplate.opsForHash().entries(dataKey);
|
|
||||||
DeviceReportRespDTO.Activation code = new DeviceReportRespDTO.Activation();
|
|
||||||
|
|
||||||
if (cacheMap != null && cacheMap.containsKey("activation_code")) {
|
|
||||||
String cachedCode = (String) cacheMap.get("activation_code");
|
|
||||||
code.setCode(cachedCode);
|
|
||||||
code.setMessage(frontedUrl + "\n" + cachedCode);
|
|
||||||
} else {
|
|
||||||
String newCode = RandomUtil.randomNumbers(6);
|
|
||||||
code.setCode(newCode);
|
|
||||||
code.setMessage(frontedUrl + "\n" + newCode);
|
|
||||||
|
|
||||||
Map<String, Object> dataMap = new HashMap<>();
|
|
||||||
dataMap.put("id", deviceId);
|
|
||||||
dataMap.put("mac_address", macAddress);
|
|
||||||
dataMap.put("board", (deviceReport.getChipModelName() != null) ? deviceReport.getChipModelName()
|
|
||||||
: (deviceReport.getBoard() != null ? deviceReport.getBoard().getType() : "unknown"));
|
|
||||||
dataMap.put("app_version", (deviceReport.getApplication() != null)
|
|
||||||
? deviceReport.getApplication().getVersion()
|
|
||||||
: null);
|
|
||||||
dataMap.put("deviceId", deviceId);
|
|
||||||
dataMap.put("activation_code", newCode);
|
|
||||||
|
|
||||||
// 写入主数据 key
|
|
||||||
redisTemplate.opsForHash().putAll(dataKey, dataMap);
|
|
||||||
redisTemplate.expire(dataKey, 24, TimeUnit.HOURS);
|
|
||||||
|
|
||||||
// 写入反查激活码 key
|
|
||||||
String codeKey = "ota:activation:code:" + newCode;
|
|
||||||
redisTemplate.opsForValue().set(codeKey, deviceId, 24, TimeUnit.HOURS);
|
|
||||||
}
|
|
||||||
|
|
||||||
response.setActivation(code);
|
response.setActivation(code);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -221,7 +210,7 @@ public class DeviceServiceImpl extends BaseServiceImpl<DeviceDao, DeviceEntity>
|
|||||||
params.put(Constant.PAGE, dto.getPage());
|
params.put(Constant.PAGE, dto.getPage());
|
||||||
params.put(Constant.LIMIT, dto.getLimit());
|
params.put(Constant.LIMIT, dto.getLimit());
|
||||||
IPage<DeviceEntity> page = baseDao.selectPage(
|
IPage<DeviceEntity> page = baseDao.selectPage(
|
||||||
getPage(params, "sort", true),
|
getPage(params, "mac_address", true),
|
||||||
// 定义查询条件
|
// 定义查询条件
|
||||||
new QueryWrapper<DeviceEntity>()
|
new QueryWrapper<DeviceEntity>()
|
||||||
// 必须设备关键词查找
|
// 必须设备关键词查找
|
||||||
@@ -258,4 +247,65 @@ public class DeviceServiceImpl extends BaseServiceImpl<DeviceDao, DeviceEntity>
|
|||||||
serverTime.setTimezone_offset(tz.getOffset(System.currentTimeMillis()) / (60 * 1000));
|
serverTime.setTimezone_offset(tz.getOffset(System.currentTimeMillis()) / (60 * 1000));
|
||||||
return serverTime;
|
return serverTime;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String geCodeByDeviceId(String deviceId) {
|
||||||
|
String dataKey = getDeviceCacheKey(deviceId);
|
||||||
|
|
||||||
|
Map<Object, Object> cacheMap = redisTemplate.opsForHash().entries(dataKey);
|
||||||
|
if (cacheMap != null && cacheMap.containsKey("activation_code")) {
|
||||||
|
String cachedCode = (String) cacheMap.get("activation_code");
|
||||||
|
return cachedCode;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private String getDeviceCacheKey(String deviceId) {
|
||||||
|
String safeDeviceId = deviceId.replace(":", "_").toLowerCase();
|
||||||
|
String dataKey = String.format("ota:activation:data:%s", safeDeviceId);
|
||||||
|
return dataKey;
|
||||||
|
}
|
||||||
|
|
||||||
|
public DeviceReportRespDTO.Activation buildActivation(String deviceId, DeviceReportReqDTO deviceReport) {
|
||||||
|
DeviceReportRespDTO.Activation code = new DeviceReportRespDTO.Activation();
|
||||||
|
|
||||||
|
String cachedCode = geCodeByDeviceId(deviceId);
|
||||||
|
|
||||||
|
if (StringUtils.isNotBlank(cachedCode)) {
|
||||||
|
code.setCode(cachedCode);
|
||||||
|
String frontedUrl = sysParamsService.getValue(Constant.SERVER_FRONTED_URL, true);
|
||||||
|
code.setMessage(frontedUrl + "\n" + cachedCode);
|
||||||
|
code.setChallenge(deviceId);
|
||||||
|
} else {
|
||||||
|
String newCode = RandomUtil.randomNumbers(6);
|
||||||
|
code.setCode(newCode);
|
||||||
|
String frontedUrl = sysParamsService.getValue(Constant.SERVER_FRONTED_URL, true);
|
||||||
|
code.setMessage(frontedUrl + "\n" + newCode);
|
||||||
|
code.setChallenge(deviceId);
|
||||||
|
|
||||||
|
Map<String, Object> dataMap = new HashMap<>();
|
||||||
|
dataMap.put("id", deviceId);
|
||||||
|
dataMap.put("mac_address", deviceId);
|
||||||
|
|
||||||
|
dataMap.put("board", (deviceReport.getBoard() != null && deviceReport.getBoard().getType() != null)
|
||||||
|
? deviceReport.getBoard().getType()
|
||||||
|
: (deviceReport.getChipModelName() != null ? deviceReport.getChipModelName() : "unknown"));
|
||||||
|
dataMap.put("app_version", (deviceReport.getApplication() != null)
|
||||||
|
? deviceReport.getApplication().getVersion()
|
||||||
|
: null);
|
||||||
|
|
||||||
|
dataMap.put("deviceId", deviceId);
|
||||||
|
dataMap.put("activation_code", newCode);
|
||||||
|
|
||||||
|
// 写入主数据 key
|
||||||
|
String dataKey = getDeviceCacheKey(deviceId);
|
||||||
|
redisTemplate.opsForHash().putAll(dataKey, dataMap);
|
||||||
|
redisTemplate.expire(dataKey, 24, TimeUnit.HOURS);
|
||||||
|
|
||||||
|
// 写入反查激活码 key
|
||||||
|
String codeKey = "ota:activation:code:" + newCode;
|
||||||
|
redisTemplate.opsForValue().set(codeKey, deviceId, 24, TimeUnit.HOURS);
|
||||||
|
}
|
||||||
|
return code;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
+74
@@ -0,0 +1,74 @@
|
|||||||
|
package xiaozhi.modules.device.service.impl;
|
||||||
|
|
||||||
|
import java.util.Arrays;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
|
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
|
||||||
|
import com.baomidou.mybatisplus.core.metadata.IPage;
|
||||||
|
|
||||||
|
import io.micrometer.common.util.StringUtils;
|
||||||
|
import xiaozhi.common.page.PageData;
|
||||||
|
import xiaozhi.common.service.impl.BaseServiceImpl;
|
||||||
|
import xiaozhi.modules.device.dao.OtaDao;
|
||||||
|
import xiaozhi.modules.device.entity.OtaEntity;
|
||||||
|
import xiaozhi.modules.device.service.OtaService;
|
||||||
|
|
||||||
|
@Service
|
||||||
|
public class OtaServiceImpl extends BaseServiceImpl<OtaDao, OtaEntity> implements OtaService {
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public PageData<OtaEntity> page(Map<String, Object> params) {
|
||||||
|
IPage<OtaEntity> page = baseDao.selectPage(
|
||||||
|
getPage(params, "update_date", true),
|
||||||
|
getWrapper(params));
|
||||||
|
|
||||||
|
return new PageData<>(page.getRecords(), page.getTotal());
|
||||||
|
}
|
||||||
|
|
||||||
|
private QueryWrapper<OtaEntity> getWrapper(Map<String, Object> params) {
|
||||||
|
String firmwareName = (String) params.get("firmwareName");
|
||||||
|
|
||||||
|
QueryWrapper<OtaEntity> wrapper = new QueryWrapper<>();
|
||||||
|
wrapper.like(StringUtils.isNotBlank(firmwareName), "firmware_name", firmwareName);
|
||||||
|
|
||||||
|
return wrapper;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void update(OtaEntity entity) {
|
||||||
|
// 检查是否存在相同名称、类型和版本的固件(排除当前记录)
|
||||||
|
QueryWrapper<OtaEntity> queryWrapper = new QueryWrapper<OtaEntity>()
|
||||||
|
.eq("firmware_name", entity.getFirmwareName())
|
||||||
|
.eq("type", entity.getType())
|
||||||
|
.eq("version", entity.getVersion())
|
||||||
|
.ne("id", entity.getId()); // 排除当前记录
|
||||||
|
|
||||||
|
if (baseDao.selectCount(queryWrapper) > 0) {
|
||||||
|
throw new RuntimeException("已存在相同名称、类型和版本的固件,请修改后重试");
|
||||||
|
}
|
||||||
|
|
||||||
|
baseDao.updateById(entity);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void delete(String[] ids) {
|
||||||
|
baseDao.deleteBatchIds(Arrays.asList(ids));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean save(OtaEntity entity) {
|
||||||
|
// 检查是否存在相同名称、类型和版本的固件
|
||||||
|
QueryWrapper<OtaEntity> queryWrapper = new QueryWrapper<OtaEntity>()
|
||||||
|
.eq("firmware_name", entity.getFirmwareName())
|
||||||
|
.eq("type", entity.getType())
|
||||||
|
.eq("version", entity.getVersion());
|
||||||
|
|
||||||
|
if (baseDao.selectCount(queryWrapper) > 0) {
|
||||||
|
throw new RuntimeException("已存在相同名称、类型和版本的固件,请勿重复添加");
|
||||||
|
}
|
||||||
|
|
||||||
|
return baseDao.insert(entity) > 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -135,6 +135,7 @@ public class ModelController {
|
|||||||
}
|
}
|
||||||
// 将其他模型设置为非默认
|
// 将其他模型设置为非默认
|
||||||
modelConfigService.setDefaultModel(entity.getModelType(), 0);
|
modelConfigService.setDefaultModel(entity.getModelType(), 0);
|
||||||
|
entity.setIsEnabled(1);
|
||||||
entity.setIsDefault(1);
|
entity.setIsDefault(1);
|
||||||
modelConfigService.updateById(entity);
|
modelConfigService.updateById(entity);
|
||||||
|
|
||||||
|
|||||||
+1
@@ -42,6 +42,7 @@ public class ModelConfigServiceImpl extends BaseServiceImpl<ModelConfigDao, Mode
|
|||||||
List<ModelConfigEntity> entities = modelConfigDao.selectList(
|
List<ModelConfigEntity> entities = modelConfigDao.selectList(
|
||||||
new QueryWrapper<ModelConfigEntity>()
|
new QueryWrapper<ModelConfigEntity>()
|
||||||
.eq("model_type", modelType)
|
.eq("model_type", modelType)
|
||||||
|
.eq("is_enabled", 1)
|
||||||
.like(StringUtils.isNotBlank(modelName), "model_name", "%" + modelName + "%")
|
.like(StringUtils.isNotBlank(modelName), "model_name", "%" + modelName + "%")
|
||||||
.select("id", "model_name"));
|
.select("id", "model_name"));
|
||||||
return ConvertUtils.sourceToTarget(entities, ModelBasicInfoDTO.class);
|
return ConvertUtils.sourceToTarget(entities, ModelBasicInfoDTO.class);
|
||||||
|
|||||||
@@ -69,6 +69,7 @@ public class ShiroConfig {
|
|||||||
*/
|
*/
|
||||||
Map<String, String> filterMap = new LinkedHashMap<>();
|
Map<String, String> filterMap = new LinkedHashMap<>();
|
||||||
filterMap.put("/ota/**", "anon");
|
filterMap.put("/ota/**", "anon");
|
||||||
|
filterMap.put("/otaMag/download/**", "anon");
|
||||||
filterMap.put("/webjars/**", "anon");
|
filterMap.put("/webjars/**", "anon");
|
||||||
filterMap.put("/druid/**", "anon");
|
filterMap.put("/druid/**", "anon");
|
||||||
filterMap.put("/v3/api-docs/**", "anon");
|
filterMap.put("/v3/api-docs/**", "anon");
|
||||||
@@ -76,6 +77,7 @@ public class ShiroConfig {
|
|||||||
filterMap.put("/favicon.ico", "anon");
|
filterMap.put("/favicon.ico", "anon");
|
||||||
filterMap.put("/user/captcha", "anon");
|
filterMap.put("/user/captcha", "anon");
|
||||||
filterMap.put("/user/login", "anon");
|
filterMap.put("/user/login", "anon");
|
||||||
|
filterMap.put("/user/pub-config", "anon");
|
||||||
filterMap.put("/user/register", "anon");
|
filterMap.put("/user/register", "anon");
|
||||||
filterMap.put("/config/server-base", "anon");
|
filterMap.put("/config/server-base", "anon");
|
||||||
filterMap.put("/config/agent-models", "anon");
|
filterMap.put("/config/agent-models", "anon");
|
||||||
|
|||||||
+15
@@ -1,6 +1,8 @@
|
|||||||
package xiaozhi.modules.security.controller;
|
package xiaozhi.modules.security.controller;
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
import org.springframework.web.bind.annotation.GetMapping;
|
import org.springframework.web.bind.annotation.GetMapping;
|
||||||
import org.springframework.web.bind.annotation.PostMapping;
|
import org.springframework.web.bind.annotation.PostMapping;
|
||||||
@@ -13,6 +15,7 @@ import io.swagger.v3.oas.annotations.Operation;
|
|||||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||||
import jakarta.servlet.http.HttpServletResponse;
|
import jakarta.servlet.http.HttpServletResponse;
|
||||||
import lombok.AllArgsConstructor;
|
import lombok.AllArgsConstructor;
|
||||||
|
import xiaozhi.common.constant.Constant;
|
||||||
import xiaozhi.common.exception.ErrorCode;
|
import xiaozhi.common.exception.ErrorCode;
|
||||||
import xiaozhi.common.exception.RenException;
|
import xiaozhi.common.exception.RenException;
|
||||||
import xiaozhi.common.page.TokenDTO;
|
import xiaozhi.common.page.TokenDTO;
|
||||||
@@ -75,6 +78,9 @@ public class LoginController {
|
|||||||
@PostMapping("/register")
|
@PostMapping("/register")
|
||||||
@Operation(summary = "注册")
|
@Operation(summary = "注册")
|
||||||
public Result<Void> register(@RequestBody LoginDTO login) {
|
public Result<Void> register(@RequestBody LoginDTO login) {
|
||||||
|
if (!sysUserService.getAllowUserRegister()) {
|
||||||
|
throw new RenException("当前不允许普通用户注册");
|
||||||
|
}
|
||||||
// 验证是否正确输入验证码
|
// 验证是否正确输入验证码
|
||||||
boolean validate = captchaService.validate(login.getCaptchaId(), login.getCaptcha());
|
boolean validate = captchaService.validate(login.getCaptchaId(), login.getCaptcha());
|
||||||
if (!validate) {
|
if (!validate) {
|
||||||
@@ -111,4 +117,13 @@ public class LoginController {
|
|||||||
sysUserTokenService.changePassword(userId, passwordDTO);
|
sysUserTokenService.changePassword(userId, passwordDTO);
|
||||||
return new Result<>();
|
return new Result<>();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@GetMapping("/pub-config")
|
||||||
|
@Operation(summary = "公共配置")
|
||||||
|
public Result<Map<String, Object>> pubConfig() {
|
||||||
|
Map<String, Object> config = new HashMap<>();
|
||||||
|
config.put("version", Constant.VERSION);
|
||||||
|
config.put("allowUserRegister", sysUserService.getAllowUserRegister());
|
||||||
|
return new Result<Map<String, Object>>().ok(config);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
+40
-3
@@ -2,6 +2,7 @@ package xiaozhi.modules.sys.controller;
|
|||||||
|
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
|
||||||
|
import org.apache.commons.lang3.StringUtils;
|
||||||
import org.apache.shiro.authz.annotation.RequiresPermissions;
|
import org.apache.shiro.authz.annotation.RequiresPermissions;
|
||||||
import org.springframework.web.bind.annotation.GetMapping;
|
import org.springframework.web.bind.annotation.GetMapping;
|
||||||
import org.springframework.web.bind.annotation.PathVariable;
|
import org.springframework.web.bind.annotation.PathVariable;
|
||||||
@@ -20,6 +21,7 @@ import io.swagger.v3.oas.annotations.tags.Tag;
|
|||||||
import lombok.AllArgsConstructor;
|
import lombok.AllArgsConstructor;
|
||||||
import xiaozhi.common.annotation.LogOperation;
|
import xiaozhi.common.annotation.LogOperation;
|
||||||
import xiaozhi.common.constant.Constant;
|
import xiaozhi.common.constant.Constant;
|
||||||
|
import xiaozhi.common.exception.RenException;
|
||||||
import xiaozhi.common.page.PageData;
|
import xiaozhi.common.page.PageData;
|
||||||
import xiaozhi.common.utils.Result;
|
import xiaozhi.common.utils.Result;
|
||||||
import xiaozhi.common.validator.AssertUtils;
|
import xiaozhi.common.validator.AssertUtils;
|
||||||
@@ -27,8 +29,10 @@ import xiaozhi.common.validator.ValidatorUtils;
|
|||||||
import xiaozhi.common.validator.group.AddGroup;
|
import xiaozhi.common.validator.group.AddGroup;
|
||||||
import xiaozhi.common.validator.group.DefaultGroup;
|
import xiaozhi.common.validator.group.DefaultGroup;
|
||||||
import xiaozhi.common.validator.group.UpdateGroup;
|
import xiaozhi.common.validator.group.UpdateGroup;
|
||||||
|
import xiaozhi.modules.config.service.ConfigService;
|
||||||
import xiaozhi.modules.sys.dto.SysParamsDTO;
|
import xiaozhi.modules.sys.dto.SysParamsDTO;
|
||||||
import xiaozhi.modules.sys.service.SysParamsService;
|
import xiaozhi.modules.sys.service.SysParamsService;
|
||||||
|
import xiaozhi.modules.sys.utils.WebSocketValidator;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 参数管理
|
* 参数管理
|
||||||
@@ -42,6 +46,7 @@ import xiaozhi.modules.sys.service.SysParamsService;
|
|||||||
@AllArgsConstructor
|
@AllArgsConstructor
|
||||||
public class SysParamsController {
|
public class SysParamsController {
|
||||||
private final SysParamsService sysParamsService;
|
private final SysParamsService sysParamsService;
|
||||||
|
private final ConfigService configService;
|
||||||
|
|
||||||
@GetMapping("page")
|
@GetMapping("page")
|
||||||
@Operation(summary = "分页")
|
@Operation(summary = "分页")
|
||||||
@@ -77,7 +82,7 @@ public class SysParamsController {
|
|||||||
ValidatorUtils.validateEntity(dto, AddGroup.class, DefaultGroup.class);
|
ValidatorUtils.validateEntity(dto, AddGroup.class, DefaultGroup.class);
|
||||||
|
|
||||||
sysParamsService.save(dto);
|
sysParamsService.save(dto);
|
||||||
|
configService.getConfig(false);
|
||||||
return new Result<Void>();
|
return new Result<Void>();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -89,11 +94,43 @@ public class SysParamsController {
|
|||||||
// 效验数据
|
// 效验数据
|
||||||
ValidatorUtils.validateEntity(dto, UpdateGroup.class, DefaultGroup.class);
|
ValidatorUtils.validateEntity(dto, UpdateGroup.class, DefaultGroup.class);
|
||||||
|
|
||||||
sysParamsService.update(dto);
|
// 验证WebSocket地址列表
|
||||||
|
validateWebSocketUrls(dto.getParamCode(), dto.getParamValue());
|
||||||
|
|
||||||
|
sysParamsService.update(dto);
|
||||||
|
configService.getConfig(false);
|
||||||
return new Result<Void>();
|
return new Result<Void>();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 验证WebSocket地址列表
|
||||||
|
*
|
||||||
|
* @param urls WebSocket地址列表,以分号分隔
|
||||||
|
* @return 验证结果
|
||||||
|
*/
|
||||||
|
private void validateWebSocketUrls(String paramCode, String urls) {
|
||||||
|
if (!paramCode.equals(Constant.SERVER_WEBSOCKET)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
String[] wsUrls = urls.split("\\;");
|
||||||
|
if (wsUrls.length == 0) {
|
||||||
|
throw new RenException("WebSocket地址列表不能为空");
|
||||||
|
}
|
||||||
|
for (String url : wsUrls) {
|
||||||
|
if (StringUtils.isNotBlank(url)) {
|
||||||
|
// 验证WebSocket地址格式
|
||||||
|
if (!WebSocketValidator.validateUrlFormat(url)) {
|
||||||
|
throw new RenException("WebSocket地址格式不正确: " + url);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 测试WebSocket连接
|
||||||
|
if (!WebSocketValidator.testConnection(url)) {
|
||||||
|
throw new RenException("WebSocket连接测试失败: " + url);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@PostMapping("/delete")
|
@PostMapping("/delete")
|
||||||
@Operation(summary = "删除")
|
@Operation(summary = "删除")
|
||||||
@LogOperation("删除")
|
@LogOperation("删除")
|
||||||
@@ -103,7 +140,7 @@ public class SysParamsController {
|
|||||||
AssertUtils.isArrayEmpty(ids, "id");
|
AssertUtils.isArrayEmpty(ids, "id");
|
||||||
|
|
||||||
sysParamsService.delete(ids);
|
sysParamsService.delete(ids);
|
||||||
|
configService.getConfig(false);
|
||||||
return new Result<Void>();
|
return new Result<Void>();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -65,4 +65,11 @@ public interface SysUserService extends BaseService<SysUserEntity> {
|
|||||||
* @param userIds 用户ID数组
|
* @param userIds 用户ID数组
|
||||||
*/
|
*/
|
||||||
void changeStatus(Integer status, String[] userIds);
|
void changeStatus(Integer status, String[] userIds);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取是否允许用户注册
|
||||||
|
*
|
||||||
|
* @return 是否允许用户注册
|
||||||
|
*/
|
||||||
|
boolean getAllowUserRegister();
|
||||||
}
|
}
|
||||||
|
|||||||
+1
-1
@@ -37,7 +37,7 @@ public class SysParamsServiceImpl extends BaseServiceImpl<SysParamsDao, SysParam
|
|||||||
@Override
|
@Override
|
||||||
public PageData<SysParamsDTO> page(Map<String, Object> params) {
|
public PageData<SysParamsDTO> page(Map<String, Object> params) {
|
||||||
IPage<SysParamsEntity> page = baseDao.selectPage(
|
IPage<SysParamsEntity> page = baseDao.selectPage(
|
||||||
getPage(params, Constant.CREATE_DATE, false),
|
getPage(params, null, false),
|
||||||
getWrapper(params));
|
getWrapper(params));
|
||||||
|
|
||||||
return getPageData(page, SysParamsDTO.class);
|
return getPageData(page, SysParamsDTO.class);
|
||||||
|
|||||||
+16
@@ -30,6 +30,7 @@ import xiaozhi.modules.sys.dto.PasswordDTO;
|
|||||||
import xiaozhi.modules.sys.dto.SysUserDTO;
|
import xiaozhi.modules.sys.dto.SysUserDTO;
|
||||||
import xiaozhi.modules.sys.entity.SysUserEntity;
|
import xiaozhi.modules.sys.entity.SysUserEntity;
|
||||||
import xiaozhi.modules.sys.enums.SuperAdminEnum;
|
import xiaozhi.modules.sys.enums.SuperAdminEnum;
|
||||||
|
import xiaozhi.modules.sys.service.SysParamsService;
|
||||||
import xiaozhi.modules.sys.service.SysUserService;
|
import xiaozhi.modules.sys.service.SysUserService;
|
||||||
import xiaozhi.modules.sys.vo.AdminPageUserVO;
|
import xiaozhi.modules.sys.vo.AdminPageUserVO;
|
||||||
|
|
||||||
@@ -45,6 +46,8 @@ public class SysUserServiceImpl extends BaseServiceImpl<SysUserDao, SysUserEntit
|
|||||||
|
|
||||||
private final AgentService agentService;
|
private final AgentService agentService;
|
||||||
|
|
||||||
|
private final SysParamsService sysParamsService;
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public SysUserDTO getByUsername(String username) {
|
public SysUserDTO getByUsername(String username) {
|
||||||
QueryWrapper<SysUserEntity> queryWrapper = new QueryWrapper<>();
|
QueryWrapper<SysUserEntity> queryWrapper = new QueryWrapper<>();
|
||||||
@@ -205,4 +208,17 @@ public class SysUserServiceImpl extends BaseServiceImpl<SysUserDao, SysUserEntit
|
|||||||
updateById(entity);
|
updateById(entity);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean getAllowUserRegister() {
|
||||||
|
String allowUserRegister = sysParamsService.getValue(Constant.SERVER_ALLOW_USER_REGISTER, true);
|
||||||
|
if (allowUserRegister.equals("true")) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
Long userCount = baseDao.selectCount(new QueryWrapper<SysUserEntity>());
|
||||||
|
if (userCount == 0) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,46 @@
|
|||||||
|
package xiaozhi.modules.sys.utils;
|
||||||
|
|
||||||
|
import java.util.concurrent.CompletableFuture;
|
||||||
|
|
||||||
|
import org.springframework.web.socket.CloseStatus;
|
||||||
|
import org.springframework.web.socket.WebSocketHandler;
|
||||||
|
import org.springframework.web.socket.WebSocketMessage;
|
||||||
|
import org.springframework.web.socket.WebSocketSession;
|
||||||
|
|
||||||
|
public class WebSocketTestHandler implements WebSocketHandler {
|
||||||
|
private final CompletableFuture<Boolean> future;
|
||||||
|
|
||||||
|
public WebSocketTestHandler(CompletableFuture<Boolean> future) {
|
||||||
|
this.future = future;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void afterConnectionEstablished(WebSocketSession session) {
|
||||||
|
future.complete(true);
|
||||||
|
try {
|
||||||
|
session.close();
|
||||||
|
} catch (Exception e) {
|
||||||
|
// 忽略关闭异常
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void handleMessage(WebSocketSession session, WebSocketMessage<?> message) {
|
||||||
|
// 不需要处理消息
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void handleTransportError(WebSocketSession session, Throwable exception) {
|
||||||
|
future.complete(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void afterConnectionClosed(WebSocketSession session, CloseStatus closeStatus) {
|
||||||
|
// 连接关闭时不做任何处理
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean supportsPartialMessages() {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,58 @@
|
|||||||
|
package xiaozhi.modules.sys.utils;
|
||||||
|
|
||||||
|
import java.net.URI;
|
||||||
|
import java.util.concurrent.CompletableFuture;
|
||||||
|
import java.util.concurrent.TimeUnit;
|
||||||
|
import java.util.regex.Pattern;
|
||||||
|
|
||||||
|
import org.apache.commons.lang3.StringUtils;
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
import org.springframework.web.socket.client.WebSocketClient;
|
||||||
|
import org.springframework.web.socket.client.standard.StandardWebSocketClient;
|
||||||
|
|
||||||
|
public class WebSocketValidator {
|
||||||
|
private static final Logger logger = LoggerFactory.getLogger(WebSocketValidator.class);
|
||||||
|
|
||||||
|
// WebSocket URL正则表达式
|
||||||
|
private static final Pattern WS_URL_PATTERN = Pattern
|
||||||
|
.compile("^wss?://[\\w.-]+(?:\\.[\\w.-]+)*(?::\\d+)?(?:/[\\w.-]*)*$");
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 验证WebSocket地址格式
|
||||||
|
*
|
||||||
|
* @param url WebSocket地址
|
||||||
|
* @return 是否有效
|
||||||
|
*/
|
||||||
|
public static boolean validateUrlFormat(String url) {
|
||||||
|
if (StringUtils.isBlank(url)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return WS_URL_PATTERN.matcher(url.trim()).matches();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 测试WebSocket连接
|
||||||
|
*
|
||||||
|
* @param url WebSocket地址
|
||||||
|
* @return 是否可连接
|
||||||
|
*/
|
||||||
|
public static boolean testConnection(String url) {
|
||||||
|
if (!validateUrlFormat(url)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
WebSocketClient client = new StandardWebSocketClient();
|
||||||
|
CompletableFuture<Boolean> future = new CompletableFuture<>();
|
||||||
|
|
||||||
|
client.doHandshake(new WebSocketTestHandler(future), null, URI.create(url));
|
||||||
|
|
||||||
|
// 等待最多5秒获取连接结果
|
||||||
|
return future.get(5, TimeUnit.SECONDS);
|
||||||
|
} catch (Exception e) {
|
||||||
|
logger.error("WebSocket连接测试失败: {}", url, e);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -24,7 +24,6 @@ public class TimbreDataDTO {
|
|||||||
private String name;
|
private String name;
|
||||||
|
|
||||||
@Schema(description = "备注")
|
@Schema(description = "备注")
|
||||||
@NotBlank(message = "{timbre.remark.require}")
|
|
||||||
private String remark;
|
private String remark;
|
||||||
|
|
||||||
@Schema(description = "排序")
|
@Schema(description = "排序")
|
||||||
@@ -40,6 +39,5 @@ public class TimbreDataDTO {
|
|||||||
private String ttsVoice;
|
private String ttsVoice;
|
||||||
|
|
||||||
@Schema(description = "音频播放地址")
|
@Schema(description = "音频播放地址")
|
||||||
@NotBlank(message = "{timbre.voiceDemo.require}")
|
|
||||||
private String voiceDemo;
|
private String voiceDemo;
|
||||||
}
|
}
|
||||||
+1
-1
@@ -47,7 +47,7 @@ public class TimbreServiceImpl extends BaseServiceImpl<TimbreDao, TimbreEntity>
|
|||||||
params.put(Constant.PAGE, dto.getPage());
|
params.put(Constant.PAGE, dto.getPage());
|
||||||
params.put(Constant.LIMIT, dto.getLimit());
|
params.put(Constant.LIMIT, dto.getLimit());
|
||||||
IPage<TimbreEntity> page = baseDao.selectPage(
|
IPage<TimbreEntity> page = baseDao.selectPage(
|
||||||
getPage(params, "sort", true),
|
getPage(params, null, true),
|
||||||
// 定义查询条件
|
// 定义查询条件
|
||||||
new QueryWrapper<TimbreEntity>()
|
new QueryWrapper<TimbreEntity>()
|
||||||
// 必须按照ttsID查找
|
// 必须按照ttsID查找
|
||||||
|
|||||||
@@ -63,7 +63,4 @@ mybatis-plus:
|
|||||||
configuration-properties:
|
configuration-properties:
|
||||||
prefix:
|
prefix:
|
||||||
blobType: BLOB
|
blobType: BLOB
|
||||||
boolValue: TRUE
|
boolValue: TRUE
|
||||||
|
|
||||||
app:
|
|
||||||
fronted-url: http://localhost:8001
|
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
-- 对0.3.0版本之前的参数进行修改
|
||||||
|
update `sys_params` set param_value = '.mp3;.wav;.p3' where param_code = 'plugins.play_music.music_ext';
|
||||||
|
update `ai_model_config` set config_json = '{\"type\": \"intent_llm\", \"llm\": \"LLM_ChatGLMLLM\"}' where id = 'Intent_intent_llm';
|
||||||
|
|
||||||
|
-- 添加edge音色
|
||||||
|
delete from `ai_tts_voice` where tts_model_id = 'TTS_EdgeTTS';
|
||||||
|
INSERT INTO `ai_tts_voice` VALUES
|
||||||
|
('TTS_EdgeTTS0001', 'TTS_EdgeTTS', 'EdgeTTS女声-晓晓', 'zh-CN-XiaoxiaoNeural', '普通话', NULL, NULL, 1, NULL, NULL, NULL, NULL),
|
||||||
|
('TTS_EdgeTTS0002', 'TTS_EdgeTTS', 'EdgeTTS男声-云扬', 'zh-CN-YunyangNeural', '普通话', NULL, NULL, 1, NULL, NULL, NULL, NULL),
|
||||||
|
('TTS_EdgeTTS0003', 'TTS_EdgeTTS', 'EdgeTTS女声-晓伊', 'zh-CN-XiaoyiNeural', '普通话', NULL, NULL, 1, NULL, NULL, NULL, NULL),
|
||||||
|
('TTS_EdgeTTS0004', 'TTS_EdgeTTS', 'EdgeTTS男声-云健', 'zh-CN-YunjianNeural', '普通话', NULL, NULL, 1, NULL, NULL, NULL, NULL),
|
||||||
|
('TTS_EdgeTTS0005', 'TTS_EdgeTTS', 'EdgeTTS男声-云希', 'zh-CN-YunxiNeural', '普通话', NULL, NULL, 1, NULL, NULL, NULL, NULL),
|
||||||
|
('TTS_EdgeTTS0006', 'TTS_EdgeTTS', 'EdgeTTS男声-云夏', 'zh-CN-YunxiaNeural', '普通话', NULL, NULL, 1, NULL, NULL, NULL, NULL),
|
||||||
|
('TTS_EdgeTTS0007', 'TTS_EdgeTTS', 'EdgeTTS女声-辽宁小贝', 'zh-CN-liaoning-XiaobeiNeural', '辽宁', NULL, NULL, 1, NULL, NULL, NULL, NULL),
|
||||||
|
('TTS_EdgeTTS0008', 'TTS_EdgeTTS', 'EdgeTTS女声-陕西小妮', 'zh-CN-shaanxi-XiaoniNeural', '陕西', NULL, NULL, 1, NULL, NULL, NULL, NULL),
|
||||||
|
('TTS_EdgeTTS0009', 'TTS_EdgeTTS', 'EdgeTTS女声-香港海佳', 'zh-HK-HiuGaaiNeural', '粤语', 'General', 'Friendly, Positive', 1, NULL, NULL, NULL, NULL),
|
||||||
|
('TTS_EdgeTTS0010', 'TTS_EdgeTTS', 'EdgeTTS女声-香港海曼', 'zh-HK-HiuMaanNeural', '粤语', 'General', 'Friendly, Positive', 1, NULL, NULL, NULL, NULL),
|
||||||
|
('TTS_EdgeTTS0011', 'TTS_EdgeTTS', 'EdgeTTS男声-香港万龙', 'zh-HK-WanLungNeural', '粤语', 'General', 'Friendly, Positive', 1, NULL, NULL, NULL, NULL);
|
||||||
|
|
||||||
|
-- 增加是否允许用户注册参数
|
||||||
|
delete from `sys_params` where id in (103,104);
|
||||||
|
INSERT INTO `sys_params` (id, param_code, param_value, value_type, param_type, remark) VALUES (103, 'server.allow_user_register', 'false', 'boolean', 1, '是否运行管理员以外的人注册');
|
||||||
|
INSERT INTO `sys_params` (id, param_code, param_value, value_type, param_type, remark) VALUES (104, 'server.fronted_url', 'http://xiaozhi.server.com', 'string', 1, '下发六位验证码时显示的控制面板地址');
|
||||||
|
|
||||||
|
-- 修正CosyVoiceSiliconflow音色
|
||||||
|
delete from `ai_tts_voice` where tts_model_id = 'TTS_CosyVoiceSiliconflow';
|
||||||
|
INSERT INTO `ai_tts_voice` VALUES ('TTS_CosyVoiceSiliconflow0001', 'TTS_CosyVoiceSiliconflow', 'CosyVoice男声', 'FunAudioLLM/CosyVoice2-0.5B:alex', '中文', 'https://example.com/cosyvoice/alex.mp3', NULL, 6, NULL, NULL, NULL, NULL);
|
||||||
|
INSERT INTO `ai_tts_voice` VALUES ('TTS_CosyVoiceSiliconflow0002', 'TTS_CosyVoiceSiliconflow', 'CosyVoice女声', 'FunAudioLLM/CosyVoice2-0.5B:bella', '中文', 'https://example.com/cosyvoice/bella.mp3', NULL, 6, NULL, NULL, NULL, NULL);
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
-- 本文件用于初始化固件信息表,无需手动执行,在项目启动时会自动执行
|
||||||
|
-- -------------------------------------------------------
|
||||||
|
-- 初始化固件信息表
|
||||||
|
drop table if exists `ai_ota`;
|
||||||
|
CREATE TABLE `ai_ota` (
|
||||||
|
`id` varchar(32) NOT NULL COMMENT 'ID',
|
||||||
|
`firmware_name` varchar(100) DEFAULT NULL COMMENT '固件名称',
|
||||||
|
`type` varchar(50) DEFAULT NULL COMMENT '固件类型',
|
||||||
|
`version` varchar(50) DEFAULT NULL COMMENT '版本号',
|
||||||
|
`size` bigint DEFAULT NULL COMMENT '文件大小(字节)',
|
||||||
|
`remark` varchar(500) DEFAULT NULL COMMENT '备注/说明',
|
||||||
|
`firmware_path` varchar(255) DEFAULT NULL COMMENT '固件路径',
|
||||||
|
`sort` int unsigned DEFAULT '0' COMMENT '排序',
|
||||||
|
`updater` bigint DEFAULT NULL COMMENT '更新者',
|
||||||
|
`update_date` datetime DEFAULT NULL COMMENT '更新时间',
|
||||||
|
`creator` bigint DEFAULT NULL COMMENT '创建者',
|
||||||
|
`create_date` datetime DEFAULT NULL COMMENT '创建时间',
|
||||||
|
PRIMARY KEY (`id`)
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='固件信息表';
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
-- 调整意图识别配置
|
||||||
|
delete from `ai_model_config` where id = 'Intent_function_call';
|
||||||
|
INSERT INTO `ai_model_config` VALUES ('Intent_function_call', 'Intent', 'function_call', '函数调用意图识别', 0, 1, '{\"type\": \"function_call\", \"functions\": \"change_role;get_weather;get_news;play_music\"}', NULL, NULL, 3, NULL, NULL, NULL, NULL);
|
||||||
|
|
||||||
|
-- 增加单台设备每天最多聊天句数
|
||||||
|
delete from `sys_params` where id = 105;
|
||||||
|
INSERT INTO `sys_params` (id, param_code, param_value, value_type, param_type, remark) VALUES (105, 'device_max_output_size', '0', 'number', 1, '单台设备每天最多输出字数,0表示不限制');
|
||||||
@@ -0,0 +1,88 @@
|
|||||||
|
-- 删除无用模型供应器
|
||||||
|
delete from `ai_model_provider` where id = 'SYSTEM_LLM_doubao';
|
||||||
|
delete from `ai_model_provider` where id = 'SYSTEM_LLM_chatglm';
|
||||||
|
delete from `ai_model_provider` where id = 'SYSTEM_TTS_302ai';
|
||||||
|
delete from `ai_model_provider` where id = 'SYSTEM_TTS_gizwits';
|
||||||
|
|
||||||
|
-- 添加模型供应器
|
||||||
|
delete from `ai_model_provider` where id = 'SYSTEM_ASR_TencentASR';
|
||||||
|
INSERT INTO `ai_model_provider` (`id`, `model_type`, `provider_code`, `name`, `fields`, `sort`, `creator`, `create_date`, `updater`, `update_date`) VALUES
|
||||||
|
('SYSTEM_ASR_TencentASR', 'ASR', 'tencent', '腾讯语音识别', '[{"key":"appid","label":"应用ID","type":"string"},{"key":"secret_id","label":"Secret ID","type":"string"},{"key":"secret_key","label":"Secret Key","type":"string"},{"key":"output_dir","label":"输出目录","type":"string"}]', 4, 1, NOW(), 1, NOW());
|
||||||
|
|
||||||
|
-- 添加腾讯语音合成模型供应器
|
||||||
|
delete from `ai_model_provider` where id = 'SYSTEM_TTS_TencentTTS';
|
||||||
|
INSERT INTO `ai_model_provider` (`id`, `model_type`, `provider_code`, `name`, `fields`, `sort`, `creator`, `create_date`, `updater`, `update_date`) VALUES
|
||||||
|
('SYSTEM_TTS_TencentTTS', 'TTS', 'tencent', '腾讯语音合成', '[{"key":"appid","label":"应用ID","type":"string"},{"key":"secret_id","label":"Secret ID","type":"string"},{"key":"secret_key","label":"Secret Key","type":"string"},{"key":"output_dir","label":"输出目录","type":"string"},{"key":"region","label":"区域","type":"string"},{"key":"voice","label":"音色ID","type":"string"}]', 5, 1, NOW(), 1, NOW());
|
||||||
|
|
||||||
|
|
||||||
|
-- 添加edge音色
|
||||||
|
delete from `ai_tts_voice` where id in ('TTS_EdgeTTS0001', 'TTS_EdgeTTS0002', 'TTS_EdgeTTS0003', 'TTS_EdgeTTS0004', 'TTS_EdgeTTS0005', 'TTS_EdgeTTS0006', 'TTS_EdgeTTS0007', 'TTS_EdgeTTS0008', 'TTS_EdgeTTS0009', 'TTS_EdgeTTS0010', 'TTS_EdgeTTS0011');
|
||||||
|
INSERT INTO `ai_tts_voice` VALUES
|
||||||
|
('TTS_EdgeTTS0001', 'TTS_EdgeTTS', 'EdgeTTS女声-晓晓', 'zh-CN-XiaoxiaoNeural', '普通话', NULL, NULL, 1, NULL, NULL, NULL, NULL),
|
||||||
|
('TTS_EdgeTTS0002', 'TTS_EdgeTTS', 'EdgeTTS男声-云扬', 'zh-CN-YunyangNeural', '普通话', NULL, NULL, 1, NULL, NULL, NULL, NULL),
|
||||||
|
('TTS_EdgeTTS0003', 'TTS_EdgeTTS', 'EdgeTTS女声-晓伊', 'zh-CN-XiaoyiNeural', '普通话', NULL, NULL, 1, NULL, NULL, NULL, NULL),
|
||||||
|
('TTS_EdgeTTS0004', 'TTS_EdgeTTS', 'EdgeTTS男声-云健', 'zh-CN-YunjianNeural', '普通话', NULL, NULL, 1, NULL, NULL, NULL, NULL),
|
||||||
|
('TTS_EdgeTTS0005', 'TTS_EdgeTTS', 'EdgeTTS男声-云希', 'zh-CN-YunxiNeural', '普通话', NULL, NULL, 1, NULL, NULL, NULL, NULL),
|
||||||
|
('TTS_EdgeTTS0006', 'TTS_EdgeTTS', 'EdgeTTS男声-云夏', 'zh-CN-YunxiaNeural', '普通话', NULL, NULL, 1, NULL, NULL, NULL, NULL),
|
||||||
|
('TTS_EdgeTTS0007', 'TTS_EdgeTTS', 'EdgeTTS女声-辽宁小贝', 'zh-CN-liaoning-XiaobeiNeural', '辽宁', NULL, NULL, 1, NULL, NULL, NULL, NULL),
|
||||||
|
('TTS_EdgeTTS0008', 'TTS_EdgeTTS', 'EdgeTTS女声-陕西小妮', 'zh-CN-shaanxi-XiaoniNeural', '陕西', NULL, NULL, 1, NULL, NULL, NULL, NULL),
|
||||||
|
('TTS_EdgeTTS0009', 'TTS_EdgeTTS', 'EdgeTTS女声-香港海佳', 'zh-HK-HiuGaaiNeural', '粤语', 'General', 'Friendly, Positive', 1, NULL, NULL, NULL, NULL),
|
||||||
|
('TTS_EdgeTTS0010', 'TTS_EdgeTTS', 'EdgeTTS女声-香港海曼', 'zh-HK-HiuMaanNeural', '粤语', 'General', 'Friendly, Positive', 1, NULL, NULL, NULL, NULL),
|
||||||
|
('TTS_EdgeTTS0011', 'TTS_EdgeTTS', 'EdgeTTS男声-香港万龙', 'zh-HK-WanLungNeural', '粤语', 'General', 'Friendly, Positive', 1, NULL, NULL, NULL, NULL);
|
||||||
|
|
||||||
|
-- DoubaoTTS音色
|
||||||
|
delete from `ai_tts_voice` where id in ('TTS_DoubaoTTS0001', 'TTS_DoubaoTTS0002', 'TTS_DoubaoTTS0003', 'TTS_DoubaoTTS0004', 'TTS_DoubaoTTS0005');
|
||||||
|
INSERT INTO `ai_tts_voice` VALUES ('TTS_DoubaoTTS0001', 'TTS_DoubaoTTS', '通用女声', 'BV001_streaming', '普通话', 'https://lf3-speech.bytetos.com/obj/speech-tts-external/portal/Portal_Demo_BV001.mp3', NULL, 3, NULL, NULL, NULL, NULL);
|
||||||
|
INSERT INTO `ai_tts_voice` VALUES ('TTS_DoubaoTTS0002', 'TTS_DoubaoTTS', '通用男声', 'BV002_streaming', '普通话', 'https://lf3-speech.bytetos.com/obj/speech-tts-external/portal/Portal_Demo_BV002.mp3', NULL, 2, NULL, NULL, NULL, NULL);
|
||||||
|
INSERT INTO `ai_tts_voice` VALUES ('TTS_DoubaoTTS0003', 'TTS_DoubaoTTS', '阳光男生', 'BV056_streaming', '普通话', 'https://lf3-speech.bytetos.com/obj/speech-tts-external/portal/Portal_Demo_BV056.mp3', NULL, 4, NULL, NULL, NULL, NULL);
|
||||||
|
INSERT INTO `ai_tts_voice` VALUES ('TTS_DoubaoTTS0004', 'TTS_DoubaoTTS', '奶气萌娃', 'BV051_streaming', '普通话', 'https://lf3-speech.bytetos.com/obj/speech-tts-external/portal/Portal_Demo_BV051.mp3', NULL, 5, NULL, NULL, NULL, NULL);
|
||||||
|
INSERT INTO `ai_tts_voice` VALUES ('TTS_DoubaoTTS0005', 'TTS_DoubaoTTS', '湾湾小何', 'zh_female_wanwanxiaohe_moon_bigtts', '普通话', 'https://lf3-static.bytednsdoc.com/obj/eden-cn/lm_hz_ihsph/ljhwZthlaukjlkulzlp/portal/bigtts/%E6%B9%BE%E6%B9%BE%E5%B0%8F%E4%BD%95.mp3', NULL, 6, NULL, NULL, NULL, NULL);
|
||||||
|
|
||||||
|
-- 修正CosyVoiceSiliconflow音色
|
||||||
|
delete from `ai_tts_voice` where id in ('TTS_CosyVoiceSiliconflow0001', 'TTS_CosyVoiceSiliconflow0002');
|
||||||
|
INSERT INTO `ai_tts_voice` VALUES ('TTS_CosyVoiceSiliconflow0001', 'TTS_CosyVoiceSiliconflow', 'CosyVoice男声', 'FunAudioLLM/CosyVoice2-0.5B:alex', '中文', NULL, NULL, 6, NULL, NULL, NULL, NULL);
|
||||||
|
INSERT INTO `ai_tts_voice` VALUES ('TTS_CosyVoiceSiliconflow0002', 'TTS_CosyVoiceSiliconflow', 'CosyVoice女声', 'FunAudioLLM/CosyVoice2-0.5B:bella', '中文', NULL, NULL, 6, NULL, NULL, NULL, NULL);
|
||||||
|
|
||||||
|
-- CozeCnTTS音色
|
||||||
|
delete from `ai_tts_voice` where id = 'TTS_CozeCnTTS0001';
|
||||||
|
INSERT INTO `ai_tts_voice` VALUES ('TTS_CozeCnTTS0001', 'TTS_CozeCnTTS', 'CozeCn音色', '7426720361733046281', '中文', NULL, NULL, 7, NULL, NULL, NULL, NULL);
|
||||||
|
|
||||||
|
-- MinimaxTTS音色
|
||||||
|
delete from `ai_tts_voice` where id = 'TTS_MinimaxTTS0001';
|
||||||
|
INSERT INTO `ai_tts_voice` VALUES ('TTS_MinimaxTTS0001', 'TTS_MinimaxTTS', 'Minimax少女音', 'female-shaonv', '中文', NULL, NULL, 8, NULL, NULL, NULL, NULL);
|
||||||
|
|
||||||
|
-- AliyunTTS音色
|
||||||
|
delete from `ai_tts_voice` where id = 'TTS_AliyunTTS0001';
|
||||||
|
INSERT INTO `ai_tts_voice` VALUES ('TTS_AliyunTTS0001', 'TTS_AliyunTTS', '阿里云小云', 'xiaoyun', '中文', NULL, NULL, 9, NULL, NULL, NULL, NULL);
|
||||||
|
|
||||||
|
-- TTS302AI音色
|
||||||
|
delete from `ai_tts_voice` where id = 'TTS_TTS302AI0001';
|
||||||
|
INSERT INTO `ai_tts_voice` VALUES ('TTS_TTS302AI0001', 'TTS_TTS302AI', '湾湾小何', 'zh_female_wanwanxiaohe_moon_bigtts', '中文', 'https://lf3-static.bytednsdoc.com/obj/eden-cn/lm_hz_ihsph/ljhwZthlaukjlkulzlp/portal/bigtts/%E6%B9%BE%E6%B9%BE%E5%B0%8F%E4%BD%95.mp3', NULL, 10, NULL, NULL, NULL, NULL);
|
||||||
|
|
||||||
|
-- GizwitsTTS音色
|
||||||
|
delete from `ai_tts_voice` where id = 'TTS_GizwitsTTS0001';
|
||||||
|
INSERT INTO `ai_tts_voice` VALUES ('TTS_GizwitsTTS0001', 'TTS_GizwitsTTS', '机智云湾湾', 'zh_female_wanwanxiaohe_moon_bigtts', '中文', 'https://lf3-static.bytednsdoc.com/obj/eden-cn/lm_hz_ihsph/ljhwZthlaukjlkulzlp/portal/bigtts/%E6%B9%BE%E6%B9%BE%E5%B0%8F%E4%BD%95.mp3', NULL, 11, NULL, NULL, NULL, NULL);
|
||||||
|
|
||||||
|
-- ACGNTTS音色
|
||||||
|
delete from `ai_tts_voice` where id = 'TTS_ACGNTTS0001';
|
||||||
|
INSERT INTO `ai_tts_voice` VALUES ('TTS_ACGNTTS0001', 'TTS_ACGNTTS', 'ACG音色', '1695', '中文', NULL, NULL, 12, NULL, NULL, NULL, NULL);
|
||||||
|
|
||||||
|
-- OpenAITTS音色
|
||||||
|
delete from `ai_tts_voice` where id = 'TTS_OpenAITTS0001';
|
||||||
|
INSERT INTO `ai_tts_voice` VALUES ('TTS_OpenAITTS0001', 'TTS_OpenAITTS', 'OpenAI男声', 'onyx', '中文', NULL, NULL, 13, NULL, NULL, NULL, NULL);
|
||||||
|
|
||||||
|
-- 添加腾讯语音合成音色
|
||||||
|
delete from `ai_tts_voice` where id = 'TTS_TencentTTS0001';
|
||||||
|
INSERT INTO `ai_tts_voice` VALUES ('TTS_TencentTTS0001', 'TTS_TencentTTS', '智瑜', '101001', '中文', NULL, NULL, 1, NULL, NULL, NULL, NULL);
|
||||||
|
|
||||||
|
-- 其他音色
|
||||||
|
delete from `ai_tts_voice` where id = 'TTS_FishSpeech0000';
|
||||||
|
INSERT INTO `ai_tts_voice` VALUES ('TTS_FishSpeech0000', 'TTS_FishSpeech', '', '', '中文', '', NULL, 8, NULL, NULL, NULL, NULL);
|
||||||
|
|
||||||
|
delete from `ai_tts_voice` where id = 'TTS_GPT_SOVITS_V20000';
|
||||||
|
INSERT INTO `ai_tts_voice` VALUES ('TTS_GPT_SOVITS_V20000', 'TTS_GPT_SOVITS_V2', '', '', '中文', '', NULL, 8, NULL, NULL, NULL, NULL);
|
||||||
|
|
||||||
|
delete from `ai_tts_voice` where id in ('TTS_GPT_SOVITS_V30000', 'TTS_CustomTTS0000');
|
||||||
|
INSERT INTO `ai_tts_voice` VALUES ('TTS_GPT_SOVITS_V30000', 'TTS_GPT_SOVITS_V3', '', '', '中文', '', NULL, 8, NULL, NULL, NULL, NULL);
|
||||||
|
INSERT INTO `ai_tts_voice` VALUES ('TTS_CustomTTS0000', 'TTS_CustomTTS', '', '', '中文', '', NULL, 8, NULL, NULL, NULL, NULL);
|
||||||
|
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
-- 删除server.ip和server.port,改成在xiaozhi-server中配置,方便运行多个websocket
|
||||||
|
delete from `sys_params` where id = 100;
|
||||||
|
delete from `sys_params` where id = 101;
|
||||||
|
|
||||||
|
INSERT INTO `sys_params` (id, param_code, param_value, value_type, param_type, remark) VALUES (106, 'server.websocket', 'null', 'string', 1, 'websocket地址,多个用;分隔');
|
||||||
|
|
||||||
@@ -1,44 +0,0 @@
|
|||||||
-- 本文件用于初始化音色数据,无需手动执行,在项目启动时会自动执行
|
|
||||||
-- ----------------------------------------------------
|
|
||||||
|
|
||||||
-- 初始化音色数据
|
|
||||||
DELETE FROM `ai_tts_voice`;
|
|
||||||
|
|
||||||
-- EdgeTTS音色
|
|
||||||
INSERT INTO `ai_tts_voice` VALUES ('TTS_EdgeTTS0001', 'TTS_EdgeTTS', 'EdgeTTS女声', 'zh-CN-XiaoxiaoNeural', '中文', 'https://lf3-static.bytednsdoc.com/obj/eden-cn/lm_hz_ihsph/ljhwZthlaukjlkulzlp/portal/bigtts/EdgeTTS.mp3', NULL, 1, NULL, NULL, NULL, NULL);
|
|
||||||
|
|
||||||
-- DoubaoTTS音色
|
|
||||||
INSERT INTO `ai_tts_voice` VALUES ('TTS_DoubaoTTS0001', 'TTS_DoubaoTTS', '通用女声', 'BV001_streaming', '中文', 'https://lf3-speech.bytetos.com/obj/speech-tts-external/portal/Portal_Demo_BV001.mp3', NULL, 3, NULL, NULL, NULL, NULL);
|
|
||||||
INSERT INTO `ai_tts_voice` VALUES ('TTS_DoubaoTTS0002', 'TTS_DoubaoTTS', '通用男声', 'BV002_streaming', '中文', 'https://lf3-speech.bytetos.com/obj/speech-tts-external/portal/Portal_Demo_BV002.mp3', NULL, 2, NULL, NULL, NULL, NULL);
|
|
||||||
INSERT INTO `ai_tts_voice` VALUES ('TTS_DoubaoTTS0003', 'TTS_DoubaoTTS', '阳光男生', 'BV056_streaming', '中文', 'https://lf3-speech.bytetos.com/obj/speech-tts-external/portal/Portal_Demo_BV056.mp3', NULL, 4, NULL, NULL, NULL, NULL);
|
|
||||||
INSERT INTO `ai_tts_voice` VALUES ('TTS_DoubaoTTS0004', 'TTS_DoubaoTTS', '奶气萌娃', 'BV051_streaming', '中文', 'https://lf3-speech.bytetos.com/obj/speech-tts-external/portal/Portal_Demo_BV051.mp3', NULL, 5, NULL, NULL, NULL, NULL);
|
|
||||||
|
|
||||||
-- CosyVoiceSiliconflow音色
|
|
||||||
INSERT INTO `ai_tts_voice` VALUES ('TTS_CosyVoiceSiliconflow0001', 'TTS_CosyVoiceSiliconflow', 'CosyVoice男声', 'alex', '中文', 'https://example.com/cosyvoice/alex.mp3', NULL, 6, NULL, NULL, NULL, NULL);
|
|
||||||
|
|
||||||
-- CozeCnTTS音色
|
|
||||||
INSERT INTO `ai_tts_voice` VALUES ('TTS_CozeCnTTS0001', 'TTS_CozeCnTTS', 'CozeCn音色', '7426720361733046281', '中文', 'https://example.com/cozecn/voice.mp3', NULL, 7, NULL, NULL, NULL, NULL);
|
|
||||||
|
|
||||||
-- MinimaxTTS音色
|
|
||||||
INSERT INTO `ai_tts_voice` VALUES ('TTS_MinimaxTTS0001', 'TTS_MinimaxTTS', 'Minimax少女音', 'female-shaonv', '中文', 'https://example.com/minimax/female-shaonv.mp3', NULL, 8, NULL, NULL, NULL, NULL);
|
|
||||||
|
|
||||||
-- AliyunTTS音色
|
|
||||||
INSERT INTO `ai_tts_voice` VALUES ('TTS_AliyunTTS0001', 'TTS_AliyunTTS', '阿里云小云', 'xiaoyun', '中文', 'https://example.com/aliyun/xiaoyun.mp3', NULL, 9, NULL, NULL, NULL, NULL);
|
|
||||||
|
|
||||||
-- TTS302AI音色
|
|
||||||
INSERT INTO `ai_tts_voice` VALUES ('TTS_TTS302AI0001', 'TTS_TTS302AI', '湾湾小何', 'zh_female_wanwanxiaohe_moon_bigtts', '中文', 'https://example.com/302ai/wanwanxiaohe.mp3', NULL, 10, NULL, NULL, NULL, NULL);
|
|
||||||
|
|
||||||
-- GizwitsTTS音色
|
|
||||||
INSERT INTO `ai_tts_voice` VALUES ('TTS_GizwitsTTS0001', 'TTS_GizwitsTTS', '机智云湾湾', 'zh_female_wanwanxiaohe_moon_bigtts', '中文', 'https://example.com/gizwits/wanwanxiaohe.mp3', NULL, 11, NULL, NULL, NULL, NULL);
|
|
||||||
|
|
||||||
-- ACGNTTS音色
|
|
||||||
INSERT INTO `ai_tts_voice` VALUES ('TTS_ACGNTTS0001', 'TTS_ACGNTTS', 'ACG音色', '1695', '中文', 'https://example.com/acgn/voice.mp3', NULL, 12, NULL, NULL, NULL, NULL);
|
|
||||||
|
|
||||||
-- OpenAITTS音色
|
|
||||||
INSERT INTO `ai_tts_voice` VALUES ('TTS_OpenAITTS0001', 'TTS_OpenAITTS', 'OpenAI男声', 'onyx', '中文', 'https://example.com/openai/onyx.mp3', NULL, 13, NULL, NULL, NULL, NULL);
|
|
||||||
|
|
||||||
-- 其他音色
|
|
||||||
INSERT INTO `ai_tts_voice` VALUES ('TTS_FishSpeech0000', 'TTS_FishSpeech', '', '', '中文', '', NULL, 8, NULL, NULL, NULL, NULL);
|
|
||||||
INSERT INTO `ai_tts_voice` VALUES ('TTS_GPT_SOVITS_V20000', 'TTS_GPT_SOVITS_V2', '', '', '中文', '', NULL, 8, NULL, NULL, NULL, NULL);
|
|
||||||
INSERT INTO `ai_tts_voice` VALUES ('TTS_GPT_SOVITS_V30000', 'TTS_GPT_SOVITS_V3', '', '', '中文', '', NULL, 8, NULL, NULL, NULL, NULL);
|
|
||||||
INSERT INTO `ai_tts_voice` VALUES ('TTS_CustomTTS0000', 'TTS_CustomTTS', '', '', '中文', '', NULL, 8, NULL, NULL, NULL, NULL);
|
|
||||||
@@ -16,13 +16,6 @@ databaseChangeLog:
|
|||||||
- sqlFile:
|
- sqlFile:
|
||||||
encoding: utf8
|
encoding: utf8
|
||||||
path: classpath:db/changelog/202503141346.sql
|
path: classpath:db/changelog/202503141346.sql
|
||||||
- changeSet:
|
|
||||||
id: 2025_tts_voive
|
|
||||||
author: John
|
|
||||||
changes:
|
|
||||||
- sqlFile:
|
|
||||||
encoding: utf8
|
|
||||||
path: classpath:db/changelog/2025_tts_voive.sql
|
|
||||||
- changeSet:
|
- changeSet:
|
||||||
id: 202504082211
|
id: 202504082211
|
||||||
author: John
|
author: John
|
||||||
@@ -50,4 +43,39 @@ databaseChangeLog:
|
|||||||
changes:
|
changes:
|
||||||
- sqlFile:
|
- sqlFile:
|
||||||
encoding: utf8
|
encoding: utf8
|
||||||
path: classpath:db/changelog/202504112058.sql
|
path: classpath:db/changelog/202504112058.sql
|
||||||
|
- changeSet:
|
||||||
|
id: 202504151206
|
||||||
|
author: John
|
||||||
|
changes:
|
||||||
|
- sqlFile:
|
||||||
|
encoding: utf8
|
||||||
|
path: classpath:db/changelog/202504151206.sql
|
||||||
|
- changeSet:
|
||||||
|
id: 202504181536
|
||||||
|
author: John
|
||||||
|
changes:
|
||||||
|
- sqlFile:
|
||||||
|
encoding: utf8
|
||||||
|
path: classpath:db/changelog/202504181536.sql
|
||||||
|
- changeSet:
|
||||||
|
id: 202504180758
|
||||||
|
author: jiangkunyin
|
||||||
|
changes:
|
||||||
|
- sqlFile:
|
||||||
|
encoding: utf8
|
||||||
|
path: classpath:db/changelog/202504180758.sql
|
||||||
|
- changeSet:
|
||||||
|
id: 202504221135
|
||||||
|
author: John
|
||||||
|
changes:
|
||||||
|
- sqlFile:
|
||||||
|
encoding: utf8
|
||||||
|
path: classpath:db/changelog/202504221135.sql
|
||||||
|
- changeSet:
|
||||||
|
id: 202504221646
|
||||||
|
author: John
|
||||||
|
changes:
|
||||||
|
- sqlFile:
|
||||||
|
encoding: utf8
|
||||||
|
path: classpath:db/changelog/202504221646.sql
|
||||||
@@ -41,3 +41,6 @@
|
|||||||
10038=\u53C2\u6570\u503C\u5FC5\u987B\u662Ftrue\u6216false
|
10038=\u53C2\u6570\u503C\u5FC5\u987B\u662Ftrue\u6216false
|
||||||
10039=\u53C2\u6570\u503C\u5FC5\u987B\u662F\u6709\u6548\u7684JSON\u6570\u7EC4\u683C\u5F0F
|
10039=\u53C2\u6570\u503C\u5FC5\u987B\u662F\u6709\u6548\u7684JSON\u6570\u7EC4\u683C\u5F0F
|
||||||
10040=\u53C2\u6570\u503C\u5FC5\u987B\u662F\u6709\u6548\u7684JSON\u683C\u5F0F
|
10040=\u53C2\u6570\u503C\u5FC5\u987B\u662F\u6709\u6548\u7684JSON\u683C\u5F0F
|
||||||
|
|
||||||
|
10041=\u8BBE\u5907\u672A\u627E\u5230
|
||||||
|
10042={0}
|
||||||
|
|||||||
@@ -40,4 +40,7 @@
|
|||||||
10037=Parameter value must be a valid number
|
10037=Parameter value must be a valid number
|
||||||
10038=Parameter value must be true or false
|
10038=Parameter value must be true or false
|
||||||
10039=Parameter value must be a valid JSON array format
|
10039=Parameter value must be a valid JSON array format
|
||||||
10040=Parameter value must be a valid JSON format
|
10040=Parameter value must be a valid JSON format
|
||||||
|
|
||||||
|
10041=Device not found
|
||||||
|
10042={0}
|
||||||
@@ -40,4 +40,7 @@
|
|||||||
10037=\u53C2\u6570\u503C\u5FC5\u987B\u662F\u6709\u6548\u7684\u6570\u5B57
|
10037=\u53C2\u6570\u503C\u5FC5\u987B\u662F\u6709\u6548\u7684\u6570\u5B57
|
||||||
10038=\u53C2\u6570\u503C\u5FC5\u987B\u662Ftrue\u6216false
|
10038=\u53C2\u6570\u503C\u5FC5\u987B\u662Ftrue\u6216false
|
||||||
10039=\u53C2\u6570\u503C\u5FC5\u987B\u662F\u6709\u6548\u7684JSON\u6570\u7EC4\u683C\u5F0F
|
10039=\u53C2\u6570\u503C\u5FC5\u987B\u662F\u6709\u6548\u7684JSON\u6570\u7EC4\u683C\u5F0F
|
||||||
10040=\u53C2\u6570\u503C\u5FC5\u987B\u662F\u6709\u6548\u7684JSON\u683C\u5F0F
|
10040=\u53C2\u6570\u503C\u5FC5\u987B\u662F\u6709\u6548\u7684JSON\u683C\u5F0F
|
||||||
|
|
||||||
|
10041=\u8BBE\u5907\u672A\u627E\u5230
|
||||||
|
10042={0}
|
||||||
@@ -40,4 +40,7 @@
|
|||||||
10037=\u53C3\u6578\u503C\u5FC5\u9808\u662F\u6709\u6548\u7684\u6578\u5B57
|
10037=\u53C3\u6578\u503C\u5FC5\u9808\u662F\u6709\u6548\u7684\u6578\u5B57
|
||||||
10038=\u53C3\u6578\u503C\u5FC5\u9808\u662Ftrue\u6216false
|
10038=\u53C3\u6578\u503C\u5FC5\u9808\u662Ftrue\u6216false
|
||||||
10039=\u53C3\u6578\u503C\u5FC5\u9808\u662F\u6709\u6548\u7684JSON\u6578\u7D44\u683C\u5F0F
|
10039=\u53C3\u6578\u503C\u5FC5\u9808\u662F\u6709\u6548\u7684JSON\u6578\u7D44\u683C\u5F0F
|
||||||
10040=\u53C3\u6578\u503C\u5FC5\u9808\u662F\u6709\u6548\u7684JSON\u683C\u5F0F
|
10040=\u53C3\u6578\u503C\u5FC5\u9808\u662F\u6709\u6548\u7684JSON\u683C\u5F0F
|
||||||
|
|
||||||
|
10041=\u8A2D\u5099\u672A\u627E\u5230
|
||||||
|
10042={0}
|
||||||
@@ -28,8 +28,8 @@ sysuser.uuid.require=\u552F\u4E00\u6807\u8BC6\u4E0D\u80FD\u4E3A\u7A7A
|
|||||||
|
|
||||||
timbre.languages.require=\u97F3\u8272\u7684\u8BED\u8A00\u4E0D\u53EF\u4EE5\u4E3A\u7A7A
|
timbre.languages.require=\u97F3\u8272\u7684\u8BED\u8A00\u4E0D\u53EF\u4EE5\u4E3A\u7A7A
|
||||||
timbre.name.require=\u97F3\u8272\u7684\u540D\u79F0\u4E0D\u53EF\u4EE5\u4E3A\u7A7A
|
timbre.name.require=\u97F3\u8272\u7684\u540D\u79F0\u4E0D\u53EF\u4EE5\u4E3A\u7A7A
|
||||||
timbre.remark.require=\u97F3\u8272\u7684\u5907\u6CE8\u4E0D\u53EF\u4EE5\u4E3A\u7A7A
|
|
||||||
timbre.ttsModelId.require=\u97F3\u8272\u7684tts\u4E3B\u952E\u4E0D\u53EF\u4EE5\u4E3A\u7A7A
|
timbre.ttsModelId.require=\u97F3\u8272\u7684tts\u4E3B\u952E\u4E0D\u53EF\u4EE5\u4E3A\u7A7A
|
||||||
timbre.ttsVoice.require=\u97F3\u8272\u7684\u7F16\u7801\u4E0D\u53EF\u4EE5\u4E3A\u7A7A
|
timbre.ttsVoice.require=\u97F3\u8272\u7684\u7F16\u7801\u4E0D\u53EF\u4EE5\u4E3A\u7A7A
|
||||||
timbre.voiceDemo.require=\u97F3\u8272\u7684\u64AD\u653EDemo\u4E0D\u53EF\u4EE5\u4E3A\u7A7A
|
|
||||||
|
|
||||||
|
ota.device.not.found=\u8BBE\u5907\u672A\u627E\u5230
|
||||||
|
ota.device.need.bind={0}
|
||||||
|
|||||||
@@ -27,8 +27,8 @@ sysuser.uuid.require=The unique identifier cannot be empty
|
|||||||
|
|
||||||
timbre.languages.require=The language of the timbre cannot be empty
|
timbre.languages.require=The language of the timbre cannot be empty
|
||||||
timbre.name.require=The name of the timbre cannot be empty
|
timbre.name.require=The name of the timbre cannot be empty
|
||||||
timbre.remark.require=The remark of the timbre cannot be empty
|
|
||||||
timbre.ttsModelId.require=The TTS model ID of the timbre cannot be empty
|
timbre.ttsModelId.require=The TTS model ID of the timbre cannot be empty
|
||||||
timbre.ttsVoice.require=The TTS voice of the timbre cannot be empty
|
timbre.ttsVoice.require=The TTS voice of the timbre cannot be empty
|
||||||
timbre.voiceDemo.require=The voice demo of the timbre cannot be empty
|
|
||||||
|
|
||||||
|
ota.device.not.found=Device not found
|
||||||
|
ota.device.need.bind={0}
|
||||||
@@ -27,8 +27,8 @@ sysuser.uuid.require=\u552F\u4E00\u6807\u8BC6\u4E0D\u80FD\u4E3A\u7A7A
|
|||||||
|
|
||||||
timbre.languages.require=\u97F3\u8272\u7684\u8A9E\u8A00\u4E0D\u53EF\u4EE5\u70BA\u7A7A
|
timbre.languages.require=\u97F3\u8272\u7684\u8A9E\u8A00\u4E0D\u53EF\u4EE5\u70BA\u7A7A
|
||||||
timbre.name.require=\u97F3\u8272\u7684\u540D\u7A31\u4E0D\u53EF\u4EE5\u70BA\u7A7A
|
timbre.name.require=\u97F3\u8272\u7684\u540D\u7A31\u4E0D\u53EF\u4EE5\u70BA\u7A7A
|
||||||
timbre.remark.require=\u97F3\u8272\u7684\u5099\u6CE8\u4E0D\u53EF\u4EE5\u70BA\u7A7A
|
|
||||||
timbre.ttsModelId.require=\u97F3\u8272\u7684tts\u4E3B\u9375\u4E0D\u53EF\u4EE5\u70BA\u7A7A
|
timbre.ttsModelId.require=\u97F3\u8272\u7684tts\u4E3B\u9375\u4E0D\u53EF\u4EE5\u70BA\u7A7A
|
||||||
timbre.ttsVoice.require=\u97F3\u8272\u7684\u7DE8\u78BC\u4E0D\u53EF\u4EE5\u70BA\u7A7A
|
timbre.ttsVoice.require=\u97F3\u8272\u7684\u7DE8\u78BC\u4E0D\u53EF\u4EE5\u70BA\u7A7A
|
||||||
timbre.voiceDemo.require=\u97F3\u8272\u7684\u64AD\u653E\u5730\u5740\u4E0D\u53EF\u4EE5\u70BA\u7A7A
|
|
||||||
|
|
||||||
|
ota.device.not.found=\u8A2D\u5099\u672A\u627E\u5230
|
||||||
|
ota.device.need.bind={0}
|
||||||
@@ -27,7 +27,8 @@ sysuser.uuid.require=\u552F\u4E00\u6807\u8BC6\u4E0D\u80FD\u4E3A\u7A7A
|
|||||||
|
|
||||||
timbre.languages.require=\u97F3\u8272\u7684\u8BED\u8A00\u4E0D\u53EF\u4EE5\u4E3A\u7A7A
|
timbre.languages.require=\u97F3\u8272\u7684\u8BED\u8A00\u4E0D\u53EF\u4EE5\u4E3A\u7A7A
|
||||||
timbre.name.require=\u97F3\u8272\u7684\u540D\u79F0\u4E0D\u53EF\u4EE5\u4E3A\u7A7A
|
timbre.name.require=\u97F3\u8272\u7684\u540D\u79F0\u4E0D\u53EF\u4EE5\u4E3A\u7A7A
|
||||||
timbre.remark.require=\u97F3\u8272\u7684\u5907\u6CE8\u4E0D\u53EF\u4EE5\u4E3A\u7A7A
|
|
||||||
timbre.ttsModelId.require=\u97F3\u8272\u7684tts\u4E3B\u952E\u4E0D\u53EF\u4EE5\u4E3A\u7A7A
|
timbre.ttsModelId.require=\u97F3\u8272\u7684tts\u4E3B\u952E\u4E0D\u53EF\u4EE5\u4E3A\u7A7A
|
||||||
timbre.ttsVoice.require=\u97F3\u8272\u7684\u7F16\u7801\u4E0D\u53EF\u4EE5\u4E3A\u7A7A
|
timbre.ttsVoice.require=\u97F3\u8272\u7684\u7F16\u7801\u4E0D\u53EF\u4EE5\u4E3A\u7A7A
|
||||||
timbre.voiceDemo.require=\u97F3\u8272\u7684\u64AD\u653EDemo\u4E0D\u53EF\u4EE5\u4E3A\u7A7A
|
|
||||||
|
ota.device.not.found=\u8A2D\u5099\u672A\u627E\u5230
|
||||||
|
ota.device.need.bind={0}
|
||||||
@@ -1,4 +1 @@
|
|||||||
# 暂时使用群主的接口
|
VUE_APP_API_BASE_URL=/xiaozhi
|
||||||
# VUE_APP_API_BASE_URL=https://2662r3426b.vicp.fun/xiaozhi
|
|
||||||
# 如果本地开发,请使用以下接口
|
|
||||||
VUE_APP_API_BASE_URL=http://localhost:8002/xiaozhi
|
|
||||||
@@ -1 +1,3 @@
|
|||||||
VUE_APP_API_BASE_URL=/xiaozhi
|
VUE_APP_API_BASE_URL=/xiaozhi
|
||||||
|
# 是否开启CDN
|
||||||
|
VUE_APP_USE_CDN=false
|
||||||
Generated
+3249
-11
File diff suppressed because it is too large
Load Diff
@@ -31,7 +31,8 @@
|
|||||||
"sass": "^1.32.7",
|
"sass": "^1.32.7",
|
||||||
"sass-loader": "^12.0.0",
|
"sass-loader": "^12.0.0",
|
||||||
"vue-template-compiler": "^2.6.14",
|
"vue-template-compiler": "^2.6.14",
|
||||||
"webpack-bundle-analyzer": "^4.10.2"
|
"webpack-bundle-analyzer": "^4.10.2",
|
||||||
|
"workbox-webpack-plugin": "^7.3.0"
|
||||||
},
|
},
|
||||||
"browserslist": [
|
"browserslist": [
|
||||||
"> 1%",
|
"> 1%",
|
||||||
@@ -43,4 +44,4 @@
|
|||||||
"*.scss",
|
"*.scss",
|
||||||
"*.vue"
|
"*.vue"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
@@ -9,7 +9,7 @@
|
|||||||
<title>
|
<title>
|
||||||
<%= process.env.VUE_APP_TITLE %>
|
<%= process.env.VUE_APP_TITLE %>
|
||||||
</title>
|
</title>
|
||||||
<% if (htmlWebpackPlugin.options.cdn) { %>
|
<% if (htmlWebpackPlugin.options.cdn && htmlWebpackPlugin.options.cdn.css) { %>
|
||||||
<% for (var i in htmlWebpackPlugin.options.cdn.css) { %>
|
<% for (var i in htmlWebpackPlugin.options.cdn.css) { %>
|
||||||
<link rel="stylesheet" href="<%= htmlWebpackPlugin.options.cdn.css[i] %>">
|
<link rel="stylesheet" href="<%= htmlWebpackPlugin.options.cdn.css[i] %>">
|
||||||
<% } %>
|
<% } %>
|
||||||
@@ -23,7 +23,7 @@
|
|||||||
</noscript>
|
</noscript>
|
||||||
<div id="app"></div>
|
<div id="app"></div>
|
||||||
<!-- built files will be auto injected -->
|
<!-- built files will be auto injected -->
|
||||||
<% if (htmlWebpackPlugin.options.cdn) { %>
|
<% if (htmlWebpackPlugin.options.cdn && htmlWebpackPlugin.options.cdn.js) { %>
|
||||||
<% for (var i in htmlWebpackPlugin.options.cdn.js) { %>
|
<% for (var i in htmlWebpackPlugin.options.cdn.js) { %>
|
||||||
<script src="<%= htmlWebpackPlugin.options.cdn.js[i] %>"></script>
|
<script src="<%= htmlWebpackPlugin.options.cdn.js[i] %>"></script>
|
||||||
<% } %>
|
<% } %>
|
||||||
|
|||||||
@@ -0,0 +1,70 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="zh-CN">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>离线模式 - 小智控制台</title>
|
||||||
|
<style>
|
||||||
|
body {
|
||||||
|
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
height: 100vh;
|
||||||
|
margin: 0;
|
||||||
|
background-color: #f5f7fa;
|
||||||
|
color: #333;
|
||||||
|
}
|
||||||
|
.container {
|
||||||
|
text-align: center;
|
||||||
|
padding: 2rem;
|
||||||
|
background: white;
|
||||||
|
border-radius: 10px;
|
||||||
|
box-shadow: 0 0 20px rgba(0, 0, 0, 0.1);
|
||||||
|
max-width: 80%;
|
||||||
|
}
|
||||||
|
h1 {
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
color: #409EFF;
|
||||||
|
}
|
||||||
|
p {
|
||||||
|
margin-bottom: 1.5rem;
|
||||||
|
line-height: 1.6;
|
||||||
|
}
|
||||||
|
.btn {
|
||||||
|
background-color: #409EFF;
|
||||||
|
color: white;
|
||||||
|
border: none;
|
||||||
|
padding: 0.8rem 1.5rem;
|
||||||
|
border-radius: 4px;
|
||||||
|
font-size: 1rem;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: background-color 0.3s;
|
||||||
|
}
|
||||||
|
.btn:hover {
|
||||||
|
background-color: #337ecc;
|
||||||
|
}
|
||||||
|
.icon {
|
||||||
|
font-size: 4rem;
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
color: #409EFF;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="container">
|
||||||
|
<div class="icon">📶</div>
|
||||||
|
<h1>您当前处于离线模式</h1>
|
||||||
|
<p>看起来您的网络连接有问题,无法连接到小智控制台服务器。</p>
|
||||||
|
<p>部分已缓存的内容和静态资源可能仍然可用,但功能可能受到限制。</p>
|
||||||
|
<button class="btn" onclick="window.location.reload()">重新尝试连接</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
// 检测网络状态变化
|
||||||
|
window.addEventListener('online', () => {
|
||||||
|
window.location.reload();
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
<template>
|
<template>
|
||||||
<div id="app">
|
<div id="app">
|
||||||
<router-view />
|
<router-view />
|
||||||
|
<cache-viewer v-if="isCDNEnabled" :visible.sync="showCacheViewer" />
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@@ -45,4 +46,118 @@ nav {
|
|||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
<script>
|
<script>
|
||||||
|
import CacheViewer from '@/components/CacheViewer.vue';
|
||||||
|
import { logCacheStatus } from '@/utils/cacheViewer';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
components: {
|
||||||
|
CacheViewer
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
showCacheViewer: false,
|
||||||
|
isCDNEnabled: process.env.VUE_APP_USE_CDN === 'true'
|
||||||
|
};
|
||||||
|
},
|
||||||
|
mounted() {
|
||||||
|
// 只有在启用CDN时才添加相关事件和功能
|
||||||
|
if (this.isCDNEnabled) {
|
||||||
|
// 添加全局快捷键Alt+C用于显示缓存查看器
|
||||||
|
document.addEventListener('keydown', this.handleKeyDown);
|
||||||
|
|
||||||
|
// 在全局对象上添加缓存检查方法,便于调试
|
||||||
|
window.checkCDNCacheStatus = () => {
|
||||||
|
this.showCacheViewer = true;
|
||||||
|
};
|
||||||
|
|
||||||
|
// 在控制台输出提示信息
|
||||||
|
console.info(
|
||||||
|
'%c[小智服务] CDN缓存检查工具已加载',
|
||||||
|
'color: #409EFF; font-weight: bold;'
|
||||||
|
);
|
||||||
|
console.info(
|
||||||
|
'按下 Alt+C 组合键或在控制台运行 checkCDNCacheStatus() 可以查看CDN缓存状态'
|
||||||
|
);
|
||||||
|
|
||||||
|
// 检查Service Worker状态
|
||||||
|
this.checkServiceWorkerStatus();
|
||||||
|
} else {
|
||||||
|
console.info(
|
||||||
|
'%c[小智服务] CDN模式已禁用,使用本地打包资源',
|
||||||
|
'color: #67C23A; font-weight: bold;'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
beforeDestroy() {
|
||||||
|
// 只有在启用CDN时才需要移除事件监听
|
||||||
|
if (this.isCDNEnabled) {
|
||||||
|
document.removeEventListener('keydown', this.handleKeyDown);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
handleKeyDown(e) {
|
||||||
|
// Alt+C 快捷键
|
||||||
|
if (e.altKey && e.key === 'c') {
|
||||||
|
this.showCacheViewer = true;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
async checkServiceWorkerStatus() {
|
||||||
|
// 检查Service Worker是否已注册
|
||||||
|
if ('serviceWorker' in navigator) {
|
||||||
|
try {
|
||||||
|
const registrations = await navigator.serviceWorker.getRegistrations();
|
||||||
|
if (registrations.length > 0) {
|
||||||
|
console.info(
|
||||||
|
'%c[小智服务] Service Worker已注册',
|
||||||
|
'color: #67C23A; font-weight: bold;'
|
||||||
|
);
|
||||||
|
|
||||||
|
// 输出缓存状态到控制台
|
||||||
|
setTimeout(async () => {
|
||||||
|
const hasCaches = await logCacheStatus();
|
||||||
|
if (!hasCaches) {
|
||||||
|
console.info(
|
||||||
|
'%c[小智服务] 还未检测到缓存,请刷新页面或等待缓存建立',
|
||||||
|
'color: #E6A23C; font-weight: bold;'
|
||||||
|
);
|
||||||
|
|
||||||
|
// 开发环境下提供额外提示
|
||||||
|
if (process.env.NODE_ENV === 'development') {
|
||||||
|
console.info(
|
||||||
|
'%c[小智服务] 在开发环境中,Service Worker可能无法正常初始化缓存',
|
||||||
|
'color: #E6A23C; font-weight: bold;'
|
||||||
|
);
|
||||||
|
console.info('请尝试以下方法检查Service Worker是否生效:');
|
||||||
|
console.info('1. 在开发者工具的Application/Application标签页中查看Service Worker状态');
|
||||||
|
console.info('2. 在开发者工具的Application/Cache/Cache Storage中查看缓存内容');
|
||||||
|
console.info('3. 使用生产构建(npm run build)并通过HTTP服务器访问以测试完整功能');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, 2000);
|
||||||
|
} else {
|
||||||
|
console.info(
|
||||||
|
'%c[小智服务] Service Worker未注册,CDN资源可能无法缓存',
|
||||||
|
'color: #F56C6C; font-weight: bold;'
|
||||||
|
);
|
||||||
|
|
||||||
|
if (process.env.NODE_ENV === 'development') {
|
||||||
|
console.info(
|
||||||
|
'%c[小智服务] 在开发环境中,这是正常现象',
|
||||||
|
'color: #E6A23C; font-weight: bold;'
|
||||||
|
);
|
||||||
|
console.info('Service Worker通常只在生产环境中生效');
|
||||||
|
console.info('要测试Service Worker功能:');
|
||||||
|
console.info('1. 运行npm run build构建生产版本');
|
||||||
|
console.info('2. 通过HTTP服务器访问构建后的页面');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('检查Service Worker状态失败:', error);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
console.warn('当前浏览器不支持Service Worker,CDN资源缓存功能不可用');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
</script>
|
</script>
|
||||||
@@ -5,6 +5,7 @@ import device from './module/device.js'
|
|||||||
import model from './module/model.js'
|
import model from './module/model.js'
|
||||||
import timbre from "./module/timbre.js"
|
import timbre from "./module/timbre.js"
|
||||||
import user from './module/user.js'
|
import user from './module/user.js'
|
||||||
|
import ota from './module/ota.js'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 接口地址
|
* 接口地址
|
||||||
@@ -31,4 +32,5 @@ export default {
|
|||||||
device,
|
device,
|
||||||
model,
|
model,
|
||||||
timbre,
|
timbre,
|
||||||
|
ota
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
import { getServiceUrl } from '../api';
|
import { getServiceUrl } from '../api';
|
||||||
import RequestService from '../httpRequest';
|
import RequestService from '../httpRequest';
|
||||||
|
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
// 获取模型配置列表
|
// 获取模型配置列表
|
||||||
getModelList(params, callback) {
|
getModelList(params, callback) {
|
||||||
@@ -162,21 +161,25 @@ export default {
|
|||||||
// 更新模型配置
|
// 更新模型配置
|
||||||
updateModel(params, callback) {
|
updateModel(params, callback) {
|
||||||
const { modelType, provideCode, id, formData } = params;
|
const { modelType, provideCode, id, formData } = params;
|
||||||
|
const payload = {
|
||||||
|
...formData,
|
||||||
|
configJson: formData.configJson
|
||||||
|
};
|
||||||
RequestService.sendRequest()
|
RequestService.sendRequest()
|
||||||
.url(`${getServiceUrl()}/models/${modelType}/${provideCode}/${id}`)
|
.url(`${getServiceUrl()}/models/${modelType}/${provideCode}/${id}`)
|
||||||
.method('PUT')
|
.method('PUT')
|
||||||
.data(formData)
|
.data(payload)
|
||||||
.success((res) => {
|
.success((res) => {
|
||||||
RequestService.clearRequestTime();
|
RequestService.clearRequestTime();
|
||||||
callback(res);
|
callback(res);
|
||||||
})
|
})
|
||||||
.fail((err) => {
|
.fail((err) => {
|
||||||
console.error('更新模型失败:', err);
|
console.error('更新模型失败:', err);
|
||||||
this.$message.error(err.msg || '更新模型失败');
|
this.$message.error(err.msg || '更新模型失败');
|
||||||
RequestService.reAjaxFun(() => {
|
RequestService.reAjaxFun(() => {
|
||||||
this.updateModel(params, callback);
|
this.updateModel(params, callback);
|
||||||
});
|
});
|
||||||
}).send();
|
}).send();
|
||||||
},
|
},
|
||||||
// 设置默认模型
|
// 设置默认模型
|
||||||
setDefaultModel(id, callback) {
|
setDefaultModel(id, callback) {
|
||||||
|
|||||||
@@ -0,0 +1,123 @@
|
|||||||
|
import { getServiceUrl } from '../api';
|
||||||
|
import RequestService from '../httpRequest';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
// 分页查询OTA固件信息
|
||||||
|
getOtaList(params, callback) {
|
||||||
|
RequestService.sendRequest()
|
||||||
|
.url(`${getServiceUrl()}/otaMag`)
|
||||||
|
.method('GET')
|
||||||
|
.data(params)
|
||||||
|
.success((res) => {
|
||||||
|
RequestService.clearRequestTime();
|
||||||
|
callback(res);
|
||||||
|
})
|
||||||
|
.fail((err) => {
|
||||||
|
console.error('获取OTA固件列表失败:', err);
|
||||||
|
RequestService.reAjaxFun(() => {
|
||||||
|
this.getOtaList(params, callback);
|
||||||
|
});
|
||||||
|
}).send();
|
||||||
|
},
|
||||||
|
// 获取单个OTA固件信息
|
||||||
|
getOtaInfo(id, callback) {
|
||||||
|
RequestService.sendRequest()
|
||||||
|
.url(`${getServiceUrl()}/otaMag/${id}`)
|
||||||
|
.method('GET')
|
||||||
|
.success((res) => {
|
||||||
|
RequestService.clearRequestTime();
|
||||||
|
callback(res);
|
||||||
|
})
|
||||||
|
.fail((err) => {
|
||||||
|
console.error('获取OTA固件信息失败:', err);
|
||||||
|
RequestService.reAjaxFun(() => {
|
||||||
|
this.getOtaInfo(id, callback);
|
||||||
|
});
|
||||||
|
}).send();
|
||||||
|
},
|
||||||
|
// 保存OTA固件信息
|
||||||
|
saveOta(entity, callback) {
|
||||||
|
RequestService.sendRequest()
|
||||||
|
.url(`${getServiceUrl()}/otaMag`)
|
||||||
|
.method('POST')
|
||||||
|
.data(entity)
|
||||||
|
.success((res) => {
|
||||||
|
RequestService.clearRequestTime();
|
||||||
|
callback(res);
|
||||||
|
})
|
||||||
|
.fail((err) => {
|
||||||
|
console.error('保存OTA固件信息失败:', err);
|
||||||
|
RequestService.reAjaxFun(() => {
|
||||||
|
this.saveOta(entity, callback);
|
||||||
|
});
|
||||||
|
}).send();
|
||||||
|
},
|
||||||
|
// 更新OTA固件信息
|
||||||
|
updateOta(id, entity, callback) {
|
||||||
|
RequestService.sendRequest()
|
||||||
|
.url(`${getServiceUrl()}/otaMag/${id}`)
|
||||||
|
.method('PUT')
|
||||||
|
.data(entity)
|
||||||
|
.success((res) => {
|
||||||
|
RequestService.clearRequestTime();
|
||||||
|
callback(res);
|
||||||
|
})
|
||||||
|
.fail((err) => {
|
||||||
|
console.error('更新OTA固件信息失败:', err);
|
||||||
|
RequestService.reAjaxFun(() => {
|
||||||
|
this.updateOta(id, entity, callback);
|
||||||
|
});
|
||||||
|
}).send();
|
||||||
|
},
|
||||||
|
// 删除OTA固件
|
||||||
|
deleteOta(id, callback) {
|
||||||
|
RequestService.sendRequest()
|
||||||
|
.url(`${getServiceUrl()}/otaMag/${id}`)
|
||||||
|
.method('DELETE')
|
||||||
|
.success((res) => {
|
||||||
|
RequestService.clearRequestTime();
|
||||||
|
callback(res);
|
||||||
|
})
|
||||||
|
.fail((err) => {
|
||||||
|
console.error('删除OTA固件失败:', err);
|
||||||
|
RequestService.reAjaxFun(() => {
|
||||||
|
this.deleteOta(id, callback);
|
||||||
|
});
|
||||||
|
}).send();
|
||||||
|
},
|
||||||
|
// 上传固件文件
|
||||||
|
uploadFirmware(file, callback) {
|
||||||
|
const formData = new FormData();
|
||||||
|
formData.append('file', file);
|
||||||
|
RequestService.sendRequest()
|
||||||
|
.url(`${getServiceUrl()}/otaMag/upload`)
|
||||||
|
.method('POST')
|
||||||
|
.data(formData)
|
||||||
|
.success((res) => {
|
||||||
|
RequestService.clearRequestTime();
|
||||||
|
callback(res);
|
||||||
|
})
|
||||||
|
.fail((err) => {
|
||||||
|
console.error('上传固件文件失败:', err);
|
||||||
|
RequestService.reAjaxFun(() => {
|
||||||
|
this.uploadFirmware(file, callback);
|
||||||
|
});
|
||||||
|
}).send();
|
||||||
|
},
|
||||||
|
// 获取固件下载链接
|
||||||
|
getDownloadUrl(id, callback) {
|
||||||
|
RequestService.sendRequest()
|
||||||
|
.url(`${getServiceUrl()}/otaMag/getDownloadUrl/${id}`)
|
||||||
|
.method('GET')
|
||||||
|
.success((res) => {
|
||||||
|
RequestService.clearRequestTime();
|
||||||
|
callback(res);
|
||||||
|
})
|
||||||
|
.fail((err) => {
|
||||||
|
console.error('获取下载链接失败:', err);
|
||||||
|
RequestService.reAjaxFun(() => {
|
||||||
|
this.getDownloadUrl(id, callback);
|
||||||
|
});
|
||||||
|
}).send();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -106,7 +106,7 @@ export default {
|
|||||||
},
|
},
|
||||||
// 修改用户状态
|
// 修改用户状态
|
||||||
changeUserStatus(status, userIds, successCallback) {
|
changeUserStatus(status, userIds, successCallback) {
|
||||||
console.log(555,userIds)
|
console.log(555, userIds)
|
||||||
RequestService.sendRequest()
|
RequestService.sendRequest()
|
||||||
.url(`${getServiceUrl()}/admin/users/changeStatus/${status}`)
|
.url(`${getServiceUrl()}/admin/users/changeStatus/${status}`)
|
||||||
.method('put')
|
.method('put')
|
||||||
@@ -122,4 +122,20 @@ export default {
|
|||||||
})
|
})
|
||||||
}).send()
|
}).send()
|
||||||
},
|
},
|
||||||
|
// 获取公共配置
|
||||||
|
getPubConfig(callback) {
|
||||||
|
RequestService.sendRequest()
|
||||||
|
.url(`${getServiceUrl()}/user/pub-config`)
|
||||||
|
.method('GET')
|
||||||
|
.success((res) => {
|
||||||
|
RequestService.clearRequestTime();
|
||||||
|
callback(res);
|
||||||
|
})
|
||||||
|
.fail((err) => {
|
||||||
|
console.error('获取公共配置失败:', err);
|
||||||
|
RequestService.reAjaxFun(() => {
|
||||||
|
this.getPubConfig(callback);
|
||||||
|
});
|
||||||
|
}).send();
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|||||||
Binary file not shown.
|
After Width: | Height: | Size: 7.6 KiB |
@@ -1,5 +1,5 @@
|
|||||||
<template>
|
<template>
|
||||||
<el-dialog :visible.sync="visible" width="400px" center>
|
<el-dialog :visible="visible" @close="handleClose" width="400px" center>
|
||||||
<div
|
<div
|
||||||
style="margin: 0 10px 10px;display: flex;align-items: center;gap: 10px;font-weight: 700;font-size: 20px;text-align: left;color: #3d4566;">
|
style="margin: 0 10px 10px;display: flex;align-items: center;gap: 10px;font-weight: 700;font-size: 20px;text-align: left;color: #3d4566;">
|
||||||
<div
|
<div
|
||||||
@@ -79,7 +79,10 @@ export default {
|
|||||||
cancel() {
|
cancel() {
|
||||||
this.$emit('update:visible', false)
|
this.$emit('update:visible', false)
|
||||||
this.deviceCode = ""
|
this.deviceCode = ""
|
||||||
}
|
},
|
||||||
|
handleClose() {
|
||||||
|
this.$emit('update:visible', false);
|
||||||
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -1,12 +1,11 @@
|
|||||||
<template>
|
<template>
|
||||||
<el-dialog :visible="dialogVisible" @update:visible="handleVisibleChange" width="975px" center custom-class="custom-dialog" :show-close="false"
|
<el-dialog :visible="dialogVisible" @update:visible="handleVisibleChange" width="975px" center
|
||||||
class="center-dialog">
|
custom-class="custom-dialog" :show-close="false" class="center-dialog">
|
||||||
<div style="margin: 0 18px; text-align: left; padding: 10px; border-radius: 10px;">
|
<div style="margin: 0 18px; text-align: left; padding: 10px; border-radius: 10px;">
|
||||||
<div style="font-size: 30px; color: #3d4566; margin-top: -10px; margin-bottom: 10px; text-align: center;">
|
<div style="font-size: 30px; color: #3d4566; margin-top: -10px; margin-bottom: 10px; text-align: center;">
|
||||||
添加模型
|
添加模型
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
||||||
<button class="custom-close-btn" @click="handleClose">
|
<button class="custom-close-btn" @click="handleClose">
|
||||||
×
|
×
|
||||||
</button>
|
</button>
|
||||||
@@ -19,7 +18,7 @@
|
|||||||
<span style="margin-right: 8px;">是否启用</span>
|
<span style="margin-right: 8px;">是否启用</span>
|
||||||
<el-switch v-model="formData.isEnabled" class="custom-switch"></el-switch>
|
<el-switch v-model="formData.isEnabled" class="custom-switch"></el-switch>
|
||||||
</div>
|
</div>
|
||||||
<div style="display: flex; align-items: center;">
|
<div style="display: none; align-items: center;">
|
||||||
<span style="margin-right: 8px;">设为默认</span>
|
<span style="margin-right: 8px;">设为默认</span>
|
||||||
<el-switch v-model="formData.isDefault" class="custom-switch"></el-switch>
|
<el-switch v-model="formData.isDefault" class="custom-switch"></el-switch>
|
||||||
</div>
|
</div>
|
||||||
@@ -45,7 +44,7 @@
|
|||||||
</el-select>
|
</el-select>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
<el-form-item label="排序号" prop="sortOrder" style="flex: 1;">
|
<el-form-item label="排序号" prop="sortOrder" style="flex: 1;">
|
||||||
<el-input v-model="formData.sort" placeholder="请输入排序号" class="custom-input-bg"></el-input>
|
<el-input v-model="formData.sort" type="number" placeholder="请输入排序号" class="custom-input-bg"></el-input>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -66,18 +65,10 @@
|
|||||||
<el-form :model="formData.configJson" label-width="auto" label-position="left" class="custom-form">
|
<el-form :model="formData.configJson" label-width="auto" label-position="left" class="custom-form">
|
||||||
<template v-for="(row, rowIndex) in chunkedCallInfoFields">
|
<template v-for="(row, rowIndex) in chunkedCallInfoFields">
|
||||||
<div :key="rowIndex" style="display: flex; gap: 20px; margin-bottom: 0;">
|
<div :key="rowIndex" style="display: flex; gap: 20px; margin-bottom: 0;">
|
||||||
<el-form-item
|
<el-form-item v-for="field in row" :key="field.prop" :label="field.label" :prop="field.prop"
|
||||||
v-for="field in row"
|
|
||||||
:key="field.prop"
|
|
||||||
:label="field.label"
|
|
||||||
:prop="field.prop"
|
|
||||||
style="flex: 1;">
|
style="flex: 1;">
|
||||||
<el-input
|
<el-input v-model="formData.configJson[field.prop]" :placeholder="field.placeholder"
|
||||||
v-model="formData.configJson[field.prop]"
|
:type="field.type || 'text'" class="custom-input-bg" :show-password="field.type === 'password'">
|
||||||
:placeholder="field.placeholder"
|
|
||||||
:type="field.type || 'text'"
|
|
||||||
class="custom-input-bg"
|
|
||||||
:show-password="field.type === 'password'">
|
|
||||||
</el-input>
|
</el-input>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
</div>
|
</div>
|
||||||
@@ -86,7 +77,12 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div style="display: flex;justify-content: center;">
|
<div style="display: flex;justify-content: center;">
|
||||||
<el-button type="primary" @click="confirm" class="save-btn">
|
<el-button
|
||||||
|
type="primary"
|
||||||
|
@click="confirm"
|
||||||
|
class="save-btn"
|
||||||
|
:loading="saving"
|
||||||
|
:disabled="saving">
|
||||||
保存
|
保存
|
||||||
</el-button>
|
</el-button>
|
||||||
</div>
|
</div>
|
||||||
@@ -103,6 +99,7 @@ export default {
|
|||||||
},
|
},
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
|
saving: false,
|
||||||
providers: [],
|
providers: [],
|
||||||
dialogVisible: false,
|
dialogVisible: false,
|
||||||
providersLoaded: false,
|
providersLoaded: false,
|
||||||
@@ -124,7 +121,7 @@ export default {
|
|||||||
watch: {
|
watch: {
|
||||||
visible(val) {
|
visible(val) {
|
||||||
this.dialogVisible = val;
|
this.dialogVisible = val;
|
||||||
if(val) {
|
if (val) {
|
||||||
this.initConfigJson();
|
this.initConfigJson();
|
||||||
} else {
|
} else {
|
||||||
this.resetForm();
|
this.resetForm();
|
||||||
@@ -184,6 +181,7 @@ export default {
|
|||||||
},
|
},
|
||||||
|
|
||||||
handleClose() {
|
handleClose() {
|
||||||
|
this.saving = false;
|
||||||
this.$emit('update:visible', false);
|
this.$emit('update:visible', false);
|
||||||
},
|
},
|
||||||
initDynamicConfig() {
|
initDynamicConfig() {
|
||||||
@@ -194,8 +192,11 @@ export default {
|
|||||||
this.formData.configJson = newConfig;
|
this.formData.configJson = newConfig;
|
||||||
},
|
},
|
||||||
confirm() {
|
confirm() {
|
||||||
|
this.saving = true;
|
||||||
|
|
||||||
if (!this.formData.supplier) {
|
if (!this.formData.supplier) {
|
||||||
this.$message.error('请选择供应器');
|
this.$message.error('请选择供应器');
|
||||||
|
this.saving = false;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -215,11 +216,18 @@ export default {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
this.$emit('confirm', submitData);
|
try {
|
||||||
this.$emit('update:visible', false);
|
this.$emit('confirm', submitData);
|
||||||
this.resetForm();
|
this.$emit('update:visible', false);
|
||||||
|
this.resetForm();
|
||||||
|
} catch (e) {
|
||||||
|
console.error(e);
|
||||||
|
} finally {
|
||||||
|
this.saving = false;
|
||||||
|
}
|
||||||
},
|
},
|
||||||
resetForm() {
|
resetForm() {
|
||||||
|
this.saving = false;
|
||||||
this.formData = {
|
this.formData = {
|
||||||
modelName: '',
|
modelName: '',
|
||||||
modelCode: '',
|
modelCode: '',
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<template>
|
<template>
|
||||||
<el-dialog :visible.sync="visible" width="400px" center @open="handleOpen">
|
<el-dialog :visible="visible" @close="handleClose" width="400px" center @open="handleOpen">
|
||||||
<div
|
<div
|
||||||
style="margin: 0 10px 10px;display: flex;align-items: center;gap: 10px;font-weight: 700;font-size: 20px;text-align: left;color: #3d4566;">
|
style="margin: 0 10px 10px;display: flex;align-items: center;gap: 10px;font-weight: 700;font-size: 20px;text-align: left;color: #3d4566;">
|
||||||
<div
|
<div
|
||||||
@@ -11,7 +11,7 @@
|
|||||||
<div style="height: 1px;background: #e8f0ff;" />
|
<div style="height: 1px;background: #e8f0ff;" />
|
||||||
<div style="margin: 22px 15px;">
|
<div style="margin: 22px 15px;">
|
||||||
<div style="font-weight: 400;font-size: 14px;text-align: left;color: #3d4566;">
|
<div style="font-weight: 400;font-size: 14px;text-align: left;color: #3d4566;">
|
||||||
<div style="color: red;display: inline-block;">*</div> 智慧体名称:
|
<div style="color: red;display: inline-block;">*</div> 智能体名称:
|
||||||
</div>
|
</div>
|
||||||
<div class="input-46" style="margin-top: 12px;">
|
<div class="input-46" style="margin-top: 12px;">
|
||||||
<el-input ref="inputRef" placeholder="请输入智能体名称.." v-model="wisdomBodyName" @keyup.enter.native="confirm" />
|
<el-input ref="inputRef" placeholder="请输入智能体名称.." v-model="wisdomBodyName" @keyup.enter.native="confirm" />
|
||||||
@@ -66,7 +66,10 @@ export default {
|
|||||||
cancel() {
|
cancel() {
|
||||||
this.$emit('update:visible', false)
|
this.$emit('update:visible', false)
|
||||||
this.wisdomBodyName = ""
|
this.wisdomBodyName = ""
|
||||||
}
|
},
|
||||||
|
handleClose() {
|
||||||
|
this.$emit('update:visible', false);
|
||||||
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -0,0 +1,207 @@
|
|||||||
|
<template>
|
||||||
|
<el-dialog
|
||||||
|
title="CDN资源缓存状态"
|
||||||
|
:visible.sync="visible"
|
||||||
|
width="70%"
|
||||||
|
:before-close="handleClose"
|
||||||
|
>
|
||||||
|
<div v-if="isLoading" class="loading-container">
|
||||||
|
<p>正在加载缓存信息...</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-else>
|
||||||
|
<div v-if="!cacheAvailable" class="no-cache-message">
|
||||||
|
<i class="el-icon-warning-outline"></i>
|
||||||
|
<p>您的浏览器不支持Cache API或Service Worker未安装</p>
|
||||||
|
<el-button type="primary" @click="refreshPage">刷新页面</el-button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-else>
|
||||||
|
<el-alert
|
||||||
|
v-if="cacheData.totalCached === 0"
|
||||||
|
title="未发现缓存的CDN资源"
|
||||||
|
type="warning"
|
||||||
|
:closable="false"
|
||||||
|
show-icon
|
||||||
|
>
|
||||||
|
<p>Service Worker可能尚未完成初始化或缓存尚未建立。请刷新页面或等待一会后再试。</p>
|
||||||
|
</el-alert>
|
||||||
|
|
||||||
|
<div v-else>
|
||||||
|
<el-alert
|
||||||
|
title="CDN资源缓存状态"
|
||||||
|
type="success"
|
||||||
|
:closable="false"
|
||||||
|
show-icon
|
||||||
|
>
|
||||||
|
共发现 {{ cacheData.totalCached }} 个缓存资源
|
||||||
|
</el-alert>
|
||||||
|
|
||||||
|
<h3>JavaScript 资源 ({{ cacheData.js.length }})</h3>
|
||||||
|
<el-table :data="cacheData.js" stripe style="width: 100%">
|
||||||
|
<el-table-column prop="url" label="URL" width="auto" show-overflow-tooltip />
|
||||||
|
<el-table-column prop="cached" label="状态" width="100">
|
||||||
|
<template slot-scope="scope">
|
||||||
|
<el-tag type="success" v-if="scope.row.cached">已缓存</el-tag>
|
||||||
|
<el-tag type="danger" v-else>未缓存</el-tag>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
</el-table>
|
||||||
|
|
||||||
|
<h3>CSS 资源 ({{ cacheData.css.length }})</h3>
|
||||||
|
<el-table :data="cacheData.css" stripe style="width: 100%">
|
||||||
|
<el-table-column prop="url" label="URL" width="auto" show-overflow-tooltip />
|
||||||
|
<el-table-column prop="cached" label="状态" width="100">
|
||||||
|
<template slot-scope="scope">
|
||||||
|
<el-tag type="success" v-if="scope.row.cached">已缓存</el-tag>
|
||||||
|
<el-tag type="danger" v-else>未缓存</el-tag>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
</el-table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<span slot="footer" class="dialog-footer">
|
||||||
|
<el-button @click="handleClose">关闭</el-button>
|
||||||
|
<el-button type="primary" @click="refreshCache">刷新缓存状态</el-button>
|
||||||
|
<el-button type="danger" @click="clearCache">清除缓存</el-button>
|
||||||
|
</span>
|
||||||
|
</el-dialog>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import {
|
||||||
|
getCacheNames,
|
||||||
|
checkCdnCacheStatus,
|
||||||
|
clearAllCaches,
|
||||||
|
logCacheStatus
|
||||||
|
} from '../utils/cacheViewer';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'CacheViewer',
|
||||||
|
props: {
|
||||||
|
visible: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
isLoading: true,
|
||||||
|
cacheAvailable: false,
|
||||||
|
cacheData: {
|
||||||
|
css: [],
|
||||||
|
js: [],
|
||||||
|
totalCached: 0,
|
||||||
|
totalNotCached: 0
|
||||||
|
}
|
||||||
|
};
|
||||||
|
},
|
||||||
|
watch: {
|
||||||
|
visible(newVal) {
|
||||||
|
if (newVal) {
|
||||||
|
this.loadCacheData();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
async loadCacheData() {
|
||||||
|
this.isLoading = true;
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 先检查是否支持缓存API
|
||||||
|
if (!('caches' in window)) {
|
||||||
|
this.cacheAvailable = false;
|
||||||
|
this.isLoading = false;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查是否有Service Worker缓存
|
||||||
|
const cacheNames = await getCacheNames();
|
||||||
|
this.cacheAvailable = cacheNames.length > 0;
|
||||||
|
|
||||||
|
if (this.cacheAvailable) {
|
||||||
|
// 获取CDN缓存状态
|
||||||
|
this.cacheData = await checkCdnCacheStatus();
|
||||||
|
|
||||||
|
// 在控制台输出完整缓存状态
|
||||||
|
await logCacheStatus();
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('加载缓存数据失败:', error);
|
||||||
|
this.$message.error('加载缓存数据失败');
|
||||||
|
} finally {
|
||||||
|
this.isLoading = false;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
async refreshCache() {
|
||||||
|
this.loadCacheData();
|
||||||
|
this.$message.success('正在刷新缓存状态');
|
||||||
|
},
|
||||||
|
|
||||||
|
async clearCache() {
|
||||||
|
this.$confirm('确定要清除所有缓存吗?', '警告', {
|
||||||
|
confirmButtonText: '确定',
|
||||||
|
cancelButtonText: '取消',
|
||||||
|
type: 'warning'
|
||||||
|
}).then(async () => {
|
||||||
|
try {
|
||||||
|
const success = await clearAllCaches();
|
||||||
|
if (success) {
|
||||||
|
this.$message.success('缓存已清除');
|
||||||
|
await this.loadCacheData();
|
||||||
|
} else {
|
||||||
|
this.$message.error('清除缓存失败');
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('清除缓存失败:', error);
|
||||||
|
this.$message.error('清除缓存失败');
|
||||||
|
}
|
||||||
|
}).catch(() => {
|
||||||
|
this.$message.info('已取消清除');
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
refreshPage() {
|
||||||
|
window.location.reload();
|
||||||
|
},
|
||||||
|
|
||||||
|
handleClose() {
|
||||||
|
this.$emit('update:visible', false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.loading-container {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
padding: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.loading-spinner {
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.no-cache-message {
|
||||||
|
text-align: center;
|
||||||
|
padding: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.no-cache-message i {
|
||||||
|
font-size: 48px;
|
||||||
|
color: #E6A23C;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
h3 {
|
||||||
|
margin-top: 20px;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -23,12 +23,6 @@
|
|||||||
<div class="settings-btn" @click="handleConfigure">
|
<div class="settings-btn" @click="handleConfigure">
|
||||||
配置角色
|
配置角色
|
||||||
</div>
|
</div>
|
||||||
<!-- <div class="settings-btn">-->
|
|
||||||
<!-- 声纹识别-->
|
|
||||||
<!-- </div>-->
|
|
||||||
<!-- <div class="settings-btn">-->
|
|
||||||
<!-- 历史对话-->
|
|
||||||
<!-- </div>-->
|
|
||||||
<div class="settings-btn" @click="handleDeviceManage">
|
<div class="settings-btn" @click="handleDeviceManage">
|
||||||
设备管理({{ device.deviceCount }})
|
设备管理({{ device.deviceCount }})
|
||||||
</div>
|
</div>
|
||||||
@@ -80,10 +74,11 @@ export default {
|
|||||||
|
|
||||||
.settings-btn {
|
.settings-btn {
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
font-size: 10px;
|
font-size: 12px;
|
||||||
color: #5778ff;
|
color: #5778ff;
|
||||||
background: #e6ebff;
|
background: #e6ebff;
|
||||||
width: 57px;
|
width: auto;
|
||||||
|
padding: 0 12px;
|
||||||
height: 21px;
|
height: 21px;
|
||||||
line-height: 21px;
|
line-height: 21px;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
@@ -94,7 +89,7 @@ export default {
|
|||||||
display: flex;
|
display: flex;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
margin-top: 15px;
|
margin-top: 15px;
|
||||||
font-size: 10px;
|
font-size: 12px;
|
||||||
color: #979db1;
|
color: #979db1;
|
||||||
font-weight: 400;
|
font-weight: 400;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,136 @@
|
|||||||
|
<template>
|
||||||
|
<el-dialog :title="title" :visible.sync="visible" width="500px" @close="handleClose">
|
||||||
|
<el-form ref="form" :model="form" :rules="rules" label-width="100px">
|
||||||
|
<el-form-item label="固件名称" prop="firmwareName">
|
||||||
|
<el-input v-model="form.firmwareName" placeholder="请输入固件名称(板子+版本号)"></el-input>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="固件类型" prop="type">
|
||||||
|
<el-select v-model="form.type" placeholder="请选择固件类型" style="width: 100%;" filterable>
|
||||||
|
<el-option v-for="item in firmwareTypes" :key="item.key" :label="item.name" :value="item.key"></el-option>
|
||||||
|
</el-select>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="版本号" prop="version">
|
||||||
|
<el-input v-model="form.version" placeholder="请输入版本号(x.x.x格式)"></el-input>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="固件文件" prop="firmwarePath">
|
||||||
|
<el-upload class="upload-demo" action="#" :http-request="handleUpload" :before-upload="beforeUpload"
|
||||||
|
:accept="'.bin,.apk'" :limit="1" :multiple="false" :auto-upload="true">
|
||||||
|
<el-button size="small" type="primary">点击上传</el-button>
|
||||||
|
<div slot="tip" class="el-upload__tip">只能上传固件文件(.bin/.apk),且不超过100MB</div>
|
||||||
|
</el-upload>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="备注" prop="remark">
|
||||||
|
<el-input type="textarea" v-model="form.remark" placeholder="请输入备注信息"></el-input>
|
||||||
|
</el-form-item>
|
||||||
|
</el-form>
|
||||||
|
<div slot="footer" class="dialog-footer">
|
||||||
|
<el-button @click="handleCancel">取 消</el-button>
|
||||||
|
<el-button type="primary" @click="handleSubmit">确 定</el-button>
|
||||||
|
</div>
|
||||||
|
</el-dialog>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import Api from '@/apis/api';
|
||||||
|
import { FIRMWARE_TYPES } from '@/utils';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'FirmwareDialog',
|
||||||
|
props: {
|
||||||
|
visible: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false
|
||||||
|
},
|
||||||
|
title: {
|
||||||
|
type: String,
|
||||||
|
default: ''
|
||||||
|
},
|
||||||
|
form: {
|
||||||
|
type: Object,
|
||||||
|
default: () => ({})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
firmwareTypes: FIRMWARE_TYPES,
|
||||||
|
rules: {
|
||||||
|
firmwareName: [
|
||||||
|
{ required: true, message: '请输入固件名称(板子+版本号)', trigger: 'blur' }
|
||||||
|
],
|
||||||
|
type: [
|
||||||
|
{ required: true, message: '请选择固件类型', trigger: 'change' }
|
||||||
|
],
|
||||||
|
version: [
|
||||||
|
{ required: true, message: '请输入版本号', trigger: 'blur' },
|
||||||
|
{ pattern: /^\d+\.\d+\.\d+$/, message: '版本号格式不正确,请输入x.x.x格式', trigger: 'blur' }
|
||||||
|
],
|
||||||
|
firmwarePath: [
|
||||||
|
{ required: false, message: '请上传固件文件', trigger: 'change' }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
handleClose() {
|
||||||
|
this.$refs.form.clearValidate();
|
||||||
|
this.$emit('cancel');
|
||||||
|
},
|
||||||
|
handleCancel() {
|
||||||
|
this.$refs.form.clearValidate();
|
||||||
|
this.$emit('cancel');
|
||||||
|
},
|
||||||
|
handleSubmit() {
|
||||||
|
this.$refs.form.validate(valid => {
|
||||||
|
if (valid) {
|
||||||
|
// 如果是新增模式且没有上传文件,则提示错误
|
||||||
|
if (!this.form.id && !this.form.firmwarePath) {
|
||||||
|
this.$message.error('请上传固件文件')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
// 提交成功后将关闭对话框的逻辑交给父组件处理
|
||||||
|
this.$emit('submit', this.form)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
},
|
||||||
|
beforeUpload(file) {
|
||||||
|
const isValidSize = file.size / 1024 / 1024 < 100
|
||||||
|
const isValidType = ['.bin', '.apk'].some(ext => file.name.toLowerCase().endsWith(ext))
|
||||||
|
|
||||||
|
if (!isValidType) {
|
||||||
|
this.$message.error('只能上传.bin/.apk格式的固件文件!')
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if (!isValidSize) {
|
||||||
|
this.$message.error('固件文件大小不能超过100MB!')
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
},
|
||||||
|
handleUpload(options) {
|
||||||
|
const { file } = options
|
||||||
|
Api.ota.uploadFirmware(file, (res) => {
|
||||||
|
res = res.data
|
||||||
|
if (res.code === 0) {
|
||||||
|
this.form.firmwarePath = res.data
|
||||||
|
this.form.size = file.size
|
||||||
|
this.$message.success('固件文件上传成功')
|
||||||
|
} else {
|
||||||
|
this.$message.error(res.msg || '文件上传失败')
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.upload-demo {
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
|
||||||
|
.el-upload__tip {
|
||||||
|
line-height: 1.2;
|
||||||
|
padding-top: 5px;
|
||||||
|
color: #909399;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -32,6 +32,12 @@
|
|||||||
:style="{ filter: $route.path === '/params-management' ? 'brightness(0) invert(1)' : 'None' }" />
|
:style="{ filter: $route.path === '/params-management' ? 'brightness(0) invert(1)' : 'None' }" />
|
||||||
参数管理
|
参数管理
|
||||||
</div>
|
</div>
|
||||||
|
<div v-if="isSuperAdmin" class="equipment-management"
|
||||||
|
:class="{ 'active-tab': $route.path === '/ota-management' }" @click="goOtaManagement">
|
||||||
|
<img loading="lazy" alt="" src="@/assets/header/firmware_update.png"
|
||||||
|
:style="{ filter: $route.path === '/ota-management' ? 'brightness(0) invert(1)' : 'None' }" />
|
||||||
|
OTA管理
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 右侧元素 -->
|
<!-- 右侧元素 -->
|
||||||
@@ -105,6 +111,9 @@ export default {
|
|||||||
goParamManagement() {
|
goParamManagement() {
|
||||||
this.$router.push('/params-management')
|
this.$router.push('/params-management')
|
||||||
},
|
},
|
||||||
|
goOtaManagement() {
|
||||||
|
this.$router.push('/ota-management')
|
||||||
|
},
|
||||||
// 获取用户信息
|
// 获取用户信息
|
||||||
fetchUserInfo() {
|
fetchUserInfo() {
|
||||||
userApi.getUserInfo(({ data }) => {
|
userApi.getUserInfo(({ data }) => {
|
||||||
@@ -170,7 +179,7 @@ export default {
|
|||||||
.header {
|
.header {
|
||||||
background: #f6fcfe66;
|
background: #f6fcfe66;
|
||||||
border: 1px solid #fff;
|
border: 1px solid #fff;
|
||||||
height: 53px !important;
|
height: 63px !important;
|
||||||
min-width: 900px;
|
min-width: 900px;
|
||||||
/* 设置最小宽度防止过度压缩 */
|
/* 设置最小宽度防止过度压缩 */
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
@@ -197,7 +206,7 @@ export default {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.brand-img {
|
.brand-img {
|
||||||
height: 18px;
|
height: 20px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.header-center {
|
.header-center {
|
||||||
@@ -219,13 +228,13 @@ export default {
|
|||||||
|
|
||||||
.equipment-management {
|
.equipment-management {
|
||||||
padding: 0 9px;
|
padding: 0 9px;
|
||||||
width: 82px;
|
width: px;
|
||||||
height: 24px;
|
height: 30px;
|
||||||
border-radius: 12px;
|
border-radius: 15px;
|
||||||
background: #deeafe;
|
background: #deeafe;
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
font-size: 10px;
|
font-size: 14px;
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
gap: 7px;
|
gap: 7px;
|
||||||
color: #3d4566;
|
color: #3d4566;
|
||||||
@@ -235,6 +244,7 @@ export default {
|
|||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
/* 防止导航按钮被压缩 */
|
/* 防止导航按钮被压缩 */
|
||||||
|
padding: 0px 15px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.equipment-management.active-tab {
|
.equipment-management.active-tab {
|
||||||
|
|||||||
@@ -17,7 +17,7 @@
|
|||||||
<span style="margin-right: 8px;">是否启用</span>
|
<span style="margin-right: 8px;">是否启用</span>
|
||||||
<el-switch v-model="form.isEnabled" :active-value="1" :inactive-value="0" class="custom-switch"></el-switch>
|
<el-switch v-model="form.isEnabled" :active-value="1" :inactive-value="0" class="custom-switch"></el-switch>
|
||||||
</div>
|
</div>
|
||||||
<div style="display: flex; align-items: center;">
|
<div style="display: none; align-items: center;">
|
||||||
<span style="margin-right: 8px;">设为默认</span>
|
<span style="margin-right: 8px;">设为默认</span>
|
||||||
<el-switch v-model="form.isDefault" :active-value="1" :inactive-value="0" class="custom-switch"></el-switch>
|
<el-switch v-model="form.isDefault" :active-value="1" :inactive-value="0" class="custom-switch"></el-switch>
|
||||||
</div>
|
</div>
|
||||||
@@ -64,18 +64,10 @@
|
|||||||
<el-form :model="form.configJson" ref="callInfoForm" label-width="auto" class="custom-form">
|
<el-form :model="form.configJson" ref="callInfoForm" label-width="auto" class="custom-form">
|
||||||
<template v-for="(row, rowIndex) in chunkedCallInfoFields">
|
<template v-for="(row, rowIndex) in chunkedCallInfoFields">
|
||||||
<div :key="rowIndex" style="display: flex; gap: 20px; margin-bottom: 0;">
|
<div :key="rowIndex" style="display: flex; gap: 20px; margin-bottom: 0;">
|
||||||
<el-form-item
|
<el-form-item v-for="field in row" :key="field.prop" :label="field.label" :prop="field.prop"
|
||||||
v-for="field in row"
|
|
||||||
:key="field.prop"
|
|
||||||
:label="field.label"
|
|
||||||
:prop="field.prop"
|
|
||||||
style="flex: 1;">
|
style="flex: 1;">
|
||||||
<el-input
|
<el-input v-model="form.configJson[field.prop]" :placeholder="field.placeholder" :type="field.type"
|
||||||
v-model="form.configJson[field.prop]"
|
class="custom-input-bg" :show-password="field.type === 'password'">
|
||||||
:placeholder="field.placeholder"
|
|
||||||
:type="field.type"
|
|
||||||
class="custom-input-bg"
|
|
||||||
:show-password="field.type === 'password'">
|
|
||||||
</el-input>
|
</el-input>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
</div>
|
</div>
|
||||||
@@ -84,7 +76,12 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div style="display: flex;justify-content: center;">
|
<div style="display: flex;justify-content: center;">
|
||||||
<el-button type="primary" @click="handleSave" class="save-btn">
|
<el-button
|
||||||
|
type="primary"
|
||||||
|
@click="handleSave"
|
||||||
|
class="save-btn"
|
||||||
|
:loading="saving"
|
||||||
|
:disabled="saving">
|
||||||
保存
|
保存
|
||||||
</el-button>
|
</el-button>
|
||||||
</div>
|
</div>
|
||||||
@@ -94,14 +91,6 @@
|
|||||||
<script>
|
<script>
|
||||||
import Api from '@/apis/api';
|
import Api from '@/apis/api';
|
||||||
|
|
||||||
const DEFAULT_CONFIG_JSON = {
|
|
||||||
type: "",
|
|
||||||
base_url: "",
|
|
||||||
model_name: "",
|
|
||||||
api_key: "",
|
|
||||||
empty: false
|
|
||||||
};
|
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: "ModelEditDialog",
|
name: "ModelEditDialog",
|
||||||
props: {
|
props: {
|
||||||
@@ -118,6 +107,7 @@ export default {
|
|||||||
dialogVisible: this.visible,
|
dialogVisible: this.visible,
|
||||||
providers: [],
|
providers: [],
|
||||||
providersLoaded: false,
|
providersLoaded: false,
|
||||||
|
saving: false,
|
||||||
allProvidersData: null,
|
allProvidersData: null,
|
||||||
pendingProviderType: null,
|
pendingProviderType: null,
|
||||||
pendingModelData: null,
|
pendingModelData: null,
|
||||||
@@ -132,7 +122,7 @@ export default {
|
|||||||
docLink: "",
|
docLink: "",
|
||||||
remark: "",
|
remark: "",
|
||||||
sort: 0,
|
sort: 0,
|
||||||
configJson: JSON.parse(JSON.stringify(DEFAULT_CONFIG_JSON))
|
configJson: {}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
@@ -173,57 +163,73 @@ export default {
|
|||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
resetForm() {
|
resetForm() {
|
||||||
this.form = {
|
this.form = {
|
||||||
id: "",
|
id: "",
|
||||||
modelType: "",
|
modelType: "",
|
||||||
modelCode: "",
|
modelCode: "",
|
||||||
modelName: "",
|
modelName: "",
|
||||||
isDefault: false,
|
isDefault: false,
|
||||||
isEnabled: false,
|
isEnabled: false,
|
||||||
docLink: "",
|
docLink: "",
|
||||||
remark: "",
|
remark: "",
|
||||||
sort: 0,
|
sort: 0,
|
||||||
configJson: JSON.parse(JSON.stringify(DEFAULT_CONFIG_JSON))
|
configJson: {}
|
||||||
};
|
};
|
||||||
|
this.dynamicCallInfoFields.forEach(field => {
|
||||||
|
this.$set(this.form.configJson, field.prop, '');
|
||||||
|
});
|
||||||
},
|
},
|
||||||
resetProviders() {
|
resetProviders() {
|
||||||
this.providers = [];
|
this.providers = [];
|
||||||
this.providersLoaded = false;
|
this.providersLoaded = false;
|
||||||
},
|
},
|
||||||
loadModelData() {
|
loadModelData() {
|
||||||
if (this.modelData.id) {
|
if (this.modelData.id) {
|
||||||
Api.model.getModelConfig(this.modelData.id, ({ data }) => {
|
Api.model.getModelConfig(this.modelData.id, ({ data }) => {
|
||||||
if (data.code === 0 && data.data) {
|
if (data.code === 0 && data.data) {
|
||||||
const model = data.data;
|
const model = data.data;
|
||||||
this.pendingProviderType = model.configJson.type;
|
this.pendingProviderType = model.configJson.type;
|
||||||
this.pendingModelData = model;
|
this.pendingModelData = model;
|
||||||
|
|
||||||
if (this.providersLoaded) {
|
if (this.providersLoaded) {
|
||||||
this.loadProviderFields(model.configJson.type);
|
this.loadProviderFields(model.configJson.type);
|
||||||
} else {
|
} else {
|
||||||
this.loadProviders();
|
this.loadProviders();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
handleSave() {
|
handleSave() {
|
||||||
const provideCode = this.form.configJson.type;
|
this.saving = true; // 开始保存加载
|
||||||
const formData = {
|
|
||||||
id: this.modelData.id,
|
const provideCode = this.form.configJson.type;
|
||||||
modelCode: this.form.modelCode,
|
const formData = {
|
||||||
modelName: this.form.modelName,
|
id: this.modelData.id,
|
||||||
isDefault: this.form.isDefault ? 1 : 0,
|
modelCode: this.form.modelCode,
|
||||||
isEnabled: this.form.isEnabled ? 1 : 0,
|
modelName: this.form.modelName,
|
||||||
docLink: this.form.docLink,
|
isDefault: this.form.isDefault ? 1 : 0,
|
||||||
remark: this.form.remark,
|
isEnabled: this.form.isEnabled ? 1 : 0,
|
||||||
sort: this.form.sort || 0,
|
docLink: this.form.docLink,
|
||||||
configJson: {
|
remark: this.form.remark,
|
||||||
...this.form.configJson,
|
sort: this.form.sort || 0,
|
||||||
}
|
configJson: {
|
||||||
|
...this.form.configJson,
|
||||||
|
}
|
||||||
};
|
};
|
||||||
this.$emit("save", { provideCode, formData });
|
|
||||||
this.dialogVisible = false;
|
this.$emit("save", {
|
||||||
|
provideCode,
|
||||||
|
formData,
|
||||||
|
done: () => {
|
||||||
|
this.saving = false; // 保存完成后回调
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// 如果父组件不处理done回调,3秒后自动关闭加载状态
|
||||||
|
setTimeout(() => {
|
||||||
|
this.saving = false;
|
||||||
|
}, 3000);
|
||||||
},
|
},
|
||||||
loadProviders() {
|
loadProviders() {
|
||||||
if (this.providersLoaded) return;
|
if (this.providersLoaded) return;
|
||||||
@@ -266,6 +272,8 @@ export default {
|
|||||||
this.dynamicCallInfoFields.forEach(field => {
|
this.dynamicCallInfoFields.forEach(field => {
|
||||||
if (!configJson.hasOwnProperty(field.prop)) {
|
if (!configJson.hasOwnProperty(field.prop)) {
|
||||||
configJson[field.prop] = '';
|
configJson[field.prop] = '';
|
||||||
|
} else if (typeof configJson[field.prop] !== 'string') {
|
||||||
|
configJson[field.prop] = String(configJson[field.prop]);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -280,7 +288,6 @@ export default {
|
|||||||
remark: model.remark,
|
remark: model.remark,
|
||||||
sort: Number(model.sort) || 0,
|
sort: Number(model.sort) || 0,
|
||||||
configJson: {
|
configJson: {
|
||||||
...DEFAULT_CONFIG_JSON,
|
|
||||||
...configJson
|
...configJson
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -40,7 +40,12 @@
|
|||||||
</el-form>
|
</el-form>
|
||||||
|
|
||||||
<div class="dialog-footer">
|
<div class="dialog-footer">
|
||||||
<el-button type="primary" @click="submit" class="save-btn">
|
<el-button
|
||||||
|
type="primary"
|
||||||
|
@click="submit"
|
||||||
|
class="save-btn"
|
||||||
|
:loading="saving"
|
||||||
|
:disabled="saving">
|
||||||
保存
|
保存
|
||||||
</el-button>
|
</el-button>
|
||||||
<el-button @click="cancel" class="cancel-btn">
|
<el-button @click="cancel" class="cancel-btn">
|
||||||
@@ -76,6 +81,7 @@ export default {
|
|||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
dialogKey: Date.now(),
|
dialogKey: Date.now(),
|
||||||
|
saving: false,
|
||||||
valueTypeOptions: [
|
valueTypeOptions: [
|
||||||
{ value: 'string', label: '字符串(string)' },
|
{ value: 'string', label: '字符串(string)' },
|
||||||
{ value: 'number', label: '数字(number)' },
|
{ value: 'number', label: '数字(number)' },
|
||||||
@@ -100,11 +106,22 @@ export default {
|
|||||||
submit() {
|
submit() {
|
||||||
this.$refs.form.validate((valid) => {
|
this.$refs.form.validate((valid) => {
|
||||||
if (valid) {
|
if (valid) {
|
||||||
this.$emit('submit', this.form);
|
this.saving = true; // 开始加载
|
||||||
|
this.$emit('submit', {
|
||||||
|
form: this.form,
|
||||||
|
done: () => {
|
||||||
|
this.saving = false; // 加载完成
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
this.saving = false;
|
||||||
|
}, 3000);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
cancel() {
|
cancel() {
|
||||||
|
this.saving = false; // 取消时重置状态
|
||||||
this.$emit('cancel');
|
this.$emit('cancel');
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -14,13 +14,16 @@
|
|||||||
class="table-container"
|
class="table-container"
|
||||||
ref="tableContainer"
|
ref="tableContainer"
|
||||||
@scroll="handleScroll">
|
@scroll="handleScroll">
|
||||||
<el-table
|
<el-table
|
||||||
v-loading="loading"
|
v-loading="loading"
|
||||||
:data="filteredTtsModels"
|
:data="filteredTtsModels"
|
||||||
style="width: 100%;"
|
style="width: 100%;"
|
||||||
class="data-table"
|
class="data-table"
|
||||||
header-row-class-name="table-header"
|
header-row-class-name="table-header"
|
||||||
:fit="true">
|
:fit="true"
|
||||||
|
element-loading-text="拼命加载中"
|
||||||
|
element-loading-spinner="el-icon-loading"
|
||||||
|
element-loading-background="rgba(0, 0, 0, 0.8)">
|
||||||
<el-table-column label="选择" width="50" align="center">
|
<el-table-column label="选择" width="50" align="center">
|
||||||
<template slot-scope="scope">
|
<template slot-scope="scope">
|
||||||
<el-checkbox v-model="scope.row.selected"></el-checkbox>
|
<el-checkbox v-model="scope.row.selected"></el-checkbox>
|
||||||
@@ -187,28 +190,28 @@ export default {
|
|||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
loadData() {
|
loadData() {
|
||||||
|
this.loading = true;
|
||||||
const params = {
|
const params = {
|
||||||
ttsModelId: this.ttsModelId,
|
ttsModelId: this.ttsModelId,
|
||||||
page: this.currentPage,
|
page: this.currentPage,
|
||||||
limit: this.pageSize,
|
limit: this.pageSize,
|
||||||
name: this.searchQuery
|
name: this.searchQuery
|
||||||
};
|
};
|
||||||
|
|
||||||
Api.timbre.getVoiceList(params, (data) => {
|
Api.timbre.getVoiceList(params, (data) => {
|
||||||
if (data.code === 0) {
|
if (data.code === 0) {
|
||||||
this.ttsModels = data.data.list
|
this.ttsModels = data.data.list
|
||||||
.map(item => ({
|
.map(item => ({
|
||||||
id: item.id || '',
|
id: item.id || '',
|
||||||
voiceCode: item.ttsVoice || '',
|
voiceCode: item.ttsVoice || '',
|
||||||
voiceName: item.name || '未命名音色',
|
voiceName: item.name || '未命名音色',
|
||||||
languageType: item.languages || '',
|
languageType: item.languages || '',
|
||||||
remark: item.remark || '',
|
remark: item.remark || '',
|
||||||
voiceDemo: item.voiceDemo || '',
|
voiceDemo: item.voiceDemo || '',
|
||||||
selected: false,
|
selected: false,
|
||||||
editing: false,
|
editing: false,
|
||||||
sort: Number(item.sort)
|
sort: Number(item.sort)
|
||||||
}))
|
}))
|
||||||
.sort((a, b) => a.sort - b.sort);
|
.sort((a, b) => a.sort - b.sort);
|
||||||
this.total = data.total;
|
this.total = data.total;
|
||||||
} else {
|
} else {
|
||||||
this.$message.error({
|
this.$message.error({
|
||||||
@@ -216,12 +219,14 @@ export default {
|
|||||||
showClose: true
|
showClose: true
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
this.loading = false;
|
||||||
}, (err) => {
|
}, (err) => {
|
||||||
console.error('加载失败:', err);
|
console.error('加载失败:', err);
|
||||||
this.$message.error({
|
this.$message.error({
|
||||||
message: '加载音色数据失败',
|
message: '加载音色数据失败',
|
||||||
showClose: true
|
showClose: true
|
||||||
});
|
});
|
||||||
|
this.loading = false;
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -551,28 +556,26 @@ export default {
|
|||||||
|
|
||||||
/* 备注文本 */
|
/* 备注文本 */
|
||||||
::v-deep .remark-input .el-textarea__inner {
|
::v-deep .remark-input .el-textarea__inner {
|
||||||
background-color: #f5f5f5;
|
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
border: 1px solid #e6e6e6;
|
border: 1px solid #e6e6e6;
|
||||||
padding: 8px 12px;
|
padding: 8px 12px;
|
||||||
resize: none;
|
resize: none;
|
||||||
max-height: 40px !important;
|
max-height: 40px !important;
|
||||||
line-height: 1.5;
|
line-height: 1.5;
|
||||||
}
|
background-color: transparent !important;
|
||||||
|
|
||||||
::v-deep .remark-input .el-textarea__inner::placeholder {
|
|
||||||
color: black !important;
|
|
||||||
opacity: 0.7;
|
|
||||||
}
|
|
||||||
|
|
||||||
::v-deep .remark-input .el-textarea__inner {
|
|
||||||
background-color: #f4f6fa;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
::v-deep .remark-input .el-textarea__inner:focus {
|
::v-deep .remark-input .el-textarea__inner:focus {
|
||||||
background-color: #edeffb;
|
border-color: #409EFF !important;
|
||||||
|
outline: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
::v-deep .remark-input .el-textarea__inner::placeholder {
|
||||||
|
color: #c0c4cc !important;
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
/* 滚动容器 */
|
/* 滚动容器 */
|
||||||
.scroll-wrapper {
|
.scroll-wrapper {
|
||||||
display: flex;
|
display: flex;
|
||||||
@@ -582,7 +585,7 @@ export default {
|
|||||||
|
|
||||||
.table-container {
|
.table-container {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
overflow-y: scroll;
|
overflow: auto;
|
||||||
scrollbar-width: none;
|
scrollbar-width: none;
|
||||||
padding-right: 15px;
|
padding-right: 15px;
|
||||||
width: calc(100% - 16px);
|
width: calc(100% - 16px);
|
||||||
@@ -641,8 +644,8 @@ export default {
|
|||||||
|
|
||||||
/* 音频播放器容器样式 */
|
/* 音频播放器容器样式 */
|
||||||
.custom-audio-container {
|
.custom-audio-container {
|
||||||
width: 280px;
|
width: 90%;
|
||||||
margin: auto;
|
margin: 0 auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 新增按钮组样式 */
|
/* 新增按钮组样式 */
|
||||||
@@ -650,6 +653,12 @@ export default {
|
|||||||
bottom: 20px;
|
bottom: 20px;
|
||||||
padding-top: 10px;
|
padding-top: 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.action-buttons .el-button {
|
||||||
|
padding: 8px 15px;
|
||||||
|
font-size: 11px;
|
||||||
|
}
|
||||||
|
|
||||||
.edit-btn,
|
.edit-btn,
|
||||||
.delete-btn,
|
.delete-btn,
|
||||||
.save-btn {
|
.save-btn {
|
||||||
@@ -668,4 +677,40 @@ export default {
|
|||||||
.save-btn {
|
.save-btn {
|
||||||
color: #5cca8e !important;
|
color: #5cca8e !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* 表格单元格自适应 */
|
||||||
|
::v-deep .el-table__body-wrapper {
|
||||||
|
overflow-x: hidden !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
::v-deep .el-table td {
|
||||||
|
white-space: pre-wrap !important;
|
||||||
|
word-break: break-all !important;
|
||||||
|
}
|
||||||
|
/* 按钮组定位调整 */
|
||||||
|
.action-buttons {
|
||||||
|
position: static;
|
||||||
|
padding: 15px 0;
|
||||||
|
background: white;
|
||||||
|
box-shadow: 0 -2px 12px rgba(0,0,0,0.05);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 输入框自适应 */
|
||||||
|
::v-deep .el-input__inner,
|
||||||
|
::v-deep .el-textarea__inner {
|
||||||
|
width: 100% !important;
|
||||||
|
min-width: 120px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 音频输入框特殊处理 */
|
||||||
|
.audio-input ::v-deep .el-input__inner {
|
||||||
|
min-width: 200px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 操作按钮弹性布局 */
|
||||||
|
::v-deep .el-table__row .el-button {
|
||||||
|
flex-shrink: 0;
|
||||||
|
margin: 2px !important;
|
||||||
|
}
|
||||||
|
|
||||||
</style>
|
</style>
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
<template>
|
||||||
|
<div class="copyright">
|
||||||
|
©2025 xiaozhi-esp32-server v{{ version }}
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import { mapState } from 'vuex';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'VersionFooter',
|
||||||
|
computed: {
|
||||||
|
...mapState({
|
||||||
|
version: state => state.pubConfig.version
|
||||||
|
})
|
||||||
|
},
|
||||||
|
mounted() {
|
||||||
|
this.$store.dispatch('fetchPubConfig');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped></style>
|
||||||
@@ -6,11 +6,16 @@ import App from './App.vue';
|
|||||||
import router from './router';
|
import router from './router';
|
||||||
import store from './store';
|
import store from './store';
|
||||||
import './styles/global.scss';
|
import './styles/global.scss';
|
||||||
|
import { register as registerServiceWorker } from './registerServiceWorker';
|
||||||
|
|
||||||
Vue.use(ElementUI);
|
Vue.use(ElementUI);
|
||||||
|
|
||||||
Vue.config.productionTip = false
|
Vue.config.productionTip = false
|
||||||
|
|
||||||
|
// 注册Service Worker
|
||||||
|
registerServiceWorker();
|
||||||
|
|
||||||
|
// 创建Vue实例
|
||||||
new Vue({
|
new Vue({
|
||||||
router,
|
router,
|
||||||
store,
|
store,
|
||||||
|
|||||||
@@ -0,0 +1,113 @@
|
|||||||
|
/* eslint-disable no-console */
|
||||||
|
|
||||||
|
export const register = () => {
|
||||||
|
if (process.env.NODE_ENV === 'production' && 'serviceWorker' in navigator) {
|
||||||
|
window.addEventListener('load', () => {
|
||||||
|
const swUrl = `${process.env.BASE_URL}service-worker.js`;
|
||||||
|
|
||||||
|
console.info(`[小智服务] 正在尝试注册Service Worker,URL: ${swUrl}`);
|
||||||
|
|
||||||
|
// 先检查Service Worker是否已注册
|
||||||
|
navigator.serviceWorker.getRegistrations().then(registrations => {
|
||||||
|
if (registrations.length > 0) {
|
||||||
|
console.info('[小智服务] 发现已有Service Worker注册,正在检查更新');
|
||||||
|
}
|
||||||
|
|
||||||
|
// 继续注册Service Worker
|
||||||
|
navigator.serviceWorker
|
||||||
|
.register(swUrl)
|
||||||
|
.then(registration => {
|
||||||
|
console.info('[小智服务] Service Worker注册成功');
|
||||||
|
|
||||||
|
// 更新处理
|
||||||
|
registration.onupdatefound = () => {
|
||||||
|
const installingWorker = registration.installing;
|
||||||
|
if (installingWorker == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
installingWorker.onstatechange = () => {
|
||||||
|
if (installingWorker.state === 'installed') {
|
||||||
|
if (navigator.serviceWorker.controller) {
|
||||||
|
// 内容已缓存更新,通知用户刷新
|
||||||
|
console.log('[小智服务] 新内容可用,请刷新页面');
|
||||||
|
// 可以在这里展示更新提示
|
||||||
|
const updateNotification = document.createElement('div');
|
||||||
|
updateNotification.style.cssText = `
|
||||||
|
position: fixed;
|
||||||
|
bottom: 20px;
|
||||||
|
right: 20px;
|
||||||
|
background: #409EFF;
|
||||||
|
color: white;
|
||||||
|
padding: 12px 20px;
|
||||||
|
border-radius: 4px;
|
||||||
|
box-shadow: 0 2px 12px 0 rgba(0,0,0,.1);
|
||||||
|
z-index: 9999;
|
||||||
|
`;
|
||||||
|
updateNotification.innerHTML = `
|
||||||
|
<div style="display: flex; align-items: center;">
|
||||||
|
<span style="margin-right: 10px;">发现新版本,点击刷新应用</span>
|
||||||
|
<button style="background: white; color: #409EFF; border: none; padding: 5px 10px; border-radius: 3px; cursor: pointer;">刷新</button>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
document.body.appendChild(updateNotification);
|
||||||
|
updateNotification.querySelector('button').addEventListener('click', () => {
|
||||||
|
window.location.reload();
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
// 一切正常,Service Worker已成功安装
|
||||||
|
console.log('[小智服务] 内容已缓存供离线使用');
|
||||||
|
|
||||||
|
// 可以在这里初始化缓存
|
||||||
|
setTimeout(() => {
|
||||||
|
// 预热CDN缓存
|
||||||
|
const cdnUrls = [
|
||||||
|
'https://unpkg.com/element-ui@2.15.14/lib/theme-chalk/index.css',
|
||||||
|
'https://cdnjs.cloudflare.com/ajax/libs/normalize/8.0.1/normalize.min.css',
|
||||||
|
'https://unpkg.com/vue@2.6.14/dist/vue.min.js',
|
||||||
|
'https://unpkg.com/vue-router@3.6.5/dist/vue-router.min.js',
|
||||||
|
'https://unpkg.com/vuex@3.6.2/dist/vuex.min.js',
|
||||||
|
'https://unpkg.com/element-ui@2.15.14/lib/index.js',
|
||||||
|
'https://unpkg.com/axios@0.27.2/dist/axios.min.js',
|
||||||
|
'https://unpkg.com/opus-decoder@0.7.7/dist/opus-decoder.min.js'
|
||||||
|
];
|
||||||
|
|
||||||
|
// 预热缓存
|
||||||
|
cdnUrls.forEach(url => {
|
||||||
|
fetch(url, { mode: 'no-cors' }).catch(err => {
|
||||||
|
console.log(`预热缓存 ${url} 失败`, err);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}, 2000);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
};
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
console.error('Service Worker 注册失败:', error);
|
||||||
|
|
||||||
|
if (error.name === 'TypeError' && error.message.includes('Failed to register a ServiceWorker')) {
|
||||||
|
console.warn('[小智服务] 注册Service Worker时出现网络错误,CDN资源可能无法缓存');
|
||||||
|
if (process.env.NODE_ENV === 'production') {
|
||||||
|
console.info(
|
||||||
|
'可能原因:1. 服务器未配置正确的MIME类型 2. 服务器SSL证书问题 3. 服务器未返回service-worker.js文件'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const unregister = () => {
|
||||||
|
if ('serviceWorker' in navigator) {
|
||||||
|
navigator.serviceWorker.ready
|
||||||
|
.then(registration => {
|
||||||
|
registration.unregister();
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
console.error(error.message);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -62,13 +62,6 @@ const routes = [
|
|||||||
return import('../views/ModelConfig.vue')
|
return import('../views/ModelConfig.vue')
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
|
||||||
path: '/test',
|
|
||||||
name: 'TestServer',
|
|
||||||
component: function () {
|
|
||||||
return import('../views/test.vue')
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
path: '/params-management',
|
path: '/params-management',
|
||||||
name: 'ParamsManagement',
|
name: 'ParamsManagement',
|
||||||
@@ -80,6 +73,17 @@ const routes = [
|
|||||||
title: '参数管理'
|
title: '参数管理'
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: '/ota-management',
|
||||||
|
name: 'OtaManagement',
|
||||||
|
component: function () {
|
||||||
|
return import('../views/OtaManagement.vue')
|
||||||
|
},
|
||||||
|
meta: {
|
||||||
|
requiresAuth: true,
|
||||||
|
title: 'OTA管理'
|
||||||
|
}
|
||||||
|
},
|
||||||
]
|
]
|
||||||
|
|
||||||
const router = new VueRouter({
|
const router = new VueRouter({
|
||||||
|
|||||||
@@ -0,0 +1,174 @@
|
|||||||
|
/* global self, workbox */
|
||||||
|
|
||||||
|
// 自定义Service Worker安装和激活的处理逻辑
|
||||||
|
self.addEventListener('message', (event) => {
|
||||||
|
if (event.data && event.data.type === 'SKIP_WAITING') {
|
||||||
|
self.skipWaiting();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// CDN资源列表
|
||||||
|
const CDN_CSS = [
|
||||||
|
'https://unpkg.com/element-ui@2.15.14/lib/theme-chalk/index.css',
|
||||||
|
'https://cdnjs.cloudflare.com/ajax/libs/normalize/8.0.1/normalize.min.css'
|
||||||
|
];
|
||||||
|
|
||||||
|
const CDN_JS = [
|
||||||
|
'https://unpkg.com/vue@2.6.14/dist/vue.min.js',
|
||||||
|
'https://unpkg.com/vue-router@3.6.5/dist/vue-router.min.js',
|
||||||
|
'https://unpkg.com/vuex@3.6.2/dist/vuex.min.js',
|
||||||
|
'https://unpkg.com/element-ui@2.15.14/lib/index.js',
|
||||||
|
'https://unpkg.com/axios@0.27.2/dist/axios.min.js',
|
||||||
|
'https://unpkg.com/opus-decoder@0.7.7/dist/opus-decoder.min.js'
|
||||||
|
];
|
||||||
|
|
||||||
|
// 当Service Worker被注入manifest后会自动执行
|
||||||
|
const manifest = self.__WB_MANIFEST || [];
|
||||||
|
|
||||||
|
// 检查是否启用CDN模式
|
||||||
|
const isCDNEnabled = manifest.some(entry =>
|
||||||
|
entry.url === 'cdn-mode' && entry.revision === 'enabled'
|
||||||
|
);
|
||||||
|
|
||||||
|
console.log(`Service Worker 已初始化, CDN模式: ${isCDNEnabled ? '启用' : '禁用'}`);
|
||||||
|
|
||||||
|
// 注入workbox相关代码
|
||||||
|
importScripts('https://storage.googleapis.com/workbox-cdn/releases/7.0.0/workbox-sw.js');
|
||||||
|
workbox.setConfig({ debug: false });
|
||||||
|
|
||||||
|
// 开启workbox
|
||||||
|
workbox.core.skipWaiting();
|
||||||
|
workbox.core.clientsClaim();
|
||||||
|
|
||||||
|
// 预缓存离线页面
|
||||||
|
const OFFLINE_URL = '/offline.html';
|
||||||
|
workbox.precaching.precacheAndRoute([
|
||||||
|
{ url: OFFLINE_URL, revision: null }
|
||||||
|
]);
|
||||||
|
|
||||||
|
// 添加安装完成事件处理器,在控制台显示安装消息
|
||||||
|
self.addEventListener('install', event => {
|
||||||
|
if (isCDNEnabled) {
|
||||||
|
console.log('Service Worker 已安装,开始缓存CDN资源');
|
||||||
|
} else {
|
||||||
|
console.log('Service Worker 已安装,CDN模式禁用,仅缓存本地资源');
|
||||||
|
}
|
||||||
|
|
||||||
|
// 确保离线页面被缓存
|
||||||
|
event.waitUntil(
|
||||||
|
caches.open('offline-cache').then((cache) => {
|
||||||
|
return cache.add(OFFLINE_URL);
|
||||||
|
})
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
// 添加激活事件处理器
|
||||||
|
self.addEventListener('activate', event => {
|
||||||
|
console.log('Service Worker 已激活,现在控制着页面');
|
||||||
|
|
||||||
|
// 清理旧版本缓存
|
||||||
|
event.waitUntil(
|
||||||
|
caches.keys().then(cacheNames => {
|
||||||
|
return Promise.all(
|
||||||
|
cacheNames.filter(cacheName => {
|
||||||
|
// 清理除当前版本外的缓存
|
||||||
|
return cacheName.startsWith('workbox-') && !workbox.core.cacheNames.runtime.includes(cacheName);
|
||||||
|
}).map(cacheName => {
|
||||||
|
return caches.delete(cacheName);
|
||||||
|
})
|
||||||
|
);
|
||||||
|
})
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
// 添加fetch事件拦截器,用于查看CDN资源是否命中缓存
|
||||||
|
self.addEventListener('fetch', event => {
|
||||||
|
// 只有启用CDN模式时才进行CDN资源缓存监控
|
||||||
|
if (isCDNEnabled) {
|
||||||
|
const url = new URL(event.request.url);
|
||||||
|
|
||||||
|
// 针对CDN资源,输出是否命中缓存的信息
|
||||||
|
if ([...CDN_CSS, ...CDN_JS].includes(url.href)) {
|
||||||
|
// 不干扰正常的fetch流程,只添加日志
|
||||||
|
console.log(`请求CDN资源: ${url.href}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// 仅在CDN模式下缓存CDN资源
|
||||||
|
if (isCDNEnabled) {
|
||||||
|
// 缓存CDN的CSS资源
|
||||||
|
workbox.routing.registerRoute(
|
||||||
|
({ url }) => CDN_CSS.includes(url.href),
|
||||||
|
new workbox.strategies.CacheFirst({
|
||||||
|
cacheName: 'cdn-stylesheets',
|
||||||
|
plugins: [
|
||||||
|
new workbox.expiration.ExpirationPlugin({
|
||||||
|
maxAgeSeconds: 365 * 24 * 60 * 60, // 增加到1年缓存
|
||||||
|
maxEntries: 10, // 最多缓存10个CSS文件
|
||||||
|
}),
|
||||||
|
new workbox.cacheableResponse.CacheableResponsePlugin({
|
||||||
|
statuses: [0, 200], // 缓存成功响应
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
// 缓存CDN的JS资源
|
||||||
|
workbox.routing.registerRoute(
|
||||||
|
({ url }) => CDN_JS.includes(url.href),
|
||||||
|
new workbox.strategies.CacheFirst({
|
||||||
|
cacheName: 'cdn-scripts',
|
||||||
|
plugins: [
|
||||||
|
new workbox.expiration.ExpirationPlugin({
|
||||||
|
maxAgeSeconds: 365 * 24 * 60 * 60, // 增加到1年缓存
|
||||||
|
maxEntries: 20, // 最多缓存20个JS文件
|
||||||
|
}),
|
||||||
|
new workbox.cacheableResponse.CacheableResponsePlugin({
|
||||||
|
statuses: [0, 200], // 缓存成功响应
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 无论是否启用CDN模式,都缓存本地静态资源
|
||||||
|
workbox.routing.registerRoute(
|
||||||
|
/\.(?:js|css|png|jpg|jpeg|svg|gif|ico|woff|woff2|eot|ttf|otf)$/,
|
||||||
|
new workbox.strategies.StaleWhileRevalidate({
|
||||||
|
cacheName: 'static-resources',
|
||||||
|
plugins: [
|
||||||
|
new workbox.expiration.ExpirationPlugin({
|
||||||
|
maxAgeSeconds: 7 * 24 * 60 * 60, // 7天缓存
|
||||||
|
maxEntries: 50, // 最多缓存50个文件
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
// 缓存HTML页面
|
||||||
|
workbox.routing.registerRoute(
|
||||||
|
/\.html$/,
|
||||||
|
new workbox.strategies.NetworkFirst({
|
||||||
|
cacheName: 'html-cache',
|
||||||
|
plugins: [
|
||||||
|
new workbox.expiration.ExpirationPlugin({
|
||||||
|
maxAgeSeconds: 1 * 24 * 60 * 60, // 1天缓存
|
||||||
|
maxEntries: 10, // 最多缓存10个HTML文件
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
// 离线页面 - 使用更可靠的处理方式
|
||||||
|
workbox.routing.setCatchHandler(async ({ event }) => {
|
||||||
|
// 根据请求类型返回适当的默认页面
|
||||||
|
switch (event.request.destination) {
|
||||||
|
case 'document':
|
||||||
|
// 如果是网页请求,返回离线页面
|
||||||
|
return caches.match(OFFLINE_URL);
|
||||||
|
default:
|
||||||
|
// 所有其他请求返回错误
|
||||||
|
return Response.error();
|
||||||
|
}
|
||||||
|
});
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
import { goToPage } from "@/utils";
|
import { goToPage } from "@/utils";
|
||||||
import Vue from 'vue';
|
import Vue from 'vue';
|
||||||
import Vuex from 'vuex';
|
import Vuex from 'vuex';
|
||||||
|
import Api from '../apis/api';
|
||||||
import Constant from '../utils/constant';
|
import Constant from '../utils/constant';
|
||||||
|
|
||||||
Vue.use(Vuex)
|
Vue.use(Vuex)
|
||||||
@@ -9,7 +10,11 @@ export default new Vuex.Store({
|
|||||||
state: {
|
state: {
|
||||||
token: '',
|
token: '',
|
||||||
userInfo: {}, // 添加用户信息存储
|
userInfo: {}, // 添加用户信息存储
|
||||||
isSuperAdmin: false // 添加superAdmin状态
|
isSuperAdmin: false, // 添加superAdmin状态
|
||||||
|
pubConfig: { // 添加公共配置存储
|
||||||
|
version: '',
|
||||||
|
allowUserRegister: false
|
||||||
|
}
|
||||||
},
|
},
|
||||||
getters: {
|
getters: {
|
||||||
getToken(state) {
|
getToken(state) {
|
||||||
@@ -26,6 +31,9 @@ export default new Vuex.Store({
|
|||||||
return state.isSuperAdmin
|
return state.isSuperAdmin
|
||||||
}
|
}
|
||||||
return localStorage.getItem('isSuperAdmin') === 'true'
|
return localStorage.getItem('isSuperAdmin') === 'true'
|
||||||
|
},
|
||||||
|
getPubConfig(state) {
|
||||||
|
return state.pubConfig
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
mutations: {
|
mutations: {
|
||||||
@@ -39,6 +47,9 @@ export default new Vuex.Store({
|
|||||||
state.isSuperAdmin = isSuperAdmin
|
state.isSuperAdmin = isSuperAdmin
|
||||||
localStorage.setItem('isSuperAdmin', isSuperAdmin)
|
localStorage.setItem('isSuperAdmin', isSuperAdmin)
|
||||||
},
|
},
|
||||||
|
setPubConfig(state, config) {
|
||||||
|
state.pubConfig = config
|
||||||
|
},
|
||||||
clearAuth(state) {
|
clearAuth(state) {
|
||||||
state.token = ''
|
state.token = ''
|
||||||
state.userInfo = {}
|
state.userInfo = {}
|
||||||
@@ -55,6 +66,17 @@ export default new Vuex.Store({
|
|||||||
goToPage(Constant.PAGE.LOGIN, true);
|
goToPage(Constant.PAGE.LOGIN, true);
|
||||||
window.location.reload(); // 彻底重置状态
|
window.location.reload(); // 彻底重置状态
|
||||||
})
|
})
|
||||||
|
},
|
||||||
|
// 添加获取公共配置的 action
|
||||||
|
fetchPubConfig({ commit }) {
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
Api.user.getPubConfig(({ data }) => {
|
||||||
|
if (data.code === 0) {
|
||||||
|
commit('setPubConfig', data.data);
|
||||||
|
}
|
||||||
|
resolve();
|
||||||
|
});
|
||||||
|
});
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
modules: {
|
modules: {
|
||||||
|
|||||||
@@ -0,0 +1,142 @@
|
|||||||
|
/**
|
||||||
|
* 缓存查看工具 - 用于检查CDN资源是否已被Service Worker缓存
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取所有Service Worker缓存的名称
|
||||||
|
* @returns {Promise<string[]>} 缓存名称列表
|
||||||
|
*/
|
||||||
|
export const getCacheNames = async () => {
|
||||||
|
if (!('caches' in window)) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
return await caches.keys();
|
||||||
|
} catch (error) {
|
||||||
|
console.error('获取缓存名称失败:', error);
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取指定缓存中的所有URL
|
||||||
|
* @param {string} cacheName 缓存名称
|
||||||
|
* @returns {Promise<string[]>} 缓存的URL列表
|
||||||
|
*/
|
||||||
|
export const getCacheUrls = async (cacheName) => {
|
||||||
|
if (!('caches' in window)) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const cache = await caches.open(cacheName);
|
||||||
|
const requests = await cache.keys();
|
||||||
|
return requests.map(request => request.url);
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`获取缓存 ${cacheName} 的URL失败:`, error);
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 检查特定URL是否已被缓存
|
||||||
|
* @param {string} url 要检查的URL
|
||||||
|
* @returns {Promise<boolean>} 是否已缓存
|
||||||
|
*/
|
||||||
|
export const isUrlCached = async (url) => {
|
||||||
|
if (!('caches' in window)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const cacheNames = await getCacheNames();
|
||||||
|
for (const cacheName of cacheNames) {
|
||||||
|
const cache = await caches.open(cacheName);
|
||||||
|
const match = await cache.match(url);
|
||||||
|
if (match) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`检查URL ${url} 是否缓存失败:`, error);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取当前页面所有CDN资源的缓存状态
|
||||||
|
* @returns {Promise<Object>} 缓存状态对象
|
||||||
|
*/
|
||||||
|
export const checkCdnCacheStatus = async () => {
|
||||||
|
// 从CDN缓存中查找资源
|
||||||
|
const cdnCaches = ['cdn-stylesheets', 'cdn-scripts'];
|
||||||
|
const results = {
|
||||||
|
css: [],
|
||||||
|
js: [],
|
||||||
|
totalCached: 0,
|
||||||
|
totalNotCached: 0
|
||||||
|
};
|
||||||
|
|
||||||
|
for (const cacheName of cdnCaches) {
|
||||||
|
try {
|
||||||
|
const urls = await getCacheUrls(cacheName);
|
||||||
|
|
||||||
|
// 区分CSS和JS资源
|
||||||
|
for (const url of urls) {
|
||||||
|
if (url.endsWith('.css')) {
|
||||||
|
results.css.push({ url, cached: true });
|
||||||
|
} else if (url.endsWith('.js')) {
|
||||||
|
results.js.push({ url, cached: true });
|
||||||
|
}
|
||||||
|
results.totalCached++;
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`获取 ${cacheName} 缓存信息失败:`, error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return results;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 清除所有Service Worker缓存
|
||||||
|
* @returns {Promise<boolean>} 是否成功清除
|
||||||
|
*/
|
||||||
|
export const clearAllCaches = async () => {
|
||||||
|
if (!('caches' in window)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const cacheNames = await getCacheNames();
|
||||||
|
for (const cacheName of cacheNames) {
|
||||||
|
await caches.delete(cacheName);
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('清除所有缓存失败:', error);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 将缓存状态输出到控制台
|
||||||
|
*/
|
||||||
|
export const logCacheStatus = async () => {
|
||||||
|
console.group('Service Worker 缓存状态');
|
||||||
|
|
||||||
|
const cacheNames = await getCacheNames();
|
||||||
|
console.log('已发现的缓存:', cacheNames);
|
||||||
|
|
||||||
|
for (const cacheName of cacheNames) {
|
||||||
|
const urls = await getCacheUrls(cacheName);
|
||||||
|
console.group(`缓存: ${cacheName} (${urls.length} 项)`);
|
||||||
|
urls.forEach(url => console.log(url));
|
||||||
|
console.groupEnd();
|
||||||
|
}
|
||||||
|
|
||||||
|
console.groupEnd();
|
||||||
|
return cacheNames.length > 0;
|
||||||
|
};
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
// 日期格式化函数
|
||||||
|
export function formatDate(date) {
|
||||||
|
if (!date) return '';
|
||||||
|
const d = new Date(date);
|
||||||
|
const year = d.getFullYear();
|
||||||
|
const month = String(d.getMonth() + 1).padStart(2, '0');
|
||||||
|
const day = String(d.getDate()).padStart(2, '0');
|
||||||
|
const hour = String(d.getHours()).padStart(2, '0');
|
||||||
|
const minute = String(d.getMinutes()).padStart(2, '0');
|
||||||
|
const second = String(d.getSeconds()).padStart(2, '0');
|
||||||
|
return `${year}-${month}-${day} ${hour}:${minute}:${second}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 文件大小格式化函数
|
||||||
|
export function formatFileSize(bytes) {
|
||||||
|
if (!bytes || bytes === 0) return '0 B';
|
||||||
|
const units = ['B', 'KB', 'MB', 'GB', 'TB'];
|
||||||
|
const k = 1024;
|
||||||
|
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
||||||
|
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + units[i];
|
||||||
|
}
|
||||||
@@ -136,3 +136,63 @@ export function getUUID() {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 固件类型选项
|
||||||
|
*/
|
||||||
|
export const FIRMWARE_TYPES = [
|
||||||
|
{ "key": "bread-compact-wifi", "name": "面包板新版接线(WiFi)" },
|
||||||
|
{ "key": "bread-compact-wifi-lcd", "name": "面包板新版接线(WiFi)+ LCD" },
|
||||||
|
{ "key": "bread-compact-ml307", "name": "面包板新版接线(ML307 AT)" },
|
||||||
|
{ "key": "bread-compact-esp32", "name": "面包板(WiFi) ESP32 DevKit" },
|
||||||
|
{ "key": "bread-compact-esp32-lcd", "name": "面包板(WiFi+ LCD) ESP32 DevKit" },
|
||||||
|
{ "key": "df-k10", "name": "DFRobot 行空板 k10" },
|
||||||
|
{ "key": "esp32-cgc", "name": "ESP32 CGC" },
|
||||||
|
{ "key": "esp-box-3", "name": "ESP BOX 3" },
|
||||||
|
{ "key": "esp-box", "name": "ESP BOX" },
|
||||||
|
{ "key": "esp-box-lite", "name": "ESP BOX Lite" },
|
||||||
|
{ "key": "kevin-box-1", "name": "Kevin Box 1" },
|
||||||
|
{ "key": "kevin-box-2", "name": "Kevin Box 2" },
|
||||||
|
{ "key": "kevin-c3", "name": "Kevin C3" },
|
||||||
|
{ "key": "kevin-sp-v3-dev", "name": "Kevin SP V3开发板" },
|
||||||
|
{ "key": "kevin-sp-v4-dev", "name": "Kevin SP V4开发板" },
|
||||||
|
{ "key": "kevin-yuying-313lcd", "name": "鱼鹰科技3.13LCD开发板" },
|
||||||
|
{ "key": "lichuang-dev", "name": "立创·实战派ESP32-S3开发板" },
|
||||||
|
{ "key": "lichuang-c3-dev", "name": "立创·实战派ESP32-C3开发板" },
|
||||||
|
{ "key": "magiclick-2p4", "name": "神奇按钮 Magiclick_2.4" },
|
||||||
|
{ "key": "magiclick-2p5", "name": "神奇按钮 Magiclick_2.5" },
|
||||||
|
{ "key": "magiclick-c3", "name": "神奇按钮 Magiclick_C3" },
|
||||||
|
{ "key": "magiclick-c3-v2", "name": "神奇按钮 Magiclick_C3_v2" },
|
||||||
|
{ "key": "m5stack-core-s3", "name": "M5Stack CoreS3" },
|
||||||
|
{ "key": "atoms3-echo-base", "name": "AtomS3 + Echo Base" },
|
||||||
|
{ "key": "atoms3r-echo-base", "name": "AtomS3R + Echo Base" },
|
||||||
|
{ "key": "atoms3r-cam-m12-echo-base", "name": "AtomS3R CAM/M12 + Echo Base" },
|
||||||
|
{ "key": "atommatrix-echo-base", "name": "AtomMatrix + Echo Base" },
|
||||||
|
{ "key": "xmini-c3", "name": "虾哥 Mini C3" },
|
||||||
|
{ "key": "esp32s3-korvo2-v3", "name": "ESP32S3_KORVO2_V3开发板" },
|
||||||
|
{ "key": "esp-sparkbot", "name": "ESP-SparkBot开发板" },
|
||||||
|
{ "key": "esp-spot-s3", "name": "ESP-Spot-S3" },
|
||||||
|
{ "key": "esp32-s3-touch-amoled-1.8", "name": "Waveshare ESP32-S3-Touch-AMOLED-1.8" },
|
||||||
|
{ "key": "esp32-s3-touch-lcd-1.85c", "name": "Waveshare ESP32-S3-Touch-LCD-1.85C" },
|
||||||
|
{ "key": "esp32-s3-touch-lcd-1.85", "name": "Waveshare ESP32-S3-Touch-LCD-1.85" },
|
||||||
|
{ "key": "esp32-s3-touch-lcd-1.46", "name": "Waveshare ESP32-S3-Touch-LCD-1.46" },
|
||||||
|
{ "key": "esp32-s3-touch-lcd-3.5", "name": "Waveshare ESP32-S3-Touch-LCD-3.5" },
|
||||||
|
{ "key": "tudouzi", "name": "土豆子" },
|
||||||
|
{ "key": "lilygo-t-circle-s3", "name": "LILYGO T-Circle-S3" },
|
||||||
|
{ "key": "lilygo-t-cameraplus-s3", "name": "LILYGO T-CameraPlus-S3" },
|
||||||
|
{ "key": "movecall-moji-esp32s3", "name": "Movecall Moji 小智AI衍生版" },
|
||||||
|
{ "key": "movecall-cuican-esp32s3", "name": "Movecall CuiCan 璀璨·AI吊坠" },
|
||||||
|
{ "key": "atk-dnesp32s3", "name": "正点原子DNESP32S3开发板" },
|
||||||
|
{ "key": "atk-dnesp32s3-box", "name": "正点原子DNESP32S3-BOX" },
|
||||||
|
{ "key": "du-chatx", "name": "嘟嘟开发板CHATX(wifi)" },
|
||||||
|
{ "key": "taiji-pi-s3", "name": "太极小派esp32s3" },
|
||||||
|
{ "key": "xingzhi-cube-0.85tft-wifi", "name": "无名科技星智0.85(WIFI)" },
|
||||||
|
{ "key": "xingzhi-cube-0.85tft-ml307", "name": "无名科技星智0.85(ML307)" },
|
||||||
|
{ "key": "xingzhi-cube-0.96oled-wifi", "name": "无名科技星智0.96(WIFI)" },
|
||||||
|
{ "key": "xingzhi-cube-0.96oled-ml307", "name": "无名科技星智0.96(ML307)" },
|
||||||
|
{ "key": "xingzhi-cube-1.54tft-wifi", "name": "无名科技星智1.54(WIFI)" },
|
||||||
|
{ "key": "xingzhi-cube-1.54tft-ml307", "name": "无名科技星智1.54(ML307)" },
|
||||||
|
{ "key": "sensecap-watcher", "name": "SenseCAP Watcher" },
|
||||||
|
{ "key": "doit-s3-aibox", "name": "四博智联AI陪伴盒子" },
|
||||||
|
{ "key": "mixgo-nova", "name": "元控·青春" }
|
||||||
|
]
|
||||||
|
|
||||||
|
|||||||
@@ -1,54 +1,104 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="welcome">
|
<div class="welcome">
|
||||||
<HeaderBar />
|
<HeaderBar />
|
||||||
<el-main style="padding: 20px; display: flex; flex-direction: column;">
|
|
||||||
<div class="table-container">
|
<div class="operation-bar">
|
||||||
<h3 class="device-list-title">设备列表</h3>
|
<h2 class="page-title">设备管理</h2>
|
||||||
<el-button type="primary" class="add-device-btn" @click="handleAddDevice">
|
<div class="right-operations">
|
||||||
+ 添加设备
|
<el-input placeholder="请输入设备型号或Mac地址查询" v-model="searchKeyword" class="search-input"
|
||||||
</el-button>
|
@keyup.enter.native="handleSearch" clearable />
|
||||||
<el-table :data="paginatedDeviceList" style="width: 100%; margin-top: 20px" border stripe>
|
<el-button class="btn-search" @click="handleSearch">搜索</el-button>
|
||||||
<el-table-column label="设备型号" prop="model" flex></el-table-column>
|
|
||||||
<el-table-column label="固件版本" prop="firmwareVersion" width="120"></el-table-column>
|
|
||||||
<el-table-column label="Mac地址" prop="macAddress"></el-table-column>
|
|
||||||
<el-table-column label="绑定时间" prop="bindTime" width="200"></el-table-column>
|
|
||||||
<el-table-column label="最近对话" prop="lastConversation" width="140"></el-table-column>
|
|
||||||
<el-table-column label="备注" width="180">
|
|
||||||
<template slot-scope="scope">
|
|
||||||
<el-input v-if="scope.row.isEdit" v-model="scope.row.remark" size="mini"
|
|
||||||
@blur="stopEditRemark(scope.$index)"></el-input>
|
|
||||||
<span v-else>
|
|
||||||
<i v-if="!scope.row.remark" class="el-icon-edit" @click="startEditRemark(scope.$index, scope.row)"></i>
|
|
||||||
<span v-else @click="startEditRemark(scope.$index, scope.row)">
|
|
||||||
{{ scope.row.remark }}
|
|
||||||
</span>
|
|
||||||
</span>
|
|
||||||
</template>
|
|
||||||
</el-table-column>
|
|
||||||
<el-table-column label="OTA升级" width="120">
|
|
||||||
<template slot-scope="scope">
|
|
||||||
<el-switch v-model="scope.row.otaSwitch" size="mini" active-color="#13ce66"
|
|
||||||
inactive-color="#ff4949"></el-switch>
|
|
||||||
</template>
|
|
||||||
</el-table-column>
|
|
||||||
<el-table-column label="操作" width="80">
|
|
||||||
<template slot-scope="scope">
|
|
||||||
<el-button size="mini" type="text" @click="handleUnbind(scope.row.device_id)" style="color: #ff4949">
|
|
||||||
解绑
|
|
||||||
</el-button>
|
|
||||||
</template>
|
|
||||||
</el-table-column>
|
|
||||||
</el-table>
|
|
||||||
<el-pagination class="pagination" @size-change="handleSizeChange" @current-change="handleCurrentChange"
|
|
||||||
:current-page="currentPage" :page-sizes="[5, 10, 20, 50]" :page-size="pageSize"
|
|
||||||
layout="total, sizes, prev, pager, next, jumper" :total="deviceList.length"></el-pagination>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="copyright">
|
</div>
|
||||||
©2025 xiaozhi-esp32-server
|
|
||||||
|
<div class="main-wrapper">
|
||||||
|
<div class="content-panel">
|
||||||
|
<div class="content-area">
|
||||||
|
<el-card class="device-card" shadow="never">
|
||||||
|
<el-table
|
||||||
|
ref="deviceTable"
|
||||||
|
:data="paginatedDeviceList"
|
||||||
|
class="transparent-table"
|
||||||
|
:header-cell-class-name="headerCellClassName"
|
||||||
|
v-loading="loading"
|
||||||
|
element-loading-text="拼命加载中"
|
||||||
|
element-loading-spinner="el-icon-loading"
|
||||||
|
element-loading-background="rgba(255, 255, 255, 0.7)">
|
||||||
|
<el-table-column label="选择" align="center" width="120">
|
||||||
|
<template slot-scope="scope">
|
||||||
|
<el-checkbox v-model="scope.row.selected"></el-checkbox>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column label="设备型号" prop="model" align="center">
|
||||||
|
<template slot-scope="scope">
|
||||||
|
{{ getFirmwareTypeName(scope.row.model) }}
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column label="固件版本" prop="firmwareVersion" align="center" ></el-table-column>
|
||||||
|
<el-table-column label="Mac地址" prop="macAddress" align="center"></el-table-column>
|
||||||
|
<el-table-column label="绑定时间" prop="bindTime" align="center"></el-table-column>
|
||||||
|
<el-table-column label="最近对话" prop="lastConversation" align="center"></el-table-column>
|
||||||
|
<el-table-column label="备注" align="center">
|
||||||
|
<template slot-scope="scope">
|
||||||
|
<el-input v-if="scope.row.isEdit" v-model="scope.row.remark" size="mini"
|
||||||
|
@blur="stopEditRemark(scope.$index)"></el-input>
|
||||||
|
<span v-else>
|
||||||
|
<i v-if="!scope.row.remark" class="el-icon-edit"
|
||||||
|
@click="startEditRemark(scope.$index, scope.row)"></i>
|
||||||
|
<span v-else @click="startEditRemark(scope.$index, scope.row)">
|
||||||
|
{{ scope.row.remark }}
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column label="OTA升级" align="center">
|
||||||
|
<template slot-scope="scope">
|
||||||
|
<el-switch v-model="scope.row.otaSwitch" size="mini" active-color="#13ce66"
|
||||||
|
inactive-color="#ff4949"></el-switch>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column label="操作" align="center">
|
||||||
|
<template slot-scope="scope">
|
||||||
|
<el-button size="mini" type="text" @click="handleUnbind(scope.row.device_id)">
|
||||||
|
解绑
|
||||||
|
</el-button>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
</el-table>
|
||||||
|
|
||||||
|
<div class="table_bottom">
|
||||||
|
<div class="ctrl_btn">
|
||||||
|
<el-button size="mini" type="primary" class="select-all-btn" @click="handleSelectAll">
|
||||||
|
{{ isAllSelected ? '取消全选' : '全选' }}
|
||||||
|
</el-button>
|
||||||
|
<el-button type="success" size="mini" class="add-device-btn" @click="handleAddDevice">
|
||||||
|
新增
|
||||||
|
</el-button>
|
||||||
|
<el-button size="mini" type="danger" icon="el-icon-delete" @click="deleteSelected">解绑</el-button>
|
||||||
|
</div>
|
||||||
|
<div class="custom-pagination">
|
||||||
|
<el-select v-model="pageSize" @change="handlePageSizeChange" class="page-size-select">
|
||||||
|
<el-option v-for="item in pageSizeOptions" :key="item" :label="`${item}条/页`" :value="item">
|
||||||
|
</el-option>
|
||||||
|
</el-select>
|
||||||
|
<button class="pagination-btn" :disabled="currentPage === 1" @click="goFirst">首页</button>
|
||||||
|
<button class="pagination-btn" :disabled="currentPage === 1" @click="goPrev">上一页</button>
|
||||||
|
<button v-for="page in visiblePages" :key="page" class="pagination-btn"
|
||||||
|
:class="{ active: page === currentPage }" @click="goToPage(page)">
|
||||||
|
{{ page }}
|
||||||
|
</button>
|
||||||
|
<button class="pagination-btn" :disabled="currentPage === pageCount" @click="goNext">下一页</button>
|
||||||
|
<span class="total-text">共{{ deviceList.length }}条记录</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</el-card>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<AddDeviceDialog :visible.sync="addDeviceDialogVisible" :agent-id="currentAgentId"
|
</div>
|
||||||
@refresh="fetchBindDevices(currentAgentId)" />
|
|
||||||
</el-main>
|
<AddDeviceDialog :visible.sync="addDeviceDialogVisible" :agent-id="currentAgentId"
|
||||||
|
@refresh="fetchBindDevices(currentAgentId)" />
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@@ -56,26 +106,62 @@
|
|||||||
import Api from '@/apis/api';
|
import Api from '@/apis/api';
|
||||||
import AddDeviceDialog from "@/components/AddDeviceDialog.vue";
|
import AddDeviceDialog from "@/components/AddDeviceDialog.vue";
|
||||||
import HeaderBar from "@/components/HeaderBar.vue";
|
import HeaderBar from "@/components/HeaderBar.vue";
|
||||||
|
import { FIRMWARE_TYPES } from "@/utils";
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
components: { HeaderBar, AddDeviceDialog },
|
components: { HeaderBar, AddDeviceDialog },
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
addDeviceDialogVisible: false,
|
addDeviceDialogVisible: false,
|
||||||
|
selectedDevices: [],
|
||||||
|
isAllSelected: false,
|
||||||
|
searchKeyword: "",
|
||||||
|
activeSearchKeyword: "",
|
||||||
currentAgentId: this.$route.query.agentId || '',
|
currentAgentId: this.$route.query.agentId || '',
|
||||||
currentPage: 1,
|
currentPage: 1,
|
||||||
pageSize: 5,
|
pageSize: 10,
|
||||||
|
pageSizeOptions: [10, 20, 50, 100],
|
||||||
deviceList: [],
|
deviceList: [],
|
||||||
loading: false,
|
loading: false,
|
||||||
userApi: null,
|
userApi: null,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
|
filteredDeviceList() {
|
||||||
|
const keyword = this.activeSearchKeyword.toLowerCase();
|
||||||
|
if (!keyword) return this.deviceList;
|
||||||
|
return this.deviceList.filter(device =>
|
||||||
|
(device.model && device.model.toLowerCase().includes(keyword)) ||
|
||||||
|
(device.macAddress && device.macAddress.toLowerCase().includes(keyword))
|
||||||
|
);
|
||||||
|
},
|
||||||
|
|
||||||
paginatedDeviceList() {
|
paginatedDeviceList() {
|
||||||
const start = (this.currentPage - 1) * this.pageSize;
|
const start = (this.currentPage - 1) * this.pageSize;
|
||||||
const end = start + this.pageSize;
|
const end = start + this.pageSize;
|
||||||
return this.deviceList.slice(start, end);
|
return this.filteredDeviceList.slice(start, end).map(item => ({
|
||||||
}
|
...item,
|
||||||
|
selected: false
|
||||||
|
}));
|
||||||
|
},
|
||||||
|
pageCount() {
|
||||||
|
return Math.ceil(this.filteredDeviceList.length / this.pageSize);
|
||||||
|
},
|
||||||
|
visiblePages() {
|
||||||
|
const pages = [];
|
||||||
|
const maxVisible = 3;
|
||||||
|
let start = Math.max(1, this.currentPage - 1);
|
||||||
|
let end = Math.min(this.pageCount, start + maxVisible - 1);
|
||||||
|
|
||||||
|
if (end - start + 1 < maxVisible) {
|
||||||
|
start = Math.max(1, end - maxVisible + 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (let i = start; i <= end; i++) {
|
||||||
|
pages.push(i);
|
||||||
|
}
|
||||||
|
return pages;
|
||||||
|
},
|
||||||
},
|
},
|
||||||
mounted() {
|
mounted() {
|
||||||
const agentId = this.$route.query.agentId;
|
const agentId = this.$route.query.agentId;
|
||||||
@@ -84,6 +170,74 @@ export default {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
|
handlePageSizeChange(val) {
|
||||||
|
this.pageSize = val;
|
||||||
|
this.currentPage = 1;
|
||||||
|
},
|
||||||
|
handleSearch() {
|
||||||
|
this.activeSearchKeyword = this.searchKeyword;
|
||||||
|
this.currentPage = 1;
|
||||||
|
},
|
||||||
|
|
||||||
|
handleSelectAll() {
|
||||||
|
this.isAllSelected = !this.isAllSelected;
|
||||||
|
this.paginatedDeviceList.forEach(row => {
|
||||||
|
row.selected = this.isAllSelected;
|
||||||
|
});
|
||||||
|
this.selectedDevices = this.paginatedDeviceList.filter(device => device.selected);
|
||||||
|
},
|
||||||
|
|
||||||
|
deleteSelected() {
|
||||||
|
this.selectedDevices = this.paginatedDeviceList.filter(device => device.selected);
|
||||||
|
if (this.selectedDevices.length === 0) {
|
||||||
|
this.$message.warning({
|
||||||
|
message: '请至少选择一条记录',
|
||||||
|
showClose: true
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.$confirm(`确认要解绑选中的 ${this.selectedDevices.length} 台设备吗?`, '警告', {
|
||||||
|
confirmButtonText: '确定',
|
||||||
|
cancelButtonText: '取消',
|
||||||
|
type: 'warning'
|
||||||
|
}).then(() => {
|
||||||
|
const deviceIds = this.selectedDevices.map(device => device.device_id);
|
||||||
|
this.batchUnbindDevices(deviceIds);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
batchUnbindDevices(deviceIds) {
|
||||||
|
const promises = deviceIds.map(id => {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
Api.device.unbindDevice(id, ({ data }) => {
|
||||||
|
if (data.code === 0) {
|
||||||
|
resolve();
|
||||||
|
} else {
|
||||||
|
reject(data.msg || '解绑失败');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
Promise.all(promises)
|
||||||
|
.then(() => {
|
||||||
|
this.$message.success({
|
||||||
|
message: `成功解绑 ${deviceIds.length} 台设备`,
|
||||||
|
showClose: true
|
||||||
|
});
|
||||||
|
this.fetchBindDevices(this.currentAgentId);
|
||||||
|
this.selectedDevices = [];
|
||||||
|
this.isAllSelected = false;
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
this.$message.error({
|
||||||
|
message: error || '批量解绑过程中出现错误',
|
||||||
|
showClose: true
|
||||||
|
});
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
handleAddDevice() {
|
handleAddDevice() {
|
||||||
this.addDeviceDialogVisible = true;
|
this.addDeviceDialogVisible = true;
|
||||||
},
|
},
|
||||||
@@ -115,46 +269,65 @@ export default {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
handleSizeChange(val) {
|
goFirst() {
|
||||||
this.pageSize = val;
|
this.currentPage = 1;
|
||||||
},
|
},
|
||||||
handleCurrentChange(val) {
|
goPrev() {
|
||||||
this.currentPage = val;
|
if (this.currentPage > 1) this.currentPage--;
|
||||||
},
|
},
|
||||||
|
goNext() {
|
||||||
|
if (this.currentPage < this.pageCount) this.currentPage++;
|
||||||
|
},
|
||||||
|
goToPage(page) {
|
||||||
|
this.currentPage = page;
|
||||||
|
},
|
||||||
|
|
||||||
fetchBindDevices(agentId) {
|
fetchBindDevices(agentId) {
|
||||||
this.loading = true;
|
this.loading = true;
|
||||||
Api.device.getAgentBindDevices(agentId, ({ data }) => {
|
Api.device.getAgentBindDevices(agentId, ({ data }) => {
|
||||||
this.loading = false;
|
this.loading = false;
|
||||||
if (data.code === 0) {
|
if (data.code === 0) {
|
||||||
// 格式化日期并按照绑定时间降序排列
|
|
||||||
this.deviceList = data.data.map(device => {
|
this.deviceList = data.data.map(device => {
|
||||||
// 格式化绑定时间
|
|
||||||
const bindDate = new Date(device.createDate);
|
const bindDate = new Date(device.createDate);
|
||||||
const formattedBindTime = `${bindDate.getFullYear()}-${(bindDate.getMonth() + 1).toString().padStart(2, '0')}-${bindDate.getDate().toString().padStart(2, '0')} ${bindDate.getHours().toString().padStart(2, '0')}:${bindDate.getMinutes().toString().padStart(2, '0')}:${bindDate.getSeconds().toString().padStart(2, '0')}`;
|
const formattedBindTime = `${bindDate.getFullYear()}-${(bindDate.getMonth() + 1).toString().padStart(2, '0')}-${bindDate.getDate().toString().padStart(2, '0')} ${bindDate.getHours().toString().padStart(2, '0')}:${bindDate.getMinutes().toString().padStart(2, '0')}:${bindDate.getSeconds().toString().padStart(2, '0')}`;
|
||||||
|
let formattedLastConversation = '';
|
||||||
|
if (device.lastConnectedAt) {
|
||||||
|
const lastConvoDate = new Date(device.lastConnectedAt);
|
||||||
|
formattedLastConversation = `${lastConvoDate.getFullYear()}-${(lastConvoDate.getMonth() + 1).toString().padStart(2, '0')}-${lastConvoDate.getDate().toString().padStart(2, '0')} ${lastConvoDate.getHours().toString().padStart(2, '0')}:${lastConvoDate.getMinutes().toString().padStart(2, '0')}:${lastConvoDate.getSeconds().toString().padStart(2, '0')}`;
|
||||||
|
}
|
||||||
return {
|
return {
|
||||||
device_id: device.id,
|
device_id: device.id,
|
||||||
model: device.board,
|
model: device.board,
|
||||||
firmwareVersion: device.appVersion,
|
firmwareVersion: device.appVersion,
|
||||||
macAddress: device.macAddress,
|
macAddress: device.macAddress,
|
||||||
bindTime: formattedBindTime, // 使用格式化后的时间
|
bindTime: formattedBindTime,
|
||||||
lastConversation: device.lastConnectedAt,
|
lastConversation: formattedLastConversation,
|
||||||
remark: device.alias,
|
remark: device.alias,
|
||||||
isEdit: false,
|
isEdit: false,
|
||||||
otaSwitch: device.autoUpdate === 1,
|
otaSwitch: device.autoUpdate === 1,
|
||||||
// 添加原始时间用于排序
|
|
||||||
rawBindTime: new Date(device.createDate).getTime()
|
rawBindTime: new Date(device.createDate).getTime()
|
||||||
};
|
};
|
||||||
})
|
})
|
||||||
// 按照绑定时间降序排序
|
|
||||||
.sort((a, b) => a.rawBindTime - b.rawBindTime);
|
.sort((a, b) => a.rawBindTime - b.rawBindTime);
|
||||||
|
this.activeSearchKeyword = "";
|
||||||
|
this.searchKeyword = "";
|
||||||
} else {
|
} else {
|
||||||
this.$message.error(data.msg || '获取设备列表失败');
|
this.$message.error(data.msg || '获取设备列表失败');
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
headerCellClassName({ columnIndex }) {
|
||||||
|
if (columnIndex === 0) {
|
||||||
|
return "custom-selection-header";
|
||||||
|
}
|
||||||
|
return "";
|
||||||
|
},
|
||||||
|
getFirmwareTypeName(type) {
|
||||||
|
const firmwareType = FIRMWARE_TYPES.find(item => item.key === type);
|
||||||
|
return firmwareType ? firmwareType.name : type;
|
||||||
|
},
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
@@ -163,58 +336,360 @@ export default {
|
|||||||
min-height: 506px;
|
min-height: 506px;
|
||||||
height: 100vh;
|
height: 100vh;
|
||||||
display: flex;
|
display: flex;
|
||||||
|
position: relative;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
background: linear-gradient(145deg, #e6eeff, #eff0ff);
|
background: linear-gradient(to bottom right, #dce8ff, #e4eeff, #e6cbfd);
|
||||||
background-size: cover;
|
background-size: cover;
|
||||||
background-position: center;
|
|
||||||
-webkit-background-size: cover;
|
-webkit-background-size: cover;
|
||||||
-o-background-size: cover;
|
-o-background-size: cover;
|
||||||
}
|
}
|
||||||
|
|
||||||
.table-container {
|
.main-wrapper {
|
||||||
background: #f9fafc;
|
margin: 5px 22px;
|
||||||
padding: 20px;
|
border-radius: 15px;
|
||||||
border-radius: 20px;
|
min-height: calc(100vh - 24vh);
|
||||||
|
height: auto;
|
||||||
|
max-height: 80vh;
|
||||||
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.1);
|
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.1);
|
||||||
margin-top: 15px;
|
position: relative;
|
||||||
}
|
background: rgba(237, 242, 255, 0.5);
|
||||||
|
|
||||||
.add-device-btn {
|
|
||||||
float: right;
|
|
||||||
background: #409eff;
|
|
||||||
border: none;
|
|
||||||
border-radius: 10px;
|
|
||||||
width: 105px;
|
|
||||||
height: 32px;
|
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
flex-direction: column;
|
||||||
justify-content: center;
|
|
||||||
font-size: 14px;
|
|
||||||
gap: 8px;
|
|
||||||
margin-bottom: 15px;
|
|
||||||
|
|
||||||
&:hover {
|
|
||||||
background: #3a8ee6;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.device-list-title {
|
.operation-bar {
|
||||||
float: left;
|
display: flex;
|
||||||
font-size: 18px;
|
justify-content: space-between;
|
||||||
font-weight: 700;
|
align-items: center;
|
||||||
margin: 5px;
|
padding: 16px 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.page-title {
|
||||||
|
font-size: 24px;
|
||||||
|
margin: 0;
|
||||||
color: #2c3e50;
|
color: #2c3e50;
|
||||||
}
|
}
|
||||||
|
|
||||||
.el-icon-edit {
|
.right-operations {
|
||||||
color: #409eff;
|
display: flex;
|
||||||
cursor: pointer;
|
gap: 10px;
|
||||||
font-size: 14px;
|
margin-left: auto;
|
||||||
vertical-align: middle;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.pagination {
|
.search-input {
|
||||||
margin-top: 20px;
|
width: 280px;
|
||||||
text-align: right;
|
border-radius: 4px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.btn-search {
|
||||||
|
background: linear-gradient(135deg, #6b8cff, #a966ff);
|
||||||
|
border: none;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
::v-deep .search-input .el-input__inner {
|
||||||
|
border-radius: 4px;
|
||||||
|
border: 1px solid #DCDFE6;
|
||||||
|
background-color: white;
|
||||||
|
transition: border-color 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
::v-deep .page-size-select {
|
||||||
|
width: 100px;
|
||||||
|
margin-right: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
::v-deep .page-size-select .el-input__inner {
|
||||||
|
height: 32px;
|
||||||
|
line-height: 32px;
|
||||||
|
border-radius: 4px;
|
||||||
|
border: 1px solid #e4e7ed;
|
||||||
|
background: #dee7ff;
|
||||||
|
color: #606266;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
::v-deep .page-size-select .el-input__suffix {
|
||||||
|
right: 6px;
|
||||||
|
width: 15px;
|
||||||
|
height: 20px;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
top: 6px;
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
::v-deep .page-size-select .el-input__suffix-inner {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
::v-deep .page-size-select .el-icon-arrow-up:before {
|
||||||
|
content: "";
|
||||||
|
display: inline-block;
|
||||||
|
border-left: 6px solid transparent;
|
||||||
|
border-right: 6px solid transparent;
|
||||||
|
border-top: 9px solid #606266;
|
||||||
|
position: relative;
|
||||||
|
transform: rotate(0deg);
|
||||||
|
transition: transform 0.3s;
|
||||||
|
}
|
||||||
|
|
||||||
|
::v-deep .search-input .el-input__inner:focus {
|
||||||
|
border-color: #6b8cff;
|
||||||
|
outline: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.content-panel {
|
||||||
|
flex: 1;
|
||||||
|
display: flex;
|
||||||
|
overflow: hidden;
|
||||||
|
height: 100%;
|
||||||
|
border-radius: 15px;
|
||||||
|
background: transparent;
|
||||||
|
border: 1px solid #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.content-area {
|
||||||
|
flex: 1;
|
||||||
|
height: 100%;
|
||||||
|
min-width: 600px;
|
||||||
|
overflow: auto;
|
||||||
|
background-color: white;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.device-card {
|
||||||
|
background: white;
|
||||||
|
border: none;
|
||||||
|
box-shadow: none;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
flex: 1;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
::v-deep .el-card__body {
|
||||||
|
padding: 15px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
flex: 1;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.table_bottom {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
margin-top: 10px;
|
||||||
|
padding-bottom: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
.ctrl_btn {
|
||||||
|
display: flex;
|
||||||
|
gap: 8px;
|
||||||
|
padding-left: 26px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ctrl_btn .el-button {
|
||||||
|
min-width: 72px;
|
||||||
|
height: 32px;
|
||||||
|
padding: 7px 12px 7px 10px;
|
||||||
|
font-size: 12px;
|
||||||
|
border-radius: 4px;
|
||||||
|
line-height: 1;
|
||||||
|
font-weight: 500;
|
||||||
|
border: none;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
box-shadow: 0 2px 6px rgba(0, 0, 0, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.ctrl_btn .el-button:hover {
|
||||||
|
transform: translateY(-1px);
|
||||||
|
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.15);
|
||||||
|
}
|
||||||
|
|
||||||
|
.ctrl_btn .el-button--primary {
|
||||||
|
background: #5f70f3;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ctrl_btn .el-button--success {
|
||||||
|
background: #5bc98c;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ctrl_btn .el-button--danger {
|
||||||
|
background: #fd5b63;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.custom-pagination {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.custom-pagination .el-select {
|
||||||
|
margin-right: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.custom-pagination .pagination-btn:first-child,
|
||||||
|
.custom-pagination .pagination-btn:nth-child(2),
|
||||||
|
.custom-pagination .pagination-btn:nth-last-child(2),
|
||||||
|
.custom-pagination .pagination-btn:nth-child(3) {
|
||||||
|
min-width: 60px;
|
||||||
|
height: 32px;
|
||||||
|
padding: 0 12px;
|
||||||
|
border-radius: 4px;
|
||||||
|
border: 1px solid #e4e7ed;
|
||||||
|
background: #dee7ff;
|
||||||
|
color: #606266;
|
||||||
|
font-size: 14px;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.custom-pagination .pagination-btn:first-child:hover,
|
||||||
|
.custom-pagination .pagination-btn:nth-child(2):hover,
|
||||||
|
.custom-pagination .pagination-btn:nth-last-child(2):hover,
|
||||||
|
.custom-pagination .pagination-btn:nth-child(3):hover {
|
||||||
|
background: #d7dce6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.custom-pagination .pagination-btn:disabled {
|
||||||
|
opacity: 0.6;
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
|
||||||
|
.custom-pagination .pagination-btn:not(:first-child):not(:nth-child(3)):not(:nth-child(2)):not(:nth-last-child(2)) {
|
||||||
|
min-width: 28px;
|
||||||
|
height: 32px;
|
||||||
|
padding: 0;
|
||||||
|
border-radius: 4px;
|
||||||
|
border: 1px solid transparent;
|
||||||
|
background: transparent;
|
||||||
|
color: #606266;
|
||||||
|
font-size: 14px;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.custom-pagination .pagination-btn:not(:first-child):not(:nth-child(3)):not(:nth-child(2)):not(:nth-last-child(2)):hover {
|
||||||
|
background: rgba(245, 247, 250, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.custom-pagination .pagination-btn.active {
|
||||||
|
background: #5f70f3 !important;
|
||||||
|
color: #ffffff !important;
|
||||||
|
border-color: #5f70f3 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.custom-pagination .pagination-btn.active:hover {
|
||||||
|
background: #6d7cf5 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.custom-pagination .total-text {
|
||||||
|
color: #909399;
|
||||||
|
font-size: 14px;
|
||||||
|
margin-left: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.transparent-table) {
|
||||||
|
background: white;
|
||||||
|
border: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.transparent-table .el-table__header th) {
|
||||||
|
background: white !important;
|
||||||
|
color: black;
|
||||||
|
border-right: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.transparent-table .el-table__body tr td) {
|
||||||
|
border-top: 1px solid rgba(0, 0, 0, 0.04);
|
||||||
|
border-bottom: 1px solid rgba(0, 0, 0, 0.04);
|
||||||
|
border-right: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.transparent-table .el-table__header tr th:first-child .cell),
|
||||||
|
:deep(.transparent-table .el-table__body tr td:first-child .cell) {
|
||||||
|
padding-left: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.el-icon-edit) {
|
||||||
|
color: #7079aa;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.el-icon-edit:hover) {
|
||||||
|
color: #5a64b5;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.custom-selection-header .el-checkbox) {
|
||||||
|
display: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
:deep(.el-table .el-button--text) {
|
||||||
|
color: #7079aa;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.el-table .el-button--text:hover) {
|
||||||
|
color: #5a64b5;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.transparent-table) {
|
||||||
|
flex: 1;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
max-height: calc(100vh - 40vh);
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.el-table__body-wrapper) {
|
||||||
|
flex: 1;
|
||||||
|
overflow-y: auto;
|
||||||
|
max-height: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.el-table__header-wrapper) {
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (min-width: 1144px) {
|
||||||
|
.table_bottom {
|
||||||
|
margin-top: 40px;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.transparent-table) .el-table__body tr td {
|
||||||
|
padding-top: 16px;
|
||||||
|
padding-bottom: 16px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.el-checkbox__inner) {
|
||||||
|
background-color: #eeeeee !important;
|
||||||
|
border-color: #cccccc !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.el-checkbox__inner:hover) {
|
||||||
|
border-color: #cccccc !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.el-checkbox__input.is-checked .el-checkbox__inner) {
|
||||||
|
background-color: #5f70f3 !important;
|
||||||
|
border-color: #5f70f3 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
::v-deep .el-table--border::after,
|
||||||
|
::v-deep .el-table--group::after,
|
||||||
|
::v-deep .el-table::before {
|
||||||
|
display: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -3,21 +3,15 @@
|
|||||||
<HeaderBar />
|
<HeaderBar />
|
||||||
|
|
||||||
<div class="operation-bar">
|
<div class="operation-bar">
|
||||||
<h2 class="page-title">{{ modelTypeText }}</h2>
|
<h2 class="page-title">{{ modelTypeText }}</h2>
|
||||||
<div class="action-group">
|
<div class="action-group">
|
||||||
<div class="search-group">
|
<div class="search-group">
|
||||||
<el-input
|
<el-input placeholder="请输入模型名称查询" v-model="search" class="search-input" clearable
|
||||||
placeholder="请输入模型名称查询"
|
@keyup.enter.native="handleSearch" style="width: 240px" />
|
||||||
v-model="search"
|
<el-button class="btn-search" @click="handleSearch">
|
||||||
class="search-input"
|
搜索
|
||||||
clearable
|
</el-button>
|
||||||
@keyup.enter.native="handleSearch"
|
</div>
|
||||||
style="width: 240px"
|
|
||||||
/>
|
|
||||||
<el-button class="btn-search" @click="handleSearch">
|
|
||||||
搜索
|
|
||||||
</el-button>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -49,74 +43,91 @@
|
|||||||
|
|
||||||
<!-- 右侧内容 -->
|
<!-- 右侧内容 -->
|
||||||
<div class="content-area">
|
<div class="content-area">
|
||||||
<el-table ref="modelTable" style="width: 100%" :header-cell-style="{ background: 'transparent' }"
|
<el-card class="model-card" shadow="never">
|
||||||
:data="modelList" class="data-table" header-row-class-name="table-header"
|
<el-table
|
||||||
:header-cell-class-name="headerCellClassName" @selection-change="handleSelectionChange">
|
ref="modelTable"
|
||||||
<el-table-column type="selection" width="55" align="center"></el-table-column>
|
style="width: 100%"
|
||||||
<el-table-column label="模型名称" prop="modelName" align="center"></el-table-column>
|
v-loading="loading"
|
||||||
<el-table-column label="模型编码" prop="modelCode" align="center"></el-table-column>
|
element-loading-text="拼命加载中"
|
||||||
<el-table-column label="提供商" align="center">
|
element-loading-spinner="el-icon-loading"
|
||||||
<template slot-scope="scope">
|
element-loading-background="rgba(255, 255, 255, 0.7)"
|
||||||
{{ scope.row.configJson.type || '未知' }}
|
:header-cell-style="{ background: 'transparent' }"
|
||||||
</template>
|
:data="modelList"
|
||||||
</el-table-column>
|
class="data-table"
|
||||||
<el-table-column label="是否启用" align="center">
|
header-row-class-name="table-header"
|
||||||
<template slot-scope="scope">
|
:header-cell-class-name="headerCellClassName"
|
||||||
<el-switch v-model="scope.row.isEnabled" class="custom-switch" :active-value="1" :inactive-value="0"
|
@selection-change="handleSelectionChange">
|
||||||
@change="handleStatusChange(scope.row)" />
|
<el-table-column type="selection" width="55" align="center"></el-table-column>
|
||||||
</template>
|
<el-table-column label="模型ID" prop="id" align="center"></el-table-column>
|
||||||
</el-table-column>
|
<el-table-column label="模型名称" prop="modelName" align="center"></el-table-column>
|
||||||
<el-table-column label="是否默认" align="center">
|
<el-table-column label="提供商" align="center">
|
||||||
<template slot-scope="scope">
|
<template slot-scope="scope">
|
||||||
<el-switch v-model="scope.row.isDefault" class="custom-switch" :active-value="1" :inactive-value="0"
|
{{ scope.row.configJson.type || '未知' }}
|
||||||
@change="handleDefaultChange(scope.row)" />
|
</template>
|
||||||
</template>
|
</el-table-column>
|
||||||
</el-table-column>
|
<el-table-column label="是否启用" align="center">
|
||||||
<el-table-column v-if="activeTab === 'tts'" label="音色管理" align="center">
|
<template slot-scope="scope">
|
||||||
<template slot-scope="scope">
|
<el-switch v-model="scope.row.isEnabled" class="custom-switch" :active-value="1" :inactive-value="0"
|
||||||
<el-button type="text" size="mini" @click="openTtsDialog(scope.row)" class="voice-management-btn">
|
@change="handleStatusChange(scope.row)" />
|
||||||
音色管理
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column label="是否默认" align="center">
|
||||||
|
<template slot-scope="scope">
|
||||||
|
<el-switch v-model="scope.row.isDefault" class="custom-switch" :active-value="1" :inactive-value="0"
|
||||||
|
@change="handleDefaultChange(scope.row)" />
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column v-if="activeTab === 'tts'" label="音色管理" align="center">
|
||||||
|
<template slot-scope="scope">
|
||||||
|
<el-button type="text" size="mini" @click="openTtsDialog(scope.row)" class="voice-management-btn">
|
||||||
|
音色管理
|
||||||
|
</el-button>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column label="操作" align="center" width="150px">
|
||||||
|
<template slot-scope="scope">
|
||||||
|
<el-button type="text" size="mini" @click="editModel(scope.row)" class="edit-btn">
|
||||||
|
修改
|
||||||
|
</el-button>
|
||||||
|
<el-button type="text" size="mini" @click="deleteModel(scope.row)" class="delete-btn">
|
||||||
|
删除
|
||||||
|
</el-button>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
</el-table>
|
||||||
|
<div class="table-footer">
|
||||||
|
<div class="batch-actions">
|
||||||
|
<el-button size="mini" type="primary" @click="selectAll">
|
||||||
|
{{ isAllSelected ?
|
||||||
|
'取消全选' : '全选' }}
|
||||||
</el-button>
|
</el-button>
|
||||||
</template>
|
<el-button type="success" size="mini" @click="addModel" class="add-btn">
|
||||||
</el-table-column>
|
新增
|
||||||
<el-table-column label="操作" align="center" width="150px">
|
|
||||||
<template slot-scope="scope">
|
|
||||||
<el-button type="text" size="mini" @click="editModel(scope.row)" class="edit-btn">
|
|
||||||
修改
|
|
||||||
</el-button>
|
</el-button>
|
||||||
<el-button type="text" size="mini" @click="deleteModel(scope.row)" class="delete-btn">
|
<el-button size="mini" type="danger" icon="el-icon-delete" @click="batchDelete">
|
||||||
删除
|
删除
|
||||||
</el-button>
|
</el-button>
|
||||||
</template>
|
</div>
|
||||||
</el-table-column>
|
<div class="custom-pagination">
|
||||||
</el-table>
|
|
||||||
|
|
||||||
<div class="table-footer">
|
<el-select v-model="pageSize" @change="handlePageSizeChange" class="page-size-select">
|
||||||
<div class="batch-actions">
|
<el-option v-for="item in pageSizeOptions" :key="item" :label="`${item}条/页`" :value="item">
|
||||||
<el-button size="mini" type="primary" @click="selectAll" >
|
</el-option>
|
||||||
{{ isAllSelected ?
|
</el-select>
|
||||||
'取消全选' : '全选' }}
|
|
||||||
</el-button>
|
<button class="pagination-btn" :disabled="currentPage === 1" @click="goFirst">首页</button>
|
||||||
<el-button type="success" size="mini" @click="addModel" class="add-btn">
|
<button class="pagination-btn" :disabled="currentPage === 1" @click="goPrev">上一页</button>
|
||||||
新增
|
|
||||||
</el-button>
|
<button v-for="page in visiblePages" :key="page" class="pagination-btn"
|
||||||
<el-button size="mini" type="danger" icon="el-icon-delete" @click="batchDelete">
|
:class="{ active: page === currentPage }" @click="goToPage(page)">
|
||||||
删除
|
{{ page }}
|
||||||
</el-button>
|
</button>
|
||||||
|
|
||||||
|
<button class="pagination-btn" :disabled="currentPage === pageCount" @click="goNext">下一页</button>
|
||||||
|
<span class="total-text">共{{ total }}条记录</span>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="custom-pagination">
|
</el-card>
|
||||||
<button class="pagination-btn" :disabled="currentPage === 1" @click="goFirst">首页</button>
|
|
||||||
<button class="pagination-btn" :disabled="currentPage === 1" @click="goPrev">上一页</button>
|
|
||||||
|
|
||||||
<button v-for="page in visiblePages" :key="page" class="pagination-btn"
|
|
||||||
:class="{ active: page === currentPage }" @click="goToPage(page)">
|
|
||||||
{{ page }}
|
|
||||||
</button>
|
|
||||||
|
|
||||||
<button class="pagination-btn" :disabled="currentPage === pageCount" @click="goNext">下一页</button>
|
|
||||||
<span class="total-text">共{{ total }}条记录</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -125,10 +136,6 @@
|
|||||||
<TtsModel :visible.sync="ttsDialogVisible" :ttsModelId="selectedTtsModelId" />
|
<TtsModel :visible.sync="ttsDialogVisible" :ttsModelId="selectedTtsModelId" />
|
||||||
<AddModelDialog :modelType="activeTab" :visible.sync="addDialogVisible" @confirm="handleAddConfirm" />
|
<AddModelDialog :modelType="activeTab" :visible.sync="addDialogVisible" @confirm="handleAddConfirm" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="copyright">
|
|
||||||
©2025 xiaozhi-esp32-server
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@@ -151,11 +158,13 @@ export default {
|
|||||||
ttsDialogVisible: false,
|
ttsDialogVisible: false,
|
||||||
selectedTtsModelId: '',
|
selectedTtsModelId: '',
|
||||||
modelList: [],
|
modelList: [],
|
||||||
|
pageSizeOptions: [10, 20, 50, 100],
|
||||||
currentPage: 1,
|
currentPage: 1,
|
||||||
pageSize: 5,
|
pageSize: 10,
|
||||||
total: 0,
|
total: 0,
|
||||||
selectedModels: [],
|
selectedModels: [],
|
||||||
isAllSelected: false
|
isAllSelected: false,
|
||||||
|
loading: false
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -164,7 +173,6 @@ export default {
|
|||||||
},
|
},
|
||||||
|
|
||||||
computed: {
|
computed: {
|
||||||
|
|
||||||
modelTypeText() {
|
modelTypeText() {
|
||||||
const map = {
|
const map = {
|
||||||
vad: '语言活动检测模型(VAD)',
|
vad: '语言活动检测模型(VAD)',
|
||||||
@@ -176,8 +184,6 @@ export default {
|
|||||||
}
|
}
|
||||||
return map[this.activeTab] || '模型配置'
|
return map[this.activeTab] || '模型配置'
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
||||||
pageCount() {
|
pageCount() {
|
||||||
return Math.ceil(this.total / this.pageSize);
|
return Math.ceil(this.total / this.pageSize);
|
||||||
},
|
},
|
||||||
@@ -199,6 +205,11 @@ export default {
|
|||||||
},
|
},
|
||||||
|
|
||||||
methods: {
|
methods: {
|
||||||
|
handlePageSizeChange(val) {
|
||||||
|
this.pageSize = val;
|
||||||
|
this.currentPage = 1;
|
||||||
|
this.loadData();
|
||||||
|
},
|
||||||
openTtsDialog(row) {
|
openTtsDialog(row) {
|
||||||
this.selectedTtsModelId = row.id;
|
this.selectedTtsModelId = row.id;
|
||||||
this.ttsDialogVisible = true;
|
this.ttsDialogVisible = true;
|
||||||
@@ -211,7 +222,8 @@ export default {
|
|||||||
},
|
},
|
||||||
handleMenuSelect(index) {
|
handleMenuSelect(index) {
|
||||||
this.activeTab = index;
|
this.activeTab = index;
|
||||||
this.currentPage = 1;
|
this.currentPage = 1; // 重置到第一页
|
||||||
|
this.pageSize = 10; // 可选:重置每页条数
|
||||||
this.loadData();
|
this.loadData();
|
||||||
},
|
},
|
||||||
handleSearch() {
|
handleSearch() {
|
||||||
@@ -296,9 +308,10 @@ export default {
|
|||||||
this.currentPage = page;
|
this.currentPage = page;
|
||||||
this.$refs.modelTable.clearSelection();
|
this.$refs.modelTable.clearSelection();
|
||||||
},
|
},
|
||||||
handleModelSave({ provideCode, formData }) {
|
handleModelSave({ provideCode, formData, done }) {
|
||||||
const modelType = this.activeTab;
|
const modelType = this.activeTab;
|
||||||
const id = formData.id;
|
const id = formData.id;
|
||||||
|
|
||||||
Api.model.updateModel(
|
Api.model.updateModel(
|
||||||
{ modelType, provideCode, id, formData },
|
{ modelType, provideCode, id, formData },
|
||||||
({ data }) => {
|
({ data }) => {
|
||||||
@@ -309,6 +322,7 @@ export default {
|
|||||||
} else {
|
} else {
|
||||||
this.$message.error(data.msg || '保存失败');
|
this.$message.error(data.msg || '保存失败');
|
||||||
}
|
}
|
||||||
|
done && done(); // 调用done回调关闭加载状态
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
@@ -380,6 +394,7 @@ export default {
|
|||||||
|
|
||||||
// 获取模型配置列表
|
// 获取模型配置列表
|
||||||
loadData() {
|
loadData() {
|
||||||
|
this.loading = true; // 开始加载
|
||||||
const params = {
|
const params = {
|
||||||
modelType: this.activeTab,
|
modelType: this.activeTab,
|
||||||
modelName: this.search,
|
modelName: this.search,
|
||||||
@@ -388,6 +403,7 @@ export default {
|
|||||||
};
|
};
|
||||||
|
|
||||||
Api.model.getModelList(params, ({ data }) => {
|
Api.model.getModelList(params, ({ data }) => {
|
||||||
|
this.loading = false; // 结束加载
|
||||||
if (data.code === 0) {
|
if (data.code === 0) {
|
||||||
this.modelList = data.data.list;
|
this.modelList = data.data.list;
|
||||||
this.total = data.data.total;
|
this.total = data.data.total;
|
||||||
@@ -456,7 +472,9 @@ export default {
|
|||||||
.main-wrapper {
|
.main-wrapper {
|
||||||
margin: 5px 22px;
|
margin: 5px 22px;
|
||||||
border-radius: 15px;
|
border-radius: 15px;
|
||||||
min-height: 600px;
|
min-height: calc(100vh - 24vh);
|
||||||
|
height: auto;
|
||||||
|
max-height: 80vh;
|
||||||
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.1);
|
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.1);
|
||||||
position: relative;
|
position: relative;
|
||||||
background: rgba(237, 242, 255, 0.5);
|
background: rgba(237, 242, 255, 0.5);
|
||||||
@@ -474,12 +492,6 @@ export default {
|
|||||||
margin: 0;
|
margin: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.right-operations {
|
|
||||||
display: flex;
|
|
||||||
gap: 10px;
|
|
||||||
margin-left: auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
.content-panel {
|
.content-panel {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
display: flex;
|
display: flex;
|
||||||
@@ -552,17 +564,10 @@ export default {
|
|||||||
padding: 24px;
|
padding: 24px;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
min-width: 600px;
|
min-width: 600px;
|
||||||
overflow-x: auto;
|
overflow: hidden;
|
||||||
background-color: white;
|
background-color: white;
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
.title-bar {
|
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: space-between;
|
flex-direction: column;
|
||||||
align-items: center;
|
|
||||||
margin-bottom: 24px;
|
|
||||||
flex-wrap: nowrap;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.action-group {
|
.action-group {
|
||||||
@@ -586,6 +591,11 @@ export default {
|
|||||||
color: white;
|
color: white;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.btn-search:hover {
|
||||||
|
opacity: 0.9;
|
||||||
|
transform: translateY(-1px);
|
||||||
|
}
|
||||||
|
|
||||||
::v-deep .search-input .el-input__inner {
|
::v-deep .search-input .el-input__inner {
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
border: 1px solid #DCDFE6;
|
border: 1px solid #DCDFE6;
|
||||||
@@ -593,22 +603,55 @@ export default {
|
|||||||
transition: border-color 0.2s;
|
transition: border-color 0.2s;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
::v-deep .page-size-select {
|
||||||
|
width: 100px;
|
||||||
|
margin-right: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
::v-deep .page-size-select .el-input__inner {
|
||||||
|
height: 32px;
|
||||||
|
line-height: 32px;
|
||||||
|
border-radius: 4px;
|
||||||
|
border: 1px solid #e4e7ed;
|
||||||
|
background: #dee7ff;
|
||||||
|
color: #606266;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
::v-deep .page-size-select .el-input__suffix {
|
||||||
|
right: 6px;
|
||||||
|
width: 15px;
|
||||||
|
height: 20px;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
top: 6px;
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
::v-deep .page-size-select .el-input__suffix-inner {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
::v-deep .page-size-select .el-icon-arrow-up:before {
|
||||||
|
content: "";
|
||||||
|
display: inline-block;
|
||||||
|
border-left: 6px solid transparent;
|
||||||
|
border-right: 6px solid transparent;
|
||||||
|
border-top: 9px solid #606266;
|
||||||
|
position: relative;
|
||||||
|
transform: rotate(0deg);
|
||||||
|
transition: transform 0.3s;
|
||||||
|
}
|
||||||
|
|
||||||
::v-deep .search-input .el-input__inner:focus {
|
::v-deep .search-input .el-input__inner:focus {
|
||||||
border-color: #6b8cff;
|
border-color: #6b8cff;
|
||||||
outline: none;
|
outline: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.btn-search {
|
|
||||||
background: linear-gradient(135deg, #6b8cff, #a966ff);
|
|
||||||
border: none;
|
|
||||||
color: white;
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn-search:hover {
|
|
||||||
opacity: 0.9;
|
|
||||||
transform: translateY(-1px);
|
|
||||||
}
|
|
||||||
|
|
||||||
.data-table {
|
.data-table {
|
||||||
border-radius: 6px;
|
border-radius: 6px;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
@@ -626,12 +669,14 @@ export default {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.table-footer {
|
.table-footer {
|
||||||
margin-top: 24px;
|
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
padding: 16px 0;
|
padding: 16px 0;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
flex-shrink: 0;
|
||||||
|
min-height: 60px;
|
||||||
|
background: white;
|
||||||
}
|
}
|
||||||
|
|
||||||
.batch-actions {
|
.batch-actions {
|
||||||
@@ -639,26 +684,6 @@ export default {
|
|||||||
gap: 8px;
|
gap: 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.copyright {
|
|
||||||
text-align: center;
|
|
||||||
color: #979db1;
|
|
||||||
font-size: 12px;
|
|
||||||
font-weight: 400;
|
|
||||||
margin-top: auto;
|
|
||||||
padding: 30px 0 20px;
|
|
||||||
position: absolute;
|
|
||||||
bottom: 0;
|
|
||||||
left: 50%;
|
|
||||||
transform: translateX(-50%);
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.title-wrapper {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.batch-actions .el-button {
|
.batch-actions .el-button {
|
||||||
min-width: 72px;
|
min-width: 72px;
|
||||||
height: 32px;
|
height: 32px;
|
||||||
@@ -710,11 +735,6 @@ export default {
|
|||||||
background-color: transparent !important;
|
background-color: transparent !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.pagination-container {
|
|
||||||
display: flex;
|
|
||||||
justify-content: flex-end;
|
|
||||||
}
|
|
||||||
|
|
||||||
::v-deep .el-table .custom-selection-header .cell .el-checkbox__inner {
|
::v-deep .el-table .custom-selection-header .cell .el-checkbox__inner {
|
||||||
display: none !important;
|
display: none !important;
|
||||||
}
|
}
|
||||||
@@ -815,11 +835,11 @@ export default {
|
|||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 8px;
|
gap: 8px;
|
||||||
margin-top: 15px;
|
|
||||||
|
|
||||||
/* 导航按钮样式 (首页、上一页、下一页) */
|
/* 导航按钮样式 (首页、上一页、下一页) */
|
||||||
.pagination-btn:first-child,
|
.pagination-btn:first-child,
|
||||||
.pagination-btn:nth-child(2),
|
.pagination-btn:nth-child(2),
|
||||||
|
.pagination-btn:nth-child(3),
|
||||||
.pagination-btn:nth-last-child(2) {
|
.pagination-btn:nth-last-child(2) {
|
||||||
min-width: 60px;
|
min-width: 60px;
|
||||||
height: 32px;
|
height: 32px;
|
||||||
@@ -843,7 +863,7 @@ export default {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/* 数字按钮样式 */
|
/* 数字按钮样式 */
|
||||||
.pagination-btn:not(:first-child):not(:nth-child(2)):not(:nth-last-child(2)) {
|
.pagination-btn:not(:first-child):not(:nth-child(2)):not(:nth-child(3)):not(:nth-last-child(2)) {
|
||||||
min-width: 28px;
|
min-width: 28px;
|
||||||
height: 32px;
|
height: 32px;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
@@ -876,4 +896,50 @@ export default {
|
|||||||
margin-left: 10px;
|
margin-left: 10px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.model-card {
|
||||||
|
background: white;
|
||||||
|
flex: 1;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
border: none;
|
||||||
|
box-shadow: none;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.model-card ::v-deep .el-card__body {
|
||||||
|
padding: 0;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
flex: 1;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.data-table {
|
||||||
|
--table-max-height: calc(100vh - 45vh);
|
||||||
|
max-height: var(--table-max-height);
|
||||||
|
}
|
||||||
|
|
||||||
|
.data-table ::v-deep .el-table__body-wrapper {
|
||||||
|
max-height: calc(var(--table-max-height) - 80px);
|
||||||
|
overflow-y: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
::v-deep .el-loading-mask {
|
||||||
|
background-color: rgba(255, 255, 255, 0.6) !important;
|
||||||
|
backdrop-filter: blur(2px);
|
||||||
|
}
|
||||||
|
::v-deep .el-loading-spinner .circular {
|
||||||
|
width: 28px;
|
||||||
|
height: 28px;
|
||||||
|
}
|
||||||
|
::v-deep .el-loading-spinner .path {
|
||||||
|
stroke: #6b8cff;
|
||||||
|
}
|
||||||
|
::v-deep .el-loading-text {
|
||||||
|
color: #6b8cff !important;
|
||||||
|
font-size: 14px;
|
||||||
|
margin-top: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -0,0 +1,643 @@
|
|||||||
|
<template>
|
||||||
|
<div class="welcome">
|
||||||
|
<HeaderBar />
|
||||||
|
|
||||||
|
<div class="operation-bar">
|
||||||
|
<h2 class="page-title">固件管理</h2>
|
||||||
|
<div class="right-operations">
|
||||||
|
<el-input placeholder="请输入固件名称查询" v-model="searchName" class="search-input"
|
||||||
|
@keyup.enter.native="handleSearch" clearable />
|
||||||
|
<el-button class="btn-search" @click="handleSearch">搜索</el-button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="main-wrapper">
|
||||||
|
<div class="content-panel">
|
||||||
|
<div class="content-area">
|
||||||
|
<el-card class="params-card" shadow="never">
|
||||||
|
<el-table ref="paramsTable" :data="paramsList" class="transparent-table"
|
||||||
|
:header-cell-class-name="headerCellClassName">
|
||||||
|
<el-table-column label="选择" align="center" width="120">
|
||||||
|
<template slot-scope="scope">
|
||||||
|
<el-checkbox v-model="scope.row.selected"></el-checkbox>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column label="固件名称" prop="firmwareName" align="center"></el-table-column>
|
||||||
|
<el-table-column label="固件类型" prop="type" align="center">
|
||||||
|
<template slot-scope="scope">
|
||||||
|
{{ getFirmwareTypeName(scope.row.type) }}
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column label="版本号" prop="version" align="center"></el-table-column>
|
||||||
|
<el-table-column label="文件大小" prop="size" align="center">
|
||||||
|
<template slot-scope="scope">
|
||||||
|
{{ formatFileSize(scope.row.size) }}
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column label="备注" prop="remark" align="center"
|
||||||
|
show-overflow-tooltip></el-table-column>
|
||||||
|
<el-table-column label="创建时间" prop="createDate" align="center">
|
||||||
|
<template slot-scope="scope">
|
||||||
|
{{ formatDate(scope.row.createDate) }}
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column label="操作" align="center">
|
||||||
|
<template slot-scope="scope">
|
||||||
|
<el-button size="mini" type="text"
|
||||||
|
@click="downloadFirmware(scope.row)">下载</el-button>
|
||||||
|
<el-button size="mini" type="text" @click="editParam(scope.row)">编辑</el-button>
|
||||||
|
<el-button size="mini" type="text" @click="deleteParam(scope.row)">删除</el-button>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
</el-table>
|
||||||
|
|
||||||
|
<div class="table_bottom">
|
||||||
|
<div class="ctrl_btn">
|
||||||
|
<el-button size="mini" type="primary" class="select-all-btn" @click="handleSelectAll">
|
||||||
|
{{ isAllSelected ? '取消全选' : '全选' }}
|
||||||
|
</el-button>
|
||||||
|
<el-button size="mini" type="success" @click="showAddDialog"
|
||||||
|
style="background: #5bc98c;border: None;">新增</el-button>
|
||||||
|
<el-button size="mini" type="danger" icon="el-icon-delete"
|
||||||
|
@click="deleteSelectedParams">删除</el-button>
|
||||||
|
</div>
|
||||||
|
<div class="custom-pagination">
|
||||||
|
<el-select v-model="pageSize" @change="handlePageSizeChange" class="page-size-select">
|
||||||
|
<el-option v-for="item in pageSizeOptions" :key="item" :label="`${item}条/页`"
|
||||||
|
:value="item">
|
||||||
|
</el-option>
|
||||||
|
</el-select>
|
||||||
|
<button class="pagination-btn" :disabled="currentPage === 1" @click="goFirst">
|
||||||
|
首页
|
||||||
|
</button>
|
||||||
|
<button class="pagination-btn" :disabled="currentPage === 1" @click="goPrev">
|
||||||
|
上一页
|
||||||
|
</button>
|
||||||
|
<button v-for="page in visiblePages" :key="page" class="pagination-btn"
|
||||||
|
:class="{ active: page === currentPage }" @click="goToPage(page)">
|
||||||
|
{{ page }}
|
||||||
|
</button>
|
||||||
|
<button class="pagination-btn" :disabled="currentPage === pageCount" @click="goNext">
|
||||||
|
下一页
|
||||||
|
</button>
|
||||||
|
<span class="total-text">共{{ total }}条记录</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</el-card>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 新增/编辑固件对话框 -->
|
||||||
|
<firmware-dialog :title="dialogTitle" :visible.sync="dialogVisible" :form="firmwareForm" @submit="handleSubmit"
|
||||||
|
@cancel="dialogVisible = false" />
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import Api from "@/apis/api";
|
||||||
|
import FirmwareDialog from "@/components/FirmwareDialog.vue";
|
||||||
|
import HeaderBar from "@/components/HeaderBar.vue";
|
||||||
|
import { FIRMWARE_TYPES } from "@/utils";
|
||||||
|
import { formatDate, formatFileSize } from "@/utils/format";
|
||||||
|
|
||||||
|
export default {
|
||||||
|
components: { HeaderBar, FirmwareDialog },
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
searchName: "",
|
||||||
|
paramsList: [],
|
||||||
|
firmwareList: [],
|
||||||
|
currentPage: 1,
|
||||||
|
pageSize: 10,
|
||||||
|
pageSizeOptions: [10, 20, 50, 100],
|
||||||
|
total: 0,
|
||||||
|
dialogVisible: false,
|
||||||
|
dialogTitle: "新增固件",
|
||||||
|
isAllSelected: false,
|
||||||
|
firmwareForm: {
|
||||||
|
id: null,
|
||||||
|
firmwareName: "",
|
||||||
|
type: "",
|
||||||
|
version: "",
|
||||||
|
size: 0,
|
||||||
|
remark: "",
|
||||||
|
firmwarePath: ""
|
||||||
|
},
|
||||||
|
};
|
||||||
|
},
|
||||||
|
created() {
|
||||||
|
this.fetchFirmwareList();
|
||||||
|
},
|
||||||
|
|
||||||
|
computed: {
|
||||||
|
pageCount() {
|
||||||
|
return Math.ceil(this.total / this.pageSize);
|
||||||
|
},
|
||||||
|
visiblePages() {
|
||||||
|
const pages = [];
|
||||||
|
const maxVisible = 3;
|
||||||
|
let start = Math.max(1, this.currentPage - 1);
|
||||||
|
let end = Math.min(this.pageCount, start + maxVisible - 1);
|
||||||
|
|
||||||
|
if (end - start + 1 < maxVisible) {
|
||||||
|
start = Math.max(1, end - maxVisible + 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (let i = start; i <= end; i++) {
|
||||||
|
pages.push(i);
|
||||||
|
}
|
||||||
|
return pages;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
handlePageSizeChange(val) {
|
||||||
|
this.pageSize = val;
|
||||||
|
this.currentPage = 1;
|
||||||
|
this.fetchFirmwareList();
|
||||||
|
},
|
||||||
|
fetchFirmwareList() {
|
||||||
|
const params = {
|
||||||
|
pageNum: this.currentPage,
|
||||||
|
pageSize: this.pageSize,
|
||||||
|
firmwareName: this.searchName || "",
|
||||||
|
orderField: "create_date",
|
||||||
|
order: "desc"
|
||||||
|
};
|
||||||
|
Api.ota.getOtaList(params, (res) => {
|
||||||
|
res = res.data
|
||||||
|
if (res.code === 0) {
|
||||||
|
this.firmwareList = res.data.list.map(item => ({
|
||||||
|
...item,
|
||||||
|
selected: false
|
||||||
|
}));
|
||||||
|
this.paramsList = this.firmwareList;
|
||||||
|
this.total = res.data.total || 0;
|
||||||
|
} else {
|
||||||
|
this.firmwareList = [];
|
||||||
|
this.paramsList = [];
|
||||||
|
this.total = 0;
|
||||||
|
this.$message.error({
|
||||||
|
message: res?.data?.msg || '获取固件列表失败',
|
||||||
|
showClose: true
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
handleSearch() {
|
||||||
|
this.currentPage = 1;
|
||||||
|
this.fetchFirmwareList();
|
||||||
|
},
|
||||||
|
handleSelectAll() {
|
||||||
|
this.isAllSelected = !this.isAllSelected;
|
||||||
|
this.firmwareList.forEach(row => {
|
||||||
|
row.selected = this.isAllSelected;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
showAddDialog() {
|
||||||
|
this.dialogTitle = "新增固件";
|
||||||
|
// 完全重置表单数据
|
||||||
|
this.firmwareForm = {
|
||||||
|
id: null,
|
||||||
|
firmwareName: "",
|
||||||
|
type: "",
|
||||||
|
version: "",
|
||||||
|
size: 0,
|
||||||
|
remark: "",
|
||||||
|
firmwarePath: ""
|
||||||
|
};
|
||||||
|
this.$nextTick(() => {
|
||||||
|
// 重置表单的校验状态
|
||||||
|
if (this.$refs.firmwareDialog && this.$refs.firmwareDialog.$refs.form) {
|
||||||
|
this.$refs.firmwareDialog.$refs.form.clearValidate();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
this.dialogVisible = true;
|
||||||
|
},
|
||||||
|
editParam(row) {
|
||||||
|
this.dialogTitle = "编辑固件";
|
||||||
|
this.firmwareForm = { ...row };
|
||||||
|
this.dialogVisible = true;
|
||||||
|
},
|
||||||
|
handleSubmit(form) {
|
||||||
|
if (form.id) {
|
||||||
|
// 编辑
|
||||||
|
Api.ota.updateOta(form.id, form, (res) => {
|
||||||
|
res = res.data;
|
||||||
|
if (res.code === 0) {
|
||||||
|
this.$message.success({
|
||||||
|
message: "修改成功",
|
||||||
|
showClose: true
|
||||||
|
});
|
||||||
|
this.dialogVisible = false;
|
||||||
|
this.fetchFirmwareList();
|
||||||
|
} else {
|
||||||
|
this.$message.error({
|
||||||
|
message: res.msg || "修改失败",
|
||||||
|
showClose: true
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
// 新增
|
||||||
|
Api.ota.saveOta(form, (res) => {
|
||||||
|
res = res.data;
|
||||||
|
if (res.code === 0) {
|
||||||
|
this.$message.success({
|
||||||
|
message: "新增成功",
|
||||||
|
showClose: true
|
||||||
|
});
|
||||||
|
this.dialogVisible = false;
|
||||||
|
this.fetchFirmwareList();
|
||||||
|
} else {
|
||||||
|
this.$message.error({
|
||||||
|
message: res.msg || "新增失败",
|
||||||
|
showClose: true
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
deleteSelectedParams() {
|
||||||
|
const selectedRows = this.firmwareList.filter(row => row.selected);
|
||||||
|
if (selectedRows.length === 0) {
|
||||||
|
this.$message.warning({
|
||||||
|
message: "请先选择需要删除的固件",
|
||||||
|
showClose: true
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.deleteParam(selectedRows);
|
||||||
|
},
|
||||||
|
deleteParam(row) {
|
||||||
|
// 处理单个参数或参数数组
|
||||||
|
const params = Array.isArray(row) ? row : [row];
|
||||||
|
|
||||||
|
if (Array.isArray(row) && row.length === 0) {
|
||||||
|
this.$message.warning({
|
||||||
|
message: "请先选择需要删除的参数",
|
||||||
|
showClose: true
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const paramCount = params.length;
|
||||||
|
this.$confirm(`确定要删除选中的${paramCount}个固件吗?`, '警告', {
|
||||||
|
confirmButtonText: '确定',
|
||||||
|
cancelButtonText: '取消',
|
||||||
|
type: 'warning',
|
||||||
|
distinguishCancelAndClose: true
|
||||||
|
}).then(() => {
|
||||||
|
const ids = params.map(param => param.id);
|
||||||
|
if (ids.some(id => !id)) {
|
||||||
|
this.$message.error({
|
||||||
|
message: '存在无效的参数ID',
|
||||||
|
showClose: true
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Api.ota.deleteOta(ids, (res) => {
|
||||||
|
res = res.data;
|
||||||
|
if (res.code === 0) {
|
||||||
|
this.$message.success({
|
||||||
|
message: `成功删除${paramCount}个固件`,
|
||||||
|
showClose: true
|
||||||
|
});
|
||||||
|
this.fetchFirmwareList();
|
||||||
|
} else {
|
||||||
|
this.$message.error({
|
||||||
|
message: res.msg || '删除失败,请重试',
|
||||||
|
showClose: true
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}).catch(action => {
|
||||||
|
if (action === 'cancel') {
|
||||||
|
this.$message({
|
||||||
|
type: 'info',
|
||||||
|
message: '已取消删除操作',
|
||||||
|
duration: 1000
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
this.$message({
|
||||||
|
type: 'info',
|
||||||
|
message: '操作已关闭',
|
||||||
|
duration: 1000
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
headerCellClassName({ columnIndex }) {
|
||||||
|
if (columnIndex === 0) {
|
||||||
|
return "custom-selection-header";
|
||||||
|
}
|
||||||
|
return "";
|
||||||
|
},
|
||||||
|
goFirst() {
|
||||||
|
this.currentPage = 1;
|
||||||
|
this.fetchFirmwareList();
|
||||||
|
},
|
||||||
|
goPrev() {
|
||||||
|
if (this.currentPage > 1) {
|
||||||
|
this.currentPage--;
|
||||||
|
this.fetchFirmwareList();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
goNext() {
|
||||||
|
if (this.currentPage < this.pageCount) {
|
||||||
|
this.currentPage++;
|
||||||
|
this.fetchFirmwareList();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
goToPage(page) {
|
||||||
|
this.currentPage = page;
|
||||||
|
this.fetchFirmwareList();
|
||||||
|
},
|
||||||
|
downloadFirmware(firmware) {
|
||||||
|
if (!firmware || !firmware.id) {
|
||||||
|
this.$message.error('固件信息不完整');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// 先获取下载链接
|
||||||
|
Api.ota.getDownloadUrl(firmware.id, (res) => {
|
||||||
|
if (res.data.code === 0) {
|
||||||
|
const uuid = res.data.data;
|
||||||
|
const baseUrl = process.env.VUE_APP_API_BASE_URL || '';
|
||||||
|
window.open(`${window.location.origin}${baseUrl}/otaMag/download/${uuid}`);
|
||||||
|
} else {
|
||||||
|
this.$message.error('获取下载链接失败');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
formatDate,
|
||||||
|
formatFileSize,
|
||||||
|
getFirmwareTypeName(type) {
|
||||||
|
const firmwareType = FIRMWARE_TYPES.find(item => item.key === type);
|
||||||
|
return firmwareType ? firmwareType.name : type;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.welcome {
|
||||||
|
min-width: 900px;
|
||||||
|
min-height: 506px;
|
||||||
|
height: 100vh;
|
||||||
|
display: flex;
|
||||||
|
position: relative;
|
||||||
|
flex-direction: column;
|
||||||
|
background-size: cover;
|
||||||
|
background: linear-gradient(to bottom right, #dce8ff, #e4eeff, #e6cbfd) center;
|
||||||
|
-webkit-background-size: cover;
|
||||||
|
-o-background-size: cover;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.main-wrapper {
|
||||||
|
margin: 5px 22px;
|
||||||
|
border-radius: 15px;
|
||||||
|
min-height: calc(100vh - 24vh);
|
||||||
|
height: auto;
|
||||||
|
max-height: 80vh;
|
||||||
|
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.1);
|
||||||
|
position: relative;
|
||||||
|
background: rgba(237, 242, 255, 0.5);
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.operation-bar {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
padding: 16px 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.page-title {
|
||||||
|
font-size: 24px;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.right-operations {
|
||||||
|
display: flex;
|
||||||
|
gap: 10px;
|
||||||
|
margin-left: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-input {
|
||||||
|
width: 240px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-search {
|
||||||
|
background: linear-gradient(135deg, #6b8cff, #a966ff);
|
||||||
|
border: none;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.content-panel {
|
||||||
|
flex: 1;
|
||||||
|
display: flex;
|
||||||
|
overflow: hidden;
|
||||||
|
height: 100%;
|
||||||
|
border-radius: 15px;
|
||||||
|
background: transparent;
|
||||||
|
border: 1px solid #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.content-area {
|
||||||
|
flex: 1;
|
||||||
|
height: 100%;
|
||||||
|
min-width: 600px;
|
||||||
|
overflow: auto;
|
||||||
|
background-color: white;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.params-card {
|
||||||
|
background: white;
|
||||||
|
border: none;
|
||||||
|
box-shadow: none;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
flex: 1;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.table_bottom {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
margin-top: 10px;
|
||||||
|
padding-bottom: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ctrl_btn {
|
||||||
|
display: flex;
|
||||||
|
gap: 8px;
|
||||||
|
padding-left: 26px;
|
||||||
|
|
||||||
|
.el-button {
|
||||||
|
min-width: 72px;
|
||||||
|
height: 32px;
|
||||||
|
padding: 7px 12px 7px 10px;
|
||||||
|
font-size: 12px;
|
||||||
|
border-radius: 4px;
|
||||||
|
line-height: 1;
|
||||||
|
font-weight: 500;
|
||||||
|
border: none;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
box-shadow: 0 2px 6px rgba(0, 0, 0, 0.1);
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
transform: translateY(-1px);
|
||||||
|
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.15);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.el-button--primary {
|
||||||
|
background: #5f70f3;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.el-button--danger {
|
||||||
|
background: #fd5b63;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.custom-pagination {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 10px;
|
||||||
|
|
||||||
|
.el-select {
|
||||||
|
margin-right: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pagination-btn:first-child,
|
||||||
|
.pagination-btn:nth-child(2),
|
||||||
|
.pagination-btn:nth-last-child(2),
|
||||||
|
.pagination-btn:nth-child(3) {
|
||||||
|
min-width: 60px;
|
||||||
|
height: 32px;
|
||||||
|
padding: 0 12px;
|
||||||
|
border-radius: 4px;
|
||||||
|
border: 1px solid #e4e7ed;
|
||||||
|
background: #dee7ff;
|
||||||
|
color: #606266;
|
||||||
|
font-size: 14px;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background: #d7dce6;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:disabled {
|
||||||
|
opacity: 0.6;
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.pagination-btn:not(:first-child):not(:nth-child(3)):not(:nth-child(2)):not(:nth-last-child(2)) {
|
||||||
|
min-width: 28px;
|
||||||
|
height: 32px;
|
||||||
|
padding: 0;
|
||||||
|
border-radius: 4px;
|
||||||
|
border: 1px solid transparent;
|
||||||
|
background: transparent;
|
||||||
|
color: #606266;
|
||||||
|
font-size: 14px;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background: rgba(245, 247, 250, 0.3);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.pagination-btn.active {
|
||||||
|
background: #5f70f3 !important;
|
||||||
|
color: #ffffff !important;
|
||||||
|
border-color: #5f70f3 !important;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background: #6d7cf5 !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.total-text {
|
||||||
|
margin-left: 10px;
|
||||||
|
color: #606266;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.page-size-select {
|
||||||
|
width: 100px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.custom-selection-header {
|
||||||
|
text-align: center !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.transparent-table) {
|
||||||
|
background-color: transparent;
|
||||||
|
|
||||||
|
.el-table__header-wrapper {
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.el-table__header th {
|
||||||
|
background-color: white !important;
|
||||||
|
color: black;
|
||||||
|
font-weight: 600;
|
||||||
|
height: 40px;
|
||||||
|
padding: 8px 0;
|
||||||
|
font-size: 14px;
|
||||||
|
border-bottom: 1px solid #e4e7ed;
|
||||||
|
}
|
||||||
|
|
||||||
|
.el-table__body-wrapper {
|
||||||
|
td {
|
||||||
|
background-color: transparent;
|
||||||
|
border-bottom: 1px solid #e4e7ed;
|
||||||
|
padding: 8px 0;
|
||||||
|
height: 40px;
|
||||||
|
color: #606266;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.el-table__row:hover>td {
|
||||||
|
background-color: #f5f7fa !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.el-table .el-button--text) {
|
||||||
|
color: #7079aa !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.el-table .el-button--text:hover) {
|
||||||
|
color: #5a64b5 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.el-checkbox__inner) {
|
||||||
|
background-color: #eeeeee !important;
|
||||||
|
border-color: #cccccc !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.el-checkbox__inner:hover) {
|
||||||
|
border-color: #cccccc !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.el-checkbox__input.is-checked .el-checkbox__inner) {
|
||||||
|
background-color: #5f70f3 !important;
|
||||||
|
border-color: #5f70f3 !important;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -15,9 +15,20 @@
|
|||||||
<div class="content-panel">
|
<div class="content-panel">
|
||||||
<div class="content-area">
|
<div class="content-area">
|
||||||
<el-card class="params-card" shadow="never">
|
<el-card class="params-card" shadow="never">
|
||||||
<el-table ref="paramsTable" :data="paramsList" class="transparent-table"
|
<el-table
|
||||||
:header-cell-class-name="headerCellClassName">
|
ref="paramsTable"
|
||||||
<el-table-column label="选择" type="selection" align="center" width="120"></el-table-column>
|
:data="paramsList"
|
||||||
|
class="transparent-table"
|
||||||
|
v-loading="loading"
|
||||||
|
element-loading-text="拼命加载中"
|
||||||
|
element-loading-spinner="el-icon-loading"
|
||||||
|
element-loading-background="rgba(255, 255, 255, 0.7)"
|
||||||
|
:header-cell-class-name="headerCellClassName">
|
||||||
|
<el-table-column label="选择" align="center" width="120">
|
||||||
|
<template slot-scope="scope">
|
||||||
|
<el-checkbox v-model="scope.row.selected"></el-checkbox>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
<el-table-column label="参数编码" prop="paramCode" align="center"></el-table-column>
|
<el-table-column label="参数编码" prop="paramCode" align="center"></el-table-column>
|
||||||
<el-table-column label="参数值" prop="paramValue" align="center"
|
<el-table-column label="参数值" prop="paramValue" align="center"
|
||||||
show-overflow-tooltip></el-table-column>
|
show-overflow-tooltip></el-table-column>
|
||||||
@@ -37,9 +48,17 @@
|
|||||||
</el-button>
|
</el-button>
|
||||||
<el-button size="mini" type="success" @click="showAddDialog">新增</el-button>
|
<el-button size="mini" type="success" @click="showAddDialog">新增</el-button>
|
||||||
<el-button size="mini" type="danger" icon="el-icon-delete"
|
<el-button size="mini" type="danger" icon="el-icon-delete"
|
||||||
@click="deleteParam($refs.paramsTable.selection)">删除</el-button>
|
@click="deleteSelectedParams">删除</el-button>
|
||||||
</div>
|
</div>
|
||||||
<div class="custom-pagination">
|
<div class="custom-pagination">
|
||||||
|
<el-select v-model="pageSize" @change="handlePageSizeChange" class="page-size-select">
|
||||||
|
<el-option
|
||||||
|
v-for="item in pageSizeOptions"
|
||||||
|
:key="item"
|
||||||
|
:label="`${item}条/页`"
|
||||||
|
:value="item">
|
||||||
|
</el-option>
|
||||||
|
</el-select>
|
||||||
<button class="pagination-btn" :disabled="currentPage === 1" @click="goFirst">
|
<button class="pagination-btn" :disabled="currentPage === 1" @click="goFirst">
|
||||||
首页
|
首页
|
||||||
</button>
|
</button>
|
||||||
@@ -65,7 +84,6 @@
|
|||||||
<param-dialog :title="dialogTitle" :visible.sync="dialogVisible" :form="paramForm" @submit="handleSubmit"
|
<param-dialog :title="dialogTitle" :visible.sync="dialogVisible" :form="paramForm" @submit="handleSubmit"
|
||||||
@cancel="dialogVisible = false" />
|
@cancel="dialogVisible = false" />
|
||||||
|
|
||||||
<div class="copyright">©2025 xiaozhi-esp32-server</div>
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@@ -81,7 +99,9 @@ export default {
|
|||||||
searchCode: "",
|
searchCode: "",
|
||||||
paramsList: [],
|
paramsList: [],
|
||||||
currentPage: 1,
|
currentPage: 1,
|
||||||
pageSize: 5,
|
loading: false,
|
||||||
|
pageSize: 10,
|
||||||
|
pageSizeOptions: [10, 20, 50, 100],
|
||||||
total: 0,
|
total: 0,
|
||||||
dialogVisible: false,
|
dialogVisible: false,
|
||||||
dialogTitle: "新增参数",
|
dialogTitle: "新增参数",
|
||||||
@@ -96,7 +116,9 @@ export default {
|
|||||||
},
|
},
|
||||||
created() {
|
created() {
|
||||||
this.fetchParams();
|
this.fetchParams();
|
||||||
|
|
||||||
},
|
},
|
||||||
|
|
||||||
computed: {
|
computed: {
|
||||||
pageCount() {
|
pageCount() {
|
||||||
return Math.ceil(this.total / this.pageSize);
|
return Math.ceil(this.total / this.pageSize);
|
||||||
@@ -118,34 +140,45 @@ export default {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
|
handlePageSizeChange(val) {
|
||||||
|
this.pageSize = val;
|
||||||
|
this.currentPage = 1;
|
||||||
|
this.fetchParams();
|
||||||
|
},
|
||||||
fetchParams() {
|
fetchParams() {
|
||||||
Api.admin.getParamsList(
|
this.loading = true;
|
||||||
{
|
Api.admin.getParamsList(
|
||||||
page: this.currentPage,
|
{
|
||||||
limit: this.pageSize,
|
page: this.currentPage,
|
||||||
paramCode: this.searchCode,
|
limit: this.pageSize,
|
||||||
},
|
paramCode: this.searchCode,
|
||||||
({ data }) => {
|
},
|
||||||
if (data.code === 0) {
|
({ data }) => {
|
||||||
this.paramsList = data.data.list;
|
this.loading = false;
|
||||||
this.total = data.data.total;
|
if (data.code === 0) {
|
||||||
} else {
|
this.paramsList = data.data.list.map(item => ({
|
||||||
this.$message.error(data.msg || '获取参数列表失败');
|
...item,
|
||||||
}
|
selected: false
|
||||||
}
|
}));
|
||||||
);
|
this.total = data.data.total;
|
||||||
|
} else {
|
||||||
|
this.$message.error({
|
||||||
|
message: data.msg || '获取参数列表失败',
|
||||||
|
showClose: true
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
},
|
},
|
||||||
handleSearch() {
|
handleSearch() {
|
||||||
this.currentPage = 1;
|
this.currentPage = 1;
|
||||||
this.fetchParams();
|
this.fetchParams();
|
||||||
},
|
},
|
||||||
handleSelectAll() {
|
handleSelectAll() {
|
||||||
if (this.isAllSelected) {
|
|
||||||
this.$refs.paramsTable.clearSelection();
|
|
||||||
} else {
|
|
||||||
this.$refs.paramsTable.toggleAllSelection();
|
|
||||||
}
|
|
||||||
this.isAllSelected = !this.isAllSelected;
|
this.isAllSelected = !this.isAllSelected;
|
||||||
|
this.paramsList.forEach(row => {
|
||||||
|
row.selected = this.isAllSelected;
|
||||||
|
});
|
||||||
},
|
},
|
||||||
showAddDialog() {
|
showAddDialog() {
|
||||||
this.dialogTitle = "新增参数";
|
this.dialogTitle = "新增参数";
|
||||||
@@ -162,37 +195,60 @@ export default {
|
|||||||
this.paramForm = { ...row };
|
this.paramForm = { ...row };
|
||||||
this.dialogVisible = true;
|
this.dialogVisible = true;
|
||||||
},
|
},
|
||||||
handleSubmit(form) {
|
|
||||||
if (form.id) {
|
handleSubmit({ form, done }) {
|
||||||
// 编辑
|
if (form.id) {
|
||||||
Api.admin.updateParam(form, ({ data }) => {
|
// 编辑
|
||||||
if (data.code === 0) {
|
Api.admin.updateParam(form, ({ data }) => {
|
||||||
this.$message.success("修改成功");
|
if (data.code === 0) {
|
||||||
this.dialogVisible = false;
|
this.$message.success({
|
||||||
this.fetchParams();
|
message:"修改成功",
|
||||||
}
|
showClose:true
|
||||||
});
|
});
|
||||||
} else {
|
this.dialogVisible = false;
|
||||||
// 新增
|
this.fetchParams();
|
||||||
Api.admin.addParam(form, ({ data }) => {
|
}
|
||||||
if (data.code === 0) {
|
done && done();
|
||||||
this.$message.success("新增成功");
|
});
|
||||||
this.dialogVisible = false;
|
} else {
|
||||||
this.fetchParams();
|
// 新增
|
||||||
}
|
Api.admin.addParam(form, ({ data }) => {
|
||||||
|
if (data.code === 0) {
|
||||||
|
this.$message.success({
|
||||||
|
message:"新增成功",
|
||||||
|
showClose:true
|
||||||
});
|
});
|
||||||
|
this.dialogVisible = false;
|
||||||
|
this.fetchParams();
|
||||||
|
}
|
||||||
|
done && done();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
deleteSelectedParams() {
|
||||||
|
const selectedRows = this.paramsList.filter(row => row.selected);
|
||||||
|
if (selectedRows.length === 0) {
|
||||||
|
this.$message.warning({
|
||||||
|
message: "请先选择需要删除的参数",
|
||||||
|
showClose: true
|
||||||
|
});
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
this.deleteParam(selectedRows);
|
||||||
},
|
},
|
||||||
deleteParam(row) {
|
deleteParam(row) {
|
||||||
// 处理单个参数或参数数组
|
// 处理单个参数或参数数组
|
||||||
const params = Array.isArray(row) ? row : [row];
|
const params = Array.isArray(row) ? row : [row];
|
||||||
|
|
||||||
if (Array.isArray(row) && row.length === 0) {
|
if (Array.isArray(row) && row.length === 0) {
|
||||||
this.$message.warning("请先选择需要删除的参数");
|
this.$message.warning({
|
||||||
|
message:"请先选择需要删除的参数",
|
||||||
|
showClose:true
|
||||||
|
});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
const paramCount = params.length;
|
const paramCount = params.length;
|
||||||
this.$confirm(`确定要删除选中的${paramCount}个参数吗?`, '警告', {
|
this.$confirm(`确定要删除选中的${paramCount}个参数吗?`, '警告', {
|
||||||
confirmButtonText: '确定',
|
confirmButtonText: '确定',
|
||||||
@@ -202,7 +258,10 @@ export default {
|
|||||||
}).then(() => {
|
}).then(() => {
|
||||||
const ids = params.map(param => param.id);
|
const ids = params.map(param => param.id);
|
||||||
if (ids.some(id => isNaN(id))) {
|
if (ids.some(id => isNaN(id))) {
|
||||||
this.$message.error('存在无效的参数ID');
|
this.$message.error({
|
||||||
|
message:'存在无效的参数ID',
|
||||||
|
showClose: true
|
||||||
|
});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -212,7 +271,7 @@ export default {
|
|||||||
message: `成功删除${paramCount}个参数`,
|
message: `成功删除${paramCount}个参数`,
|
||||||
showClose: true
|
showClose: true
|
||||||
});
|
});
|
||||||
this.fetchParams(); // 刷新参数列表
|
this.fetchParams();
|
||||||
} else {
|
} else {
|
||||||
this.$message.error({
|
this.$message.error({
|
||||||
message: data.msg || '删除失败,请重试',
|
message: data.msg || '删除失败,请重试',
|
||||||
@@ -278,15 +337,20 @@ export default {
|
|||||||
background: linear-gradient(to bottom right, #dce8ff, #e4eeff, #e6cbfd) center;
|
background: linear-gradient(to bottom right, #dce8ff, #e4eeff, #e6cbfd) center;
|
||||||
-webkit-background-size: cover;
|
-webkit-background-size: cover;
|
||||||
-o-background-size: cover;
|
-o-background-size: cover;
|
||||||
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
.main-wrapper {
|
.main-wrapper {
|
||||||
margin: 5px 22px;
|
margin: 5px 22px;
|
||||||
border-radius: 15px;
|
border-radius: 15px;
|
||||||
min-height: 600px;
|
min-height: calc(100vh - 24vh);
|
||||||
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.1);
|
height: auto;
|
||||||
position: relative;
|
max-height: 80vh;
|
||||||
background: rgba(237, 242, 255, 0.5);
|
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.1);
|
||||||
|
position: relative;
|
||||||
|
background: rgba(237, 242, 255, 0.5);
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
}
|
}
|
||||||
|
|
||||||
.operation-bar {
|
.operation-bar {
|
||||||
@@ -331,14 +395,28 @@ export default {
|
|||||||
flex: 1;
|
flex: 1;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
min-width: 600px;
|
min-width: 600px;
|
||||||
overflow-x: auto;
|
overflow: auto;
|
||||||
background-color: white;
|
background-color: white;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
}
|
}
|
||||||
|
|
||||||
.params-card {
|
.params-card {
|
||||||
background: white;
|
background: white;
|
||||||
border: none;
|
flex: 1;
|
||||||
box-shadow: none;
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
border: none;
|
||||||
|
box-shadow: none;
|
||||||
|
overflow: hidden;
|
||||||
|
|
||||||
|
::v-deep .el-card__body {
|
||||||
|
padding: 15px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
flex: 1;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.table_bottom {
|
.table_bottom {
|
||||||
@@ -346,6 +424,7 @@ export default {
|
|||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
margin-top: 10px;
|
margin-top: 10px;
|
||||||
|
padding-bottom: 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.ctrl_btn {
|
.ctrl_btn {
|
||||||
@@ -382,29 +461,19 @@ export default {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.copyright {
|
|
||||||
text-align: center;
|
|
||||||
color: #979db1;
|
|
||||||
font-size: 12px;
|
|
||||||
font-weight: 400;
|
|
||||||
margin-top: auto;
|
|
||||||
padding: 30px 0 20px;
|
|
||||||
position: absolute;
|
|
||||||
bottom: 0;
|
|
||||||
left: 50%;
|
|
||||||
transform: translateX(-50%);
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.custom-pagination {
|
.custom-pagination {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 8px;
|
gap: 10px;
|
||||||
margin-top: 15px;
|
|
||||||
|
.el-select {
|
||||||
|
margin-right: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
.pagination-btn:first-child,
|
.pagination-btn:first-child,
|
||||||
.pagination-btn:nth-child(2),
|
.pagination-btn:nth-child(2),
|
||||||
.pagination-btn:nth-last-child(2) {
|
.pagination-btn:nth-last-child(2),
|
||||||
|
.pagination-btn:nth-child(3) {
|
||||||
min-width: 60px;
|
min-width: 60px;
|
||||||
height: 32px;
|
height: 32px;
|
||||||
padding: 0 12px;
|
padding: 0 12px;
|
||||||
@@ -426,7 +495,7 @@ export default {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.pagination-btn:not(:first-child):not(:nth-child(2)):not(:nth-last-child(2)) {
|
.pagination-btn:not(:first-child):not(:nth-child(3)):not(:nth-child(2)):not(:nth-last-child(2)) {
|
||||||
min-width: 28px;
|
min-width: 28px;
|
||||||
height: 32px;
|
height: 32px;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
@@ -462,7 +531,19 @@ export default {
|
|||||||
|
|
||||||
:deep(.transparent-table) {
|
:deep(.transparent-table) {
|
||||||
background: white;
|
background: white;
|
||||||
|
flex: 1;
|
||||||
|
width: 100%;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
.el-table__body-wrapper {
|
||||||
|
flex: 1;
|
||||||
|
overflow-y: auto;
|
||||||
|
max-height: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.el-table__header-wrapper {
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
.el-table__header th {
|
.el-table__header th {
|
||||||
background: white !important;
|
background: white !important;
|
||||||
color: black;
|
color: black;
|
||||||
@@ -482,19 +563,6 @@ export default {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
:deep(.custom-selection-header) {
|
|
||||||
.el-checkbox {
|
|
||||||
display: none !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
&::after {
|
|
||||||
content: "选择";
|
|
||||||
display: inline-block;
|
|
||||||
color: black;
|
|
||||||
font-weight: bold;
|
|
||||||
padding-bottom: 18px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
:deep(.el-checkbox__inner) {
|
:deep(.el-checkbox__inner) {
|
||||||
background-color: #eeeeee !important;
|
background-color: #eeeeee !important;
|
||||||
@@ -550,4 +618,84 @@ export default {
|
|||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
text-overflow: ellipsis;
|
text-overflow: ellipsis;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.page-size-select {
|
||||||
|
width: 100px;
|
||||||
|
margin-right: 10px;
|
||||||
|
|
||||||
|
:deep(.el-input__inner) {
|
||||||
|
height: 32px;
|
||||||
|
line-height: 32px;
|
||||||
|
border-radius: 4px;
|
||||||
|
border: 1px solid #e4e7ed;
|
||||||
|
background: #dee7ff;
|
||||||
|
color: #606266;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.el-input__suffix) {
|
||||||
|
right: 6px;
|
||||||
|
width: 15px;
|
||||||
|
height: 20px;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
top: 6px;
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.el-input__suffix-inner) {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.el-icon-arrow-up:before) {
|
||||||
|
content: "";
|
||||||
|
display: inline-block;
|
||||||
|
border-left: 6px solid transparent;
|
||||||
|
border-right: 6px solid transparent;
|
||||||
|
border-top: 9px solid #606266;
|
||||||
|
position: relative;
|
||||||
|
transform: rotate(0deg);
|
||||||
|
transition: transform 0.3s;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.el-table) {
|
||||||
|
.el-table__body-wrapper {
|
||||||
|
transition: height 0.3s ease;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.el-table {
|
||||||
|
--table-max-height: calc(100vh - 40vh);
|
||||||
|
max-height: var(--table-max-height);
|
||||||
|
|
||||||
|
.el-table__body-wrapper {
|
||||||
|
max-height: calc(var(--table-max-height) - 40px);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.el-loading-mask) {
|
||||||
|
background-color: rgba(255, 255, 255, 0.6) !important;
|
||||||
|
backdrop-filter: blur(2px);
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.el-loading-spinner .circular) {
|
||||||
|
width: 28px;
|
||||||
|
height: 28px;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.el-loading-spinner .path) {
|
||||||
|
stroke: #6b8cff;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.el-loading-text) {
|
||||||
|
color: #6b8cff !important;
|
||||||
|
font-size: 14px;
|
||||||
|
margin-top: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -15,9 +15,19 @@
|
|||||||
<div class="content-panel">
|
<div class="content-panel">
|
||||||
<div class="content-area">
|
<div class="content-area">
|
||||||
<el-card class="user-card" shadow="never">
|
<el-card class="user-card" shadow="never">
|
||||||
<el-table ref="userTable" :data="userList" class="transparent-table"
|
<el-table
|
||||||
:header-cell-class-name="headerCellClassName">
|
ref="userTable"
|
||||||
<el-table-column label="选择" type="selection" align="center" width="120"></el-table-column>
|
:data="userList"
|
||||||
|
class="transparent-table"
|
||||||
|
v-loading="loading"
|
||||||
|
element-loading-text="拼命加载中"
|
||||||
|
element-loading-spinner="el-icon-loading"
|
||||||
|
element-loading-background="rgba(255, 255, 255, 0.7)">
|
||||||
|
<el-table-column label="选择" align="center" width="120">
|
||||||
|
<template slot-scope="scope">
|
||||||
|
<el-checkbox v-model="scope.row.selected"></el-checkbox>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
<el-table-column label="用户Id" prop="userid" align="center"></el-table-column>
|
<el-table-column label="用户Id" prop="userid" align="center"></el-table-column>
|
||||||
<el-table-column label="手机号码" prop="mobile" align="center"></el-table-column>
|
<el-table-column label="手机号码" prop="mobile" align="center"></el-table-column>
|
||||||
<el-table-column label="设备数量" prop="deviceCount" align="center"></el-table-column>
|
<el-table-column label="设备数量" prop="deviceCount" align="center"></el-table-column>
|
||||||
@@ -42,7 +52,7 @@
|
|||||||
<div class="table_bottom">
|
<div class="table_bottom">
|
||||||
<div class="ctrl_btn">
|
<div class="ctrl_btn">
|
||||||
<el-button size="mini" type="primary" class="select-all-btn" @click="handleSelectAll">
|
<el-button size="mini" type="primary" class="select-all-btn" @click="handleSelectAll">
|
||||||
{{ isAllSelected ? '取消全选' : '全选' }}
|
{{ isAllSelected ? '取消全选' : '全选' }}
|
||||||
</el-button>
|
</el-button>
|
||||||
<el-button size="mini" type="success" icon="el-icon-circle-check" @click="batchEnable">启用</el-button>
|
<el-button size="mini" type="success" icon="el-icon-circle-check" @click="batchEnable">启用</el-button>
|
||||||
<el-button size="mini" type="warning" @click="batchDisable"><i
|
<el-button size="mini" type="warning" @click="batchDisable"><i
|
||||||
@@ -50,6 +60,15 @@
|
|||||||
<el-button size="mini" type="danger" icon="el-icon-delete" @click="batchDelete">删除</el-button>
|
<el-button size="mini" type="danger" icon="el-icon-delete" @click="batchDelete">删除</el-button>
|
||||||
</div>
|
</div>
|
||||||
<div class="custom-pagination">
|
<div class="custom-pagination">
|
||||||
|
<el-select v-model="pageSize" @change="handlePageSizeChange" class="page-size-select">
|
||||||
|
<el-option
|
||||||
|
v-for="item in pageSizeOptions"
|
||||||
|
:key="item"
|
||||||
|
:label="`${item}条/页`"
|
||||||
|
:value="item">
|
||||||
|
</el-option>
|
||||||
|
</el-select>
|
||||||
|
|
||||||
<button class="pagination-btn" :disabled="currentPage === 1" @click="goFirst">
|
<button class="pagination-btn" :disabled="currentPage === 1" @click="goFirst">
|
||||||
首页
|
首页
|
||||||
</button>
|
</button>
|
||||||
@@ -70,7 +89,6 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="copyright">©2025 xiaozhi-esp32-server</div>
|
|
||||||
<view-password-dialog :visible.sync="showViewPassword" :password="currentPassword" />
|
<view-password-dialog :visible.sync="showViewPassword" :password="currentPassword" />
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
@@ -88,10 +106,12 @@ export default {
|
|||||||
currentPassword: "",
|
currentPassword: "",
|
||||||
searchPhone: "",
|
searchPhone: "",
|
||||||
userList: [],
|
userList: [],
|
||||||
|
pageSizeOptions: [10, 20, 50, 100],
|
||||||
currentPage: 1,
|
currentPage: 1,
|
||||||
pageSize: 5,
|
pageSize: 10,
|
||||||
total: 0,
|
total: 0,
|
||||||
isAllSelected: false
|
isAllSelected: false,
|
||||||
|
loading: false,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
created() {
|
created() {
|
||||||
@@ -118,7 +138,14 @@ export default {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
|
handlePageSizeChange(val) {
|
||||||
|
this.pageSize = val;
|
||||||
|
this.currentPage = 1;
|
||||||
|
this.fetchUsers();
|
||||||
|
},
|
||||||
|
|
||||||
fetchUsers() {
|
fetchUsers() {
|
||||||
|
this.loading = true;
|
||||||
Api.admin.getUserList(
|
Api.admin.getUserList(
|
||||||
{
|
{
|
||||||
page: this.currentPage,
|
page: this.currentPage,
|
||||||
@@ -126,8 +153,12 @@ export default {
|
|||||||
mobile: this.searchPhone,
|
mobile: this.searchPhone,
|
||||||
},
|
},
|
||||||
({ data }) => {
|
({ data }) => {
|
||||||
|
this.loading = false; // 结束加载
|
||||||
if (data.code === 0) {
|
if (data.code === 0) {
|
||||||
this.userList = data.data.list
|
this.userList = data.data.list.map(item => ({
|
||||||
|
...item,
|
||||||
|
selected: false
|
||||||
|
}));
|
||||||
this.total = data.data.total;
|
this.total = data.data.total;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -138,15 +169,13 @@ export default {
|
|||||||
this.fetchUsers();
|
this.fetchUsers();
|
||||||
},
|
},
|
||||||
handleSelectAll() {
|
handleSelectAll() {
|
||||||
if (this.isAllSelected) {
|
this.isAllSelected = !this.isAllSelected;
|
||||||
this.$refs.userTable.clearSelection();
|
this.userList.forEach(row => {
|
||||||
} else {
|
row.selected = this.isAllSelected;
|
||||||
this.$refs.userTable.toggleAllSelection();
|
});
|
||||||
}
|
|
||||||
this.isAllSelected = !this.isAllSelected;
|
|
||||||
},
|
},
|
||||||
batchDelete() {
|
batchDelete() {
|
||||||
const selectedUsers = this.$refs.userTable.selection;
|
const selectedUsers = this.userList.filter(user => user.selected);
|
||||||
if (selectedUsers.length === 0) {
|
if (selectedUsers.length === 0) {
|
||||||
this.$message.warning("请先选择需要删除的用户");
|
this.$message.warning("请先选择需要删除的用户");
|
||||||
return;
|
return;
|
||||||
@@ -211,20 +240,12 @@ export default {
|
|||||||
});
|
});
|
||||||
},
|
},
|
||||||
batchEnable() {
|
batchEnable() {
|
||||||
const selectedUsers = this.$refs.userTable.selection;
|
const selectedUsers = this.userList.filter(user => user.selected);
|
||||||
if (selectedUsers.length === 0) {
|
this.handleChangeStatus(selectedUsers, 1);
|
||||||
this.$message.warning("请先选择需要启用的用户");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
this.handleChangeStatus(selectedUsers, 1);
|
|
||||||
},
|
},
|
||||||
batchDisable() {
|
batchDisable() {
|
||||||
const selectedUsers = this.$refs.userTable.selection;
|
const selectedUsers = this.userList.filter(user => user.selected);
|
||||||
if (selectedUsers.length === 0) {
|
this.handleChangeStatus(selectedUsers, 0);
|
||||||
this.$message.warning("请先选择需要禁用的用户");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
this.handleChangeStatus(selectedUsers, 0);
|
|
||||||
},
|
},
|
||||||
resetPassword(row) {
|
resetPassword(row) {
|
||||||
this.$confirm("重置后将会生成新密码,是否继续?", "提示", {
|
this.$confirm("重置后将会生成新密码,是否继续?", "提示", {
|
||||||
@@ -267,12 +288,6 @@ export default {
|
|||||||
})
|
})
|
||||||
.catch(() => { });
|
.catch(() => { });
|
||||||
},
|
},
|
||||||
headerCellClassName({ columnIndex }) {
|
|
||||||
if (columnIndex === 0) {
|
|
||||||
return "custom-selection-header";
|
|
||||||
}
|
|
||||||
return "";
|
|
||||||
},
|
|
||||||
goFirst() {
|
goFirst() {
|
||||||
this.currentPage = 1;
|
this.currentPage = 1;
|
||||||
this.fetchUsers();
|
this.fetchUsers();
|
||||||
@@ -344,15 +359,20 @@ export default {
|
|||||||
background: linear-gradient(to bottom right, #dce8ff, #e4eeff, #e6cbfd) center;
|
background: linear-gradient(to bottom right, #dce8ff, #e4eeff, #e6cbfd) center;
|
||||||
-webkit-background-size: cover;
|
-webkit-background-size: cover;
|
||||||
-o-background-size: cover;
|
-o-background-size: cover;
|
||||||
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
.main-wrapper {
|
.main-wrapper {
|
||||||
margin: 5px 22px;
|
margin: 5px 22px;
|
||||||
border-radius: 15px;
|
border-radius: 15px;
|
||||||
min-height: 600px;
|
min-height: calc(100vh - 24vh);
|
||||||
|
height: auto;
|
||||||
|
max-height: 80vh;
|
||||||
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.1);
|
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.1);
|
||||||
position: relative;
|
position: relative;
|
||||||
background: rgba(237, 242, 255, 0.5);
|
background: rgba(237, 242, 255, 0.5);
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
}
|
}
|
||||||
|
|
||||||
.operation-bar {
|
.operation-bar {
|
||||||
@@ -399,12 +419,26 @@ export default {
|
|||||||
min-width: 600px;
|
min-width: 600px;
|
||||||
overflow-x: auto;
|
overflow-x: auto;
|
||||||
background-color: white;
|
background-color: white;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
}
|
}
|
||||||
|
|
||||||
.user-card {
|
.user-card {
|
||||||
background: white;
|
background: white;
|
||||||
|
flex: 1;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
border: none;
|
border: none;
|
||||||
box-shadow: none;
|
box-shadow: none;
|
||||||
|
overflow: hidden;
|
||||||
|
|
||||||
|
::v-deep .el-card__body {
|
||||||
|
padding: 15px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
flex: 1;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.table_bottom {
|
.table_bottom {
|
||||||
@@ -465,28 +499,18 @@ export default {
|
|||||||
color: black;
|
color: black;
|
||||||
}
|
}
|
||||||
|
|
||||||
.copyright {
|
|
||||||
text-align: center;
|
|
||||||
color: #979db1;
|
|
||||||
font-size: 12px;
|
|
||||||
font-weight: 400;
|
|
||||||
margin-top: auto;
|
|
||||||
padding: 30px 0 20px;
|
|
||||||
position: absolute;
|
|
||||||
bottom: 0;
|
|
||||||
left: 50%;
|
|
||||||
transform: translateX(-50%);
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.custom-pagination {
|
.custom-pagination {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 8px;
|
gap: 8px;
|
||||||
margin-top: 15px;
|
|
||||||
|
.el-select {
|
||||||
|
margin-right: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
.pagination-btn:first-child,
|
.pagination-btn:first-child,
|
||||||
.pagination-btn:nth-child(2),
|
.pagination-btn:nth-child(2),
|
||||||
|
.pagination-btn:nth-child(3),
|
||||||
.pagination-btn:nth-last-child(2) {
|
.pagination-btn:nth-last-child(2) {
|
||||||
min-width: 60px;
|
min-width: 60px;
|
||||||
height: 32px;
|
height: 32px;
|
||||||
@@ -509,7 +533,7 @@ export default {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.pagination-btn:not(:first-child):not(:nth-child(2)):not(:nth-last-child(2)) {
|
.pagination-btn:not(:first-child):not(:nth-child(2)):not(:nth-child(3)):not(:nth-last-child(2)) {
|
||||||
min-width: 28px;
|
min-width: 28px;
|
||||||
height: 32px;
|
height: 32px;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
@@ -545,6 +569,19 @@ export default {
|
|||||||
|
|
||||||
:deep(.transparent-table) {
|
:deep(.transparent-table) {
|
||||||
background: white;
|
background: white;
|
||||||
|
flex: 1;
|
||||||
|
width: 100%;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
.el-table__body-wrapper {
|
||||||
|
flex: 1;
|
||||||
|
overflow-y: auto;
|
||||||
|
max-height: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.el-table__header-wrapper {
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
.el-table__header th {
|
.el-table__header th {
|
||||||
background: white !important;
|
background: white !important;
|
||||||
@@ -573,19 +610,6 @@ export default {
|
|||||||
color: #5a64b5 !important;
|
color: #5a64b5 !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
:deep(.custom-selection-header) {
|
|
||||||
.el-checkbox {
|
|
||||||
display: none !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
&::after {
|
|
||||||
content: "选择";
|
|
||||||
display: inline-block;
|
|
||||||
color: black;
|
|
||||||
font-weight: bold;
|
|
||||||
padding-bottom: 18px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
:deep(.el-checkbox__inner) {
|
:deep(.el-checkbox__inner) {
|
||||||
background-color: #eeeeee !important;
|
background-color: #eeeeee !important;
|
||||||
@@ -622,4 +646,75 @@ export default {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.page-size-select {
|
||||||
|
width: 100px;
|
||||||
|
margin-right: 10px;
|
||||||
|
|
||||||
|
:deep(.el-input__inner) {
|
||||||
|
height: 32px;
|
||||||
|
line-height: 32px;
|
||||||
|
border-radius: 4px;
|
||||||
|
border: 1px solid #e4e7ed;
|
||||||
|
background: #dee7ff;
|
||||||
|
color: #606266;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.el-input__suffix) {
|
||||||
|
right: 6px;
|
||||||
|
width: 15px;
|
||||||
|
height: 20px;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
top: 6px;
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.el-input__suffix-inner) {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.el-icon-arrow-up:before) {
|
||||||
|
content: "";
|
||||||
|
display: inline-block;
|
||||||
|
border-left: 6px solid transparent;
|
||||||
|
border-right: 6px solid transparent;
|
||||||
|
border-top: 9px solid #606266;
|
||||||
|
position: relative;
|
||||||
|
transform: rotate(0deg);
|
||||||
|
transition: transform 0.3s;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.el-table {
|
||||||
|
--table-max-height: calc(100vh - 40vh);
|
||||||
|
max-height: var(--table-max-height);
|
||||||
|
|
||||||
|
.el-table__body-wrapper {
|
||||||
|
max-height: calc(var(--table-max-height) - 40px);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.el-loading-mask) {
|
||||||
|
background-color: rgba(255, 255, 255, 0.6) !important;
|
||||||
|
backdrop-filter: blur(2px);
|
||||||
|
}
|
||||||
|
:deep(.el-loading-spinner .circular) {
|
||||||
|
width: 28px;
|
||||||
|
height: 28px;
|
||||||
|
}
|
||||||
|
:deep(.el-loading-spinner .path) {
|
||||||
|
stroke: #6b8cff;
|
||||||
|
}
|
||||||
|
:deep(.el-loading-text) {
|
||||||
|
color: #6b8cff !important;
|
||||||
|
font-size: 14px;
|
||||||
|
margin-top: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -19,27 +19,49 @@
|
|||||||
<div class="hi-hint">
|
<div class="hi-hint">
|
||||||
Hello, Let's have a wonderful day!
|
Hello, Let's have a wonderful day!
|
||||||
</div>
|
</div>
|
||||||
<div class="add-device-btn" @click="showAddDialog">
|
<div class="add-device-btn">
|
||||||
<div class="left-add">
|
<div class="left-add" @click="showAddDialog">
|
||||||
添加智能体
|
添加智能体
|
||||||
</div>
|
</div>
|
||||||
<div style="width: 23px;height: 13px;background: #5778ff;margin-left: -10px;" />
|
<div style="width: 23px;height: 13px;background: #5778ff;margin-left: -10px;" />
|
||||||
<div class="right-add">
|
<div class="right-add">
|
||||||
<i class="el-icon-right" style="font-size: 20px;color: #fff;" />
|
<i class="el-icon-right" @click="showAddDialog" style="font-size: 20px;color: #fff;" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="device-list-container">
|
<div class="device-list-container">
|
||||||
<DeviceItem v-for="(item, index) in devices" :key="index" :device="item" @configure="goToRoleConfig"
|
<template v-if="isLoading">
|
||||||
@deviceManage="handleDeviceManage" @delete="handleDeleteAgent" />
|
<div
|
||||||
|
v-for="i in skeletonCount"
|
||||||
|
:key="'skeleton-'+i"
|
||||||
|
class="skeleton-item"
|
||||||
|
>
|
||||||
|
<div class="skeleton-image"></div>
|
||||||
|
<div class="skeleton-content">
|
||||||
|
<div class="skeleton-line"></div>
|
||||||
|
<div class="skeleton-line-short"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template v-else>
|
||||||
|
<DeviceItem
|
||||||
|
v-for="(item, index) in devices"
|
||||||
|
:key="index"
|
||||||
|
:device="item"
|
||||||
|
@configure="goToRoleConfig"
|
||||||
|
@deviceManage="handleDeviceManage"
|
||||||
|
@delete="handleDeleteAgent"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="copyright">
|
|
||||||
©2025 xiaozhi-esp32-server
|
|
||||||
</div>
|
|
||||||
<AddWisdomBodyDialog :visible.sync="addDeviceDialogVisible" @confirm="handleWisdomBodyAdded" />
|
<AddWisdomBodyDialog :visible.sync="addDeviceDialogVisible" @confirm="handleWisdomBodyAdded" />
|
||||||
</el-main>
|
</el-main>
|
||||||
|
<el-footer>
|
||||||
|
<version-footer />
|
||||||
|
</el-footer>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</template>
|
</template>
|
||||||
@@ -49,17 +71,20 @@ import Api from '@/apis/api';
|
|||||||
import AddWisdomBodyDialog from '@/components/AddWisdomBodyDialog.vue';
|
import AddWisdomBodyDialog from '@/components/AddWisdomBodyDialog.vue';
|
||||||
import DeviceItem from '@/components/DeviceItem.vue';
|
import DeviceItem from '@/components/DeviceItem.vue';
|
||||||
import HeaderBar from '@/components/HeaderBar.vue';
|
import HeaderBar from '@/components/HeaderBar.vue';
|
||||||
|
import VersionFooter from '@/components/VersionFooter.vue';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'HomePage',
|
name: 'HomePage',
|
||||||
components: { DeviceItem, AddWisdomBodyDialog, HeaderBar },
|
components: { DeviceItem, AddWisdomBodyDialog, HeaderBar, VersionFooter },
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
addDeviceDialogVisible: false,
|
addDeviceDialogVisible: false,
|
||||||
devices: [],
|
devices: [],
|
||||||
originalDevices: [],
|
originalDevices: [],
|
||||||
isSearching: false,
|
isSearching: false,
|
||||||
searchRegex: null
|
searchRegex: null,
|
||||||
|
isLoading: true,
|
||||||
|
skeletonCount: localStorage.getItem('skeletonCount') || 8,
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -108,12 +133,26 @@ export default {
|
|||||||
},
|
},
|
||||||
// 获取智能体列表
|
// 获取智能体列表
|
||||||
fetchAgentList() {
|
fetchAgentList() {
|
||||||
|
this.isLoading = true;
|
||||||
Api.agent.getAgentList(({ data }) => {
|
Api.agent.getAgentList(({ data }) => {
|
||||||
this.originalDevices = data.data.map(item => ({
|
if (data?.data) {
|
||||||
...item,
|
this.originalDevices = data.data.map(item => ({
|
||||||
agentId: item.id // 字段映射
|
...item,
|
||||||
}));
|
agentId: item.id
|
||||||
this.handleSearchReset(); // 重置搜索状态
|
}));
|
||||||
|
|
||||||
|
// 动态设置骨架屏数量(可选)
|
||||||
|
this.skeletonCount = Math.min(
|
||||||
|
Math.max(this.originalDevices.length, 3), // 最少3个
|
||||||
|
10 // 最多10个
|
||||||
|
);
|
||||||
|
|
||||||
|
this.handleSearchReset();
|
||||||
|
}
|
||||||
|
this.isLoading = false;
|
||||||
|
}, (error) => {
|
||||||
|
console.error('Failed to fetch agent list:', error);
|
||||||
|
this.isLoading = false;
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
// 删除智能体
|
// 删除智能体
|
||||||
@@ -198,7 +237,7 @@ export default {
|
|||||||
|
|
||||||
.hi-hint {
|
.hi-hint {
|
||||||
font-weight: 400;
|
font-weight: 400;
|
||||||
font-size: 10px;
|
font-size: 12px;
|
||||||
text-align: left;
|
text-align: left;
|
||||||
color: #818cae;
|
color: #818cae;
|
||||||
margin-left: 75px;
|
margin-left: 75px;
|
||||||
@@ -219,7 +258,7 @@ export default {
|
|||||||
border-radius: 17px;
|
border-radius: 17px;
|
||||||
background: #5778ff;
|
background: #5778ff;
|
||||||
color: #fff;
|
color: #fff;
|
||||||
font-size: 10px;
|
font-size: 14px;
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
line-height: 34px;
|
line-height: 34px;
|
||||||
@@ -260,4 +299,66 @@ export default {
|
|||||||
text-align: center;
|
text-align: center;
|
||||||
/* 居中显示 */
|
/* 居中显示 */
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* 骨架屏动画 */
|
||||||
|
@keyframes shimmer {
|
||||||
|
100% { transform: translateX(100%); }
|
||||||
|
}
|
||||||
|
|
||||||
|
.skeleton-item {
|
||||||
|
background: #fff;
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 20px;
|
||||||
|
height: 120px;
|
||||||
|
position: relative;
|
||||||
|
overflow: hidden;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.skeleton-image {
|
||||||
|
width: 80px;
|
||||||
|
height: 80px;
|
||||||
|
background: #f0f2f5;
|
||||||
|
border-radius: 4px;
|
||||||
|
float: left;
|
||||||
|
position: relative;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.skeleton-content {
|
||||||
|
margin-left: 100px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.skeleton-line {
|
||||||
|
height: 16px;
|
||||||
|
background: #f0f2f5;
|
||||||
|
border-radius: 4px;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
width: 70%;
|
||||||
|
position: relative;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.skeleton-line-short {
|
||||||
|
height: 12px;
|
||||||
|
background: #f0f2f5;
|
||||||
|
border-radius: 4px;
|
||||||
|
width: 50%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.skeleton-item::after {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 50%;
|
||||||
|
height: 100%;
|
||||||
|
background: linear-gradient(
|
||||||
|
90deg,
|
||||||
|
rgba(255,255,255,0),
|
||||||
|
rgba(255,255,255,0.3),
|
||||||
|
rgba(255,255,255,0)
|
||||||
|
);
|
||||||
|
animation: shimmer 1.5s infinite;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
@@ -38,7 +38,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
style="font-weight: 400;font-size: 14px;text-align: left;color: #5778ff;display: flex;justify-content: space-between;margin-top: 20px;">
|
style="font-weight: 400;font-size: 14px;text-align: left;color: #5778ff;display: flex;justify-content: space-between;margin-top: 20px;">
|
||||||
<div style="cursor: pointer;" @click="goToRegister">新用户注册</div>
|
<div v-if="allowUserRegister" style="cursor: pointer;" @click="goToRegister">新用户注册</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="login-btn" @click="login">登录</div>
|
<div class="login-btn" @click="login">登录</div>
|
||||||
@@ -51,9 +51,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</el-main>
|
</el-main>
|
||||||
<el-footer>
|
<el-footer>
|
||||||
<div class="copyright">
|
<version-footer />
|
||||||
©2025 xiaozhi-esp32-server
|
|
||||||
</div>
|
|
||||||
</el-footer>
|
</el-footer>
|
||||||
</el-container>
|
</el-container>
|
||||||
</div>
|
</div>
|
||||||
@@ -61,11 +59,20 @@
|
|||||||
|
|
||||||
<script>
|
<script>
|
||||||
import Api from '@/apis/api';
|
import Api from '@/apis/api';
|
||||||
|
import VersionFooter from '@/components/VersionFooter.vue';
|
||||||
import { getUUID, goToPage, showDanger, showSuccess } from '@/utils';
|
import { getUUID, goToPage, showDanger, showSuccess } from '@/utils';
|
||||||
|
import { mapState } from 'vuex';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'login',
|
name: 'login',
|
||||||
|
components: {
|
||||||
|
VersionFooter
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
...mapState({
|
||||||
|
allowUserRegister: state => state.pubConfig.allowUserRegister
|
||||||
|
})
|
||||||
|
},
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
activeName: "username",
|
activeName: "username",
|
||||||
@@ -81,6 +88,7 @@ export default {
|
|||||||
},
|
},
|
||||||
mounted() {
|
mounted() {
|
||||||
this.fetchCaptcha();
|
this.fetchCaptcha();
|
||||||
|
this.$store.dispatch('fetchPubConfig');
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
fetchCaptcha() {
|
fetchCaptcha() {
|
||||||
@@ -144,7 +152,7 @@ export default {
|
|||||||
|
|
||||||
goToRegister() {
|
goToRegister() {
|
||||||
goToPage('/register')
|
goToPage('/register')
|
||||||
}
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="welcome">
|
<div class="welcome" @keyup.enter="register">
|
||||||
<el-container style="height: 100%;">
|
<el-container style="height: 100%;">
|
||||||
<!-- 保持相同的头部 -->
|
<!-- 保持相同的头部 -->
|
||||||
<el-header>
|
<el-header>
|
||||||
@@ -72,9 +72,7 @@
|
|||||||
|
|
||||||
<!-- 保持相同的页脚 -->
|
<!-- 保持相同的页脚 -->
|
||||||
<el-footer>
|
<el-footer>
|
||||||
<div class="copyright">
|
<version-footer />
|
||||||
©2025 xiaozhi-esp32-server
|
|
||||||
</div>
|
|
||||||
</el-footer>
|
</el-footer>
|
||||||
</el-container>
|
</el-container>
|
||||||
</div>
|
</div>
|
||||||
@@ -82,10 +80,20 @@
|
|||||||
|
|
||||||
<script>
|
<script>
|
||||||
import Api from '@/apis/api';
|
import Api from '@/apis/api';
|
||||||
|
import VersionFooter from '@/components/VersionFooter.vue';
|
||||||
import { getUUID, goToPage, showDanger, showSuccess } from '@/utils';
|
import { getUUID, goToPage, showDanger, showSuccess } from '@/utils';
|
||||||
|
import { mapState } from 'vuex';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'register',
|
name: 'register',
|
||||||
|
components: {
|
||||||
|
VersionFooter
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
...mapState({
|
||||||
|
allowUserRegister: state => state.pubConfig.allowUserRegister
|
||||||
|
})
|
||||||
|
},
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
form: {
|
form: {
|
||||||
@@ -99,6 +107,14 @@ export default {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
mounted() {
|
mounted() {
|
||||||
|
this.$store.dispatch('fetchPubConfig').then(() => {
|
||||||
|
if (!this.allowUserRegister) {
|
||||||
|
showDanger('当前不允许普通用户注册');
|
||||||
|
setTimeout(() => {
|
||||||
|
goToPage('/login');
|
||||||
|
}, 1500);
|
||||||
|
}
|
||||||
|
});
|
||||||
this.fetchCaptcha();
|
this.fetchCaptcha();
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
|
|||||||
@@ -1,90 +1,129 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="welcome">
|
<div class="welcome">
|
||||||
<HeaderBar />
|
<HeaderBar/>
|
||||||
<el-main style="padding: 16px;display: flex;flex-direction: column;">
|
|
||||||
<div style="border-radius: 16px;background: #fafcfe; border: 1px solid #e8f0ff;">
|
<div class="operation-bar">
|
||||||
<div
|
<h2 class="page-title">角色配置</h2>
|
||||||
style="padding: 15px 24px;font-weight: 700;font-size: 19px;text-align: left;color: #3d4566;display: flex;gap: 13px;align-items: center;">
|
</div>
|
||||||
<div
|
|
||||||
style="width: 37px;height: 37px;background: #5778ff;border-radius: 50%;display: flex;align-items: center;justify-content: center;">
|
<div class="main-wrapper">
|
||||||
<img loading="lazy" src="@/assets/home/setting-user.png" alt="" style="width: 19px;height: 19px;" />
|
<div class="content-panel">
|
||||||
</div>
|
<div class="content-area">
|
||||||
{{ form.agentName }}
|
<el-card class="config-card" shadow="never">
|
||||||
</div>
|
<div class="config-header">
|
||||||
<div style="height: 1px;background: #e8f0ff;" />
|
<div class="header-icon">
|
||||||
<el-form ref="form" :model="form" label-width="72px">
|
<img loading="lazy" src="@/assets/home/setting-user.png" alt="">
|
||||||
<div style="padding: 16px 24px;">
|
|
||||||
<div style="display: grid; grid-template-columns: 1fr 1fr; gap: 24px;">
|
|
||||||
<div>
|
|
||||||
<el-form-item label="助手昵称:">
|
|
||||||
<div class="input-46" style="width: 100%;">
|
|
||||||
<el-input v-model="form.agentName" />
|
|
||||||
</div>
|
|
||||||
</el-form-item>
|
|
||||||
<el-form-item label="角色模版:">
|
|
||||||
<div style="display: flex;gap: 8px;flex-wrap: wrap;">
|
|
||||||
<div v-for="(template, index) in templates" :key="`template-${index}`" class="template-item"
|
|
||||||
:class="{ 'template-loading': loadingTemplate }" @click="selectTemplate(template)">
|
|
||||||
{{ template.agentName }}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</el-form-item>
|
|
||||||
<el-form-item label="角色介绍:">
|
|
||||||
<div class="textarea-box">
|
|
||||||
<el-input type="textarea" rows="5" resize="none" placeholder="请输入内容" v-model="form.systemPrompt"
|
|
||||||
maxlength="2000" show-word-limit />
|
|
||||||
</div>
|
|
||||||
</el-form-item>
|
|
||||||
<el-form-item label="语言编码:">
|
|
||||||
<div class="input-46" style="width: 100%;">
|
|
||||||
<el-input v-model="form.langCode" placeholder="请输入语言编码,如:zh_CN" maxlength="10" show-word-limit />
|
|
||||||
</div>
|
|
||||||
</el-form-item>
|
|
||||||
<el-form-item label="交互语种:">
|
|
||||||
<div class="input-46" style="width: 100%;">
|
|
||||||
<el-input v-model="form.language" placeholder="请输入交互语种,如:中文" maxlength="10" show-word-limit />
|
|
||||||
</div>
|
|
||||||
</el-form-item>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<el-form-item v-for="(model, index) in models" :key="`model-${index}`" :label="model.label"
|
|
||||||
class="model-item">
|
|
||||||
<el-select v-model="form.model[model.key]" filterable placeholder="请选择" class="select-field">
|
|
||||||
<el-option v-for="(item, optionIndex) in modelOptions[model.type]"
|
|
||||||
:key="`option-${index}-${optionIndex}`" :label="item.label" :value="item.value" />
|
|
||||||
</el-select>
|
|
||||||
</el-form-item>
|
|
||||||
<el-form-item label="角色音色:">
|
|
||||||
<div style="display: flex;gap: 8px;align-items: center;">
|
|
||||||
<div class="input-46" style="width: 100%;">
|
|
||||||
<el-select v-model="form.ttsVoiceId" placeholder="请选择" style="width: 100%;">
|
|
||||||
<el-option v-for="(item, index) in voiceOptions" :key="`voice-${index}`" :label="item.label"
|
|
||||||
:value="item.value" />
|
|
||||||
</el-select>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</el-form-item>
|
|
||||||
</div>
|
</div>
|
||||||
|
<span class="header-title">{{ form.agentName }}</span>
|
||||||
|
<button class="custom-close-btn" @click="goToHome">
|
||||||
|
×
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
<div class="divider"></div>
|
||||||
</el-form>
|
|
||||||
<div style="display: flex;padding: 16px;gap: 8px;align-items: center;">
|
<el-form ref="form" :model="form" label-width="72px">
|
||||||
<div class="save-btn" @click="saveConfig">
|
<div class="form-content">
|
||||||
保存配置
|
<div class="form-grid">
|
||||||
</div>
|
<div class="form-column">
|
||||||
<div class="reset-btn" @click="resetConfig">
|
<el-form-item label="助手昵称:">
|
||||||
重制
|
<el-input v-model="form.agentName" class="form-input"/>
|
||||||
</div>
|
</el-form-item>
|
||||||
<div class="clear-text">
|
<el-form-item label="角色模版:">
|
||||||
<img loading="lazy" src="@/assets/home/red-info.png" alt="" style="width: 19px;height: 19px;" />
|
<div class="template-container">
|
||||||
保存配置后,需要重启设备,新的配置才会生效。
|
<div
|
||||||
</div>
|
v-for="(template, index) in templates"
|
||||||
|
:key="`template-${index}`"
|
||||||
|
class="template-item"
|
||||||
|
:class="{ 'template-loading': loadingTemplate }"
|
||||||
|
@click="selectTemplate(template)"
|
||||||
|
>
|
||||||
|
{{ template.agentName }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="角色介绍:">
|
||||||
|
<el-input
|
||||||
|
type="textarea"
|
||||||
|
rows="5"
|
||||||
|
resize="none"
|
||||||
|
placeholder="请输入内容"
|
||||||
|
v-model="form.systemPrompt"
|
||||||
|
maxlength="2000"
|
||||||
|
show-word-limit
|
||||||
|
class="form-textarea"
|
||||||
|
/>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="语言编码:">
|
||||||
|
<el-input
|
||||||
|
v-model="form.langCode"
|
||||||
|
placeholder="请输入语言编码,如:zh_CN"
|
||||||
|
maxlength="10"
|
||||||
|
show-word-limit
|
||||||
|
class="form-input"
|
||||||
|
/>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="交互语种:">
|
||||||
|
<el-input
|
||||||
|
v-model="form.language"
|
||||||
|
placeholder="请输入交互语种,如:中文"
|
||||||
|
maxlength="10"
|
||||||
|
show-word-limit
|
||||||
|
class="form-input"
|
||||||
|
/>
|
||||||
|
</el-form-item>
|
||||||
|
<div class="action-bar">
|
||||||
|
<el-button type="primary" class="save-btn" @click="saveConfig">保存配置</el-button>
|
||||||
|
<el-button class="reset-btn" @click="resetConfig">重置</el-button>
|
||||||
|
<div class="hint-text">
|
||||||
|
<img loading="lazy" src="@/assets/home/red-info.png" alt="">
|
||||||
|
<span>保存配置后,需要重启设备,新的配置才会生效。</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="form-column">
|
||||||
|
<el-form-item
|
||||||
|
v-for="(model, index) in models"
|
||||||
|
:key="`model-${index}`"
|
||||||
|
:label="model.label"
|
||||||
|
class="model-item"
|
||||||
|
>
|
||||||
|
<el-select
|
||||||
|
v-model="form.model[model.key]"
|
||||||
|
filterable
|
||||||
|
placeholder="请选择"
|
||||||
|
class="form-select"
|
||||||
|
>
|
||||||
|
<el-option
|
||||||
|
v-for="(item, optionIndex) in modelOptions[model.type]"
|
||||||
|
:key="`option-${index}-${optionIndex}`"
|
||||||
|
:label="item.label"
|
||||||
|
:value="item.value"
|
||||||
|
/>
|
||||||
|
</el-select>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="角色音色:">
|
||||||
|
<el-select
|
||||||
|
v-model="form.ttsVoiceId"
|
||||||
|
placeholder="请选择"
|
||||||
|
class="form-select"
|
||||||
|
>
|
||||||
|
<el-option
|
||||||
|
v-for="(item, index) in voiceOptions"
|
||||||
|
:key="`voice-${index}`"
|
||||||
|
:label="item.label"
|
||||||
|
:value="item.value"
|
||||||
|
/>
|
||||||
|
</el-select>
|
||||||
|
</el-form-item>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</el-form>
|
||||||
|
|
||||||
|
</el-card>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="copyright">
|
</div>
|
||||||
©2025 xiaozhi-esp32-server
|
|
||||||
</div>
|
|
||||||
</el-main>
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@@ -92,10 +131,9 @@
|
|||||||
import Api from '@/apis/api';
|
import Api from '@/apis/api';
|
||||||
import HeaderBar from "@/components/HeaderBar.vue";
|
import HeaderBar from "@/components/HeaderBar.vue";
|
||||||
|
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'RoleConfigPage',
|
name: 'RoleConfigPage',
|
||||||
components: { HeaderBar },
|
components: {HeaderBar},
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
form: {
|
form: {
|
||||||
@@ -116,12 +154,12 @@ export default {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
models: [
|
models: [
|
||||||
{ label: '语音活动检测(VAD)', key: 'vadModelId', type: 'VAD' },
|
{label: '语音活动检测(VAD)', key: 'vadModelId', type: 'VAD'},
|
||||||
{ label: '语音识别(ASR)', key: 'asrModelId', type: 'ASR' },
|
{label: '语音识别(ASR)', key: 'asrModelId', type: 'ASR'},
|
||||||
{ label: '大语言模型(LLM)', key: 'llmModelId', type: 'LLM' },
|
{label: '大语言模型(LLM)', key: 'llmModelId', type: 'LLM'},
|
||||||
{ label: '意图识别(Intent)', key: 'intentModelId', type: 'Intent' },
|
{label: '意图识别(Intent)', key: 'intentModelId', type: 'Intent'},
|
||||||
{ label: '记忆(Memory)', key: 'memModelId', type: 'Memory' },
|
{label: '记忆(Memory)', key: 'memModelId', type: 'Memory'},
|
||||||
{ label: '语音合成(TTS)', key: 'ttsModelId', type: 'TTS' },
|
{label: '语音合成(TTS)', key: 'ttsModelId', type: 'TTS'},
|
||||||
],
|
],
|
||||||
modelOptions: {},
|
modelOptions: {},
|
||||||
templates: [],
|
templates: [],
|
||||||
@@ -130,6 +168,9 @@ export default {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
|
goToHome() {
|
||||||
|
this.$router.push('/home');
|
||||||
|
},
|
||||||
saveConfig() {
|
saveConfig() {
|
||||||
const configData = {
|
const configData = {
|
||||||
agentCode: this.form.agentCode,
|
agentCode: this.form.agentCode,
|
||||||
@@ -146,7 +187,7 @@ export default {
|
|||||||
language: this.form.language,
|
language: this.form.language,
|
||||||
sort: this.form.sort
|
sort: this.form.sort
|
||||||
};
|
};
|
||||||
Api.agent.updateAgentConfig(this.$route.query.agentId, configData, ({ data }) => {
|
Api.agent.updateAgentConfig(this.$route.query.agentId, configData, ({data}) => {
|
||||||
if (data.code === 0) {
|
if (data.code === 0) {
|
||||||
this.$message.success({
|
this.$message.success({
|
||||||
message: '配置保存成功',
|
message: '配置保存成功',
|
||||||
@@ -166,7 +207,6 @@ export default {
|
|||||||
cancelButtonText: '取消',
|
cancelButtonText: '取消',
|
||||||
type: 'warning'
|
type: 'warning'
|
||||||
}).then(() => {
|
}).then(() => {
|
||||||
// 重置表单
|
|
||||||
this.form = {
|
this.form = {
|
||||||
agentCode: "",
|
agentCode: "",
|
||||||
agentName: "",
|
agentName: "",
|
||||||
@@ -189,10 +229,10 @@ export default {
|
|||||||
showClose: true
|
showClose: true
|
||||||
})
|
})
|
||||||
}).catch(() => {
|
}).catch(() => {
|
||||||
})
|
});
|
||||||
},
|
},
|
||||||
fetchTemplates() {
|
fetchTemplates() {
|
||||||
Api.agent.getAgentTemplate(({ data }) => {
|
Api.agent.getAgentTemplate(({data}) => {
|
||||||
if (data.code === 0) {
|
if (data.code === 0) {
|
||||||
this.templates = data.data;
|
this.templates = data.data;
|
||||||
} else {
|
} else {
|
||||||
@@ -237,7 +277,7 @@ export default {
|
|||||||
};
|
};
|
||||||
},
|
},
|
||||||
fetchAgentConfig(agentId) {
|
fetchAgentConfig(agentId) {
|
||||||
Api.agent.getDeviceConfig(agentId, ({ data }) => {
|
Api.agent.getDeviceConfig(agentId, ({data}) => {
|
||||||
if (data.code === 0) {
|
if (data.code === 0) {
|
||||||
this.form = {
|
this.form = {
|
||||||
...this.form,
|
...this.form,
|
||||||
@@ -257,9 +297,8 @@ export default {
|
|||||||
});
|
});
|
||||||
},
|
},
|
||||||
fetchModelOptions() {
|
fetchModelOptions() {
|
||||||
// 为每个模型类型获取选项
|
|
||||||
this.models.forEach(model => {
|
this.models.forEach(model => {
|
||||||
Api.model.getModelNames(model.type, '', ({ data }) => {
|
Api.model.getModelNames(model.type, '', ({data}) => {
|
||||||
if (data.code === 0) {
|
if (data.code === 0) {
|
||||||
this.$set(this.modelOptions, model.type, data.data.map(item => ({
|
this.$set(this.modelOptions, model.type, data.data.map(item => ({
|
||||||
value: item.id,
|
value: item.id,
|
||||||
@@ -276,7 +315,7 @@ export default {
|
|||||||
this.voiceOptions = [];
|
this.voiceOptions = [];
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
Api.model.getModelVoices(modelId, '', ({ data }) => {
|
Api.model.getModelVoices(modelId, '', ({data}) => {
|
||||||
if (data.code === 0 && data.data) {
|
if (data.code === 0 && data.data) {
|
||||||
this.voiceOptions = data.data.map(voice => ({
|
this.voiceOptions = data.data.map(voice => ({
|
||||||
value: voice.id,
|
value: voice.id,
|
||||||
@@ -291,7 +330,6 @@ export default {
|
|||||||
watch: {
|
watch: {
|
||||||
'form.model.ttsModelId': {
|
'form.model.ttsModelId': {
|
||||||
handler(newVal, oldVal) {
|
handler(newVal, oldVal) {
|
||||||
console.log('TTS模型变化:', newVal);
|
|
||||||
if (oldVal && newVal !== oldVal) {
|
if (oldVal && newVal !== oldVal) {
|
||||||
this.form.ttsVoiceId = '';
|
this.form.ttsVoiceId = '';
|
||||||
this.fetchVoiceOptions(newVal);
|
this.fetchVoiceOptions(newVal);
|
||||||
@@ -327,60 +365,132 @@ export default {
|
|||||||
min-height: 506px;
|
min-height: 506px;
|
||||||
height: 100vh;
|
height: 100vh;
|
||||||
display: flex;
|
display: flex;
|
||||||
|
position: relative;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
background: linear-gradient(145deg, #e6eeff, #eff0ff);
|
background: linear-gradient(to bottom right, #dce8ff, #e4eeff, #e6cbfd);
|
||||||
background-size: cover;
|
background-size: cover;
|
||||||
/* 确保背景图像覆盖整个元素 */
|
|
||||||
background-position: center;
|
|
||||||
/* 从顶部中心对齐 */
|
|
||||||
-webkit-background-size: cover;
|
-webkit-background-size: cover;
|
||||||
/* 兼容老版本WebKit浏览器 */
|
|
||||||
-o-background-size: cover;
|
-o-background-size: cover;
|
||||||
/* 兼容老版本Opera浏览器 */
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
.el-form-item ::v-deep .el-form-item__label {
|
.operation-bar {
|
||||||
font-size: 10px !important;
|
display: flex;
|
||||||
color: #3d4566 !important;
|
justify-content: space-between;
|
||||||
font-weight: 400;
|
align-items: center;
|
||||||
line-height: 22px;
|
padding: 16px 24px;
|
||||||
padding-bottom: 2px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.select-field {
|
.page-title {
|
||||||
width: 100%;
|
font-size: 24px;
|
||||||
max-width: 720px;
|
margin: 0;
|
||||||
border: 1px solid #e4e6ef;
|
color: #2c3e50;
|
||||||
background: #f6f8fb;
|
|
||||||
border-radius: 8px;
|
|
||||||
height: 36px !important;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.audio-box {
|
.main-wrapper {
|
||||||
|
margin: 5px 22px;
|
||||||
|
border-radius: 15px;
|
||||||
|
height: calc(100vh - 24vh);
|
||||||
|
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.1);
|
||||||
|
position: relative;
|
||||||
|
background: rgba(237, 242, 255, 0.5);
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.content-panel {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
height: 37px;
|
display: flex;
|
||||||
border-radius: 20px;
|
overflow: hidden;
|
||||||
border: 1px solid #e4e6ef;
|
height: 100%;
|
||||||
|
border-radius: 15px;
|
||||||
|
background: transparent;
|
||||||
|
border: 1px solid #fff;
|
||||||
}
|
}
|
||||||
|
|
||||||
.clear-btn {
|
.content-area {
|
||||||
width: 48px;
|
flex: 1;
|
||||||
height: 19px;
|
height: 100%;
|
||||||
background: #fd8383;
|
min-width: 600px;
|
||||||
border-radius: 10px;
|
overflow: auto;
|
||||||
line-height: 19px;
|
background-color: white;
|
||||||
font-size: 11px;
|
display: flex;
|
||||||
color: #fff;
|
flex-direction: column;
|
||||||
cursor: pointer;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.clear-text {
|
.config-card {
|
||||||
color: #979db1;
|
background: white;
|
||||||
font-size: 11px;
|
border: none;
|
||||||
|
box-shadow: none;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
flex: 1;
|
||||||
|
overflow-y: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.config-header {
|
||||||
|
position: relative;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
gap: 13px;
|
||||||
|
padding: 0 0 5px 0;
|
||||||
|
font-weight: 700;
|
||||||
|
font-size: 19px;
|
||||||
|
color: #3d4566;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header-icon {
|
||||||
|
width: 37px;
|
||||||
|
height: 37px;
|
||||||
|
background: #5778ff;
|
||||||
|
border-radius: 50%;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header-icon img {
|
||||||
|
width: 19px;
|
||||||
|
height: 19px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.divider {
|
||||||
|
height: 1px;
|
||||||
|
background: #e8f0ff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-content {
|
||||||
|
padding: 20px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 1fr 1fr;
|
||||||
|
gap: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-column {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-input {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-select {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-textarea {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.template-container {
|
||||||
|
display: flex;
|
||||||
gap: 8px;
|
gap: 8px;
|
||||||
margin-left: 16px;
|
flex-wrap: wrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
.template-item {
|
.template-item {
|
||||||
@@ -401,47 +511,91 @@ export default {
|
|||||||
background-color: #d0d8ff;
|
background-color: #d0d8ff;
|
||||||
}
|
}
|
||||||
|
|
||||||
.prompt-bottom {
|
.action-bar {
|
||||||
margin-bottom: 4px;
|
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: space-between;
|
flex-wrap: wrap;
|
||||||
padding: 0 16px;
|
gap: 8px;
|
||||||
|
margin-top: 20px;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
.input-46 {
|
.action-bar {
|
||||||
border: 1px solid #e4e6ef;
|
.el-button.save-btn {
|
||||||
background: #f6f8fb;
|
background: #5778ff;
|
||||||
border-radius: 8px;
|
color: white;
|
||||||
height: 36px !important;
|
border: none;
|
||||||
|
border-radius: 18px;
|
||||||
|
padding: 10px 20px;
|
||||||
|
width: 100px;
|
||||||
|
height: 35px;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.el-button.reset-btn {
|
||||||
|
background: #e6ebff;
|
||||||
|
color: #5778ff;
|
||||||
|
border: 1px solid #adbdff;
|
||||||
|
border-radius: 18px;
|
||||||
|
padding: 10px 20px;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.save-btn,
|
.hint-text {
|
||||||
.reset-btn {
|
display: flex;
|
||||||
width: 112px;
|
align-items: center;
|
||||||
height: 37px;
|
gap: 8px;
|
||||||
border-radius: 18px;
|
color: #979db1;
|
||||||
line-height: 37px;
|
font-size: 11px;
|
||||||
box-sizing: border-box;
|
margin-left: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hint-text img {
|
||||||
|
width: 19px;
|
||||||
|
height: 19px;
|
||||||
|
}
|
||||||
|
|
||||||
|
::v-deep .el-form-item__label {
|
||||||
|
font-size: 10px !important;
|
||||||
|
color: #3d4566 !important;
|
||||||
|
font-weight: 400;
|
||||||
|
line-height: 22px;
|
||||||
|
padding-bottom: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
::v-deep .el-textarea .el-input__count {
|
||||||
|
color: #909399;
|
||||||
|
background: none;
|
||||||
|
position: absolute;
|
||||||
|
font-size: 12px;
|
||||||
|
bottom: -10%;
|
||||||
|
right: 3%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.custom-close-btn {
|
||||||
|
position: absolute;
|
||||||
|
top: 25%;
|
||||||
|
right: 0;
|
||||||
|
transform: translateY(-50%);
|
||||||
|
width: 35px;
|
||||||
|
height: 35px;
|
||||||
|
border-radius: 50%;
|
||||||
|
border: 2px solid #cfcfcf;
|
||||||
|
background: none;
|
||||||
|
font-size: 30px;
|
||||||
|
font-weight: lighter;
|
||||||
|
color: #cfcfcf;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
font-size: 11px
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
z-index: 1;
|
||||||
|
padding: 0;
|
||||||
|
outline: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.save-btn {
|
.custom-close-btn:hover {
|
||||||
border-radius: 18px;
|
color: #409EFF;
|
||||||
background: #5778ff;
|
border-color: #409EFF;
|
||||||
color: #fff;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.reset-btn {
|
</style>
|
||||||
border: 1px solid #adbdff;
|
|
||||||
background: #e6ebff;
|
|
||||||
color: #5778ff;
|
|
||||||
}
|
|
||||||
|
|
||||||
.textarea-box {
|
|
||||||
border: 1px solid #e4e6ef;
|
|
||||||
border-radius: 8px;
|
|
||||||
background: #f6f8fb;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
@@ -1,400 +0,0 @@
|
|||||||
<template>
|
|
||||||
<div class="container">
|
|
||||||
<h1 class="title">XiaoZhi ESP32 Server 测试助手</h1>
|
|
||||||
<div class="chat-container" ref="chatContainer">
|
|
||||||
<div v-for="(message, index) in messages" :key="index"
|
|
||||||
:class="['message', message.role === 'user' ? 'user' : 'assistant']">
|
|
||||||
<span>{{ message.content }}</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="controls">
|
|
||||||
<button @click="toggleRecording" :disabled="wsStatus !== 'connected'" :class="{ recording: isRecording }">
|
|
||||||
{{ isRecording ? '停止录音' : '开始录音' }}
|
|
||||||
</button>
|
|
||||||
<p>WebSocket: {{ wsStatus }}</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script>
|
|
||||||
import { OpusDecoder } from 'opus-decoder';
|
|
||||||
import Recorder from 'opus-recorder';
|
|
||||||
|
|
||||||
export default {
|
|
||||||
name: 'TestPage',
|
|
||||||
data() {
|
|
||||||
return {
|
|
||||||
messages: [],
|
|
||||||
wsStatus: 'disconnected',
|
|
||||||
isRecording: false,
|
|
||||||
ws: null,
|
|
||||||
recorder: null,
|
|
||||||
stream: null,
|
|
||||||
audioContext: null,
|
|
||||||
sourceNode: null,
|
|
||||||
audioDecoder: null,
|
|
||||||
audioBufferQueue: [], // 当前句子的音频缓冲区
|
|
||||||
playbackQueue: [], // 播放队列,存储待播放的句子
|
|
||||||
isPlaying: false,
|
|
||||||
}
|
|
||||||
},
|
|
||||||
methods: {
|
|
||||||
connectWebSocket() {
|
|
||||||
this.ws = new WebSocket('ws://192.168.3.97:8000');//修改服务端地址
|
|
||||||
this.ws.binaryType = 'arraybuffer';
|
|
||||||
this.ws.onopen = () => {
|
|
||||||
this.wsStatus = 'connected';
|
|
||||||
console.log('WebSocket 连接成功');
|
|
||||||
this.ws.send(JSON.stringify({ type: 'auth', 'device-id': 'test-device' }));
|
|
||||||
this.audioDecoder = new OpusDecoder({ sampleRate: 16000, channels: 1 });
|
|
||||||
};
|
|
||||||
this.ws.onmessage = async (event) => {
|
|
||||||
if (typeof event.data === 'string') {
|
|
||||||
const msg = JSON.parse(event.data);
|
|
||||||
console.log('收到文本消息:', msg);
|
|
||||||
|
|
||||||
if (msg.type === 'stt') {
|
|
||||||
this.messages.push({ role: 'user', content: msg.text });
|
|
||||||
this.scrollToBottom();
|
|
||||||
} else if (msg.type === 'llm') {
|
|
||||||
this.messages.push({ role: 'assistant', content: msg.text });
|
|
||||||
this.scrollToBottom();
|
|
||||||
} else if (msg.type === 'tts') {
|
|
||||||
if (msg.state === 'sentence_start') {
|
|
||||||
this.audioBufferQueue = [];
|
|
||||||
const lastMessage = this.messages[this.messages.length - 1];
|
|
||||||
if (!lastMessage || lastMessage.content !== msg.text) {
|
|
||||||
this.messages.push({ role: 'assistant', content: msg.text });
|
|
||||||
this.scrollToBottom();
|
|
||||||
}
|
|
||||||
} else if (msg.state === 'sentence_end') {
|
|
||||||
if (this.audioBufferQueue.length > 0) {
|
|
||||||
this.playbackQueue.push([...this.audioBufferQueue]);
|
|
||||||
this.audioBufferQueue = [];
|
|
||||||
this.playNextInQueue();
|
|
||||||
}
|
|
||||||
} else if (msg.state === 'stop') {
|
|
||||||
console.log('TTS 任务结束');
|
|
||||||
if (this.audioBufferQueue.length > 0) {
|
|
||||||
this.playbackQueue.push([...this.audioBufferQueue]);
|
|
||||||
this.audioBufferQueue = [];
|
|
||||||
this.playNextInQueue();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else if (msg.type === 'hello') {
|
|
||||||
console.log('收到服务器初始化消息:', msg);
|
|
||||||
}
|
|
||||||
} else if (event.data instanceof ArrayBuffer) {
|
|
||||||
console.log('收到音频帧,大小:', event.data.byteLength, '字节');
|
|
||||||
const opusFrame = new Uint8Array(event.data);
|
|
||||||
const frameHead = Array.from(opusFrame.slice(0, 8))
|
|
||||||
.map(b => b.toString(16).padStart(2, '0'))
|
|
||||||
.join(' ');
|
|
||||||
console.log('音频帧前8字节:', frameHead);
|
|
||||||
|
|
||||||
try {
|
|
||||||
const decoded = this.audioDecoder.decodeFrame(opusFrame);
|
|
||||||
console.log('解码结果:', decoded);
|
|
||||||
if (decoded && decoded.channelData && decoded.channelData[0]) {
|
|
||||||
const pcmData = decoded.channelData[0];
|
|
||||||
if (pcmData.length > 0) {
|
|
||||||
this.audioBufferQueue.push(pcmData);
|
|
||||||
console.log('解码音频帧,PCM 数据长度:', pcmData.length);
|
|
||||||
if (this.audioBufferQueue.length >= 5 && this.playbackQueue.length === 0 && !this.isPlaying) {
|
|
||||||
this.playbackQueue.push([...this.audioBufferQueue]);
|
|
||||||
this.audioBufferQueue = [];
|
|
||||||
this.playNextInQueue();
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
console.warn('解码成功,但 PCM 数据长度为 0');
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
console.warn('解码结果无效:', decoded);
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
console.error('Opus 解码错误:', e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
this.ws.onerror = (error) => {
|
|
||||||
console.error('WebSocket 错误:', error);
|
|
||||||
this.wsStatus = 'error';
|
|
||||||
};
|
|
||||||
this.ws.onclose = () => {
|
|
||||||
this.wsStatus = 'disconnected';
|
|
||||||
console.log('WebSocket 断开');
|
|
||||||
setTimeout(this.connectWebSocket, 1000);
|
|
||||||
};
|
|
||||||
},
|
|
||||||
scrollToBottom() {
|
|
||||||
this.$nextTick(() => {
|
|
||||||
if (this.$refs.chatContainer) {
|
|
||||||
this.$refs.chatContainer.scrollTop = this.$refs.chatContainer.scrollHeight;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
},
|
|
||||||
async playNextInQueue() {
|
|
||||||
if (this.isPlaying || this.playbackQueue.length === 0) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
this.isPlaying = true;
|
|
||||||
|
|
||||||
if (!this.audioContext || this.audioContext.state === 'closed') {
|
|
||||||
this.audioContext = new (window.AudioContext || window.webkitAudioContext)({ sampleRate: 16000 });
|
|
||||||
}
|
|
||||||
|
|
||||||
const pcmBuffers = this.playbackQueue.shift();
|
|
||||||
const totalLength = pcmBuffers.reduce((sum, pcm) => sum + pcm.length, 0);
|
|
||||||
if (totalLength === 0) {
|
|
||||||
console.error('音频缓冲区总长度为 0,无法播放');
|
|
||||||
this.isPlaying = false;
|
|
||||||
this.playNextInQueue();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const mergedPcm = new Float32Array(totalLength);
|
|
||||||
let offset = 0;
|
|
||||||
for (const pcm of pcmBuffers) {
|
|
||||||
mergedPcm.set(pcm, offset);
|
|
||||||
offset += pcm.length;
|
|
||||||
}
|
|
||||||
|
|
||||||
const audioBuffer = this.audioContext.createBuffer(1, totalLength, 16000);
|
|
||||||
audioBuffer.getChannelData(0).set(mergedPcm);
|
|
||||||
|
|
||||||
const source = this.audioContext.createBufferSource();
|
|
||||||
source.buffer = audioBuffer;
|
|
||||||
source.connect(this.audioContext.destination);
|
|
||||||
source.onended = () => {
|
|
||||||
this.isPlaying = false;
|
|
||||||
console.log('音频播放结束');
|
|
||||||
this.playNextInQueue();
|
|
||||||
};
|
|
||||||
source.start();
|
|
||||||
console.log('开始播放音频,总长度:', totalLength);
|
|
||||||
},
|
|
||||||
stripOggContainer(data) {
|
|
||||||
let arrayBuffer;
|
|
||||||
if (data instanceof ArrayBuffer) {
|
|
||||||
arrayBuffer = data;
|
|
||||||
} else if (data.buffer instanceof ArrayBuffer) {
|
|
||||||
arrayBuffer = data.buffer;
|
|
||||||
} else {
|
|
||||||
console.error('输入数据类型不支持:', data);
|
|
||||||
return [data];
|
|
||||||
}
|
|
||||||
|
|
||||||
const dataView = new DataView(arrayBuffer);
|
|
||||||
const frames = [];
|
|
||||||
let offset = 0;
|
|
||||||
|
|
||||||
while (offset < arrayBuffer.byteLength) {
|
|
||||||
if (dataView.getUint32(offset) === 0x4F676753) { // 'OggS'
|
|
||||||
const segmentTableLength = dataView.getUint8(offset + 26);
|
|
||||||
const segmentTableOffset = offset + 27;
|
|
||||||
let segmentOffset = segmentTableOffset + segmentTableLength;
|
|
||||||
|
|
||||||
for (let i = 0; i < segmentTableLength; i++) {
|
|
||||||
const segmentLength = dataView.getUint8(segmentTableOffset + i);
|
|
||||||
const segmentData = arrayBuffer.slice(segmentOffset, segmentOffset + segmentLength);
|
|
||||||
segmentOffset += segmentLength;
|
|
||||||
|
|
||||||
const header = new TextDecoder().decode(segmentData.slice(0, 8));
|
|
||||||
if (header === 'OpusHead' || header === 'OpusTags') {
|
|
||||||
console.log(`跳过 ${header},大小: ${segmentLength} 字节`);
|
|
||||||
} else if (segmentLength >= 50 && segmentLength <= 300) {
|
|
||||||
frames.push(segmentData);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
offset = segmentOffset;
|
|
||||||
} else {
|
|
||||||
const remainingLength = arrayBuffer.byteLength - offset;
|
|
||||||
if (remainingLength >= 50 && remainingLength <= 300) {
|
|
||||||
frames.push(arrayBuffer.slice(offset));
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (frames.length === 0) {
|
|
||||||
console.warn('未找到有效裸 Opus 帧,返回原始数据');
|
|
||||||
return [arrayBuffer];
|
|
||||||
}
|
|
||||||
console.log('剥离后找到', frames.length, '个裸 Opus 帧');
|
|
||||||
return frames;
|
|
||||||
},
|
|
||||||
async initRecorder() {
|
|
||||||
console.log('开始初始化录音');
|
|
||||||
try {
|
|
||||||
if (this.stream) {
|
|
||||||
this.stream.getTracks().forEach(track => track.stop());
|
|
||||||
}
|
|
||||||
this.stream = await navigator.mediaDevices.getUserMedia({ audio: true });
|
|
||||||
console.log('获取麦克风权限成功,stream:', this.stream);
|
|
||||||
|
|
||||||
if (!this.audioContext || this.audioContext.state === 'closed') {
|
|
||||||
this.audioContext = new (window.AudioContext || window.webkitAudioContext)({ sampleRate: 16000 });
|
|
||||||
}
|
|
||||||
this.sourceNode = this.audioContext.createMediaStreamSource(this.stream);
|
|
||||||
|
|
||||||
this.recorder = new Recorder({
|
|
||||||
encoderPath: '/encoderWorker.min.js',
|
|
||||||
sampleRate: 16000,
|
|
||||||
numberOfChannels: 1,
|
|
||||||
frameSize: 960,
|
|
||||||
encoderApplication: 2048,
|
|
||||||
encoderFrameSize: 60,
|
|
||||||
encoderBitRate: 24000,
|
|
||||||
monitorGain: 0,
|
|
||||||
sourceNode: this.sourceNode,
|
|
||||||
ogg: false,
|
|
||||||
streamPages: false,
|
|
||||||
maxFramesPerPage: 1,
|
|
||||||
});
|
|
||||||
|
|
||||||
this.recorder.ondataavailable = (data) => {
|
|
||||||
console.log('录音数据可用:', data.byteLength, '类型:', data.constructor.name);
|
|
||||||
const frames = this.stripOggContainer(data);
|
|
||||||
frames.forEach((frame, index) => {
|
|
||||||
const frameHead = Array.from(new Uint8Array(frame.slice(0, 8)))
|
|
||||||
.map(b => b.toString(16).padStart(2, '0'))
|
|
||||||
.join(' ');
|
|
||||||
console.log(`帧 ${index} 大小: ${frame.byteLength} 字节,前8字节: ${frameHead}`);
|
|
||||||
|
|
||||||
if (frame.byteLength < 50 || frame.byteLength > 300) {
|
|
||||||
console.warn(`帧 ${index} 大小异常: ${frame.byteLength} 字节,预期 50-300 字节`);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.ws && this.ws.readyState === WebSocket.OPEN) {
|
|
||||||
this.ws.send(frame);
|
|
||||||
console.log('发送裸 Opus 帧:', frame.byteLength);
|
|
||||||
} else {
|
|
||||||
console.warn('WebSocket 未连接,跳过发送');
|
|
||||||
}
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
this.recorder.onstart = () => {
|
|
||||||
console.log('录音已启动');
|
|
||||||
};
|
|
||||||
|
|
||||||
this.recorder.onstop = () => {
|
|
||||||
console.log('录音停止');
|
|
||||||
this.stream.getTracks().forEach(track => track.stop());
|
|
||||||
this.stream = null;
|
|
||||||
this.sourceNode = null;
|
|
||||||
this.scrollToBottom();
|
|
||||||
};
|
|
||||||
|
|
||||||
console.log('Recorder 初始化成功');
|
|
||||||
} catch (err) {
|
|
||||||
console.error('初始化录音失败:', err);
|
|
||||||
alert('无法访问麦克风或录音初始化失败,请检查权限');
|
|
||||||
}
|
|
||||||
},
|
|
||||||
async toggleRecording() {
|
|
||||||
console.log('点击 toggleRecording,当前状态:', this.isRecording, 'WebSocket 状态:', this.wsStatus);
|
|
||||||
if (!this.recorder) {
|
|
||||||
await this.initRecorder();
|
|
||||||
if (!this.recorder) {
|
|
||||||
console.error('recorder 初始化失败');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (this.isRecording) {
|
|
||||||
console.log('停止录音');
|
|
||||||
this.recorder.stop();
|
|
||||||
this.isRecording = false;
|
|
||||||
} else {
|
|
||||||
try {
|
|
||||||
console.log('开始录音');
|
|
||||||
await this.initRecorder();
|
|
||||||
await this.recorder.start();
|
|
||||||
console.log('录音开始后,状态:', this.recorder.state);
|
|
||||||
this.isRecording = true;
|
|
||||||
} catch (err) {
|
|
||||||
console.error('录音启动失败:', err);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
},
|
|
||||||
mounted() {
|
|
||||||
console.log('组件挂载,初始化 WebSocket');
|
|
||||||
this.connectWebSocket();
|
|
||||||
},
|
|
||||||
destroyed() {
|
|
||||||
if (this.ws) this.ws.close();
|
|
||||||
if (this.stream) this.stream.getTracks().forEach(track => track.stop());
|
|
||||||
if (this.recorder) this.recorder.stop();
|
|
||||||
if (this.audioContext) this.audioContext.close();
|
|
||||||
if (this.audioDecoder) this.audioDecoder.destroy();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style scoped>
|
|
||||||
.container {
|
|
||||||
max-width: 800px;
|
|
||||||
margin: 0 auto;
|
|
||||||
padding: 20px;
|
|
||||||
font-family: Arial, sans-serif;
|
|
||||||
}
|
|
||||||
|
|
||||||
.title {
|
|
||||||
text-align: center;
|
|
||||||
font-size: 24px;
|
|
||||||
margin-bottom: 20px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.chat-container {
|
|
||||||
height: 60vh;
|
|
||||||
overflow-y: auto;
|
|
||||||
border: 1px solid #ccc;
|
|
||||||
padding: 10px;
|
|
||||||
background: #f9f9f9;
|
|
||||||
border-radius: 5px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.message {
|
|
||||||
margin: 10px 0;
|
|
||||||
padding: 8px 12px;
|
|
||||||
border-radius: 5px;
|
|
||||||
max-width: 70%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.user {
|
|
||||||
background: #007bff;
|
|
||||||
color: white;
|
|
||||||
margin-left: auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
.assistant {
|
|
||||||
background: #e9ecef;
|
|
||||||
color: black;
|
|
||||||
}
|
|
||||||
|
|
||||||
.controls {
|
|
||||||
text-align: center;
|
|
||||||
margin-top: 20px;
|
|
||||||
}
|
|
||||||
|
|
||||||
button {
|
|
||||||
padding: 10px 20px;
|
|
||||||
font-size: 16px;
|
|
||||||
cursor: pointer;
|
|
||||||
background: #007bff;
|
|
||||||
color: white;
|
|
||||||
border: none;
|
|
||||||
border-radius: 5px;
|
|
||||||
}
|
|
||||||
|
|
||||||
button:disabled {
|
|
||||||
background: #ccc;
|
|
||||||
cursor: not-allowed;
|
|
||||||
}
|
|
||||||
|
|
||||||
button.recording {
|
|
||||||
background: #dc3545;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
@@ -6,15 +6,47 @@ const TerserPlugin = require('terser-webpack-plugin');
|
|||||||
const CompressionPlugin = require('compression-webpack-plugin')
|
const CompressionPlugin = require('compression-webpack-plugin')
|
||||||
// BundleAnalyzerPlugin 用于分析打包后的文件
|
// BundleAnalyzerPlugin 用于分析打包后的文件
|
||||||
const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin;
|
const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin;
|
||||||
|
// WorkboxPlugin 用于生成Service Worker
|
||||||
|
const { InjectManifest } = require('workbox-webpack-plugin');
|
||||||
// 引入 path 模块
|
// 引入 path 模块
|
||||||
const path = require('path');
|
|
||||||
|
const path = require('path')
|
||||||
|
|
||||||
|
function resolve(dir) {
|
||||||
|
return path.join(__dirname, dir)
|
||||||
|
}
|
||||||
|
|
||||||
// 确保加载 .env 文件
|
// 确保加载 .env 文件
|
||||||
dotenv.config();
|
dotenv.config();
|
||||||
|
|
||||||
|
// 定义CDN资源列表,确保Service Worker也能访问
|
||||||
|
const cdnResources = {
|
||||||
|
css: [
|
||||||
|
'https://unpkg.com/element-ui@2.15.14/lib/theme-chalk/index.css',
|
||||||
|
'https://cdnjs.cloudflare.com/ajax/libs/normalize/8.0.1/normalize.min.css'
|
||||||
|
],
|
||||||
|
js: [
|
||||||
|
'https://unpkg.com/vue@2.6.14/dist/vue.min.js',
|
||||||
|
'https://unpkg.com/vue-router@3.6.5/dist/vue-router.min.js',
|
||||||
|
'https://unpkg.com/vuex@3.6.2/dist/vuex.min.js',
|
||||||
|
'https://unpkg.com/element-ui@2.15.14/lib/index.js',
|
||||||
|
'https://unpkg.com/axios@0.27.2/dist/axios.min.js',
|
||||||
|
'https://unpkg.com/opus-decoder@0.7.7/dist/opus-decoder.min.js'
|
||||||
|
]
|
||||||
|
};
|
||||||
|
|
||||||
|
// 判断是否使用CDN
|
||||||
|
const useCDN = process.env.VUE_APP_USE_CDN === 'true';
|
||||||
|
|
||||||
module.exports = defineConfig({
|
module.exports = defineConfig({
|
||||||
productionSourceMap: process.env.NODE_ENV === 'production' ? false : true, // 生产环境不生成 source map
|
productionSourceMap: process.env.NODE_ENV === 'production' ? false : true, // 生产环境不生成 source map
|
||||||
devServer: {
|
devServer: {
|
||||||
port: 8001, // 指定端口为 8001
|
port: 8001, // 指定端口为 8001
|
||||||
proxy: {
|
proxy: {
|
||||||
|
'/xiaozhi': {
|
||||||
|
target: 'http://127.0.0.1:8002',
|
||||||
|
changeOrigin: true
|
||||||
|
}
|
||||||
},
|
},
|
||||||
client: {
|
client: {
|
||||||
overlay: false, // 不显示 webpack 错误覆盖层
|
overlay: false, // 不显示 webpack 错误覆盖层
|
||||||
@@ -25,21 +57,9 @@ module.exports = defineConfig({
|
|||||||
// 修改 HTML 插件配置,动态插入 CDN 链接
|
// 修改 HTML 插件配置,动态插入 CDN 链接
|
||||||
config.plugin('html')
|
config.plugin('html')
|
||||||
.tap(args => {
|
.tap(args => {
|
||||||
if (process.env.NODE_ENV === 'production') {
|
// 根据配置决定是否使用CDN
|
||||||
args[0].cdn = {
|
if (process.env.NODE_ENV === 'production' && useCDN) {
|
||||||
css: [
|
args[0].cdn = cdnResources;
|
||||||
'https://cdn.jsdelivr.net/npm/element-ui@2.15.14/lib/theme-chalk/index.css',
|
|
||||||
'https://cdnjs.cloudflare.com/ajax/libs/normalize/8.0.1/normalize.min.css'
|
|
||||||
],
|
|
||||||
js: [
|
|
||||||
'https://cdn.jsdelivr.net/npm/vue@2.6.14/dist/vue.min.js',
|
|
||||||
'https://cdn.jsdelivr.net/npm/vue-router@3.6.5/dist/vue-router.min.js',
|
|
||||||
'https://cdn.jsdelivr.net/npm/vuex@3.6.2/dist/vuex.min.js',
|
|
||||||
'https://cdn.jsdelivr.net/npm/element-ui@2.15.14/lib/index.js',
|
|
||||||
'https://cdn.jsdelivr.net/npm/axios@0.27.2/dist/axios.min.js',
|
|
||||||
'https://cdn.jsdelivr.net/npm/opus-decoder@0.7.7/dist/opus-decoder.min.js'
|
|
||||||
]
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
return args;
|
return args;
|
||||||
});
|
});
|
||||||
@@ -97,14 +117,38 @@ module.exports = defineConfig({
|
|||||||
minRatio: 0.8
|
minRatio: 0.8
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
config.externals = {
|
|
||||||
'vue': 'Vue',
|
// 根据是否使用CDN来决定是否添加Service Worker
|
||||||
'vue-router': 'VueRouter',
|
config.plugins.push(
|
||||||
'vuex': 'Vuex',
|
new InjectManifest({
|
||||||
'element-ui': 'ELEMENT',
|
swSrc: path.resolve(__dirname, 'src/service-worker.js'),
|
||||||
'axios': 'axios',
|
swDest: 'service-worker.js',
|
||||||
'opus-decoder': 'OpusDecoder'
|
exclude: [/\.map$/, /asset-manifest\.json$/],
|
||||||
};
|
maximumFileSizeToCacheInBytes: 5 * 1024 * 1024, // 5MB
|
||||||
|
// 自定义Service Worker注入点
|
||||||
|
injectionPoint: 'self.__WB_MANIFEST',
|
||||||
|
// 添加额外信息传递给Service Worker
|
||||||
|
additionalManifestEntries: useCDN ?
|
||||||
|
[{ url: 'cdn-mode', revision: 'enabled' }] :
|
||||||
|
[{ url: 'cdn-mode', revision: 'disabled' }]
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
// 如果使用CDN,则配置externals排除依赖包
|
||||||
|
if (useCDN) {
|
||||||
|
config.externals = {
|
||||||
|
'vue': 'Vue',
|
||||||
|
'vue-router': 'VueRouter',
|
||||||
|
'vuex': 'Vuex',
|
||||||
|
'element-ui': 'ELEMENT',
|
||||||
|
'axios': 'axios',
|
||||||
|
'opus-decoder': 'OpusDecoder'
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
// 确保不使用CDN时不设置externals,让webpack打包所有依赖
|
||||||
|
config.externals = {};
|
||||||
|
}
|
||||||
|
|
||||||
if (process.env.ANALYZE === 'true') { // 通过环境变量控制
|
if (process.env.ANALYZE === 'true') { // 通过环境变量控制
|
||||||
config.plugins.push(
|
config.plugins.push(
|
||||||
new BundleAnalyzerPlugin({
|
new BundleAnalyzerPlugin({
|
||||||
@@ -124,13 +168,13 @@ module.exports = defineConfig({
|
|||||||
config: [__filename] // 每次配置文件修改时缓存失效
|
config: [__filename] // 每次配置文件修改时缓存失效
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
config.resolve.alias = {
|
|
||||||
'@': path.resolve(__dirname, 'src'), // 让 '@' 代表 'src' 目录
|
|
||||||
'@assets': path.resolve(__dirname, 'src/assets'), // 设置 '@assets' 为 'src/assets' 目录
|
|
||||||
'@components': path.resolve(__dirname, 'src/components'), // 设置 '@components' 为 'src/components' 目录
|
|
||||||
'@views': path.resolve(__dirname, 'src/views'), // 设置 '@views' 为 'src/views' 目录
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
// 将CDN资源信息暴露给service-worker.js
|
||||||
|
pwa: {
|
||||||
|
workboxOptions: {
|
||||||
|
skipWaiting: true,
|
||||||
|
clientsClaim: true
|
||||||
|
}
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -147,7 +147,7 @@ selected_module:
|
|||||||
Memory: nomem
|
Memory: nomem
|
||||||
# 意图识别模块开启后,可以播放音乐、控制音量、识别退出指令。
|
# 意图识别模块开启后,可以播放音乐、控制音量、识别退出指令。
|
||||||
# 不想开通意图识别,就设置成:nointent
|
# 不想开通意图识别,就设置成:nointent
|
||||||
# 意图识别可使用intent_llm,如果你的LLM是DifyLLM或CozeLLM,建议使用这个。优点:通用性强,缺点:增加串行前置意图识别模块,会增加处理时间,这个意图识别暂时不支持控制音量大小等iot操作
|
# 意图识别可使用intent_llm。优点:通用性强,缺点:增加串行前置意图识别模块,会增加处理时间,这个意图识别暂时不支持控制音量大小等iot操作
|
||||||
# 意图识别可使用function_call,缺点:需要所选择的LLM支持function_call,优点:按需调用工具、速度快,理论上能全部操作所有iot指令
|
# 意图识别可使用function_call,缺点:需要所选择的LLM支持function_call,优点:按需调用工具、速度快,理论上能全部操作所有iot指令
|
||||||
# 默认免费的ChatGLMLLM就已经支持function_call,但是如果像追求稳定建议把LLM设置成:DoubaoLLM,使用的具体model_name是:doubao-pro-32k-functioncall-241028
|
# 默认免费的ChatGLMLLM就已经支持function_call,但是如果像追求稳定建议把LLM设置成:DoubaoLLM,使用的具体model_name是:doubao-pro-32k-functioncall-241028
|
||||||
Intent: function_call
|
Intent: function_call
|
||||||
@@ -163,7 +163,7 @@ Intent:
|
|||||||
type: intent_llm
|
type: intent_llm
|
||||||
# 配备意图识别独立的思考模型
|
# 配备意图识别独立的思考模型
|
||||||
# 如果这里不填,则会默认使用selected_module.LLM的模型作为意图识别的思考模型
|
# 如果这里不填,则会默认使用selected_module.LLM的模型作为意图识别的思考模型
|
||||||
# 如果你的selected_module.LLM选择了DifyLLM或CozeLLM,这里最好使用独立的LLM作为意图识别,例如使用免费的ChatGLMLLM
|
# 如果你的不想使用selected_module.LLM意图识别,这里最好使用独立的LLM作为意图识别,例如使用免费的ChatGLMLLM
|
||||||
llm: ChatGLMLLM
|
llm: ChatGLMLLM
|
||||||
function_call:
|
function_call:
|
||||||
# 不需要动type
|
# 不需要动type
|
||||||
|
|||||||
Binary file not shown.
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user