Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
16e5dc8c78 | ||
|
|
f77970dd7d | ||
|
|
1ebfcd1f13 | ||
|
|
3ca322961e | ||
|
|
0eb0a600a7 | ||
|
|
3ed06a9e34 | ||
|
|
67dde7cc47 | ||
|
|
efedc18f02 | ||
|
|
0270de1844 | ||
|
|
06b0ff139b | ||
|
|
799ea28d82 | ||
|
|
5a68252d22 | ||
|
|
97404e53fe | ||
|
|
b052158cee | ||
|
|
979ef24b79 | ||
|
|
0f4f0f01fd | ||
|
|
18e1de4f45 | ||
|
|
aa846a096f | ||
|
|
a679412230 | ||
|
|
cf3e14bb07 | ||
|
|
3a73a17253 | ||
|
|
3b27ebcd35 | ||
|
|
fa6074d284 | ||
|
|
d809cfcb01 | ||
|
|
eb2c09bbdc | ||
|
|
2cf8f79482 | ||
|
|
1c94844bb9 | ||
|
|
0c53b8f543 | ||
|
|
1518dfca30 | ||
|
|
7529c500a4 | ||
|
|
b6ed220e48 | ||
|
|
b152c5dc23 | ||
|
|
04bfddf457 | ||
|
|
9d78ea29c3 | ||
|
|
f82f364189 | ||
|
|
9705149b2b | ||
|
|
7986148013 | ||
|
|
19f5cb5b4f | ||
|
|
ea48b948bf | ||
|
|
3c5ca9f143 | ||
|
|
da72ecaa1b | ||
|
|
f297e1b648 | ||
|
|
a1973e07b7 | ||
|
|
cd58b2c3cb | ||
|
|
a5be67d04b | ||
|
|
e6dc6187c8 | ||
|
|
7f4011f9de | ||
|
|
211cf891af | ||
|
|
89a1229b53 | ||
|
|
071ed99a32 | ||
|
|
ea14be27e6 | ||
|
|
a834f163b7 | ||
|
|
7849e6b030 | ||
|
|
0d869baaa0 | ||
|
|
164eb5481c | ||
|
|
969ee3cb12 | ||
|
|
ee54d87383 | ||
|
|
849026c015 | ||
|
|
e5bb3e7791 | ||
|
|
1ffc40e71d | ||
|
|
45fd850757 | ||
|
|
8a5dac181e | ||
|
|
ecf20300ff | ||
|
|
74def844f0 | ||
|
|
a5a9bd1abe | ||
|
|
2d9b915d51 | ||
|
|
5f87fc8361 | ||
|
|
6859fdc90e | ||
|
|
b8ae0568e9 | ||
|
|
1dc6f143f3 | ||
|
|
67532dc230 | ||
|
|
b2876def99 | ||
|
|
0c73a8b6a4 | ||
|
|
c857a3164d | ||
|
|
a964192eb0 | ||
|
|
32da03818e | ||
|
|
7f17e3e08e | ||
|
|
c237e1f323 | ||
|
|
087a202e31 | ||
|
|
4b1460d938 | ||
|
|
f2aec524d2 | ||
|
|
1a97ca137c | ||
|
|
2b6f457ae6 | ||
|
|
3f4e0b4aa0 | ||
|
|
e30ded4de9 | ||
|
|
07242a84ff | ||
|
|
a2074dc0d8 | ||
|
|
28b6c85650 | ||
|
|
af300138eb | ||
|
|
2bf5725048 | ||
|
|
3d73141788 | ||
|
|
945ab29bc3 | ||
|
|
fe0f7dcf75 | ||
|
|
1a3ffbc8e2 | ||
|
|
71278b1898 | ||
|
|
0173c7abe1 | ||
|
|
8a50eea4ce | ||
|
|
b9891bb795 | ||
|
|
b903a88cd5 | ||
|
|
f3fcf2fedf | ||
|
|
a825f0874c | ||
|
|
8a5ca46418 |
@@ -53,113 +53,98 @@ Spearheaded by Professor Siyuan Liu's Team (South China University of Technology
|
||||
<table>
|
||||
<tr>
|
||||
<td>
|
||||
<a href="https://www.bilibili.com/video/BV1FMFyejExX" target="_blank">
|
||||
<picture>
|
||||
<img alt="响应速度感受" src="docs/images/demo9.png" />
|
||||
</picture>
|
||||
</a>
|
||||
<a href="https://www.bilibili.com/video/BV1FMFyejExX" target="_blank">
|
||||
<picture>
|
||||
<img alt="响应速度感受" src="docs/images/demo9.png" /></picture>
|
||||
</a>
|
||||
</td>
|
||||
<td>
|
||||
<a href="https://www.bilibili.com/video/BV1vchQzaEse" target="_blank">
|
||||
<picture>
|
||||
<img alt="速度优化秘诀" src="docs/images/demo6.png" />
|
||||
</picture>
|
||||
</a>
|
||||
<a href="https://www.bilibili.com/video/BV1vchQzaEse" target="_blank">
|
||||
<picture>
|
||||
<img alt="速度优化秘诀" src="docs/images/demo6.png" /></picture>
|
||||
</a>
|
||||
</td>
|
||||
<td>
|
||||
<a href="https://www.bilibili.com/video/BV1C1tCzUEZh" target="_blank">
|
||||
<picture>
|
||||
<img alt="复杂医疗场景" src="docs/images/demo1.png" />
|
||||
</picture>
|
||||
</a>
|
||||
<a href="https://www.bilibili.com/video/BV1WEcxzFEAT" target="_blank">
|
||||
<picture>
|
||||
<img alt="小智数字人 支持语音唤醒" src="docs/images/demo8.png" /></picture>
|
||||
</a>
|
||||
</td>
|
||||
<td>
|
||||
<a href="https://www.bilibili.com/video/BV1zUW5zJEkq" target="_blank">
|
||||
<picture>
|
||||
<img alt="MQTT指令下发" src="docs/images/demo4.png" />
|
||||
</picture>
|
||||
</a>
|
||||
<a href="https://www.bilibili.com/video/BV1CKVz6UEuB" target="_blank">
|
||||
<picture>
|
||||
<img alt="设备呼叫设备,打电话" src="docs/images/demo0.png" /></picture>
|
||||
</a>
|
||||
</td>
|
||||
<td>
|
||||
<a href="https://www.bilibili.com/video/BV1Exu3zqEDe" target="_blank">
|
||||
<picture>
|
||||
<img alt="声纹识别" src="docs/images/demo14.png" />
|
||||
</picture>
|
||||
</a>
|
||||
<a href="https://www.bilibili.com/video/BV1C1tCzUEZh" target="_blank">
|
||||
<picture>
|
||||
<img alt="复杂医疗场景" src="docs/images/demo1.png" /></picture>
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
<a href="https://www.bilibili.com/video/BV1pNXWYGEx1" target="_blank">
|
||||
<picture>
|
||||
<img alt="控制家电开关" src="docs/images/demo5.png" />
|
||||
</picture>
|
||||
</a>
|
||||
<a href="https://www.bilibili.com/video/BV1VC96Y5EMH" target="_blank">
|
||||
<picture>
|
||||
<img alt="播放音乐查询天气播报新闻" src="docs/images/demo7.png" /></picture>
|
||||
</a>
|
||||
</td>
|
||||
<td>
|
||||
<a href="https://www.bilibili.com/video/BV1ZQKUzYExM" target="_blank">
|
||||
<picture>
|
||||
<img alt="MCP接入点" src="docs/images/demo13.png" />
|
||||
</picture>
|
||||
</a>
|
||||
</td>
|
||||
<td>
|
||||
<a href="https://www.bilibili.com/video/BV1TJ7WzzEo6" target="_blank">
|
||||
<picture>
|
||||
<img alt="多指令任务" src="docs/images/demo11.png" />
|
||||
</picture>
|
||||
</a>
|
||||
</td>
|
||||
<td>
|
||||
<a href="https://www.bilibili.com/video/BV1VC96Y5EMH" target="_blank">
|
||||
<picture>
|
||||
<img alt="播放音乐" src="docs/images/demo7.png" />
|
||||
</picture>
|
||||
</a>
|
||||
</td>
|
||||
<td>
|
||||
<a href="https://www.bilibili.com/video/BV1Z8XuYZEAS" target="_blank">
|
||||
<picture>
|
||||
<img alt="天气插件" src="docs/images/demo8.png" />
|
||||
</picture>
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
<a href="https://www.bilibili.com/video/BV12J7WzBEaH" target="_blank">
|
||||
<picture>
|
||||
<img alt="实时打断" src="docs/images/demo10.png" />
|
||||
</picture>
|
||||
</a>
|
||||
<picture>
|
||||
<img alt="实时打断" src="docs/images/demo10.png" /></picture>
|
||||
</a>
|
||||
</td>
|
||||
<td>
|
||||
<a href="https://www.bilibili.com/video/BV1Co76z7EvK" target="_blank">
|
||||
<picture>
|
||||
<img alt="拍照识物品" src="docs/images/demo12.png" />
|
||||
</picture>
|
||||
</a>
|
||||
<picture>
|
||||
<img alt="拍照识物品" src="docs/images/demo12.png" /></picture>
|
||||
</a>
|
||||
</td>
|
||||
<td>
|
||||
<a href="https://www.bilibili.com/video/BV1CDKWemEU6" target="_blank">
|
||||
<picture>
|
||||
<img alt="自定义音色" src="docs/images/demo2.png" />
|
||||
</picture>
|
||||
</a>
|
||||
<a href="https://www.bilibili.com/video/BV1pNXWYGEx1" target="_blank">
|
||||
<picture>
|
||||
<img alt="控制家电开关" src="docs/images/demo5.png" /></picture>
|
||||
</a>
|
||||
</td>
|
||||
<td>
|
||||
<a href="https://www.bilibili.com/video/BV12yA2egEaC" target="_blank">
|
||||
<picture>
|
||||
<img alt="使用粤语交流" src="docs/images/demo3.png" />
|
||||
</picture>
|
||||
</a>
|
||||
<a href="https://www.bilibili.com/video/BV1TJ7WzzEo6" target="_blank">
|
||||
<picture>
|
||||
<img alt="多指令任务" src="docs/images/demo11.png" /></picture>
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
<a href="https://www.bilibili.com/video/BV1ZQKUzYExM" target="_blank">
|
||||
<picture>
|
||||
<img alt="MCP接入点" src="docs/images/demo13.png" /></picture>
|
||||
</a>
|
||||
</td>
|
||||
<td>
|
||||
<a href="https://www.bilibili.com/video/BV17LXWYvENb" target="_blank">
|
||||
<picture>
|
||||
<img alt="播报新闻" src="docs/images/demo0.png" />
|
||||
</picture>
|
||||
</a>
|
||||
<a href="https://www.bilibili.com/video/BV1zUW5zJEkq" target="_blank">
|
||||
<picture>
|
||||
<img alt="MQTT指令下发" src="docs/images/demo4.png" /></picture>
|
||||
</a>
|
||||
</td>
|
||||
<td>
|
||||
<a href="https://www.bilibili.com/video/BV1Exu3zqEDe" target="_blank">
|
||||
<picture>
|
||||
<img alt="声纹识别" src="docs/images/demo14.png" /></picture>
|
||||
</a>
|
||||
</td>
|
||||
<td>
|
||||
<a href="https://www.bilibili.com/video/BV1CDKWemEU6" target="_blank">
|
||||
<picture>
|
||||
<img alt="自定义音色" src="docs/images/demo2.png" /></picture>
|
||||
</a>
|
||||
</td>
|
||||
<td>
|
||||
<a href="https://www.bilibili.com/video/BV12yA2egEaC" target="_blank">
|
||||
<picture>
|
||||
<img alt="使用粤语交流" src="docs/images/demo3.png" /></picture>
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
@@ -65,6 +65,7 @@ VAD:
|
||||
3、[如何部署MQTT网关开启MQTT+UDP协议](./mqtt-gateway-integration.md)<br/>
|
||||
4、[如何自动拉取本项目最新代码自动编译和启动](./dev-ops-integration.md)<br/>
|
||||
5、[如何与Nginx集成](https://github.com/xinnan-tech/xiaozhi-esp32-server/issues/791)<br/>
|
||||
6、[修改代码后怎么编译自己的Docker镜像](./docker-build.md)<br/>
|
||||
|
||||
### 8、编译固件相关教程
|
||||
1、[如何自己编译小智固件](./firmware-build.md)<br/>
|
||||
@@ -85,6 +86,7 @@ VAD:
|
||||
11、[如何集成PowerMem智能记忆](./powermem-integration.md)<br/>
|
||||
12、[如何配置天气插件查询天气](./weather-integration.md)<br/>
|
||||
13、[如何开启设备呼叫插件](./device-call-guide.md)<br/>
|
||||
14、[如何开启联网搜索功能](./web-search-integration.md)<br/>
|
||||
|
||||
### 10、数字人相关教程
|
||||
1、[数字人digital-human启动方法](./digital-human-wakeword.md)<br/>
|
||||
|
||||
@@ -1,21 +1,74 @@
|
||||
# 本地编译docker镜像方法
|
||||
|
||||
现在本项目已经使用github自动编译docker功能,本文档是提供给有本地编译docker镜像需求的朋友准备的。
|
||||
现在本项目已经使用`github`的`自动编译docker镜像`功能,如果您拉取的是项目发行的镜像,您没有自己编译镜像的需求,那就忽略本文档。
|
||||
|
||||
1、安装docker
|
||||
```
|
||||
如果您修改了源码,然后想采用`docker`的方式部署运行,可以参照以下步骤操作:
|
||||
|
||||
## 1、环境准备
|
||||
|
||||
安装docker:
|
||||
```bash
|
||||
sudo apt-get install docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin
|
||||
```
|
||||
2、编译docker镜像
|
||||
```
|
||||
#进入项目根目录
|
||||
# 编译server
|
||||
docker build -t xiaozhi-esp32-server:server_latest -f ./Dockerfile-server .
|
||||
# 编译web
|
||||
docker build -t xiaozhi-esp32-server:web_latest -f ./Dockerfile-web .
|
||||
|
||||
# 编译完成后,可以使用docker-compose启动项目
|
||||
# docker-compose.yml你需要修改成自己编译的镜像版本
|
||||
## 2、编译镜像
|
||||
|
||||
当你修改好代码后,需要编译新的镜像时,需要按照以下步骤操作:
|
||||
|
||||
准备好你的`你的用户名`和`新的版本号`。
|
||||
- 这个`你的用户名`是你在`docker hub`注册的用户名,例如`xiaozhi`。当然,如果你不需要推送到`docker hub`,你可以自由定义。
|
||||
- 这个`新的版本号`是你编译的镜像版本,例如`1.2.3`,你可以根据需要自定义或者使用日期格式(例如`20260609`)主要是方便和现在运行的版本号做区分,同时也方便下次回忆你是什么时候构建的,不要和现在你本机运行的版本号相同。
|
||||
|
||||
进入`xiaozhi-esp32-server`项目根目录,编译 server 和 web 两个镜像:
|
||||
|
||||
```bash
|
||||
cd 项目根目录
|
||||
|
||||
# 编译server镜像
|
||||
docker build -f Dockerfile-server -t 你的用户名/xiaozhi-esp32-server:新的版本号 .
|
||||
|
||||
# 编译web镜像
|
||||
docker build -f Dockerfile-web -t 你的用户名/xiaozhi-esp32-server-web:新的版本号 .
|
||||
|
||||
```
|
||||
|
||||
## 3、修改docker-compose配置
|
||||
|
||||
```bash
|
||||
cd main/xiaozhi-server
|
||||
docker compose up -d
|
||||
```
|
||||
|
||||
编辑 `docker-compose_all.yml` 文件,将镜像版本替换为你刚才编译的版本:
|
||||
|
||||
```yaml
|
||||
services:
|
||||
xiaozhi-esp32-server:
|
||||
image: 你的用户名/xiaozhi-esp32-server:新的版本号 # 修改为你的镜像地址
|
||||
...
|
||||
|
||||
xiaozhi-esp32-server-web:
|
||||
image: 你的用户名/xiaozhi-esp32-server-web:新的版本号 #修改为你的镜像地址
|
||||
...
|
||||
```
|
||||
|
||||
## 4、重启服务
|
||||
|
||||
```bash
|
||||
# 停止旧容器
|
||||
docker compose -f docker-compose_all.yml down
|
||||
|
||||
# 启动新容器
|
||||
docker compose -f docker-compose_all.yml up -d
|
||||
```
|
||||
|
||||
## 5、验证
|
||||
|
||||
查看日志确认服务启动正常:
|
||||
|
||||
```bash
|
||||
# 查看server日志
|
||||
docker logs -f -n 50 xiaozhi-esp32-server
|
||||
|
||||
# 查看web日志
|
||||
docker logs -f -n 50 xiaozhi-esp32-server-web
|
||||
```
|
||||
|
||||
|
Before Width: | Height: | Size: 289 KiB After Width: | Height: | Size: 152 KiB |
|
Before Width: | Height: | Size: 325 KiB After Width: | Height: | Size: 143 KiB |
|
Before Width: | Height: | Size: 280 KiB After Width: | Height: | Size: 108 KiB |
@@ -52,113 +52,98 @@ Möchten Sie die Nutzungseffekte sehen? Klicken Sie auf die Videos unten 🎥
|
||||
<table>
|
||||
<tr>
|
||||
<td>
|
||||
<a href="https://www.bilibili.com/video/BV1FMFyejExX" target="_blank">
|
||||
<picture>
|
||||
<img alt="响应速度感受" src="../images/demo9.png" />
|
||||
</picture>
|
||||
</a>
|
||||
<a href="https://www.bilibili.com/video/BV1FMFyejExX" target="_blank">
|
||||
<picture>
|
||||
<img alt="响应速度感受" src="docs/images/demo9.png" /></picture>
|
||||
</a>
|
||||
</td>
|
||||
<td>
|
||||
<a href="https://www.bilibili.com/video/BV1vchQzaEse" target="_blank">
|
||||
<picture>
|
||||
<img alt="速度优化秘诀" src="../images/demo6.png" />
|
||||
</picture>
|
||||
</a>
|
||||
<a href="https://www.bilibili.com/video/BV1vchQzaEse" target="_blank">
|
||||
<picture>
|
||||
<img alt="速度优化秘诀" src="docs/images/demo6.png" /></picture>
|
||||
</a>
|
||||
</td>
|
||||
<td>
|
||||
<a href="https://www.bilibili.com/video/BV1C1tCzUEZh" target="_blank">
|
||||
<picture>
|
||||
<img alt="复杂医疗场景" src="../images/demo1.png" />
|
||||
</picture>
|
||||
</a>
|
||||
<a href="https://www.bilibili.com/video/BV1WEcxzFEAT" target="_blank">
|
||||
<picture>
|
||||
<img alt="小智数字人 支持语音唤醒" src="docs/images/demo8.png" /></picture>
|
||||
</a>
|
||||
</td>
|
||||
<td>
|
||||
<a href="https://www.bilibili.com/video/BV1zUW5zJEkq" target="_blank">
|
||||
<picture>
|
||||
<img alt="MQTT指令下发" src="../images/demo4.png" />
|
||||
</picture>
|
||||
</a>
|
||||
<a href="https://www.bilibili.com/video/BV1CKVz6UEuB" target="_blank">
|
||||
<picture>
|
||||
<img alt="设备呼叫设备,打电话" src="docs/images/demo0.png" /></picture>
|
||||
</a>
|
||||
</td>
|
||||
<td>
|
||||
<a href="https://www.bilibili.com/video/BV1Exu3zqEDe" target="_blank">
|
||||
<picture>
|
||||
<img alt="声纹识别" src="../images/demo14.png" />
|
||||
</picture>
|
||||
</a>
|
||||
<a href="https://www.bilibili.com/video/BV1C1tCzUEZh" target="_blank">
|
||||
<picture>
|
||||
<img alt="复杂医疗场景" src="docs/images/demo1.png" /></picture>
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
<a href="https://www.bilibili.com/video/BV1pNXWYGEx1" target="_blank">
|
||||
<picture>
|
||||
<img alt="控制家电开关" src="../images/demo5.png" />
|
||||
</picture>
|
||||
</a>
|
||||
<a href="https://www.bilibili.com/video/BV1VC96Y5EMH" target="_blank">
|
||||
<picture>
|
||||
<img alt="播放音乐查询天气播报新闻" src="docs/images/demo7.png" /></picture>
|
||||
</a>
|
||||
</td>
|
||||
<td>
|
||||
<a href="https://www.bilibili.com/video/BV1ZQKUzYExM" target="_blank">
|
||||
<picture>
|
||||
<img alt="MCP接入点" src="../images/demo13.png" />
|
||||
</picture>
|
||||
</a>
|
||||
</td>
|
||||
<td>
|
||||
<a href="https://www.bilibili.com/video/BV1TJ7WzzEo6" target="_blank">
|
||||
<picture>
|
||||
<img alt="多指令任务" src="../images/demo11.png" />
|
||||
</picture>
|
||||
</a>
|
||||
</td>
|
||||
<td>
|
||||
<a href="https://www.bilibili.com/video/BV1VC96Y5EMH" target="_blank">
|
||||
<picture>
|
||||
<img alt="播放音乐" src="../images/demo7.png" />
|
||||
</picture>
|
||||
</a>
|
||||
</td>
|
||||
<td>
|
||||
<a href="https://www.bilibili.com/video/BV1Z8XuYZEAS" target="_blank">
|
||||
<picture>
|
||||
<img alt="天气插件" src="../images/demo8.png" />
|
||||
</picture>
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
<a href="https://www.bilibili.com/video/BV12J7WzBEaH" target="_blank">
|
||||
<picture>
|
||||
<img alt="实时打断" src="../images/demo10.png" />
|
||||
</picture>
|
||||
</a>
|
||||
<picture>
|
||||
<img alt="实时打断" src="docs/images/demo10.png" /></picture>
|
||||
</a>
|
||||
</td>
|
||||
<td>
|
||||
<a href="https://www.bilibili.com/video/BV1Co76z7EvK" target="_blank">
|
||||
<picture>
|
||||
<img alt="拍照识物品" src="../images/demo12.png" />
|
||||
</picture>
|
||||
</a>
|
||||
<picture>
|
||||
<img alt="拍照识物品" src="docs/images/demo12.png" /></picture>
|
||||
</a>
|
||||
</td>
|
||||
<td>
|
||||
<a href="https://www.bilibili.com/video/BV1CDKWemEU6" target="_blank">
|
||||
<picture>
|
||||
<img alt="自定义音色" src="../images/demo2.png" />
|
||||
</picture>
|
||||
</a>
|
||||
<a href="https://www.bilibili.com/video/BV1pNXWYGEx1" target="_blank">
|
||||
<picture>
|
||||
<img alt="控制家电开关" src="docs/images/demo5.png" /></picture>
|
||||
</a>
|
||||
</td>
|
||||
<td>
|
||||
<a href="https://www.bilibili.com/video/BV12yA2egEaC" target="_blank">
|
||||
<picture>
|
||||
<img alt="使用粤语交流" src="../images/demo3.png" />
|
||||
</picture>
|
||||
</a>
|
||||
<a href="https://www.bilibili.com/video/BV1TJ7WzzEo6" target="_blank">
|
||||
<picture>
|
||||
<img alt="多指令任务" src="docs/images/demo11.png" /></picture>
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
<a href="https://www.bilibili.com/video/BV1ZQKUzYExM" target="_blank">
|
||||
<picture>
|
||||
<img alt="MCP接入点" src="docs/images/demo13.png" /></picture>
|
||||
</a>
|
||||
</td>
|
||||
<td>
|
||||
<a href="https://www.bilibili.com/video/BV17LXWYvENb" target="_blank">
|
||||
<picture>
|
||||
<img alt="播报新闻" src="../images/demo0.png" />
|
||||
</picture>
|
||||
</a>
|
||||
<a href="https://www.bilibili.com/video/BV1zUW5zJEkq" target="_blank">
|
||||
<picture>
|
||||
<img alt="MQTT指令下发" src="docs/images/demo4.png" /></picture>
|
||||
</a>
|
||||
</td>
|
||||
<td>
|
||||
<a href="https://www.bilibili.com/video/BV1Exu3zqEDe" target="_blank">
|
||||
<picture>
|
||||
<img alt="声纹识别" src="docs/images/demo14.png" /></picture>
|
||||
</a>
|
||||
</td>
|
||||
<td>
|
||||
<a href="https://www.bilibili.com/video/BV1CDKWemEU6" target="_blank">
|
||||
<picture>
|
||||
<img alt="自定义音色" src="docs/images/demo2.png" /></picture>
|
||||
</a>
|
||||
</td>
|
||||
<td>
|
||||
<a href="https://www.bilibili.com/video/BV12yA2egEaC" target="_blank">
|
||||
<picture>
|
||||
<img alt="使用粤语交流" src="docs/images/demo3.png" /></picture>
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
@@ -52,113 +52,98 @@ Want to see the usage effects? Click the videos below 🎥
|
||||
<table>
|
||||
<tr>
|
||||
<td>
|
||||
<a href="https://www.bilibili.com/video/BV1FMFyejExX" target="_blank">
|
||||
<picture>
|
||||
<img alt="响应速度感受" src="../images/demo9.png" />
|
||||
</picture>
|
||||
</a>
|
||||
<a href="https://www.bilibili.com/video/BV1FMFyejExX" target="_blank">
|
||||
<picture>
|
||||
<img alt="响应速度感受" src="docs/images/demo9.png" /></picture>
|
||||
</a>
|
||||
</td>
|
||||
<td>
|
||||
<a href="https://www.bilibili.com/video/BV1vchQzaEse" target="_blank">
|
||||
<picture>
|
||||
<img alt="速度优化秘诀" src="../images/demo6.png" />
|
||||
</picture>
|
||||
</a>
|
||||
<a href="https://www.bilibili.com/video/BV1vchQzaEse" target="_blank">
|
||||
<picture>
|
||||
<img alt="速度优化秘诀" src="docs/images/demo6.png" /></picture>
|
||||
</a>
|
||||
</td>
|
||||
<td>
|
||||
<a href="https://www.bilibili.com/video/BV1C1tCzUEZh" target="_blank">
|
||||
<picture>
|
||||
<img alt="复杂医疗场景" src="../images/demo1.png" />
|
||||
</picture>
|
||||
</a>
|
||||
<a href="https://www.bilibili.com/video/BV1WEcxzFEAT" target="_blank">
|
||||
<picture>
|
||||
<img alt="小智数字人 支持语音唤醒" src="docs/images/demo8.png" /></picture>
|
||||
</a>
|
||||
</td>
|
||||
<td>
|
||||
<a href="https://www.bilibili.com/video/BV1zUW5zJEkq" target="_blank">
|
||||
<picture>
|
||||
<img alt="MQTT指令下发" src="../images/demo4.png" />
|
||||
</picture>
|
||||
</a>
|
||||
<a href="https://www.bilibili.com/video/BV1CKVz6UEuB" target="_blank">
|
||||
<picture>
|
||||
<img alt="设备呼叫设备,打电话" src="docs/images/demo0.png" /></picture>
|
||||
</a>
|
||||
</td>
|
||||
<td>
|
||||
<a href="https://www.bilibili.com/video/BV1Exu3zqEDe" target="_blank">
|
||||
<picture>
|
||||
<img alt="声纹识别" src="../images/demo14.png" />
|
||||
</picture>
|
||||
</a>
|
||||
<a href="https://www.bilibili.com/video/BV1C1tCzUEZh" target="_blank">
|
||||
<picture>
|
||||
<img alt="复杂医疗场景" src="docs/images/demo1.png" /></picture>
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
<a href="https://www.bilibili.com/video/BV1pNXWYGEx1" target="_blank">
|
||||
<picture>
|
||||
<img alt="控制家电开关" src="../images/demo5.png" />
|
||||
</picture>
|
||||
</a>
|
||||
<a href="https://www.bilibili.com/video/BV1VC96Y5EMH" target="_blank">
|
||||
<picture>
|
||||
<img alt="播放音乐查询天气播报新闻" src="docs/images/demo7.png" /></picture>
|
||||
</a>
|
||||
</td>
|
||||
<td>
|
||||
<a href="https://www.bilibili.com/video/BV1ZQKUzYExM" target="_blank">
|
||||
<picture>
|
||||
<img alt="MCP接入点" src="../images/demo13.png" />
|
||||
</picture>
|
||||
</a>
|
||||
</td>
|
||||
<td>
|
||||
<a href="https://www.bilibili.com/video/BV1TJ7WzzEo6" target="_blank">
|
||||
<picture>
|
||||
<img alt="多指令任务" src="../images/demo11.png" />
|
||||
</picture>
|
||||
</a>
|
||||
</td>
|
||||
<td>
|
||||
<a href="https://www.bilibili.com/video/BV1VC96Y5EMH" target="_blank">
|
||||
<picture>
|
||||
<img alt="播放音乐" src="../images/demo7.png" />
|
||||
</picture>
|
||||
</a>
|
||||
</td>
|
||||
<td>
|
||||
<a href="https://www.bilibili.com/video/BV1Z8XuYZEAS" target="_blank">
|
||||
<picture>
|
||||
<img alt="天气插件" src="../images/demo8.png" />
|
||||
</picture>
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
<a href="https://www.bilibili.com/video/BV12J7WzBEaH" target="_blank">
|
||||
<picture>
|
||||
<img alt="实时打断" src="../images/demo10.png" />
|
||||
</picture>
|
||||
</a>
|
||||
<picture>
|
||||
<img alt="实时打断" src="docs/images/demo10.png" /></picture>
|
||||
</a>
|
||||
</td>
|
||||
<td>
|
||||
<a href="https://www.bilibili.com/video/BV1Co76z7EvK" target="_blank">
|
||||
<picture>
|
||||
<img alt="拍照识物品" src="../images/demo12.png" />
|
||||
</picture>
|
||||
</a>
|
||||
<picture>
|
||||
<img alt="拍照识物品" src="docs/images/demo12.png" /></picture>
|
||||
</a>
|
||||
</td>
|
||||
<td>
|
||||
<a href="https://www.bilibili.com/video/BV1CDKWemEU6" target="_blank">
|
||||
<picture>
|
||||
<img alt="自定义音色" src="../images/demo2.png" />
|
||||
</picture>
|
||||
</a>
|
||||
<a href="https://www.bilibili.com/video/BV1pNXWYGEx1" target="_blank">
|
||||
<picture>
|
||||
<img alt="控制家电开关" src="docs/images/demo5.png" /></picture>
|
||||
</a>
|
||||
</td>
|
||||
<td>
|
||||
<a href="https://www.bilibili.com/video/BV12yA2egEaC" target="_blank">
|
||||
<picture>
|
||||
<img alt="使用粤语交流" src="../images/demo3.png" />
|
||||
</picture>
|
||||
</a>
|
||||
<a href="https://www.bilibili.com/video/BV1TJ7WzzEo6" target="_blank">
|
||||
<picture>
|
||||
<img alt="多指令任务" src="docs/images/demo11.png" /></picture>
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
<a href="https://www.bilibili.com/video/BV1ZQKUzYExM" target="_blank">
|
||||
<picture>
|
||||
<img alt="MCP接入点" src="docs/images/demo13.png" /></picture>
|
||||
</a>
|
||||
</td>
|
||||
<td>
|
||||
<a href="https://www.bilibili.com/video/BV17LXWYvENb" target="_blank">
|
||||
<picture>
|
||||
<img alt="播报新闻" src="../images/demo0.png" />
|
||||
</picture>
|
||||
</a>
|
||||
<a href="https://www.bilibili.com/video/BV1zUW5zJEkq" target="_blank">
|
||||
<picture>
|
||||
<img alt="MQTT指令下发" src="docs/images/demo4.png" /></picture>
|
||||
</a>
|
||||
</td>
|
||||
<td>
|
||||
<a href="https://www.bilibili.com/video/BV1Exu3zqEDe" target="_blank">
|
||||
<picture>
|
||||
<img alt="声纹识别" src="docs/images/demo14.png" /></picture>
|
||||
</a>
|
||||
</td>
|
||||
<td>
|
||||
<a href="https://www.bilibili.com/video/BV1CDKWemEU6" target="_blank">
|
||||
<picture>
|
||||
<img alt="自定义音色" src="docs/images/demo2.png" /></picture>
|
||||
</a>
|
||||
</td>
|
||||
<td>
|
||||
<a href="https://www.bilibili.com/video/BV12yA2egEaC" target="_blank">
|
||||
<picture>
|
||||
<img alt="使用粤语交流" src="docs/images/demo3.png" /></picture>
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
@@ -52,113 +52,98 @@ Quer ver os efeitos de uso? Clique nos vídeos abaixo 🎥
|
||||
<table>
|
||||
<tr>
|
||||
<td>
|
||||
<a href="https://www.bilibili.com/video/BV1FMFyejExX" target="_blank">
|
||||
<picture>
|
||||
<img alt="Experiência de velocidade de resposta" src="../images/demo9.png" />
|
||||
</picture>
|
||||
</a>
|
||||
<a href="https://www.bilibili.com/video/BV1FMFyejExX" target="_blank">
|
||||
<picture>
|
||||
<img alt="响应速度感受" src="docs/images/demo9.png" /></picture>
|
||||
</a>
|
||||
</td>
|
||||
<td>
|
||||
<a href="https://www.bilibili.com/video/BV1vchQzaEse" target="_blank">
|
||||
<picture>
|
||||
<img alt="Segredo da otimização de velocidade" src="../images/demo6.png" />
|
||||
</picture>
|
||||
</a>
|
||||
<a href="https://www.bilibili.com/video/BV1vchQzaEse" target="_blank">
|
||||
<picture>
|
||||
<img alt="速度优化秘诀" src="docs/images/demo6.png" /></picture>
|
||||
</a>
|
||||
</td>
|
||||
<td>
|
||||
<a href="https://www.bilibili.com/video/BV1C1tCzUEZh" target="_blank">
|
||||
<picture>
|
||||
<img alt="Cenário médico complexo" src="../images/demo1.png" />
|
||||
</picture>
|
||||
</a>
|
||||
<a href="https://www.bilibili.com/video/BV1WEcxzFEAT" target="_blank">
|
||||
<picture>
|
||||
<img alt="小智数字人 支持语音唤醒" src="docs/images/demo8.png" /></picture>
|
||||
</a>
|
||||
</td>
|
||||
<td>
|
||||
<a href="https://www.bilibili.com/video/BV1zUW5zJEkq" target="_blank">
|
||||
<picture>
|
||||
<img alt="Envio de comandos MQTT" src="../images/demo4.png" />
|
||||
</picture>
|
||||
</a>
|
||||
<a href="https://www.bilibili.com/video/BV1CKVz6UEuB" target="_blank">
|
||||
<picture>
|
||||
<img alt="设备呼叫设备,打电话" src="docs/images/demo0.png" /></picture>
|
||||
</a>
|
||||
</td>
|
||||
<td>
|
||||
<a href="https://www.bilibili.com/video/BV1Exu3zqEDe" target="_blank">
|
||||
<picture>
|
||||
<img alt="Reconhecimento de impressão vocal" src="../images/demo14.png" />
|
||||
</picture>
|
||||
</a>
|
||||
<a href="https://www.bilibili.com/video/BV1C1tCzUEZh" target="_blank">
|
||||
<picture>
|
||||
<img alt="复杂医疗场景" src="docs/images/demo1.png" /></picture>
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
<a href="https://www.bilibili.com/video/BV1pNXWYGEx1" target="_blank">
|
||||
<picture>
|
||||
<img alt="Controle de interruptores de eletrodomésticos" src="../images/demo5.png" />
|
||||
</picture>
|
||||
</a>
|
||||
<a href="https://www.bilibili.com/video/BV1VC96Y5EMH" target="_blank">
|
||||
<picture>
|
||||
<img alt="播放音乐查询天气播报新闻" src="docs/images/demo7.png" /></picture>
|
||||
</a>
|
||||
</td>
|
||||
<td>
|
||||
<a href="https://www.bilibili.com/video/BV1ZQKUzYExM" target="_blank">
|
||||
<picture>
|
||||
<img alt="Ponto de acesso MCP" src="../images/demo13.png" />
|
||||
</picture>
|
||||
</a>
|
||||
</td>
|
||||
<td>
|
||||
<a href="https://www.bilibili.com/video/BV1TJ7WzzEo6" target="_blank">
|
||||
<picture>
|
||||
<img alt="Tarefas com múltiplos comandos" src="../images/demo11.png" />
|
||||
</picture>
|
||||
</a>
|
||||
</td>
|
||||
<td>
|
||||
<a href="https://www.bilibili.com/video/BV1VC96Y5EMH" target="_blank">
|
||||
<picture>
|
||||
<img alt="Reproduzir música" src="../images/demo7.png" />
|
||||
</picture>
|
||||
</a>
|
||||
</td>
|
||||
<td>
|
||||
<a href="https://www.bilibili.com/video/BV1Z8XuYZEAS" target="_blank">
|
||||
<picture>
|
||||
<img alt="Plugin de clima" src="../images/demo8.png" />
|
||||
</picture>
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
<a href="https://www.bilibili.com/video/BV12J7WzBEaH" target="_blank">
|
||||
<picture>
|
||||
<img alt="Interrupção em tempo real" src="../images/demo10.png" />
|
||||
</picture>
|
||||
</a>
|
||||
<picture>
|
||||
<img alt="实时打断" src="docs/images/demo10.png" /></picture>
|
||||
</a>
|
||||
</td>
|
||||
<td>
|
||||
<a href="https://www.bilibili.com/video/BV1Co76z7EvK" target="_blank">
|
||||
<picture>
|
||||
<img alt="Fotografar e identificar objetos" src="../images/demo12.png" />
|
||||
</picture>
|
||||
</a>
|
||||
<picture>
|
||||
<img alt="拍照识物品" src="docs/images/demo12.png" /></picture>
|
||||
</a>
|
||||
</td>
|
||||
<td>
|
||||
<a href="https://www.bilibili.com/video/BV1CDKWemEU6" target="_blank">
|
||||
<picture>
|
||||
<img alt="Timbre de voz personalizado" src="../images/demo2.png" />
|
||||
</picture>
|
||||
</a>
|
||||
<a href="https://www.bilibili.com/video/BV1pNXWYGEx1" target="_blank">
|
||||
<picture>
|
||||
<img alt="控制家电开关" src="docs/images/demo5.png" /></picture>
|
||||
</a>
|
||||
</td>
|
||||
<td>
|
||||
<a href="https://www.bilibili.com/video/BV12yA2egEaC" target="_blank">
|
||||
<picture>
|
||||
<img alt="Comunicação em cantonês" src="../images/demo3.png" />
|
||||
</picture>
|
||||
</a>
|
||||
<a href="https://www.bilibili.com/video/BV1TJ7WzzEo6" target="_blank">
|
||||
<picture>
|
||||
<img alt="多指令任务" src="docs/images/demo11.png" /></picture>
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
<a href="https://www.bilibili.com/video/BV1ZQKUzYExM" target="_blank">
|
||||
<picture>
|
||||
<img alt="MCP接入点" src="docs/images/demo13.png" /></picture>
|
||||
</a>
|
||||
</td>
|
||||
<td>
|
||||
<a href="https://www.bilibili.com/video/BV17LXWYvENb" target="_blank">
|
||||
<picture>
|
||||
<img alt="Transmissão de notícias" src="../images/demo0.png" />
|
||||
</picture>
|
||||
</a>
|
||||
<a href="https://www.bilibili.com/video/BV1zUW5zJEkq" target="_blank">
|
||||
<picture>
|
||||
<img alt="MQTT指令下发" src="docs/images/demo4.png" /></picture>
|
||||
</a>
|
||||
</td>
|
||||
<td>
|
||||
<a href="https://www.bilibili.com/video/BV1Exu3zqEDe" target="_blank">
|
||||
<picture>
|
||||
<img alt="声纹识别" src="docs/images/demo14.png" /></picture>
|
||||
</a>
|
||||
</td>
|
||||
<td>
|
||||
<a href="https://www.bilibili.com/video/BV1CDKWemEU6" target="_blank">
|
||||
<picture>
|
||||
<img alt="自定义音色" src="docs/images/demo2.png" /></picture>
|
||||
</a>
|
||||
</td>
|
||||
<td>
|
||||
<a href="https://www.bilibili.com/video/BV12yA2egEaC" target="_blank">
|
||||
<picture>
|
||||
<img alt="使用粤语交流" src="docs/images/demo3.png" /></picture>
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
@@ -52,113 +52,98 @@ Muốn xem hiệu quả sử dụng? Hãy xem video 🎥
|
||||
<table>
|
||||
<tr>
|
||||
<td>
|
||||
<a href="https://www.bilibili.com/video/BV1FMFyejExX" target="_blank">
|
||||
<picture>
|
||||
<img alt="响应速度感受" src="../images/demo9.png" />
|
||||
</picture>
|
||||
</a>
|
||||
<a href="https://www.bilibili.com/video/BV1FMFyejExX" target="_blank">
|
||||
<picture>
|
||||
<img alt="响应速度感受" src="docs/images/demo9.png" /></picture>
|
||||
</a>
|
||||
</td>
|
||||
<td>
|
||||
<a href="https://www.bilibili.com/video/BV1vchQzaEse" target="_blank">
|
||||
<picture>
|
||||
<img alt="速度优化秘诀" src="../images/demo6.png" />
|
||||
</picture>
|
||||
</a>
|
||||
<a href="https://www.bilibili.com/video/BV1vchQzaEse" target="_blank">
|
||||
<picture>
|
||||
<img alt="速度优化秘诀" src="docs/images/demo6.png" /></picture>
|
||||
</a>
|
||||
</td>
|
||||
<td>
|
||||
<a href="https://www.bilibili.com/video/BV1C1tCzUEZh" target="_blank">
|
||||
<picture>
|
||||
<img alt="复杂医疗场景" src="../images/demo1.png" />
|
||||
</picture>
|
||||
</a>
|
||||
<a href="https://www.bilibili.com/video/BV1WEcxzFEAT" target="_blank">
|
||||
<picture>
|
||||
<img alt="小智数字人 支持语音唤醒" src="docs/images/demo8.png" /></picture>
|
||||
</a>
|
||||
</td>
|
||||
<td>
|
||||
<a href="https://www.bilibili.com/video/BV1zUW5zJEkq" target="_blank">
|
||||
<picture>
|
||||
<img alt="MQTT指令下发" src="../images/demo4.png" />
|
||||
</picture>
|
||||
</a>
|
||||
<a href="https://www.bilibili.com/video/BV1CKVz6UEuB" target="_blank">
|
||||
<picture>
|
||||
<img alt="设备呼叫设备,打电话" src="docs/images/demo0.png" /></picture>
|
||||
</a>
|
||||
</td>
|
||||
<td>
|
||||
<a href="https://www.bilibili.com/video/BV1Exu3zqEDe" target="_blank">
|
||||
<picture>
|
||||
<img alt="声纹识别" src="../images/demo14.png" />
|
||||
</picture>
|
||||
</a>
|
||||
<a href="https://www.bilibili.com/video/BV1C1tCzUEZh" target="_blank">
|
||||
<picture>
|
||||
<img alt="复杂医疗场景" src="docs/images/demo1.png" /></picture>
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
<a href="https://www.bilibili.com/video/BV1pNXWYGEx1" target="_blank">
|
||||
<picture>
|
||||
<img alt="控制家电开关" src="../images/demo5.png" />
|
||||
</picture>
|
||||
</a>
|
||||
<a href="https://www.bilibili.com/video/BV1VC96Y5EMH" target="_blank">
|
||||
<picture>
|
||||
<img alt="播放音乐查询天气播报新闻" src="docs/images/demo7.png" /></picture>
|
||||
</a>
|
||||
</td>
|
||||
<td>
|
||||
<a href="https://www.bilibili.com/video/BV1ZQKUzYExM" target="_blank">
|
||||
<picture>
|
||||
<img alt="MCP接入点" src="../images/demo13.png" />
|
||||
</picture>
|
||||
</a>
|
||||
</td>
|
||||
<td>
|
||||
<a href="https://www.bilibili.com/video/BV1TJ7WzzEo6" target="_blank">
|
||||
<picture>
|
||||
<img alt="多指令任务" src="../images/demo11.png" />
|
||||
</picture>
|
||||
</a>
|
||||
</td>
|
||||
<td>
|
||||
<a href="https://www.bilibili.com/video/BV1VC96Y5EMH" target="_blank">
|
||||
<picture>
|
||||
<img alt="播放音乐" src="../images/demo7.png" />
|
||||
</picture>
|
||||
</a>
|
||||
</td>
|
||||
<td>
|
||||
<a href="https://www.bilibili.com/video/BV1Z8XuYZEAS" target="_blank">
|
||||
<picture>
|
||||
<img alt="天气插件" src="../images/demo8.png" />
|
||||
</picture>
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
<a href="https://www.bilibili.com/video/BV12J7WzBEaH" target="_blank">
|
||||
<picture>
|
||||
<img alt="实时打断" src="../images/demo10.png" />
|
||||
</picture>
|
||||
</a>
|
||||
<picture>
|
||||
<img alt="实时打断" src="docs/images/demo10.png" /></picture>
|
||||
</a>
|
||||
</td>
|
||||
<td>
|
||||
<a href="https://www.bilibili.com/video/BV1Co76z7EvK" target="_blank">
|
||||
<picture>
|
||||
<img alt="拍照识物品" src="../images/demo12.png" />
|
||||
</picture>
|
||||
</a>
|
||||
<picture>
|
||||
<img alt="拍照识物品" src="docs/images/demo12.png" /></picture>
|
||||
</a>
|
||||
</td>
|
||||
<td>
|
||||
<a href="https://www.bilibili.com/video/BV1CDKWemEU6" target="_blank">
|
||||
<picture>
|
||||
<img alt="自定义音色" src="../images/demo2.png" />
|
||||
</picture>
|
||||
</a>
|
||||
<a href="https://www.bilibili.com/video/BV1pNXWYGEx1" target="_blank">
|
||||
<picture>
|
||||
<img alt="控制家电开关" src="docs/images/demo5.png" /></picture>
|
||||
</a>
|
||||
</td>
|
||||
<td>
|
||||
<a href="https://www.bilibili.com/video/BV12yA2egEaC" target="_blank">
|
||||
<picture>
|
||||
<img alt="使用粤语交流" src="../images/demo3.png" />
|
||||
</picture>
|
||||
</a>
|
||||
<a href="https://www.bilibili.com/video/BV1TJ7WzzEo6" target="_blank">
|
||||
<picture>
|
||||
<img alt="多指令任务" src="docs/images/demo11.png" /></picture>
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
<a href="https://www.bilibili.com/video/BV1ZQKUzYExM" target="_blank">
|
||||
<picture>
|
||||
<img alt="MCP接入点" src="docs/images/demo13.png" /></picture>
|
||||
</a>
|
||||
</td>
|
||||
<td>
|
||||
<a href="https://www.bilibili.com/video/BV17LXWYvENb" target="_blank">
|
||||
<picture>
|
||||
<img alt="播报新闻" src="../images/demo0.png" />
|
||||
</picture>
|
||||
</a>
|
||||
<a href="https://www.bilibili.com/video/BV1zUW5zJEkq" target="_blank">
|
||||
<picture>
|
||||
<img alt="MQTT指令下发" src="docs/images/demo4.png" /></picture>
|
||||
</a>
|
||||
</td>
|
||||
<td>
|
||||
<a href="https://www.bilibili.com/video/BV1Exu3zqEDe" target="_blank">
|
||||
<picture>
|
||||
<img alt="声纹识别" src="docs/images/demo14.png" /></picture>
|
||||
</a>
|
||||
</td>
|
||||
<td>
|
||||
<a href="https://www.bilibili.com/video/BV1CDKWemEU6" target="_blank">
|
||||
<picture>
|
||||
<img alt="自定义音色" src="docs/images/demo2.png" /></picture>
|
||||
</a>
|
||||
</td>
|
||||
<td>
|
||||
<a href="https://www.bilibili.com/video/BV12yA2egEaC" target="_blank">
|
||||
<picture>
|
||||
<img alt="使用粤语交流" src="docs/images/demo3.png" /></picture>
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
@@ -0,0 +1,71 @@
|
||||
# 联网搜索插件使用指南
|
||||
|
||||
## 功能简介
|
||||
|
||||
联网搜索插件 `web_search` 支持在对话过程中实时联网搜索信息并返回结果。插件支持两个搜索源:秘塔(Metaso)和Tavily,用户可根据需要选择其中一个。
|
||||
|
||||
## API Key申请指南
|
||||
|
||||
目前我们适配了`秘塔搜索`和`Tavily搜索`。
|
||||
- Tavily搜索:每个月1000次免费额度。
|
||||
- 秘塔搜索:拥有较为优质的国内数据源。
|
||||
|
||||
## API Key申请指南
|
||||
|
||||
### 方式一:使用秘塔搜索
|
||||
|
||||
- 访问 [秘塔搜索API](https://metaso.cn/search-api/api-keys),注册并登录账号
|
||||
- 在API密钥管理页面,点击"创建新的Key"
|
||||
- 复制生成的API Key(以 `mk-` 为前缀),这是配置所需的关键信息
|
||||
|
||||
### 方式二:使用Tavily搜索
|
||||
|
||||
- 访问 [Tavily控制台](https://app.tavily.com/home),注册并登录账号
|
||||
- 在控制台中创建API Key
|
||||
- 复制生成的API Key(以 `tvly-` 为前缀),这是配置所需的关键信息
|
||||
|
||||
## 配置方式
|
||||
|
||||
### 方式1. 使用智控台部署(推荐)
|
||||
|
||||
- 登录智控台
|
||||
- 进入"配置角色"页面,选择要配置的智能体
|
||||
- 点击"编辑功能"按钮,在右侧参数配置区域找到"联网搜索"插件
|
||||
- 勾选"联网搜索"
|
||||
- 填入搜索源(`metaso`或`tavily`),并将对应的`API Key`填入配置项
|
||||
- 保存配置,再保存智能体配置
|
||||
|
||||
### 方式2. 单模块xiaozhi-server部署
|
||||
|
||||
在 `data/.config.yaml` 中配置:
|
||||
|
||||
- 将搜索源填入 `provider`,可选值为 `metaso` 或 `tavily`
|
||||
- 将申请到的API Key填入 `api_key`
|
||||
|
||||
```yaml
|
||||
plugins:
|
||||
web_search:
|
||||
provider: "metaso"
|
||||
api_key: "你的API Key"
|
||||
```
|
||||
|
||||
如需自定义返回结果数量和工具描述,可额外配置 `max_results` 和 `description`:
|
||||
|
||||
```yaml
|
||||
plugins:
|
||||
web_search:
|
||||
provider: "metaso"
|
||||
description: "联网搜索工具。当用户明确需要联网搜索问题时使用此工具。"
|
||||
max_results: 5
|
||||
api_key: "你的API Key"
|
||||
```
|
||||
|
||||
同时在 `functions` 列表中确保已启用 `web_search`:
|
||||
|
||||
```yaml
|
||||
plugins:
|
||||
functions:
|
||||
- web_search
|
||||
```
|
||||
|
||||
配置完成后重启服务即可生效。
|
||||
@@ -324,7 +324,7 @@ public interface Constant {
|
||||
/**
|
||||
* 版本号
|
||||
*/
|
||||
public static final String VERSION = "0.9.4";
|
||||
public static final String VERSION = "0.9.5";
|
||||
|
||||
/**
|
||||
* 无效固件URL
|
||||
|
||||
@@ -1,48 +0,0 @@
|
||||
package xiaozhi.modules.agent.dto;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
/**
|
||||
* MCP JSON-RPC 响应 DTO
|
||||
*/
|
||||
@Data
|
||||
public class McpJsonRpcResponse {
|
||||
private String jsonrpc = "2.0";
|
||||
private Integer id;
|
||||
private McpResult result;
|
||||
private McpError error;
|
||||
|
||||
public McpJsonRpcResponse() {
|
||||
}
|
||||
|
||||
@Data
|
||||
public static class McpResult {
|
||||
private String type;
|
||||
private String message;
|
||||
private String agent_id;
|
||||
private McpTool[] tools;
|
||||
|
||||
public McpResult() {
|
||||
}
|
||||
}
|
||||
|
||||
@Data
|
||||
public static class McpTool {
|
||||
private String name;
|
||||
private String description;
|
||||
private Object inputSchema;
|
||||
|
||||
public McpTool() {
|
||||
}
|
||||
}
|
||||
|
||||
@Data
|
||||
public static class McpError {
|
||||
private Integer code;
|
||||
private String message;
|
||||
private Object data;
|
||||
|
||||
public McpError() {
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -8,6 +8,7 @@ import org.apache.shiro.authz.annotation.RequiresPermissions;
|
||||
import org.springframework.beans.BeanUtils;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.PathVariable;
|
||||
import org.springframework.web.bind.annotation.RequestParam;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.PutMapping;
|
||||
import org.springframework.web.bind.annotation.RequestBody;
|
||||
@@ -174,20 +175,26 @@ public class DeviceController {
|
||||
return new Result<Object>().ok(deviceAddressBookService.getAddressBookList(macAddress));
|
||||
}
|
||||
|
||||
@GetMapping("/address-book/lookup")
|
||||
@Operation(summary = "根据昵称查找目标设备")
|
||||
public Result<Map<String, String>> lookupByNickname(String callerMac, String nickname) {
|
||||
Map<String, String> result = deviceAddressBookService.lookupByNickname(callerMac, nickname);
|
||||
@GetMapping("/address-book/call")
|
||||
@Operation(summary = "根据昵称发起呼叫")
|
||||
public Result<Map<String, Object>> callByNickname(String callerMac, String nickname,
|
||||
@RequestParam(required = false, defaultValue = "false") boolean answer) {
|
||||
Map<String, Object> result = deviceAddressBookService.callByNickname(callerMac, nickname, answer);
|
||||
if (result == null) {
|
||||
return new Result<Map<String, String>>().error("未找到对应设备");
|
||||
return new Result<Map<String, Object>>().error("未找到对应设备");
|
||||
}
|
||||
return new Result<Map<String, String>>().ok(result);
|
||||
return new Result<Map<String, Object>>().ok(result);
|
||||
}
|
||||
|
||||
@PutMapping("/address-book/alias")
|
||||
@Operation(summary = "更新设备通讯录别名")
|
||||
@RequiresPermissions("sys:role:normal")
|
||||
public Result<Void> updateAlias(@Valid @RequestBody DeviceAddressBookAliasDTO dto) {
|
||||
UserDetail user = SecurityUser.getUser();
|
||||
DeviceEntity callerDevice = deviceService.getDeviceByMacAddress(dto.getMacAddress());
|
||||
if (callerDevice == null || !callerDevice.getUserId().equals(user.getId())) {
|
||||
return new Result<Void>().error("无权限操作该设备");
|
||||
}
|
||||
deviceAddressBookService.saveOrUpdate(dto.getMacAddress(), dto.getTargetMac(), dto.getAlias(), null);
|
||||
return new Result<Void>();
|
||||
}
|
||||
@@ -196,14 +203,12 @@ public class DeviceController {
|
||||
@Operation(summary = "更新设备通讯录权限")
|
||||
@RequiresPermissions("sys:role:normal")
|
||||
public Result<Void> updatePermission(@Valid @RequestBody DeviceAddressBookPermissionDTO dto) {
|
||||
UserDetail user = SecurityUser.getUser();
|
||||
DeviceEntity callerDevice = deviceService.getDeviceByMacAddress(dto.getMacAddress());
|
||||
if (callerDevice == null || !callerDevice.getUserId().equals(user.getId())) {
|
||||
return new Result<Void>().error("无权限操作该设备");
|
||||
}
|
||||
deviceAddressBookService.saveOrUpdate(dto.getMacAddress(), dto.getTargetMac(), null, dto.getHasPermission());
|
||||
return new Result<Void>();
|
||||
}
|
||||
|
||||
@GetMapping("/call/forward")
|
||||
@Operation(summary = "转发呼叫请求到网关")
|
||||
public Result<Map<String, Object>> forwardCallRequest(String callerMac, String targetMac, String callerNickname) {
|
||||
Map<String, Object> result = deviceService.forwardCallRequest(callerMac, targetMac, callerNickname);
|
||||
return new Result<Map<String, Object>>().ok(result);
|
||||
}
|
||||
}
|
||||
@@ -26,4 +26,9 @@ public interface DeviceAddressBookDao extends BaseMapper<DeviceAddressBookEntity
|
||||
* 更新权限
|
||||
*/
|
||||
void updatePermission(@Param("macAddress") String macAddress, @Param("targetMac") String targetMac, @Param("hasPermission") Boolean hasPermission);
|
||||
|
||||
/**
|
||||
* 批量删除设备相关的通讯录记录
|
||||
*/
|
||||
void deleteByMacAddresses(@Param("macAddresses") List<String> macAddresses);
|
||||
}
|
||||
@@ -1,27 +0,0 @@
|
||||
package xiaozhi.modules.device.dto;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Data;
|
||||
|
||||
/**
|
||||
* 设备绑定的DTO
|
||||
*
|
||||
* @author zjy
|
||||
* @since 2025-3-28
|
||||
*/
|
||||
@Data
|
||||
@AllArgsConstructor
|
||||
@Schema(description = "设备连接头信息")
|
||||
public class DeviceBindDTO {
|
||||
|
||||
@Schema(description = "mac地址")
|
||||
private String macAddress;
|
||||
|
||||
@Schema(description = "所属用户id")
|
||||
private Long userId;
|
||||
|
||||
@Schema(description = "智能体id")
|
||||
private String agentId;
|
||||
|
||||
}
|
||||
@@ -38,10 +38,15 @@ public interface DeviceAddressBookService {
|
||||
void refreshCache();
|
||||
|
||||
/**
|
||||
* 根据昵称查找目标设备信息
|
||||
* 根据昵称发起呼叫
|
||||
* @param callerMac 主叫方MAC地址
|
||||
* @param nickname 被叫方昵称
|
||||
* @return {targetMac: 目标MAC, callerNickname: 目标如何称呼主叫方}
|
||||
* @param isAnswer 是否为接听模式(跳过权限检查)
|
||||
*/
|
||||
Map<String, String> lookupByNickname(String callerMac, String nickname);
|
||||
Map<String, Object> callByNickname(String callerMac, String nickname, boolean isAnswer);
|
||||
|
||||
/**
|
||||
* 批量删除设备相关的通讯录记录
|
||||
*/
|
||||
void deleteByMacAddresses(List<String> macAddresses);
|
||||
}
|
||||
@@ -138,10 +138,4 @@ public interface DeviceService extends BaseService<DeviceEntity> {
|
||||
*/
|
||||
Object callDeviceTool(String deviceId, String toolName, Map<String, Object> arguments);
|
||||
|
||||
/**
|
||||
* 转发呼叫请求到网关
|
||||
* @return 网关响应 {status, message}
|
||||
*/
|
||||
Map<String, Object> forwardCallRequest(String callerMac, String targetMac, String callerNickname);
|
||||
|
||||
}
|
||||
}
|
||||
@@ -1,20 +1,27 @@
|
||||
package xiaozhi.modules.device.service.impl;
|
||||
|
||||
import java.util.Date;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.stream.Collectors;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.springframework.context.annotation.Lazy;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
|
||||
|
||||
import cn.hutool.http.HttpRequest;
|
||||
import cn.hutool.json.JSONUtil;
|
||||
import xiaozhi.common.redis.RedisKeys;
|
||||
import xiaozhi.common.redis.RedisUtils;
|
||||
import xiaozhi.modules.device.dao.DeviceAddressBookDao;
|
||||
import xiaozhi.modules.device.entity.DeviceAddressBookEntity;
|
||||
import xiaozhi.modules.device.service.DeviceAddressBookService;
|
||||
import xiaozhi.modules.device.service.DeviceService;
|
||||
import xiaozhi.modules.sys.service.SysParamsService;
|
||||
|
||||
@Service
|
||||
public class DeviceAddressBookServiceImpl implements DeviceAddressBookService {
|
||||
@@ -22,12 +29,14 @@ public class DeviceAddressBookServiceImpl implements DeviceAddressBookService {
|
||||
private final DeviceAddressBookDao deviceAddressBookDao;
|
||||
private final RedisUtils redisUtils;
|
||||
private final DeviceService deviceService;
|
||||
private final SysParamsService sysParamsService;
|
||||
|
||||
public DeviceAddressBookServiceImpl(DeviceAddressBookDao deviceAddressBookDao, RedisUtils redisUtils,
|
||||
DeviceService deviceService) {
|
||||
@Lazy DeviceService deviceService, SysParamsService sysParamsService) {
|
||||
this.deviceAddressBookDao = deviceAddressBookDao;
|
||||
this.redisUtils = redisUtils;
|
||||
this.deviceService = deviceService;
|
||||
this.sysParamsService = sysParamsService;
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -47,33 +56,46 @@ public class DeviceAddressBookServiceImpl implements DeviceAddressBookService {
|
||||
}
|
||||
|
||||
@Override
|
||||
public Map<String, String> lookupByNickname(String callerMac, String nickname) {
|
||||
public Map<String, Object> callByNickname(String callerMac, String nickname, boolean isAnswer) {
|
||||
Map<String, Map<String, String>> allBooks = getAllAddressBooks();
|
||||
|
||||
if (isAnswer) {
|
||||
return postToMqtt("/api/call/accept", Map.of("mac", callerMac), "接听");
|
||||
}
|
||||
|
||||
// 主动呼叫模式
|
||||
Map<String, String> callerBook = allBooks.get(callerMac.toLowerCase());
|
||||
if (callerBook == null) {
|
||||
return null;
|
||||
return errorResult("未找到备注为'" + nickname + "'的设备");
|
||||
}
|
||||
// 从缓存获取 targetMac,格式: "mac|permission"
|
||||
String targetMacWithPerm = callerBook.get(nickname);
|
||||
if (targetMacWithPerm == null) {
|
||||
return null;
|
||||
return errorResult("未找到备注为'" + nickname + "'的设备");
|
||||
}
|
||||
// 解析 targetMac 和 permission
|
||||
String[] parts = targetMacWithPerm.split("\\|");
|
||||
String targetMac = parts[0];
|
||||
boolean hasPermission = parts.length > 1 && "1".equals(parts[1]);
|
||||
|
||||
Map<String, String> targetBook = allBooks.get(targetMac.toLowerCase());
|
||||
if (targetBook == null) {
|
||||
return null;
|
||||
if (!hasPermission) {
|
||||
return errorResult("呼叫失败,您没有权限呼叫该设备");
|
||||
}
|
||||
// 检查双向关系:目标设备是否也添加了主叫方为联系人
|
||||
String callerNickname = targetBook.get(callerMac.toLowerCase());
|
||||
Map<String, String> result = new HashMap<>();
|
||||
result.put("targetMac", targetMac);
|
||||
result.put("callerNickname", callerNickname);
|
||||
result.put("hasPermission", hasPermission ? "true" : "false");
|
||||
return result;
|
||||
|
||||
// 获取目标设备如何称呼主叫方
|
||||
Map<String, String> targetBook = allBooks.get(targetMac.toLowerCase());
|
||||
String callerNickname = null;
|
||||
if (targetBook != null) {
|
||||
callerNickname = targetBook.get(callerMac.toLowerCase());
|
||||
}
|
||||
if (StringUtils.isBlank(callerNickname)) {
|
||||
callerNickname = deviceService.getDeviceByMacAddress(callerMac).getAlias();
|
||||
if (StringUtils.isBlank(callerNickname)) {
|
||||
callerNickname = formatMacAsDeviceName(callerMac);
|
||||
}
|
||||
}
|
||||
|
||||
return postToMqtt("/api/call/request",
|
||||
Map.of("caller_mac", callerMac, "target_mac", targetMac, "caller_nickname", callerNickname),
|
||||
"呼叫");
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -81,43 +103,44 @@ public class DeviceAddressBookServiceImpl implements DeviceAddressBookService {
|
||||
Map<String, Map<String, String>> result = new HashMap<>();
|
||||
List<DeviceAddressBookEntity> allRecords = deviceAddressBookDao.selectList(null);
|
||||
Map<String, String> reverseMap = new HashMap<>();
|
||||
Map<String, Boolean> permissionMap = new HashMap<>();
|
||||
|
||||
for (DeviceAddressBookEntity entity : allRecords) {
|
||||
String macA = entity.getMacAddress().toLowerCase();
|
||||
String macB = entity.getTargetMac().toLowerCase();
|
||||
String alias = entity.getAlias();
|
||||
Boolean hasPermission = entity.getHasPermission();
|
||||
|
||||
// 构建正向映射: A对B的映射 nickname -> macB|permission
|
||||
if (alias != null && !alias.isEmpty()) {
|
||||
result.computeIfAbsent(macA, k -> new HashMap<>());
|
||||
String permStr = (hasPermission != null && hasPermission) ? "1" : "0";
|
||||
result.get(macA).put(alias, macB + "|" + permStr);
|
||||
}
|
||||
|
||||
// 构建反向记录用于查找B对A的称呼
|
||||
if (alias != null && !alias.isEmpty()) {
|
||||
// 反向记录: (macB, macA) = B对A的称呼
|
||||
reverseMap.put(macB + ":" + macA, alias);
|
||||
}
|
||||
// 记录 A 对 B 是否有权限呼叫
|
||||
permissionMap.put(macA + ":" + macB, hasPermission != null && hasPermission);
|
||||
}
|
||||
|
||||
// 构建反向映射: B对A的映射 macA -> nickname
|
||||
for (DeviceAddressBookEntity entity : allRecords) {
|
||||
String macA = entity.getMacAddress().toLowerCase();
|
||||
String macB = entity.getTargetMac().toLowerCase();
|
||||
String aliasAtoB = entity.getAlias();
|
||||
result.computeIfAbsent(macA, k -> new HashMap<>());
|
||||
result.computeIfAbsent(macB, k -> new HashMap<>());
|
||||
if (aliasAtoB != null && !aliasAtoB.isEmpty()) {
|
||||
// A对B的映射: nickname -> macB|permission
|
||||
Boolean perm = entity.getHasPermission();
|
||||
String permStr = (perm != null && perm) ? "1" : "0";
|
||||
result.get(macA).put(aliasAtoB, macB + "|" + permStr);
|
||||
}
|
||||
// B对A的映射: macA -> nickname(查反向记录)
|
||||
String aliasBtoA = reverseMap.get(macA + ":" + macB);
|
||||
if (aliasBtoA != null && !aliasBtoA.isEmpty()) {
|
||||
result.computeIfAbsent(macB, k -> new HashMap<>());
|
||||
result.get(macB).put(macA, aliasBtoA);
|
||||
}
|
||||
}
|
||||
|
||||
redisUtils.set(RedisKeys.getAddressBookKey(), result);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void updateAlias(String macAddress, String targetMac, String alias) {
|
||||
deviceAddressBookDao.updateAlias(macAddress, targetMac, alias);
|
||||
String finalAlias = generateUniqueAlias(macAddress, targetMac, alias);
|
||||
deviceAddressBookDao.updateAlias(macAddress, targetMac, finalAlias);
|
||||
refreshCache();
|
||||
}
|
||||
|
||||
@@ -132,20 +155,23 @@ public class DeviceAddressBookServiceImpl implements DeviceAddressBookService {
|
||||
QueryWrapper<DeviceAddressBookEntity> wrapper = new QueryWrapper<>();
|
||||
wrapper.eq("mac_address", macAddress).eq("target_mac", targetMac);
|
||||
DeviceAddressBookEntity record = deviceAddressBookDao.selectOne(wrapper);
|
||||
System.out.println("saveOrUpdate - mac=" + macAddress + ", targetMac=" + targetMac + ", alias=" + alias + ", record=" + (record == null ? "null" : "not null"));
|
||||
if (record == null) {
|
||||
DeviceAddressBookEntity entity = new DeviceAddressBookEntity();
|
||||
entity.setMacAddress(macAddress);
|
||||
entity.setTargetMac(targetMac);
|
||||
// 如果 alias 为空,就用默认使用设备名称
|
||||
// 如果 alias 为空,就默认使用设备名称
|
||||
if (StringUtils.isBlank(alias)) {
|
||||
alias = deviceService.getDeviceByMacAddress(targetMac).getAlias();
|
||||
}
|
||||
// 检查重名
|
||||
alias = generateUniqueAlias(macAddress, targetMac, alias);
|
||||
entity.setAlias(alias);
|
||||
entity.setHasPermission(hasPermission);
|
||||
deviceAddressBookDao.insert(entity);
|
||||
} else {
|
||||
if (alias != null) {
|
||||
deviceAddressBookDao.updateAlias(macAddress, targetMac, alias);
|
||||
updateAlias(macAddress, targetMac, alias);
|
||||
}
|
||||
if (hasPermission != null) {
|
||||
deviceAddressBookDao.updatePermission(macAddress, targetMac, hasPermission);
|
||||
@@ -153,4 +179,95 @@ public class DeviceAddressBookServiceImpl implements DeviceAddressBookService {
|
||||
}
|
||||
refreshCache();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void deleteByMacAddresses(List<String> macAddresses) {
|
||||
if (macAddresses == null || macAddresses.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
deviceAddressBookDao.deleteByMacAddresses(macAddresses);
|
||||
refreshCache();
|
||||
}
|
||||
|
||||
private Map<String, Object> errorResult(String message) {
|
||||
Map<String, Object> result = new HashMap<>();
|
||||
result.put("status", "error");
|
||||
result.put("message", message);
|
||||
return result;
|
||||
}
|
||||
|
||||
private Map<String, Object> postToMqtt(String path, Map<String, Object> body, String action) {
|
||||
Map<String, Object> result = new HashMap<>();
|
||||
result.put("status", "error");
|
||||
|
||||
String mqttGatewayUrl = sysParamsService.getValue("server.mqtt_manager_api", true);
|
||||
String mqttSignatureKey = sysParamsService.getValue("server.mqtt_signature_key", true);
|
||||
|
||||
if (StringUtils.isBlank(mqttGatewayUrl) || "null".equals(mqttGatewayUrl)
|
||||
|| StringUtils.isBlank(mqttSignatureKey) || "null".equals(mqttSignatureKey)) {
|
||||
result.put("message", action + "失败,网关配置缺失");
|
||||
return result;
|
||||
}
|
||||
|
||||
String dateStr = new java.text.SimpleDateFormat("yyyy-MM-dd").format(new Date());
|
||||
try {
|
||||
java.security.MessageDigest md = java.security.MessageDigest.getInstance("SHA-256");
|
||||
byte[] hash = md.digest((dateStr + mqttSignatureKey).getBytes(StandardCharsets.UTF_8));
|
||||
StringBuilder hexString = new StringBuilder();
|
||||
for (byte b : hash) {
|
||||
String hex = Integer.toHexString(0xff & b);
|
||||
if (hex.length() == 1) {
|
||||
hexString.append('0');
|
||||
}
|
||||
hexString.append(hex);
|
||||
}
|
||||
String token = hexString.toString();
|
||||
|
||||
String url = "http://" + mqttGatewayUrl + path;
|
||||
String response = HttpRequest.post(url)
|
||||
.header("Authorization", "Bearer " + token)
|
||||
.header("Content-Type", "application/json")
|
||||
.body(JSONUtil.toJsonStr(body))
|
||||
.timeout(5000)
|
||||
.execute()
|
||||
.body();
|
||||
|
||||
if (StringUtils.isNotBlank(response)) {
|
||||
Map<String, Object> gwResult = JSONUtil.parseObj(response);
|
||||
result.put("status", gwResult.get("status"));
|
||||
result.put("message", gwResult.get("message"));
|
||||
}
|
||||
return result;
|
||||
} catch (Exception e) {
|
||||
result.put("message", action + "失败,请稍后再试");
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
private String formatMacAsDeviceName(String mac) {
|
||||
if (StringUtils.isBlank(mac) || mac.length() < 2) {
|
||||
return mac;
|
||||
}
|
||||
String lastTwo = mac.substring(mac.length() - 2);
|
||||
return "尾号为" + lastTwo + "的设备";
|
||||
}
|
||||
|
||||
private String generateUniqueAlias(String macAddress, String targetMac, String alias) {
|
||||
QueryWrapper<DeviceAddressBookEntity> wrapper = new QueryWrapper<>();
|
||||
wrapper.eq("mac_address", macAddress);
|
||||
List<DeviceAddressBookEntity> existing = deviceAddressBookDao.selectList(wrapper);
|
||||
List<String> existNames = existing.stream()
|
||||
.map(DeviceAddressBookEntity::getAlias)
|
||||
.filter(a -> a != null && !a.isEmpty())
|
||||
.collect(Collectors.toList());
|
||||
if (!existNames.contains(alias)) {
|
||||
return alias;
|
||||
}
|
||||
int suffix = 1;
|
||||
String newAlias;
|
||||
while (existNames.contains(newAlias = alias + suffix)) {
|
||||
suffix++;
|
||||
}
|
||||
return newAlias;
|
||||
}
|
||||
}
|
||||
@@ -6,6 +6,7 @@ import java.security.NoSuchAlgorithmException;
|
||||
import java.time.Instant;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Base64;
|
||||
import java.util.Collections;
|
||||
import java.util.Date;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
@@ -63,6 +64,7 @@ import xiaozhi.modules.device.dto.DeviceReportReqDTO;
|
||||
import xiaozhi.modules.device.dto.DeviceReportRespDTO;
|
||||
import xiaozhi.modules.device.entity.DeviceEntity;
|
||||
import xiaozhi.modules.device.entity.OtaEntity;
|
||||
import xiaozhi.modules.device.service.DeviceAddressBookService;
|
||||
import xiaozhi.modules.device.service.DeviceService;
|
||||
import xiaozhi.modules.device.service.OtaService;
|
||||
import xiaozhi.modules.device.vo.UserShowDeviceListVO;
|
||||
@@ -80,6 +82,7 @@ public class DeviceServiceImpl extends BaseServiceImpl<DeviceDao, DeviceEntity>
|
||||
private final SysParamsService sysParamsService;
|
||||
private final RedisUtils redisUtils;
|
||||
private final OtaService otaService;
|
||||
private final DeviceAddressBookService deviceAddressBookService;
|
||||
|
||||
@Async
|
||||
public void updateDeviceConnectionInfo(String agentId, String deviceId, String appVersion) {
|
||||
@@ -314,11 +317,12 @@ public class DeviceServiceImpl extends BaseServiceImpl<DeviceDao, DeviceEntity>
|
||||
|
||||
@Override
|
||||
public void unbindDevice(Long userId, String deviceId) {
|
||||
// 先查询设备信息,获取agentId
|
||||
// 先查询设备信息,获取agentId和macAddress
|
||||
DeviceEntity device = baseDao.selectById(deviceId);
|
||||
if (device == null) {
|
||||
return;
|
||||
}
|
||||
String macAddress = device.getMacAddress();
|
||||
if (StringUtils.isNotBlank(device.getAgentId())) {
|
||||
// 清除智能体设备数量缓存
|
||||
redisUtils.delete(RedisKeys.getAgentDeviceCountById(device.getAgentId()));
|
||||
@@ -328,6 +332,9 @@ public class DeviceServiceImpl extends BaseServiceImpl<DeviceDao, DeviceEntity>
|
||||
wrapper.eq("user_id", userId);
|
||||
wrapper.eq("id", deviceId);
|
||||
baseDao.delete(wrapper);
|
||||
|
||||
// 删除设备相关的通讯录权限记录
|
||||
deviceAddressBookService.deleteByMacAddresses(Collections.singletonList(macAddress));
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -346,9 +353,23 @@ public class DeviceServiceImpl extends BaseServiceImpl<DeviceDao, DeviceEntity>
|
||||
|
||||
@Override
|
||||
public void deleteByAgentId(String agentId) {
|
||||
// 先查询该智能体下的所有设备,获取mac地址用于删除通讯录记录
|
||||
QueryWrapper<DeviceEntity> queryWrapper = new QueryWrapper<>();
|
||||
queryWrapper.eq("agent_id", agentId);
|
||||
List<DeviceEntity> devices = baseDao.selectList(queryWrapper);
|
||||
|
||||
// 删除设备
|
||||
UpdateWrapper<DeviceEntity> wrapper = new UpdateWrapper<>();
|
||||
wrapper.eq("agent_id", agentId);
|
||||
baseDao.delete(wrapper);
|
||||
|
||||
// 批量删除这些设备相关的所有通讯录权限记录
|
||||
if (!devices.isEmpty()) {
|
||||
List<String> macAddresses = devices.stream()
|
||||
.map(DeviceEntity::getMacAddress)
|
||||
.collect(Collectors.toList());
|
||||
deviceAddressBookService.deleteByMacAddresses(macAddresses);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -891,70 +912,4 @@ public class DeviceServiceImpl extends BaseServiceImpl<DeviceDao, DeviceEntity>
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Map<String, Object> forwardCallRequest(String callerMac, String targetMac, String callerNickname) {
|
||||
Map<String, Object> result = new HashMap<>();
|
||||
result.put("status", "error");
|
||||
result.put("message", "网关配置缺失");
|
||||
|
||||
// 从系统参数获取网关配置
|
||||
String mqttGatewayUrl = sysParamsService.getValue("server.mqtt_manager_api", true);
|
||||
String mqttSignatureKey = sysParamsService.getValue("server.mqtt_signature_key", true);
|
||||
|
||||
if (StringUtils.isBlank(mqttGatewayUrl) || "null".equals(mqttGatewayUrl)) {
|
||||
log.error("MQTT网关地址未配置");
|
||||
result.put("message", "MQTT网关地址未配置");
|
||||
return result;
|
||||
}
|
||||
if (StringUtils.isBlank(mqttSignatureKey) || "null".equals(mqttSignatureKey)) {
|
||||
log.error("MQTT签名密钥未配置");
|
||||
result.put("message", "MQTT签名密钥未配置");
|
||||
return result;
|
||||
}
|
||||
|
||||
// 生成token: SHA256(date + secret)
|
||||
String dateStr = new java.text.SimpleDateFormat("yyyy-MM-dd").format(new Date());
|
||||
try {
|
||||
java.security.MessageDigest md = java.security.MessageDigest.getInstance("SHA-256");
|
||||
byte[] hash = md.digest((dateStr + mqttSignatureKey).getBytes(StandardCharsets.UTF_8));
|
||||
StringBuilder hexString = new StringBuilder();
|
||||
for (byte b : hash) {
|
||||
String hex = Integer.toHexString(0xff & b);
|
||||
if (hex.length() == 1) {
|
||||
hexString.append('0');
|
||||
}
|
||||
hexString.append(hex);
|
||||
}
|
||||
String token = hexString.toString();
|
||||
|
||||
// 构建请求体
|
||||
Map<String, Object> body = new HashMap<>();
|
||||
body.put("caller_mac", callerMac);
|
||||
body.put("target_mac", targetMac);
|
||||
body.put("caller_nickname", callerNickname);
|
||||
|
||||
// 发送请求并获取响应
|
||||
String url = "http://" + mqttGatewayUrl + "/api/call/request";
|
||||
String response = cn.hutool.http.HttpRequest.post(url)
|
||||
.header("Authorization", "Bearer " + token)
|
||||
.header("Content-Type", "application/json")
|
||||
.body(JSONUtil.toJsonStr(body))
|
||||
.timeout(5000)
|
||||
.execute()
|
||||
.body();
|
||||
|
||||
// 解析网关响应
|
||||
if (StringUtils.isNotBlank(response)) {
|
||||
Map<String, Object> gwResult = JSONUtil.parseObj(response);
|
||||
result.put("status", gwResult.get("status"));
|
||||
result.put("message", gwResult.get("message"));
|
||||
}
|
||||
return result;
|
||||
} catch (Exception e) {
|
||||
log.error("转发呼叫请求失败: {}", e.getMessage());
|
||||
result.put("message", "呼叫请求转发失败: " + e.getMessage());
|
||||
return result;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,68 +0,0 @@
|
||||
package xiaozhi.modules.knowledge.dto;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.util.Date;
|
||||
import java.util.Map;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.Data;
|
||||
|
||||
/**
|
||||
* 文档 DTO
|
||||
*/
|
||||
@Data
|
||||
@Schema(description = "知识库文档")
|
||||
public class DocumentDTO implements Serializable {
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
@Schema(description = "本地ID")
|
||||
private String id;
|
||||
|
||||
@Schema(description = "知识库ID")
|
||||
private String datasetId;
|
||||
|
||||
@Schema(description = "RAGFlow文档ID")
|
||||
private String documentId;
|
||||
|
||||
@Schema(description = "文档名称")
|
||||
private String name;
|
||||
|
||||
@Schema(description = "文件大小")
|
||||
private Long size;
|
||||
|
||||
@Schema(description = "文件类型")
|
||||
private String type;
|
||||
|
||||
@Schema(description = "分块方法")
|
||||
private String chunkMethod;
|
||||
|
||||
@Schema(description = "解析配置")
|
||||
private Map<String, Object> parserConfig;
|
||||
|
||||
@Schema(description = "处理状态 (1:解析中 3:成功 4:失败)")
|
||||
private Integer status;
|
||||
|
||||
@Schema(description = "错误信息")
|
||||
private String error;
|
||||
|
||||
@Schema(description = "分块数量")
|
||||
private Integer chunkCount;
|
||||
|
||||
@Schema(description = "Token数量")
|
||||
private Long tokenCount;
|
||||
|
||||
@Schema(description = "是否启用")
|
||||
private Integer enabled;
|
||||
|
||||
@Schema(description = "创建时间")
|
||||
private Date createdAt;
|
||||
|
||||
@Schema(description = "更新时间")
|
||||
private Date updatedAt;
|
||||
|
||||
@Schema(description = "上传进度 (虚拟字段)")
|
||||
private Double progress;
|
||||
|
||||
@Schema(description = "缩略图/预览图 (虚拟字段)")
|
||||
private String thumbnail;
|
||||
}
|
||||
@@ -67,4 +67,7 @@ public class KnowledgeBaseDTO implements Serializable {
|
||||
|
||||
@Schema(description = "文档数量")
|
||||
private Integer documentCount;
|
||||
|
||||
@Schema(description = "异常提示")
|
||||
private String errorMessage;
|
||||
}
|
||||
@@ -1,421 +0,0 @@
|
||||
package xiaozhi.modules.knowledge.dto.agent;
|
||||
|
||||
import lombok.*;
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import java.io.Serializable;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
import jakarta.validation.constraints.*;
|
||||
|
||||
@Schema(description = "智能体 (Agent) 管理聚合 DTO")
|
||||
public class AgentDTO {
|
||||
|
||||
// ========== 1. Agent 管理 (CRUD) - 对应 RAGFlow_Agent接口详解 ==========
|
||||
@Data
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
@Schema(description = "Agent 创建请求")
|
||||
public static class CreateReq implements Serializable {
|
||||
@Schema(description = "Agent 标题", requiredMode = Schema.RequiredMode.REQUIRED, example = "My Agent")
|
||||
@NotBlank(message = "Agent 标题不能为空")
|
||||
@JsonProperty("title")
|
||||
private String title;
|
||||
|
||||
@Schema(description = "DSL 定义 (画布 JSON)", requiredMode = Schema.RequiredMode.REQUIRED)
|
||||
@NotNull(message = "DSL 定义不能为空")
|
||||
@JsonProperty("dsl")
|
||||
private Map<String, Object> dsl;
|
||||
|
||||
@Schema(description = "描述", example = "这是一个测试 Agent")
|
||||
@JsonProperty("description")
|
||||
private String description;
|
||||
|
||||
@Schema(description = "头像 URL", example = "http://example.com/avatar.png")
|
||||
@JsonProperty("avatar")
|
||||
private String avatar;
|
||||
}
|
||||
|
||||
@Data
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
@Schema(description = "Agent 更新请求")
|
||||
public static class UpdateReq implements Serializable {
|
||||
@Schema(description = "Agent 标题", example = "Updated Agent")
|
||||
@JsonProperty("title")
|
||||
private String title;
|
||||
|
||||
@Schema(description = "DSL 定义 (画布 JSON)")
|
||||
@JsonProperty("dsl")
|
||||
private Map<String, Object> dsl;
|
||||
|
||||
@Schema(description = "描述")
|
||||
@JsonProperty("description")
|
||||
private String description;
|
||||
|
||||
@Schema(description = "头像 URL")
|
||||
@JsonProperty("avatar")
|
||||
private String avatar;
|
||||
}
|
||||
|
||||
@Data
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
@Schema(description = "Agent 列表请求")
|
||||
public static class ListReq implements Serializable {
|
||||
@Schema(description = "页码", defaultValue = "1")
|
||||
@JsonProperty("page")
|
||||
@Builder.Default
|
||||
private Integer page = 1;
|
||||
|
||||
@Schema(description = "每页大小", defaultValue = "10")
|
||||
@JsonProperty("page_size")
|
||||
@Builder.Default
|
||||
private Integer pageSize = 10;
|
||||
|
||||
@Schema(description = "排序字段", defaultValue = "update_time")
|
||||
@JsonProperty("orderby")
|
||||
@Builder.Default
|
||||
private String orderby = "update_time";
|
||||
|
||||
@Schema(description = "是否降序", defaultValue = "true")
|
||||
@JsonProperty("desc")
|
||||
@Builder.Default
|
||||
private Boolean desc = true;
|
||||
|
||||
@Schema(description = "Agent ID 过滤")
|
||||
@JsonProperty("id")
|
||||
private String id;
|
||||
|
||||
@Schema(description = "标题模糊搜索")
|
||||
@JsonProperty("title")
|
||||
private String title;
|
||||
}
|
||||
|
||||
@Data
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
@Schema(description = "Agent 响应对象")
|
||||
public static class AgentVO implements Serializable {
|
||||
@Schema(description = "Agent ID")
|
||||
@JsonProperty("id")
|
||||
private String id;
|
||||
|
||||
@Schema(description = "标题")
|
||||
@JsonProperty("title")
|
||||
private String title;
|
||||
|
||||
@Schema(description = "描述")
|
||||
@JsonProperty("description")
|
||||
private String description;
|
||||
|
||||
@Schema(description = "头像")
|
||||
@JsonProperty("avatar")
|
||||
private String avatar;
|
||||
|
||||
@Schema(description = "DSL 定义")
|
||||
@JsonProperty("dsl")
|
||||
private Map<String, Object> dsl;
|
||||
|
||||
@Schema(description = "创建者 ID")
|
||||
@JsonProperty("user_id")
|
||||
private String userId;
|
||||
|
||||
@Schema(description = "画布分类")
|
||||
@JsonProperty("canvas_category")
|
||||
private String canvasCategory;
|
||||
|
||||
@Schema(description = "创建时间 (时间戳)")
|
||||
@JsonProperty("create_time")
|
||||
private Long createTime;
|
||||
|
||||
@Schema(description = "更新时间 (时间戳)")
|
||||
@JsonProperty("update_time")
|
||||
private Long updateTime;
|
||||
}
|
||||
|
||||
// ========== 2. Webhook 调试与追踪 - 对应 RAGFlow_Agent接口详解 ==========
|
||||
@Data
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
@Schema(description = "Webhook 触发请求 (参数动态)")
|
||||
public static class WebhookTriggerReq implements Serializable {
|
||||
@Schema(description = "输入变量", requiredMode = Schema.RequiredMode.REQUIRED)
|
||||
@NotNull(message = "输入变量不能为空")
|
||||
@JsonProperty("inputs")
|
||||
private Map<String, Object> inputs;
|
||||
|
||||
@Schema(description = "查询词", example = "Hello")
|
||||
@JsonProperty("query")
|
||||
private String query;
|
||||
}
|
||||
|
||||
@Data
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
@Schema(description = "Webhook 追踪请求")
|
||||
public static class WebhookTraceReq implements Serializable {
|
||||
@Schema(description = "时间戳游标", example = "1700000000.0")
|
||||
@JsonProperty("since_ts")
|
||||
private Double sinceTs;
|
||||
|
||||
@Schema(description = "Webhook ID")
|
||||
@JsonProperty("webhook_id")
|
||||
private String webhookId;
|
||||
}
|
||||
|
||||
@Data
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
@Schema(description = "Webhook 追踪响应")
|
||||
public static class WebhookTraceVO implements Serializable {
|
||||
@Schema(description = "Webhook ID")
|
||||
@JsonProperty("webhook_id")
|
||||
private String webhookId;
|
||||
|
||||
@Schema(description = "是否结束")
|
||||
@JsonProperty("finished")
|
||||
private Boolean finished;
|
||||
|
||||
@Schema(description = "下一次查询的时间戳游标")
|
||||
@JsonProperty("next_since_ts")
|
||||
private Double nextSinceTs;
|
||||
|
||||
@Schema(description = "事件列表")
|
||||
@JsonProperty("events")
|
||||
private List<TraceEvent> events;
|
||||
|
||||
@Data
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
@Schema(description = "追踪事件项")
|
||||
public static class TraceEvent implements Serializable {
|
||||
@Schema(description = "时间戳")
|
||||
@JsonProperty("ts")
|
||||
private Double ts;
|
||||
|
||||
@Schema(description = "事件类型")
|
||||
@JsonProperty("event")
|
||||
private String event;
|
||||
|
||||
@Schema(description = "事件数据")
|
||||
@JsonProperty("data")
|
||||
private Object data;
|
||||
}
|
||||
}
|
||||
|
||||
// ========== 3. Agent 会话 (Session) - 对应 RAGFlow_Agent_Dify接口详解 ==========
|
||||
@Data
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
@Schema(description = "Session 创建请求")
|
||||
public static class SessionCreateReq implements Serializable {
|
||||
@Schema(description = "用户 ID")
|
||||
@JsonProperty("user_id")
|
||||
private String userId;
|
||||
}
|
||||
|
||||
@Data
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
@Schema(description = "Session 列表请求")
|
||||
public static class SessionListReq implements Serializable {
|
||||
@Schema(description = "页码", defaultValue = "1")
|
||||
@JsonProperty("page")
|
||||
@Builder.Default
|
||||
private Integer page = 1;
|
||||
|
||||
@Schema(description = "每页大小", defaultValue = "10")
|
||||
@JsonProperty("page_size")
|
||||
@Builder.Default
|
||||
private Integer pageSize = 10;
|
||||
|
||||
@Schema(description = "排序字段", defaultValue = "create_time")
|
||||
@JsonProperty("orderby")
|
||||
@Builder.Default
|
||||
private String orderby = "create_time";
|
||||
|
||||
@Schema(description = "是否降序", defaultValue = "true")
|
||||
@JsonProperty("desc")
|
||||
@Builder.Default
|
||||
private Boolean desc = true;
|
||||
|
||||
@Schema(description = "Session ID")
|
||||
@JsonProperty("id")
|
||||
private String id;
|
||||
|
||||
@Schema(description = "用户 ID")
|
||||
@JsonProperty("user_id")
|
||||
private String userId;
|
||||
|
||||
@Schema(description = "是否返回 DSL")
|
||||
@JsonProperty("dsl")
|
||||
@Builder.Default
|
||||
private Boolean dsl = false;
|
||||
}
|
||||
|
||||
@Data
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
@Schema(description = "Session 批量删除请求")
|
||||
public static class SessionBatchDeleteReq implements Serializable {
|
||||
@Schema(description = "会话 ID 列表", requiredMode = Schema.RequiredMode.REQUIRED)
|
||||
@JsonProperty("ids")
|
||||
@NotEmpty(message = "ID列表不能为空")
|
||||
private List<String> ids;
|
||||
}
|
||||
|
||||
@Data
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
@Schema(description = "Session 响应对象")
|
||||
public static class SessionVO implements Serializable {
|
||||
@Schema(description = "Session ID")
|
||||
@JsonProperty("id")
|
||||
private String id;
|
||||
|
||||
@Schema(description = "Agent ID")
|
||||
@JsonProperty("agent_id")
|
||||
private String agentId;
|
||||
|
||||
@Schema(description = "用户 ID")
|
||||
@JsonProperty("user_id")
|
||||
private String userId;
|
||||
|
||||
@Schema(description = "来源")
|
||||
@JsonProperty("source")
|
||||
private String source;
|
||||
|
||||
@Schema(description = "DSL 定义")
|
||||
@JsonProperty("dsl")
|
||||
private Map<String, Object> dsl;
|
||||
|
||||
@Schema(description = "消息列表")
|
||||
@JsonProperty("messages")
|
||||
private List<Map<String, Object>> messages;
|
||||
}
|
||||
|
||||
// ========== 4. Agent 对话 (Completion) - 对应 RAGFlow_Agent_Dify接口详解 ==========
|
||||
@Data
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
@Schema(description = "Completion 对话请求")
|
||||
public static class CompletionReq implements Serializable {
|
||||
@Schema(description = "会话 ID", requiredMode = Schema.RequiredMode.REQUIRED)
|
||||
@NotBlank(message = "会话 ID 不能为空")
|
||||
@JsonProperty("session_id")
|
||||
private String sessionId;
|
||||
|
||||
@Schema(description = "用户问题")
|
||||
@JsonProperty("question")
|
||||
private String question;
|
||||
|
||||
@Schema(description = "是否流式返回", defaultValue = "true")
|
||||
@JsonProperty("stream")
|
||||
@Builder.Default
|
||||
private Boolean stream = true;
|
||||
|
||||
@Schema(description = "是否返回追踪信息", defaultValue = "false")
|
||||
@JsonProperty("return_trace")
|
||||
@Builder.Default
|
||||
private Boolean returnTrace = false;
|
||||
}
|
||||
|
||||
@Data
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
@Schema(description = "Completion 对话响应")
|
||||
public static class CompletionVO implements Serializable {
|
||||
@Schema(description = "会话 ID")
|
||||
@JsonProperty("id")
|
||||
private String id;
|
||||
|
||||
@Schema(description = "回复内容")
|
||||
@JsonProperty("content")
|
||||
private String content;
|
||||
|
||||
@Schema(description = "引用来源")
|
||||
@JsonProperty("reference")
|
||||
private Map<String, Object> reference;
|
||||
|
||||
@Schema(description = "追踪信息")
|
||||
@JsonProperty("trace")
|
||||
private List<Object> trace;
|
||||
}
|
||||
|
||||
// ========== 5. Dify 兼容检索 - 对应 RAGFlow_Agent_Dify接口详解 ==========
|
||||
@Data
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
@Schema(description = "Dify 兼容检索请求")
|
||||
public static class DifyRetrievalReq implements Serializable {
|
||||
@Schema(description = "知识库 ID")
|
||||
@JsonProperty("knowledge_id")
|
||||
private String knowledgeId;
|
||||
|
||||
@Schema(description = "查询词")
|
||||
@JsonProperty("query")
|
||||
private String query;
|
||||
|
||||
@Schema(description = "检索设置")
|
||||
@JsonProperty("retrieval_setting")
|
||||
private Map<String, Object> retrievalSetting;
|
||||
|
||||
@Schema(description = "元数据过滤条件")
|
||||
@JsonProperty("metadata_condition")
|
||||
private Map<String, Object> metadataCondition;
|
||||
|
||||
@Schema(description = "是否使用知识图谱")
|
||||
@JsonProperty("use_kg")
|
||||
private Boolean useKg;
|
||||
}
|
||||
|
||||
@Data
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
@Schema(description = "Dify 兼容检索响应")
|
||||
public static class DifyRetrievalVO implements Serializable {
|
||||
@Schema(description = "检索结果列表")
|
||||
@JsonProperty("records")
|
||||
private List<Record> records;
|
||||
|
||||
@Data
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
@Schema(description = "检索记录")
|
||||
public static class Record implements Serializable {
|
||||
@Schema(description = "内容")
|
||||
@JsonProperty("content")
|
||||
private String content;
|
||||
|
||||
@Schema(description = "相似度分数")
|
||||
@JsonProperty("score")
|
||||
private Double score;
|
||||
|
||||
@Schema(description = "标题")
|
||||
@JsonProperty("title")
|
||||
private String title;
|
||||
|
||||
@Schema(description = "元数据")
|
||||
@JsonProperty("metadata")
|
||||
private Map<String, Object> metadata;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,126 +0,0 @@
|
||||
package xiaozhi.modules.knowledge.dto.bot;
|
||||
|
||||
import lombok.*;
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import java.io.Serializable;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
import jakarta.validation.constraints.*;
|
||||
|
||||
@Schema(description = "外部机器人 (Bot) 聚合 DTO")
|
||||
public class BotDTO {
|
||||
|
||||
// ========== 1. SearchBot (检索机器人) ==========
|
||||
|
||||
// 对应 /api/v1/searchbots/ask
|
||||
@Data
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
@Schema(description = "SearchBot 提问请求")
|
||||
public static class SearchAskReq implements Serializable {
|
||||
@Schema(description = "用户问题", requiredMode = Schema.RequiredMode.REQUIRED, example = "What is RAG?")
|
||||
@NotBlank(message = "问题不能为空")
|
||||
@JsonProperty("question")
|
||||
private String question;
|
||||
|
||||
@Schema(description = "是否返回引用", defaultValue = "false")
|
||||
@JsonProperty("quote")
|
||||
@Builder.Default
|
||||
private Boolean quote = false;
|
||||
|
||||
@Schema(description = "是否流式返回", defaultValue = "true")
|
||||
@JsonProperty("stream")
|
||||
@Builder.Default
|
||||
private Boolean stream = true;
|
||||
}
|
||||
|
||||
@Data
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
@Schema(description = "SearchBot 提问响应")
|
||||
public static class SearchAskVO implements Serializable {
|
||||
@Schema(description = "回答内容")
|
||||
@JsonProperty("answer")
|
||||
private String answer;
|
||||
|
||||
@Schema(description = "引用来源 (Value 结构通常对应 RetrievalDTO.HitVO)")
|
||||
@JsonProperty("reference")
|
||||
private Map<String, Object> reference;
|
||||
}
|
||||
|
||||
// 对应 /api/v1/searchbots/related_questions
|
||||
@Data
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
@Schema(description = "相关问题请求")
|
||||
public static class RelatedQuestionReq implements Serializable {
|
||||
@Schema(description = "用户问题", requiredMode = Schema.RequiredMode.REQUIRED)
|
||||
@NotBlank(message = "问题不能为空")
|
||||
@JsonProperty("question")
|
||||
private String question;
|
||||
}
|
||||
|
||||
// 对应 /api/v1/searchbots/mindmap
|
||||
@Data
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
@Schema(description = "思维导图请求")
|
||||
public static class MindMapReq implements Serializable {
|
||||
@Schema(description = "用户问题", requiredMode = Schema.RequiredMode.REQUIRED)
|
||||
@NotBlank(message = "问题不能为空")
|
||||
@JsonProperty("question")
|
||||
private String question;
|
||||
}
|
||||
|
||||
// ========== 2. AgentBot (嵌入式 Agent) ==========
|
||||
|
||||
// 对应 /api/v1/agentbots/{id}/inputs
|
||||
@Data
|
||||
@Builder
|
||||
@AllArgsConstructor
|
||||
@Schema(description = "AgentBot 输入参数请求")
|
||||
public static class AgentInputsReq implements Serializable {
|
||||
}
|
||||
|
||||
@Data
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
@Schema(description = "AgentBot 输入参数定义响应")
|
||||
public static class AgentInputsVO implements Serializable {
|
||||
@Schema(description = "表单变量定义列表")
|
||||
@JsonProperty("variables")
|
||||
private List<Map<String, Object>> variables;
|
||||
}
|
||||
|
||||
// 对应 /api/v1/agentbots/{id}/completions
|
||||
@Data
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
@Schema(description = "AgentBot 对话请求")
|
||||
public static class AgentCompletionReq implements Serializable {
|
||||
@Schema(description = "输入参数值")
|
||||
@JsonProperty("inputs")
|
||||
private Map<String, Object> inputs;
|
||||
|
||||
@Schema(description = "用户查询", requiredMode = Schema.RequiredMode.REQUIRED)
|
||||
@NotBlank(message = "查询内容不能为空")
|
||||
@JsonProperty("question")
|
||||
private String question;
|
||||
|
||||
@Schema(description = "是否流式返回", defaultValue = "true")
|
||||
@JsonProperty("stream")
|
||||
@Builder.Default
|
||||
private Boolean stream = true;
|
||||
|
||||
@Schema(description = "会话 ID")
|
||||
@JsonProperty("session_id")
|
||||
private String sessionId;
|
||||
}
|
||||
}
|
||||
@@ -1,50 +0,0 @@
|
||||
package xiaozhi.modules.knowledge.dto.chat;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.Data;
|
||||
|
||||
/**
|
||||
* 聊天对话请求 DTO (OpenAI 兼容格式)
|
||||
*/
|
||||
@Data
|
||||
@Schema(description = "聊天对话请求")
|
||||
public class ChatCompletionRequest implements Serializable {
|
||||
|
||||
@Schema(description = "模型标识 (对应 agent_id 或 bot_id)", requiredMode = Schema.RequiredMode.REQUIRED)
|
||||
@JsonProperty("model")
|
||||
private String model;
|
||||
|
||||
@Schema(description = "对话消息列表", requiredMode = Schema.RequiredMode.REQUIRED)
|
||||
@JsonProperty("messages")
|
||||
private List<Message> messages;
|
||||
|
||||
@Schema(description = "是否流式返回", defaultValue = "false")
|
||||
@JsonProperty("stream")
|
||||
private Boolean stream = false;
|
||||
|
||||
@Schema(description = "温度系数 (0-1)", defaultValue = "0.7")
|
||||
@JsonProperty("temperature")
|
||||
private Double temperature;
|
||||
|
||||
@Schema(description = "Session ID (可选,用于延续会话)")
|
||||
@JsonProperty("session_id")
|
||||
private String sessionId;
|
||||
|
||||
@Schema(description = "其他RAGFlow特定参数 (可选)")
|
||||
private Map<String, Object> extra;
|
||||
|
||||
@Data
|
||||
public static class Message implements Serializable {
|
||||
@Schema(description = "角色 (system, user, assistant)", requiredMode = Schema.RequiredMode.REQUIRED)
|
||||
private String role;
|
||||
|
||||
@Schema(description = "内容", requiredMode = Schema.RequiredMode.REQUIRED)
|
||||
private String content;
|
||||
}
|
||||
}
|
||||
@@ -1,523 +0,0 @@
|
||||
package xiaozhi.modules.knowledge.dto.chat;
|
||||
|
||||
import lombok.*;
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import java.io.Serializable;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
import jakarta.validation.constraints.*;
|
||||
|
||||
/**
|
||||
* 对话管理聚合 DTO
|
||||
* <p>
|
||||
* 容器类,内含对话助手、会话和消息的所有请求/响应对象。
|
||||
* </p>
|
||||
*/
|
||||
@Schema(description = "对话管理聚合 DTO")
|
||||
public class ChatDTO {
|
||||
|
||||
// ========== 1. 对话助手 (Assistant/Bot) 相关 ==========
|
||||
|
||||
/**
|
||||
* 提示词配置
|
||||
*/
|
||||
@Data
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
@Builder
|
||||
@Schema(description = "提示词配置")
|
||||
public static class PromptConfig implements Serializable {
|
||||
|
||||
@Schema(description = "系统提示词", example = "你是一个专业的客服助手...")
|
||||
@JsonProperty("prompt")
|
||||
private String systemPrompt;
|
||||
|
||||
@Schema(description = "开场白", example = "您好,我是您的智能助手,请问有什么可以帮您?")
|
||||
private String opener;
|
||||
|
||||
@Schema(description = "空结果回复", example = "抱歉,我没有找到相关信息。")
|
||||
@JsonProperty("empty_response")
|
||||
private String emptyResponse;
|
||||
|
||||
@Schema(description = "是否展示引用", example = "true")
|
||||
@JsonProperty("show_quote")
|
||||
private Boolean quote;
|
||||
|
||||
@Schema(description = "是否启用 TTS", example = "false")
|
||||
private Boolean tts;
|
||||
|
||||
@Schema(description = "相似度阈值 (0.0 - 1.0)", example = "0.2")
|
||||
@JsonProperty("similarity_threshold")
|
||||
private Float similarityThreshold;
|
||||
|
||||
@Schema(description = "关键词相似度权重 (0.0 - 1.0)", example = "0.7")
|
||||
@JsonProperty("keywords_similarity_weight")
|
||||
private Float vectorSimilarityWeight;
|
||||
|
||||
@Schema(description = "检索 Top N", example = "6")
|
||||
@JsonProperty("top_n")
|
||||
private Integer topK;
|
||||
|
||||
@Schema(description = "Rerank 模型", example = "rerank_model_001")
|
||||
@JsonProperty("rerank_model")
|
||||
private String rerankId;
|
||||
|
||||
@Schema(description = "是否启用多轮对话优化", example = "false")
|
||||
@JsonProperty("refine_multiturn")
|
||||
private Boolean refineMultigraph;
|
||||
|
||||
@Schema(description = "变量列表")
|
||||
private List<Map<String, Object>> variables;
|
||||
}
|
||||
|
||||
/**
|
||||
* LLM 配置
|
||||
*/
|
||||
@Data
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
@Builder
|
||||
@Schema(description = "LLM 模型配置")
|
||||
public static class LLMConfig implements Serializable {
|
||||
|
||||
@NotBlank(message = "模型名称不能为空")
|
||||
@Schema(description = "模型名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "gpt-4")
|
||||
@JsonProperty("model_name")
|
||||
private String modelName;
|
||||
|
||||
@Schema(description = "温度参数 (0.0 - 2.0)", example = "0.7")
|
||||
private Float temperature;
|
||||
|
||||
@Schema(description = "Top P 采样", example = "0.9")
|
||||
@JsonProperty("top_p")
|
||||
private Float topP;
|
||||
|
||||
@Schema(description = "最大 Token 数", example = "4096")
|
||||
@JsonProperty("max_tokens")
|
||||
private Integer maxTokens;
|
||||
|
||||
@Schema(description = "存在惩罚", example = "0.0")
|
||||
@JsonProperty("presence_penalty")
|
||||
private Float presencePenalty;
|
||||
|
||||
@Schema(description = "频率惩罚", example = "0.0")
|
||||
@JsonProperty("frequency_penalty")
|
||||
private Float frequencyPenalty;
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建助手请求
|
||||
*/
|
||||
@Data
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
@Builder
|
||||
@Schema(description = "创建助手请求")
|
||||
public static class AssistantCreateReq implements Serializable {
|
||||
|
||||
@NotBlank(message = "助手名称不能为空")
|
||||
@Schema(description = "助手名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "智能客服助手")
|
||||
private String name;
|
||||
|
||||
@Schema(description = "助手头像 (Base64 编码)", example = "")
|
||||
private String avatar;
|
||||
|
||||
@Schema(description = "关联的知识库 ID 列表", example = "[\"kb_001\", \"kb_002\"]")
|
||||
@JsonProperty("dataset_ids")
|
||||
private List<String> datasetIds;
|
||||
|
||||
@Schema(description = "助手描述", example = "这是一个智能客服助手")
|
||||
private String description;
|
||||
|
||||
@Schema(description = "LLM 模型配置")
|
||||
@JsonProperty("llm")
|
||||
private LLMConfig llm;
|
||||
|
||||
@Schema(description = "提示词配置")
|
||||
@JsonProperty("prompt")
|
||||
private PromptConfig promptConfig;
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新助手请求
|
||||
*/
|
||||
@Data
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
@Builder
|
||||
@Schema(description = "更新助手请求")
|
||||
public static class AssistantUpdateReq implements Serializable {
|
||||
|
||||
@Schema(description = "助手名称", example = "智能客服助手 V2")
|
||||
private String name;
|
||||
|
||||
@Schema(description = "助手头像 (Base64 编码)", example = "")
|
||||
private String avatar;
|
||||
|
||||
@Schema(description = "关联的知识库 ID 列表", example = "[\"kb_001\", \"kb_002\"]")
|
||||
@JsonProperty("dataset_ids")
|
||||
private List<String> datasetIds;
|
||||
|
||||
@Schema(description = "助手描述", example = "这是一个智能客服助手")
|
||||
private String description;
|
||||
|
||||
@Schema(description = "LLM 模型配置")
|
||||
@JsonProperty("llm")
|
||||
private LLMConfig llm;
|
||||
|
||||
@Schema(description = "提示词配置")
|
||||
@JsonProperty("prompt")
|
||||
private PromptConfig promptConfig;
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询助手列表请求
|
||||
*/
|
||||
@Data
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
@Builder
|
||||
@Schema(description = "查询助手列表请求")
|
||||
public static class AssistantListReq implements Serializable {
|
||||
|
||||
@Schema(description = "页码 (从 1 开始)", example = "1")
|
||||
private Integer page;
|
||||
|
||||
@Schema(description = "每页数量", example = "30")
|
||||
@JsonProperty("page_size")
|
||||
private Integer pageSize;
|
||||
|
||||
@Schema(description = "按名称过滤 (模糊匹配)", example = "客服")
|
||||
private String name;
|
||||
|
||||
@Schema(description = "排序字段: create_time / update_time", example = "create_time")
|
||||
private String orderby;
|
||||
|
||||
@Schema(description = "是否降序", example = "true")
|
||||
private Boolean desc;
|
||||
|
||||
@Schema(description = "按 ID 精确筛选", example = "assistant_001")
|
||||
private String id;
|
||||
}
|
||||
|
||||
/**
|
||||
* 助手详情 VO
|
||||
*/
|
||||
@Data
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
@Builder
|
||||
@Schema(description = "助手详情 VO")
|
||||
public static class AssistantVO implements Serializable {
|
||||
|
||||
@Schema(description = "助手 ID", example = "assistant_001")
|
||||
private String id;
|
||||
|
||||
@Schema(description = "租户 ID", example = "tenant_001")
|
||||
@JsonProperty("tenant_id")
|
||||
private String tenantId;
|
||||
|
||||
@Schema(description = "助手名称", example = "智能客服助手")
|
||||
private String name;
|
||||
|
||||
@Schema(description = "助手头像", example = "")
|
||||
private String avatar;
|
||||
|
||||
@Schema(description = "关联的知识库 ID 列表")
|
||||
@JsonProperty("dataset_ids")
|
||||
private List<String> datasetIds;
|
||||
|
||||
@Schema(description = "关联的知识库列表 (详情)")
|
||||
private List<SimpleDatasetVO> datasets;
|
||||
|
||||
@Schema(description = "助手描述")
|
||||
private String description;
|
||||
|
||||
@Schema(description = "LLM 模型配置")
|
||||
@JsonProperty("llm")
|
||||
private LLMConfig llm;
|
||||
|
||||
@Schema(description = "提示词配置")
|
||||
@JsonProperty("prompt")
|
||||
private PromptConfig promptConfig;
|
||||
|
||||
@Schema(description = "创建时间 (时间戳)", example = "1700000000000")
|
||||
@JsonProperty("create_time")
|
||||
private Long createTime;
|
||||
|
||||
@Schema(description = "更新时间 (时间戳)", example = "1700000001000")
|
||||
@JsonProperty("update_time")
|
||||
private Long updateTime;
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除助手请求
|
||||
*/
|
||||
@Data
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
@Builder
|
||||
@Schema(description = "删除助手请求")
|
||||
public static class AssistantDeleteReq implements Serializable {
|
||||
|
||||
@Schema(description = "要删除的助手 ID 列表", example = "[\"assistant_001\", \"assistant_002\"]")
|
||||
private List<String> ids;
|
||||
}
|
||||
|
||||
// ========== 2. 会话 (Session) 相关 ==========
|
||||
|
||||
/**
|
||||
* 创建会话请求
|
||||
*/
|
||||
@Data
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
@Builder
|
||||
@Schema(description = "创建会话请求")
|
||||
public static class SessionCreateReq implements Serializable {
|
||||
|
||||
@Schema(description = "会话名称", example = "技术咨询会话")
|
||||
private String name;
|
||||
|
||||
@Schema(description = "用户 ID", example = "user_001")
|
||||
@JsonProperty("user_id")
|
||||
private String userId;
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新会话请求
|
||||
*/
|
||||
@Data
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
@Builder
|
||||
@Schema(description = "更新会话请求")
|
||||
public static class SessionUpdateReq implements Serializable {
|
||||
|
||||
@Schema(description = "会话名称", example = "技术咨询会话 - 更新")
|
||||
private String name;
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询会话列表请求
|
||||
*/
|
||||
@Data
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
@Builder
|
||||
@Schema(description = "查询会话列表请求")
|
||||
public static class SessionListReq implements Serializable {
|
||||
|
||||
@Schema(description = "助手 ID", example = "assistant_001")
|
||||
@JsonProperty("assistant_id")
|
||||
private String assistantId;
|
||||
|
||||
@Schema(description = "页码 (从 1 开始)", example = "1")
|
||||
private Integer page;
|
||||
|
||||
@Schema(description = "每页数量", example = "30")
|
||||
@JsonProperty("page_size")
|
||||
private Integer pageSize;
|
||||
|
||||
@Schema(description = "按名称过滤", example = "技术")
|
||||
private String name;
|
||||
|
||||
@Schema(description = "排序字段", example = "create_time")
|
||||
private String orderby;
|
||||
|
||||
@Schema(description = "是否降序", example = "true")
|
||||
private Boolean desc;
|
||||
|
||||
@Schema(description = "会话 ID 精确筛选", example = "session_001")
|
||||
private String id;
|
||||
|
||||
@Schema(description = "用户标识筛选", example = "user_001")
|
||||
@JsonProperty("user_id")
|
||||
private String userId;
|
||||
}
|
||||
|
||||
/**
|
||||
* 会话详情 VO
|
||||
*/
|
||||
@Data
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
@Builder
|
||||
@Schema(description = "会话详情 VO")
|
||||
public static class SessionVO implements Serializable {
|
||||
|
||||
@Schema(description = "会话 ID", example = "session_001")
|
||||
private String id;
|
||||
|
||||
@Schema(description = "助手 ID", example = "assistant_001")
|
||||
@JsonProperty("chat_id")
|
||||
private String chatId;
|
||||
|
||||
@Schema(description = "助手 ID (兼容旧版)", example = "assistant_001")
|
||||
@JsonProperty("assistant_id")
|
||||
private String assistantId;
|
||||
|
||||
@Schema(description = "会话名称", example = "技术咨询会话")
|
||||
private String name;
|
||||
|
||||
@Schema(description = "创建时间 (时间戳)", example = "1700000000000")
|
||||
@JsonProperty("create_time")
|
||||
private Long createTime;
|
||||
|
||||
@Schema(description = "更新时间 (时间戳)", example = "1700000001000")
|
||||
@JsonProperty("update_time")
|
||||
private Long updateTime;
|
||||
|
||||
@Schema(description = "创建日期", example = "2024-05-01 10:00:00")
|
||||
@JsonProperty("create_date")
|
||||
private String createDate;
|
||||
|
||||
@Schema(description = "更新日期", example = "2024-05-01 10:00:00")
|
||||
@JsonProperty("update_date")
|
||||
private String updateDate;
|
||||
|
||||
@Schema(description = "用户 ID", example = "user_001")
|
||||
@JsonProperty("user_id")
|
||||
private String userId;
|
||||
|
||||
@Schema(description = "对话历史消息列表")
|
||||
private List<Map<String, Object>> messages;
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除会话请求
|
||||
*/
|
||||
@Data
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
@Builder
|
||||
@Schema(description = "删除会话请求")
|
||||
public static class SessionDeleteReq implements Serializable {
|
||||
|
||||
@Schema(description = "要删除的会话 ID 列表", example = "[\"session_001\", \"session_002\"]")
|
||||
private List<String> ids;
|
||||
}
|
||||
|
||||
// ========== 3. 消息/对话 (Completion) 相关 ==========
|
||||
|
||||
/**
|
||||
* 发送消息请求
|
||||
*/
|
||||
@Data
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
@Builder
|
||||
@Schema(description = "发送消息请求")
|
||||
public static class CompletionReq implements Serializable {
|
||||
|
||||
@NotBlank(message = "问题内容不能为空")
|
||||
@Schema(description = "用户问题", requiredMode = Schema.RequiredMode.REQUIRED, example = "请介绍一下你们的产品")
|
||||
private String question;
|
||||
|
||||
@Schema(description = "是否使用流式响应 (SSE)", example = "true")
|
||||
@Builder.Default
|
||||
private Boolean stream = true;
|
||||
|
||||
@NotBlank(message = "会话 ID 不能为空")
|
||||
@Schema(description = "会话 ID (可选,不传则创建新会话)", example = "session_001")
|
||||
@JsonProperty("session_id")
|
||||
private String sessionId;
|
||||
|
||||
@Schema(description = "是否展示引用", example = "true")
|
||||
private Boolean quote;
|
||||
|
||||
@Schema(description = "指定检索的文档 ID 列表 (逗号分隔)", example = "doc_001,doc_002")
|
||||
@JsonProperty("doc_ids")
|
||||
private String docIds;
|
||||
|
||||
@Schema(description = "元数据过滤条件")
|
||||
@JsonProperty("metadata_condition")
|
||||
private Map<String, Object> metadataCondition;
|
||||
}
|
||||
|
||||
/**
|
||||
* 消息响应 VO
|
||||
*/
|
||||
@Data
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
@Builder
|
||||
@Schema(description = "消息响应 VO")
|
||||
public static class CompletionVO implements Serializable {
|
||||
|
||||
@Schema(description = "AI 回答内容")
|
||||
private String answer;
|
||||
|
||||
@Schema(description = "引用信息")
|
||||
private Reference reference;
|
||||
|
||||
@Schema(description = "会话 ID", example = "session_001")
|
||||
@JsonProperty("session_id")
|
||||
private String sessionId;
|
||||
|
||||
@Schema(description = "任务 ID (用于流式响应追踪)", example = "task_001")
|
||||
@JsonProperty("task_id")
|
||||
private String taskId;
|
||||
|
||||
/**
|
||||
* 引用信息 (检索命中结果)
|
||||
*/
|
||||
@Data
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
@Builder
|
||||
@Schema(description = "引用信息")
|
||||
public static class Reference implements Serializable {
|
||||
|
||||
@Schema(description = "命中的文档块列表")
|
||||
private List<xiaozhi.modules.knowledge.dto.document.RetrievalDTO.HitVO> chunks;
|
||||
|
||||
@Schema(description = "文档聚合信息")
|
||||
@JsonProperty("doc_aggs")
|
||||
private List<DocAgg> docAggs;
|
||||
}
|
||||
|
||||
/**
|
||||
* 文档聚合信息
|
||||
*/
|
||||
@Data
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
@Builder
|
||||
@Schema(description = "文档聚合信息")
|
||||
public static class DocAgg implements Serializable {
|
||||
|
||||
@Schema(description = "文档 ID", example = "doc_001")
|
||||
@JsonProperty("doc_id")
|
||||
private String docId;
|
||||
|
||||
@Schema(description = "文档名称", example = "产品手册.pdf")
|
||||
@JsonProperty("doc_name")
|
||||
private String docName;
|
||||
|
||||
@Schema(description = "命中次数", example = "3")
|
||||
private Integer count;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 简易知识库 VO (用于 Assistant 列表)
|
||||
*/
|
||||
@Data
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
@Builder
|
||||
@Schema(description = "简易知识库 VO")
|
||||
public static class SimpleDatasetVO implements Serializable {
|
||||
@Schema(description = "知识库 ID")
|
||||
private String id;
|
||||
@Schema(description = "知识库名称")
|
||||
private String name;
|
||||
@Schema(description = "头像")
|
||||
private String avatar;
|
||||
@Schema(description = "分块数量")
|
||||
@JsonProperty("chunk_num")
|
||||
private Integer chunkNum;
|
||||
}
|
||||
}
|
||||
@@ -1,79 +0,0 @@
|
||||
package xiaozhi.modules.knowledge.dto.common;
|
||||
|
||||
import lombok.*;
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import java.io.Serializable;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
import jakarta.validation.constraints.*;
|
||||
|
||||
@Schema(description = "通用扩展功能 DTO")
|
||||
public class CommonDTO {
|
||||
|
||||
// ========== 1. 引用详情 (detail_share_embedded) ==========
|
||||
|
||||
@Data
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
@Schema(description = "引用详情请求")
|
||||
public static class ReferenceDetailReq implements Serializable {
|
||||
@Schema(description = "切片 ID", requiredMode = Schema.RequiredMode.REQUIRED)
|
||||
@NotBlank(message = "切片 ID 不能为空")
|
||||
@JsonProperty("chunk_id")
|
||||
private String chunkId;
|
||||
|
||||
@Schema(description = "知识库 ID")
|
||||
@JsonProperty("knowledge_id")
|
||||
private String knowledgeId;
|
||||
}
|
||||
|
||||
@Data
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
@Schema(description = "引用详情响应")
|
||||
public static class ReferenceDetailVO implements Serializable {
|
||||
@Schema(description = "切片 ID")
|
||||
@JsonProperty("chunk_id")
|
||||
private String chunkId;
|
||||
|
||||
@Schema(description = "完整内容")
|
||||
@JsonProperty("content_with_weight")
|
||||
private String contentWithWeight;
|
||||
|
||||
@Schema(description = "文档名称")
|
||||
@JsonProperty("doc_name")
|
||||
private String docName;
|
||||
|
||||
@Schema(description = "图片 ID 列表")
|
||||
@JsonProperty("img_id")
|
||||
private String imageId; // 注意:RAGFlow 有时返回 String 有时返回 List,需根据实际情况确认,暂定 String 用于 ID
|
||||
|
||||
@Schema(description = "文档 ID")
|
||||
@JsonProperty("doc_id")
|
||||
private String docId;
|
||||
}
|
||||
|
||||
// ========== 2. 通用问答 (ask_about) - 调试用 ==========
|
||||
|
||||
@Data
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
@Schema(description = "通用问答请求 (调试用)")
|
||||
public static class AskAboutReq implements Serializable {
|
||||
@Schema(description = "用户问题", requiredMode = Schema.RequiredMode.REQUIRED, example = "What is this dataset about?")
|
||||
@NotBlank(message = "问题不能为空")
|
||||
@JsonProperty("question")
|
||||
private String question;
|
||||
|
||||
@Schema(description = "数据集 ID 列表", requiredMode = Schema.RequiredMode.REQUIRED)
|
||||
@NotEmpty(message = "数据集列表不能为空")
|
||||
@JsonProperty("dataset_ids")
|
||||
private List<String> datasetIds;
|
||||
}
|
||||
|
||||
// 响应通常复用 String 或者简单的 Map 结构,视具体实现而定,暂不定义专用 VO
|
||||
}
|
||||
@@ -1,363 +0,0 @@
|
||||
package xiaozhi.modules.knowledge.dto.file;
|
||||
|
||||
import lombok.*;
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import java.io.Serializable;
|
||||
import java.util.List;
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
import jakarta.validation.constraints.*;
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
|
||||
/**
|
||||
* 文件管理聚合 DTO
|
||||
* <p>
|
||||
* 容器类,内含文件模块所有请求/响应对象的静态内部类定义。
|
||||
* </p>
|
||||
*/
|
||||
@Schema(description = "文件管理聚合 DTO")
|
||||
public class FileDTO {
|
||||
|
||||
// ========== 请求类 ==========
|
||||
|
||||
/**
|
||||
* 文件上传请求 (对应接口 1: upload)
|
||||
*/
|
||||
@Data
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
@Builder
|
||||
@Schema(description = "文件上传请求")
|
||||
public static class UploadReq implements Serializable {
|
||||
|
||||
@NotNull(message = "文件不能为空")
|
||||
@Schema(description = "上传的文件", requiredMode = Schema.RequiredMode.REQUIRED)
|
||||
private MultipartFile file;
|
||||
|
||||
@Schema(description = "父文件夹 ID (为空则上传到根目录)", example = "folder_001")
|
||||
@JsonProperty("parent_id")
|
||||
private String parentId;
|
||||
}
|
||||
|
||||
/**
|
||||
* 新建文件夹请求 (对应接口 2: create)
|
||||
*/
|
||||
@Data
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
@Builder
|
||||
@Schema(description = "新建文件夹请求")
|
||||
public static class CreateReq implements Serializable {
|
||||
|
||||
@NotBlank(message = "文件夹名称不能为空")
|
||||
@Schema(description = "文件夹名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "新建文件夹")
|
||||
private String name;
|
||||
|
||||
@Schema(description = "父文件夹 ID (为空则创建在根目录)", example = "folder_001")
|
||||
@JsonProperty("parent_id")
|
||||
private String parentId;
|
||||
|
||||
@NotBlank(message = "类型不能为空")
|
||||
@Schema(description = "类型: FOLDER", requiredMode = Schema.RequiredMode.REQUIRED, example = "FOLDER")
|
||||
@Builder.Default
|
||||
private String type = "FOLDER";
|
||||
}
|
||||
|
||||
/**
|
||||
* 重命名请求 (对应接口 6: rename)
|
||||
*/
|
||||
@Data
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
@Builder
|
||||
@Schema(description = "重命名请求")
|
||||
public static class RenameReq implements Serializable {
|
||||
|
||||
@NotBlank(message = "文件 ID 不能为空")
|
||||
@Schema(description = "文件/文件夹 ID", requiredMode = Schema.RequiredMode.REQUIRED, example = "file_001")
|
||||
@JsonProperty("file_id")
|
||||
private String fileId;
|
||||
|
||||
@NotBlank(message = "新名称不能为空")
|
||||
@Schema(description = "新名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "重命名后的文件")
|
||||
private String name;
|
||||
}
|
||||
|
||||
/**
|
||||
* 移动请求 (对应接口 7: move)
|
||||
*/
|
||||
@Data
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
@Builder
|
||||
@Schema(description = "移动请求")
|
||||
public static class MoveReq implements Serializable {
|
||||
|
||||
@NotEmpty(message = "源文件 ID 列表不能为空")
|
||||
@Schema(description = "源文件/文件夹 ID 列表", requiredMode = Schema.RequiredMode.REQUIRED, example = "[\"file_001\", \"file_002\"]")
|
||||
@JsonProperty("src_file_ids")
|
||||
private List<String> srcFileIds;
|
||||
|
||||
@NotBlank(message = "目标文件夹 ID 不能为空")
|
||||
@Schema(description = "目标文件夹 ID", requiredMode = Schema.RequiredMode.REQUIRED, example = "folder_002")
|
||||
@JsonProperty("dest_file_id")
|
||||
private String destFileId;
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量删除请求 (对应接口 8: rm)
|
||||
*/
|
||||
@Data
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
@Builder
|
||||
@Schema(description = "批量删除请求")
|
||||
public static class RemoveReq implements Serializable {
|
||||
|
||||
@NotEmpty(message = "文件 ID 列表不能为空")
|
||||
@Schema(description = "文件/文件夹 ID 列表", requiredMode = Schema.RequiredMode.REQUIRED, example = "[\"file_001\", \"file_002\"]")
|
||||
@JsonProperty("file_ids")
|
||||
private List<String> fileIds;
|
||||
}
|
||||
|
||||
/**
|
||||
* 导入知识库请求 (对应接口 9: convert)
|
||||
*/
|
||||
@Data
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
@Builder
|
||||
@Schema(description = "导入知识库请求")
|
||||
public static class ConvertReq implements Serializable {
|
||||
|
||||
@NotEmpty(message = "文件 ID 列表不能为空")
|
||||
@Schema(description = "文件 ID 列表", requiredMode = Schema.RequiredMode.REQUIRED, example = "[\"file_001\", \"file_002\"]")
|
||||
@JsonProperty("file_ids")
|
||||
private List<String> fileIds;
|
||||
|
||||
@NotEmpty(message = "知识库 ID 列表不能为空")
|
||||
@Schema(description = "目标知识库 ID 列表", requiredMode = Schema.RequiredMode.REQUIRED, example = "[\"kb_001\"]")
|
||||
@JsonProperty("kb_ids")
|
||||
private List<String> kbIds;
|
||||
}
|
||||
|
||||
/**
|
||||
* 列表查询请求 (对应接口 3: list_files)
|
||||
*/
|
||||
@Data
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
@Builder
|
||||
@Schema(description = "列表查询请求")
|
||||
public static class ListReq implements Serializable {
|
||||
|
||||
@Schema(description = "父文件夹 ID (为空则查询根目录)", example = "folder_001")
|
||||
@JsonProperty("parent_id")
|
||||
private String parentId;
|
||||
|
||||
@Schema(description = "关键词搜索", example = "文档")
|
||||
private String keywords;
|
||||
|
||||
@Schema(description = "页码 (从 1 开始)", example = "1")
|
||||
private Integer page;
|
||||
|
||||
@Schema(description = "每页数量", example = "30")
|
||||
@JsonProperty("page_size")
|
||||
private Integer pageSize;
|
||||
|
||||
@Schema(description = "排序字段: create_time / update_time / name / size", example = "create_time")
|
||||
private String orderby;
|
||||
|
||||
@Schema(description = "是否降序", example = "true")
|
||||
private Boolean desc;
|
||||
}
|
||||
|
||||
// ========== 响应类 ==========
|
||||
|
||||
/**
|
||||
* 文件/文件夹基础信息 VO
|
||||
*/
|
||||
@Data
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
@Builder
|
||||
@Schema(description = "文件/文件夹基础信息")
|
||||
public static class InfoVO implements Serializable {
|
||||
|
||||
@Schema(description = "文件/文件夹 ID", example = "file_001")
|
||||
private String id;
|
||||
|
||||
@Schema(description = "父文件夹 ID", example = "folder_001")
|
||||
@JsonProperty("parent_id")
|
||||
private String parentId;
|
||||
|
||||
@Schema(description = "租户 ID", example = "tenant_001")
|
||||
@JsonProperty("tenant_id")
|
||||
private String tenantId;
|
||||
|
||||
@Schema(description = "创建者 ID", example = "user_001")
|
||||
@JsonProperty("created_by")
|
||||
private String createdBy;
|
||||
|
||||
@Schema(description = "类型: FOLDER / FILE", example = "FOLDER")
|
||||
private String type;
|
||||
|
||||
@Schema(description = "名称", example = "我的文件夹")
|
||||
private String name;
|
||||
|
||||
@Schema(description = "路径位置", example = "/root/folder")
|
||||
private String location;
|
||||
|
||||
@Schema(description = "文件大小 (字节)", example = "1024")
|
||||
private Long size;
|
||||
|
||||
@Schema(description = "来源类型", example = "local")
|
||||
@JsonProperty("source_type")
|
||||
private String sourceType;
|
||||
|
||||
@Schema(description = "创建时间 (时间戳)", example = "1700000000000")
|
||||
@JsonProperty("create_time")
|
||||
private Long createTime;
|
||||
|
||||
@Schema(description = "创建日期 (格式化)", example = "2024-01-15 10:30:00")
|
||||
@JsonProperty("create_date")
|
||||
private String createDate;
|
||||
|
||||
@Schema(description = "更新时间 (时间戳)", example = "1700000001000")
|
||||
@JsonProperty("update_time")
|
||||
private Long updateTime;
|
||||
|
||||
@Schema(description = "更新日期 (格式化)", example = "2024-01-15 11:00:00")
|
||||
@JsonProperty("update_date")
|
||||
private String updateDate;
|
||||
|
||||
@Schema(description = "文件扩展名", example = "pdf")
|
||||
private String extension;
|
||||
}
|
||||
|
||||
/**
|
||||
* 列表响应 VO (对应接口 3: list_files)
|
||||
*/
|
||||
@Data
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
@Builder
|
||||
@Schema(description = "文件列表响应")
|
||||
public static class ListVO implements Serializable {
|
||||
|
||||
@Schema(description = "总记录数", example = "100")
|
||||
private Long total;
|
||||
|
||||
@Schema(description = "当前父文件夹信息")
|
||||
@JsonProperty("parent_folder")
|
||||
private InfoVO parentFolder;
|
||||
|
||||
@Schema(description = "文件/文件夹列表")
|
||||
private List<InfoVO> files;
|
||||
|
||||
@Schema(description = "面包屑导航路径")
|
||||
private List<InfoVO> breadcrumb;
|
||||
}
|
||||
|
||||
/**
|
||||
* 转换结果项 VO (对应接口 9: convert)
|
||||
*/
|
||||
@Data
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
@Builder
|
||||
@Schema(description = "文件转换结果项")
|
||||
public static class ConvertVO implements Serializable {
|
||||
|
||||
@Schema(description = "转换记录 ID", example = "convert_001")
|
||||
private String id;
|
||||
|
||||
@Schema(description = "源文件 ID", example = "file_001")
|
||||
@JsonProperty("file_id")
|
||||
private String fileId;
|
||||
|
||||
@Schema(description = "目标文档 ID", example = "doc_001")
|
||||
@JsonProperty("document_id")
|
||||
private String documentId;
|
||||
|
||||
@Schema(description = "创建时间 (时间戳)", example = "1700000000000")
|
||||
@JsonProperty("create_time")
|
||||
private Long createTime;
|
||||
|
||||
@Schema(description = "创建日期 (格式化)", example = "2024-01-15 10:30:00")
|
||||
@JsonProperty("create_date")
|
||||
private String createDate;
|
||||
|
||||
@Schema(description = "更新时间 (时间戳)", example = "1700000001000")
|
||||
@JsonProperty("update_time")
|
||||
private Long updateTime;
|
||||
|
||||
@Schema(description = "更新日期 (格式化)", example = "2024-01-15 11:00:00")
|
||||
@JsonProperty("update_date")
|
||||
private String updateDate;
|
||||
}
|
||||
|
||||
/**
|
||||
* 转换状态 VO (对应接口 10: get_convert_status)
|
||||
*/
|
||||
@Data
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
@Builder
|
||||
@Schema(description = "文件转换状态")
|
||||
public static class ConvertStatusVO implements Serializable {
|
||||
|
||||
@Schema(description = "转换状态: pending / processing / completed / failed", example = "completed")
|
||||
private String status;
|
||||
|
||||
@Schema(description = "转换进度 (0.0 - 1.0)", example = "1.0")
|
||||
private Float progress;
|
||||
|
||||
@Schema(description = "状态消息", example = "转换完成")
|
||||
private String message;
|
||||
}
|
||||
|
||||
/**
|
||||
* 面包屑 VO (对应接口 12: all_parent_folder)
|
||||
*/
|
||||
@Data
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
@Builder
|
||||
@Schema(description = "面包屑导航 (所有父文件夹)")
|
||||
public static class BreadcrumbVO implements Serializable {
|
||||
|
||||
@Schema(description = "父文件夹列表 (从根到当前的路径)")
|
||||
@JsonProperty("parent_folders")
|
||||
private List<InfoVO> parentFolders;
|
||||
}
|
||||
|
||||
/**
|
||||
* 根目录信息 VO (对应接口 10: get_root_folder)
|
||||
*/
|
||||
@Data
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
@Builder
|
||||
@Schema(description = "根目录信息")
|
||||
public static class RootFolderVO implements Serializable {
|
||||
|
||||
@Schema(description = "根文件夹信息")
|
||||
@JsonProperty("root_folder")
|
||||
private InfoVO rootFolder;
|
||||
}
|
||||
|
||||
/**
|
||||
* 父目录信息 VO (对应接口 11: get_parent_folder)
|
||||
*/
|
||||
@Data
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
@Builder
|
||||
@Schema(description = "父目录信息")
|
||||
public static class ParentFolderVO implements Serializable {
|
||||
|
||||
@Schema(description = "父文件夹信息")
|
||||
@JsonProperty("parent_folder")
|
||||
private InfoVO parentFolder;
|
||||
}
|
||||
}
|
||||
@@ -485,16 +485,11 @@ public class RAGFlowAdapter extends KnowledgeBaseAdapter {
|
||||
|
||||
@Override
|
||||
public Integer getDocumentCount(String datasetId) {
|
||||
try {
|
||||
DatasetDTO.InfoVO info = getDatasetInfo(datasetId);
|
||||
if (info != null && info.getDocumentCount() != null) {
|
||||
return info.getDocumentCount().intValue();
|
||||
}
|
||||
return 0;
|
||||
} catch (Exception e) {
|
||||
log.warn("获取文档数量失败: {}", e.getMessage());
|
||||
return 0;
|
||||
DatasetDTO.InfoVO info = getDatasetInfo(datasetId);
|
||||
if (info != null && info.getDocumentCount() != null) {
|
||||
return info.getDocumentCount().intValue();
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -518,7 +513,7 @@ public class RAGFlowAdapter extends KnowledgeBaseAdapter {
|
||||
return null;
|
||||
} catch (Exception e) {
|
||||
log.warn("获取数据集信息失败: datasetId={}, error={}", datasetId, e.getMessage());
|
||||
return null;
|
||||
throw convertToRenException(e);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -100,9 +100,9 @@ public class KnowledgeBaseServiceImpl extends BaseServiceImpl<KnowledgeBaseDao,
|
||||
}
|
||||
|
||||
DatasetDTO.InfoVO datasetInfo = adapter.getDatasetInfo(dto.getDatasetId());
|
||||
|
||||
// getDatasetInfo 正常返回 null 表示远端确实不存在;异常时已抛出 RenException 由外层 catch 接管
|
||||
if (datasetInfo == null) {
|
||||
// RAGFlow 端已删除 → 本地级联清理
|
||||
// RAGFlow 端已确认删除 → 本地级联清理
|
||||
log.info("数据集 {} 在 RAGFlow 端不存在,执行本地清理", dto.getDatasetId());
|
||||
cleanupLocalDataset(dto.getDatasetId(), dto.getId());
|
||||
// 标记为已删除,让上层从列表中移除
|
||||
@@ -145,8 +145,9 @@ public class KnowledgeBaseServiceImpl extends BaseServiceImpl<KnowledgeBaseDao,
|
||||
}
|
||||
|
||||
} catch (Exception e) {
|
||||
log.warn("同步数据集信息失败 {}: {}", dto.getName(), e.getMessage());
|
||||
log.error("同步数据集信息失败 {}: {}", dto.getName(), e.getMessage());
|
||||
dto.setDocumentCount(0);
|
||||
dto.setErrorMessage(e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -529,7 +529,8 @@ public class KnowledgeFilesServiceImpl extends BaseServiceImpl<DocumentDao, Docu
|
||||
adapter.deleteDocument(datasetId, req);
|
||||
log.info("远程批量删除请求成功");
|
||||
} catch (Exception e) {
|
||||
log.warn("远程删除请求部分或全部失败: {}", e.getMessage());
|
||||
log.error("远程删除请求失败,中止本地清理以避免数据不一致: {}", e.getMessage());
|
||||
throw new RenException(e.getMessage());
|
||||
}
|
||||
|
||||
// 4. 原子化清理本地影子记录并同步统计数据
|
||||
|
||||
@@ -87,8 +87,7 @@ public class ShiroConfig {
|
||||
filterMap.put("/user/retrieve-password", "anon");
|
||||
// 将config路径使用server服务过滤器
|
||||
filterMap.put("/config/**", "server");
|
||||
filterMap.put("/device/address-book/lookup", "server");
|
||||
filterMap.put("/device/call/forward", "server");
|
||||
filterMap.put("/device/address-book/call", "server");
|
||||
filterMap.put("/agent/chat-history/report", "server");
|
||||
filterMap.put("/agent/chat-history/download/**", "anon");
|
||||
filterMap.put("/agent/chat-summary/**", "server");
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
-- 更新EdgeTTS供应器增加语速、音调、音量配置
|
||||
UPDATE `ai_model_provider`
|
||||
SET fields = '[{"key":"voice","label":"音色","type":"string"},{"key":"output_dir","label":"输出目录","type":"string"},{"key":"rate","label":"语速(-100~100)","type":"number"},{"key":"volume","label":"音量(0~100)","type":"number"},{"key":"pitch","label":"音调(-100~100)","type":"number"}]'
|
||||
WHERE id = 'SYSTEM_TTS_edge';
|
||||
|
||||
UPDATE `ai_model_config` SET
|
||||
`remark` = 'EdgeTTS配置说明:
|
||||
1. 使用微软Edge TTS服务
|
||||
2. 支持多种语言和音色
|
||||
3. 免费使用,无需注册
|
||||
4. 需要网络连接
|
||||
5. 输出文件保存在tmp/目录
|
||||
6. 语速:-100~100,0为正常速度
|
||||
7. 音量:0~100,50为正常音量
|
||||
8. 音调:-100~100,0为正常音调' WHERE `id` = 'TTS_EdgeTTS';
|
||||
@@ -0,0 +1,40 @@
|
||||
-- 修正、添加Minimax流式TTS音色
|
||||
delete from `ai_tts_voice` where tts_model_id = 'TTS_MinimaxStreamTTS';
|
||||
|
||||
INSERT INTO `ai_tts_voice` VALUES ('TTS_MinimaxStreamTTS_0001', 'TTS_MinimaxStreamTTS', '少女音', 'female-shaonv', '普通话', NULL, NULL, NULL, NULL, 1, NULL, NULL, NULL, NULL);
|
||||
INSERT INTO `ai_tts_voice` VALUES ('TTS_MinimaxStreamTTS_0002', 'TTS_MinimaxStreamTTS', '成熟女声', 'female-chengshu', '普通话', NULL, NULL, NULL, NULL, 2, NULL, NULL, NULL, NULL);
|
||||
INSERT INTO `ai_tts_voice` VALUES ('TTS_MinimaxStreamTTS_0003', 'TTS_MinimaxStreamTTS', '霸道少爷', 'badao_shaoye', '普通话', NULL, NULL, NULL, NULL, 3, NULL, NULL, NULL, NULL);
|
||||
INSERT INTO `ai_tts_voice` VALUES ('TTS_MinimaxStreamTTS_0004', 'TTS_MinimaxStreamTTS', '病娇弟弟', 'bingjiao_didi', '普通话', NULL, NULL, NULL, NULL, 4, NULL, NULL, NULL, NULL);
|
||||
INSERT INTO `ai_tts_voice` VALUES ('TTS_MinimaxStreamTTS_0005', 'TTS_MinimaxStreamTTS', '纯真学弟', 'chunzhen_xuedi', '普通话', NULL, NULL, NULL, NULL, 5, NULL, NULL, NULL, NULL);
|
||||
INSERT INTO `ai_tts_voice` VALUES ('TTS_MinimaxStreamTTS_0006', 'TTS_MinimaxStreamTTS', '冷淡学长', 'lengdan_xiongzhang', '普通话', NULL, NULL, NULL, NULL, 6, NULL, NULL, NULL, NULL);
|
||||
INSERT INTO `ai_tts_voice` VALUES ('TTS_MinimaxStreamTTS_0007', 'TTS_MinimaxStreamTTS', '甜美小玲', 'tianxin_xiaoling', '普通话', NULL, NULL, NULL, NULL, 7, NULL, NULL, NULL, NULL);
|
||||
INSERT INTO `ai_tts_voice` VALUES ('TTS_MinimaxStreamTTS_0008', 'TTS_MinimaxStreamTTS', '俏皮萌妹', 'qiaopi_mengmei', '普通话', NULL, NULL, NULL, NULL, 8, NULL, NULL, NULL, NULL);
|
||||
INSERT INTO `ai_tts_voice` VALUES ('TTS_MinimaxStreamTTS_0009', 'TTS_MinimaxStreamTTS', '妩媚御姐', 'wumei_yujie', '普通话', NULL, NULL, NULL, NULL, 9, NULL, NULL, NULL, NULL);
|
||||
INSERT INTO `ai_tts_voice` VALUES ('TTS_MinimaxStreamTTS_0010', 'TTS_MinimaxStreamTTS', '调教学妹', 'diadia_xuemei', '普通话', NULL, NULL, NULL, NULL, 10, NULL, NULL, NULL, NULL);
|
||||
INSERT INTO `ai_tts_voice` VALUES ('TTS_MinimaxStreamTTS_0011', 'TTS_MinimaxStreamTTS', '淡雅学姐', 'danya_xuejie', '普通话', NULL, NULL, NULL, NULL, 11, NULL, NULL, NULL, NULL);
|
||||
INSERT INTO `ai_tts_voice` VALUES ('TTS_MinimaxStreamTTS_0012', 'TTS_MinimaxStreamTTS', 'Santa Claus', 'Santa_Claus', '普通话', NULL, NULL, NULL, NULL, 12, NULL, NULL, NULL, NULL);
|
||||
INSERT INTO `ai_tts_voice` VALUES ('TTS_MinimaxStreamTTS_0013', 'TTS_MinimaxStreamTTS', 'Grinch', 'Grinch', '普通话', NULL, NULL, NULL, NULL, 13, NULL, NULL, NULL, NULL);
|
||||
INSERT INTO `ai_tts_voice` VALUES ('TTS_MinimaxStreamTTS_0014', 'TTS_MinimaxStreamTTS', '专业女主持', 'Cantonese_ProfessionalHost(F)', '粤语', "https://filecdn.minimax.chat/public/84589766-c651-477b-b320-36b89599f953.mp3", NULL, NULL, NULL, 14, NULL, NULL, NULL, NULL);
|
||||
INSERT INTO `ai_tts_voice` VALUES ('TTS_MinimaxStreamTTS_0015', 'TTS_MinimaxStreamTTS', '温柔女声', 'Cantonese_GentleLady', '粤语', "https://filecdn.minimax.chat/public/d391936d-d586-4dab-886c-a2a084a20920.mp3", NULL, NULL, NULL, 15, NULL, NULL, NULL, NULL);
|
||||
INSERT INTO `ai_tts_voice` VALUES ('TTS_MinimaxStreamTTS_0016', 'TTS_MinimaxStreamTTS', '专业男主持', 'Cantonese_ProfessionalHost(M)', '粤语', NULL, NULL, NULL, NULL, 16, NULL, NULL, NULL, NULL);
|
||||
INSERT INTO `ai_tts_voice` VALUES ('TTS_MinimaxStreamTTS_0017', 'TTS_MinimaxStreamTTS', '活泼男声', 'Cantonese_PlayfulMan', '粤语', NULL, NULL, NULL, NULL, 17, NULL, NULL, NULL, NULL);
|
||||
INSERT INTO `ai_tts_voice` VALUES ('TTS_MinimaxStreamTTS_0018', 'TTS_MinimaxStreamTTS', '可爱女孩', 'Cantonese_CuteGirl', '粤语', "https://filecdn.minimax.chat/public/ff96520a-ac12-491d-945e-d6337467eb7d.mp3", NULL, NULL, NULL, 18, NULL, NULL, NULL, NULL);
|
||||
INSERT INTO `ai_tts_voice` VALUES ('TTS_MinimaxStreamTTS_0019', 'TTS_MinimaxStreamTTS', '善良女声', 'Cantonese_KindWoman', '粤语', "https://filecdn.minimax.chat/public/78239a18-5e9c-4538-94f6-2f504c080374.mp3", NULL, NULL, NULL, 19, NULL, NULL, NULL, NULL);
|
||||
INSERT INTO `ai_tts_voice` VALUES ('TTS_MinimaxStreamTTS_0020', 'TTS_MinimaxStreamTTS', 'Crisp News Anchor', 'Cantonese_crisp_news_anchor_vv2', '粤语', 'https://cdn.hailuoai.video/open-hailuo-video-web/public_assets/741a5a4b-f42a-4a9b-ae83-3954d097ab73.mp3', NULL, NULL, NULL, 20, NULL, NULL, NULL, NULL);
|
||||
INSERT INTO `ai_tts_voice` VALUES ('TTS_MinimaxStreamTTS_0021', 'TTS_MinimaxStreamTTS', 'Pro Reporter', 'Cantonese_professional_reporter_vv2', '粤语', 'https://cdn.hailuoai.video/open-hailuo-video-web/public_assets/77afb86d-2cc4-4149-8c2e-e1174fa849d4.mp3', NULL, NULL, NULL, 21, NULL, NULL, NULL, NULL);
|
||||
INSERT INTO `ai_tts_voice` VALUES ('TTS_MinimaxStreamTTS_0022', 'TTS_MinimaxStreamTTS', 'Crisp Reporter', 'Cantonese_crisp_reporter_vv2', '粤语', 'https://cdn.hailuoai.video/open-hailuo-video-web/public_assets/4ad39a61-628e-481c-9d60-bfb7c36bceb8.mp3', NULL, NULL, NULL, 22, NULL, NULL, NULL, NULL);
|
||||
INSERT INTO `ai_tts_voice` VALUES ('TTS_MinimaxStreamTTS_0023', 'TTS_MinimaxStreamTTS', 'Articulate', 'Cantonese_Articulate_commentator_vv2', '粤语', 'https://cdn.hailuoai.video/open-hailuo-video-web/public_assets/9346601d-3035-4f9e-9b1c-0e03f30e5eef.mp3', NULL, NULL, NULL, 23, NULL, NULL, NULL, NULL);
|
||||
INSERT INTO `ai_tts_voice` VALUES ('TTS_MinimaxStreamTTS_0024', 'TTS_MinimaxStreamTTS', 'Warm Reporter', 'Cantonese_Warm_Reporter_vv2', '粤语', 'https://cdn.hailuoai.video/open-hailuo-video-web/public_assets/c651050e-d6a5-4653-b09e-5177bdc8b04c.mp3', NULL, NULL, NULL, 24, NULL, NULL, NULL, NULL);
|
||||
INSERT INTO `ai_tts_voice` VALUES ('TTS_MinimaxStreamTTS_0025', 'TTS_MinimaxStreamTTS', 'Energetic', 'Cantonese_energetic_commentator_vv2', '粤语', 'https://cdn.hailuoai.video/open-hailuo-video-web/public_assets/1fd1c461-71da-450d-91bb-45f819249b44.mp3', NULL, NULL, NULL, 25, NULL, NULL, NULL, NULL);
|
||||
INSERT INTO `ai_tts_voice` VALUES ('TTS_MinimaxStreamTTS_0026', 'TTS_MinimaxStreamTTS', 'Objective Narrator', 'Cantonese_objective_narrator_vv2', '粤语', 'https://cdn.hailuoai.video/open-hailuo-video-web/public_assets/463c5a68-a7b5-4fe9-85b6-d494dca74abb.mp3', NULL, NULL, NULL, 26, NULL, NULL, NULL, NULL);
|
||||
INSERT INTO `ai_tts_voice` VALUES ('TTS_MinimaxStreamTTS_0027', 'TTS_MinimaxStreamTTS', '沙哑男声', 'Cantonese_Narrator', '粤语', 'https://filecdn.minimax.chat/public/22e29cb0-0f5a-443a-9fa9-a32dabd55cf9.mp3', NULL, NULL, NULL, 27, NULL, NULL, NULL, NULL);
|
||||
INSERT INTO `ai_tts_voice` VALUES ('TTS_MinimaxStreamTTS_0028', 'TTS_MinimaxStreamTTS', 'News Anchor', 'Cantonese_news_anchor_vv2', '粤语', 'https://cdn.hailuoai.video/open-hailuo-video-web/public_assets/59b9ba49-47d4-48f1-b207-09c0e0769a1b.mp3', NULL, NULL, NULL, 28, NULL, NULL, NULL, NULL);
|
||||
INSERT INTO `ai_tts_voice` VALUES ('TTS_MinimaxStreamTTS_0029', 'TTS_MinimaxStreamTTS', '渊博学者', 'Cantonese_WiselProfessor', '粤语', 'https://filecdn.minimax.chat/public/8de234c4-64a7-4539-a316-c3b43ee1d0a9.mp3', NULL, NULL, NULL, 29, NULL, NULL, NULL, NULL);
|
||||
INSERT INTO `ai_tts_voice` VALUES ('TTS_MinimaxStreamTTS_0030', 'TTS_MinimaxStreamTTS', 'Resonant Host', 'Cantonese_resonant_host_vv2', '粤语', 'https://cdn.hailuoai.video/open-hailuo-video-web/public_assets/ec8fc0ce-6657-4c49-9bf0-3953a15046ec.mp3', NULL, NULL, NULL, 30, NULL, NULL, NULL, NULL);
|
||||
INSERT INTO `ai_tts_voice` VALUES ('TTS_MinimaxStreamTTS_0031', 'TTS_MinimaxStreamTTS', '冷漠员工', 'Cantonese_IndifferentStaff', '粤语', 'https://filecdn.minimax.chat/public/9686682c-3fc8-45f0-bd88-2a4692e1f63e.mp3', NULL, NULL, NULL, 31, NULL, NULL, NULL, NULL);
|
||||
INSERT INTO `ai_tts_voice` VALUES ('TTS_MinimaxStreamTTS_0032', 'TTS_MinimaxStreamTTS', 'Male News Anchor', 'Cantonese_Male_news_anchor_vv2', '粤语', 'https://cdn.hailuoai.video/open-hailuo-video-web/public_assets/2055ee8b-fb25-424c-b8cb-e2baf971e520.mp3', NULL, NULL, NULL, 32, NULL, NULL, NULL, NULL);
|
||||
INSERT INTO `ai_tts_voice` VALUES ('TTS_MinimaxStreamTTS_0033', 'TTS_MinimaxStreamTTS', 'History Narrator', 'Cantonese_casual_narrator_vv2', '粤语', 'https://cdn.hailuoai.video/open-hailuo-video-web/public_assets/c170f9e4-145d-4331-9910-1c3a0e6bbc3a.mp3', NULL, NULL, NULL, 33, NULL, NULL, NULL, NULL);
|
||||
INSERT INTO `ai_tts_voice` VALUES ('TTS_MinimaxStreamTTS_0034', 'TTS_MinimaxStreamTTS', 'Steady Reporter', 'Cantonese_Steady_reporter_vv2', '粤语', 'https://cdn.hailuoai.video/open-hailuo-video-web/public_assets/381cfee2-772d-4a73-b30c-7914aec05771.mp3', NULL, NULL, NULL, 34, NULL, NULL, NULL, NULL);
|
||||
INSERT INTO `ai_tts_voice` VALUES ('TTS_MinimaxStreamTTS_0035', 'TTS_MinimaxStreamTTS', 'Serious Commentator', 'Cantonese_serious_commentator_vv2', '粤语', 'https://cdn.hailuoai.video/open-hailuo-video-web/public_assets/57335c8a-32ea-475f-a198-b0de7fa1b126.mp3', NULL, NULL, NULL, 35, NULL, NULL, NULL, NULL);
|
||||
INSERT INTO `ai_tts_voice` VALUES ('TTS_MinimaxStreamTTS_0036', 'TTS_MinimaxStreamTTS', 'Podcast Host', 'Cantonese_podcast_host_vv2', '粤语', 'https://cdn.hailuoai.video/open-hailuo-video-web/public_assets/048b617f-e1f8-422c-89fb-fdbc6e303675.mp3', NULL, NULL, NULL, 36, NULL, NULL, NULL, NULL);
|
||||
INSERT INTO `ai_tts_voice` VALUES ('TTS_MinimaxStreamTTS_0037', 'TTS_MinimaxStreamTTS', 'Obj commentator', 'Cantonese_Objective_commentator_vv2', '粤语', 'https://cdn.hailuoai.video/open-hailuo-video-web/public_assets/a8f55e44-b726-4ba5-815a-f1b515ba57ef.mp3', NULL, NULL, NULL, 37, NULL, NULL, NULL, NULL);
|
||||
@@ -676,3 +676,17 @@ databaseChangeLog:
|
||||
- sqlFile:
|
||||
encoding: utf8
|
||||
path: classpath:db/changelog/202605251426.sql
|
||||
- changeSet:
|
||||
id: 202606231030
|
||||
author: RanChen
|
||||
changes:
|
||||
- sqlFile:
|
||||
encoding: utf8
|
||||
path: classpath:db/changelog/202606231030.sql
|
||||
- changeSet:
|
||||
id: 202606261131
|
||||
author: RanChen
|
||||
changes:
|
||||
- sqlFile:
|
||||
encoding: utf8
|
||||
path: classpath:db/changelog/202606261131.sql
|
||||
|
||||
@@ -25,4 +25,15 @@
|
||||
INSERT INTO ai_device_address_book (mac_address, target_mac, alias, has_permission, creator, create_date, updater, update_date)
|
||||
VALUES (#{macAddress}, #{targetMac}, #{alias}, #{hasPermission}, #{creator}, NOW(), #{updater}, NOW())
|
||||
</insert>
|
||||
|
||||
<delete id="deleteByMacAddresses">
|
||||
DELETE FROM ai_device_address_book WHERE mac_address IN
|
||||
<foreach collection="macAddresses" item="mac" open="(" separator="," close=")">
|
||||
#{mac}
|
||||
</foreach>
|
||||
OR target_mac IN
|
||||
<foreach collection="macAddresses" item="mac" open="(" separator="," close=")">
|
||||
#{mac}
|
||||
</foreach>
|
||||
</delete>
|
||||
</mapper>
|
||||
@@ -265,7 +265,7 @@ function showAbout() {
|
||||
title: t('settings.aboutApp', { appName: import.meta.env.VITE_APP_TITLE }),
|
||||
content: t('settings.aboutContent', {
|
||||
appName: import.meta.env.VITE_APP_TITLE,
|
||||
version: '0.9.4'
|
||||
version: '0.9.5'
|
||||
}),
|
||||
showCancel: false,
|
||||
confirmText: t('common.confirm'),
|
||||
|
||||
@@ -80,7 +80,7 @@ export default {
|
||||
}).send()
|
||||
},
|
||||
// 保存
|
||||
addParam(data, callback) {
|
||||
addParam(data, callback, failCallback) {
|
||||
RequestService.sendRequest()
|
||||
.url(`${getServiceUrl()}/admin/params`)
|
||||
.method('POST')
|
||||
@@ -89,6 +89,10 @@ export default {
|
||||
RequestService.clearRequestTime()
|
||||
callback(res)
|
||||
})
|
||||
.fail((err) => {
|
||||
RequestService.clearRequestTime()
|
||||
failCallback(err)
|
||||
})
|
||||
.networkFail((err) => {
|
||||
console.error('添加参数失败:', err)
|
||||
RequestService.reAjaxFun(() => {
|
||||
|
||||
|
After Width: | Height: | Size: 659 B |
|
After Width: | Height: | Size: 721 B |
|
After Width: | Height: | Size: 1.1 KiB |
|
After Width: | Height: | Size: 1.3 KiB |
|
After Width: | Height: | Size: 913 B |
|
After Width: | Height: | Size: 769 B |
|
After Width: | Height: | Size: 889 B |
|
After Width: | Height: | Size: 995 B |
|
After Width: | Height: | Size: 25 KiB |
|
After Width: | Height: | Size: 669 B |
|
After Width: | Height: | Size: 26 KiB |
|
After Width: | Height: | Size: 28 KiB |
|
After Width: | Height: | Size: 29 KiB |
|
After Width: | Height: | Size: 682 B |
|
After Width: | Height: | Size: 52 KiB |
|
After Width: | Height: | Size: 29 KiB |
|
After Width: | Height: | Size: 30 KiB |
@@ -1,39 +1,38 @@
|
||||
<template>
|
||||
<el-dialog :visible="dialogVisible" @update:visible="handleVisibleChange" width="57%" center
|
||||
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="font-size: 30px; color: #3d4566; margin-top: -10px; margin-bottom: 10px; text-align: center;">
|
||||
{{ $t('modelConfigDialog.addModel') }}
|
||||
</div>
|
||||
|
||||
<button class="custom-close-btn" @click="handleClose">
|
||||
×
|
||||
</button>
|
||||
|
||||
<CustomDialog
|
||||
:visible.sync="dialogVisible"
|
||||
:title="$t('modelConfigDialog.addModel')"
|
||||
width="57%"
|
||||
class="add-model-dialog"
|
||||
:confirmLoading="saving"
|
||||
@close="handleClose"
|
||||
@confirm="confirm"
|
||||
>
|
||||
<div class="dialog-content">
|
||||
<!-- 模型信息部分 -->
|
||||
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 10px;">
|
||||
<div style="font-size: 20px; font-weight: bold; color: #3d4566;">{{ $t('modelConfigDialog.modelInfo') }}</div>
|
||||
<div style="display: flex; align-items: center; gap: 20px;">
|
||||
<div style="display: flex; align-items: center;">
|
||||
<span style="margin-right: 8px;">{{ $t('modelConfigDialog.enable') }}</span>
|
||||
<div class="section-header">
|
||||
<div class="section-title">{{ $t('modelConfigDialog.modelInfo') }}</div>
|
||||
<div class="switch-group">
|
||||
<div class="switch-item">
|
||||
<span>{{ $t('modelConfigDialog.enable') }}</span>
|
||||
<el-switch v-model="formData.isEnabled" class="custom-switch"></el-switch>
|
||||
</div>
|
||||
<div style="display: none; align-items: center;">
|
||||
<span style="margin-right: 8px;">{{ $t('modelConfigDialog.setDefault') }}</span>
|
||||
<div class="switch-item" style="display: none;">
|
||||
<span>{{ $t('modelConfigDialog.setDefault') }}</span>
|
||||
<el-switch v-model="formData.isDefault" class="custom-switch"></el-switch>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style="height: 2px; background: #e9e9e9; margin-bottom: 22px;"></div>
|
||||
<el-form :model="formData" label-width="100px" label-position="left" class="custom-form">
|
||||
<div style="display: flex; gap: 20px; margin-bottom: 0;">
|
||||
<div class="divider"></div>
|
||||
<el-form :model="formData" label-width="auto" label-position="left" class="custom-form">
|
||||
<div class="form-row">
|
||||
<el-form-item :label="$t('modelConfigDialog.modelId')" prop="id" style="flex: 1;">
|
||||
<el-input v-model="formData.id" :placeholder="$t('modelConfigDialog.enterModelId')" class="custom-input-bg"
|
||||
maxlength="32"></el-input>
|
||||
</el-form-item>
|
||||
</div>
|
||||
<div style="display: flex; gap: 20px; margin-bottom: 0;">
|
||||
<div class="form-row">
|
||||
<el-form-item :label="$t('modelConfigDialog.modelName')" prop="modelName" style="flex: 1;">
|
||||
<el-input v-model="formData.modelName" :placeholder="$t('modelConfigDialog.enterModelName')"
|
||||
class="custom-input-bg"></el-input>
|
||||
@@ -44,7 +43,7 @@
|
||||
</el-form-item>
|
||||
</div>
|
||||
|
||||
<div style="display: flex; gap: 20px; margin-bottom: 0;">
|
||||
<div class="form-row">
|
||||
<el-form-item :label="$t('modelConfigDialog.supplier')" prop="supplier" style="flex: 1;">
|
||||
<el-select v-model="formData.supplier" :placeholder="$t('modelConfigDialog.selectSupplier')"
|
||||
class="custom-select custom-input-bg" style="width: 100%;" @focus="loadProviders" filterable>
|
||||
@@ -57,7 +56,6 @@
|
||||
</el-form-item>
|
||||
</div>
|
||||
|
||||
|
||||
<el-form-item :label="$t('modelConfigDialog.docLink')" prop="docLink" style="margin-bottom: 27px;">
|
||||
<el-input v-model="formData.docLink" :placeholder="$t('modelConfigDialog.enterDocLink')"
|
||||
class="custom-input-bg"></el-input>
|
||||
@@ -70,13 +68,13 @@
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
|
||||
<div style="font-size: 20px; font-weight: bold; color: #3d4566; margin-bottom: 15px;">{{
|
||||
$t('modelConfigDialog.callInfo') }}</div>
|
||||
<div style="height: 2px; background: #e9e9e9; margin-bottom: 22px;"></div>
|
||||
<teleport v-if="chunkedCallInfoFields.length">
|
||||
<div class="section-title">{{ $t('modelConfigDialog.callInfo') }}</div>
|
||||
<div class="divider"></div>
|
||||
</teleport>
|
||||
|
||||
<el-form :model="formData.configJson" label-width="auto" label-position="left" class="custom-form">
|
||||
<div v-for="(row, rowIndex) in chunkedCallInfoFields" :key="rowIndex"
|
||||
style="display: flex; gap: 20px; margin-bottom: 0;">
|
||||
<div v-for="(row, rowIndex) in chunkedCallInfoFields" :key="rowIndex" class="form-row">
|
||||
<el-form-item v-for="field in row" :key="field.prop" :label="field.label" :prop="field.prop" style="flex: 1;">
|
||||
<el-input v-model="formData.configJson[field.prop]" :placeholder="field.placeholder"
|
||||
:type="field.type || 'text'" class="custom-input-bg" :show-password="field.type === 'password'">
|
||||
@@ -85,19 +83,17 @@
|
||||
</div>
|
||||
</el-form>
|
||||
</div>
|
||||
|
||||
<div style="display: flex;justify-content: center;">
|
||||
<el-button type="primary" @click="confirm" class="save-btn" :loading="saving" :disabled="saving">
|
||||
{{ $t('modelConfigDialog.save') }}
|
||||
</el-button>
|
||||
</div>
|
||||
</el-dialog>
|
||||
</CustomDialog>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import Api from '@/apis/api';
|
||||
import CustomDialog from './CustomDialog.vue';
|
||||
export default {
|
||||
name: 'AddModelDialog',
|
||||
components: {
|
||||
CustomDialog
|
||||
},
|
||||
props: {
|
||||
visible: { type: Boolean, required: true },
|
||||
modelType: { type: String, required: true }
|
||||
@@ -178,16 +174,10 @@ export default {
|
||||
});
|
||||
this.formData.configJson = { ...defaultConfig };
|
||||
},
|
||||
handleVisibleChange(val) {
|
||||
this.dialogVisible = val;
|
||||
this.$emit('update:visible', val);
|
||||
if (!val) {
|
||||
this.resetForm();
|
||||
}
|
||||
},
|
||||
|
||||
handleClose() {
|
||||
this.saving = false;
|
||||
this.dialogVisible = false;
|
||||
this.$emit('update:visible', false);
|
||||
},
|
||||
initDynamicConfig() {
|
||||
@@ -232,7 +222,7 @@ export default {
|
||||
|
||||
try {
|
||||
this.$emit('confirm', submitData);
|
||||
this.$emit('update:visible', false);
|
||||
this.handleClose();
|
||||
this.resetForm();
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
@@ -297,134 +287,71 @@ export default {
|
||||
}
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.custom-dialog {
|
||||
position: relative;
|
||||
border-radius: 20px;
|
||||
overflow: hidden;
|
||||
background: white;
|
||||
padding-bottom: 17px;
|
||||
}
|
||||
<style lang="scss" scoped>
|
||||
@import '@/styles/global.scss';
|
||||
|
||||
.custom-dialog .el-dialog__header {
|
||||
padding: 0;
|
||||
border-bottom: none;
|
||||
::v-deep .el-dialog {
|
||||
margin-top: 6vh !important;
|
||||
}
|
||||
|
||||
.center-dialog {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
::v-deep .el-dialog__body {
|
||||
max-height: 60vh;
|
||||
overflow-y: auto;
|
||||
@include scrollbar-style;
|
||||
}
|
||||
.add-model-dialog {
|
||||
.section-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.center-dialog .el-dialog {
|
||||
margin: 0 0 auto !important;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
.section-title {
|
||||
margin-bottom: 10px;
|
||||
font-size: 20px;
|
||||
font-weight: bold;
|
||||
color: #3d4566;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.custom-close-btn {
|
||||
position: absolute;
|
||||
top: 20px;
|
||||
right: 20px;
|
||||
width: 35px;
|
||||
height: 35px;
|
||||
border-radius: 50%;
|
||||
border: 2px solid #cfcfcf;
|
||||
background: none;
|
||||
font-size: 30px;
|
||||
font-weight: lighter;
|
||||
color: #cfcfcf;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
z-index: 1;
|
||||
padding: 0;
|
||||
outline: none;
|
||||
}
|
||||
.switch-group {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 20px;
|
||||
}
|
||||
|
||||
.custom-close-btn:hover {
|
||||
color: #409EFF;
|
||||
border-color: #409EFF;
|
||||
}
|
||||
.switch-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
.custom-select .el-input__suffix {
|
||||
background: #e6e8ea;
|
||||
right: 6px;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
top: 9px;
|
||||
}
|
||||
span {
|
||||
margin-right: 8px;
|
||||
}
|
||||
}
|
||||
|
||||
.custom-select .el-input__suffix-inner {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 100%;
|
||||
}
|
||||
.divider {
|
||||
height: 1px;
|
||||
background: #e9e9e9;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.custom-select .el-icon-arrow-up:before {
|
||||
content: "";
|
||||
display: inline-block;
|
||||
width: 0;
|
||||
height: 0;
|
||||
border-left: 5px solid transparent;
|
||||
border-right: 5px solid transparent;
|
||||
border-top: 7px solid #c0c4cc;
|
||||
position: relative;
|
||||
top: -2px;
|
||||
transform: rotate(180deg);
|
||||
.form-row {
|
||||
display: flex;
|
||||
gap: 20px;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.dialog-footer {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
gap: 20px;
|
||||
padding: 16px 20px;
|
||||
}
|
||||
}
|
||||
|
||||
.custom-form .el-form-item {
|
||||
margin-bottom: 20px;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.custom-form .el-form-item__label {
|
||||
color: #3d4566;
|
||||
font-weight: normal;
|
||||
text-align: right;
|
||||
padding-right: 20px;
|
||||
|
||||
}
|
||||
|
||||
.custom-form .el-form-item.prop-remark .el-form-item__label {
|
||||
margin-top: -4px;
|
||||
}
|
||||
|
||||
.custom-input-bg .el-input__inner::-webkit-input-placeholder,
|
||||
.custom-input-bg .el-textarea__inner::-webkit-input-placeholder {
|
||||
color: #9c9f9e;
|
||||
}
|
||||
|
||||
|
||||
.custom-input-bg .el-input__inner,
|
||||
.custom-input-bg .el-textarea__inner {
|
||||
background-color: #ffffff;
|
||||
}
|
||||
|
||||
|
||||
.save-btn {
|
||||
background: #e6f0fd;
|
||||
color: #237ff4;
|
||||
border: 1px solid #b3d1ff;
|
||||
width: 150px;
|
||||
height: 40px;
|
||||
font-size: 16px;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.save-btn:hover {
|
||||
background: linear-gradient(to right, #237ff4, #9c40d5);
|
||||
color: white;
|
||||
border: none;
|
||||
}
|
||||
|
||||
|
||||
.custom-switch .el-switch__core {
|
||||
border-radius: 20px;
|
||||
height: 23px;
|
||||
@@ -439,7 +366,7 @@ export default {
|
||||
background-color: white;
|
||||
top: 3px;
|
||||
left: 4px;
|
||||
transition: all .3s;
|
||||
transition: all 0.3s;
|
||||
}
|
||||
|
||||
.custom-switch.is-checked .el-switch__core {
|
||||
@@ -454,12 +381,10 @@ export default {
|
||||
background-color: #1b47ee;
|
||||
}
|
||||
|
||||
|
||||
[style*="display: flex"] {
|
||||
gap: 20px;
|
||||
}
|
||||
|
||||
.custom-input-bg .el-input__inner {
|
||||
height: 32px;
|
||||
}
|
||||
::v-deep .el-input__inner {
|
||||
height: 32px;
|
||||
}
|
||||
</style>
|
||||
@@ -1,55 +1,42 @@
|
||||
<template>
|
||||
<form>
|
||||
<el-dialog :visible.sync="dialogVisible" width="24%" center>
|
||||
<div
|
||||
style="margin: 0 10px 10px;display: flex;align-items: center;gap: 10px;font-weight: 700;font-size: 20px;text-align: left;color: #3d4566;">
|
||||
<div
|
||||
style="width: 40px;height: 40px;border-radius: 50%;background: #5778ff;display: flex;align-items: center;justify-content: center;">
|
||||
<img loading="lazy" src="@/assets/login/shield.png" alt=""
|
||||
style="width: 19px;height: 23px; filter: brightness(0) invert(1);" />
|
||||
</div>
|
||||
{{ $t('changePassword.title') }}
|
||||
</div>
|
||||
<div style="height: 1px;background: #e8f0ff;" />
|
||||
<div style="margin: 22px 15px;">
|
||||
<div style="font-weight: 400;font-size: 14px;text-align: left;color: #3d4566;">
|
||||
<div style="color: red;display: inline-block;">*</div>
|
||||
<CustomDialog
|
||||
:title="$t('changePassword.title')"
|
||||
:visible.sync="dialogVisible"
|
||||
width="600px"
|
||||
@cancel="cancel"
|
||||
@confirm="confirm">
|
||||
<div class="password-form">
|
||||
<div class="form-label">
|
||||
<span class="required">*</span>
|
||||
{{ $t('changePassword.oldPasswordLabel') }}
|
||||
</div>
|
||||
<div class="input-46" style="margin-top: 12px;">
|
||||
<el-input :placeholder="$t('changePassword.oldPasswordPlaceholder')" v-model="oldPassword" type="password" show-password />
|
||||
</div>
|
||||
<div style="font-weight: 400;font-size: 14px;text-align: left;color: #3d4566;margin-top: 12px;">
|
||||
<div style="color: red;display: inline-block;">*</div>
|
||||
<div class="form-label" style="margin-top: 12px;">
|
||||
<span class="required">*</span>
|
||||
{{ $t('changePassword.newPasswordLabel') }}
|
||||
</div>
|
||||
<div class="input-46" style="margin-top: 12px;">
|
||||
<el-input :placeholder="$t('changePassword.newPasswordPlaceholder')" v-model="newPassword" type="password" show-password />
|
||||
</div>
|
||||
<div style="font-weight: 400;font-size: 14px;text-align: left;color: #3d4566;margin-top: 12px;">
|
||||
<div style="color: red;display: inline-block;">*</div>
|
||||
<div class="form-label" style="margin-top: 12px;">
|
||||
<span class="required">*</span>
|
||||
{{ $t('changePassword.confirmPasswordLabel') }}
|
||||
</div>
|
||||
<div class="input-46" style="margin-top: 12px;">
|
||||
<el-input :placeholder="$t('changePassword.confirmPasswordPlaceholder')" v-model="confirmNewPassword" type="password" show-password />
|
||||
</div>
|
||||
</div>
|
||||
<div style="display: flex;margin: 15px 15px;gap: 7px;">
|
||||
<div class="dialog-btn" @click="confirm">
|
||||
{{ $t('changePassword.confirmButton') }}
|
||||
</div>
|
||||
<div class="dialog-btn" style="background: #e6ebff;border: 1px solid #adbdff;color: #5778ff;" @click="cancel">
|
||||
{{ $t('changePassword.cancelButton') }}
|
||||
</div>
|
||||
</div>
|
||||
</el-dialog>
|
||||
</CustomDialog>
|
||||
</form>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import userApi from '@/apis/module/user';
|
||||
import { mapActions } from 'vuex';
|
||||
import { changeLanguage } from '@/i18n';
|
||||
import CustomDialog from '@/components/CustomDialog.vue';
|
||||
|
||||
export default {
|
||||
name: 'ChangePasswordDialog',
|
||||
@@ -67,6 +54,9 @@ export default {
|
||||
confirmNewPassword: ""
|
||||
}
|
||||
},
|
||||
components: {
|
||||
CustomDialog
|
||||
},
|
||||
watch: {
|
||||
value(val) {
|
||||
this.dialogVisible = val;
|
||||
@@ -100,7 +90,6 @@ export default {
|
||||
});
|
||||
this.logout().then(() => {
|
||||
this.$router.push('/login');
|
||||
this.$emit('update:visible', false);
|
||||
});
|
||||
} else {
|
||||
this.$message.error(res.data.msg || this.$t('changePassword.changeFailed'));
|
||||
@@ -108,10 +97,9 @@ export default {
|
||||
}, (err) => {
|
||||
this.$message.error(err.msg || this.$t('changePassword.changeFailed'));
|
||||
});
|
||||
this.$emit('input', false);
|
||||
this.dialogVisible = false;
|
||||
},
|
||||
cancel() {
|
||||
this.dialogVisible = false;
|
||||
this.resetForm();
|
||||
},
|
||||
resetForm() {
|
||||
@@ -124,38 +112,18 @@ export default {
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
||||
.form-label {
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.required {
|
||||
color: red;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.input-46 {
|
||||
background: #f6f8fb;
|
||||
border-radius: 15px;
|
||||
}
|
||||
|
||||
.dialog-btn {
|
||||
cursor: pointer;
|
||||
flex: 1;
|
||||
border-radius: 23px;
|
||||
background: #5778ff;
|
||||
height: 40px;
|
||||
font-weight: 500;
|
||||
font-size: 12px;
|
||||
color: #fff;
|
||||
line-height: 40px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
::v-deep .el-dialog {
|
||||
border-radius: 15px;
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
::v-deep .el-dialog__headerbtn {
|
||||
display: none;
|
||||
}
|
||||
|
||||
::v-deep .el-dialog__body {
|
||||
padding: 4px 6px;
|
||||
}
|
||||
|
||||
::v-deep .el-dialog__header {
|
||||
padding: 10px;
|
||||
}
|
||||
</style>
|
||||
@@ -1,7 +1,19 @@
|
||||
<template>
|
||||
<el-dialog
|
||||
:title="$t('chatHistory.with') + agentName + $t('chatHistory.dialogTitle') + (currentMacAddress ? '[' + currentMacAddress + ']' : '')"
|
||||
:visible.sync="dialogVisible" width="80%" :before-close="handleClose" custom-class="chat-history-dialog">
|
||||
<CustomDialog
|
||||
:title="$t('chatHistory.with') + agentName + $t('chatHistory.dialogTitle')"
|
||||
:visible.sync="dialogVisible"
|
||||
width="80%"
|
||||
:footer="false"
|
||||
:close-on-click-modal="false"
|
||||
custom-class="chat-history-dialog">
|
||||
<template v-slot:title>
|
||||
<span style="font-size: 18px;">
|
||||
{{ $t('chatHistory.with') + agentName + $t('chatHistory.dialogTitle') }}
|
||||
<template v-if="currentMacAddress">
|
||||
[<MacAddressMask :macAddress="currentMacAddress" />]
|
||||
</template>
|
||||
</span>
|
||||
</template>
|
||||
<div class="chat-container">
|
||||
<div class="session-list" @scroll="handleScroll">
|
||||
<div v-for="session in sessions" :key="session.sessionId" class="session-item"
|
||||
@@ -69,12 +81,14 @@
|
||||
{{ $t('chatHistory.downloadCurrentSession') }}
|
||||
</el-button>
|
||||
</div>
|
||||
</el-dialog>
|
||||
</CustomDialog>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { debounce } from '@/utils'
|
||||
import Api from '@/apis/api';
|
||||
import CustomDialog from '@/components/CustomDialog.vue';
|
||||
import MacAddressMask from '@/components/MacAddressMask.vue';
|
||||
|
||||
export default {
|
||||
name: 'ChatHistoryDialog',
|
||||
@@ -110,6 +124,10 @@ export default {
|
||||
expandedToolResults: {} // 跟踪工具结果的展开状态
|
||||
};
|
||||
},
|
||||
components: {
|
||||
CustomDialog,
|
||||
MacAddressMask,
|
||||
},
|
||||
watch: {
|
||||
visible(val) {
|
||||
this.dialogVisible = val;
|
||||
@@ -229,9 +247,6 @@ export default {
|
||||
this.isFirstLoad = true;
|
||||
this.expandedToolResults = {};
|
||||
},
|
||||
handleClose() {
|
||||
this.dialogVisible = false;
|
||||
},
|
||||
loadSessions() {
|
||||
if (this.loading || (!this.isFirstLoad && !this.hasMore)) {
|
||||
return;
|
||||
@@ -623,11 +638,6 @@ export default {
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.chat-history-dialog .el-dialog__header {
|
||||
background-color: #e6f7ff;
|
||||
padding: 15px 20px;
|
||||
}
|
||||
|
||||
.chat-history-dialog .el-dialog__body {
|
||||
padding: 0;
|
||||
overflow: hidden;
|
||||
|
||||
@@ -0,0 +1,161 @@
|
||||
<template>
|
||||
<el-button
|
||||
:class="['custom-button', `custom-button--${type}`]"
|
||||
v-bind="$attrs"
|
||||
@click="$emit('click', $event)"
|
||||
>
|
||||
<slot />
|
||||
</el-button>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'CustomButton',
|
||||
props: {
|
||||
type: {
|
||||
type: String,
|
||||
default: 'default',
|
||||
validator: (val) => ['default', 'confirm', 'delete', 'add'].includes(val)
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.custom-button--default {
|
||||
width: fit-content;
|
||||
background: #4998ff;
|
||||
color: #ffffff;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
transition: all 0.3s ease;
|
||||
|
||||
&.el-button--small {
|
||||
padding: 9px 14px;
|
||||
font-size: 12px;
|
||||
}
|
||||
&.el-button--medium {
|
||||
padding: 12px 20px;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background: #62a4fc;
|
||||
color: #ffffff;
|
||||
border-color: transparent;
|
||||
box-shadow: 0 2px 8px rgba(74, 124, 253, 0.2);
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
|
||||
&.is-disabled,
|
||||
&.is-disabled:hover {
|
||||
background: linear-gradient(to right, #f0f3fe, #f5f6fe);
|
||||
color: rgba(74, 124, 253, 0.5);
|
||||
border-color: transparent;
|
||||
box-shadow: none;
|
||||
transform: none;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
}
|
||||
|
||||
.custom-button--confirm {
|
||||
width: fit-content;
|
||||
background: linear-gradient(to right, #4a7cfd, #8154fc);
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
transition: all 0.3s ease;
|
||||
|
||||
&.el-button--small {
|
||||
padding: 9px 14px;
|
||||
font-size: 12px;
|
||||
}
|
||||
&.el-button--medium {
|
||||
padding: 12px 20px;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
color: white;
|
||||
border-color: transparent;
|
||||
box-shadow: 0 2px 8px rgba(74, 124, 253, 0.3);
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
|
||||
&.is-disabled,
|
||||
&.is-disabled:hover {
|
||||
background: linear-gradient(to right, #a0b4fd, #b8a4fd);
|
||||
color: rgba(255, 255, 255, 0.6);
|
||||
border-color: transparent;
|
||||
box-shadow: none;
|
||||
transform: none;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
}
|
||||
|
||||
.custom-button--delete {
|
||||
width: fit-content;
|
||||
background: #ff6b6b;
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
transition: all 0.3s ease;
|
||||
|
||||
&.el-button--small {
|
||||
padding: 9px 14px;
|
||||
font-size: 12px;
|
||||
}
|
||||
&.el-button--medium {
|
||||
padding: 12px 20px;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
color: white;
|
||||
border-color: transparent;
|
||||
box-shadow: 0 2px 8px rgba(255, 107, 107, 0.3);
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
|
||||
&.is-disabled,
|
||||
&.is-disabled:hover {
|
||||
background: linear-gradient(to right, #ffaaaa, #ffcaca);
|
||||
color: rgba(255, 255, 255, 0.6);
|
||||
border-color: transparent;
|
||||
box-shadow: none;
|
||||
transform: none;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
}
|
||||
|
||||
.custom-button--add {
|
||||
width: fit-content;
|
||||
background: #52c41a;
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
transition: all 0.3s ease;
|
||||
|
||||
&.el-button--small {
|
||||
padding: 9px 14px;
|
||||
font-size: 12px;
|
||||
}
|
||||
&.el-button--medium {
|
||||
padding: 12px 20px;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
color: white;
|
||||
border-color: transparent;
|
||||
box-shadow: 0 2px 8px rgba(82, 196, 26, 0.3);
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
|
||||
&.is-disabled,
|
||||
&.is-disabled:hover {
|
||||
background: linear-gradient(to right, #a0e89a, #c4f0c0);
|
||||
color: rgba(255, 255, 255, 0.6);
|
||||
border-color: transparent;
|
||||
box-shadow: none;
|
||||
transform: none;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,233 @@
|
||||
<template>
|
||||
<el-dialog
|
||||
:title="title"
|
||||
:visible.sync="dialogVisible"
|
||||
:width="width"
|
||||
:close-on-click-modal="closeOnClickModal"
|
||||
:close-on-press-escape="closeOnPressEscape"
|
||||
:show-close="showClose"
|
||||
:destroy-on-close="destroyOnClose"
|
||||
:custom-class="customClass"
|
||||
class="custom-dialog"
|
||||
@close="handleClose"
|
||||
@open="handleOpen"
|
||||
>
|
||||
<template slot="title">
|
||||
<slot name="title">
|
||||
<div class="dialog-title">
|
||||
<img src="@/assets/knowledge-base/level.png" class="title-icon" />
|
||||
<span>{{ title }}</span>
|
||||
</div>
|
||||
</slot>
|
||||
</template>
|
||||
<slot></slot>
|
||||
<template slot="footer">
|
||||
<div v-if="footer" class="dialog-footer">
|
||||
<CustomButton @click="handleCancel">{{ cancelText }}</CustomButton>
|
||||
<CustomButton :loading="confirmLoading" type="confirm" @click="handleConfirm">
|
||||
<span class="confirm-inner">
|
||||
<img src="@/assets/knowledge-base/star.png" class="confirm-icon" />
|
||||
{{ confirmText }}
|
||||
</span>
|
||||
</CustomButton>
|
||||
</div>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import CustomButton from './CustomButton.vue';
|
||||
export default {
|
||||
name: "CustomDialog",
|
||||
props: {
|
||||
title: {
|
||||
type: String,
|
||||
default: ""
|
||||
},
|
||||
visible: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
confirmLoading: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
width: {
|
||||
type: String,
|
||||
default: "600px"
|
||||
},
|
||||
footer: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
},
|
||||
closeOnClickModal: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
closeOnPressEscape: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
},
|
||||
showClose: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
},
|
||||
destroyOnClose: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
},
|
||||
customClass: {
|
||||
type: String,
|
||||
default: ""
|
||||
},
|
||||
cancelText: {
|
||||
type: String,
|
||||
default: "取消"
|
||||
},
|
||||
confirmText: {
|
||||
type: String,
|
||||
default: "确认保存"
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
dialogVisible: this.visible
|
||||
};
|
||||
},
|
||||
components: {
|
||||
CustomButton
|
||||
},
|
||||
watch: {
|
||||
visible(val) {
|
||||
this.dialogVisible = val;
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
handleClose() {
|
||||
this.dialogVisible = false;
|
||||
this.$emit("update:visible", false);
|
||||
this.$emit("close");
|
||||
},
|
||||
handleOpen() {
|
||||
this.$emit("open");
|
||||
},
|
||||
handleCancel() {
|
||||
this.dialogVisible = false;
|
||||
this.$emit("update:visible", false);
|
||||
this.$emit("cancel");
|
||||
},
|
||||
handleConfirm() {
|
||||
this.$emit("confirm");
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.custom-dialog {
|
||||
::v-deep .el-dialog {
|
||||
border-radius: 10px;
|
||||
overflow: hidden;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
::v-deep .el-dialog__header {
|
||||
padding: 16px 20px 12px;
|
||||
background: linear-gradient(135deg, #e2eeff, #edeafe);
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
::v-deep .el-dialog__title {
|
||||
font-size: 16px;
|
||||
font-weight: 500;
|
||||
color: #1a1a1a;
|
||||
}
|
||||
|
||||
.dialog-title {
|
||||
font-size: 18px;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
> span {
|
||||
line-height: 18px;
|
||||
font-weight: 500;
|
||||
}
|
||||
}
|
||||
|
||||
.title-icon {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
margin-right: 8px;
|
||||
}
|
||||
|
||||
::v-deep .el-dialog__headerbtn {
|
||||
top: 12px;
|
||||
right: 16px;
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
border: none;
|
||||
border-radius: 50%;
|
||||
background: #fff;
|
||||
box-shadow: 0 2px 6px rgba(0, 0, 0, 0.12);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
.el-dialog__close {
|
||||
font-size: 18px;
|
||||
color: #666;
|
||||
position: static;
|
||||
transform: none;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background: #fff;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.18);
|
||||
|
||||
.el-dialog__close {
|
||||
color: #333;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
::v-deep .el-dialog__body {
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
::v-deep .el-dialog__footer {
|
||||
padding: 12px 20px 16px;
|
||||
}
|
||||
|
||||
.dialog-footer {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
|
||||
.el-button {
|
||||
padding: 10px 20px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.el-button--primary {
|
||||
background: linear-gradient(to right, #4a7cfd, #8154fc);
|
||||
border: none;
|
||||
|
||||
&:hover,
|
||||
&:focus {
|
||||
background: linear-gradient(to right, #4a7cfd, #8154fc);
|
||||
opacity: 0.85;
|
||||
}
|
||||
}
|
||||
|
||||
.confirm-inner {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.confirm-icon {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
margin-right: 4px;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,65 @@
|
||||
<template>
|
||||
<div class="pagination-wrapper">
|
||||
<el-pagination
|
||||
:current-page="currentPage"
|
||||
:page-size="pageSize"
|
||||
:page-sizes="pageSizeOptions"
|
||||
:total="total"
|
||||
layout="sizes, prev, pager, next, total"
|
||||
background
|
||||
@size-change="handleSizeChange"
|
||||
@current-change="handlePageChange"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'Pagination',
|
||||
props: {
|
||||
total: {
|
||||
type: Number,
|
||||
default: 0
|
||||
},
|
||||
currentPage: {
|
||||
type: Number,
|
||||
default: 1
|
||||
},
|
||||
pageSize: {
|
||||
type: Number,
|
||||
default: 10
|
||||
},
|
||||
pageSizeOptions: {
|
||||
type: Array,
|
||||
default: () => [10, 20, 50, 100]
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
handleSizeChange(val) {
|
||||
this.$emit('size-change', val);
|
||||
},
|
||||
handlePageChange(page) {
|
||||
this.$emit('page-change', page);
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
::v-deep .el-pagination {
|
||||
.btn-next,
|
||||
.btn-prev,
|
||||
.el-pager li {
|
||||
background: transparent !important;
|
||||
min-width: 36px !important;
|
||||
}
|
||||
.el-pager li:not(.disabled).active {
|
||||
background: linear-gradient(135deg, #2a6dfe 0%, #6b8cff 50%, #2a6dfe 100%) !important;
|
||||
border-radius: 6px;
|
||||
box-shadow: 0 1px 3px rgba(45, 111, 254, 0.3);
|
||||
color: #fff;
|
||||
font-weight: 600;
|
||||
text-shadow: 0 1px 1px rgba(0, 0, 0, 0.15);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,309 @@
|
||||
<template>
|
||||
<div class="custom-table-wrapper">
|
||||
<div class="table-container" :style="{ height: tableContainerHeight }">
|
||||
<el-table
|
||||
ref="tableRef"
|
||||
:data="data"
|
||||
:class="['custom-table', tableClass]"
|
||||
height="100%"
|
||||
v-loading="loading"
|
||||
:element-loading-text="loadingText"
|
||||
:element-loading-spinner="loadingSpinner"
|
||||
:element-loading-background="loadingBackground"
|
||||
:header-cell-class-name="headerCellClassName"
|
||||
:row-class-name="rowClassName"
|
||||
@selection-change="handleSelectionChange"
|
||||
@row-click="handleRowClick"
|
||||
>
|
||||
<!-- 选择列 -->
|
||||
<el-table-column
|
||||
v-if="showSelection"
|
||||
width="55"
|
||||
align="center"
|
||||
label="选择"
|
||||
>
|
||||
<template slot-scope="scope">
|
||||
<slot
|
||||
v-if="$scopedSlots.selection"
|
||||
name="selection"
|
||||
:row="scope.row"
|
||||
:$index="scope.$index"
|
||||
/>
|
||||
<el-checkbox
|
||||
v-else
|
||||
:value="scope.row.selected"
|
||||
@change="handleCheckboxChange(scope.row)"
|
||||
/>
|
||||
</template>
|
||||
</el-table-column>
|
||||
|
||||
<!-- 动态列 -->
|
||||
<el-table-column
|
||||
v-for="column in columns"
|
||||
:key="column.prop"
|
||||
:prop="column.prop"
|
||||
:label="column.label"
|
||||
:width="column.width"
|
||||
:min-width="column.minWidth"
|
||||
:align="column.align || 'center'"
|
||||
:show-overflow-tooltip="column.showOverflowTooltip !== false"
|
||||
>
|
||||
<template slot-scope="scope">
|
||||
<!-- 自定义插槽:优先使用 column.slot 指定的插槽名,否则用 column.prop 作为插槽名 -->
|
||||
<slot
|
||||
v-if="$scopedSlots[column.slot] || $scopedSlots[column.prop]"
|
||||
:name="column.slot || column.prop"
|
||||
:row="scope.row"
|
||||
:$index="scope.$index"
|
||||
:column="column"
|
||||
/>
|
||||
<!-- 默认显示 -->
|
||||
<template v-else>
|
||||
{{ scope.row[column.prop] }}
|
||||
</template>
|
||||
</template>
|
||||
</el-table-column>
|
||||
|
||||
<!-- 操作列 -->
|
||||
<el-table-column
|
||||
v-if="showOperations"
|
||||
:label="operationsLabel"
|
||||
align="center"
|
||||
:width="operationsWidth"
|
||||
>
|
||||
<template slot-scope="scope">
|
||||
<slot name="operations" :row="scope.row" :$index="scope.$index" />
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</div>
|
||||
|
||||
<!-- 分页 -->
|
||||
<div class="table-footer">
|
||||
<slot name="footer-btns"></slot>
|
||||
<CustomPagination
|
||||
v-if="showPagination"
|
||||
:total="total"
|
||||
:current-page="currentPage"
|
||||
:page-size="pageSize"
|
||||
:page-size-options="pageSizeOptions"
|
||||
@size-change="handleSizeChange"
|
||||
@page-change="handlePageChange"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import CustomPagination from './CustomPagination.vue';
|
||||
|
||||
export default {
|
||||
name: 'CustomTable',
|
||||
components: {
|
||||
CustomPagination
|
||||
},
|
||||
props: {
|
||||
// 表格数据
|
||||
data: {
|
||||
type: Array,
|
||||
default: () => []
|
||||
},
|
||||
// 列配置
|
||||
columns: {
|
||||
type: Array,
|
||||
default: () => []
|
||||
},
|
||||
// 是否显示选择框
|
||||
showSelection: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
// 是否显示操作列
|
||||
showOperations: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
operationsLabel: {
|
||||
type: String,
|
||||
default: '操作'
|
||||
},
|
||||
operationsWidth: {
|
||||
type: [String, Number],
|
||||
default: 180
|
||||
},
|
||||
// 分页相关
|
||||
showPagination: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
},
|
||||
total: {
|
||||
type: Number,
|
||||
default: 0
|
||||
},
|
||||
currentPage: {
|
||||
type: Number,
|
||||
default: 1
|
||||
},
|
||||
pageSize: {
|
||||
type: Number,
|
||||
default: 10
|
||||
},
|
||||
pageSizeOptions: {
|
||||
type: Array,
|
||||
default: () => [10, 20, 50, 100]
|
||||
},
|
||||
// 加载状态
|
||||
loading: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
loadingText: {
|
||||
type: String,
|
||||
default: 'Loading'
|
||||
},
|
||||
loadingSpinner: {
|
||||
type: String,
|
||||
default: 'el-icon-loading'
|
||||
},
|
||||
loadingBackground: {
|
||||
type: String,
|
||||
default: 'rgba(255, 255, 255, 0.7)'
|
||||
},
|
||||
// 自定义类名
|
||||
tableClass: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
headerCellClassName: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
rowClassName: {
|
||||
type: [String, Function],
|
||||
default: ''
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
tableContainerHeight() {
|
||||
return this.showPagination ? 'calc(100% - 48px)' : '100%';
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
// 复选框变化
|
||||
handleCheckboxChange(row) {
|
||||
this.$set(row, 'selected', !row.selected);
|
||||
},
|
||||
// 分页事件
|
||||
handleSizeChange(val) {
|
||||
this.$emit('size-change', val);
|
||||
},
|
||||
handlePageChange(page) {
|
||||
this.$emit('page-change', page);
|
||||
},
|
||||
// 选择事件
|
||||
handleSelectionChange(selection) {
|
||||
this.$emit('selection-change', selection);
|
||||
},
|
||||
// 行点击事件
|
||||
handleRowClick(row, column, event) {
|
||||
this.$emit('row-click', row, column, event);
|
||||
},
|
||||
// 清除选择
|
||||
clearSelection() {
|
||||
this.$refs.tableRef && this.$refs.tableRef.clearSelection();
|
||||
},
|
||||
// 切换选择
|
||||
toggleRowSelection(row, selected) {
|
||||
this.$refs.tableRef && this.$refs.tableRef.toggleRowSelection(row, selected);
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.custom-table-wrapper {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-height: 0;
|
||||
.table-container {
|
||||
width: 100%;
|
||||
box-shadow: 0 2px 12px rgba(74, 124, 253, 0.12);
|
||||
border-radius: 6px;
|
||||
.custom-table {
|
||||
width: 100%;
|
||||
border: 1px solid #eef3fd;
|
||||
border-bottom: none;
|
||||
border-radius: 6px;
|
||||
.el-table__body-wrapper {
|
||||
overflow-y: auto;
|
||||
&::-webkit-scrollbar {
|
||||
width: 6px;
|
||||
}
|
||||
&::-webkit-scrollbar-thumb {
|
||||
background: #a1c9fd;
|
||||
border-radius: 3px;
|
||||
}
|
||||
&::-webkit-scrollbar-track {
|
||||
background: #f0f3fe;
|
||||
border-radius: 3px;
|
||||
}
|
||||
}
|
||||
.el-table__header {
|
||||
th {
|
||||
color: #342f45;
|
||||
background: #edf2fc !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
:deep(.el-table) {
|
||||
.el-table__body-wrapper {
|
||||
overflow-y: auto;
|
||||
&::-webkit-scrollbar {
|
||||
width: 6px;
|
||||
}
|
||||
&::-webkit-scrollbar-thumb {
|
||||
background: #a1c9fd;
|
||||
border-radius: 3px;
|
||||
}
|
||||
&::-webkit-scrollbar-track {
|
||||
background: #f0f3fe;
|
||||
border-radius: 3px;
|
||||
}
|
||||
}
|
||||
.el-table__header {
|
||||
th {
|
||||
color: #342f45;
|
||||
background: #edf2fc !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
.table-footer {
|
||||
padding: 16px 0px 0px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
: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>
|
||||
@@ -1,116 +1,117 @@
|
||||
<template>
|
||||
<el-dialog :title="title" :visible.sync="dialogVisible" width="30%" @close="handleClose">
|
||||
<el-form :model="form" :rules="rules" ref="form" label-width="auto">
|
||||
<el-form-item :label="$t('dictDataDialog.dictLabel')" prop="dictLabel">
|
||||
<el-input v-model="form.dictLabel" :placeholder="$t('dictDataDialog.dictLabelPlaceholder')"></el-input>
|
||||
</el-form-item>
|
||||
<el-form-item :label="$t('dictDataDialog.dictValue')" prop="dictValue">
|
||||
<el-input v-model="form.dictValue" :placeholder="$t('dictDataDialog.dictValuePlaceholder')"></el-input>
|
||||
</el-form-item>
|
||||
<el-form-item :label="$t('dictDataDialog.sort')" prop="sort">
|
||||
<el-input-number v-model="form.sort" :min="0" :max="999" style="width: 100%;"></el-input-number>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<div slot="footer" class="dialog-footer">
|
||||
<el-button @click="handleClose">{{ $t('button.cancel') }}</el-button>
|
||||
<el-button type="primary" @click="handleSave">{{ $t('button.save') }}</el-button>
|
||||
</div>
|
||||
</el-dialog>
|
||||
<CustomDialog
|
||||
:title="title"
|
||||
:visible.sync="dialogVisible"
|
||||
width="600px"
|
||||
@confirm="submit"
|
||||
@close="cancel"
|
||||
:confirmLoading="saving"
|
||||
>
|
||||
<el-form :model="form" :rules="rules" ref="form" label-width="auto" label-position="left">
|
||||
<el-form-item :label="$t('dictDataDialog.dictLabel')" prop="dictLabel" class="form-item">
|
||||
<el-input v-model="form.dictLabel" :placeholder="$t('dictDataDialog.dictLabelPlaceholder')" />
|
||||
</el-form-item>
|
||||
<el-form-item :label="$t('dictDataDialog.dictValue')" prop="dictValue" class="form-item">
|
||||
<el-input v-model="form.dictValue" :placeholder="$t('dictDataDialog.dictValuePlaceholder')" />
|
||||
</el-form-item>
|
||||
<el-form-item :label="$t('dictDataDialog.sort')" prop="sort" class="form-item">
|
||||
<el-input-number v-model="form.sort" :min="0" :max="999" class="custom-input-number"></el-input-number>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</CustomDialog>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import CustomDialog from './CustomDialog.vue';
|
||||
export default {
|
||||
name: 'DictDataDialog',
|
||||
props: {
|
||||
visible: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
title: {
|
||||
type: String,
|
||||
default: () => this.$t('dictDataDialog.addDictData')
|
||||
},
|
||||
dictData: {
|
||||
type: Object,
|
||||
default: () => ({})
|
||||
},
|
||||
dictTypeId: {
|
||||
type: [Number, String],
|
||||
default: null
|
||||
}
|
||||
name: 'DictDataDialog',
|
||||
props: {
|
||||
visible: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
dialogVisible: this.visible,
|
||||
form: {
|
||||
id: null,
|
||||
dictTypeId: null,
|
||||
dictLabel: '',
|
||||
dictValue: '',
|
||||
sort: 0
|
||||
},
|
||||
rules: {
|
||||
dictLabel: [{ required: true, message: this.$t('dictDataDialog.requiredDictLabel'), trigger: 'blur' }],
|
||||
dictValue: [{ required: true, message: this.$t('dictDataDialog.requiredDictValue'), trigger: 'blur' }]
|
||||
}
|
||||
}
|
||||
title: {
|
||||
type: String,
|
||||
default: () => this.$t('dictDataDialog.addDictData')
|
||||
},
|
||||
watch: {
|
||||
dictData: {
|
||||
handler(val) {
|
||||
if (val) {
|
||||
this.form = { ...val }
|
||||
}
|
||||
},
|
||||
immediate: true
|
||||
},
|
||||
dictTypeId: {
|
||||
handler(val) {
|
||||
if (val) {
|
||||
this.form.dictTypeId = val
|
||||
}
|
||||
},
|
||||
immediate: true
|
||||
},
|
||||
visible(val) {
|
||||
this.dialogVisible = val;
|
||||
},
|
||||
dialogVisible(val) {
|
||||
this.$emit('update:visible', val);
|
||||
}
|
||||
dictData: {
|
||||
type: Object,
|
||||
default: () => ({})
|
||||
},
|
||||
methods: {
|
||||
handleClose() {
|
||||
this.dialogVisible = false;
|
||||
this.resetForm();
|
||||
},
|
||||
resetForm() {
|
||||
this.form = {
|
||||
id: null,
|
||||
dictTypeId: this.dictTypeId,
|
||||
dictLabel: '',
|
||||
dictValue: '',
|
||||
sort: 0
|
||||
}
|
||||
this.$refs.form?.resetFields()
|
||||
},
|
||||
handleSave() {
|
||||
this.$refs.form.validate(valid => {
|
||||
if (valid) {
|
||||
this.$emit('save', this.form)
|
||||
}
|
||||
})
|
||||
}
|
||||
dictTypeId: {
|
||||
type: [Number, String],
|
||||
default: null
|
||||
}
|
||||
}
|
||||
},
|
||||
components: {
|
||||
CustomDialog
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
dialogVisible: this.visible,
|
||||
saving: false,
|
||||
form: {
|
||||
id: null,
|
||||
dictTypeId: null,
|
||||
dictLabel: '',
|
||||
dictValue: '',
|
||||
sort: 0
|
||||
},
|
||||
rules: {
|
||||
dictLabel: [{ required: true, message: this.$t('dictDataDialog.requiredDictLabel'), trigger: 'blur' }],
|
||||
dictValue: [{ required: true, message: this.$t('dictDataDialog.requiredDictValue'), trigger: 'blur' }]
|
||||
}
|
||||
};
|
||||
},
|
||||
watch: {
|
||||
visible(val) {
|
||||
this.dialogVisible = val;
|
||||
},
|
||||
dialogVisible(val) {
|
||||
this.$emit('update:visible', val);
|
||||
if (!val) {
|
||||
this.saving = false;
|
||||
}
|
||||
},
|
||||
dictData: {
|
||||
handler(val) {
|
||||
if (val) {
|
||||
this.form = { ...val };
|
||||
}
|
||||
},
|
||||
immediate: true
|
||||
},
|
||||
dictTypeId: {
|
||||
handler(val) {
|
||||
if (val) {
|
||||
this.form.dictTypeId = val;
|
||||
}
|
||||
},
|
||||
immediate: true
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
submit() {
|
||||
this.$refs.form.validate((valid) => {
|
||||
if (valid) {
|
||||
this.saving = true;
|
||||
this.$emit('save', this.form);
|
||||
}
|
||||
});
|
||||
},
|
||||
cancel() {
|
||||
this.saving = false;
|
||||
this.$emit('cancel');
|
||||
},
|
||||
resetSaving() {
|
||||
this.saving = false;
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.dialog-footer {
|
||||
text-align: right;
|
||||
<style scoped lang="scss">
|
||||
.custom-input-number {
|
||||
width: 100%;
|
||||
}
|
||||
:deep(.el-dialog) {
|
||||
border-radius: 15px;
|
||||
}
|
||||
|
||||
</style>
|
||||
</style>
|
||||
|
||||
@@ -1,97 +1,94 @@
|
||||
<template>
|
||||
<el-dialog :title="title" :visible.sync="dialogVisible" width="30%" @close="handleClose">
|
||||
<el-form :model="form" :rules="rules" ref="form" label-width="auto">
|
||||
<el-form-item :label="$t('dictTypeDialog.dictName')" prop="dictName">
|
||||
<el-input v-model="form.dictName" :placeholder="$t('dictTypeDialog.dictNamePlaceholder')"></el-input>
|
||||
</el-form-item>
|
||||
<el-form-item :label="$t('dictTypeDialog.dictType')" prop="dictType">
|
||||
<el-input v-model="form.dictType" :placeholder="$t('dictTypeDialog.dictTypePlaceholder')"></el-input>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<div slot="footer" class="dialog-footer">
|
||||
<el-button @click="handleClose">{{ $t('button.cancel') }}</el-button>
|
||||
<el-button type="primary" @click="handleSave">{{ $t('button.save') }}</el-button>
|
||||
</div>
|
||||
</el-dialog>
|
||||
<CustomDialog
|
||||
:title="title"
|
||||
:visible.sync="dialogVisible"
|
||||
width="600px"
|
||||
@confirm="submit"
|
||||
@close="cancel"
|
||||
:confirmLoading="saving"
|
||||
>
|
||||
<el-form :model="form" :rules="rules" ref="form" label-width="auto" label-position="left">
|
||||
<el-form-item :label="$t('dictTypeDialog.dictName')" prop="dictName" class="form-item">
|
||||
<el-input v-model="form.dictName" :placeholder="$t('dictTypeDialog.dictNamePlaceholder')" />
|
||||
</el-form-item>
|
||||
<el-form-item :label="$t('dictTypeDialog.dictType')" prop="dictType" class="form-item">
|
||||
<el-input v-model="form.dictType" :placeholder="$t('dictTypeDialog.dictTypePlaceholder')" />
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</CustomDialog>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import CustomDialog from './CustomDialog.vue';
|
||||
export default {
|
||||
name: 'DictTypeDialog',
|
||||
props: {
|
||||
visible: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
title: {
|
||||
type: String,
|
||||
default: () => this.$t('dictTypeDialog.addDictType')
|
||||
},
|
||||
dictTypeData: {
|
||||
type: Object,
|
||||
default: () => ({})
|
||||
}
|
||||
name: 'DictTypeDialog',
|
||||
props: {
|
||||
visible: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
dialogVisible: this.visible,
|
||||
form: {
|
||||
id: null,
|
||||
dictName: '',
|
||||
dictType: ''
|
||||
},
|
||||
rules: {
|
||||
dictName: [{ required: true, message: this.$t('dictTypeDialog.requiredDictName'), trigger: 'blur' }],
|
||||
dictType: [{ required: true, message: this.$t('dictTypeDialog.requiredDictType'), trigger: 'blur' }]
|
||||
}
|
||||
}
|
||||
title: {
|
||||
type: String,
|
||||
default: () => this.$t('dictTypeDialog.addDictType')
|
||||
},
|
||||
watch: {
|
||||
visible(val) {
|
||||
this.dialogVisible = val;
|
||||
},
|
||||
dialogVisible(val) {
|
||||
this.$emit('update:visible', val);
|
||||
},
|
||||
dictTypeData: {
|
||||
handler(val) {
|
||||
if (val) {
|
||||
this.form = { ...val }
|
||||
}
|
||||
},
|
||||
immediate: true
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
handleClose() {
|
||||
this.dialogVisible = false;
|
||||
this.resetForm()
|
||||
},
|
||||
resetForm() {
|
||||
this.form = {
|
||||
id: null,
|
||||
dictName: '',
|
||||
dictType: ''
|
||||
}
|
||||
this.$refs.form?.resetFields()
|
||||
},
|
||||
handleSave() {
|
||||
this.$refs.form.validate(valid => {
|
||||
if (valid) {
|
||||
this.$emit('save', this.form)
|
||||
}
|
||||
})
|
||||
}
|
||||
dictTypeData: {
|
||||
type: Object,
|
||||
default: () => ({})
|
||||
}
|
||||
}
|
||||
},
|
||||
components: {
|
||||
CustomDialog
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
dialogVisible: this.visible,
|
||||
saving: false,
|
||||
form: {
|
||||
id: null,
|
||||
dictName: '',
|
||||
dictType: ''
|
||||
},
|
||||
rules: {
|
||||
dictName: [{ required: true, message: this.$t('dictTypeDialog.requiredDictName'), trigger: 'blur' }],
|
||||
dictType: [{ required: true, message: this.$t('dictTypeDialog.requiredDictType'), trigger: 'blur' }]
|
||||
}
|
||||
};
|
||||
},
|
||||
watch: {
|
||||
visible(val) {
|
||||
this.dialogVisible = val;
|
||||
},
|
||||
dialogVisible(val) {
|
||||
this.$emit('update:visible', val);
|
||||
if (!val) {
|
||||
this.saving = false;
|
||||
}
|
||||
},
|
||||
dictTypeData: {
|
||||
handler(val) {
|
||||
if (val) {
|
||||
this.form = { ...val };
|
||||
}
|
||||
},
|
||||
immediate: true
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
submit() {
|
||||
this.$refs.form.validate((valid) => {
|
||||
if (valid) {
|
||||
this.saving = true;
|
||||
this.$emit('save', this.form);
|
||||
}
|
||||
});
|
||||
},
|
||||
cancel() {
|
||||
this.saving = false;
|
||||
this.$emit('cancel');
|
||||
},
|
||||
resetSaving() {
|
||||
this.saving = false;
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.dialog-footer {
|
||||
text-align: right;
|
||||
}
|
||||
:deep(.el-dialog) {
|
||||
border-radius: 15px;
|
||||
}
|
||||
|
||||
</style>
|
||||
@@ -1,18 +1,25 @@
|
||||
<template>
|
||||
<el-dialog :title="title" :visible.sync="dialogVisible" :close-on-click-modal="false" @close="handleClose"
|
||||
@open="handleOpen">
|
||||
<el-form ref="form" :model="form" :rules="rules" label-width="auto">
|
||||
<CustomDialog
|
||||
:title="title"
|
||||
:visible.sync="visible"
|
||||
width="800px"
|
||||
@confirm="submit"
|
||||
@close="cancel"
|
||||
@open="handleOpen"
|
||||
:confirmLoading="saving"
|
||||
>
|
||||
<el-form ref="form" :model="form" :rules="rules" label-width="auto" label-position="left" class="firmware-form">
|
||||
<el-form-item :label="$t('firmwareDialog.firmwareName')" prop="firmwareName">
|
||||
<el-input v-model="form.firmwareName" :placeholder="$t('firmwareDialog.firmwareNamePlaceholder')"></el-input>
|
||||
<el-input v-model="form.firmwareName" :placeholder="$t('firmwareDialog.firmwareNamePlaceholder')" />
|
||||
</el-form-item>
|
||||
<el-form-item :label="$t('firmwareDialog.firmwareType')" prop="type">
|
||||
<el-select v-model="form.type" :placeholder="$t('firmwareDialog.firmwareTypePlaceholder')" style="width: 100%;"
|
||||
filterable :disabled="isTypeDisabled">
|
||||
<el-select v-model="form.type" :placeholder="$t('firmwareDialog.firmwareTypePlaceholder')"
|
||||
class="custom-select" filterable :disabled="isTypeDisabled">
|
||||
<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="$t('firmwareDialog.version')" prop="version">
|
||||
<el-input v-model="form.version" :placeholder="$t('firmwareDialog.versionPlaceholder')"></el-input>
|
||||
<el-input v-model="form.version" :placeholder="$t('firmwareDialog.versionPlaceholder')" />
|
||||
</el-form-item>
|
||||
<el-form-item :label="$t('firmwareDialog.firmwareFile')" prop="firmwarePath">
|
||||
<el-upload ref="upload" class="upload-demo" action="#" :http-request="handleUpload"
|
||||
@@ -29,21 +36,21 @@
|
||||
</el-form-item>
|
||||
<el-form-item :label="$t('firmwareDialog.remark')" prop="remark">
|
||||
<el-input type="textarea" v-model="form.remark"
|
||||
:placeholder="$t('firmwareDialog.remarkPlaceholder')"></el-input>
|
||||
:placeholder="$t('firmwareDialog.remarkPlaceholder')" />
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<div slot="footer" class="dialog-footer">
|
||||
<el-button @click="handleCancel">{{ $t('button.cancel') }}</el-button>
|
||||
<el-button type="primary" @click="handleSubmit">{{ $t('button.save') }}</el-button>
|
||||
</div>
|
||||
</el-dialog>
|
||||
</CustomDialog>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import Api from '@/apis/api';
|
||||
import CustomDialog from './CustomDialog.vue';
|
||||
|
||||
export default {
|
||||
name: 'FirmwareDialog',
|
||||
components: {
|
||||
CustomDialog
|
||||
},
|
||||
props: {
|
||||
visible: {
|
||||
type: Boolean,
|
||||
@@ -68,7 +75,7 @@ export default {
|
||||
uploadProgress: 0,
|
||||
uploadStatus: '',
|
||||
isUploading: false,
|
||||
dialogVisible: this.visible,
|
||||
saving: false,
|
||||
rules: {
|
||||
firmwareName: [
|
||||
{ required: true, message: this.$t('firmwareDialog.requiredFirmwareName'), trigger: 'blur' }
|
||||
@@ -92,28 +99,8 @@ export default {
|
||||
return !!this.form.id
|
||||
}
|
||||
},
|
||||
created() {
|
||||
// 移除 getDictDataByType 调用
|
||||
},
|
||||
watch: {
|
||||
visible(val) {
|
||||
this.dialogVisible = val;
|
||||
},
|
||||
dialogVisible(val) {
|
||||
this.$emit('update:visible', val);
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
// 移除 getFirmwareTypes 方法
|
||||
handleClose() {
|
||||
this.dialogVisible = false;
|
||||
this.$emit('cancel');
|
||||
},
|
||||
handleCancel() {
|
||||
this.$refs.form.clearValidate();
|
||||
this.$emit('cancel');
|
||||
},
|
||||
handleSubmit() {
|
||||
submit() {
|
||||
this.$refs.form.validate(valid => {
|
||||
if (valid) {
|
||||
// 如果是新增模式且没有上传文件,则提示错误
|
||||
@@ -121,11 +108,20 @@ export default {
|
||||
this.$message.error(this.$t('firmwareDialog.requiredFirmwareFile'))
|
||||
return
|
||||
}
|
||||
this.saving = true
|
||||
// 提交成功后将关闭对话框的逻辑交给父组件处理
|
||||
this.$emit('submit', this.form)
|
||||
}
|
||||
})
|
||||
},
|
||||
cancel() {
|
||||
this.saving = false
|
||||
this.$emit('cancel')
|
||||
},
|
||||
// 提供给父组件调用以重置saving状态
|
||||
resetSaving() {
|
||||
this.saving = false
|
||||
},
|
||||
beforeUpload(file) {
|
||||
const isValidSize = file.size / 1024 / 1024 < 100
|
||||
const isValidType = ['.bin', '.apk'].some(ext => file.name.toLowerCase().endsWith(ext))
|
||||
@@ -197,6 +193,7 @@ export default {
|
||||
this.uploadProgress = 0
|
||||
this.uploadStatus = ''
|
||||
this.isUploading = false
|
||||
this.saving = false
|
||||
// 重置表单中的文件相关字段
|
||||
if (!this.form.id) { // 只在新增时重置
|
||||
this.form.firmwarePath = ''
|
||||
@@ -213,25 +210,24 @@ export default {
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
::v-deep .el-dialog {
|
||||
border-radius: 20px;
|
||||
<style scoped lang="scss">
|
||||
.firmware-form {
|
||||
.custom-select {
|
||||
width: 100%;
|
||||
}
|
||||
.upload-demo {
|
||||
text-align: left;
|
||||
}
|
||||
.el-upload__tip {
|
||||
line-height: 1.2;
|
||||
padding-top: 2%;
|
||||
color: #909399;
|
||||
}
|
||||
.hint-text {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
font-size: 14px;
|
||||
}
|
||||
}
|
||||
|
||||
.upload-demo {
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.el-upload__tip {
|
||||
line-height: 1.2;
|
||||
padding-top: 2%;
|
||||
color: #909399;
|
||||
}
|
||||
|
||||
.hint-text {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
font-size: 14px;
|
||||
}
|
||||
</style>
|
||||
</style>
|
||||
|
||||
@@ -101,7 +101,8 @@
|
||||
$route.path === '/agent-template-management' ||
|
||||
$route.path === '/ota-management' ||
|
||||
$route.path === '/user-management' ||
|
||||
$route.path === '/feature-management',
|
||||
$route.path === '/feature-management' ||
|
||||
$route.path === '/replacement-word-management'
|
||||
}" @visible-change="handleParamDropdownVisibleChange">
|
||||
<span class="el-dropdown-link">
|
||||
<img loading="lazy" alt="" src="@/assets/header/param_management.png" :style="{
|
||||
@@ -113,7 +114,8 @@
|
||||
$route.path === '/agent-template-management' ||
|
||||
$route.path === '/ota-management' ||
|
||||
$route.path === '/user-management' ||
|
||||
$route.path === '/feature-management'
|
||||
$route.path === '/feature-management' ||
|
||||
$route.path === '/replacement-word-management'
|
||||
? 'brightness(0) invert(1)'
|
||||
: 'None',
|
||||
}" />
|
||||
@@ -154,32 +156,6 @@
|
||||
|
||||
<!-- 右侧元素 -->
|
||||
<div class="header-right">
|
||||
<div class="search-container" v-if="$route.path === '/home' && !(userInfo.superAdmin && isSmallScreen)">
|
||||
<div class="search-wrapper">
|
||||
<el-input v-model="search" :placeholder="$t('header.searchPlaceholder')" class="custom-search-input"
|
||||
@keyup.enter.native="handleSearch" @focus="showSearchHistory" @blur="hideSearchHistory" clearable
|
||||
ref="searchInput">
|
||||
<i slot="suffix" class="el-icon-search search-icon" @click="handleSearch"></i>
|
||||
</el-input>
|
||||
<!-- 搜索历史下拉框 -->
|
||||
<div v-if="showHistory && searchHistory.length > 0" class="search-history-dropdown">
|
||||
<div class="search-history-header">
|
||||
<span>{{ $t("header.searchHistory") }}</span>
|
||||
<el-button type="text" size="small" class="clear-history-btn" @click="clearSearchHistory">
|
||||
{{ $t("header.clearHistory") }}
|
||||
</el-button>
|
||||
</div>
|
||||
<div class="search-history-list">
|
||||
<div v-for="(item, index) in searchHistory" :key="index" class="search-history-item"
|
||||
@click.stop="selectSearchHistory(item)">
|
||||
<span class="history-text">{{ item }}</span>
|
||||
<i class="el-icon-close clear-item-icon" @click.stop="removeSearchHistory(index)"></i>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<img loading="lazy" alt="" src="@/assets/home/avatar.png" class="avatar-img" @click="handleAvatarClick" />
|
||||
<span class="el-user-dropdown" @click="handleAvatarClick">
|
||||
{{ userInfo.username || "加载中..." }}
|
||||
@@ -220,12 +196,6 @@ export default {
|
||||
voiceCloneDropdownVisible: false,
|
||||
userMenuVisible: false, // 添加用户菜单可见状态
|
||||
menuVisibleTimer: null, // 菜单显示定时器,防止够快触发
|
||||
isSmallScreen: false,
|
||||
// 搜索历史相关
|
||||
searchHistory: [],
|
||||
showHistory: false,
|
||||
SEARCH_HISTORY_KEY: "xiaozhi_search_history",
|
||||
MAX_HISTORY_COUNT: 3,
|
||||
// Cascader 配置
|
||||
cascaderProps: {
|
||||
expandTrigger: "click",
|
||||
@@ -351,17 +321,9 @@ export default {
|
||||
},
|
||||
},
|
||||
async mounted() {
|
||||
this.checkScreenSize();
|
||||
window.addEventListener("resize", this.checkScreenSize);
|
||||
// 从localStorage加载搜索历史
|
||||
this.loadSearchHistory();
|
||||
// 等待featureManager初始化完成后再加载功能状态
|
||||
await this.loadFeatureStatus();
|
||||
},
|
||||
//移除事件监听器
|
||||
beforeDestroy() {
|
||||
window.removeEventListener("resize", this.checkScreenSize);
|
||||
},
|
||||
methods: {
|
||||
handleRouter(type) {
|
||||
this.$router.push(this.routerPaths[type]);
|
||||
@@ -371,104 +333,6 @@ export default {
|
||||
// 等待featureManager初始化完成
|
||||
await featureManager.waitForInitialization();
|
||||
},
|
||||
checkScreenSize() {
|
||||
this.isSmallScreen = window.innerWidth <= 1386;
|
||||
},
|
||||
// 处理搜索
|
||||
handleSearch() {
|
||||
const searchValue = this.search.trim();
|
||||
|
||||
// 如果搜索内容为空,触发重置事件
|
||||
if (!searchValue) {
|
||||
this.$emit("search-reset");
|
||||
return;
|
||||
}
|
||||
|
||||
// 保存搜索历史
|
||||
this.saveSearchHistory(searchValue);
|
||||
|
||||
// 触发搜索事件,将搜索关键词传递给父组件
|
||||
this.$emit("search", searchValue);
|
||||
|
||||
// 搜索完成后让输入框失去焦点,从而触发blur事件隐藏搜索历史
|
||||
if (this.$refs.searchInput) {
|
||||
this.$refs.searchInput.blur();
|
||||
}
|
||||
},
|
||||
|
||||
// 显示搜索历史
|
||||
showSearchHistory() {
|
||||
this.showHistory = true;
|
||||
},
|
||||
|
||||
// 隐藏搜索历史
|
||||
hideSearchHistory() {
|
||||
// 延迟隐藏,以便点击事件能够执行
|
||||
setTimeout(() => {
|
||||
this.showHistory = false;
|
||||
}, 200);
|
||||
},
|
||||
|
||||
// 加载搜索历史
|
||||
loadSearchHistory() {
|
||||
try {
|
||||
const history = localStorage.getItem(this.SEARCH_HISTORY_KEY);
|
||||
if (history) {
|
||||
this.searchHistory = JSON.parse(history);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("加载搜索历史失败:", error);
|
||||
this.searchHistory = [];
|
||||
}
|
||||
},
|
||||
|
||||
// 保存搜索历史
|
||||
saveSearchHistory(keyword) {
|
||||
if (!keyword || this.searchHistory.includes(keyword)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 添加到历史记录开头
|
||||
this.searchHistory.unshift(keyword);
|
||||
|
||||
// 限制历史记录数量
|
||||
if (this.searchHistory.length > this.MAX_HISTORY_COUNT) {
|
||||
this.searchHistory = this.searchHistory.slice(0, this.MAX_HISTORY_COUNT);
|
||||
}
|
||||
|
||||
// 保存到localStorage
|
||||
try {
|
||||
localStorage.setItem(this.SEARCH_HISTORY_KEY, JSON.stringify(this.searchHistory));
|
||||
} catch (error) {
|
||||
console.error("保存搜索历史失败:", error);
|
||||
}
|
||||
},
|
||||
|
||||
// 选择搜索历史项
|
||||
selectSearchHistory(keyword) {
|
||||
this.search = keyword;
|
||||
this.handleSearch();
|
||||
},
|
||||
|
||||
// 移除单个搜索历史项
|
||||
removeSearchHistory(index) {
|
||||
this.searchHistory.splice(index, 1);
|
||||
try {
|
||||
localStorage.setItem(this.SEARCH_HISTORY_KEY, JSON.stringify(this.searchHistory));
|
||||
} catch (error) {
|
||||
console.error("更新搜索历史失败:", error);
|
||||
}
|
||||
},
|
||||
|
||||
// 清空所有搜索历史
|
||||
clearSearchHistory() {
|
||||
this.searchHistory = [];
|
||||
try {
|
||||
localStorage.removeItem(this.SEARCH_HISTORY_KEY);
|
||||
} catch (error) {
|
||||
console.error("清空搜索历史失败:", error);
|
||||
}
|
||||
},
|
||||
// 显示修改密码弹窗
|
||||
showChangePasswordDialog() {
|
||||
this.isChangePasswordDialogVisible = true;
|
||||
@@ -631,11 +495,9 @@ export default {
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.header {
|
||||
background: #f6fcfe66;
|
||||
border: 1px solid #fff;
|
||||
background: linear-gradient(180deg, #dfeafe, #eff4ff);
|
||||
height: 63px !important;
|
||||
min-width: 900px;
|
||||
/* 设置最小宽度防止过度压缩 */
|
||||
overflow: visible;
|
||||
}
|
||||
|
||||
@@ -651,7 +513,7 @@ export default {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
min-width: 120px;
|
||||
min-width: 130px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
@@ -668,42 +530,53 @@ export default {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 25px;
|
||||
position: absolute;
|
||||
left: 50%;
|
||||
transform: translateX(calc(-50% - 80px));
|
||||
background: white;
|
||||
border-radius: 30px;
|
||||
box-shadow: 0 0 6px 0px #cfe1fb;
|
||||
padding: 4px;
|
||||
}
|
||||
|
||||
.header-right {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 7px;
|
||||
min-width: 300px;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
.equipment-management {
|
||||
height: 30px;
|
||||
border-radius: 15px;
|
||||
background: #deeafe;
|
||||
padding: 8px 16px;
|
||||
border-radius: 30px;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
font-size: 14px;
|
||||
font-size: 16px;
|
||||
font-weight: 500;
|
||||
gap: 7px;
|
||||
color: #3d4566;
|
||||
color: #6c79a8;
|
||||
margin-left: 1px;
|
||||
align-items: center;
|
||||
transition: all 0.3s ease;
|
||||
cursor: pointer;
|
||||
flex-shrink: 0;
|
||||
/* 防止导航按钮被压缩 */
|
||||
padding: 0 15px;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.equipment-management.active-tab {
|
||||
background: #5778ff !important;
|
||||
color: #fff !important;
|
||||
background: linear-gradient(90deg, #2983fe 0%, #5251fc 100%);
|
||||
box-shadow: 0 1px 8px rgba(41, 131, 254, 0.5), inset 0 1px 0 rgba(255, 255, 255, 0.3);
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.equipment-management.active-tab::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
height: 50%;
|
||||
background: linear-gradient(to bottom, rgba(255, 255, 255, 0.4) 0%, rgba(255, 255, 255, 0) 100%);
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.equipment-management img {
|
||||
@@ -711,121 +584,6 @@ export default {
|
||||
height: 13px;
|
||||
}
|
||||
|
||||
.search-container {
|
||||
margin-right: 5px;
|
||||
flex: 0.9;
|
||||
min-width: 60px;
|
||||
max-width: none;
|
||||
}
|
||||
|
||||
.search-wrapper {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.search-history-dropdown {
|
||||
position: absolute;
|
||||
top: 100%;
|
||||
left: 0;
|
||||
right: 0;
|
||||
background: white;
|
||||
border: 1px solid #e4e6ef;
|
||||
border-radius: 4px;
|
||||
box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1);
|
||||
z-index: 1000;
|
||||
margin-top: 2px;
|
||||
}
|
||||
|
||||
.search-history-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 8px 12px;
|
||||
border-bottom: 1px solid #f0f0f0;
|
||||
font-size: 12px;
|
||||
color: #909399;
|
||||
}
|
||||
|
||||
.clear-history-btn {
|
||||
color: #909399;
|
||||
font-size: 11px;
|
||||
padding: 0;
|
||||
height: auto;
|
||||
}
|
||||
|
||||
.clear-history-btn:hover {
|
||||
color: #606266;
|
||||
}
|
||||
|
||||
.search-history-list {
|
||||
max-height: 200px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.search-history-item {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 8px 12px;
|
||||
cursor: pointer;
|
||||
font-size: 12px;
|
||||
color: #606266;
|
||||
}
|
||||
|
||||
.search-history-item:hover {
|
||||
background-color: #f5f7fa;
|
||||
}
|
||||
|
||||
.clear-item-icon {
|
||||
font-size: 10px;
|
||||
color: #909399;
|
||||
visibility: hidden;
|
||||
}
|
||||
|
||||
.more-dropdown {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.more-dropdown .el-dropdown-link {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 7px;
|
||||
height: 100%;
|
||||
padding: 0 15px;
|
||||
}
|
||||
|
||||
.search-history-item:hover .clear-item-icon {
|
||||
visibility: visible;
|
||||
}
|
||||
|
||||
.clear-item-icon:hover {
|
||||
color: #ff4949;
|
||||
}
|
||||
|
||||
.custom-search-input>>>.el-input__inner {
|
||||
height: 18px;
|
||||
border-radius: 9px;
|
||||
background-color: #fff;
|
||||
border: 1px solid #e4e6ef;
|
||||
padding-left: 8px;
|
||||
font-size: 9px;
|
||||
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.1);
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.search-icon {
|
||||
cursor: pointer;
|
||||
color: #909399;
|
||||
margin-right: 3px;
|
||||
font-size: 9px;
|
||||
line-height: 18px;
|
||||
}
|
||||
|
||||
.custom-search-input::v-deep .el-input__suffix-inner {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.avatar-img {
|
||||
width: 21px;
|
||||
height: 21px;
|
||||
@@ -841,7 +599,6 @@ export default {
|
||||
.nav-text {
|
||||
white-space: normal;
|
||||
text-align: center;
|
||||
max-width: 80px;
|
||||
line-height: 1.2;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<el-dialog :title="title" :visible="dialogVisible" width="600px" class="knowledge-base-dialog" @close="handleClose">
|
||||
<CustomDialog :title="title" :visible.sync="dialogVisible" width="600px" class="knowledge-base-dialog" @close="handleClose" @confirm="handleSubmit">
|
||||
<el-form ref="knowledgeBaseForm" :model="form" :rules="rules" label-width="100px" size="medium">
|
||||
<el-form-item :label="$t('knowledgeBaseDialog.name')" prop="name">
|
||||
<el-input v-model="form.name" :placeholder="$t('knowledgeBaseDialog.namePlaceholder')" clearable></el-input>
|
||||
@@ -16,15 +16,12 @@
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<div slot="footer" class="dialog-footer">
|
||||
<el-button @click="handleClose">{{ $t('knowledgeBaseDialog.cancel') }}</el-button>
|
||||
<el-button type="primary" @click="handleSubmit">{{ $t('knowledgeBaseDialog.confirm') }}</el-button>
|
||||
</div>
|
||||
</el-dialog>
|
||||
</CustomDialog>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import Api from "@/apis/api";
|
||||
import CustomDialog from "./CustomDialog.vue";
|
||||
|
||||
export default {
|
||||
name: "KnowledgeBaseDialog",
|
||||
@@ -93,6 +90,9 @@ export default {
|
||||
}
|
||||
};
|
||||
},
|
||||
components: {
|
||||
CustomDialog
|
||||
},
|
||||
watch: {
|
||||
visible(val) {
|
||||
this.dialogVisible = val;
|
||||
|
||||
@@ -0,0 +1,43 @@
|
||||
<template>
|
||||
<span v-if="isValid" class="mac-address-wrapper">{{ prefix }}<span class="mac-address-mask">{{ middle }}</span>{{ suffix }}</span>
|
||||
<span v-else>{{ macAddress }}</span>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
props: {
|
||||
macAddress: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
isValid() {
|
||||
return /^(?:[0-9A-Z]{2}[:-]){5}[0-9A-Z]{2}$/i.test(this.macAddress);
|
||||
},
|
||||
segments() {
|
||||
return this.macAddress.split(':');
|
||||
},
|
||||
prefix() {
|
||||
return this.segments.slice(0, 2).join(':');
|
||||
},
|
||||
middle() {
|
||||
return ':' + this.segments.slice(2, 4).join(':') + ':';
|
||||
},
|
||||
suffix() {
|
||||
return this.segments.slice(4).join(':');
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.mac-address-wrapper:hover .mac-address-mask {
|
||||
filter: none;
|
||||
}
|
||||
|
||||
.mac-address-mask {
|
||||
filter: blur(4px);
|
||||
transition: filter 0.3s;
|
||||
}
|
||||
</style>
|
||||
@@ -1,43 +1,45 @@
|
||||
<template>
|
||||
<el-dialog :title="$t('manualAddDeviceDialog.title')" :visible="visible" @close="handleClose" width="30%" center>
|
||||
<div class="dialog-content">
|
||||
<el-form :model="deviceForm" :rules="rules" ref="deviceForm" label-width="auto">
|
||||
<el-form-item :label="$t('manualAddDeviceDialog.deviceType')" prop="board">
|
||||
<el-select v-model="deviceForm.board" :placeholder="$t('manualAddDeviceDialog.deviceTypePlaceholder')" style="width: 100%">
|
||||
<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="$t('manualAddDeviceDialog.firmwareVersion')" prop="appVersion">
|
||||
<el-input v-model="deviceForm.appVersion" :placeholder="$t('manualAddDeviceDialog.firmwareVersionPlaceholder')"></el-input>
|
||||
</el-form-item>
|
||||
<el-form-item :label="$t('manualAddDeviceDialog.macAddress')" prop="macAddress">
|
||||
<el-input v-model="deviceForm.macAddress" :placeholder="$t('manualAddDeviceDialog.macAddressPlaceholder')"></el-input>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</div>
|
||||
<div style="display: flex;margin: 15px 15px;gap: 7px;">
|
||||
<div class="dialog-btn" @click="submitForm">{{ $t('manualAddDeviceDialog.confirm') }}</div>
|
||||
<div class="dialog-btn cancel-btn" @click="cancel">{{ $t('manualAddDeviceDialog.cancel') }}</div>
|
||||
</div>
|
||||
</el-dialog>
|
||||
<CustomDialog
|
||||
:title="$t('manualAddDeviceDialog.title')"
|
||||
:visible="visible"
|
||||
width="600px"
|
||||
:destroy-on-close="false"
|
||||
@close="closeDialog"
|
||||
@confirm="submitForm"
|
||||
>
|
||||
<el-form :model="deviceForm" :rules="rules" ref="deviceForm" label-width="auto">
|
||||
<el-form-item :label="$t('manualAddDeviceDialog.deviceType')" prop="board">
|
||||
<el-select v-model="deviceForm.board" :placeholder="$t('manualAddDeviceDialog.deviceTypePlaceholder')" style="width: 100%">
|
||||
<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="$t('manualAddDeviceDialog.firmwareVersion')" prop="appVersion">
|
||||
<el-input v-model="deviceForm.appVersion" :placeholder="$t('manualAddDeviceDialog.firmwareVersionPlaceholder')"></el-input>
|
||||
</el-form-item>
|
||||
<el-form-item :label="$t('manualAddDeviceDialog.macAddress')" prop="macAddress">
|
||||
<el-input v-model="deviceForm.macAddress" :placeholder="$t('manualAddDeviceDialog.macAddressPlaceholder')"></el-input>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</CustomDialog>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import Api from '@/apis/api';
|
||||
import CustomDialog from './CustomDialog.vue';
|
||||
|
||||
export default {
|
||||
name: 'ManualAddDeviceDialog',
|
||||
components: { CustomDialog },
|
||||
props: {
|
||||
visible: { type: Boolean, required: true },
|
||||
agentId: { type: String, required: true }
|
||||
},
|
||||
data() {
|
||||
// MAC地址验证规则
|
||||
const validateMac = (rule, value, callback) => {
|
||||
const macRegex = /^([0-9A-Fa-f]{2}[:-]){5}([0-9A-Fa-f]{2})$/;
|
||||
if (!value) {
|
||||
@@ -67,7 +69,7 @@ export default {
|
||||
{ required: true, validator: validateMac, trigger: 'blur' }
|
||||
]
|
||||
}
|
||||
}
|
||||
};
|
||||
},
|
||||
created() {
|
||||
this.getFirmwareTypes();
|
||||
@@ -94,7 +96,7 @@ export default {
|
||||
agentId: this.agentId,
|
||||
...this.deviceForm
|
||||
};
|
||||
|
||||
|
||||
Api.device.manualAddDevice(params, ({ data }) => {
|
||||
if (data.code === 0) {
|
||||
this.$message.success(this.$t('manualAddDeviceDialog.addSuccess'));
|
||||
@@ -108,51 +110,7 @@ export default {
|
||||
closeDialog() {
|
||||
this.$emit('update:visible', false);
|
||||
this.$refs.deviceForm.resetFields();
|
||||
},
|
||||
cancel() {
|
||||
this.closeDialog();
|
||||
},
|
||||
handleClose() {
|
||||
this.closeDialog();
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.dialog-content {
|
||||
padding: 0 20px;
|
||||
}
|
||||
|
||||
.dialog-btn {
|
||||
cursor: pointer;
|
||||
flex: 1;
|
||||
border-radius: 23px;
|
||||
background: #5778ff;
|
||||
height: 40px;
|
||||
font-weight: 500;
|
||||
font-size: 12px;
|
||||
color: #fff;
|
||||
line-height: 40px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.cancel-btn {
|
||||
background: #e6ebff;
|
||||
border: 1px solid #adbdff;
|
||||
color: #5778ff;
|
||||
}
|
||||
|
||||
::v-deep .el-dialog {
|
||||
border-radius: 15px;
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
::v-deep .el-dialog__body {
|
||||
padding: 20px 20px;
|
||||
}
|
||||
|
||||
::v-deep .el-form-item {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,20 @@
|
||||
<template>
|
||||
<svg t="1780649285700" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg"
|
||||
p-id="79487" xmlns:xlink="http://www.w3.org/1999/xlink" width="48" height="48">
|
||||
<path
|
||||
d="M917.333333 138.666667C917.333333 85.333333 870.4 42.666667 810.666667 42.666667H219.733333C157.866667 42.666667 106.666667 89.6 106.666667 145.066667V855.466667c0 70.4 57.6 128 128 128h576c59.733333 0 106.666667-46.933333 106.666666-106.666667V138.666667zM234.666667 213.333333h362.666666c23.466667 0 42.666667 19.2 42.666667 42.666667s-19.2 42.666667-42.666667 42.666667H234.666667c-23.466667 0-42.666667-19.2-42.666667-42.666667s19.2-42.666667 42.666667-42.666667z m0 128h277.333333c23.466667 0 42.666667 19.2 42.666667 42.666667s-19.2 42.666667-42.666667 42.666667H234.666667c-23.466667 0-42.666667-19.2-42.666667-42.666667s19.2-42.666667 42.666667-42.666667z m597.333333 533.333334c0 12.8-8.533333 21.333333-21.333333 21.333333H236.8c-21.333333 0-40.533333-14.933333-44.8-36.266667-4.266667-25.6 17.066667-49.066667 42.666667-49.066666h576c12.8 0 21.333333 8.533333 21.333333 21.333333v42.666667z"
|
||||
:fill="color" p-id="79488"></path>
|
||||
</svg>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'ManualIcon',
|
||||
props: {
|
||||
color: {
|
||||
type: String,
|
||||
default: '#b3a0fa',
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
@@ -1,135 +1,109 @@
|
||||
<template>
|
||||
<el-dialog :visible.sync="dialogVisible" :close-on-click-modal="false" width="57%" center 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="
|
||||
font-size: 30px;
|
||||
color: #3d4566;
|
||||
margin-top: -10px;
|
||||
margin-bottom: 10px;
|
||||
text-align: center;
|
||||
">
|
||||
{{
|
||||
modelData.duplicateMode
|
||||
? $t("modelConfigDialog.duplicateModel")
|
||||
: $t("modelConfigDialog.editModel")
|
||||
}}
|
||||
</div>
|
||||
|
||||
<button class="custom-close-btn" @click="dialogVisible = false">×</button>
|
||||
|
||||
<div style="
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 10px;
|
||||
">
|
||||
<div style="font-size: 20px; font-weight: bold; color: #3d4566">
|
||||
{{ $t("modelConfigDialog.modelInfo") }}
|
||||
<CustomDialog
|
||||
:title="$t('modelConfigDialog.editModel')"
|
||||
:visible.sync="dialogVisible"
|
||||
width="57%"
|
||||
class="model-edit-dialog"
|
||||
:confirmLoading="saving"
|
||||
@confirm="handleSave"
|
||||
@close="handleClose"
|
||||
@open="handleOpen"
|
||||
>
|
||||
<div class="dialog-scroll-body">
|
||||
<div class="header-row">
|
||||
<div class="section-title">{{ $t("modelConfigDialog.modelInfo") }}</div>
|
||||
<div class="switch-group">
|
||||
<div class="switch-item">
|
||||
<span class="switch-label">{{ $t("modelConfigDialog.enable") }}</span>
|
||||
<el-switch v-model="form.isEnabled" :active-value="1" :inactive-value="0" class="custom-switch"></el-switch>
|
||||
</div>
|
||||
<div style="display: flex; align-items: center; gap: 20px">
|
||||
<div style="display: flex; align-items: center">
|
||||
<span style="margin-right: 8px">{{ $t("modelConfigDialog.enable") }}</span>
|
||||
<el-switch v-model="form.isEnabled" :active-value="1" :inactive-value="0" class="custom-switch"></el-switch>
|
||||
</div>
|
||||
<div style="display: none; align-items: center">
|
||||
<span style="margin-right: 8px">{{
|
||||
$t("modelConfigDialog.setDefault")
|
||||
}}</span>
|
||||
<el-switch v-model="form.isDefault" :active-value="1" :inactive-value="0" class="custom-switch"></el-switch>
|
||||
</div>
|
||||
<div class="switch-item hidden">
|
||||
<span class="switch-label">{{ $t("modelConfigDialog.setDefault") }}</span>
|
||||
<el-switch v-model="form.isDefault" :active-value="1" :inactive-value="0" class="custom-switch"></el-switch>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style="height: 2px; background: #e9e9e9; margin-bottom: 22px"></div>
|
||||
<div class="section-divider"></div>
|
||||
|
||||
<el-form :model="form" ref="form" label-width="auto" label-position="left" class="custom-form">
|
||||
<div style="display: flex; gap: 20px; margin-bottom: 0">
|
||||
<el-form-item :label="$t('modelConfigDialog.modelName')" prop="name" style="flex: 1">
|
||||
<el-input v-model="form.modelName" :placeholder="$t('modelConfigDialog.enterModelName')"
|
||||
class="custom-input-bg"></el-input>
|
||||
</el-form-item>
|
||||
<el-form-item :label="$t('modelConfigDialog.modelCode')" prop="code" style="flex: 1">
|
||||
<el-input v-model="form.modelCode" :placeholder="$t('modelConfigDialog.enterModelCode')"
|
||||
class="custom-input-bg"></el-input>
|
||||
</el-form-item>
|
||||
</div>
|
||||
|
||||
<div style="display: flex; gap: 20px; margin-bottom: 0">
|
||||
<el-form-item :label="$t('modelConfigDialog.supplier')" prop="supplier" style="flex: 1">
|
||||
<el-select v-model="form.configJson.type" :placeholder="$t('modelConfigDialog.selectSupplier')"
|
||||
class="custom-select custom-input-bg" style="width: 100%" @focus="loadProviders" filterable>
|
||||
<el-option v-for="item in providers" :key="item.value" :label="item.label" :value="item.value" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item :label="$t('modelConfigDialog.sortOrder')" prop="sort" style="flex: 1">
|
||||
<el-input v-model.number="form.sort" type="number" :placeholder="$t('modelConfigDialog.enterSortOrder')"
|
||||
class="custom-input-bg"></el-input>
|
||||
</el-form-item>
|
||||
</div>
|
||||
|
||||
<el-form-item :label="$t('modelConfigDialog.docLink')" prop="docUrl" style="margin-bottom: 27px">
|
||||
<el-input v-model="form.docLink" :placeholder="$t('modelConfigDialog.enterDocLink')"
|
||||
class="custom-input-bg"></el-input>
|
||||
<el-form :model="form" ref="form" label-width="auto" label-position="left">
|
||||
<div class="form-row">
|
||||
<el-form-item :label="$t('modelConfigDialog.modelName')" prop="name" style="flex: 1">
|
||||
<el-input v-model="form.modelName" :placeholder="$t('modelConfigDialog.enterModelName')"></el-input>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item :label="$t('modelConfigDialog.remark')" prop="remark" class="prop-remark">
|
||||
<el-input v-model="form.remark" type="textarea" :rows="3" :placeholder="$t('modelConfigDialog.enterRemark')"
|
||||
:autosize="{ minRows: 3, maxRows: 5 }" class="custom-input-bg"></el-input>
|
||||
<el-form-item :label="$t('modelConfigDialog.modelCode')" prop="code" style="flex: 1">
|
||||
<el-input v-model="form.modelCode" :placeholder="$t('modelConfigDialog.enterModelCode')"></el-input>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
|
||||
<div style="font-size: 20px; font-weight: bold; color: #3d4566; margin-bottom: 15px">
|
||||
{{ $t("modelConfigDialog.callInfo") }}
|
||||
</div>
|
||||
<div style="height: 2px; background: #e9e9e9; margin-bottom: 22px"></div>
|
||||
|
||||
<el-form :model="form.configJson" ref="callInfoForm" label-width="auto" class="custom-form">
|
||||
<template v-for="(row, rowIndex) in chunkedCallInfoFields">
|
||||
<div :key="rowIndex" style="display: flex; gap: 20px; margin-bottom: 0">
|
||||
<el-form-item v-for="field in row" :key="field.prop" :label="field.label" :prop="field.prop"
|
||||
style="flex: 1">
|
||||
<template v-if="field.type === 'json-textarea'">
|
||||
<el-input v-model="fieldJsonMap[field.prop]" type="textarea" :rows="3"
|
||||
:placeholder="$t('modelConfigDialog.enterJsonExample')" class="custom-input-bg"
|
||||
@change="(val) => handleJsonChange(field.prop, val)" @focus="
|
||||
isSensitiveField(field.prop)
|
||||
? handleJsonInputFocus(field.prop, fieldJsonMap[field.prop])
|
||||
: undefined
|
||||
" @blur="
|
||||
isSensitiveField(field.prop)
|
||||
? handleJsonInputBlur(field.prop)
|
||||
: undefined
|
||||
"></el-input>
|
||||
</template>
|
||||
<div class="form-row">
|
||||
<el-form-item :label="$t('modelConfigDialog.supplier')" prop="supplier" style="flex: 1">
|
||||
<el-select v-model="form.configJson.type" :placeholder="$t('modelConfigDialog.selectSupplier')"
|
||||
style="width: 100%" @focus="loadProviders" filterable>
|
||||
<el-option v-for="item in providers" :key="item.value" :label="item.label" :value="item.value" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item :label="$t('modelConfigDialog.sortOrder')" prop="sort" style="flex: 1">
|
||||
<el-input v-model.number="form.sort" type="number" :placeholder="$t('modelConfigDialog.enterSortOrder')"></el-input>
|
||||
</el-form-item>
|
||||
</div>
|
||||
|
||||
<el-input v-else v-model="form.configJson[field.prop]" :placeholder="field.placeholder" :type="field.type"
|
||||
class="custom-input-bg" :show-password="field.type === 'password'" @focus="
|
||||
<el-form-item :label="$t('modelConfigDialog.docLink')" prop="docUrl" style="margin-bottom: 27px">
|
||||
<el-input v-model="form.docLink" :placeholder="$t('modelConfigDialog.enterDocLink')"></el-input>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item :label="$t('modelConfigDialog.remark')" prop="remark" class="prop-remark">
|
||||
<el-input v-model="form.remark" type="textarea" :rows="3" :autosize="{ minRows: 3, maxRows: 5 }" :placeholder="$t('modelConfigDialog.enterRemark')"></el-input>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
|
||||
<teleport v-if="chunkedCallInfoFields.length">
|
||||
<div class="section-title">{{ $t("modelConfigDialog.callInfo") }}</div>
|
||||
<div class="section-divider"></div>
|
||||
</teleport>
|
||||
|
||||
<el-form :model="form.configJson" ref="callInfoForm" label-width="auto" label-position="left">
|
||||
<template>
|
||||
<div v-for="(row, rowIndex) in chunkedCallInfoFields" :key="rowIndex" class="form-row">
|
||||
<el-form-item v-for="field in row" :key="field.prop" :label="field.label" :prop="field.prop"
|
||||
style="flex: 1">
|
||||
<template v-if="field.type === 'json-textarea'">
|
||||
<el-input v-model="fieldJsonMap[field.prop]" type="textarea" :rows="3"
|
||||
:placeholder="$t('modelConfigDialog.enterJsonExample')"
|
||||
@change="(val) => handleJsonChange(field.prop, val)" @focus="
|
||||
isSensitiveField(field.prop)
|
||||
? handleInputFocus(field.prop, form.configJson[field.prop])
|
||||
? handleJsonInputFocus(field.prop, fieldJsonMap[field.prop])
|
||||
: undefined
|
||||
" @blur="
|
||||
isSensitiveField(field.prop) ? handleInputBlur(field.prop) : undefined
|
||||
isSensitiveField(field.prop)
|
||||
? handleJsonInputBlur(field.prop)
|
||||
: undefined
|
||||
"></el-input>
|
||||
</el-form-item>
|
||||
</div>
|
||||
</template>
|
||||
</el-form>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<div style="display: flex; justify-content: center">
|
||||
<el-button type="primary" @click="handleSave" class="save-btn" :loading="saving" :disabled="saving">
|
||||
{{ $t("modelConfigDialog.save") }}
|
||||
</el-button>
|
||||
<el-input v-else v-model="form.configJson[field.prop]" :placeholder="field.placeholder" :type="field.type"
|
||||
:show-password="field.type === 'password'" @focus="
|
||||
isSensitiveField(field.prop)
|
||||
? handleInputFocus(field.prop, form.configJson[field.prop])
|
||||
: undefined
|
||||
" @blur="
|
||||
isSensitiveField(field.prop) ? handleInputBlur(field.prop) : undefined
|
||||
"></el-input>
|
||||
</el-form-item>
|
||||
</div>
|
||||
</template>
|
||||
</el-form>
|
||||
</div>
|
||||
</el-dialog>
|
||||
</CustomDialog>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import CustomDialog from './CustomDialog.vue';
|
||||
import Api from "@/apis/api";
|
||||
|
||||
export default {
|
||||
name: "ModelEditDialog",
|
||||
components: { CustomDialog },
|
||||
props: {
|
||||
visible: { type: Boolean, default: false },
|
||||
modelData: {
|
||||
@@ -175,6 +149,11 @@ export default {
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
title() {
|
||||
return this.modelData.duplicateMode
|
||||
? this.$t("modelConfigDialog.duplicateModel")
|
||||
: this.$t("modelConfigDialog.editModel");
|
||||
},
|
||||
chunkedCallInfoFields() {
|
||||
const chunkSize = 2;
|
||||
const result = [];
|
||||
@@ -189,19 +168,11 @@ export default {
|
||||
this.resetProviders();
|
||||
this.loadProviders();
|
||||
},
|
||||
dialogVisible(val) {
|
||||
this.$emit("update:visible", val);
|
||||
if (!val) {
|
||||
this.resetForm();
|
||||
} else if (val && this.modelData.id) {
|
||||
this.loadModelData();
|
||||
}
|
||||
},
|
||||
visible(val) {
|
||||
this.dialogVisible = val;
|
||||
if (val) {
|
||||
this.loadProviders();
|
||||
}
|
||||
},
|
||||
dialogVisible(val) {
|
||||
this.$emit("update:visible", val);
|
||||
},
|
||||
"form.configJson.type"(newVal) {
|
||||
if (newVal && this.providersLoaded) {
|
||||
@@ -210,6 +181,19 @@ export default {
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
handleOpen() {
|
||||
this.loadProviders();
|
||||
if (this.modelData.id) {
|
||||
this.loadModelData();
|
||||
}
|
||||
},
|
||||
handleClose() {
|
||||
this.saving = false;
|
||||
// 处理关闭弹窗闪动问题
|
||||
setTimeout(() => {
|
||||
this.resetForm();
|
||||
}, 200)
|
||||
},
|
||||
resetForm() {
|
||||
this.form = {
|
||||
id: "",
|
||||
@@ -480,168 +464,67 @@ export default {
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.custom-dialog {
|
||||
position: relative;
|
||||
border-radius: 20px;
|
||||
overflow: hidden;
|
||||
background: white;
|
||||
padding-bottom: 17px;
|
||||
}
|
||||
@import '@/styles/global.scss';
|
||||
|
||||
.custom-dialog .el-dialog__header {
|
||||
padding: 0;
|
||||
border-bottom: none;
|
||||
::v-deep .el-dialog {
|
||||
margin-top: 6vh !important;
|
||||
}
|
||||
|
||||
.center-dialog {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
::v-deep .el-dialog__body {
|
||||
max-height: 60vh;
|
||||
overflow-y: auto;
|
||||
@include scrollbar-style;
|
||||
}
|
||||
.model-edit-dialog {
|
||||
.header-row {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.custom-close-btn {
|
||||
position: absolute;
|
||||
top: 20px;
|
||||
right: 20px;
|
||||
width: 35px;
|
||||
height: 35px;
|
||||
border-radius: 50%;
|
||||
border: 2px solid #cfcfcf;
|
||||
background: none;
|
||||
font-size: 30px;
|
||||
font-weight: lighter;
|
||||
color: #cfcfcf;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
z-index: 1;
|
||||
padding: 0;
|
||||
outline: none;
|
||||
}
|
||||
.section-title {
|
||||
margin-bottom: 10px;
|
||||
font-size: 20px;
|
||||
font-weight: bold;
|
||||
color: #3d4566;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.custom-close-btn:hover {
|
||||
color: #409eff;
|
||||
border-color: #409eff;
|
||||
}
|
||||
.switch-group {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 20px;
|
||||
}
|
||||
|
||||
.custom-select .el-input__suffix {
|
||||
background: #e6e8ea;
|
||||
right: 6px;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
top: 9px;
|
||||
}
|
||||
.switch-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
.custom-select .el-input__suffix-inner {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 100%;
|
||||
}
|
||||
&.hidden {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.custom-select .el-icon-arrow-up:before {
|
||||
content: "";
|
||||
display: inline-block;
|
||||
width: 0;
|
||||
height: 0;
|
||||
border-left: 5px solid transparent;
|
||||
border-right: 5px solid transparent;
|
||||
border-top: 7px solid #c0c4cc;
|
||||
position: relative;
|
||||
top: -2px;
|
||||
transform: rotate(180deg);
|
||||
}
|
||||
.switch-label {
|
||||
margin-right: 8px;
|
||||
}
|
||||
}
|
||||
|
||||
.custom-form .el-form-item {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
.section-divider {
|
||||
height: 1px;
|
||||
background: #e9e9e9;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.custom-form .el-form-item__label {
|
||||
color: #3d4566;
|
||||
font-weight: normal;
|
||||
text-align: right;
|
||||
padding-right: 20px;
|
||||
}
|
||||
.form-row {
|
||||
display: flex;
|
||||
gap: 20px;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.custom-form .el-form-item.prop-remark .el-form-item__label {
|
||||
margin-top: -4px;
|
||||
}
|
||||
|
||||
.custom-input-bg .el-input__inner::-webkit-input-placeholder,
|
||||
.custom-input-bg .el-textarea__inner::-webkit-input-placeholder {
|
||||
color: #9c9f9e;
|
||||
}
|
||||
|
||||
.custom-input-bg .el-input__inner,
|
||||
.custom-input-bg .el-textarea__inner {
|
||||
background-color: #f6f8fc;
|
||||
}
|
||||
|
||||
.save-btn {
|
||||
background: #e6f0fd;
|
||||
color: #237ff4;
|
||||
border: 1px solid #b3d1ff;
|
||||
width: 150px;
|
||||
height: 40px;
|
||||
font-size: 16px;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.save-btn:hover {
|
||||
background: linear-gradient(to right, #237ff4, #9c40d5);
|
||||
color: white;
|
||||
border: none;
|
||||
}
|
||||
|
||||
.custom-switch .el-switch__core {
|
||||
border-radius: 20px;
|
||||
height: 23px;
|
||||
background-color: #c0ccda;
|
||||
width: 35px;
|
||||
padding: 0 20px;
|
||||
}
|
||||
|
||||
.custom-switch .el-switch__core:after {
|
||||
width: 15px;
|
||||
height: 15px;
|
||||
background-color: white;
|
||||
top: 3px;
|
||||
left: 4px;
|
||||
transition: all 0.3s;
|
||||
}
|
||||
|
||||
.custom-switch.is-checked .el-switch__core {
|
||||
border-color: #b5bcf0;
|
||||
background-color: #cfd7fa;
|
||||
padding: 0 20px;
|
||||
}
|
||||
|
||||
.custom-switch.is-checked .el-switch__core:after {
|
||||
left: 100%;
|
||||
margin-left: -18px;
|
||||
background-color: #1b47ee;
|
||||
}
|
||||
|
||||
[style*="display: flex"] {
|
||||
gap: 20px;
|
||||
}
|
||||
|
||||
.custom-input-bg .el-input__inner {
|
||||
height: 32px;
|
||||
}
|
||||
|
||||
.custom-form .el-form-item {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.custom-form .el-form-item__label {
|
||||
color: #3d4566;
|
||||
font-weight: normal;
|
||||
text-align: right;
|
||||
padding-right: 20px;
|
||||
::v-deep .el-input__inner {
|
||||
height: 32px;
|
||||
}
|
||||
::v-deep .el-form-item {
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,16 +1,14 @@
|
||||
<template>
|
||||
<el-dialog :title="title" :visible.sync="visible" width="520px" class="param-dialog-wrapper" :append-to-body="true"
|
||||
:close-on-click-modal="false" :key="dialogKey" custom-class="custom-param-dialog" :show-close="false">
|
||||
<CustomDialog
|
||||
:title="title"
|
||||
:visible.sync="visible"
|
||||
width="600px"
|
||||
class="param-dialog-wrapper"
|
||||
@confirm="submit"
|
||||
@close="cancel"
|
||||
:confirmLoading="saving"
|
||||
>
|
||||
<div class="dialog-container">
|
||||
<div class="dialog-header">
|
||||
<h2 class="dialog-title">{{ title }}</h2>
|
||||
<button class="custom-close-btn" @click="cancel">
|
||||
<svg width="14" height="14" viewBox="0 0 14 14" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M13 1L1 13M1 1L13 13" stroke="currentColor" stroke-width="2" stroke-linecap="round" />
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<el-form :model="form" :rules="rules" ref="form" label-width="auto" label-position="left" class="param-form">
|
||||
<el-form-item :label="$t('paramDialog.paramCode')" prop="paramCode" class="form-item">
|
||||
<el-input v-model="form.paramCode" :placeholder="$t('paramDialog.paramCodePlaceholder')"
|
||||
@@ -54,20 +52,12 @@
|
||||
class="custom-textarea"></el-input>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
|
||||
<div class="dialog-footer">
|
||||
<el-button type="primary" @click="submit" class="save-btn" :loading="saving" :disabled="saving">
|
||||
{{ $t('paramDialog.save') }}
|
||||
</el-button>
|
||||
<el-button @click="cancel" class="cancel-btn">
|
||||
{{ $t('paramDialog.cancel') }}
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</el-dialog>
|
||||
</CustomDialog>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import CustomDialog from './CustomDialog.vue';
|
||||
export default {
|
||||
props: {
|
||||
title: {
|
||||
@@ -89,6 +79,9 @@ export default {
|
||||
})
|
||||
}
|
||||
},
|
||||
components: {
|
||||
CustomDialog
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
dialogKey: Date.now(),
|
||||
@@ -210,61 +203,9 @@ export default {
|
||||
|
||||
<style scoped lang="scss">
|
||||
.param-dialog-wrapper {
|
||||
.dialog-container {
|
||||
padding: 24px 32px;
|
||||
background: linear-gradient(135deg, #f8fafc 0%, #f1f5f9 100%);
|
||||
}
|
||||
|
||||
.dialog-header {
|
||||
position: relative;
|
||||
margin-bottom: 24px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.dialog-title {
|
||||
font-size: 20px;
|
||||
color: #1e293b;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
font-weight: 600;
|
||||
letter-spacing: 0.5px;
|
||||
}
|
||||
|
||||
.custom-close-btn {
|
||||
position: absolute;
|
||||
top: -8px;
|
||||
right: -8px;
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
border-radius: 50%;
|
||||
border: none;
|
||||
background: #f1f5f9;
|
||||
color: #64748b;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 0;
|
||||
outline: none;
|
||||
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
||||
|
||||
&:hover {
|
||||
color: #ffffff;
|
||||
background: #ef4444;
|
||||
transform: rotate(90deg);
|
||||
box-shadow: 0 4px 6px rgba(239, 68, 68, 0.2);
|
||||
}
|
||||
|
||||
svg {
|
||||
transition: transform 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
}
|
||||
}
|
||||
|
||||
.param-form {
|
||||
.form-item {
|
||||
margin-bottom: 20px;
|
||||
|
||||
:deep(.el-form-item__label) {
|
||||
color: #475569;
|
||||
font-weight: 500;
|
||||
@@ -355,65 +296,5 @@ export default {
|
||||
margin-top: -4px;
|
||||
}
|
||||
}
|
||||
|
||||
.dialog-footer {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
padding: 16px 0 0;
|
||||
margin-top: 16px;
|
||||
|
||||
.save-btn {
|
||||
width: 120px;
|
||||
height: 42px;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
border-radius: 8px;
|
||||
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
background: #3b82f6;
|
||||
color: white;
|
||||
border: none;
|
||||
letter-spacing: 0.5px;
|
||||
box-shadow: 0 2px 4px rgba(59, 130, 246, 0.2);
|
||||
|
||||
&:hover {
|
||||
background: #2563eb;
|
||||
transform: translateY(-1px);
|
||||
box-shadow: 0 4px 6px rgba(59, 130, 246, 0.3);
|
||||
}
|
||||
|
||||
&:active {
|
||||
transform: translateY(0);
|
||||
box-shadow: 0 2px 3px rgba(59, 130, 246, 0.2);
|
||||
}
|
||||
}
|
||||
|
||||
.cancel-btn {
|
||||
width: 120px;
|
||||
height: 42px;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
border-radius: 8px;
|
||||
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
background: #ffffff;
|
||||
color: #64748b;
|
||||
border: 1px solid #e2e8f0;
|
||||
margin-left: 16px;
|
||||
letter-spacing: 0.5px;
|
||||
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.05);
|
||||
|
||||
&:hover {
|
||||
background: #f8fafc;
|
||||
color: #475569;
|
||||
border-color: #cbd5e1;
|
||||
transform: translateY(-1px);
|
||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
&:active {
|
||||
transform: translateY(0);
|
||||
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.05);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,56 +1,54 @@
|
||||
<template>
|
||||
<el-dialog :visible="visible" :close-on-click-modal="false" @update:visible="handleVisibleChange" width="57%" center 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="font-size: 30px; color: #3d4566; margin-top: -15px; margin-bottom: 20px; text-align: center;">
|
||||
{{ title }}
|
||||
</div>
|
||||
|
||||
<button class="custom-close-btn" @click="handleClose">×</button>
|
||||
|
||||
<el-form :model="form" label-width="auto" :rules="rules" ref="form" class="custom-form">
|
||||
<div style="display: flex; gap: 20px; margin-bottom: 20px;">
|
||||
<el-form-item :label="$t('providerDialog.category')" prop="modelType" style="flex: 1;">
|
||||
<el-select v-model="form.modelType" :placeholder="$t('providerDialog.selectCategory')" class="custom-input-bg" style="width: 100%;">
|
||||
<CustomDialog
|
||||
:title="title"
|
||||
:visible.sync="visible"
|
||||
width="57%"
|
||||
class="provider-dialog-wrapper"
|
||||
@confirm="submit"
|
||||
@close="handleClose"
|
||||
:confirmLoading="saving"
|
||||
>
|
||||
<div class="dialog-container">
|
||||
<el-form :model="form" :rules="rules" ref="form" label-width="auto" label-position="left" class="provider-form">
|
||||
<div class="form-row">
|
||||
<el-form-item :label="$t('providerDialog.category')" prop="modelType" class="form-item">
|
||||
<el-select v-model="form.modelType" :placeholder="$t('providerDialog.selectCategory')" class="custom-select">
|
||||
<el-option v-for="item in modelTypes" :key="item.value" :label="item.label" :value="item.value">
|
||||
</el-option>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item :label="$t('providerDialog.code')" prop="providerCode" style="flex: 1;">
|
||||
<el-input v-model="form.providerCode" :placeholder="$t('providerDialog.inputCode')" class="custom-input-bg"></el-input>
|
||||
<el-form-item :label="$t('providerDialog.code')" prop="providerCode" class="form-item">
|
||||
<el-input v-model="form.providerCode" :placeholder="$t('providerDialog.inputCode')" class="custom-input"></el-input>
|
||||
</el-form-item>
|
||||
</div>
|
||||
|
||||
<div style="display: flex; gap: 20px; margin-bottom: 20px;">
|
||||
<el-form-item :label="$t('providerDialog.name')" prop="name" style="flex: 1;">
|
||||
<el-input v-model="form.name" :placeholder="$t('providerDialog.inputName')" class="custom-input-bg"></el-input>
|
||||
<div class="form-row">
|
||||
<el-form-item :label="$t('providerDialog.name')" prop="name" class="form-item">
|
||||
<el-input v-model="form.name" :placeholder="$t('providerDialog.inputName')" class="custom-input"></el-input>
|
||||
</el-form-item>
|
||||
<el-form-item :label="$t('providerDialog.sort')" prop="sort" style="flex: 1;">
|
||||
<el-input-number v-model="form.sort" :min="0" controls-position="right" class="custom-input-bg"
|
||||
<el-form-item :label="$t('providerDialog.sort')" prop="sort" class="form-item">
|
||||
<el-input-number v-model="form.sort" :min="0" controls-position="right" class="custom-input-number"
|
||||
style="width: 100%;"></el-input-number>
|
||||
</el-form-item>
|
||||
</div>
|
||||
|
||||
<div style="font-size: 20px; font-weight: bold; color: #3d4566; margin-bottom: 15px;">
|
||||
{{ $t('providerDialog.fieldConfig') }}
|
||||
<div style="display: inline-block; float: right;">
|
||||
<el-button type="primary" @click="addField" size="small" style="background: #5bc98c; border: none;"
|
||||
<div class="field-config-header">
|
||||
<span class="field-config-title">{{ $t('providerDialog.fieldConfig') }}</span>
|
||||
<div class="field-config-actions">
|
||||
<el-button type="primary" @click="addField" size="small" class="btn-add"
|
||||
:disabled="hasIncompleteFields">
|
||||
{{ $t('providerDialog.add') }}
|
||||
</el-button>
|
||||
<el-button type="primary" @click="toggleSelectAllFields" size="small"
|
||||
style="background: #5f70f3; border: none; margin-left: 10px;">
|
||||
<el-button type="primary" @click="toggleSelectAllFields" size="small" class="btn-select-all">
|
||||
{{ isAllFieldsSelected ? $t('providerDialog.deselectAll') : $t('providerDialog.selectAll') }}
|
||||
</el-button>
|
||||
<el-button type="danger" @click="batchRemoveFields" size="small"
|
||||
style="background: red; border: none; margin-left: 10px;">
|
||||
<el-button type="danger" @click="batchRemoveFields" size="small" class="btn-batch-delete">
|
||||
{{ $t('providerDialog.batchDelete') }}
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
<div style="height: 2px; background: #e9e9e9; margin-bottom: 22px;"></div>
|
||||
<div class="divider"></div>
|
||||
|
||||
<div class="fields-container">
|
||||
<el-table :data="form.fields" style="width: 100%;" border size="medium" :key="tableKey">
|
||||
@@ -122,26 +120,27 @@
|
||||
</div>
|
||||
</el-form>
|
||||
</div>
|
||||
|
||||
<div style="display: flex; justify-content: center;">
|
||||
<el-button type="primary" @click="submit" class="save-btn" :loading="saving">{{ $t('providerDialog.save') }}</el-button>
|
||||
</div>
|
||||
</el-dialog>
|
||||
</CustomDialog>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import CustomDialog from './CustomDialog.vue';
|
||||
export default {
|
||||
name: 'ProviderDialog',
|
||||
props: {
|
||||
title: String,
|
||||
visible: Boolean,
|
||||
form: Object,
|
||||
modelTypes: Array
|
||||
},
|
||||
components: {
|
||||
CustomDialog
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
saving: false,
|
||||
isAllFieldsSelected: false,
|
||||
tableKey: 0 // 用于强制表格重新渲染
|
||||
tableKey: 0
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
@@ -199,16 +198,8 @@ export default {
|
||||
}));
|
||||
},
|
||||
|
||||
handleVisibleChange(val) {
|
||||
this.$emit('update:visible', val);
|
||||
if (!val) {
|
||||
this.resetForm();
|
||||
}
|
||||
},
|
||||
|
||||
handleClose() {
|
||||
this.resetForm();
|
||||
this.$emit('update:visible', false);
|
||||
this.$emit('cancel');
|
||||
},
|
||||
|
||||
@@ -288,7 +279,7 @@ export default {
|
||||
},
|
||||
|
||||
forceTableRerender() {
|
||||
this.tableKey += 1; // 改变key值强制表格重新渲染
|
||||
this.tableKey += 1;
|
||||
},
|
||||
|
||||
submit() {
|
||||
@@ -331,8 +322,7 @@ export default {
|
||||
}
|
||||
this.isAllFieldsSelected = false;
|
||||
this.forceTableRerender();
|
||||
},
|
||||
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
visible(val) {
|
||||
@@ -344,97 +334,40 @@ export default {
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
::v-deep .custom-dialog.el-dialog {
|
||||
margin-top: 0 !important;
|
||||
border-radius: 20px !important;
|
||||
}
|
||||
|
||||
::v-deep .custom-dialog .el-dialog__header {
|
||||
padding: 0;
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.custom-close-btn {
|
||||
position: absolute;
|
||||
top: 20px;
|
||||
right: 20px;
|
||||
width: 35px;
|
||||
height: 35px;
|
||||
border-radius: 50%;
|
||||
border: 2px solid #cfcfcf;
|
||||
background: none;
|
||||
font-size: 30px;
|
||||
font-weight: lighter;
|
||||
color: #cfcfcf;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
z-index: 1;
|
||||
padding: 0;
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.custom-close-btn:hover {
|
||||
color: #409EFF;
|
||||
border-color: #409EFF;
|
||||
}
|
||||
|
||||
.custom-form .el-form-item {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.custom-form .el-form-item__label {
|
||||
color: #3d4566;
|
||||
font-weight: normal;
|
||||
text-align: right;
|
||||
padding-right: 20px;
|
||||
}
|
||||
|
||||
.custom-input-bg .el-input__inner {
|
||||
background-color: #f6f8fc;
|
||||
height: 32px;
|
||||
}
|
||||
|
||||
.custom-input-bg .el-input__inner::-webkit-input-placeholder {
|
||||
color: #9c9f9e;
|
||||
}
|
||||
|
||||
.fields-container {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.save-btn {
|
||||
background: #e6f0fd;
|
||||
color: #237ff4;
|
||||
border: 1px solid #b3d1ff;
|
||||
width: 150px;
|
||||
height: 40px;
|
||||
font-size: 16px;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.save-btn:hover {
|
||||
background: linear-gradient(to right, #237ff4, #9c40d5);
|
||||
color: white;
|
||||
border: none;
|
||||
}
|
||||
|
||||
.el-table {
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.el-table::before {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.el-table th,
|
||||
.el-table td {
|
||||
padding: 8px 0;
|
||||
}
|
||||
|
||||
.el-button.is-circle {
|
||||
border-radius: 2px;
|
||||
<style scoped lang="scss">
|
||||
.provider-dialog-wrapper {
|
||||
.provider-form {
|
||||
.form-row {
|
||||
display: flex;
|
||||
gap: 20px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
.form-item {
|
||||
flex: 1;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
.custom-select {
|
||||
width: 100%;
|
||||
}
|
||||
.custom-input-number {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
.field-config-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 15px;
|
||||
.field-config-title {
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
color: #3d4566;
|
||||
}
|
||||
}
|
||||
.divider {
|
||||
height: 2px;
|
||||
background: #e9e9e9;
|
||||
margin-bottom: 22px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -1,76 +1,67 @@
|
||||
<template>
|
||||
<el-dialog :title="title" :visible.sync="visible" width="700px" class="param-dialog-wrapper" :append-to-body="true"
|
||||
:close-on-click-modal="false" :key="dialogKey" custom-class="custom-param-dialog" :show-close="false">
|
||||
<div class="dialog-container">
|
||||
<div class="dialog-header">
|
||||
<h2 class="dialog-title">{{ title }}</h2>
|
||||
<button class="custom-close-btn" @click="cancel">
|
||||
<svg width="14" height="14" viewBox="0 0 14 14" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M13 1L1 13M1 1L13 13" stroke="currentColor" stroke-width="2" stroke-linecap="round" />
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
<CustomDialog
|
||||
:title="title"
|
||||
:visible.sync="visible"
|
||||
width="700px"
|
||||
class="param-dialog-wrapper"
|
||||
@confirm="submit"
|
||||
@close="cancel"
|
||||
:confirmLoading="saving"
|
||||
>
|
||||
<el-form :model="localForm" :rules="rules" ref="form" label-width="auto" label-position="left" class="param-form">
|
||||
<el-form-item :label="$t('replacementDialog.fileName')" prop="fileName">
|
||||
<el-input v-model="localForm.fileName" :placeholder="$t('replacementDialog.fileNamePlaceholder')"
|
||||
@input="clearFieldError('fileName')"></el-input>
|
||||
</el-form-item>
|
||||
|
||||
<el-form :model="localForm" :rules="rules" ref="form" label-width="auto" label-position="left" class="param-form">
|
||||
<el-form-item :label="$t('replacementDialog.fileName')" prop="fileName" class="form-item">
|
||||
<el-input v-model="localForm.fileName" :placeholder="$t('replacementDialog.fileNamePlaceholder')"
|
||||
class="custom-input" @input="clearFieldError('fileName')"></el-input>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item :label="$t('replacementDialog.content')" prop="content" class="form-item content-item">
|
||||
<div class="content-wrapper">
|
||||
<el-input
|
||||
type="textarea"
|
||||
v-model="localForm.content"
|
||||
:placeholder="$t('replacementDialog.contentPlaceholder')"
|
||||
:rows="8"
|
||||
class="custom-textarea"
|
||||
@input="clearFieldError('content')"
|
||||
></el-input>
|
||||
<p class="format-tip">{{ $t('replacementDialog.formatTip') }}</p>
|
||||
<div class="upload-section">
|
||||
<el-upload
|
||||
class="upload-btn"
|
||||
action=""
|
||||
:auto-upload="false"
|
||||
:show-file-list="false"
|
||||
accept=".txt"
|
||||
:on-change="handleFileChange"
|
||||
>
|
||||
<el-button size="small" type="primary" class="upload-file-btn">
|
||||
<div class="upload-file-content">
|
||||
<svg width="14" height="14" viewBox="0 0 14 14" fill="none" xmlns="http://www.w3.org/2000/svg" style="margin-right: 4px;">
|
||||
<path d="M7 1V13M1 7H13" stroke="currentColor" stroke-width="2" stroke-linecap="round"/>
|
||||
</svg>
|
||||
<div>
|
||||
<p>{{ $t('replacementDialog.clickUploadTip') }}</p>
|
||||
<p>{{ $t('replacementDialog.uploadCoverTip') }}</p>
|
||||
</div>
|
||||
<el-form-item :label="$t('replacementDialog.content')" prop="content" class="content-item">
|
||||
<div class="content-wrapper">
|
||||
<el-input
|
||||
type="textarea"
|
||||
v-model="localForm.content"
|
||||
:placeholder="$t('replacementDialog.contentPlaceholder')"
|
||||
:rows="8"
|
||||
@input="clearFieldError('content')"
|
||||
></el-input>
|
||||
<p class="format-tip">{{ $t('replacementDialog.formatTip') }}</p>
|
||||
<div class="upload-section">
|
||||
<el-upload
|
||||
class="upload-btn"
|
||||
action=""
|
||||
:auto-upload="false"
|
||||
:show-file-list="false"
|
||||
accept=".txt"
|
||||
:on-change="handleFileChange"
|
||||
>
|
||||
<el-button size="small" type="primary" class="upload-file-btn">
|
||||
<div class="upload-file-content">
|
||||
<svg width="14" height="14" viewBox="0 0 14 14" fill="none" xmlns="http://www.w3.org/2000/svg" style="margin-right: 4px;">
|
||||
<path d="M7 1V13M1 7H13" stroke="currentColor" stroke-width="2" stroke-linecap="round"/>
|
||||
</svg>
|
||||
<div>
|
||||
<p>{{ $t('replacementDialog.clickUploadTip') }}</p>
|
||||
<p>{{ $t('replacementDialog.uploadCoverTip') }}</p>
|
||||
</div>
|
||||
</el-button>
|
||||
</el-upload>
|
||||
<span class="word-count" :class="{ 'over-limit': isOverLimit }">
|
||||
{{ wordCountText }}{{ $t('replacementDialog.wordCountUnit') }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</el-button>
|
||||
</el-upload>
|
||||
<span class="word-count" :class="{ 'over-limit': isOverLimit }">
|
||||
{{ wordCountText }}{{ $t('replacementDialog.wordCountUnit') }}
|
||||
</span>
|
||||
</div>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
|
||||
<div class="dialog-footer">
|
||||
<el-button type="primary" @click="submit" class="save-btn" :loading="saving" :disabled="saving">
|
||||
{{ $t('replacementDialog.save') }}
|
||||
</el-button>
|
||||
<el-button @click="cancel" class="cancel-btn">
|
||||
{{ $t('replacementDialog.cancel') }}
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</CustomDialog>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import CustomDialog from './CustomDialog.vue';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
CustomDialog
|
||||
},
|
||||
props: {
|
||||
title: {
|
||||
type: String,
|
||||
@@ -270,155 +261,21 @@ export default {
|
||||
};
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.custom-param-dialog {
|
||||
border-radius: 16px !important;
|
||||
overflow: hidden;
|
||||
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.15) !important;
|
||||
border: none !important;
|
||||
|
||||
.el-dialog__header {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.el-dialog__body {
|
||||
padding: 0 !important;
|
||||
border-radius: 16px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.param-dialog-wrapper {
|
||||
.dialog-container {
|
||||
padding: 24px 32px;
|
||||
background: linear-gradient(135deg, #f8fafc 0%, #f1f5f9 100%);
|
||||
}
|
||||
|
||||
.dialog-header {
|
||||
position: relative;
|
||||
margin-bottom: 24px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.dialog-title {
|
||||
font-size: 20px;
|
||||
color: #1e293b;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
font-weight: 600;
|
||||
letter-spacing: 0.5px;
|
||||
}
|
||||
|
||||
.custom-close-btn {
|
||||
position: absolute;
|
||||
top: -8px;
|
||||
right: -8px;
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
border-radius: 50%;
|
||||
border: none;
|
||||
background: #f1f5f9;
|
||||
color: #64748b;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 0;
|
||||
outline: none;
|
||||
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
||||
|
||||
&:hover {
|
||||
color: #ffffff;
|
||||
background: #ef4444;
|
||||
transform: rotate(90deg);
|
||||
box-shadow: 0 4px 6px rgba(239, 68, 68, 0.2);
|
||||
}
|
||||
|
||||
svg {
|
||||
transition: transform 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
}
|
||||
}
|
||||
|
||||
.param-form {
|
||||
.form-item {
|
||||
margin-bottom: 20px;
|
||||
|
||||
:deep(.el-form-item__label) {
|
||||
color: #475569;
|
||||
font-weight: 500;
|
||||
padding-right: 12px;
|
||||
text-align: right;
|
||||
font-size: 14px;
|
||||
letter-spacing: 0.2px;
|
||||
}
|
||||
}
|
||||
|
||||
.content-item {
|
||||
:deep(.el-form-item__content) {
|
||||
line-height: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.custom-input {
|
||||
:deep(.el-input__inner) {
|
||||
background-color: #ffffff;
|
||||
border-radius: 8px;
|
||||
border: 1px solid #e2e8f0;
|
||||
height: 42px;
|
||||
padding: 0 14px;
|
||||
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
font-size: 14px;
|
||||
color: #334155;
|
||||
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.05);
|
||||
|
||||
&:focus {
|
||||
border-color: #3b82f6;
|
||||
box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.2);
|
||||
background-color: #ffffff;
|
||||
}
|
||||
|
||||
&::placeholder {
|
||||
color: #94a3b8;
|
||||
font-weight: 400;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.content-wrapper {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.custom-textarea {
|
||||
:deep(.el-textarea__inner) {
|
||||
background-color: #ffffff;
|
||||
border-radius: 8px;
|
||||
border: 1px solid #e2e8f0;
|
||||
padding: 12px 14px;
|
||||
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
font-size: 14px;
|
||||
color: #334155;
|
||||
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.05);
|
||||
line-height: 1.8;
|
||||
resize: none;
|
||||
white-space: pre;
|
||||
overflow-x: auto;
|
||||
word-wrap: normal;
|
||||
|
||||
&:focus {
|
||||
border-color: #3b82f6;
|
||||
box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.2);
|
||||
background-color: #ffffff;
|
||||
}
|
||||
|
||||
&::placeholder {
|
||||
color: #94a3b8;
|
||||
font-weight: 400;
|
||||
}
|
||||
}
|
||||
}
|
||||
.format-tip {
|
||||
text-align: left;
|
||||
font-size: 12px;
|
||||
color: #5778ff;
|
||||
}
|
||||
@@ -472,65 +329,5 @@ export default {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.dialog-footer {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
padding: 16px 0 0;
|
||||
margin-top: 16px;
|
||||
|
||||
.save-btn {
|
||||
width: 120px;
|
||||
height: 42px;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
border-radius: 8px;
|
||||
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
background: #3b82f6;
|
||||
color: white;
|
||||
border: none;
|
||||
letter-spacing: 0.5px;
|
||||
box-shadow: 0 2px 4px rgba(59, 130, 246, 0.2);
|
||||
|
||||
&:hover {
|
||||
background: #2563eb;
|
||||
transform: translateY(-1px);
|
||||
box-shadow: 0 4px 6px rgba(59, 130, 246, 0.3);
|
||||
}
|
||||
|
||||
&:active {
|
||||
transform: translateY(0);
|
||||
box-shadow: 0 2px 3px rgba(59, 130, 246, 0.2);
|
||||
}
|
||||
}
|
||||
|
||||
.cancel-btn {
|
||||
width: 120px;
|
||||
height: 42px;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
border-radius: 8px;
|
||||
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
background: #ffffff;
|
||||
color: #64748b;
|
||||
border: 1px solid #e2e8f0;
|
||||
margin-left: 16px;
|
||||
letter-spacing: 0.5px;
|
||||
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.05);
|
||||
|
||||
&:hover {
|
||||
background: #f8fafc;
|
||||
color: #475569;
|
||||
border-color: #cbd5e1;
|
||||
transform: translateY(-1px);
|
||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
&:active {
|
||||
transform: translateY(0);
|
||||
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.05);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,9 +1,7 @@
|
||||
<template>
|
||||
<el-dialog :visible.sync="localVisible" width="90%" @close="handleClose" :show-close="false" :append-to-body="true"
|
||||
:close-on-click-modal="true">
|
||||
<button class="custom-close-btn" @click="handleClose">
|
||||
×
|
||||
</button>
|
||||
<CustomDialog :visible.sync="localVisible" :title="$t('modelConfig.voiceManagement')" width="90%"
|
||||
:close-on-click-modal="true" :destroy-on-close="false" :footer="false" :append-to-body="true"
|
||||
@close="handleClose">
|
||||
<div class="scroll-wrapper">
|
||||
<div class="table-container" ref="tableContainer" @scroll="handleScroll">
|
||||
<el-table v-loading="loading" :data="filteredTtsModels" style="width: 100%;" class="data-table"
|
||||
@@ -92,25 +90,27 @@
|
||||
</div>
|
||||
</div>
|
||||
<div class="action-buttons">
|
||||
<el-button type="primary" size="mini" @click="toggleSelectAll" style="background: #606ff3;border: None">
|
||||
<CustomButton :icon="selectAll ? 'el-icon-circle-close' : 'el-icon-circle-check'" size="small" type="default" @click="toggleSelectAll">
|
||||
{{ selectAll ? $t('ttsModel.deselectAll') : $t('ttsModel.selectAll') }}
|
||||
</el-button>
|
||||
<el-button type="primary" size="mini" @click="addNew" style="background: #5bc98c;border: None;">
|
||||
</CustomButton>
|
||||
<CustomButton icon="el-icon-plus" size="small" type="add" @click="addNew">
|
||||
{{ $t('ttsModel.add') }}
|
||||
</el-button>
|
||||
<el-button type="primary" size="mini" @click="deleteRow(filteredTtsModels.filter(row => row.selected))"
|
||||
style="background: red;border:None">{{ $t('ttsModel.delete') }}
|
||||
</el-button>
|
||||
</CustomButton>
|
||||
<CustomButton icon="el-icon-delete" size="small" type="delete" @click="deleteRow(filteredTtsModels.filter(row => row.selected))">
|
||||
{{ $t('ttsModel.delete') }}
|
||||
</CustomButton>
|
||||
</div>
|
||||
</el-dialog>
|
||||
</CustomDialog>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import Api from "@/apis/api";
|
||||
import AudioPlayer from './AudioPlayer.vue';
|
||||
import CustomDialog from './CustomDialog.vue';
|
||||
import CustomButton from './CustomButton.vue';
|
||||
|
||||
export default {
|
||||
components: { AudioPlayer },
|
||||
components: { AudioPlayer, CustomDialog, CustomButton },
|
||||
props: {
|
||||
visible: {
|
||||
type: Boolean,
|
||||
@@ -532,18 +532,6 @@ export default {
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
::v-deep .el-dialog {
|
||||
border-radius: 8px !important;
|
||||
overflow: hidden;
|
||||
top: 1vh !important;
|
||||
}
|
||||
|
||||
::v-deep .el-dialog__header {
|
||||
display: none !important;
|
||||
padding: 0 !important;
|
||||
margin: 0 !important;
|
||||
}
|
||||
|
||||
/* 表格样式 */
|
||||
::v-deep .data-table .el-table__header th {
|
||||
color: black;
|
||||
@@ -570,33 +558,6 @@ export default {
|
||||
border: none !important;
|
||||
}
|
||||
|
||||
/* 关闭按钮 */
|
||||
.custom-close-btn {
|
||||
position: absolute;
|
||||
top: 15px;
|
||||
right: 15px;
|
||||
width: 30px;
|
||||
height: 30px;
|
||||
border-radius: 50%;
|
||||
border: 2px solid #cfcfcf;
|
||||
background: none;
|
||||
font-size: 30px;
|
||||
font-weight: lighter;
|
||||
color: #cfcfcf;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
z-index: 1;
|
||||
padding: 0;
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.custom-close-btn:hover {
|
||||
color: #409EFF;
|
||||
border-color: #409EFF;
|
||||
}
|
||||
|
||||
/* 备注文本 */
|
||||
::v-deep .remark-input .el-textarea__inner {
|
||||
border-radius: 4px;
|
||||
@@ -691,11 +652,6 @@ export default {
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.action-buttons .el-button {
|
||||
padding: 8px 15px;
|
||||
font-size: 11px;
|
||||
}
|
||||
|
||||
.edit-btn,
|
||||
.delete-btn,
|
||||
.save-btn {
|
||||
@@ -727,9 +683,8 @@ export default {
|
||||
|
||||
/* 按钮组定位调整 */
|
||||
.action-buttons {
|
||||
position: static;
|
||||
padding: 15px 0;
|
||||
background: white;
|
||||
padding-top: 10px;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
/* 输入框自适应 */
|
||||
|
||||
@@ -1,6 +1,11 @@
|
||||
<template>
|
||||
<el-dialog :title="$t('voiceClone.dialogTitle')" :visible.sync="visible" width="900px" top="10vh"
|
||||
:before-close="handleClose" class="voice-clone-dialog">
|
||||
<CustomDialog
|
||||
:title="$t('voiceClone.dialogTitle')"
|
||||
:visible.sync="innerVisible"
|
||||
width="900px"
|
||||
:footer="false"
|
||||
@close="handleDialogClose"
|
||||
>
|
||||
<div class="dialog-content">
|
||||
<!-- 步骤指示器 -->
|
||||
<div class="steps-header">
|
||||
@@ -73,22 +78,28 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<span slot="footer" class="dialog-footer">
|
||||
<el-button @click="handleClose">{{ currentStep === 1 ? $t('voiceClone.cancel') : $t('voiceClone.prevStep')
|
||||
}}</el-button>
|
||||
<el-button type="primary" @click="handleNext" :loading="uploading">
|
||||
<div class="custom-footer">
|
||||
<CustomButton @click="handleCancel">
|
||||
{{ currentStep === 1 ? $t('voiceClone.cancel') : $t('voiceClone.prevStep') }}
|
||||
</CustomButton>
|
||||
<CustomButton type="confirm" :loading="uploading" @click="handleNext">
|
||||
{{ currentStep === 1 ? $t('voiceClone.nextStep') : $t('voiceClone.upload') }}
|
||||
</el-button>
|
||||
</span>
|
||||
</el-dialog>
|
||||
</CustomButton>
|
||||
</div>
|
||||
</CustomDialog>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import Api from "@/apis/api";
|
||||
import CustomDialog from './CustomDialog.vue';
|
||||
import CustomButton from './CustomButton.vue';
|
||||
|
||||
export default {
|
||||
name: 'VoiceCloneDialog',
|
||||
components: {
|
||||
CustomDialog,
|
||||
CustomButton
|
||||
},
|
||||
props: {
|
||||
visible: {
|
||||
type: Boolean,
|
||||
@@ -122,6 +133,14 @@ export default {
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
innerVisible: {
|
||||
get() {
|
||||
return this.visible;
|
||||
},
|
||||
set(val) {
|
||||
this.$emit('update:visible', val);
|
||||
}
|
||||
},
|
||||
selectedDuration() {
|
||||
if (this.selectionStart !== null && this.selectionEnd !== null && this.audioBuffer) {
|
||||
const duration = this.audioBuffer.duration;
|
||||
@@ -146,14 +165,16 @@ export default {
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
handleClose() {
|
||||
handleCancel() {
|
||||
if (this.currentStep === 2) {
|
||||
this.currentStep = 1;
|
||||
} else {
|
||||
this.resetDialog();
|
||||
this.$emit('update:visible', false);
|
||||
}
|
||||
},
|
||||
handleDialogClose() {
|
||||
this.resetDialog();
|
||||
},
|
||||
resetDialog() {
|
||||
this.currentStep = 1;
|
||||
this.audioFile = null;
|
||||
@@ -509,13 +530,6 @@ export default {
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.voice-clone-dialog {
|
||||
::v-deep .el-dialog__body {
|
||||
padding: 20px 30px;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
.steps-header {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
@@ -578,7 +592,6 @@ export default {
|
||||
background: #e4e7ed;
|
||||
margin-left: 12px;
|
||||
}
|
||||
|
||||
.step-content {
|
||||
padding: 20px 0;
|
||||
}
|
||||
@@ -685,10 +698,7 @@ export default {
|
||||
gap: 12px;
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
.dialog-footer {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: 10px;
|
||||
.custom-footer {
|
||||
text-align: right;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,55 +1,43 @@
|
||||
<template>
|
||||
<el-dialog :title="title" :visible.sync="visible" width="520px" class="param-dialog-wrapper" :append-to-body="true"
|
||||
:close-on-click-modal="false" :key="dialogKey" custom-class="custom-param-dialog" :show-close="false">
|
||||
<div class="dialog-container">
|
||||
<div class="dialog-header">
|
||||
<h2 class="dialog-title">{{ title }}</h2>
|
||||
<button class="custom-close-btn" @click="cancel">
|
||||
<svg width="14" height="14" viewBox="0 0 14 14" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M13 1L1 13M1 1L13 13" stroke="currentColor" stroke-width="2" stroke-linecap="round" />
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
<CustomDialog
|
||||
:title="title"
|
||||
:visible.sync="innerVisible"
|
||||
:confirm-loading="saving"
|
||||
width="600px"
|
||||
@confirm="submit"
|
||||
@cancel="cancel"
|
||||
>
|
||||
<el-form :model="form" :rules="rules" ref="form" label-width="auto" label-position="left" class="param-form">
|
||||
<el-form-item :label="$t('voicePrintDialog.voicePrintVector')" prop="audioId" class="form-item">
|
||||
<el-select v-model="form.audioId" :placeholder="$t('voicePrintDialog.selectVoiceMessage')" class="custom-select">
|
||||
<el-option v-for="item in valueTypeOptions" :key="item.audioId" :label="item.content" :value="item.audioId">
|
||||
<span style="float: left">{{ item.content }}</span>
|
||||
<span style="float: right; color: #8492a6; font-size: 13px">
|
||||
<i :class="getAudioIconClass(item.audioId)" @click.stop="playAudio(item.audioId)"
|
||||
class="audio-icon"></i>
|
||||
</span>
|
||||
</el-option>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
|
||||
<el-form :model="form" :rules="rules" ref="form" label-width="auto" label-position="left" class="param-form">
|
||||
<el-form-item :label="$t('voicePrintDialog.voicePrintVector')" prop="audioId" class="form-item">
|
||||
<el-select v-model="form.audioId" :placeholder="$t('voicePrintDialog.selectVoiceMessage')" class="custom-select">
|
||||
<el-option v-for="item in valueTypeOptions" :key="item.audioId" :label="item.content" :value="item.audioId">
|
||||
<span style="float: left">{{ item.content }}</span>
|
||||
<span style="float: right; color: #8492a6; font-size: 13px">
|
||||
<i :class="getAudioIconClass(item.audioId)" @click.stop="playAudio(item.audioId)"
|
||||
class="audio-icon"></i>
|
||||
</span>
|
||||
</el-option>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item :label="$t('voicePrintDialog.name')" prop="sourceName" class="form-item">
|
||||
<el-input v-model="form.sourceName" :placeholder="$t('voicePrintDialog.enterName')" class="custom-input"></el-input>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item :label="$t('voicePrintDialog.name')" prop="sourceName" class="form-item">
|
||||
<el-input v-model="form.sourceName" :placeholder="$t('voicePrintDialog.enterName')" class="custom-input"></el-input>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item :label="$t('voicePrintDialog.description')" prop="introduce" class="form-item remark-item">
|
||||
<el-input type="textarea" v-model="form.introduce" :placeholder="$t('voicePrintDialog.enterDescription')" :rows="3" class="custom-textarea"
|
||||
maxlength="100" show-word-limit></el-input>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
|
||||
<div class="dialog-footer">
|
||||
<el-button type="primary" @click="submit" class="save-btn" :loading="saving" :disabled="saving">
|
||||
{{ $t('voicePrintDialog.save') }}
|
||||
</el-button>
|
||||
<el-button @click="cancel" class="cancel-btn">
|
||||
{{ $t('voicePrintDialog.cancel') }}
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</el-dialog>
|
||||
<el-form-item :label="$t('voicePrintDialog.description')" prop="introduce" class="form-item remark-item">
|
||||
<el-input type="textarea" v-model="form.introduce" :placeholder="$t('voicePrintDialog.enterDescription')" :rows="3" class="custom-textarea"
|
||||
maxlength="100" show-word-limit></el-input>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</CustomDialog>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import api from '@/apis/api';
|
||||
import CustomDialog from './CustomDialog.vue';
|
||||
|
||||
export default {
|
||||
components: { CustomDialog },
|
||||
props: {
|
||||
title: {
|
||||
type: String,
|
||||
@@ -74,7 +62,6 @@ export default {
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
dialogKey: Date.now(),
|
||||
saving: false,
|
||||
playingAudioId: null,
|
||||
audioElement: null,
|
||||
@@ -94,6 +81,16 @@ export default {
|
||||
}
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
innerVisible: {
|
||||
get() {
|
||||
return this.visible;
|
||||
},
|
||||
set(val) {
|
||||
this.$emit('update:visible', val);
|
||||
}
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
getAudioIconClass(audioId) {
|
||||
if (this.playingAudioId === audioId) {
|
||||
@@ -137,11 +134,11 @@ export default {
|
||||
submit() {
|
||||
this.$refs.form.validate((valid) => {
|
||||
if (valid) {
|
||||
this.saving = true; // 开始加载
|
||||
this.saving = true;
|
||||
this.$emit('submit', {
|
||||
form: this.form,
|
||||
done: () => {
|
||||
this.saving = false; // 加载完成
|
||||
this.saving = false;
|
||||
}
|
||||
});
|
||||
|
||||
@@ -152,14 +149,14 @@ export default {
|
||||
});
|
||||
},
|
||||
cancel() {
|
||||
this.saving = false; // 取消时重置状态
|
||||
this.dialogKey = Date.now();
|
||||
this.saving = false;
|
||||
this.$emit('cancel');
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
visible(newVal) {
|
||||
if (newVal) {
|
||||
this.saving = false;
|
||||
api.agent.getRecentlyFiftyByAgentId(this.agentId, ((data) => {
|
||||
this.valueTypeOptions = data.data.data.map(item => ({
|
||||
...item
|
||||
@@ -184,24 +181,6 @@ export default {
|
||||
};
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.custom-param-dialog {
|
||||
border-radius: 16px !important;
|
||||
overflow: hidden;
|
||||
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.15) !important;
|
||||
border: none !important;
|
||||
|
||||
.el-dialog__header {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.el-dialog__body {
|
||||
padding: 0 !important;
|
||||
border-radius: 16px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.audio-icon {
|
||||
font-size: 20px;
|
||||
@@ -210,211 +189,98 @@ export default {
|
||||
color: #1890ff;
|
||||
}
|
||||
|
||||
.param-dialog-wrapper {
|
||||
.dialog-container {
|
||||
padding: 24px 32px;
|
||||
background: linear-gradient(135deg, #f8fafc 0%, #f1f5f9 100%);
|
||||
}
|
||||
.param-form {
|
||||
.form-item {
|
||||
margin-bottom: 20px;
|
||||
|
||||
.dialog-header {
|
||||
position: relative;
|
||||
margin-bottom: 24px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.dialog-title {
|
||||
font-size: 20px;
|
||||
color: #1e293b;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
font-weight: 600;
|
||||
letter-spacing: 0.5px;
|
||||
}
|
||||
|
||||
.custom-close-btn {
|
||||
position: absolute;
|
||||
top: -8px;
|
||||
right: -8px;
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
border-radius: 50%;
|
||||
border: none;
|
||||
background: #f1f5f9;
|
||||
color: #64748b;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 0;
|
||||
outline: none;
|
||||
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
||||
|
||||
&:hover {
|
||||
color: #ffffff;
|
||||
background: #ef4444;
|
||||
transform: rotate(90deg);
|
||||
box-shadow: 0 4px 6px rgba(239, 68, 68, 0.2);
|
||||
}
|
||||
|
||||
svg {
|
||||
transition: transform 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
}
|
||||
}
|
||||
|
||||
.param-form {
|
||||
.form-item {
|
||||
margin-bottom: 20px;
|
||||
|
||||
:deep(.el-form-item__label) {
|
||||
color: #475569;
|
||||
font-weight: 500;
|
||||
padding-right: 12px;
|
||||
text-align: right;
|
||||
font-size: 14px;
|
||||
letter-spacing: 0.2px;
|
||||
}
|
||||
}
|
||||
|
||||
.custom-input {
|
||||
:deep(.el-input__inner) {
|
||||
background-color: #ffffff;
|
||||
border-radius: 8px;
|
||||
border: 1px solid #e2e8f0;
|
||||
height: 42px;
|
||||
padding: 0 14px;
|
||||
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
font-size: 14px;
|
||||
color: #334155;
|
||||
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.05);
|
||||
|
||||
&:focus {
|
||||
border-color: #3b82f6;
|
||||
box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.2);
|
||||
background-color: #ffffff;
|
||||
}
|
||||
|
||||
&::placeholder {
|
||||
color: #94a3b8;
|
||||
font-weight: 400;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.custom-select {
|
||||
width: 100%;
|
||||
|
||||
:deep(.el-input__inner) {
|
||||
background-color: #ffffff;
|
||||
border-radius: 8px;
|
||||
border: 1px solid #e2e8f0;
|
||||
height: 42px;
|
||||
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
font-size: 14px;
|
||||
color: #334155;
|
||||
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.05);
|
||||
|
||||
&:focus {
|
||||
border-color: #3b82f6;
|
||||
box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.2);
|
||||
background-color: #ffffff;
|
||||
}
|
||||
|
||||
&::placeholder {
|
||||
color: #94a3b8;
|
||||
font-weight: 400;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.custom-textarea {
|
||||
:deep(.el-textarea__inner) {
|
||||
background-color: #ffffff;
|
||||
border-radius: 8px;
|
||||
border: 1px solid #e2e8f0;
|
||||
padding: 12px 14px;
|
||||
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
font-size: 14px;
|
||||
color: #334155;
|
||||
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.05);
|
||||
line-height: 1.5;
|
||||
|
||||
&:focus {
|
||||
border-color: #3b82f6;
|
||||
box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.2);
|
||||
background-color: #ffffff;
|
||||
}
|
||||
|
||||
&::placeholder {
|
||||
color: #94a3b8;
|
||||
font-weight: 400;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.remark-item :deep(.el-form-item__label) {
|
||||
margin-top: -4px;
|
||||
}
|
||||
}
|
||||
|
||||
.dialog-footer {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
padding: 16px 0 0;
|
||||
margin-top: 16px;
|
||||
|
||||
.save-btn {
|
||||
width: 120px;
|
||||
height: 42px;
|
||||
font-size: 14px;
|
||||
:deep(.el-form-item__label) {
|
||||
color: #475569;
|
||||
font-weight: 500;
|
||||
border-radius: 8px;
|
||||
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
background: #3b82f6;
|
||||
color: white;
|
||||
border: none;
|
||||
letter-spacing: 0.5px;
|
||||
box-shadow: 0 2px 4px rgba(59, 130, 246, 0.2);
|
||||
|
||||
&:hover {
|
||||
background: #2563eb;
|
||||
transform: translateY(-1px);
|
||||
box-shadow: 0 4px 6px rgba(59, 130, 246, 0.3);
|
||||
}
|
||||
|
||||
&:active {
|
||||
transform: translateY(0);
|
||||
box-shadow: 0 2px 3px rgba(59, 130, 246, 0.2);
|
||||
}
|
||||
}
|
||||
|
||||
.cancel-btn {
|
||||
width: 120px;
|
||||
height: 42px;
|
||||
padding-right: 12px;
|
||||
text-align: right;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
letter-spacing: 0.2px;
|
||||
}
|
||||
}
|
||||
|
||||
.custom-input {
|
||||
:deep(.el-input__inner) {
|
||||
background-color: #ffffff;
|
||||
border-radius: 8px;
|
||||
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
background: #ffffff;
|
||||
color: #64748b;
|
||||
border: 1px solid #e2e8f0;
|
||||
margin-left: 16px;
|
||||
letter-spacing: 0.5px;
|
||||
height: 42px;
|
||||
padding: 0 14px;
|
||||
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
font-size: 14px;
|
||||
color: #334155;
|
||||
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.05);
|
||||
|
||||
&:hover {
|
||||
background: #f8fafc;
|
||||
color: #475569;
|
||||
border-color: #cbd5e1;
|
||||
transform: translateY(-1px);
|
||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
||||
&:focus {
|
||||
border-color: #3b82f6;
|
||||
box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.2);
|
||||
background-color: #ffffff;
|
||||
}
|
||||
|
||||
&:active {
|
||||
transform: translateY(0);
|
||||
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.05);
|
||||
&::placeholder {
|
||||
color: #94a3b8;
|
||||
font-weight: 400;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.custom-select {
|
||||
width: 100%;
|
||||
|
||||
:deep(.el-input__inner) {
|
||||
background-color: #ffffff;
|
||||
border-radius: 8px;
|
||||
border: 1px solid #e2e8f0;
|
||||
height: 42px;
|
||||
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
font-size: 14px;
|
||||
color: #334155;
|
||||
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.05);
|
||||
|
||||
&:focus {
|
||||
border-color: #3b82f6;
|
||||
box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.2);
|
||||
background-color: #ffffff;
|
||||
}
|
||||
|
||||
&::placeholder {
|
||||
color: #94a3b8;
|
||||
font-weight: 400;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.custom-textarea {
|
||||
:deep(.el-textarea__inner) {
|
||||
background-color: #ffffff;
|
||||
border-radius: 8px;
|
||||
border: 1px solid #e2e8f0;
|
||||
padding: 12px 14px;
|
||||
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
font-size: 14px;
|
||||
color: #334155;
|
||||
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.05);
|
||||
line-height: 1.5;
|
||||
|
||||
&:focus {
|
||||
border-color: #3b82f6;
|
||||
box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.2);
|
||||
background-color: #ffffff;
|
||||
}
|
||||
|
||||
&::placeholder {
|
||||
color: #94a3b8;
|
||||
font-weight: 400;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.remark-item :deep(.el-form-item__label) {
|
||||
margin-top: -4px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,7 +1,15 @@
|
||||
<template>
|
||||
<el-dialog :title="title" :visible.sync="dialogVisible" :close-on-click-modal="false" @close="handleClose"
|
||||
@open="handleOpen" width="500px">
|
||||
<el-form ref="form" :model="form" :rules="rules" label-width="120px">
|
||||
<CustomDialog
|
||||
:title="title"
|
||||
:visible.sync="dialogVisible"
|
||||
:close-on-click-modal="false"
|
||||
width="600px"
|
||||
@close="handleClose"
|
||||
@open="handleOpen"
|
||||
@cancel="handleCancel"
|
||||
@confirm="handleSubmit"
|
||||
>
|
||||
<el-form ref="form" :model="form" :rules="rules" label-width="auto">
|
||||
<el-form-item :label="$t('voiceClone.platformName')" prop="modelId">
|
||||
<el-select v-model="form.modelId" :placeholder="$t('voiceClone.platformNamePlaceholder')" filterable
|
||||
style="width: 100%" @change="handlePlatformChange">
|
||||
@@ -31,19 +39,16 @@
|
||||
</el-input>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
|
||||
<div slot="footer" class="dialog-footer">
|
||||
<el-button @click="handleCancel">{{ $t('voiceClone.operationCancelled') }}</el-button>
|
||||
<el-button type="primary" @click="handleSubmit">{{ $t('voiceClone.addNew') }}</el-button>
|
||||
</div>
|
||||
</el-dialog>
|
||||
</CustomDialog>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import Api from '@/apis/api';
|
||||
import CustomDialog from './CustomDialog.vue';
|
||||
|
||||
export default {
|
||||
name: 'VoiceCloneDialog',
|
||||
components: { CustomDialog },
|
||||
props: {
|
||||
visible: {
|
||||
type: Boolean,
|
||||
@@ -154,9 +159,3 @@ export default {
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
::v-deep .el-dialog {
|
||||
border-radius: 20px;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -352,6 +352,8 @@ export default {
|
||||
|
||||
// Replacement word management page related
|
||||
'replacementWordManagement.pageTitle': 'Ersetzungswortverwaltung',
|
||||
'replacementWordManagement.searchPlaceholder': 'Bitte geben Sie den Dateinamen zur Suche ein',
|
||||
'replacementWordManagement.search': 'Suchen',
|
||||
'replacementWordManagement.fileName': 'Dateiname',
|
||||
'replacementWordManagement.replacementWordCount': 'Anzahl der Ersetzungswörter',
|
||||
'replacementWordManagement.replacementWordContent': 'Ersetzungswortinhalt',
|
||||
@@ -591,6 +593,7 @@ export default {
|
||||
|
||||
// Dictionary management page text
|
||||
'dictManagement.pageTitle': 'Wörterbuchverwaltung',
|
||||
'dictManagement.dictTypeCategory': 'Wörterbuchkategorie',
|
||||
'dictManagement.searchPlaceholder': 'Bitte Wörterbuchwert-Label zur Suche eingeben',
|
||||
'dictManagement.search': 'Suchen',
|
||||
'dictManagement.dictTypeName': 'WDT-Name',
|
||||
@@ -645,6 +648,8 @@ export default {
|
||||
'user.disable': 'Deaktivieren',
|
||||
'user.delete': 'Löschen',
|
||||
'user.selectUsersFirst': 'Bitte wählen Sie zuerst Benutzer zum Löschen aus',
|
||||
'user.selectUsersToDisable': 'Bitte wählen Sie Benutzer zum Deaktivieren aus',
|
||||
'user.selectUsersToEnable': 'Bitte wählen Sie Benutzer zum Aktivieren aus',
|
||||
'user.confirmDeleteSelected': 'Sind Sie sicher, dass Sie {count} ausgewählte Benutzer löschen möchten?',
|
||||
'user.deleting': 'Löscht...',
|
||||
'user.deleteSuccess': 'Erfolgreich {count} Benutzer gelöscht',
|
||||
@@ -704,6 +709,9 @@ export default {
|
||||
'common.cancel': 'Abbrechen',
|
||||
'common.sensitive': 'Sensibel',
|
||||
'common.loading': 'Lädt',
|
||||
'common.copySuccess': 'Erfolgreich kopiert',
|
||||
'common.copyFailed': 'Kopieren fehlgeschlagen',
|
||||
'common.copyAddress': 'Adresse kopieren',
|
||||
|
||||
// Language switch
|
||||
'language.zhCN': '中文简体',
|
||||
@@ -1116,6 +1124,8 @@ export default {
|
||||
'agentTemplateManagement.action': 'Aktion',
|
||||
'agentTemplateManagement.createTemplate': 'Vorlage erstellen',
|
||||
'templateQuickConfig.newTemplate': 'Neue Vorlage',
|
||||
'templateQuickConfig.editTemplate': 'Vorlage bearbeiten',
|
||||
'templateQuickConfig.addTemplate': 'Vorlage hinzufügen',
|
||||
'agentTemplateManagement.editTemplate': 'Vorlage bearbeiten',
|
||||
'agentTemplateManagement.deleteTemplate': 'Vorlage löschen',
|
||||
'agentTemplateManagement.deleteSuccess': 'Vorlage erfolgreich gelöscht',
|
||||
@@ -1195,6 +1205,8 @@ export default {
|
||||
'voiceClone.operationClosed': 'Popup geschlossen',
|
||||
'voiceClone.cloneSuccess': 'Klonen erfolgreich',
|
||||
'voiceClone.cloneFailed': 'Klonen fehlgeschlagen',
|
||||
'voiceClone.cloneErrorTip': 'Klonen fehlgeschlagen, bitte bewegen Sie die Maus über die Fehlermeldung für Details',
|
||||
'voiceClone.apiError': 'API-Aufruf Fehler',
|
||||
'voiceClone.confirmClone': 'Sind Sie sicher, dass Sie diese Stimme klonen möchten?',
|
||||
'voiceClone.onlySuccessCanClone': 'Nur erfolgreich trainierte Stimmen können geklont werden',
|
||||
'common.insufficient': 'Unzureichend',
|
||||
@@ -1212,6 +1224,7 @@ export default {
|
||||
'voiceClone.training': 'Training',
|
||||
'voiceClone.trainSuccess': 'Training erfolgreich',
|
||||
'voiceClone.trainFailed': 'Training fehlgeschlagen',
|
||||
'voiceClone.trainFailedWithError': 'Training fehlgeschlagen: {error}',
|
||||
'voiceClone.itemsPerPage': '{items} Einträge/Seite',
|
||||
'voiceClone.firstPage': 'Erste Seite',
|
||||
'voiceClone.prevPage': 'Vorherige Seite',
|
||||
@@ -1351,6 +1364,7 @@ export default {
|
||||
'knowledgeFileUpload.confirmParse': 'Sind Sie sicher, dass Sie dieses Dokument parsen möchten?',
|
||||
'knowledgeFileUpload.nameRequired': 'Bitte Dokumentnamen eingeben',
|
||||
'knowledgeFileUpload.fileRequired': 'Bitte wählen Sie eine Datei zum Hochladen aus',
|
||||
'knowledgeFileUpload.fileSizeExceeded': 'Dateigröße darf 10MB nicht überschreiten!',
|
||||
'knowledgeFileUpload.getListFailed': 'Dokumentenliste konnte nicht abgerufen werden',
|
||||
'knowledgeFileUpload.parseCancelled': 'Parsen abgebrochen',
|
||||
'knowledgeFileUpload.deleteCancelled': 'Löschen abgebrochen',
|
||||
@@ -1384,8 +1398,20 @@ export default {
|
||||
'knowledgeFileUpload.comprehensiveSimilarity': 'Umfassende Ähnlichkeit',
|
||||
'knowledgeFileUpload.content': 'Inhalt:',
|
||||
'knowledgeFileUpload.testQuestionRequired': 'Bitte Testfrage eingeben',
|
||||
'knowledgeFileUpload.sourceDocument': 'Quelldokument',
|
||||
'knowledgeFileUpload.parsing': 'Analysiere...',
|
||||
'knowledgeBaseDialog.descriptionRequired': 'Bitte Wissensbasis-Beschreibung eingeben',
|
||||
|
||||
// Knowledge Base Management page text
|
||||
'knowledgeBaseManagement.loading': 'Laden...',
|
||||
'knowledgeBaseManagement.noData': 'Keine Daten',
|
||||
'knowledgeBaseManagement.switchKnowledgeBase': 'Wissensbasis wechseln',
|
||||
'knowledgeBaseManagement.currentKnowledgeBaseDocuments': 'Aktuelle Wissensbasis-Dokumentassets',
|
||||
'knowledgeBaseManagement.enabled': 'Aktiviert',
|
||||
'knowledgeBaseManagement.disabled': 'Deaktiviert',
|
||||
'knowledgeBaseManagement.totalDocuments': 'Gesamt {total} Dokumente',
|
||||
'knowledgeBaseManagement.getListFailed': 'Liste konnte nicht abgerufen werden',
|
||||
|
||||
// Feature Management page text
|
||||
'featureManagement.selectAll': 'Alle auswählen',
|
||||
'featureManagement.deselectAll': 'Alle abwählen',
|
||||
@@ -1401,6 +1427,12 @@ export default {
|
||||
'featureManagement.cancel': 'Abbrechen',
|
||||
'featureManagement.resetSuccess': 'Funktionskonfiguration erfolgreich zurückgesetzt',
|
||||
'featureManagement.noChanges': 'Keine Änderungen zum Speichern',
|
||||
'featureManagement.saving': 'Speichern...',
|
||||
'featureManagement.groupName.featureManagement': 'Funktionsmodul-Schalter',
|
||||
'featureManagement.groupName.voiceManagement': 'Agent-Sichtbarkeitskonfiguration',
|
||||
'featureManagement.groupDescription.featureManagement': 'Aktivierungsstatus der Kernfunktionsmodule steuern',
|
||||
'featureManagement.groupDescription.voiceManagement': 'Konfigurieren, welche Funktionen für Agenten (KI-Assistenten) sichtbar und verfügbar sind',
|
||||
'featureManagement.moduleCount': '{count} Module insgesamt',
|
||||
|
||||
// Feature names and descriptions
|
||||
'feature.voiceprintRecognition.name': 'Stimmerkennung',
|
||||
@@ -1421,7 +1453,6 @@ export default {
|
||||
// Adressbuch Verwaltungsseite
|
||||
'addressBookManagement.title': 'Adressbuchverwaltung',
|
||||
'addressBookManagement.searchPlaceholder': 'Gerätename oder MAC-Adresse suchen',
|
||||
'addressBookManagement.search': 'Suchen',
|
||||
'addressBookManagement.selectAgent': 'Bitte Agent auswählen',
|
||||
'addressBookManagement.name': 'Adressbuchname',
|
||||
'addressBookManagement.description': 'Adressbuchbeschreibung',
|
||||
|
||||
@@ -352,6 +352,8 @@ export default {
|
||||
|
||||
// Replacement word management page related
|
||||
'replacementWordManagement.pageTitle': 'Replacement Word Management',
|
||||
'replacementWordManagement.searchPlaceholder': 'Please enter file name to search',
|
||||
'replacementWordManagement.search': 'Search',
|
||||
'replacementWordManagement.fileName': 'File Name',
|
||||
'replacementWordManagement.replacementWordCount': 'Replacement Word Count',
|
||||
'replacementWordManagement.replacementWordContent': 'Replacement Word Content',
|
||||
@@ -591,6 +593,7 @@ export default {
|
||||
|
||||
// Dictionary management page text
|
||||
'dictManagement.pageTitle': 'Dictionary Management',
|
||||
'dictManagement.dictTypeCategory': 'Dict Category',
|
||||
'dictManagement.searchPlaceholder': 'Please enter dict value label to search',
|
||||
'dictManagement.search': 'Search',
|
||||
'dictManagement.dictTypeName': 'Dict Type Name',
|
||||
@@ -645,6 +648,8 @@ export default {
|
||||
'user.disable': 'Disable',
|
||||
'user.delete': 'Delete',
|
||||
'user.selectUsersFirst': 'Please select users to delete first',
|
||||
'user.selectUsersToDisable': 'Please select users to disable',
|
||||
'user.selectUsersToEnable': 'Please select users to enable',
|
||||
'user.confirmDeleteSelected': 'Are you sure you want to delete {count} selected users?',
|
||||
'user.deleting': 'Deleting...',
|
||||
'user.deleteSuccess': 'Successfully deleted {count} users',
|
||||
@@ -704,6 +709,9 @@ export default {
|
||||
'common.cancel': 'Cancel',
|
||||
'common.sensitive': 'Sensitive',
|
||||
'common.loading': 'Loading',
|
||||
'common.copySuccess': 'Copy successful',
|
||||
'common.copyFailed': 'Copy failed',
|
||||
'common.copyAddress': 'Copy address',
|
||||
|
||||
// Language switch
|
||||
'language.zhCN': '中文简体',
|
||||
@@ -1150,6 +1158,8 @@ export default {
|
||||
'templateQuickConfig.cancel': 'Cancel',
|
||||
'templateQuickConfig.templateNotFound': 'Template not found',
|
||||
'templateQuickConfig.newTemplate': 'New Template',
|
||||
'templateQuickConfig.editTemplate': 'Edit Template',
|
||||
'templateQuickConfig.addTemplate': 'Add Template',
|
||||
'warning': 'Warning',
|
||||
'info': 'Info',
|
||||
'common.networkError': 'Network request failed',
|
||||
@@ -1195,6 +1205,8 @@ export default {
|
||||
'voiceClone.operationClosed': 'Popup closed',
|
||||
'voiceClone.cloneSuccess': 'Clone successful',
|
||||
'voiceClone.cloneFailed': 'Clone failed',
|
||||
'voiceClone.cloneErrorTip': 'Clone failed, please hover over the error tip to view error details',
|
||||
'voiceClone.apiError': 'API call error',
|
||||
'voiceClone.confirmClone': 'Are you sure you want to clone this voice?',
|
||||
'voiceClone.onlySuccessCanClone': 'Only successfully trained voices can be cloned',
|
||||
'common.insufficient': 'Insufficient',
|
||||
@@ -1212,6 +1224,7 @@ export default {
|
||||
'voiceClone.training': 'Training',
|
||||
'voiceClone.trainSuccess': 'Training successful',
|
||||
'voiceClone.trainFailed': 'Training failed',
|
||||
'voiceClone.trainFailedWithError': 'Training failed: {error}',
|
||||
'voiceClone.itemsPerPage': '{items} items/page',
|
||||
'voiceClone.firstPage': 'First Page',
|
||||
'voiceClone.prevPage': 'Previous Page',
|
||||
@@ -1351,6 +1364,7 @@ export default {
|
||||
'knowledgeFileUpload.confirmParse': 'Are you sure you want to parse this document?',
|
||||
'knowledgeFileUpload.nameRequired': 'Please enter document name',
|
||||
'knowledgeFileUpload.fileRequired': 'Please select a file to upload',
|
||||
'knowledgeFileUpload.fileSizeExceeded': 'File size cannot exceed 10MB!',
|
||||
'knowledgeFileUpload.getListFailed': 'Failed to get document list',
|
||||
'knowledgeFileUpload.parseCancelled': 'Parse cancelled',
|
||||
'knowledgeFileUpload.deleteCancelled': 'Delete cancelled',
|
||||
@@ -1382,10 +1396,22 @@ export default {
|
||||
'knowledgeFileUpload.testResult': 'Test Result:',
|
||||
'knowledgeFileUpload.noRelatedSlices': 'No related slices found',
|
||||
'knowledgeFileUpload.comprehensiveSimilarity': 'Comprehensive Similarity',
|
||||
'knowledgeFileUpload.sourceDocument': 'Source Document',
|
||||
'knowledgeFileUpload.content': 'Content:',
|
||||
'knowledgeFileUpload.testQuestionRequired': 'Please enter test question',
|
||||
'knowledgeFileUpload.parsing': 'Parsing...',
|
||||
'knowledgeBaseDialog.descriptionRequired': 'Please enter knowledge base description',
|
||||
|
||||
// Knowledge Base Management page text
|
||||
'knowledgeBaseManagement.loading': 'Loading...',
|
||||
'knowledgeBaseManagement.noData': 'No data',
|
||||
'knowledgeBaseManagement.switchKnowledgeBase': 'Switch Knowledge Base',
|
||||
'knowledgeBaseManagement.currentKnowledgeBaseDocuments': 'Current Knowledge Base Document Assets',
|
||||
'knowledgeBaseManagement.enabled': 'Enabled',
|
||||
'knowledgeBaseManagement.disabled': 'Disabled',
|
||||
'knowledgeBaseManagement.totalDocuments': 'Total {total} documents',
|
||||
'knowledgeBaseManagement.getListFailed': 'Failed to get list',
|
||||
|
||||
// Feature Management page text
|
||||
'featureManagement.selectAll': 'Select All',
|
||||
'featureManagement.deselectAll': 'Deselect All',
|
||||
@@ -1401,6 +1427,12 @@ export default {
|
||||
'featureManagement.cancel': 'Cancel',
|
||||
'featureManagement.resetSuccess': 'Feature configuration reset successfully',
|
||||
'featureManagement.noChanges': 'No changes to save',
|
||||
'featureManagement.saving': 'Saving...',
|
||||
'featureManagement.groupName.featureManagement': 'Feature Module Switch',
|
||||
'featureManagement.groupName.voiceManagement': 'Agent Visible Configuration',
|
||||
'featureManagement.groupDescription.featureManagement': 'Control the enable status of core feature modules',
|
||||
'featureManagement.groupDescription.voiceManagement': 'Configure which features are visible and available to agents (AI assistants)',
|
||||
'featureManagement.moduleCount': '{count} modules in total',
|
||||
|
||||
// Feature names and descriptions
|
||||
'feature.voiceprintRecognition.name': 'Voiceprint Recognition',
|
||||
@@ -1421,7 +1453,6 @@ export default {
|
||||
// Address Book Management page
|
||||
'addressBookManagement.title': 'Address Book Management',
|
||||
'addressBookManagement.searchPlaceholder': 'Search device name or MAC address',
|
||||
'addressBookManagement.search': 'Search',
|
||||
'addressBookManagement.selectAgent': 'Please select agent',
|
||||
'addressBookManagement.name': 'Address Book Name',
|
||||
'addressBookManagement.description': 'Address Book Description',
|
||||
|
||||
@@ -7,6 +7,14 @@ import de from './de';
|
||||
import vi from './vi';
|
||||
import ptBR from './pt_BR';
|
||||
|
||||
import enLocale from 'element-ui/lib/locale/lang/en'
|
||||
import zhLocale from 'element-ui/lib/locale/lang/zh-CN'
|
||||
import twLocale from 'element-ui/lib/locale/lang/zh-TW'
|
||||
import deLocale from 'element-ui/lib/locale/lang/de'
|
||||
import viLocale from 'element-ui/lib/locale/lang/vi'
|
||||
import ptBRLocale from 'element-ui/lib/locale/lang/pt-br'
|
||||
|
||||
|
||||
Vue.use(VueI18n);
|
||||
|
||||
// 从本地存储获取语言设置,如果没有则使用浏览器语言或默认语言
|
||||
@@ -38,12 +46,12 @@ const i18n = new VueI18n({
|
||||
locale: getDefaultLanguage(),
|
||||
fallbackLocale: 'zh_CN',
|
||||
messages: {
|
||||
'zh_CN': zhCN,
|
||||
'zh_TW': zhTW,
|
||||
'en': en,
|
||||
'de': de,
|
||||
'vi': vi,
|
||||
'pt_BR': ptBR
|
||||
'zh_CN': { ...zhLocale, ...zhCN },
|
||||
'zh_TW': { ...twLocale, ...zhTW },
|
||||
'en': { ...en, ...enLocale },
|
||||
'de': { ...de, ...deLocale },
|
||||
'vi': { ...vi, ...viLocale },
|
||||
'pt_BR': { ...ptBR, ...ptBRLocale }
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -352,6 +352,8 @@ export default {
|
||||
|
||||
// Replacement word management page related
|
||||
'replacementWordManagement.pageTitle': 'Gerenciamento de Palavras de Substituição',
|
||||
'replacementWordManagement.searchPlaceholder': 'Digite o nome do arquivo para pesquisar',
|
||||
'replacementWordManagement.search': 'Pesquisar',
|
||||
'replacementWordManagement.fileName': 'Nome do Arquivo',
|
||||
'replacementWordManagement.replacementWordCount': 'Quantidade de Palavras de Substituição',
|
||||
'replacementWordManagement.replacementWordContent': 'Conteúdo Substituto',
|
||||
@@ -591,6 +593,7 @@ export default {
|
||||
|
||||
// Página de gerenciamento de dicionário
|
||||
'dictManagement.pageTitle': 'Gerenciamento de Dicionário',
|
||||
'dictManagement.dictTypeCategory': 'Categoria do Dicionário',
|
||||
'dictManagement.searchPlaceholder': 'Por favor, insira o rótulo do dicionário para pesquisar',
|
||||
'dictManagement.search': 'Pesquisar',
|
||||
'dictManagement.dictTypeName': 'Nome do Tipo de Dicionário',
|
||||
@@ -645,6 +648,8 @@ export default {
|
||||
'user.disable': 'Desativar',
|
||||
'user.delete': 'Excluir',
|
||||
'user.selectUsersFirst': 'Por favor, selecione usuários para excluir primeiro',
|
||||
'user.selectUsersToDisable': 'Por favor, selecione usuários para desativar',
|
||||
'user.selectUsersToEnable': 'Por favor, selecione usuários para ativar',
|
||||
'user.confirmDeleteSelected': 'Tem certeza de que deseja excluir {count} usuários selecionados?',
|
||||
'user.deleting': 'Excluindo...',
|
||||
'user.deleteSuccess': '{count} usuários excluídos com sucesso',
|
||||
@@ -704,6 +709,9 @@ export default {
|
||||
'common.cancel': 'Cancelar',
|
||||
'common.sensitive': 'Sensível',
|
||||
'common.loading': 'Carregando',
|
||||
'common.copySuccess': 'Cópia bem-sucedida',
|
||||
'common.copyFailed': 'Falha ao copiar',
|
||||
'common.copyAddress': 'Copiar endereço',
|
||||
|
||||
// Troca de idioma
|
||||
'language.zhCN': '中文简体',
|
||||
@@ -1150,6 +1158,8 @@ export default {
|
||||
'templateQuickConfig.cancel': 'Cancelar',
|
||||
'templateQuickConfig.templateNotFound': 'Modelo não encontrado',
|
||||
'templateQuickConfig.newTemplate': 'Novo Modelo',
|
||||
'templateQuickConfig.editTemplate': 'Editar Modelo',
|
||||
'templateQuickConfig.addTemplate': 'Adicionar Modelo',
|
||||
'warning': 'Aviso',
|
||||
'info': 'Informação',
|
||||
'common.networkError': 'Falha na requisição de rede',
|
||||
@@ -1193,8 +1203,10 @@ export default {
|
||||
'voiceClone.selectFirst': 'Por favor, selecione recursos de voz para excluir primeiro',
|
||||
'voiceClone.operationCancelled': 'Cancelar',
|
||||
'voiceClone.operationClosed': 'Popup fechado',
|
||||
'voiceClone.cloneSuccess': 'Clonado com sucesso',
|
||||
'voiceClone.cloneSuccess': 'Clonagem bem-sucedida',
|
||||
'voiceClone.cloneFailed': 'Falha ao clonar',
|
||||
'voiceClone.cloneErrorTip': 'Falha na clonagem, passe o mouse sobre a dica de erro para ver os detalhes',
|
||||
'voiceClone.apiError': 'Erro na chamada da API',
|
||||
'voiceClone.confirmClone': 'Tem certeza de que deseja clonar esta voz?',
|
||||
'voiceClone.onlySuccessCanClone': 'Apenas vozes treinadas com sucesso podem ser clonadas',
|
||||
'common.insufficient': 'Insuficiente',
|
||||
@@ -1212,6 +1224,7 @@ export default {
|
||||
'voiceClone.training': 'Treinando',
|
||||
'voiceClone.trainSuccess': 'Treinamento bem-sucedido',
|
||||
'voiceClone.trainFailed': 'Treinamento falhou',
|
||||
'voiceClone.trainFailedWithError': 'Treinamento falhou: {error}',
|
||||
'voiceClone.itemsPerPage': '{items} itens/página',
|
||||
'voiceClone.firstPage': 'Primeira Página',
|
||||
'voiceClone.prevPage': 'Página Anterior',
|
||||
@@ -1351,6 +1364,7 @@ export default {
|
||||
'knowledgeFileUpload.confirmParse': 'Tem certeza de que deseja analisar este documento?',
|
||||
'knowledgeFileUpload.nameRequired': 'Por favor, insira o nome do documento',
|
||||
'knowledgeFileUpload.fileRequired': 'Por favor, selecione um arquivo para enviar',
|
||||
'knowledgeFileUpload.fileSizeExceeded': 'O tamanho do arquivo não pode exceder 10MB!',
|
||||
'knowledgeFileUpload.getListFailed': 'Falha ao obter lista de documentos',
|
||||
'knowledgeFileUpload.parseCancelled': 'Análise cancelada',
|
||||
'knowledgeFileUpload.deleteCancelled': 'Exclusão cancelada',
|
||||
@@ -1384,8 +1398,20 @@ export default {
|
||||
'knowledgeFileUpload.comprehensiveSimilarity': 'Similaridade Geral',
|
||||
'knowledgeFileUpload.content': 'Conteúdo:',
|
||||
'knowledgeFileUpload.testQuestionRequired': 'Por favor, insira a pergunta de teste',
|
||||
'knowledgeFileUpload.sourceDocument': 'Documento de origem',
|
||||
'knowledgeFileUpload.parsing': 'Analisando...',
|
||||
'knowledgeBaseDialog.descriptionRequired': 'Por favor, insira a descrição da base de conhecimento',
|
||||
|
||||
// Página de gerenciamento de base de conhecimento
|
||||
'knowledgeBaseManagement.loading': 'Carregando...',
|
||||
'knowledgeBaseManagement.noData': 'Sem dados',
|
||||
'knowledgeBaseManagement.switchKnowledgeBase': 'Alternar Base de Conhecimento',
|
||||
'knowledgeBaseManagement.currentKnowledgeBaseDocuments': 'Ativos de Documentos da Base de Conhecimento Atual',
|
||||
'knowledgeBaseManagement.enabled': 'Ativado',
|
||||
'knowledgeBaseManagement.disabled': 'Desativado',
|
||||
'knowledgeBaseManagement.totalDocuments': 'Total de {total} documentos',
|
||||
'knowledgeBaseManagement.getListFailed': 'Falha ao obter lista',
|
||||
|
||||
// Página de gerenciamento de funcionalidades
|
||||
'featureManagement.selectAll': 'Selecionar Tudo',
|
||||
'featureManagement.deselectAll': 'Desmarcar Tudo',
|
||||
@@ -1401,6 +1427,12 @@ export default {
|
||||
'featureManagement.cancel': 'Cancelar',
|
||||
'featureManagement.resetSuccess': 'Configuração de funcionalidades redefinida com sucesso',
|
||||
'featureManagement.noChanges': 'Nenhuma alteração para salvar',
|
||||
'featureManagement.saving': 'Salvando...',
|
||||
'featureManagement.groupName.featureManagement': 'Interruptor de Módulo de Funcionalidade',
|
||||
'featureManagement.groupName.voiceManagement': 'Configuração de Visibilidade do Agente',
|
||||
'featureManagement.groupDescription.featureManagement': 'Controlar o status de ativação dos módulos de funcionalidade principal',
|
||||
'featureManagement.groupDescription.voiceManagement': 'Configurar quais funcionalidades são visíveis e disponíveis para agentes (assistentes de IA)',
|
||||
'featureManagement.moduleCount': '{count} módulos no total',
|
||||
|
||||
// Nomes e descrições de funcionalidades
|
||||
'feature.voiceprintRecognition.name': 'Reconhecimento de Impressão Vocal',
|
||||
@@ -1421,7 +1453,6 @@ export default {
|
||||
// Página de Gerenciamento de Lista de Contatos
|
||||
'addressBookManagement.title': 'Gerenciamento de Lista de Contatos',
|
||||
'addressBookManagement.searchPlaceholder': 'Pesquisar nome do dispositivo ou endereço MAC',
|
||||
'addressBookManagement.search': 'Pesquisar',
|
||||
'addressBookManagement.selectAgent': 'Selecione um agente',
|
||||
'addressBookManagement.name': 'Nome da Lista de Contatos',
|
||||
'addressBookManagement.description': 'Descrição da Lista de Contatos',
|
||||
|
||||
@@ -352,6 +352,8 @@ export default {
|
||||
|
||||
// Replacement word management page related
|
||||
'replacementWordManagement.pageTitle': 'Quản lý từ thay thế',
|
||||
'replacementWordManagement.searchPlaceholder': 'Vui lòng nhập tên tệp để tìm kiếm',
|
||||
'replacementWordManagement.search': 'Tìm kiếm',
|
||||
'replacementWordManagement.fileName': 'Tên tệp',
|
||||
'replacementWordManagement.replacementWordCount': 'Số lượng từ thay thế',
|
||||
'replacementWordManagement.replacementWordContent': 'Nội dung từ thay thế',
|
||||
@@ -591,6 +593,7 @@ export default {
|
||||
|
||||
// Dictionary management page text
|
||||
'dictManagement.pageTitle': 'Quản lý từ điển',
|
||||
'dictManagement.dictTypeCategory': 'Danh mục từ điển',
|
||||
'dictManagement.searchPlaceholder': 'Vui lòng nhập nhãn giá trị từ điển để tìm kiếm',
|
||||
'dictManagement.search': 'Tìm kiếm',
|
||||
'dictManagement.dictTypeName': 'Tên loại từ điển',
|
||||
@@ -645,6 +648,8 @@ export default {
|
||||
'user.disable': 'Vô hiệu hóa',
|
||||
'user.delete': 'Xóa',
|
||||
'user.selectUsersFirst': 'Vui lòng chọn người dùng để xóa trước',
|
||||
'user.selectUsersToDisable': 'Vui lòng chọn người dùng để vô hiệu hóa',
|
||||
'user.selectUsersToEnable': 'Vui lòng chọn người dùng để kích hoạt',
|
||||
'user.confirmDeleteSelected': 'Bạn có chắc chắn muốn xóa {count} người dùng đã chọn?',
|
||||
'user.deleting': 'Đang xóa...',
|
||||
'user.deleteSuccess': 'Đã xóa thành công {count} người dùng',
|
||||
@@ -704,6 +709,9 @@ export default {
|
||||
'common.cancel': 'Hủy bỏ',
|
||||
'common.sensitive': 'Nhạy cảm',
|
||||
'common.loading': 'Đang tải',
|
||||
'common.copySuccess': 'Sao chép thành công',
|
||||
'common.copyFailed': 'Sao chép thất bại',
|
||||
'common.copyAddress': 'Sao chép địa chỉ',
|
||||
|
||||
// Language switch
|
||||
'language.zhCN': '中文简体',
|
||||
@@ -1116,6 +1124,8 @@ export default {
|
||||
'agentTemplateManagement.action': 'Hành động',
|
||||
'agentTemplateManagement.createTemplate': 'Tạo mẫu',
|
||||
'templateQuickConfig.newTemplate': 'Mẫu mới',
|
||||
'templateQuickConfig.editTemplate': 'Chỉnh sửa mẫu',
|
||||
'templateQuickConfig.addTemplate': 'Thêm mẫu',
|
||||
'agentTemplateManagement.editTemplate': 'Chỉnh sửa mẫu',
|
||||
'agentTemplateManagement.deleteTemplate': 'Xóa mẫu',
|
||||
'agentTemplateManagement.deleteSuccess': 'Đã xóa mẫu thành công',
|
||||
@@ -1195,6 +1205,8 @@ export default {
|
||||
'voiceClone.operationClosed': 'Đã đóng popup',
|
||||
'voiceClone.cloneSuccess': 'Nhân bản thành công',
|
||||
'voiceClone.cloneFailed': 'Nhân bản thất bại',
|
||||
'voiceClone.cloneErrorTip': 'Nhân bản thất bại, hãy di chuột qua thông báo lỗi để xem chi tiết',
|
||||
'voiceClone.apiError': 'Lỗi gọi API',
|
||||
'voiceClone.confirmClone': 'Bạn có chắc chắn muốn nhân bản giọng này?',
|
||||
'voiceClone.onlySuccessCanClone': 'Chỉ có thể nhân bản giọng đã huấn luyện thành công',
|
||||
'common.insufficient': 'Không đủ',
|
||||
@@ -1212,6 +1224,7 @@ export default {
|
||||
'voiceClone.training': 'Đang huấn luyện',
|
||||
'voiceClone.trainSuccess': 'Huấn luyện thành công',
|
||||
'voiceClone.trainFailed': 'Huấn luyện thất bại',
|
||||
'voiceClone.trainFailedWithError': 'Huấn luyện thất bại: {error}',
|
||||
'voiceClone.itemsPerPage': '{items} mục/trang',
|
||||
'voiceClone.firstPage': 'Trang đầu',
|
||||
'voiceClone.prevPage': 'Trang trước',
|
||||
@@ -1351,6 +1364,7 @@ export default {
|
||||
'knowledgeFileUpload.confirmParse': 'Bạn có chắc chắn muốn phân tích cú pháp tài liệu này?',
|
||||
'knowledgeFileUpload.nameRequired': 'Vui lòng nhập tên tài liệu',
|
||||
'knowledgeFileUpload.fileRequired': 'Vui lòng chọn tệp để tải lên',
|
||||
'knowledgeFileUpload.fileSizeExceeded': 'Kích thước tệp không được vượt quá 10MB!',
|
||||
'knowledgeFileUpload.getListFailed': 'Không thể lấy danh sách tài liệu',
|
||||
'knowledgeFileUpload.parseCancelled': 'Đã hủy phân tích cú pháp',
|
||||
'knowledgeFileUpload.deleteCancelled': 'Đã hủy xóa',
|
||||
@@ -1384,8 +1398,20 @@ export default {
|
||||
'knowledgeFileUpload.comprehensiveSimilarity': 'Độ tương đồng toàn diện',
|
||||
'knowledgeFileUpload.content': 'Nội dung:',
|
||||
'knowledgeFileUpload.testQuestionRequired': 'Vui lòng nhập câu hỏi kiểm tra',
|
||||
'knowledgeFileUpload.sourceDocument': 'Tài liệu nguồn',
|
||||
'knowledgeFileUpload.parsing': 'Đang phân tích...',
|
||||
'knowledgeBaseDialog.descriptionRequired': 'Vui lòng nhập mô tả cơ sở kiến thức',
|
||||
|
||||
// Knowledge Base Management page text
|
||||
'knowledgeBaseManagement.loading': 'Đang tải...',
|
||||
'knowledgeBaseManagement.noData': 'Không có dữ liệu',
|
||||
'knowledgeBaseManagement.switchKnowledgeBase': 'Chuyển đổi cơ sở kiến thức',
|
||||
'knowledgeBaseManagement.currentKnowledgeBaseDocuments': 'Tài sản tài liệu cơ sở kiến thức hiện tại',
|
||||
'knowledgeBaseManagement.enabled': 'Đã bật',
|
||||
'knowledgeBaseManagement.disabled': 'Đã tắt',
|
||||
'knowledgeBaseManagement.totalDocuments': 'Tổng {total} tài liệu',
|
||||
'knowledgeBaseManagement.getListFailed': 'Lấy danh sách thất bại',
|
||||
|
||||
// Feature Management page text
|
||||
'featureManagement.selectAll': 'Chọn tất cả',
|
||||
'featureManagement.deselectAll': 'Bỏ chọn tất cả',
|
||||
@@ -1401,6 +1427,12 @@ export default {
|
||||
'featureManagement.cancel': 'Hủy bỏ',
|
||||
'featureManagement.resetSuccess': 'Cấu hình tính năng đã được đặt lại thành công',
|
||||
'featureManagement.noChanges': 'Không có thay đổi nào để lưu',
|
||||
'featureManagement.saving': 'Đang lưu...',
|
||||
'featureManagement.groupName.featureManagement': 'Công tắc mô-đun tính năng',
|
||||
'featureManagement.groupName.voiceManagement': 'Cấu hình hiển thị Agent',
|
||||
'featureManagement.groupDescription.featureManagement': 'Kiểm soát trạng thái kích hoạt của các mô-đun tính năng cốt lõi',
|
||||
'featureManagement.groupDescription.voiceManagement': 'Cấu hình các tính năng hiển thị và khả dụng cho agent (trợ lý AI)',
|
||||
'featureManagement.moduleCount': 'Tổng cộng {count} mô-đun',
|
||||
|
||||
// Feature names and descriptions
|
||||
'feature.voiceprintRecognition.name': 'Nhận dạng giọng nói',
|
||||
@@ -1421,7 +1453,6 @@ export default {
|
||||
// Trang quản lý danh bạ
|
||||
'addressBookManagement.title': 'Quản lý danh bạ',
|
||||
'addressBookManagement.searchPlaceholder': 'Tìm kiếm tên thiết bị hoặc địa chỉ MAC',
|
||||
'addressBookManagement.search': 'Tìm kiếm',
|
||||
'addressBookManagement.selectAgent': 'Vui lòng chọn tác tử',
|
||||
'addressBookManagement.name': 'Tên danh bạ',
|
||||
'addressBookManagement.description': 'Mô tả danh bạ',
|
||||
|
||||
@@ -352,6 +352,8 @@ export default {
|
||||
|
||||
// 替换词对话框相关
|
||||
'replacementWordManagement.pageTitle': '替换词管理',
|
||||
'replacementWordManagement.searchPlaceholder': '请输入文件名称查询',
|
||||
'replacementWordManagement.search': '搜索',
|
||||
'replacementWordManagement.fileName': '文件名称',
|
||||
'replacementWordManagement.replacementWordCount': '替换词数量',
|
||||
'replacementWordManagement.replacementWordContent': '替换词内容',
|
||||
@@ -591,6 +593,7 @@ export default {
|
||||
|
||||
// 字典管理页面文本
|
||||
'dictManagement.pageTitle': '字典管理',
|
||||
'dictManagement.dictTypeCategory': '字典分类',
|
||||
'dictManagement.searchPlaceholder': '请输入字典值标签查询',
|
||||
'dictManagement.search': '搜索',
|
||||
'dictManagement.dictTypeName': '字典类型名称',
|
||||
@@ -645,6 +648,8 @@ export default {
|
||||
'user.disable': '禁用',
|
||||
'user.delete': '删除',
|
||||
'user.selectUsersFirst': '请先选择需要删除的用户',
|
||||
'user.selectUsersToDisable': '请选择要禁用的用户',
|
||||
'user.selectUsersToEnable': '请选择要启用的用户',
|
||||
'user.confirmDeleteSelected': '确定要删除选中的{count}个用户吗?',
|
||||
'user.deleting': '正在删除中...',
|
||||
'user.deleteSuccess': '成功删除{count}个用户',
|
||||
@@ -704,6 +709,9 @@ export default {
|
||||
'common.cancel': '取消',
|
||||
'common.sensitive': '敏感',
|
||||
'common.loading': '加载中',
|
||||
'common.copySuccess': '复制成功',
|
||||
'common.copyFailed': '复制失败',
|
||||
'common.copyAddress': '复制地址',
|
||||
|
||||
// 语言切换
|
||||
'language.zhCN': '中文简体',
|
||||
@@ -1150,6 +1158,8 @@ export default {
|
||||
'templateQuickConfig.cancel': '取消',
|
||||
'templateQuickConfig.templateNotFound': '未找到指定模板',
|
||||
'templateQuickConfig.newTemplate': '新模板',
|
||||
'templateQuickConfig.editTemplate': '编辑模板',
|
||||
'templateQuickConfig.addTemplate': '新增模板',
|
||||
'warning': '警告',
|
||||
'info': '提示',
|
||||
'common.networkError': '网络请求失败',
|
||||
@@ -1195,6 +1205,8 @@ export default {
|
||||
'voiceClone.action': '操作',
|
||||
'voiceClone.cloneSuccess': '复刻成功',
|
||||
'voiceClone.cloneFailed': '复刻失败',
|
||||
'voiceClone.cloneErrorTip': '克隆失败,请将鼠标悬停在错误提示上,查看错误详情',
|
||||
'voiceClone.apiError': '调用API时出错',
|
||||
'voiceClone.confirmClone': '确定要复刻此音色吗?',
|
||||
'voiceClone.onlySuccessCanClone': '只有训练成功的音色才能复刻',
|
||||
'common.insufficient': '不足',
|
||||
@@ -1212,6 +1224,7 @@ export default {
|
||||
'voiceClone.training': '训练中',
|
||||
'voiceClone.trainSuccess': '训练成功',
|
||||
'voiceClone.trainFailed': '训练失败',
|
||||
'voiceClone.trainFailedWithError': '训练失败:{error}',
|
||||
'voiceClone.itemsPerPage': '{items}条/页',
|
||||
'voiceClone.firstPage': '首页',
|
||||
'voiceClone.prevPage': '上一页',
|
||||
@@ -1347,10 +1360,11 @@ export default {
|
||||
'knowledgeFileUpload.parseFailed': '文档解析失败',
|
||||
'knowledgeFileUpload.deleteSuccess': '文档删除成功',
|
||||
'knowledgeFileUpload.deleteFailed': '文档删除失败',
|
||||
'knowledgeFileUpload.confirmDelete': '确定要删除此文檔吗?',
|
||||
'knowledgeFileUpload.confirmDelete': '确定要删除此文档吗?',
|
||||
'knowledgeFileUpload.confirmParse': '确定要解析该文档吗?',
|
||||
'knowledgeFileUpload.nameRequired': '请输入文档名称',
|
||||
'knowledgeFileUpload.fileRequired': '请选择要上传的文件',
|
||||
'knowledgeFileUpload.fileSizeExceeded': '文件大小不能超过10MB!',
|
||||
'knowledgeFileUpload.getListFailed': '获取文档列表失败',
|
||||
'knowledgeFileUpload.parseCancelled': '已取消解析',
|
||||
'knowledgeFileUpload.deleteCancelled': '已取消删除',
|
||||
@@ -1382,9 +1396,19 @@ export default {
|
||||
'knowledgeFileUpload.testResult': '测试结果:',
|
||||
'knowledgeFileUpload.noRelatedSlices': '未找到相关切片',
|
||||
'knowledgeFileUpload.comprehensiveSimilarity': '综合相似度',
|
||||
'knowledgeFileUpload.sourceDocument': '来源文档',
|
||||
'knowledgeFileUpload.content': '内容:',
|
||||
'knowledgeFileUpload.testQuestionRequired': '请输入测试问题',
|
||||
'knowledgeBaseDialog.descriptionRequired': '请输入知识库描述',
|
||||
'knowledgeFileUpload.parsing': '正在解析中...',
|
||||
'knowledgeBaseManagement.loading': '加载中...',
|
||||
'knowledgeBaseManagement.noData': '暂无数据',
|
||||
'knowledgeBaseManagement.switchKnowledgeBase': '切换知识库',
|
||||
'knowledgeBaseManagement.currentKnowledgeBaseDocuments': '当前知识库文档资产',
|
||||
'knowledgeBaseManagement.enabled': '已启用',
|
||||
'knowledgeBaseManagement.disabled': '已禁用',
|
||||
'knowledgeBaseManagement.totalDocuments': '共 {total} 个文档',
|
||||
'knowledgeBaseManagement.getListFailed': '获取列表失败',
|
||||
|
||||
// 系统功能配置页面文本
|
||||
'featureManagement.selectAll': '全选',
|
||||
@@ -1401,6 +1425,12 @@ export default {
|
||||
'featureManagement.cancel': '取消',
|
||||
'featureManagement.resetSuccess': '功能配置重置成功',
|
||||
'featureManagement.noChanges': '没有需要保存的更改',
|
||||
'featureManagement.saving': '保存中...',
|
||||
'featureManagement.groupName.featureManagement': '功能模块开关',
|
||||
'featureManagement.groupName.voiceManagement': '智能体可见配置',
|
||||
'featureManagement.groupDescription.featureManagement': '控制系统核心功能模块的启用状态',
|
||||
'featureManagement.groupDescription.voiceManagement': '配置哪些功能对智能体(AI助手)可见和可用',
|
||||
'featureManagement.moduleCount': '共 {count} 个模块',
|
||||
|
||||
// 功能名称和描述
|
||||
'feature.voiceprintRecognition.name': '声纹识别',
|
||||
@@ -1421,7 +1451,6 @@ export default {
|
||||
// 通讯录管理页面
|
||||
'addressBookManagement.title': '通讯录管理',
|
||||
'addressBookManagement.searchPlaceholder': '搜索设备名称或MAC地址',
|
||||
'addressBookManagement.search': '搜索',
|
||||
'addressBookManagement.selectAgent': '请选择智能体',
|
||||
'addressBookManagement.name': '通讯录名称',
|
||||
'addressBookManagement.description': '通讯录描述',
|
||||
|
||||
@@ -352,6 +352,8 @@ export default {
|
||||
|
||||
// 替換詞管理頁面相關
|
||||
'replacementWordManagement.pageTitle': '替換詞管理',
|
||||
'replacementWordManagement.searchPlaceholder': '請輸入檔案名稱查詢',
|
||||
'replacementWordManagement.search': '搜尋',
|
||||
'replacementWordManagement.fileName': '檔案名稱',
|
||||
'replacementWordManagement.replacementWordCount': '替換詞數量',
|
||||
'replacementWordManagement.replacementWordContent': '替換詞內容',
|
||||
@@ -591,6 +593,7 @@ export default {
|
||||
|
||||
// 字典管理頁面文本
|
||||
'dictManagement.pageTitle': '字典管理',
|
||||
'dictManagement.dictTypeCategory': '字典分類',
|
||||
'dictManagement.searchPlaceholder': '請輸入字典值標籤查詢',
|
||||
'dictManagement.search': '搜索',
|
||||
'dictManagement.dictTypeName': '字典類型名稱',
|
||||
@@ -645,6 +648,8 @@ export default {
|
||||
'user.disable': '禁用',
|
||||
'user.delete': '刪除',
|
||||
'user.selectUsersFirst': '請先選擇需要刪除的用戶',
|
||||
'user.selectUsersToDisable': '請選擇要禁用的用戶',
|
||||
'user.selectUsersToEnable': '請選擇要啟用的用戶',
|
||||
'user.confirmDeleteSelected': '確定要刪除選中的{count}個用戶嗎?',
|
||||
'user.deleting': '正在刪除中...',
|
||||
'user.deleteSuccess': '成功刪除{count}個用戶',
|
||||
@@ -704,6 +709,9 @@ export default {
|
||||
'common.cancel': '取消',
|
||||
'common.sensitive': '敏感',
|
||||
'common.loading': '載入中',
|
||||
'common.copySuccess': '複製成功',
|
||||
'common.copyFailed': '複製失敗',
|
||||
'common.copyAddress': '複製地址',
|
||||
|
||||
// 語言切換
|
||||
'language.zhCN': '中文简体',
|
||||
@@ -1150,6 +1158,8 @@ export default {
|
||||
'templateQuickConfig.cancel': '取消',
|
||||
'templateQuickConfig.templateNotFound': '未找到指定模板',
|
||||
'templateQuickConfig.newTemplate': '新模板',
|
||||
'templateQuickConfig.editTemplate': '編輯模板',
|
||||
'templateQuickConfig.addTemplate': '新增模板',
|
||||
'warning': '警告',
|
||||
'info': '提示',
|
||||
'common.networkError': '網路請求失敗',
|
||||
@@ -1195,6 +1205,8 @@ export default {
|
||||
'voiceClone.operationClosed': '已關閉彈窗',
|
||||
'voiceClone.cloneSuccess': '複刻成功',
|
||||
'voiceClone.cloneFailed': '複刻失敗',
|
||||
'voiceClone.cloneErrorTip': '複刻失敗,請將鼠標懸停在錯誤提示上,查看錯誤詳情',
|
||||
'voiceClone.apiError': '調用API時出錯',
|
||||
'voiceClone.confirmClone': '確定要複刻此音色嗎?',
|
||||
'voiceClone.onlySuccessCanClone': '只有訓練成功的音色才能複刻',
|
||||
'common.insufficient': '不足',
|
||||
@@ -1212,6 +1224,7 @@ export default {
|
||||
'voiceClone.training': '訓練中',
|
||||
'voiceClone.trainSuccess': '訓練成功',
|
||||
'voiceClone.trainFailed': '訓練失敗',
|
||||
'voiceClone.trainFailedWithError': '訓練失敗:{error}',
|
||||
'voiceClone.itemsPerPage': '{items}條/頁',
|
||||
'voiceClone.firstPage': '首頁',
|
||||
'voiceClone.prevPage': '上一頁',
|
||||
@@ -1351,6 +1364,7 @@ export default {
|
||||
'knowledgeFileUpload.confirmParse': '確定要解析該文檔嗎?',
|
||||
'knowledgeFileUpload.nameRequired': '請輸入文檔名稱',
|
||||
'knowledgeFileUpload.fileRequired': '請選擇要上傳的文件',
|
||||
'knowledgeFileUpload.fileSizeExceeded': '文件大小不能超過10MB!',
|
||||
'knowledgeFileUpload.getListFailed': '獲取文檔列表失敗',
|
||||
'knowledgeFileUpload.parseCancelled': '已取消解析',
|
||||
'knowledgeFileUpload.deleteCancelled': '已取消删除',
|
||||
@@ -1384,8 +1398,20 @@ export default {
|
||||
'knowledgeFileUpload.comprehensiveSimilarity': '綜合相似度',
|
||||
'knowledgeFileUpload.content': '內容:',
|
||||
'knowledgeFileUpload.testQuestionRequired': '請輸入測試問題',
|
||||
'knowledgeFileUpload.sourceDocument': '來源文檔',
|
||||
'knowledgeFileUpload.parsing': '正在解析中...',
|
||||
'knowledgeBaseDialog.descriptionRequired': '請輸入知识库描述',
|
||||
|
||||
// 知識庫管理頁面文本
|
||||
'knowledgeBaseManagement.loading': '加載中...',
|
||||
'knowledgeBaseManagement.noData': '暫無數據',
|
||||
'knowledgeBaseManagement.switchKnowledgeBase': '切換知識庫',
|
||||
'knowledgeBaseManagement.currentKnowledgeBaseDocuments': '當前知識庫文檔資產',
|
||||
'knowledgeBaseManagement.enabled': '已啟用',
|
||||
'knowledgeBaseManagement.disabled': '已禁用',
|
||||
'knowledgeBaseManagement.totalDocuments': '共 {total} 個文檔',
|
||||
'knowledgeBaseManagement.getListFailed': '獲取列表失敗',
|
||||
|
||||
// 功能管理頁面文本
|
||||
'featureManagement.selectAll': '全選',
|
||||
'featureManagement.deselectAll': '取消全選',
|
||||
@@ -1401,6 +1427,12 @@ export default {
|
||||
'featureManagement.cancel': '取消',
|
||||
'featureManagement.resetSuccess': '功能配置重置成功',
|
||||
'featureManagement.noChanges': '沒有需要儲存的更改',
|
||||
'featureManagement.saving': '儲存中...',
|
||||
'featureManagement.groupName.featureManagement': '功能模組開關',
|
||||
'featureManagement.groupName.voiceManagement': '智能體可見配置',
|
||||
'featureManagement.groupDescription.featureManagement': '控制系統核心功能模組的啟用狀態',
|
||||
'featureManagement.groupDescription.voiceManagement': '配置哪些功能對智能體(AI助手)可見和可用',
|
||||
'featureManagement.moduleCount': '共 {count} 個模組',
|
||||
|
||||
// 功能名稱和描述
|
||||
'feature.voiceprintRecognition.name': '聲紋識別',
|
||||
@@ -1421,7 +1453,6 @@ export default {
|
||||
// 通訊錄管理頁面
|
||||
'addressBookManagement.title': '通訊錄',
|
||||
'addressBookManagement.searchPlaceholder': '請輸入通訊錄名稱搜尋',
|
||||
'addressBookManagement.search': '搜尋',
|
||||
'addressBookManagement.name': '通訊錄名稱',
|
||||
'addressBookManagement.description': '通訊錄描述',
|
||||
'addressBookManagement.contactCount': '聯繫人數量',
|
||||
|
||||
@@ -6,6 +6,7 @@ import App from './App.vue';
|
||||
import router from './router';
|
||||
import store from './store';
|
||||
import i18n from './i18n';
|
||||
import locale from 'element-ui/lib/locale'
|
||||
import './styles/global.scss';
|
||||
import { register as registerServiceWorker } from './registerServiceWorker';
|
||||
import featureManager from './utils/featureManager';
|
||||
@@ -14,6 +15,7 @@ import featureManager from './utils/featureManager';
|
||||
Vue.prototype.$eventBus = new Vue();
|
||||
|
||||
Vue.use(ElementUI);
|
||||
locale.i18n((key, value) => i18n.t(key, value))
|
||||
|
||||
Vue.config.productionTip = false
|
||||
|
||||
|
||||
@@ -98,18 +98,6 @@ const routes = [
|
||||
title: '知识库管理'
|
||||
}
|
||||
},
|
||||
{
|
||||
path: '/knowledge-file-upload',
|
||||
name: 'KnowledgeFileUpload',
|
||||
component: function () {
|
||||
return import('../views/KnowledgeFileUpload.vue')
|
||||
},
|
||||
meta: {
|
||||
requiresAuth: true,
|
||||
title: '文档上传管理'
|
||||
}
|
||||
},
|
||||
|
||||
{
|
||||
path: '/server-side-management',
|
||||
name: 'ServerSideManager',
|
||||
|
||||
@@ -29,4 +29,18 @@ select:-webkit-autofill:focus {
|
||||
.popper__arrow {
|
||||
border-top-color: #e4e7ed !important;
|
||||
}
|
||||
}
|
||||
|
||||
@mixin scrollbar-style {
|
||||
&::-webkit-scrollbar {
|
||||
width: 6px;
|
||||
}
|
||||
&::-webkit-scrollbar-thumb {
|
||||
background: #a1c9fd;
|
||||
border-radius: 3px;
|
||||
}
|
||||
&::-webkit-scrollbar-track {
|
||||
background: #f0f3fe;
|
||||
border-radius: 3px;
|
||||
}
|
||||
}
|
||||
@@ -15,10 +15,9 @@
|
||||
:placeholder="$t('addressBookManagement.searchPlaceholder')"
|
||||
v-model="searchKeyword"
|
||||
class="search-input"
|
||||
@keyup.enter="handleSearch"
|
||||
@input="handleSearch"
|
||||
clearable
|
||||
/>
|
||||
<el-button class="btn-search" @click="handleSearch">{{ $t('addressBookManagement.search') }}</el-button>
|
||||
</div>
|
||||
|
||||
<!-- 智能体列表 -->
|
||||
@@ -42,9 +41,13 @@
|
||||
<img :src="getDeviceAvatar(device.id)" alt="avatar" />
|
||||
</div>
|
||||
<div class="device-content">
|
||||
<div class="device-name">{{ device.name }}</div>
|
||||
<div class="device-name">
|
||||
<MacAddressMask :macAddress="device.name" />
|
||||
</div>
|
||||
<div class="device-row">
|
||||
<span class="device-id">{{ device.deviceId }}</span>
|
||||
<span class="device-id">
|
||||
<MacAddressMask :macAddress="device.deviceId" />
|
||||
</span>
|
||||
<span class="device-status" :class="device.online ? 'online' : 'offline'">
|
||||
{{ device.online ? $t('addressBookManagement.online') : $t('addressBookManagement.offline') }}
|
||||
</span>
|
||||
@@ -76,10 +79,15 @@
|
||||
@keyup.enter="handleAliasBlur"
|
||||
autofocus
|
||||
/>
|
||||
<span v-else class="device-name-text" @click="handleStartEdit">{{ selectedDevice.name }}</span>
|
||||
<span v-else class="device-name-text" @click="handleStartEdit">
|
||||
<MacAddressMask :macAddress="selectedDevice.name" />
|
||||
</span>
|
||||
<i v-if="!isEditingAlias" class="el-icon-edit" @click="handleStartEdit"></i>
|
||||
</div>
|
||||
<div class="device-mac">{{ $t('addressBookManagement.macAddress') }}:{{ selectedDevice.deviceId }}</div>
|
||||
<div class="device-mac">
|
||||
{{ $t('addressBookManagement.macAddress') }}:
|
||||
<MacAddressMask :macAddress="selectedDevice.deviceId" />
|
||||
</div>
|
||||
<div class="device-status">
|
||||
<span>{{ $t('addressBookManagement.status') }}:</span>
|
||||
<span :class="selectedDevice.online ? 'online' : 'offline'">
|
||||
@@ -133,11 +141,9 @@
|
||||
<p class="section-desc">{{ $t('addressBookManagement.setPermissionDesc', { count: selectedPermissions.length }) }}</p>
|
||||
</div>
|
||||
<div class="section-actions">
|
||||
<el-button size="small" @click="handleCancel">{{ $t('common.cancel') }}</el-button>
|
||||
<el-button size="small" @click="handleToggleSelectAll">
|
||||
{{ isAllSelected ? $t('addressBookManagement.deselectAll') : $t('addressBookManagement.selectAll') }}
|
||||
</el-button>
|
||||
<el-button type="primary" size="small" @click="handleSavePermissions">{{ $t('addressBookManagement.save') }}</el-button>
|
||||
<CustomButton size="small" @click="handleCancel">{{ $t('common.cancel') }}</CustomButton>
|
||||
<CustomButton size="small" @click="handleToggleSelectAll">{{ isAllSelected ? $t('addressBookManagement.deselectAll') : $t('addressBookManagement.selectAll') }}</CustomButton>
|
||||
<CustomButton type="confirm" size="small" @click="handleSavePermissions">{{ $t('addressBookManagement.save') }}</CustomButton>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -166,10 +172,14 @@
|
||||
@keyup.enter="handleEditPermissionBlur(device)"
|
||||
autofocus
|
||||
/>
|
||||
<span v-else class="permission-name" @click.stop="handleStartEditPermission(device)">{{ device.addressBookAlias || device.name }}</span>
|
||||
<span v-else class="permission-name" @click.stop="handleStartEditPermission(device)">
|
||||
<MacAddressMask :macAddress="device.addressBookAlias || device.name" />
|
||||
</span>
|
||||
<i v-if="editingDeviceId !== device.id" class="el-icon-edit permission-edit-btn" @click.stop="handleStartEditPermission(device)"></i>
|
||||
</div>
|
||||
<div class="permission-id">{{ device.deviceId }}</div>
|
||||
<div class="permission-id">
|
||||
<MacAddressMask :macAddress="device.deviceId" />
|
||||
</div>
|
||||
<div class="permission-status" :class="device.online ? 'online' : 'offline'">
|
||||
{{ device.online ? $t('addressBookManagement.online') : $t('addressBookManagement.offline') }}
|
||||
</div>
|
||||
@@ -195,10 +205,12 @@ import HeaderBar from "@/components/HeaderBar.vue";
|
||||
import VersionFooter from "@/components/VersionFooter.vue";
|
||||
import Api from "@/apis/api.js";
|
||||
import AddressBookApi from "@/apis/module/addressBook.js";
|
||||
import MacAddressMask from "@/components/MacAddressMask.vue";
|
||||
import CustomButton from "@/components/CustomButton.vue";
|
||||
|
||||
export default {
|
||||
name: "AddressBookManagement",
|
||||
components: { HeaderBar, VersionFooter },
|
||||
components: { HeaderBar, VersionFooter, MacAddressMask, CustomButton },
|
||||
data() {
|
||||
return {
|
||||
searchKeyword: "",
|
||||
@@ -269,7 +281,7 @@ export default {
|
||||
remarks: device.alias || '',
|
||||
online: false,
|
||||
createDate: device.createDate,
|
||||
lastConnectedAt: device.lastConnectedAt,
|
||||
lastConnectedAt: device.lastConnectedAtTimestamp,
|
||||
deviceStatus: 'offline'
|
||||
}));
|
||||
resolve();
|
||||
@@ -277,8 +289,11 @@ export default {
|
||||
});
|
||||
});
|
||||
Promise.all(agentPromises).then(() => {
|
||||
const firstDevice = agentList[0] || {};
|
||||
this.agentDeviceOptions = agentList;
|
||||
this.filteredAgents = agentList;
|
||||
// 默认选中第一项
|
||||
this.handleDeviceClick(firstDevice.devices?.[0] || {}, firstDevice);
|
||||
// 获取设备状态
|
||||
this.fetchDeviceStatus();
|
||||
});
|
||||
@@ -334,17 +349,6 @@ export default {
|
||||
}
|
||||
});
|
||||
},
|
||||
handleAgentClick(agent) {
|
||||
if (this.expandedAgentId === agent.id) {
|
||||
this.expandedAgentId = null;
|
||||
this.selectedAgent = null;
|
||||
this.selectedDevice = null;
|
||||
} else {
|
||||
this.expandedAgentId = agent.id;
|
||||
this.selectedAgent = agent;
|
||||
this.selectedDevice = null;
|
||||
}
|
||||
},
|
||||
handleDeviceClick(device, agent) {
|
||||
this.expandedAgentId = agent.id;
|
||||
this.selectedAgent = agent;
|
||||
@@ -396,8 +400,8 @@ export default {
|
||||
alias: newName
|
||||
}, (res) => {
|
||||
if (res.data?.code === 0) {
|
||||
device.addressBookAlias = newName;
|
||||
this.$message.success(this.$t('addressBookManagement.aliasSaved'));
|
||||
this.loadAddressBookPermissions(this.selectedDevice.deviceId);
|
||||
} else {
|
||||
this.$message.error(res.data?.msg || this.$t('addressBookManagement.saveFailed'));
|
||||
}
|
||||
@@ -453,6 +457,7 @@ export default {
|
||||
} else if (results.every(r => r)) {
|
||||
this.$message.success(this.$t('addressBookManagement.permissionSaved'));
|
||||
this.originalPermissions = [...this.selectedPermissions];
|
||||
this.loadAddressBookPermissions(this.selectedDevice.deviceId);
|
||||
} else {
|
||||
this.$message.error(this.$t('addressBookManagement.partialSaveFailed'));
|
||||
}
|
||||
@@ -523,7 +528,7 @@ export default {
|
||||
getTimeAgo(timestamp) {
|
||||
if (!timestamp) return '-';
|
||||
const now = new Date();
|
||||
const date = new Date(timestamp);
|
||||
const date = new Date(Number(timestamp));
|
||||
const diff = now - date;
|
||||
|
||||
const seconds = Math.floor(diff / 1000);
|
||||
@@ -581,7 +586,7 @@ export default {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
background-size: cover;
|
||||
background: linear-gradient(to bottom right, #dce8ff, #e4eeff, #e6cbfd) center;
|
||||
background: #eff4ff;
|
||||
-webkit-background-size: cover;
|
||||
-o-background-size: cover;
|
||||
overflow: hidden;
|
||||
@@ -940,8 +945,8 @@ export default {
|
||||
}
|
||||
|
||||
.section-actions {
|
||||
margin-top: 4px;
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1124,14 +1129,6 @@ export default {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.btn-search {
|
||||
padding: 10px 20px;
|
||||
background: linear-gradient(135deg, #6b8cff, #a966ff);
|
||||
border: none;
|
||||
color: white;
|
||||
width: 80px;
|
||||
}
|
||||
|
||||
.agent-card {
|
||||
width: 100%;
|
||||
padding: 12px 0px;
|
||||
|
||||
@@ -1,141 +1,103 @@
|
||||
<template>
|
||||
<div class="welcome">
|
||||
<HeaderBar />
|
||||
|
||||
<div class="operation-bar">
|
||||
<h2 class="page-title">{{ $t("agentTemplateManagement.title") }}</h2>
|
||||
<div class="right-operations">
|
||||
<el-input
|
||||
:placeholder="$t('agentTemplateManagement.searchPlaceholder')"
|
||||
v-model="search"
|
||||
class="search-input"
|
||||
clearable
|
||||
@keyup.enter.native="handleSearch"
|
||||
style="width: 240px"
|
||||
/>
|
||||
<el-button class="btn-search" @click="handleSearch">
|
||||
{{ $t("agentTemplateManagement.search") }}
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 主体内容 -->
|
||||
<div class="main-wrapper">
|
||||
<div class="content-panel">
|
||||
<div class="content-area">
|
||||
<el-card class="template-card" shadow="never">
|
||||
<el-table
|
||||
ref="templateTable"
|
||||
:data="templateList"
|
||||
style="width: 100%"
|
||||
v-loading="templateLoading"
|
||||
:element-loading-text="$t('agentTemplateManagement.loading')"
|
||||
element-loading-spinner="el-icon-loading"
|
||||
element-loading-background="rgba(255, 255, 255, 0.7)"
|
||||
class="transparent-table"
|
||||
:header-cell-style="{ padding: '10px 20px' }"
|
||||
:cell-style="{ padding: '10px 20px' }"
|
||||
>
|
||||
<!-- 移除@row-click="handleRowClick" -->
|
||||
<!-- 自定义选择列,实现表头是"选择"文字,数据行是小方框 -->
|
||||
<el-table-column
|
||||
:label="$t('agentTemplateManagement.select')"
|
||||
align="center"
|
||||
min-width="100"
|
||||
>
|
||||
<template slot-scope="scope">
|
||||
<el-checkbox
|
||||
v-model="scope.row.selected"
|
||||
@change="handleRowSelectionChange(scope.row)"
|
||||
@click.stop
|
||||
></el-checkbox>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<!-- 模板名称 -->
|
||||
<el-table-column
|
||||
:label="$t('agentTemplateManagement.templateName')"
|
||||
prop="agentName"
|
||||
min-width="250"
|
||||
show-overflow-tooltip
|
||||
>
|
||||
<template slot-scope="scope">
|
||||
<span>{{ scope.row.agentName }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<!-- 修改为序号列,并移动到此处 -->
|
||||
<el-table-column
|
||||
:label="$t('agentTemplateManagement.serialNumber')"
|
||||
min-width="120"
|
||||
align="center"
|
||||
>
|
||||
<template slot-scope="scope">
|
||||
<span>{{ (currentPage - 1) * pageSize + scope.$index + 1 }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<!-- 操作列 -->
|
||||
<el-table-column
|
||||
:label="$t('agentTemplateManagement.action')"
|
||||
min-width="250"
|
||||
align="center"
|
||||
>
|
||||
<template slot-scope="scope">
|
||||
<div style="display: flex; justify-content: center; gap: 15px">
|
||||
<el-button type="text" @click="editTemplate(scope.row)">{{
|
||||
$t("agentTemplateManagement.editTemplate")
|
||||
}}</el-button>
|
||||
<el-button type="text" @click="deleteTemplate(scope.row)">{{
|
||||
$t("agentTemplateManagement.deleteTemplate")
|
||||
}}</el-button>
|
||||
</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
|
||||
<!-- 表格底部操作栏 -->
|
||||
<div class="table_bottom">
|
||||
<div class="ctrl_btn">
|
||||
<el-button
|
||||
type="primary"
|
||||
@click="handleSelectAll"
|
||||
size="mini"
|
||||
class="select-all-btn"
|
||||
>
|
||||
{{
|
||||
isAllSelected
|
||||
? $t("agentTemplateManagement.deselectAll")
|
||||
: $t("agentTemplateManagement.selectAll")
|
||||
}}
|
||||
</el-button>
|
||||
<el-button type="success" @click="showAddTemplateDialog" size="mini">
|
||||
{{ $t("agentTemplateManagement.createTemplate") }}
|
||||
</el-button>
|
||||
<el-button
|
||||
type="danger"
|
||||
@click="batchDeleteTemplate"
|
||||
:disabled="!hasSelected"
|
||||
size="mini"
|
||||
>
|
||||
{{ $t("agentTemplateManagement.batchDelete") }}
|
||||
</el-button>
|
||||
</div>
|
||||
|
||||
<!-- 分页 -->
|
||||
<div class="custom-pagination">
|
||||
<el-pagination
|
||||
v-model:current-page="currentPage"
|
||||
v-model:page-size="pageSize"
|
||||
:page-sizes="pageSizeOptions"
|
||||
layout="total, sizes, prev, pager, next, jumper"
|
||||
:total="total"
|
||||
@size-change="handlePageSizeChange"
|
||||
@current-change="handlePageChange"
|
||||
<div class="operation-header">
|
||||
<h2 class="page-title">{{ $t("agentTemplateManagement.title") }}</h2>
|
||||
<div class="right-operations">
|
||||
<el-input
|
||||
:placeholder="$t('agentTemplateManagement.searchPlaceholder')"
|
||||
v-model="search"
|
||||
class="search-input"
|
||||
clearable
|
||||
@keyup.enter.native="handleSearch"
|
||||
/>
|
||||
<CustomButton icon="el-icon-search" type="confirm" @click="handleSearch">
|
||||
{{ $t("agentTemplateManagement.search") }}
|
||||
</CustomButton>
|
||||
</div>
|
||||
</div>
|
||||
<CustomTable
|
||||
ref="templateTable"
|
||||
:data="templateList"
|
||||
:columns="tableColumns"
|
||||
:loading="templateLoading"
|
||||
:show-selection="true"
|
||||
:show-operations="true"
|
||||
:operations-label="$t('agentTemplateManagement.action')"
|
||||
:total="total"
|
||||
:current-page="currentPage"
|
||||
:page-size="pageSize"
|
||||
:page-size-options="pageSizeOptions"
|
||||
@size-change="handlePageSizeChange"
|
||||
@page-change="handlePageChange"
|
||||
>
|
||||
<template slot="selection" slot-scope="scope">
|
||||
<el-checkbox v-model="scope.row.selected" @change="handleRowSelectionChange(scope.row)"></el-checkbox>
|
||||
</template>
|
||||
<template slot="serialNumber" slot-scope="scope">
|
||||
<span>{{ (currentPage - 1) * pageSize + scope.$index + 1 }}</span>
|
||||
</template>
|
||||
<template slot="operations" slot-scope="scope">
|
||||
<el-button size="mini" type="text" @click="editTemplate(scope.row)">
|
||||
{{ $t("agentTemplateManagement.editTemplate") }}
|
||||
</el-button>
|
||||
<el-button size="mini" type="text" @click="deleteTemplate(scope.row)">
|
||||
{{ $t("agentTemplateManagement.deleteTemplate") }}
|
||||
</el-button>
|
||||
</template>
|
||||
<template slot="footer-btns">
|
||||
<div class="ctrl_btn">
|
||||
<CustomButton :icon="isAllSelected ? 'el-icon-circle-close' : 'el-icon-circle-check'" size="small" @click="handleSelectAll">
|
||||
{{ isAllSelected ? $t("agentTemplateManagement.deselectAll") : $t("agentTemplateManagement.selectAll") }}
|
||||
</CustomButton>
|
||||
<CustomButton type="add" icon="el-icon-plus" size="small" @click="showAddTemplateDialog">
|
||||
{{ $t("agentTemplateManagement.createTemplate") }}
|
||||
</CustomButton>
|
||||
<CustomButton size="small" type="delete" icon="el-icon-delete" @click="batchDeleteTemplate">
|
||||
{{ $t("agentTemplateManagement.batchDelete") }}
|
||||
</CustomButton>
|
||||
</div>
|
||||
</template>
|
||||
</CustomTable>
|
||||
</el-card>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 新增/编辑模板弹窗 -->
|
||||
<CustomDialog
|
||||
:title="dialogTitle"
|
||||
:visible.sync="dialogVisible"
|
||||
:confirm-loading="confirmLoading"
|
||||
:footer="true"
|
||||
:width="'1200px'"
|
||||
@confirm="handleDialogConfirm"
|
||||
@cancel="dialogVisible = false"
|
||||
>
|
||||
<el-form ref="dialogForm" :model="form" :rules="formRules" label-width="100px">
|
||||
<el-form-item :label="$t('templateQuickConfig.agentSettings.agentName')" prop="agentName">
|
||||
<el-input
|
||||
v-model="form.agentName"
|
||||
:placeholder="$t('templateQuickConfig.agentSettings.agentNamePlaceholder')"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item :label="$t('templateQuickConfig.agentSettings.systemPrompt')" prop="systemPrompt">
|
||||
<el-input
|
||||
v-model="form.systemPrompt"
|
||||
type="textarea"
|
||||
:placeholder="$t('templateQuickConfig.agentSettings.systemPromptPlaceholder')"
|
||||
show-word-limit
|
||||
maxlength="2000"
|
||||
:rows="16"
|
||||
/>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</CustomDialog>
|
||||
|
||||
<el-footer>
|
||||
<version-footer />
|
||||
</el-footer>
|
||||
@@ -146,136 +108,208 @@
|
||||
import HeaderBar from "@/components/HeaderBar";
|
||||
import agentApi from "@/apis/module/agent";
|
||||
import VersionFooter from "@/components/VersionFooter.vue";
|
||||
import CustomButton from "@/components/CustomButton.vue";
|
||||
import CustomTable from "@/components/CustomTable.vue";
|
||||
import CustomDialog from "@/components/CustomDialog.vue";
|
||||
|
||||
const DEFAULT_MODEL_CONFIG = {
|
||||
ttsModelId: "TTS_EdgeTTS",
|
||||
vadModelId: "VAD_SileroVAD",
|
||||
asrModelId: "ASR_FunASR",
|
||||
llmModelId: "LLM_ChatGLMLLM",
|
||||
vllmModelId: "VLLM_ChatGLMVLLM",
|
||||
memModelId: "Memory_nomem",
|
||||
intentModelId: "Intent_function_call"
|
||||
};
|
||||
|
||||
export default {
|
||||
name: "AgentTemplateManagement",
|
||||
components: {
|
||||
HeaderBar,
|
||||
VersionFooter
|
||||
VersionFooter,
|
||||
CustomButton,
|
||||
CustomTable,
|
||||
CustomDialog
|
||||
},
|
||||
|
||||
data() {
|
||||
return {
|
||||
// 模板相关
|
||||
templateList: [],
|
||||
templateLoading: false,
|
||||
selectedTemplates: [],
|
||||
isAllSelected: false, // 添加全选状态
|
||||
|
||||
isAllSelected: false,
|
||||
search: "",
|
||||
// 分页相关数据
|
||||
pageSizeOptions: [10, 20, 50, 100],
|
||||
currentPage: 1,
|
||||
pageSize: 10,
|
||||
total: 0,
|
||||
tableColumns: [],
|
||||
dialogVisible: false,
|
||||
dialogTitle: "",
|
||||
confirmLoading: false,
|
||||
form: {
|
||||
id: null,
|
||||
agentCode: "小智",
|
||||
agentName: "",
|
||||
systemPrompt: "",
|
||||
sort: 0,
|
||||
model: { ...DEFAULT_MODEL_CONFIG }
|
||||
},
|
||||
formRules: {
|
||||
agentName: [
|
||||
{ required: true, message: "请输入助手昵称", trigger: "blur" }
|
||||
],
|
||||
systemPrompt: [
|
||||
{ required: true, message: "请输入角色介绍", trigger: "blur" }
|
||||
]
|
||||
},
|
||||
originalForm: null
|
||||
};
|
||||
},
|
||||
created() {
|
||||
this.initTableColumns();
|
||||
this.loadTemplateList();
|
||||
},
|
||||
// 在computed部分添加hasSelected属性
|
||||
computed: {
|
||||
pageCount() {
|
||||
return Math.ceil(this.total / this.pageSize);
|
||||
},
|
||||
visiblePages() {
|
||||
return this.getVisiblePages();
|
||||
},
|
||||
hasSelected() {
|
||||
return this.selectedTemplates.length > 0;
|
||||
},
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
// 加载模板列表
|
||||
// 改进loadTemplateList方法的错误处理逻辑
|
||||
initTableColumns() {
|
||||
this.tableColumns = [
|
||||
{
|
||||
prop: "agentName",
|
||||
label: this.$t("agentTemplateManagement.templateName"),
|
||||
},
|
||||
{
|
||||
prop: "serialNumber",
|
||||
label: this.$t("agentTemplateManagement.serialNumber"),
|
||||
}
|
||||
];
|
||||
},
|
||||
loadTemplateList() {
|
||||
this.templateLoading = true;
|
||||
const params = {
|
||||
page: this.currentPage,
|
||||
limit: this.pageSize,
|
||||
limit: this.pageSize
|
||||
};
|
||||
if (this.search) {
|
||||
params.agentName = this.search;
|
||||
}
|
||||
|
||||
try {
|
||||
agentApi.getAgentTemplatesPage(
|
||||
params,
|
||||
(res) => {
|
||||
// 更健壮的响应处理逻辑
|
||||
if (res && typeof res === "object") {
|
||||
if (res.data && res.data.code === 0) {
|
||||
const responseData = res.data.data || {};
|
||||
// 为每个模板添加selected属性
|
||||
this.templateList = Array.isArray(responseData.list)
|
||||
? responseData.list.map((item) => ({ ...item, selected: false }))
|
||||
: [];
|
||||
this.total =
|
||||
typeof responseData.total === "number" ? responseData.total : 0;
|
||||
} else {
|
||||
this.templateList = [];
|
||||
this.total = 0;
|
||||
this.$message.error(
|
||||
res?.data?.msg || this.$t("agentTemplateManagement.fetchTemplateFailed")
|
||||
);
|
||||
}
|
||||
} else {
|
||||
this.templateList = [];
|
||||
this.total = 0;
|
||||
this.$message.error(
|
||||
this.$t("agentTemplateManagement.fetchTemplateBackendError")
|
||||
);
|
||||
}
|
||||
this.templateLoading = false;
|
||||
},
|
||||
(error) => {
|
||||
agentApi.getAgentTemplatesPage(
|
||||
params,
|
||||
(res) => {
|
||||
if (res && res.data && res.data.code === 0) {
|
||||
const responseData = res.data.data || {};
|
||||
this.templateList = Array.isArray(responseData.list)
|
||||
? responseData.list.map((item) => ({ ...item, selected: false }))
|
||||
: [];
|
||||
this.total = typeof responseData.total === "number" ? responseData.total : 0;
|
||||
} else {
|
||||
this.templateList = [];
|
||||
this.total = 0;
|
||||
this.templateLoading = false;
|
||||
this.$message.error(this.$t("common.networkError"));
|
||||
this.$message.error(res?.data?.msg || this.$t("agentTemplateManagement.fetchTemplateFailed"));
|
||||
}
|
||||
);
|
||||
} catch (error) {
|
||||
this.templateList = [];
|
||||
this.total = 0;
|
||||
this.templateLoading = false;
|
||||
this.$message.error(this.$t("agentTemplateManagement.fetchTemplateBackendError"));
|
||||
}
|
||||
this.templateLoading = false;
|
||||
},
|
||||
(error) => {
|
||||
this.templateList = [];
|
||||
this.total = 0;
|
||||
this.templateLoading = false;
|
||||
this.$message.error(this.$t("common.networkError"));
|
||||
}
|
||||
);
|
||||
},
|
||||
|
||||
// 搜索模板
|
||||
handleSearch() {
|
||||
if (this.search) {
|
||||
const searchValue = this.search.toLowerCase();
|
||||
const filteredList = this.templateList.filter((template) =>
|
||||
template.agentName.toLowerCase().includes(searchValue)
|
||||
);
|
||||
this.templateList = filteredList;
|
||||
this.total = filteredList.length;
|
||||
} else {
|
||||
this.loadTemplateList();
|
||||
}
|
||||
this.currentPage = 1;
|
||||
this.loadTemplateList();
|
||||
},
|
||||
|
||||
// 修改showAddTemplateDialog方法,使其跳转到与编辑页面相同的页面
|
||||
// 显示新增模板弹窗
|
||||
showAddTemplateDialog() {
|
||||
// 跳转到模板快速配置页面,不传递templateId参数表示新增
|
||||
this.$router.push({
|
||||
path: "/template-quick-config",
|
||||
});
|
||||
this.dialogTitle = this.$t("templateQuickConfig.addTemplate");
|
||||
this.form = {
|
||||
id: null,
|
||||
agentCode: "小智",
|
||||
agentName: this.$t("templateQuickConfig.newTemplate"),
|
||||
systemPrompt: "",
|
||||
sort: 1,
|
||||
model: { ...DEFAULT_MODEL_CONFIG }
|
||||
};
|
||||
this.originalForm = JSON.parse(JSON.stringify(this.form));
|
||||
this.dialogVisible = true;
|
||||
},
|
||||
|
||||
// 编辑模板
|
||||
editTemplate(row) {
|
||||
// 跳转到模板快速配置页面,并传递模板ID参数
|
||||
this.$router.push({
|
||||
path: "/template-quick-config",
|
||||
query: { templateId: row.id },
|
||||
this.dialogTitle = this.$t("templateQuickConfig.editTemplate");
|
||||
this.fetchTemplateById(row.id);
|
||||
},
|
||||
fetchTemplateById(templateId) {
|
||||
agentApi.getAgentTemplateById(templateId, (res) => {
|
||||
if (res && res.data && res.data.code === 0 && res.data.data) {
|
||||
const template = res.data.data;
|
||||
this.form = {
|
||||
id: template.id,
|
||||
agentCode: template.agentCode || "小智",
|
||||
agentName: template.agentName || "",
|
||||
systemPrompt: template.systemPrompt || "",
|
||||
sort: template.sort || 0,
|
||||
model: {
|
||||
ttsModelId: template.ttsModelId || DEFAULT_MODEL_CONFIG.ttsModelId,
|
||||
vadModelId: template.vadModelId || DEFAULT_MODEL_CONFIG.vadModelId,
|
||||
asrModelId: template.asrModelId || DEFAULT_MODEL_CONFIG.asrModelId,
|
||||
llmModelId: template.llmModelId || DEFAULT_MODEL_CONFIG.llmModelId,
|
||||
vllmModelId: template.vllmModelId || DEFAULT_MODEL_CONFIG.vllmModelId,
|
||||
memModelId: template.memModelId || DEFAULT_MODEL_CONFIG.memModelId,
|
||||
intentModelId: template.intentModelId || DEFAULT_MODEL_CONFIG.intentModelId
|
||||
}
|
||||
};
|
||||
this.originalForm = JSON.parse(JSON.stringify(this.form));
|
||||
this.dialogVisible = true;
|
||||
} else {
|
||||
this.$message.error(res?.data?.msg || this.$t("templateQuickConfig.templateNotFound"));
|
||||
}
|
||||
});
|
||||
},
|
||||
handleDialogConfirm() {
|
||||
this.$refs.dialogForm.validate((valid) => {
|
||||
if (!valid) return;
|
||||
|
||||
// 删除模板
|
||||
const configData = {
|
||||
id: this.form.id || "",
|
||||
agentCode: this.form.agentCode,
|
||||
agentName: this.form.agentName,
|
||||
systemPrompt: this.form.systemPrompt,
|
||||
sort: this.form.sort,
|
||||
functions: [],
|
||||
...this.form.model
|
||||
};
|
||||
|
||||
this.confirmLoading = true;
|
||||
const apiCall = this.form.id
|
||||
? agentApi.updateAgentTemplate
|
||||
: agentApi.addAgentTemplate;
|
||||
|
||||
apiCall(configData, (res) => {
|
||||
this.confirmLoading = false;
|
||||
if (res && res.data && res.data.code === 0) {
|
||||
this.$message.success({
|
||||
message: this.$t("templateQuickConfig.saveSuccess"),
|
||||
showClose: true
|
||||
});
|
||||
this.dialogVisible = false;
|
||||
this.loadTemplateList();
|
||||
} else {
|
||||
this.$message.error({
|
||||
message: res?.data?.msg || this.$t("templateQuickConfig.saveFailed"),
|
||||
showClose: true
|
||||
});
|
||||
}
|
||||
}, (error) => {
|
||||
this.confirmLoading = false;
|
||||
this.$message.error(this.$t("common.networkError"));
|
||||
});
|
||||
});
|
||||
},
|
||||
deleteTemplate(row) {
|
||||
this.$confirm(
|
||||
this.$t("agentTemplateManagement.confirmSingleDelete"),
|
||||
@@ -283,23 +317,18 @@ export default {
|
||||
{
|
||||
confirmButtonText: this.$t("common.confirm"),
|
||||
cancelButtonText: this.$t("common.cancel"),
|
||||
type: "warning",
|
||||
type: "warning"
|
||||
}
|
||||
)
|
||||
.then(() => {
|
||||
agentApi.deleteAgentTemplate(row.id, (res) => {
|
||||
if (res && typeof res === "object") {
|
||||
// 检查res.data是否存在且包含code=0
|
||||
if (res.data && res.data.code === 0) {
|
||||
this.$message.success(this.$t("agentTemplateManagement.deleteSuccess"));
|
||||
this.loadTemplateList();
|
||||
} else {
|
||||
this.$message.error(
|
||||
res?.data?.msg || this.$t("agentTemplateManagement.deleteFailed")
|
||||
);
|
||||
}
|
||||
if (res && res.data && res.data.code === 0) {
|
||||
this.$message.success(this.$t("agentTemplateManagement.deleteSuccess"));
|
||||
this.selectedTemplates = [];
|
||||
this.isAllSelected = false;
|
||||
this.loadTemplateList();
|
||||
} else {
|
||||
this.$message.error(this.$t("agentTemplateManagement.deleteBackendError"));
|
||||
this.$message.error(res?.data?.msg || this.$t("agentTemplateManagement.deleteFailed"));
|
||||
}
|
||||
});
|
||||
})
|
||||
@@ -307,47 +336,30 @@ export default {
|
||||
this.$message.info(this.$t("common.deleteCancelled"));
|
||||
});
|
||||
},
|
||||
|
||||
// 批量删除模板
|
||||
batchDeleteTemplate() {
|
||||
if (this.selectedTemplates.length === 0) {
|
||||
this.$message.warning(this.$t("agentTemplateManagement.selectTemplate"));
|
||||
return;
|
||||
}
|
||||
|
||||
this.$confirm(
|
||||
this.$t("agentTemplateManagement.confirmBatchDelete", {
|
||||
count: this.selectedTemplates.length,
|
||||
}),
|
||||
this.$t("agentTemplateManagement.confirmBatchDelete", { count: this.selectedTemplates.length }),
|
||||
this.$t("common.warning"),
|
||||
{
|
||||
confirmButtonText: this.$t("common.confirm"),
|
||||
cancelButtonText: this.$t("common.cancel"),
|
||||
type: "warning",
|
||||
type: "warning"
|
||||
}
|
||||
)
|
||||
.then(() => {
|
||||
// 确保参数格式正确 - 将id数组作为请求体
|
||||
const ids = this.selectedTemplates.map((template) => template.id);
|
||||
|
||||
agentApi.batchDeleteAgentTemplate(ids, (res) => {
|
||||
if (res && typeof res === "object") {
|
||||
if (res.data && res.data.code === 0) {
|
||||
this.$message.success(
|
||||
this.$t("agentTemplateManagement.batchDeleteSuccess")
|
||||
);
|
||||
// 重新加载模板列表
|
||||
this.loadTemplateList();
|
||||
// 清空选中状态
|
||||
this.selectedTemplates = [];
|
||||
this.isAllSelected = false;
|
||||
} else {
|
||||
this.$message.error(
|
||||
res?.data?.msg || this.$t("agentTemplateManagement.batchDeleteFailed")
|
||||
);
|
||||
}
|
||||
if (res && res.data && res.data.code === 0) {
|
||||
this.$message.success(this.$t("agentTemplateManagement.batchDeleteSuccess"));
|
||||
this.loadTemplateList();
|
||||
this.selectedTemplates = [];
|
||||
this.isAllSelected = false;
|
||||
} else {
|
||||
this.$message.error(this.$t("agentTemplateManagement.deleteBackendError"));
|
||||
this.$message.error(res?.data?.msg || this.$t("agentTemplateManagement.batchDeleteFailed"));
|
||||
}
|
||||
});
|
||||
})
|
||||
@@ -355,150 +367,81 @@ export default {
|
||||
this.$message.info(this.$t("common.deleteCancelled"));
|
||||
});
|
||||
},
|
||||
|
||||
// 完善分页相关方法
|
||||
handlePageChange(page) {
|
||||
this.currentPage = page;
|
||||
this.loadTemplateList();
|
||||
},
|
||||
|
||||
handlePageSizeChange(size) {
|
||||
this.pageSize = size;
|
||||
this.currentPage = 1;
|
||||
this.loadTemplateList();
|
||||
},
|
||||
|
||||
goFirst() {
|
||||
this.currentPage = 1;
|
||||
},
|
||||
goPrev() {
|
||||
this.currentPage--;
|
||||
},
|
||||
goNext() {
|
||||
this.currentPage++;
|
||||
},
|
||||
goToPage(page) {
|
||||
this.currentPage = page;
|
||||
},
|
||||
getVisiblePages() {
|
||||
const pages = [];
|
||||
const totalPages = this.pageCount;
|
||||
const currentPage = this.currentPage;
|
||||
|
||||
if (totalPages <= 7) {
|
||||
for (let i = 1; i <= totalPages; i++) {
|
||||
pages.push(i);
|
||||
}
|
||||
} else {
|
||||
if (currentPage <= 4) {
|
||||
for (let i = 1; i <= 5; i++) {
|
||||
pages.push(i);
|
||||
}
|
||||
pages.push("...");
|
||||
pages.push(totalPages);
|
||||
} else if (currentPage >= totalPages - 3) {
|
||||
pages.push(1);
|
||||
pages.push("...");
|
||||
for (let i = totalPages - 4; i <= totalPages; i++) {
|
||||
pages.push(i);
|
||||
}
|
||||
} else {
|
||||
pages.push(1);
|
||||
pages.push("...");
|
||||
for (let i = currentPage - 1; i <= currentPage + 1; i++) {
|
||||
pages.push(i);
|
||||
}
|
||||
pages.push("...");
|
||||
pages.push(totalPages);
|
||||
}
|
||||
}
|
||||
|
||||
return pages;
|
||||
},
|
||||
|
||||
// 修改handleSelectAll方法
|
||||
handleSelectAll() {
|
||||
this.isAllSelected = !this.isAllSelected;
|
||||
this.templateList.forEach((row) => {
|
||||
row.selected = this.isAllSelected;
|
||||
});
|
||||
// 更新选中的模板列表
|
||||
this.selectedTemplates = this.isAllSelected ? [...this.templateList] : [];
|
||||
},
|
||||
|
||||
// 处理行选择变化
|
||||
handleRowSelectionChange(row) {
|
||||
// 查找选中的模板
|
||||
this.selectedTemplates = this.templateList.filter((template) => template.selected);
|
||||
// 更新全选状态
|
||||
this.isAllSelected =
|
||||
this.templateList.length > 0 &&
|
||||
this.selectedTemplates.length === this.templateList.length;
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
/* 基础背景和布局设置 */
|
||||
<style lang="scss" scoped>
|
||||
.welcome {
|
||||
min-height: 100vh;
|
||||
min-width: 900px;
|
||||
min-height: 506px;
|
||||
height: 100vh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
position: relative;
|
||||
background: linear-gradient(to bottom right, #dce8ff, #e4eeff, #e6cbfd) center;
|
||||
flex-direction: column;
|
||||
background: #eff4ff;
|
||||
background-size: cover;
|
||||
overflow: hidden;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
/* 操作栏样式 */
|
||||
.operation-bar {
|
||||
.main-wrapper {
|
||||
height: calc(100vh - 63px - 35px);
|
||||
padding: 20px 22px 0;
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.operation-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 16px 24px;
|
||||
justify-content: space-between;
|
||||
padding: 0 0 16px 0;
|
||||
}
|
||||
|
||||
.page-title {
|
||||
font-weight: 500;
|
||||
font-size: 24px;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.right-operations {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
margin-left: auto;
|
||||
}
|
||||
|
||||
.search-input {
|
||||
width: 200px;
|
||||
}
|
||||
|
||||
.btn-search {
|
||||
background: linear-gradient(135deg, #6b8cff, #a966ff);
|
||||
border: none;
|
||||
color: white;
|
||||
}
|
||||
|
||||
/* 主容器样式 */
|
||||
.main-wrapper {
|
||||
// 顶部 63px 底部 35px 查询72px
|
||||
height: calc(100vh - 63px - 35px - 72px);
|
||||
margin: 0 22px;
|
||||
border-radius: 15px;
|
||||
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;
|
||||
width: 240px;
|
||||
}
|
||||
|
||||
.content-panel {
|
||||
width: 100%;
|
||||
flex: 1;
|
||||
display: flex;
|
||||
overflow: hidden;
|
||||
height: 100%;
|
||||
border-radius: 15px;
|
||||
background: transparent;
|
||||
border: 1px solid #fff;
|
||||
@@ -506,25 +449,25 @@ export default {
|
||||
|
||||
.content-area {
|
||||
flex: 1;
|
||||
height: 100%;
|
||||
min-width: 600px;
|
||||
overflow-x: auto;
|
||||
overflow: auto;
|
||||
background-color: white;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
/* 模板卡片样式 */
|
||||
.template-card {
|
||||
border: none;
|
||||
box-shadow: none;
|
||||
background: white;
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
border: none;
|
||||
box-shadow: none;
|
||||
overflow: hidden;
|
||||
|
||||
:deep(.el-card__body) {
|
||||
padding: 15px;
|
||||
::v-deep .el-card__body {
|
||||
padding: 14px 20px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex: 1;
|
||||
@@ -532,106 +475,15 @@ export default {
|
||||
}
|
||||
}
|
||||
|
||||
/* 表格样式 - 优化整合版 */
|
||||
.transparent-table {
|
||||
width: 100%;
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
:deep(.el-table) {
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
--table-max-height: calc(100vh - 42vh);
|
||||
max-height: var(--table-max-height);
|
||||
|
||||
/* 表格头部样式 */
|
||||
.el-table__header th {
|
||||
padding: 8px 0 !important;
|
||||
height: 40px !important;
|
||||
}
|
||||
|
||||
.el-table__header th .cell {
|
||||
color: #303133 !important;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
/* 表格主体样式 */
|
||||
.el-table__body {
|
||||
.el-table__row td {
|
||||
padding: 12px 0 !important;
|
||||
border-bottom: 1px solid #ebeef5;
|
||||
}
|
||||
.el-table__row:hover {
|
||||
background-color: #f5f7fa;
|
||||
}
|
||||
}
|
||||
|
||||
/* 表格按钮样式 */
|
||||
.el-button--text {
|
||||
color: #7079aa;
|
||||
}
|
||||
|
||||
.el-button--text:hover {
|
||||
color: #5a64b5;
|
||||
}
|
||||
|
||||
/* 单元格文本样式 */
|
||||
.cell {
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
}
|
||||
|
||||
/* 表格底部操作栏 */
|
||||
.table_bottom {
|
||||
display: flex;
|
||||
justify-content: space-between !important;
|
||||
align-items: center;
|
||||
margin-top: auto;
|
||||
padding: 0 20px !important;
|
||||
width: 100% !important;
|
||||
box-sizing: border-box !important;
|
||||
}
|
||||
|
||||
/* 控制按钮样式 */
|
||||
.ctrl_btn {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
padding-left: 0 !important;
|
||||
margin-left: 0 !important;
|
||||
|
||||
.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--success {
|
||||
background: #5bc98c;
|
||||
color: white;
|
||||
}
|
||||
.el-button--danger {
|
||||
background: #fd5b63;
|
||||
color: white;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
:deep(.el-table .el-button--text) {
|
||||
color: #7079aa;
|
||||
}
|
||||
|
||||
:deep(.el-table .el-button--text:hover) {
|
||||
color: #5a64b5;
|
||||
}
|
||||
</style>
|
||||
@@ -2,114 +2,83 @@
|
||||
<div class="welcome">
|
||||
<HeaderBar />
|
||||
|
||||
<div class="operation-bar">
|
||||
<h2 class="page-title">{{ $t('device.management') }}</h2>
|
||||
<div class="right-operations">
|
||||
<el-input :placeholder="$t('device.searchPlaceholder')" v-model="searchKeyword" class="search-input"
|
||||
@keyup.enter.native="handleSearch" clearable />
|
||||
<el-button class="btn-search" @click="handleSearch">{{ $t('device.search') }}</el-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<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="$t('deviceManagement.loading')" element-loading-spinner="el-icon-loading"
|
||||
element-loading-background="rgba(255, 255, 255, 0.7)">
|
||||
<el-table-column :label="$t('modelConfig.select')" 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="$t('device.model')" prop="model" align="center">
|
||||
<template slot-scope="scope">
|
||||
{{ getFirmwareTypeName(scope.row.model) }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column :label="$t('device.firmwareVersion')" prop="firmwareVersion"
|
||||
align="center"></el-table-column>
|
||||
<el-table-column :label="$t('device.macAddress')" prop="macAddress" align="center"></el-table-column>
|
||||
<el-table-column :label="$t('device.bindTime')" prop="bindTime" align="center"></el-table-column>
|
||||
<el-table-column :label="$t('device.lastConversation')" prop="lastConversation"
|
||||
align="center"></el-table-column>
|
||||
<el-table-column v-if="mqttServiceAvailable" :label="$t('device.deviceStatus')" prop="deviceStatus" align="center">
|
||||
<template slot-scope="scope">
|
||||
<el-tag v-if="scope.row.deviceStatus === 'online'" type="success">{{ $t('device.online') }}</el-tag>
|
||||
<el-tag v-else type="danger">{{ $t('device.offline') }}</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column :label="$t('device.remark')" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-input v-show="row.isEdit" v-model="row.remark" size="mini" maxlength="64" show-word-limit
|
||||
@blur="onRemarkBlur(row)" @keyup.enter.native="onRemarkEnter(row)" />
|
||||
<span v-show="!row.isEdit" class="remark-view">
|
||||
<i class="el-icon-edit" @click="row.isEdit = true" style="cursor: pointer;"></i>
|
||||
<span @click="row.isEdit = true">
|
||||
{{ row.remark || '-' }}
|
||||
</span>
|
||||
</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column :label="$t('device.autoUpdate')" align="center">
|
||||
<template slot-scope="scope">
|
||||
<el-switch v-model="scope.row.otaSwitch" size="mini" active-color="#13ce66" inactive-color="#ff4949"
|
||||
@change="handleOtaSwitchChange(scope.row)"></el-switch>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column :label="$t('device.operation')" align="center">
|
||||
<template slot-scope="scope">
|
||||
<el-button size="mini" type="text" @click="handleUnbind(scope.row.device_id)">
|
||||
{{ $t('device.unbind') }}
|
||||
</el-button>
|
||||
<el-button v-if="isGenerate(scope.row)" size="mini" type="text" @click="handleGenertor(scope.row)">
|
||||
{{ $t('device.deviceThemeGeneration') }}
|
||||
</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">
|
||||
{{ isCurrentPageAllSelected ? $t('common.deselectAll') : $t('common.selectAll') }}
|
||||
</el-button>
|
||||
<el-button type="success" size="mini" class="add-device-btn" @click="handleAddDevice">
|
||||
{{ $t('device.bindWithCode') }}
|
||||
</el-button>
|
||||
<el-button type="success" size="mini" class="add-device-btn" @click="handleManualAddDevice">
|
||||
{{ $t('device.manualAdd') }}
|
||||
</el-button>
|
||||
<el-button size="mini" type="danger" icon="el-icon-delete" @click="deleteSelected">
|
||||
{{ $t('device.unbind') }}
|
||||
</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="$t('dictManagement.itemsPerPage').replace('{items}', item)" :value="item">
|
||||
</el-option>
|
||||
</el-select>
|
||||
<button class="pagination-btn" :disabled="currentPage === 1" @click="goFirst">
|
||||
{{ $t('dictManagement.firstPage') }}
|
||||
</button>
|
||||
<button class="pagination-btn" :disabled="currentPage === 1" @click="goPrev">
|
||||
{{ $t('dictManagement.prevPage') }}
|
||||
</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">
|
||||
{{ $t('dictManagement.nextPage') }}
|
||||
</button>
|
||||
<span class="total-text">
|
||||
{{ $t('dictManagement.totalRecords').replace('{total}', deviceList.length) }}
|
||||
</span>
|
||||
<div class="operation-header">
|
||||
<h2 class="page-title">{{ $t('device.management') }}</h2>
|
||||
<div class="right-operations">
|
||||
<el-input :placeholder="$t('device.searchPlaceholder')" v-model="searchKeyword" class="search-input"
|
||||
@keyup.enter.native="handleSearch" clearable />
|
||||
<CustomButton icon="el-icon-search" type="confirm" @click="handleSearch">{{ $t('device.search') }}</CustomButton>
|
||||
</div>
|
||||
</div>
|
||||
<CustomTable
|
||||
ref="deviceTable"
|
||||
:data="paginatedDeviceList"
|
||||
:columns="tableColumns"
|
||||
:loading="loading"
|
||||
:loading-text="$t('deviceManagement.loading')"
|
||||
:show-selection="true"
|
||||
:show-operations="true"
|
||||
:operations-label="$t('device.operation')"
|
||||
:total="filteredDeviceList.length"
|
||||
:current-page="currentPage"
|
||||
:page-size="pageSize"
|
||||
:page-size-options="pageSizeOptions"
|
||||
@size-change="handlePageSizeChange"
|
||||
@page-change="goToPage"
|
||||
>
|
||||
<template slot="model" slot-scope="scope">
|
||||
{{ getFirmwareTypeName(scope.row.model) }}
|
||||
</template>
|
||||
<template slot="macAddress" slot-scope="scope">
|
||||
<MacAddressMask :macAddress="scope.row.macAddress" />
|
||||
</template>
|
||||
<template slot="deviceStatus" slot-scope="scope">
|
||||
<el-tag v-if="scope.row.deviceStatus === 'online'" type="success">{{ $t('device.online') }}</el-tag>
|
||||
<el-tag v-else type="danger">{{ $t('device.offline') }}</el-tag>
|
||||
</template>
|
||||
<template slot="remark" slot-scope="scope">
|
||||
<el-input v-show="scope.row.isEdit" v-model="scope.row.remark" size="mini" maxlength="64" show-word-limit
|
||||
@blur="onRemarkBlur(scope.row)" @keyup.enter.native="onRemarkEnter(scope.row)" />
|
||||
<span v-show="!scope.row.isEdit" class="remark-view">
|
||||
<i class="el-icon-edit" @click="scope.row.isEdit = true" style="cursor: pointer;"></i>
|
||||
<span @click="scope.row.isEdit = true">
|
||||
{{ scope.row.remark || '-' }}
|
||||
</span>
|
||||
</span>
|
||||
</template>
|
||||
<template slot="otaSwitch" slot-scope="scope">
|
||||
<el-switch v-model="scope.row.otaSwitch" size="mini" active-color="#13ce66" inactive-color="#ff4949"
|
||||
@change="handleOtaSwitchChange(scope.row)"></el-switch>
|
||||
</template>
|
||||
<template slot="operations" slot-scope="scope">
|
||||
<el-button size="mini" type="text" @click="handleUnbind(scope.row.device_id)">
|
||||
{{ $t('device.unbind') }}
|
||||
</el-button>
|
||||
<el-button v-if="isGenerate(scope.row)" size="mini" type="text" @click="handleGenertor(scope.row)">
|
||||
{{ $t('device.deviceThemeGeneration') }}
|
||||
</el-button>
|
||||
</template>
|
||||
<template slot="footer-btns">
|
||||
<div class="ctrl_btn">
|
||||
<CustomButton :icon="isCurrentPageAllSelected ? 'el-icon-circle-close' : 'el-icon-circle-check'" size="small" @click="handleSelectAll">
|
||||
{{ isCurrentPageAllSelected ? $t('common.deselectAll') : $t('common.selectAll') }}
|
||||
</CustomButton>
|
||||
<CustomButton icon="el-icon-plus" type="add" size="small" @click="handleAddDevice">
|
||||
{{ $t('device.bindWithCode') }}
|
||||
</CustomButton>
|
||||
<CustomButton icon="el-icon-plus" type="add" size="small" @click="handleManualAddDevice">
|
||||
{{ $t('device.manualAdd') }}
|
||||
</CustomButton>
|
||||
<CustomButton size="small" type="delete" icon="el-icon-delete" @click="deleteSelected">
|
||||
{{ $t('device.unbind') }}
|
||||
</CustomButton>
|
||||
</div>
|
||||
</template>
|
||||
</CustomTable>
|
||||
</el-card>
|
||||
</div>
|
||||
</div>
|
||||
@@ -131,13 +100,19 @@ import AddDeviceDialog from "@/components/AddDeviceDialog.vue";
|
||||
import HeaderBar from "@/components/HeaderBar.vue";
|
||||
import ManualAddDeviceDialog from "@/components/ManualAddDeviceDialog.vue";
|
||||
import VersionFooter from "@/components/VersionFooter.vue";
|
||||
import MacAddressMask from "@/components/MacAddressMask.vue";
|
||||
import CustomButton from "@/components/CustomButton.vue";
|
||||
import CustomTable from "@/components/CustomTable.vue";
|
||||
|
||||
export default {
|
||||
components: {
|
||||
HeaderBar,
|
||||
AddDeviceDialog,
|
||||
ManualAddDeviceDialog,
|
||||
VersionFooter
|
||||
VersionFooter,
|
||||
MacAddressMask,
|
||||
CustomButton,
|
||||
CustomTable,
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
@@ -154,7 +129,7 @@ export default {
|
||||
loading: false,
|
||||
userApi: null,
|
||||
firmwareTypes: [],
|
||||
mqttServiceAvailable: false, // MQTT服务是否可用
|
||||
mqttServiceAvailable: false,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
@@ -172,28 +147,24 @@ export default {
|
||||
const end = start + this.pageSize;
|
||||
return this.filteredDeviceList.slice(start, end);
|
||||
},
|
||||
pageCount() {
|
||||
return Math.ceil(this.filteredDeviceList.length / this.pageSize);
|
||||
},
|
||||
// 计算当前页是否全选
|
||||
isCurrentPageAllSelected() {
|
||||
return this.paginatedDeviceList.length > 0 &&
|
||||
this.paginatedDeviceList.every(device => device.selected);
|
||||
},
|
||||
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);
|
||||
tableColumns() {
|
||||
const columns = [
|
||||
{ prop: 'model', label: this.$t('device.model'), align: 'center' },
|
||||
{ prop: 'firmwareVersion', label: this.$t('device.firmwareVersion'), align: 'center' },
|
||||
{ prop: 'macAddress', label: this.$t('device.macAddress'), align: 'center' },
|
||||
{ prop: 'bindTime', label: this.$t('device.bindTime'), align: 'center' },
|
||||
{ prop: 'lastConversation', label: this.$t('device.lastConversation'), align: 'center' },
|
||||
];
|
||||
if (this.mqttServiceAvailable) {
|
||||
columns.push({ prop: 'deviceStatus', label: this.$t('device.deviceStatus'), align: 'center' });
|
||||
}
|
||||
|
||||
for (let i = start; i <= end; i++) {
|
||||
pages.push(i);
|
||||
}
|
||||
return pages;
|
||||
columns.push({ prop: 'remark', label: this.$t('device.remark'), align: 'center' });
|
||||
columns.push({ prop: 'otaSwitch', label: this.$t('device.autoUpdate'), align: 'center' });
|
||||
return columns;
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
@@ -307,14 +278,12 @@ export default {
|
||||
row._submitting = false;
|
||||
});
|
||||
},
|
||||
// 备注输入框:失焦时提交
|
||||
onRemarkBlur(row) {
|
||||
row.isEdit = false;
|
||||
setTimeout(() => {
|
||||
this.submitRemark(row);
|
||||
}, 100); // 延迟 100ms,避开 enter+blur 同时触发的窗口
|
||||
}, 100);
|
||||
},
|
||||
// 备注输入框:按回车时提交
|
||||
onRemarkEnter(row) {
|
||||
row.isEdit = false;
|
||||
this.submitRemark(row);
|
||||
@@ -348,15 +317,6 @@ export default {
|
||||
sessionStorage.setItem('devicePath', window.location.href);
|
||||
window.location.href = url;
|
||||
},
|
||||
goFirst() {
|
||||
this.currentPage = 1;
|
||||
},
|
||||
goPrev() {
|
||||
if (this.currentPage > 1) this.currentPage--;
|
||||
},
|
||||
goNext() {
|
||||
if (this.currentPage < this.pageCount) this.currentPage++;
|
||||
},
|
||||
goToPage(page) {
|
||||
this.currentPage = page;
|
||||
},
|
||||
@@ -383,7 +343,6 @@ export default {
|
||||
otaSwitch: device.autoUpdate === 1,
|
||||
rawBindTime: new Date(device.createDate).getTime(),
|
||||
selected: false,
|
||||
// 初始设置为离线状态
|
||||
deviceStatus: 'offline'
|
||||
};
|
||||
})
|
||||
@@ -391,7 +350,6 @@ export default {
|
||||
this.activeSearchKeyword = "";
|
||||
this.searchKeyword = "";
|
||||
|
||||
// 获取设备列表后,立即获取设备状态
|
||||
this.fetchDeviceStatus(agentId);
|
||||
} else {
|
||||
this.$message.error(data.msg || this.$t('device.getListFailed'));
|
||||
@@ -399,47 +357,35 @@ export default {
|
||||
});
|
||||
},
|
||||
|
||||
// 获取设备状态
|
||||
fetchDeviceStatus(agentId) {
|
||||
// 开启表格等待状态,处理动态加载表头导致鼠标所在行的hover事件无法移除的问题
|
||||
this.loading = true;
|
||||
Api.device.getDeviceStatus(agentId, ({ data }) => {
|
||||
this.loading = false;
|
||||
if (data.code === 0) {
|
||||
try {
|
||||
// 解析后端返回的设备状态JSON
|
||||
const statusData = JSON.parse(data.data);
|
||||
|
||||
// 直接使用解析后的数据作为设备状态映射(不需要devices字段包装)
|
||||
if (statusData && typeof statusData === 'object') {
|
||||
// 成功获取到设备状态
|
||||
this.mqttServiceAvailable = true;
|
||||
// 更新设备状态
|
||||
this.updateDeviceStatusFromResponse(statusData);
|
||||
} else {
|
||||
// 数据格式不正确,MQTT服务不可用
|
||||
this.mqttServiceAvailable = false;
|
||||
}
|
||||
} catch (error) {
|
||||
// JSON解析失败,MQTT服务不可用
|
||||
this.mqttServiceAvailable = false;
|
||||
}
|
||||
} else {
|
||||
// 接口调用失败,MQTT服务不可用
|
||||
this.mqttServiceAvailable = false;
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
// 根据API响应更新设备状态
|
||||
updateDeviceStatusFromResponse(deviceStatusMap) {
|
||||
this.deviceList.forEach(device => {
|
||||
// 构建设备的MQTT客户端ID
|
||||
const macAddress = device.macAddress ? device.macAddress.replace(/:/g, '_') : 'unknown';
|
||||
const groupId = device.model ? device.model.replace(/:/g, '_') : 'GID_default';
|
||||
const mqttClientId = `${groupId}@@@${macAddress}@@@${macAddress}`;
|
||||
|
||||
// 从状态映射中获取设备状态
|
||||
if (deviceStatusMap[mqttClientId]) {
|
||||
const statusInfo = deviceStatusMap[mqttClientId];
|
||||
|
||||
@@ -456,17 +402,10 @@ export default {
|
||||
|
||||
device.deviceStatus = isOnline ? 'online' : 'offline';
|
||||
} else {
|
||||
// 如果没有找到对应的状态信息,默认为离线
|
||||
device.deviceStatus = 'offline';
|
||||
}
|
||||
});
|
||||
},
|
||||
headerCellClassName({ columnIndex }) {
|
||||
if (columnIndex === 0) {
|
||||
return "custom-selection-header";
|
||||
}
|
||||
return "";
|
||||
},
|
||||
getFirmwareTypeName(type) {
|
||||
const firmwareType = this.firmwareTypes.find(item => item.key === type)
|
||||
return firmwareType ? firmwareType.name : type
|
||||
@@ -486,7 +425,6 @@ export default {
|
||||
this.$message.error(msg || this.$t('message.error'))
|
||||
})
|
||||
},
|
||||
// 判断是否可以生成表情、主题、字体bin文件
|
||||
isGenerate(row) {
|
||||
const version = row.firmwareVersion.replace(/\./g, '');
|
||||
return Number(version) >= 200;
|
||||
@@ -497,13 +435,13 @@ export default {
|
||||
if (isNaN(ts)) return '-';
|
||||
const date = new Date(ts);
|
||||
if (isNaN(date.getTime())) return '-';
|
||||
return date.toLocaleString(); // 自动适配本地时区
|
||||
return date.toLocaleString();
|
||||
},
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
<style lang="scss" scoped>
|
||||
.welcome {
|
||||
min-width: 900px;
|
||||
min-height: 506px;
|
||||
@@ -511,34 +449,33 @@ export default {
|
||||
display: flex;
|
||||
position: relative;
|
||||
flex-direction: column;
|
||||
background: linear-gradient(to bottom right, #dce8ff, #e4eeff, #e6cbfd);
|
||||
background: #eff4ff;
|
||||
background-size: cover;
|
||||
-webkit-background-size: cover;
|
||||
-o-background-size: cover;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.main-wrapper {
|
||||
height: calc(100vh - 63px - 35px - 72px);
|
||||
margin: 0 22px;
|
||||
border-radius: 15px;
|
||||
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.1);
|
||||
height: calc(100vh - 63px - 35px);
|
||||
padding: 20px 22px 0;
|
||||
position: relative;
|
||||
background: rgba(237, 242, 255, 0.5);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.operation-bar {
|
||||
.operation-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 16px 24px;
|
||||
justify-content: space-between;
|
||||
padding: 0 0 16px 0;
|
||||
}
|
||||
|
||||
.page-title {
|
||||
font-weight: 500;
|
||||
font-size: 24px;
|
||||
margin: 0;
|
||||
color: #2c3e50;
|
||||
}
|
||||
|
||||
.right-operations {
|
||||
@@ -548,74 +485,10 @@ export default {
|
||||
}
|
||||
|
||||
.search-input {
|
||||
width: 280px;
|
||||
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;
|
||||
width: 240px;
|
||||
}
|
||||
|
||||
.content-panel {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
overflow: hidden;
|
||||
height: 100%;
|
||||
@@ -636,161 +509,33 @@ export default {
|
||||
|
||||
.device-card {
|
||||
background: white;
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
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;
|
||||
::v-deep .el-card__body {
|
||||
padding: 14px 20px;
|
||||
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);
|
||||
:deep(.el-table .el-button--text) {
|
||||
color: #7079aa;
|
||||
}
|
||||
|
||||
.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-table .el-button--text:hover) {
|
||||
color: #5a64b5;
|
||||
}
|
||||
|
||||
:deep(.el-icon-edit) {
|
||||
@@ -801,65 +546,4 @@ export default {
|
||||
: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: #ffffff !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>
|
||||
|
||||
@@ -1,38 +1,38 @@
|
||||
<template>
|
||||
<div class="welcome">
|
||||
<HeaderBar />
|
||||
|
||||
<div class="operation-bar">
|
||||
<h2 class="page-title">{{ $t('header.featureManagement') }}</h2>
|
||||
</div>
|
||||
|
||||
<div class="main-wrapper">
|
||||
<div class="content-panel">
|
||||
<div class="content-area">
|
||||
<el-card class="feature-card" shadow="never">
|
||||
<div class="config-header">
|
||||
<div class="header-icon">
|
||||
<img loading="lazy" src="@/assets/home/equipment.png" alt="" />
|
||||
</div>
|
||||
<h2 class="page-title">{{ $t('header.featureManagement') }}</h2>
|
||||
<div class="header-actions">
|
||||
<el-button @click="!isSaving && toggleSelectAll()" class="btn-select-all" :disabled="isSaving">
|
||||
<CustomButton :icon="isAllSelected ? 'el-icon-circle-close' : 'el-icon-circle-check'" size="medium" @click="!isSaving && toggleSelectAll()" :disabled="isSaving">
|
||||
{{ isAllSelected ? $t('featureManagement.deselectAll') : $t('featureManagement.selectAll') }}
|
||||
</el-button>
|
||||
<el-button type="primary" class="save-btn" @click="handleSave" :disabled="isSaving">
|
||||
{{ isSaving ? $t('featureManagement.saving') : $t('featureManagement.save') }}
|
||||
</el-button>
|
||||
<el-button class="reset-btn" @click="handleReset" :disabled="isSaving">
|
||||
</CustomButton>
|
||||
<CustomButton icon="el-icon-refresh" size="medium" @click="handleReset" :disabled="isSaving">
|
||||
{{ $t('featureManagement.reset') }}
|
||||
</el-button>
|
||||
</CustomButton>
|
||||
<CustomButton icon="el-icon-setting" size="medium" type="confirm" @click="handleSave" :disabled="isSaving">
|
||||
{{ isSaving ? $t('featureManagement.saving') : $t('featureManagement.save') }}
|
||||
</CustomButton>
|
||||
</div>
|
||||
</div>
|
||||
<div class="divider"></div>
|
||||
|
||||
|
||||
<!-- 功能分组容器 - 左右布局 -->
|
||||
<div class="feature-groups-container">
|
||||
<!-- 功能管理分组 -->
|
||||
<div v-if="featureManagementFeatures.length > 0" class="feature-group">
|
||||
<h3 class="group-title">{{ $t('featureManagement.group.featureManagement') }}</h3>
|
||||
<div class="group-title">
|
||||
<img src="@/assets/setting/menu.png" alt="" width="28" height="28">
|
||||
<div class="group-header">
|
||||
<span class="group-name">{{ $t('featureManagement.groupName.featureManagement') }}</span>
|
||||
<span class="module-count">{{ $t('featureManagement.moduleCount', { count: featureManagementFeatures.length }) }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<p class="group-description">{{ $t('featureManagement.groupDescription.featureManagement') }}</p>
|
||||
<div class="features-grid">
|
||||
<div
|
||||
v-for="feature in featureManagementFeatures"
|
||||
@@ -41,23 +41,35 @@
|
||||
:class="{ 'feature-enabled': feature.enabled, 'feature-disabled': isSaving }"
|
||||
@click="!isSaving && toggleFeature(feature)"
|
||||
>
|
||||
<div class="feature-header">
|
||||
<h3 class="feature-name">{{ $t(`feature.${feature.id}.name`) }}</h3>
|
||||
<el-checkbox
|
||||
v-model="feature.enabled"
|
||||
@change="!isSaving && toggleFeature(feature)"
|
||||
class="feature-checkbox"
|
||||
:disabled="isSaving"
|
||||
/>
|
||||
<img :src="featureIcons[feature.id]" alt="" width="60" height="60">
|
||||
<div class="feature-content">
|
||||
<div class="feature-header">
|
||||
<h3 class="feature-name">{{ $t(`feature.${feature.id}.name`) }}</h3>
|
||||
<div class="feature-checkbox-container">
|
||||
<el-checkbox
|
||||
v-model="feature.enabled"
|
||||
@change="!isSaving && toggleFeature(feature)"
|
||||
class="feature-checkbox"
|
||||
:disabled="isSaving"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<p class="feature-description">{{ $t(`feature.${feature.id}.description`) }}</p>
|
||||
</div>
|
||||
<p class="feature-description">{{ $t(`feature.${feature.id}.description`) }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<!-- 语音管理分组 -->
|
||||
<div v-if="voiceManagementFeatures.length > 0" class="feature-group">
|
||||
<h3 class="group-title">{{ $t('featureManagement.group.voiceManagement') }}</h3>
|
||||
<div class="group-title">
|
||||
<img src="@/assets/setting/agent.png" alt="" width="28" height="28">
|
||||
<div class="group-header">
|
||||
<span class="group-name">{{ $t('featureManagement.groupName.voiceManagement') }}</span>
|
||||
<span class="module-count">{{ $t('featureManagement.moduleCount', { count: voiceManagementFeatures.length }) }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<p class="group-description">{{ $t('featureManagement.groupDescription.voiceManagement') }}</p>
|
||||
<div class="features-grid">
|
||||
<div
|
||||
v-for="feature in voiceManagementFeatures"
|
||||
@@ -66,16 +78,21 @@
|
||||
:class="{ 'feature-enabled': feature.enabled, 'feature-disabled': isSaving }"
|
||||
@click="!isSaving && toggleFeature(feature)"
|
||||
>
|
||||
<div class="feature-header">
|
||||
<h3 class="feature-name">{{ $t(`feature.${feature.id}.name`) }}</h3>
|
||||
<el-checkbox
|
||||
v-model="feature.enabled"
|
||||
@change="!isSaving && toggleFeature(feature)"
|
||||
class="feature-checkbox"
|
||||
:disabled="isSaving"
|
||||
/>
|
||||
<img :src="voiceManagementIcons[feature.id]" alt="" width="60" height="60">
|
||||
<div class="feature-content">
|
||||
<div class="feature-header">
|
||||
<h3 class="feature-name">{{ $t(`feature.${feature.id}.name`) }}</h3>
|
||||
<div class="feature-checkbox-container">
|
||||
<el-checkbox
|
||||
v-model="feature.enabled"
|
||||
@change="!isSaving && toggleFeature(feature)"
|
||||
class="feature-checkbox"
|
||||
:disabled="isSaving"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<p class="feature-description">{{ $t(`feature.${feature.id}.description`) }}</p>
|
||||
</div>
|
||||
<p class="feature-description">{{ $t(`feature.${feature.id}.description`) }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -101,19 +118,41 @@
|
||||
import HeaderBar from "@/components/HeaderBar.vue";
|
||||
import VersionFooter from "@/components/VersionFooter.vue";
|
||||
import featureManager from "@/utils/featureManager.js";
|
||||
import CustomButton from "@/components/CustomButton.vue";
|
||||
|
||||
import voiceprintIcon from '@/assets/setting/voiceprint.png'
|
||||
import voiceCloneIcon from '@/assets/setting/voiceclone.png'
|
||||
import knowledgeBaseIcon from '@/assets/setting/knowledgeBase.png'
|
||||
import mcpAccessPointIcon from '@/assets/setting/mcpAccessPoint.png'
|
||||
import addressBookIcon from '@/assets/setting/addressBook.png'
|
||||
import vadIcon from '@/assets/setting/vad.png'
|
||||
import asrIcon from '@/assets/setting/asr.png'
|
||||
|
||||
export default {
|
||||
name: "FeatureManagement",
|
||||
components: {
|
||||
HeaderBar,
|
||||
VersionFooter
|
||||
VersionFooter,
|
||||
CustomButton
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
pendingChanges: false,
|
||||
featureManagementFeatures: [],
|
||||
voiceManagementFeatures: [],
|
||||
isSaving: false // 添加保存状态锁定
|
||||
isSaving: false, // 添加保存状态锁定
|
||||
// 功能模块图标映射
|
||||
featureIcons: {
|
||||
'voiceprintRecognition': voiceprintIcon,
|
||||
'voiceClone': voiceCloneIcon,
|
||||
'knowledgeBase': knowledgeBaseIcon,
|
||||
'mcpAccessPoint': mcpAccessPointIcon,
|
||||
'addressBook': addressBookIcon,
|
||||
},
|
||||
voiceManagementIcons: {
|
||||
'vad': vadIcon,
|
||||
'asr': asrIcon,
|
||||
},
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
@@ -317,7 +356,9 @@ export default {
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
<style lang="scss" scoped>
|
||||
@import '@/styles/global.scss';
|
||||
|
||||
.welcome {
|
||||
min-width: 900px;
|
||||
min-height: 506px;
|
||||
@@ -326,7 +367,7 @@ export default {
|
||||
position: relative;
|
||||
flex-direction: column;
|
||||
background-size: cover;
|
||||
background: linear-gradient(to bottom right, #dce8ff, #e4eeff, #e6cbfd) center;
|
||||
background: #eff4ff;
|
||||
-webkit-background-size: cover;
|
||||
-o-background-size: cover;
|
||||
overflow: hidden;
|
||||
@@ -336,10 +377,11 @@ export default {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 16px 24px;
|
||||
padding: 22px 24px;
|
||||
}
|
||||
|
||||
.page-title {
|
||||
font-weight: 500;
|
||||
font-size: 24px;
|
||||
margin: 0;
|
||||
}
|
||||
@@ -376,60 +418,18 @@ export default {
|
||||
|
||||
.divider {
|
||||
height: 1px;
|
||||
background: #e0e0e0;
|
||||
background: #f3f1f1;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.btn-select-all {
|
||||
background: #e6ebff;
|
||||
color: #5778ff;
|
||||
border: 1px solid #adbdff;
|
||||
border-radius: 18px;
|
||||
padding: 8px 16px;
|
||||
height: 32px;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.btn-select-all:hover {
|
||||
background: #d0d8ff;
|
||||
}
|
||||
|
||||
.save-btn {
|
||||
background: #5778ff;
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: 18px;
|
||||
padding: 8px 16px;
|
||||
height: 32px;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.save-btn:hover {
|
||||
background: #4a6ae8;
|
||||
}
|
||||
|
||||
.reset-btn {
|
||||
background: #e6ebff;
|
||||
color: #5778ff;
|
||||
border: 1px solid #adbdff;
|
||||
border-radius: 18px;
|
||||
padding: 8px 16px;
|
||||
height: 32px;
|
||||
}
|
||||
|
||||
.reset-btn:hover {
|
||||
background: #d0d8ff;
|
||||
}
|
||||
|
||||
.main-wrapper {
|
||||
height: calc(100vh - 63px - 35px - 58px);
|
||||
margin: 0 22px;
|
||||
height: calc(100vh - 63px - 35px);
|
||||
margin: 20px 22px 0;
|
||||
border-radius: 15px;
|
||||
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;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
.content-panel {
|
||||
@@ -463,11 +463,12 @@ export default {
|
||||
}
|
||||
|
||||
.feature-card ::v-deep .el-card__body {
|
||||
padding: 24px;
|
||||
padding: 14px 20px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex: 1;
|
||||
overflow: hidden;
|
||||
overflow: auto;
|
||||
@include scrollbar-style;
|
||||
}
|
||||
|
||||
.features-grid {
|
||||
@@ -478,9 +479,8 @@ export default {
|
||||
|
||||
.feature-card-item {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding: 20px;
|
||||
border-radius: 12px;
|
||||
border-radius: 10px;
|
||||
border: 2px solid #e0e0e0;
|
||||
background-color: white;
|
||||
cursor: pointer;
|
||||
@@ -491,25 +491,37 @@ export default {
|
||||
|
||||
.feature-card-item:hover {
|
||||
border-color: #869bf0;
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
|
||||
box-shadow: 0 4px 10px rgba(0, 0, 0, 0.15);
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
|
||||
.feature-card-item.feature-enabled {
|
||||
border-color:#5778ff;
|
||||
box-shadow: 0 4px 16px rgba(95, 112, 243, 0.2);
|
||||
border: 2px solid transparent;
|
||||
background: linear-gradient(white, white) padding-box,
|
||||
linear-gradient(to right, #4a7cfd, #8154fc) border-box;
|
||||
box-shadow: 0 4px 10px rgba(95, 112, 243, 0.2);
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
.feature-content {
|
||||
width: 100%;
|
||||
margin-left: 16px;
|
||||
}
|
||||
|
||||
.feature-header {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 6px;
|
||||
}
|
||||
.feature-checkbox-container {
|
||||
margin-top: 4px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-bottom: 12px;
|
||||
margin-left: 30px;
|
||||
}
|
||||
|
||||
.feature-checkbox ::v-deep .el-checkbox__input {
|
||||
transform: scale(1.2);
|
||||
transform: scale(1.5);
|
||||
}
|
||||
|
||||
.feature-checkbox ::v-deep .el-checkbox__input.is-checked .el-checkbox__inner {
|
||||
@@ -521,26 +533,27 @@ export default {
|
||||
color: #5778ff;
|
||||
}
|
||||
|
||||
|
||||
.feature-name {
|
||||
white-space: normal;
|
||||
word-break: break-word;
|
||||
overflow-wrap: break-word;
|
||||
font-size: 18px;
|
||||
line-height: 1.4;
|
||||
font-weight: 600;
|
||||
color: #333;
|
||||
margin: 0;
|
||||
transition: color 0.3s ease;
|
||||
}
|
||||
|
||||
|
||||
.feature-description {
|
||||
font-size: 14px;
|
||||
line-height: 1.6;
|
||||
color: #666;
|
||||
margin: 0 0 12px 0;
|
||||
margin: 0;
|
||||
transition: color 0.3s ease;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
|
||||
/* 功能分组容器 - 左右布局 */
|
||||
.feature-groups-container {
|
||||
display: flex;
|
||||
@@ -571,14 +584,40 @@ export default {
|
||||
}
|
||||
|
||||
.group-title {
|
||||
margin-top: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
font-size: 18px;
|
||||
line-height: 18px;
|
||||
font-weight: 600;
|
||||
color: #303133;
|
||||
margin-bottom: 12px;
|
||||
padding-left: 12px;
|
||||
border-left: 4px solid #5f70f3;
|
||||
margin-bottom: 0;
|
||||
text-align: left;
|
||||
}
|
||||
.group-name {
|
||||
margin-left: 10px;
|
||||
}
|
||||
|
||||
.group-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.module-count {
|
||||
margin-left: 13px;
|
||||
font-size: 12px;
|
||||
background: #ebebfe;
|
||||
color: #5778ff;
|
||||
padding: 2px 10px;
|
||||
border-radius: 10px;
|
||||
}
|
||||
|
||||
.group-description {
|
||||
margin-top: 6px;
|
||||
text-align: left;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.features-grid {
|
||||
display: grid;
|
||||
|
||||
@@ -0,0 +1,623 @@
|
||||
<template>
|
||||
<div class="doc-section">
|
||||
<div class="doc-section-header">
|
||||
<div class="doc-section-left">
|
||||
<div class="doc-section-title">{{`${$t('knowledgeBaseManagement.currentKnowledgeBaseDocuments')}(${kb?.name})`}}</div>
|
||||
<div class="doc-section-count" v-if="kb">{{ $t('knowledgeBaseManagement.totalDocuments', { total }) }}</div>
|
||||
</div>
|
||||
<div class="doc-section-actions">
|
||||
<el-input
|
||||
:placeholder="$t('knowledgeFileUpload.documentNamePlaceholder')"
|
||||
v-model="searchDocName"
|
||||
class="doc-search-input"
|
||||
size="small"
|
||||
prefix-icon="el-icon-search"
|
||||
clearable
|
||||
/>
|
||||
<CustomButton size="small" @click="handleSearch">{{ $t('knowledgeBaseManagement.search') }}</CustomButton>
|
||||
<CustomButton type="add" size="small" @click="handleUpload">{{ $t('knowledgeBaseManagement.add') }}</CustomButton>
|
||||
<CustomButton type="confirm" size="small" @click="handleRetrievalTest">{{ $t('knowledgeFileUpload.retrievalTest') }}</CustomButton>
|
||||
</div>
|
||||
</div>
|
||||
<div class="doc-grid" v-loading="docLoading" :element-loading-text="$t('knowledgeBaseManagement.loading')">
|
||||
<div v-for="doc in documents" :key="doc.id" class="doc-card">
|
||||
<div class="doc-card-top">
|
||||
<img class="doc-file-icon" :src="getFileIconSrc(doc.name)" />
|
||||
<div class="doc-card-info">
|
||||
<div class="doc-card-name" :title="doc.name">{{ doc.name }}</div>
|
||||
<div class="doc-card-meta">{{ $t('knowledgeFileUpload.uploadTime') }} {{ formatDate(doc.createdAt) }}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="doc-card-bottom">
|
||||
<div class="doc-card-progress">
|
||||
<div class="progress-ring">
|
||||
<svg viewBox="0 0 40 40">
|
||||
<circle class="bg" cx="20" cy="20" r="15"/>
|
||||
<circle class="fg" :class="getProgressClass(doc)" cx="20" cy="20" r="15"
|
||||
stroke-dasharray="94.25"
|
||||
:stroke-dashoffset="getProgressOffset(doc)"/>
|
||||
</svg>
|
||||
<span class="progress-ring-text" :style="{ color: getProgressTextColor(doc) }">{{ getProgressText(doc) }}</span>
|
||||
</div>
|
||||
<div class="progress-container">
|
||||
<span class="progress-label" :class="getProgressClass(doc)">{{ getDocStatusText(doc) }}</span>
|
||||
<span class="doc-slice-count">{{ $t('knowledgeFileUpload.sliceCount') }} <b>{{ doc.sliceCount }}</b></span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="doc-card-actions">
|
||||
<el-button
|
||||
v-if="doc.parseStatusCode === 3"
|
||||
type="text"
|
||||
@click.stop="$emit('view-slices', doc)"
|
||||
>
|
||||
{{ $t('knowledgeFileUpload.viewSlices') }}
|
||||
</el-button>
|
||||
<el-button
|
||||
v-else
|
||||
type="text"
|
||||
:disabled="doc.parseStatusCode === 1"
|
||||
:loading="doc.parsing"
|
||||
@click.stop="handleParse(doc)"
|
||||
>
|
||||
{{ $t('knowledgeFileUpload.parse') }}
|
||||
</el-button>
|
||||
<el-button type="text" class="delete-btn" :loading="deleteLoadingMap[doc.id]" @click.stop="handleDelete(doc)">
|
||||
{{ $t('knowledgeBaseManagement.delete') }}
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="documents.length === 0 && !docLoading" class="doc-empty">
|
||||
<el-empty :description="$t('knowledgeBaseManagement.noData')"></el-empty>
|
||||
</div>
|
||||
</div>
|
||||
<CustomPagination
|
||||
class="pagination-wrapper"
|
||||
:total="total"
|
||||
:current-page="currentPage"
|
||||
:page-size="pageSize"
|
||||
:page-size-options="pageSizeOptions"
|
||||
@page-change="handlePageChange"
|
||||
@size-change="handleSizeChange"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import Api from "@/apis/api";
|
||||
import CustomButton from "@/components/CustomButton.vue";
|
||||
import CustomPagination from "@/components/CustomPagination.vue";
|
||||
|
||||
const iconMap = {
|
||||
pdf: require('@/assets/knowledge-base/pdf.png'),
|
||||
doc: require('@/assets/knowledge-base/docX.png'),
|
||||
docx: require('@/assets/knowledge-base/docX.png'),
|
||||
xls: require('@/assets/knowledge-base/xlsx.png'),
|
||||
xlsx: require('@/assets/knowledge-base/xlsx.png'),
|
||||
csv: require('@/assets/knowledge-base/xlsx.png'),
|
||||
md: require('@/assets/knowledge-base/MD.png'),
|
||||
markdown: require('@/assets/knowledge-base/MD.png'),
|
||||
txt: require('@/assets/knowledge-base/txt.png'),
|
||||
};
|
||||
|
||||
export default {
|
||||
name: 'LibraryItem',
|
||||
components: { CustomPagination, CustomButton },
|
||||
props: {
|
||||
kb: {
|
||||
type: Object,
|
||||
default: null
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
documents: [],
|
||||
docLoading: false,
|
||||
deleteLoadingMap: {},
|
||||
searchDocName: '',
|
||||
total: 0,
|
||||
currentPage: 1,
|
||||
pageSize: 10,
|
||||
pageSizeOptions: [10, 20, 50, 100],
|
||||
};
|
||||
},
|
||||
watch: {
|
||||
kb: {
|
||||
handler(newKb) {
|
||||
if (newKb && newKb.datasetId) {
|
||||
this.currentPage = 1;
|
||||
this.fetchDocuments();
|
||||
} else {
|
||||
this.documents = [];
|
||||
this.total = 0;
|
||||
}
|
||||
},
|
||||
immediate: true
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
fetchDocuments() {
|
||||
if (!this.kb || !this.kb.datasetId) return;
|
||||
this.docLoading = true;
|
||||
const params = {
|
||||
page: this.currentPage,
|
||||
page_size: this.pageSize,
|
||||
name: this.searchDocName
|
||||
};
|
||||
Api.knowledgeBase.getDocumentList(
|
||||
this.kb.datasetId,
|
||||
params,
|
||||
({ data }) => {
|
||||
this.docLoading = false;
|
||||
if (data && data.code === 0) {
|
||||
this.documents = data.data.list || [];
|
||||
this.total = data.data.total || 0;
|
||||
this.fetchSliceCountsForDocuments();
|
||||
} else {
|
||||
this.$message.error(data?.msg || '获取文档列表失败');
|
||||
this.documents = [];
|
||||
this.total = 0;
|
||||
}
|
||||
},
|
||||
(err) => {
|
||||
this.docLoading = false;
|
||||
if (err && err.data) {
|
||||
this.$message.error(err.data.msg || err.msg || '获取文档列表失败');
|
||||
} else {
|
||||
this.$message.error('获取文档列表失败');
|
||||
}
|
||||
this.documents = [];
|
||||
this.total = 0;
|
||||
}
|
||||
);
|
||||
},
|
||||
|
||||
handleSearch() {
|
||||
this.currentPage = 1;
|
||||
this.fetchDocuments();
|
||||
},
|
||||
|
||||
handlePageChange(page) {
|
||||
this.currentPage = page;
|
||||
this.fetchDocuments();
|
||||
},
|
||||
|
||||
handleSizeChange(size) {
|
||||
this.pageSize = size;
|
||||
this.currentPage = 1;
|
||||
this.fetchDocuments();
|
||||
},
|
||||
|
||||
handleUpload() {
|
||||
this.$emit('upload');
|
||||
},
|
||||
|
||||
handleRetrievalTest() {
|
||||
this.$emit('retrieval-test');
|
||||
},
|
||||
|
||||
handleDelete(doc) {
|
||||
if (!this.kb) return;
|
||||
this.$confirm(
|
||||
this.$t('knowledgeFileUpload.confirmDelete'),
|
||||
this.$t('message.warning'),
|
||||
{
|
||||
confirmButtonText: this.$t('knowledgeBaseDialog.confirm'),
|
||||
cancelButtonText: this.$t('knowledgeBaseDialog.cancel'),
|
||||
type: 'warning'
|
||||
}
|
||||
).then(() => {
|
||||
this.$set(this.deleteLoadingMap, doc.id, true);
|
||||
Api.knowledgeBase.deleteDocument(this.kb.datasetId, doc.id, (res) => {
|
||||
this.$set(this.deleteLoadingMap, doc.id, false);
|
||||
if (res.data && res.data.code === 0) {
|
||||
if (this.documents.length === 1 && this.currentPage > 1) {
|
||||
this.currentPage--;
|
||||
}
|
||||
this.fetchDocuments();
|
||||
this.$message.success(this.$t('knowledgeFileUpload.deleteSuccess'));
|
||||
} else {
|
||||
this.$message.error(res.data?.msg || this.$t('knowledgeFileUpload.deleteFailed'));
|
||||
}
|
||||
}, (err) => {
|
||||
this.$set(this.deleteLoadingMap, doc.id, false);
|
||||
this.$message.error(err?.data?.msg ||this.$t('knowledgeFileUpload.deleteFailed'));
|
||||
});
|
||||
}).catch(() => {});
|
||||
},
|
||||
|
||||
handleParse(doc) {
|
||||
this.$confirm(this.$t('knowledgeFileUpload.confirmParse'), this.$t('message.info'), {
|
||||
confirmButtonText: this.$t('knowledgeFileUpload.confirm'),
|
||||
cancelButtonText: this.$t('knowledgeFileUpload.cancel'),
|
||||
type: 'info'
|
||||
}).then(() => {
|
||||
this.$set(doc, 'parsing', true);
|
||||
doc.parseStatusCode = 1;
|
||||
Api.knowledgeBase.parseDocument(
|
||||
this.kb.datasetId,
|
||||
doc.id,
|
||||
({ data }) => {
|
||||
if (data && data.code === 0) {
|
||||
this.$message.success(this.$t('knowledgeFileUpload.parsing'));
|
||||
this.$set(doc, 'parsing', false);
|
||||
this.startParsePolling(doc);
|
||||
} else {
|
||||
this.$set(doc, 'parsing', false);
|
||||
doc.parseStatusCode = 0;
|
||||
this.$message.error(data?.msg || this.$t('knowledgeFileUpload.parseFailed'));
|
||||
}
|
||||
},
|
||||
(err) => {
|
||||
this.$set(doc, 'parsing', false);
|
||||
doc.parseStatusCode = 0;
|
||||
this.$message.error(err?.data?.msg || this.$t('knowledgeFileUpload.parseFailed'));
|
||||
}
|
||||
);
|
||||
}).catch(() => {});
|
||||
},
|
||||
|
||||
startParsePolling(doc) {
|
||||
const poll = () => {
|
||||
if (!this.kb) return;
|
||||
Api.knowledgeBase.getDocumentList(
|
||||
this.kb.datasetId,
|
||||
{ page: 1, page_size: 100, name: this.searchDocName },
|
||||
({ data }) => {
|
||||
if (data && data.code === 0) {
|
||||
const list = data.data.list || [];
|
||||
const updated = list.find(d => d.id === doc.id);
|
||||
if (updated) {
|
||||
this.$set(doc, 'parseStatusCode', updated.parseStatusCode);
|
||||
if (updated.parseStatusCode !== 1) {
|
||||
this.$set(doc, 'parsing', false);
|
||||
if (updated.parseStatusCode === 3) {
|
||||
this.$message.success(`「${doc.name}」${this.$t('knowledgeFileUpload.parseSuccess')}`);
|
||||
this.fetchSliceCountForSingleDocument(doc);
|
||||
} else if (updated.parseStatusCode === 4) {
|
||||
this.$message.error(`「${doc.name}」${this.$t('knowledgeFileUpload.parseFailed')}`);
|
||||
}
|
||||
return;
|
||||
}
|
||||
}
|
||||
setTimeout(poll, 5000);
|
||||
}
|
||||
},
|
||||
() => {
|
||||
setTimeout(poll, 5000);
|
||||
}
|
||||
);
|
||||
};
|
||||
setTimeout(poll, 3000);
|
||||
},
|
||||
|
||||
fetchSliceCountsForDocuments() {
|
||||
if (!this.documents || this.documents.length === 0) return;
|
||||
this.documents.forEach(doc => {
|
||||
this.fetchSliceCountForSingleDocument(doc);
|
||||
});
|
||||
},
|
||||
|
||||
fetchSliceCountForSingleDocument(doc) {
|
||||
const params = { page: 1, page_size: 1 };
|
||||
Api.knowledgeBase.listChunks(
|
||||
this.kb.datasetId,
|
||||
doc.id,
|
||||
params,
|
||||
({ data }) => {
|
||||
if (data && data.code === 0) {
|
||||
this.$set(doc, 'sliceCount', data.data.total || 0);
|
||||
}
|
||||
},
|
||||
() => {}
|
||||
);
|
||||
},
|
||||
|
||||
getFileIconSrc(fileName) {
|
||||
if (!fileName) return require('@/assets/knowledge-base/default.png');
|
||||
const ext = fileName.split('.').pop().toLowerCase();
|
||||
return iconMap[ext] || require('@/assets/knowledge-base/default.png');
|
||||
},
|
||||
|
||||
getProgressClass(doc) {
|
||||
const code = doc.parseStatusCode;
|
||||
if (code === 3) return 'complete';
|
||||
if (code === 1) return 'processing';
|
||||
return 'pending';
|
||||
},
|
||||
|
||||
getProgressOffset(doc) {
|
||||
const code = doc.parseStatusCode;
|
||||
const circumference = 94.25;
|
||||
if (code === 3) return 0;
|
||||
if (code === 1) {
|
||||
const progress = doc.chunkNum && doc.totalChunkNum
|
||||
? Math.round((doc.chunkNum / doc.totalChunkNum) * 100)
|
||||
: 50;
|
||||
return circumference * (1 - progress / 100);
|
||||
}
|
||||
return circumference;
|
||||
},
|
||||
|
||||
getProgressText(doc) {
|
||||
const code = doc.parseStatusCode;
|
||||
if (code === 3) return '100%';
|
||||
if (code === 1) {
|
||||
if (doc.chunkNum && doc.totalChunkNum) {
|
||||
return Math.round((doc.chunkNum / doc.totalChunkNum) * 100) + '%';
|
||||
}
|
||||
return '50%';
|
||||
}
|
||||
return '0%';
|
||||
},
|
||||
|
||||
getProgressTextColor(doc) {
|
||||
const code = doc.parseStatusCode;
|
||||
if (code === 3) return '#34c759';
|
||||
if (code === 1) return '#2f5bff';
|
||||
return '#7888a8';
|
||||
},
|
||||
|
||||
getDocStatusText(doc) {
|
||||
const code = doc.parseStatusCode;
|
||||
if (code === 3) return this.$t('knowledgeFileUpload.statusCompleted');
|
||||
if (code === 1) return this.$t('knowledgeFileUpload.statusProcessing');
|
||||
if (code === 0) return this.$t('knowledgeFileUpload.statusNotStarted');
|
||||
if (code === 4) return this.$t('knowledgeFileUpload.statusFailed');
|
||||
if (code === 2) return this.$t('knowledgeFileUpload.statusCancelled');
|
||||
return this.$t('knowledgeFileUpload.statusNotStarted');
|
||||
},
|
||||
|
||||
formatDate(dateString) {
|
||||
if (!dateString) return '';
|
||||
const date = new Date(dateString);
|
||||
return `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, '0')}-${String(date.getDate()).padStart(2, '0')}`;
|
||||
},
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
/* ========== Document Section ========== */
|
||||
.doc-section {
|
||||
background: #fff;
|
||||
border-radius: 10px;
|
||||
padding: 16px 20px;
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
.doc-section-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.doc-section-left {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.doc-section-title {
|
||||
font-size: 18px;
|
||||
font-weight: 500;
|
||||
color: #1f2a44;
|
||||
}
|
||||
|
||||
.doc-section-count {
|
||||
font-size: 18px;
|
||||
color: #788ba8;
|
||||
}
|
||||
|
||||
.doc-section-actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.doc-search-input {
|
||||
width: 200px;
|
||||
margin-right: 10px;
|
||||
}
|
||||
|
||||
/* ========== Document Grid ========== */
|
||||
.doc-grid {
|
||||
padding: 10px;
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill,minmax(300px, 1fr));
|
||||
gap: 16px 20px;
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
overflow-y: auto;
|
||||
scrollbar-width: thin;
|
||||
}
|
||||
|
||||
.doc-card {
|
||||
max-height: 110px;
|
||||
border-radius: 8px;
|
||||
border: 1px solid transparent;
|
||||
padding: 18px;
|
||||
background: #fff;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
cursor: pointer;
|
||||
transition: all 0.25s;
|
||||
position: relative;
|
||||
box-shadow: 0 0 6px rgba(47, 91, 255, 0.15);
|
||||
|
||||
&:hover {
|
||||
box-shadow: 0 0 6px rgba(47, 91, 255, 0.15);
|
||||
border: 1px solid #6b80eb;
|
||||
}
|
||||
}
|
||||
|
||||
.doc-card-top {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 14px;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.doc-file-icon {
|
||||
width: 46px;
|
||||
height: 46px;
|
||||
border-radius: 10px;
|
||||
object-fit: contain;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.doc-card-info {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: space-around;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.doc-card-name {
|
||||
font-size: 15px;
|
||||
font-weight: 500;
|
||||
color: #1f2a44;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.doc-card-meta {
|
||||
font-size: 13px;
|
||||
color: #7888a8;
|
||||
}
|
||||
|
||||
.doc-card-bottom {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.doc-card-progress {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
/* ========== Progress Ring ========== */
|
||||
.progress-ring {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
position: relative;
|
||||
|
||||
svg {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
transform: rotate(-90deg);
|
||||
}
|
||||
|
||||
.bg {
|
||||
fill: none;
|
||||
stroke: #e7ecf5;
|
||||
stroke-width: 4;
|
||||
}
|
||||
|
||||
.fg {
|
||||
fill: none;
|
||||
stroke-width: 4;
|
||||
stroke-linecap: round;
|
||||
transition: stroke-dashoffset 0.5s ease;
|
||||
|
||||
&.complete { stroke: #34c759; }
|
||||
&.processing { stroke: #2f5bff; }
|
||||
&.pending { stroke: #e7ecf5; }
|
||||
}
|
||||
}
|
||||
|
||||
.progress-ring-text {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
font-size: 10px;
|
||||
font-weight: 600;
|
||||
color: #7888a8;
|
||||
}
|
||||
|
||||
.progress-container {
|
||||
height: 36px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: space-between;
|
||||
align-items: flex-start;
|
||||
.progress-label {
|
||||
font-size: 12px;
|
||||
color: #7888a8;
|
||||
|
||||
&.complete { color: #34c759; }
|
||||
&.processing { color: #2f5bff; }
|
||||
}
|
||||
|
||||
.doc-slice-count {
|
||||
font-size: 12px;
|
||||
> b {
|
||||
font-weight: bold;
|
||||
font-size: 14px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
.doc-card-actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
|
||||
:deep(.el-button--text) {
|
||||
padding: 4px 8px;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.delete-btn {
|
||||
color: #f56c6c !important;
|
||||
&:hover {
|
||||
color: #f78989 !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.doc-empty {
|
||||
grid-column: 1 / -1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
min-height: 300px;
|
||||
color: #b0bbc9;
|
||||
font-size: 14px;
|
||||
|
||||
i {
|
||||
font-size: 48px;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
}
|
||||
|
||||
.pagination-wrapper {
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
@media (max-width: 1144px) {
|
||||
.doc-grid {
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 900px) {
|
||||
.doc-grid {
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -105,7 +105,8 @@
|
||||
>
|
||||
<el-switch
|
||||
v-model="scope.row.isEnabled"
|
||||
class="custom-switch"
|
||||
active-color="#5778ff"
|
||||
inactive-color="#DCDFE6"
|
||||
:active-value="1"
|
||||
:inactive-value="0"
|
||||
disabled
|
||||
@@ -115,7 +116,8 @@
|
||||
<el-switch
|
||||
v-else
|
||||
v-model="scope.row.isEnabled"
|
||||
class="custom-switch"
|
||||
active-color="#5778ff"
|
||||
inactive-color="#DCDFE6"
|
||||
:active-value="1"
|
||||
:inactive-value="0"
|
||||
@change="handleStatusChange(scope.row)"
|
||||
@@ -126,7 +128,8 @@
|
||||
<template slot-scope="scope">
|
||||
<el-switch
|
||||
v-model="scope.row.isDefault"
|
||||
class="custom-switch"
|
||||
active-color="#5778ff"
|
||||
inactive-color="#DCDFE6"
|
||||
:active-value="1"
|
||||
:inactive-value="0"
|
||||
@change="handleDefaultChange(scope.row)"
|
||||
@@ -184,76 +187,33 @@
|
||||
</el-table>
|
||||
<div class="table-footer">
|
||||
<div class="batch-actions">
|
||||
<el-button size="mini" type="primary" @click="selectAll">
|
||||
<CustomButton :icon="isAllSelected ? 'el-icon-circle-close' : 'el-icon-circle-check'" type="default" size="small" @click="selectAll">
|
||||
{{
|
||||
isAllSelected
|
||||
? $t("modelConfig.deselectAll")
|
||||
: $t("modelConfig.selectAll")
|
||||
}}
|
||||
</el-button>
|
||||
<el-button type="success" size="mini" @click="addModel" class="add-btn">
|
||||
</CustomButton>
|
||||
<CustomButton icon="el-icon-plus" type="add" size="small" @click="addModel">
|
||||
{{ $t("modelConfig.add") }}
|
||||
</el-button>
|
||||
<el-button
|
||||
size="mini"
|
||||
type="danger"
|
||||
</CustomButton>
|
||||
<CustomButton
|
||||
type="delete"
|
||||
size="small"
|
||||
icon="el-icon-delete"
|
||||
@click="batchDelete"
|
||||
>
|
||||
{{ $t("modelConfig.delete") }}
|
||||
</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="$t('modelConfig.itemsPerPage', { items: item })"
|
||||
:value="item"
|
||||
>
|
||||
</el-option>
|
||||
</el-select>
|
||||
|
||||
<button
|
||||
class="pagination-btn"
|
||||
:disabled="currentPage === 1"
|
||||
@click="goFirst"
|
||||
>
|
||||
{{ $t("modelConfig.firstPage") }}
|
||||
</button>
|
||||
<button
|
||||
class="pagination-btn"
|
||||
:disabled="currentPage === 1"
|
||||
@click="goPrev"
|
||||
>
|
||||
{{ $t("modelConfig.prevPage") }}
|
||||
</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"
|
||||
>
|
||||
{{ $t("modelConfig.nextPage") }}
|
||||
</button>
|
||||
<span class="total-text">{{
|
||||
$t("modelConfig.totalRecords", { total })
|
||||
}}</span>
|
||||
</CustomButton>
|
||||
</div>
|
||||
<CustomPagination
|
||||
:total="total"
|
||||
:current-page="currentPage"
|
||||
:page-size="pageSize"
|
||||
:page-size-options="pageSizeOptions"
|
||||
@size-change="handlePageSizeChange"
|
||||
@page-change="handlePageChange"
|
||||
/>
|
||||
</div>
|
||||
</el-card>
|
||||
</div>
|
||||
@@ -288,9 +248,11 @@ import AddModelDialog from "@/components/AddModelDialog.vue";
|
||||
import HeaderBar from "@/components/HeaderBar.vue";
|
||||
import ModelEditDialog from "@/components/ModelEditDialog.vue";
|
||||
import TtsModel from "@/components/TtsModel.vue";
|
||||
import CustomPagination from "@/components/CustomPagination.vue";
|
||||
import CustomButton from "@/components/CustomButton.vue";
|
||||
import VersionFooter from "@/components/VersionFooter.vue";
|
||||
export default {
|
||||
components: { HeaderBar, ModelEditDialog, TtsModel, AddModelDialog, VersionFooter },
|
||||
components: { HeaderBar, ModelEditDialog, TtsModel, AddModelDialog, VersionFooter, CustomPagination, CustomButton },
|
||||
data() {
|
||||
return {
|
||||
addDialogVisible: false,
|
||||
@@ -334,24 +296,6 @@ export default {
|
||||
this.$t("modelConfig." + this.activeTab) || this.$t("modelConfig.modelConfig")
|
||||
);
|
||||
},
|
||||
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: {
|
||||
@@ -481,9 +425,9 @@ export default {
|
||||
this.$message.info(this.$t("modelConfig.deleteCancelled"));
|
||||
});
|
||||
},
|
||||
handleCurrentChange(page) {
|
||||
handlePageChange(page) {
|
||||
this.currentPage = page;
|
||||
this.$refs.modelTable.clearSelection();
|
||||
this.loadData();
|
||||
},
|
||||
handleModelSave({ provideCode, formData, done }) {
|
||||
const modelType = this.activeTab;
|
||||
@@ -558,28 +502,6 @@ export default {
|
||||
});
|
||||
},
|
||||
|
||||
// 分页器
|
||||
goFirst() {
|
||||
this.currentPage = 1;
|
||||
this.loadData();
|
||||
},
|
||||
goPrev() {
|
||||
if (this.currentPage > 1) {
|
||||
this.currentPage--;
|
||||
this.loadData();
|
||||
}
|
||||
},
|
||||
goNext() {
|
||||
if (this.currentPage < this.pageCount) {
|
||||
this.currentPage++;
|
||||
this.loadData();
|
||||
}
|
||||
},
|
||||
goToPage(page) {
|
||||
this.currentPage = page;
|
||||
this.loadData();
|
||||
},
|
||||
|
||||
// 获取模型配置列表
|
||||
loadData() {
|
||||
this.loading = true; // 开始加载
|
||||
@@ -654,7 +576,7 @@ export default {
|
||||
position: relative;
|
||||
flex-direction: column;
|
||||
background-size: cover;
|
||||
background: linear-gradient(to bottom right, #dce8ff, #e4eeff, #e6cbfd) center;
|
||||
background: #eff4ff;
|
||||
-webkit-background-size: cover;
|
||||
-o-background-size: cover;
|
||||
}
|
||||
@@ -664,9 +586,7 @@ export default {
|
||||
height: calc(100vh - 63px - 35px - 72px);
|
||||
margin: 0 22px;
|
||||
border-radius: 15px;
|
||||
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.1);
|
||||
position: relative;
|
||||
background: rgba(237, 242, 255, 0.5);
|
||||
}
|
||||
|
||||
.operation-bar {
|
||||
@@ -793,50 +713,6 @@ export default {
|
||||
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;
|
||||
@@ -869,53 +745,53 @@ export default {
|
||||
background: white;
|
||||
}
|
||||
|
||||
.batch-actions {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
}
|
||||
// .batch-actions {
|
||||
// display: flex;
|
||||
// gap: 8px;
|
||||
// }
|
||||
|
||||
.batch-actions .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);
|
||||
}
|
||||
// .batch-actions .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);
|
||||
// }
|
||||
|
||||
.batch-actions .el-button:hover {
|
||||
transform: translateY(-1px);
|
||||
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.15);
|
||||
}
|
||||
// .batch-actions .el-button:hover {
|
||||
// transform: translateY(-1px);
|
||||
// box-shadow: 0 4px 8px rgba(0, 0, 0, 0.15);
|
||||
// }
|
||||
|
||||
.batch-actions .el-button--primary {
|
||||
background: #5f70f3 !important;
|
||||
color: white;
|
||||
}
|
||||
// .batch-actions .el-button--primary {
|
||||
// background: #5f70f3 !important;
|
||||
// color: white;
|
||||
// }
|
||||
|
||||
.batch-actions .el-button--success {
|
||||
background: #5bc98c;
|
||||
color: white;
|
||||
}
|
||||
// .batch-actions .el-button--success {
|
||||
// background: #5bc98c;
|
||||
// color: white;
|
||||
// }
|
||||
|
||||
.batch-actions .el-button--danger {
|
||||
background: #fd5b63;
|
||||
color: white;
|
||||
}
|
||||
// .batch-actions .el-button--danger {
|
||||
// background: #fd5b63;
|
||||
// color: white;
|
||||
// }
|
||||
|
||||
.batch-actions .el-button:first-child {
|
||||
background: linear-gradient(135deg, #409eff, #6b8cff);
|
||||
border: none;
|
||||
color: white;
|
||||
}
|
||||
// .batch-actions .el-button:first-child {
|
||||
// background: linear-gradient(135deg, #409eff, #6b8cff);
|
||||
// border: none;
|
||||
// color: white;
|
||||
// }
|
||||
|
||||
.batch-actions .el-button:first-child:hover {
|
||||
background: linear-gradient(135deg, #3a8ee6, #5a7cff);
|
||||
}
|
||||
// .batch-actions .el-button:first-child:hover {
|
||||
// background: linear-gradient(135deg, #3a8ee6, #5a7cff);
|
||||
// }
|
||||
|
||||
.el-table th ::v-deep .el-table__cell {
|
||||
overflow: hidden;
|
||||
@@ -1018,73 +894,6 @@ export default {
|
||||
padding-right: 10px;
|
||||
}
|
||||
|
||||
/* 分页器 */
|
||||
.custom-pagination {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
|
||||
/* 导航按钮样式 (首页、上一页、下一页) */
|
||||
.pagination-btn:first-child,
|
||||
.pagination-btn:nth-child(2),
|
||||
.pagination-btn:nth-child(3),
|
||||
.pagination-btn:nth-last-child(2) {
|
||||
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(2)):not(:nth-child(3)):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 {
|
||||
color: #909399;
|
||||
font-size: 14px;
|
||||
margin-left: 10px;
|
||||
}
|
||||
}
|
||||
|
||||
.model-card {
|
||||
background: white;
|
||||
flex: 1;
|
||||
|
||||
@@ -1,95 +1,67 @@
|
||||
<template>
|
||||
<div class="welcome">
|
||||
<HeaderBar />
|
||||
|
||||
<div class="operation-bar">
|
||||
<h2 class="page-title">{{ $t('otaManagement.firmwareManagement') }}</h2>
|
||||
<div class="right-operations">
|
||||
<el-input :placeholder="$t('otaManagement.searchPlaceholder')" v-model="searchName" class="search-input"
|
||||
@keyup.enter.native="handleSearch" clearable />
|
||||
<el-button class="btn-search" @click="handleSearch">{{ $t('otaManagement.search') }}</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" v-loading="loading"
|
||||
element-loading-text="Loading" element-loading-spinner="el-icon-loading"
|
||||
element-loading-background="rgba(255, 255, 255, 0.7)"
|
||||
:header-cell-class-name="headerCellClassName">
|
||||
<el-table-column :label="$t('modelConfig.select')" 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="$t('otaManagement.firmwareName')" prop="firmwareName" align="center"></el-table-column>
|
||||
<el-table-column :label="$t('otaManagement.firmwareType')" prop="type" align="center">
|
||||
<template slot-scope="scope">
|
||||
{{ getFirmwareTypeName(scope.row.type) }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column :label="$t('otaManagement.version')" prop="version" align="center"></el-table-column>
|
||||
<el-table-column :label="$t('otaManagement.fileSize')" prop="size" align="center">
|
||||
<template slot-scope="scope">
|
||||
{{ formatFileSize(scope.row.size) }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column :label="$t('otaManagement.remark')" prop="remark" align="center"
|
||||
show-overflow-tooltip></el-table-column>
|
||||
<el-table-column :label="$t('otaManagement.createTime')" prop="createDate" align="center">
|
||||
<template slot-scope="scope">
|
||||
{{ formatDate(scope.row.createDate) }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column :label="$t('otaManagement.updateTime')" prop="updateDate" align="center">
|
||||
<template slot-scope="scope">
|
||||
{{ formatDate(scope.row.updateDate) }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column :label="$t('otaManagement.action')" align="center">
|
||||
<template slot-scope="scope">
|
||||
<el-button size="mini" type="text"
|
||||
@click="downloadFirmware(scope.row)">{{ $t('otaManagement.download') }}</el-button>
|
||||
<el-button size="mini" type="text" @click="editParam(scope.row)">{{ $t('otaManagement.edit') }}</el-button>
|
||||
<el-button size="mini" type="text" @click="deleteParam(scope.row)">{{ $t('otaManagement.delete') }}</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 ? $t('otaManagement.deselectAll') : $t('otaManagement.selectAll') }}
|
||||
</el-button>
|
||||
<el-button size="mini" type="success" @click="showAddDialog"
|
||||
style="background: #5bc98c;border: None;">{{ $t('otaManagement.addNew') }}</el-button>
|
||||
<el-button size="mini" type="danger" icon="el-icon-delete"
|
||||
@click="deleteSelectedParams">{{ $t('otaManagement.delete') }}</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="$t('otaManagement.itemsPerPage', { items: item })"
|
||||
:value="item">
|
||||
</el-option>
|
||||
</el-select>
|
||||
<button class="pagination-btn" :disabled="currentPage === 1" @click="goFirst">
|
||||
{{ $t('otaManagement.firstPage') }}
|
||||
</button>
|
||||
<button class="pagination-btn" :disabled="currentPage === 1" @click="goPrev">
|
||||
{{ $t('otaManagement.prevPage') }}
|
||||
</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">
|
||||
{{ $t('otaManagement.nextPage') }}
|
||||
</button>
|
||||
<span class="total-text">{{ $t('otaManagement.totalRecords', { total }) }}</span>
|
||||
<div class="operation-header">
|
||||
<h2 class="page-title">{{ $t('otaManagement.firmwareManagement') }}</h2>
|
||||
<div class="right-operations">
|
||||
<el-input :placeholder="$t('otaManagement.searchPlaceholder')" v-model="searchName"
|
||||
class="search-input" @keyup.enter.native="handleSearch" clearable />
|
||||
<CustomButton icon="el-icon-search" type="confirm" @click="handleSearch">
|
||||
{{ $t('otaManagement.search') }}
|
||||
</CustomButton>
|
||||
</div>
|
||||
</div>
|
||||
<CustomTable ref="paramsTable" :data="paramsList" :columns="tableColumns" :loading="loading"
|
||||
:show-selection="true" :show-operations="true" :operations-label="$t('otaManagement.action')"
|
||||
:total="total" :current-page="currentPage" :page-size="pageSize"
|
||||
:page-size-options="pageSizeOptions" @size-change="handlePageSizeChange"
|
||||
@page-change="goToPage">
|
||||
<template slot="selection" slot-scope="scope">
|
||||
<el-checkbox v-model="scope.row.selected"></el-checkbox>
|
||||
</template>
|
||||
<template slot="type" slot-scope="scope">
|
||||
{{ getFirmwareTypeName(scope.row.type) }}
|
||||
</template>
|
||||
<template slot="size" slot-scope="scope">
|
||||
{{ formatFileSize(scope.row.size) }}
|
||||
</template>
|
||||
<template slot="createDate" slot-scope="scope">
|
||||
{{ formatDate(scope.row.createDate) }}
|
||||
</template>
|
||||
<template slot="updateDate" slot-scope="scope">
|
||||
{{ formatDate(scope.row.updateDate) }}
|
||||
</template>
|
||||
<template slot="operations" slot-scope="scope">
|
||||
<el-button size="mini" type="text"
|
||||
@click="downloadFirmware(scope.row)">{{ $t('otaManagement.download') }}</el-button>
|
||||
<el-button size="mini" type="text" @click="editParam(scope.row)">
|
||||
{{ $t('otaManagement.edit') }}
|
||||
</el-button>
|
||||
<el-button size="mini" type="text" @click="deleteParam(scope.row)">
|
||||
{{ $t('otaManagement.delete') }}
|
||||
</el-button>
|
||||
</template>
|
||||
<template slot="footer-btns">
|
||||
<div class="ctrl_btn">
|
||||
<CustomButton
|
||||
:icon="isAllSelected ? 'el-icon-circle-close' : 'el-icon-circle-check'"
|
||||
size="small" @click="handleSelectAll">
|
||||
{{ isAllSelected ? $t('otaManagement.deselectAll') : $t('otaManagement.selectAll') }}
|
||||
</CustomButton>
|
||||
<CustomButton type="add" icon="el-icon-plus" size="small" @click="showAddDialog">
|
||||
{{ $t('otaManagement.addNew') }}
|
||||
</CustomButton>
|
||||
<CustomButton size="small" type="delete" icon="el-icon-delete"
|
||||
@click="deleteSelectedParams">
|
||||
{{ $t('otaManagement.delete') }}
|
||||
</CustomButton>
|
||||
</div>
|
||||
</template>
|
||||
</CustomTable>
|
||||
</el-card>
|
||||
</div>
|
||||
</div>
|
||||
@@ -97,7 +69,7 @@
|
||||
|
||||
<!-- 新增/编辑固件对话框 -->
|
||||
<firmware-dialog :title="dialogTitle" :visible.sync="dialogVisible" :form="firmwareForm"
|
||||
:firmware-types="firmwareTypes" @submit="handleSubmit" @cancel="dialogVisible = false" />
|
||||
:firmware-types="firmwareTypes" @submit="handleSubmit" @cancel="dialogVisible = false" />
|
||||
<el-footer>
|
||||
<version-footer />
|
||||
</el-footer>
|
||||
@@ -109,11 +81,12 @@ import Api from "@/apis/api";
|
||||
import FirmwareDialog from "@/components/FirmwareDialog.vue";
|
||||
import HeaderBar from "@/components/HeaderBar.vue";
|
||||
import VersionFooter from "@/components/VersionFooter.vue";
|
||||
import CustomButton from "@/components/CustomButton.vue";
|
||||
import CustomTable from "@/components/CustomTable.vue";
|
||||
import { formatDate, formatFileSize } from "@/utils/format";
|
||||
import i18n from '@/i18n';
|
||||
|
||||
export default {
|
||||
components: { HeaderBar, FirmwareDialog, VersionFooter },
|
||||
components: { HeaderBar, FirmwareDialog, VersionFooter, CustomButton, CustomTable },
|
||||
data() {
|
||||
return {
|
||||
searchName: "",
|
||||
@@ -137,34 +110,26 @@ export default {
|
||||
firmwarePath: ""
|
||||
},
|
||||
firmwareTypes: [],
|
||||
tableColumns: []
|
||||
};
|
||||
},
|
||||
created() {
|
||||
this.initTableColumns();
|
||||
this.fetchFirmwareList();
|
||||
this.getFirmwareTypes();
|
||||
},
|
||||
|
||||
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: {
|
||||
initTableColumns() {
|
||||
this.tableColumns = [
|
||||
{ prop: 'firmwareName', label: this.$t('otaManagement.firmwareName'), align: 'center' },
|
||||
{ prop: 'type', label: this.$t('otaManagement.firmwareType'), align: 'center' },
|
||||
{ prop: 'version', label: this.$t('otaManagement.version'), align: 'center' },
|
||||
{ prop: 'size', label: this.$t('otaManagement.fileSize'), align: 'center' },
|
||||
{ prop: 'remark', label: this.$t('otaManagement.remark'), align: 'center' },
|
||||
{ prop: 'createDate', label: this.$t('otaManagement.createTime'), align: 'center' },
|
||||
{ prop: 'updateDate', label: this.$t('otaManagement.updateTime'), align: 'center' }
|
||||
];
|
||||
},
|
||||
handlePageSizeChange(val) {
|
||||
this.pageSize = val;
|
||||
this.currentPage = 1;
|
||||
@@ -194,7 +159,7 @@ export default {
|
||||
this.paramsList = [];
|
||||
this.total = 0;
|
||||
this.$message.error({
|
||||
message: res?.data?.msg || this.$t('otaManagement.getFirmwareListFailed'),
|
||||
message: res?.data?.msg || this.$t('otaManagement.fetchFirmwareListFailed'),
|
||||
showClose: true
|
||||
});
|
||||
}
|
||||
@@ -212,7 +177,6 @@ export default {
|
||||
},
|
||||
showAddDialog() {
|
||||
this.dialogTitle = this.$t('otaManagement.addFirmware');
|
||||
// 完全重置表单数据
|
||||
this.firmwareForm = {
|
||||
id: null,
|
||||
firmwareName: "",
|
||||
@@ -222,12 +186,6 @@ export default {
|
||||
remark: "",
|
||||
firmwarePath: ""
|
||||
};
|
||||
this.$nextTick(() => {
|
||||
// 重置表单的校验状态
|
||||
if (this.$refs.firmwareDialog && this.$refs.firmwareDialog.$refs.form) {
|
||||
this.$refs.firmwareDialog.$refs.form.clearValidate();
|
||||
}
|
||||
});
|
||||
this.dialogVisible = true;
|
||||
},
|
||||
editParam(row) {
|
||||
@@ -237,7 +195,6 @@ export default {
|
||||
},
|
||||
handleSubmit(form) {
|
||||
if (form.id) {
|
||||
// 编辑
|
||||
Api.ota.updateOta(form.id, form, (res) => {
|
||||
res = res.data;
|
||||
if (res.code === 0) {
|
||||
@@ -255,7 +212,6 @@ export default {
|
||||
}
|
||||
});
|
||||
} else {
|
||||
// 新增
|
||||
Api.ota.saveOta(form, (res) => {
|
||||
res = res.data;
|
||||
if (res.code === 0) {
|
||||
@@ -274,7 +230,6 @@ export default {
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
deleteSelectedParams() {
|
||||
const selectedRows = this.firmwareList.filter(row => row.selected);
|
||||
if (selectedRows.length === 0) {
|
||||
@@ -287,7 +242,6 @@ export default {
|
||||
this.deleteParam(selectedRows);
|
||||
},
|
||||
deleteParam(row) {
|
||||
// 处理单个参数或参数数组
|
||||
const params = Array.isArray(row) ? row : [row];
|
||||
|
||||
if (Array.isArray(row) && row.length === 0) {
|
||||
@@ -345,38 +299,17 @@ export default {
|
||||
}
|
||||
});
|
||||
},
|
||||
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();
|
||||
if (page !== this.currentPage) {
|
||||
this.currentPage = page;
|
||||
this.fetchFirmwareList();
|
||||
}
|
||||
},
|
||||
downloadFirmware(firmware) {
|
||||
if (!firmware || !firmware.id) {
|
||||
this.$message.error(this.$t('otaManagement.incompleteFirmwareInfo'));
|
||||
return;
|
||||
}
|
||||
// 先获取下载链接
|
||||
Api.ota.getDownloadUrl(firmware.id, (res) => {
|
||||
if (res.data.code === 0) {
|
||||
const uuid = res.data.data;
|
||||
@@ -415,32 +348,30 @@ export default {
|
||||
position: relative;
|
||||
flex-direction: column;
|
||||
background-size: cover;
|
||||
background: linear-gradient(to bottom right, #dce8ff, #e4eeff, #e6cbfd) center;
|
||||
background: #eff4ff;
|
||||
-webkit-background-size: cover;
|
||||
-o-background-size: cover;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.main-wrapper {
|
||||
// 顶部 63px 底部 35px 查询72px
|
||||
height: calc(100vh - 63px - 35px - 72px);
|
||||
margin: 0 22px;
|
||||
border-radius: 15px;
|
||||
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.1);
|
||||
height: calc(100vh - 63px - 35px);
|
||||
padding: 20px 22px 0;
|
||||
position: relative;
|
||||
background: rgba(237, 242, 255, 0.5);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.operation-bar {
|
||||
.operation-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 16px 24px;
|
||||
justify-content: space-between;
|
||||
padding: 0 0 16px 0;
|
||||
}
|
||||
|
||||
.page-title {
|
||||
font-weight: 500;
|
||||
font-size: 24px;
|
||||
margin: 0;
|
||||
}
|
||||
@@ -455,14 +386,7 @@ export default {
|
||||
width: 240px;
|
||||
}
|
||||
|
||||
.btn-search {
|
||||
background: linear-gradient(135deg, #6b8cff, #a966ff);
|
||||
border: none;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.content-panel {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
overflow: hidden;
|
||||
height: 100%;
|
||||
@@ -491,7 +415,7 @@ export default {
|
||||
overflow: hidden;
|
||||
|
||||
::v-deep .el-card__body {
|
||||
padding: 15px;
|
||||
padding: 14px 20px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex: 1;
|
||||
@@ -499,267 +423,15 @@ export default {
|
||||
}
|
||||
}
|
||||
|
||||
.table_bottom {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-top: 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: 5px;
|
||||
|
||||
.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;
|
||||
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(.transparent-table) {
|
||||
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 {
|
||||
background: white !important;
|
||||
color: black;
|
||||
font-weight: 600;
|
||||
height: 40px;
|
||||
padding: 8px 0;
|
||||
font-size: 14px;
|
||||
border-bottom: 1px solid #e4e7ed;
|
||||
}
|
||||
|
||||
.el-table__body tr {
|
||||
background-color: white;
|
||||
|
||||
td {
|
||||
border-top: 1px solid rgba(0, 0, 0, 0.04);
|
||||
border-bottom: 1px solid rgba(0, 0, 0, 0.04);
|
||||
padding: 8px 0;
|
||||
height: 40px;
|
||||
color: #606266;
|
||||
font-size: 14px;
|
||||
}
|
||||
}
|
||||
|
||||
.el-table__row:hover>td {
|
||||
background-color: #f5f7fa !important;
|
||||
}
|
||||
|
||||
&::before {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
:deep(.el-table .el-button--text) {
|
||||
color: #7079aa !important;
|
||||
color: #7079aa;
|
||||
}
|
||||
|
||||
:deep(.el-table .el-button--text:hover) {
|
||||
color: #5a64b5 !important;
|
||||
}
|
||||
|
||||
:deep(.el-checkbox__inner) {
|
||||
background-color: #ffffff !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;
|
||||
}
|
||||
|
||||
:deep(.el-loading-mask) {
|
||||
background-color: rgba(255, 255, 255, 0.6) !important;
|
||||
backdrop-filter: blur(2px);
|
||||
}
|
||||
|
||||
:deep(.el-loading-spinner .path) {
|
||||
stroke: #6b8cff;
|
||||
}
|
||||
|
||||
.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);
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 1144px) {
|
||||
.table_bottom {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-top: 40px;
|
||||
}
|
||||
|
||||
:deep(.transparent-table) {
|
||||
.el-table__body tr {
|
||||
td {
|
||||
padding-top: 16px;
|
||||
padding-bottom: 16px;
|
||||
}
|
||||
|
||||
&+tr {
|
||||
margin-top: 10px;
|
||||
}
|
||||
}
|
||||
}
|
||||
color: #5a64b5;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,49 +1,20 @@
|
||||
<template>
|
||||
<div class="welcome">
|
||||
<HeaderBar />
|
||||
|
||||
<div class="operation-bar">
|
||||
<h2 class="page-title">{{ $t('header.providerManagement') }}</h2>
|
||||
<div class="right-operations">
|
||||
<el-dropdown trigger="click" @command="handleSelectModelType" @visible-change="handleDropdownVisibleChange">
|
||||
<el-button class="category-btn">
|
||||
{{ $t('providerManagement.categoryFilter') }} {{ selectedModelTypeLabel }}<i class="el-icon-arrow-down el-icon--right"
|
||||
:class="{ 'rotate-down': DropdownVisible }"></i>
|
||||
</el-button>
|
||||
<el-dropdown-menu slot="dropdown">
|
||||
<el-dropdown-item command="">{{ $t('common.all') }}</el-dropdown-item>
|
||||
<el-dropdown-item v-for="item in translatedModelTypes" :key="item.value" :command="item.value">
|
||||
{{ item.label }}
|
||||
</el-dropdown-item>
|
||||
</el-dropdown-menu>
|
||||
</el-dropdown>
|
||||
<el-input :placeholder="$t('providerManagement.searchPlaceholder')" v-model="searchName" class="search-input" @keyup.enter.native="handleSearch"
|
||||
clearable />
|
||||
<el-button class="btn-search" @click="handleSearch">{{ $t('common.search') }}</el-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="main-wrapper">
|
||||
<div class="content-panel">
|
||||
<div class="content-area">
|
||||
<el-card class="provider-card" shadow="never">
|
||||
<el-table ref="providersTable" :data="filteredProvidersList" 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="$t('modelConfig.select')" 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="$t('providerManagement.category')" prop="modelType" align="center" width="200">
|
||||
<template slot="header" slot-scope="scope">
|
||||
<el-dropdown trigger="click" @command="handleSelectModelType"
|
||||
@visible-change="isDropdownOpen = $event">
|
||||
<span class="dropdown-trigger" :class="{ 'active': isDropdownOpen }">
|
||||
{{ $t('providerManagement.category') }}{{ selectedModelTypeLabel }} <i class="dropdown-arrow"
|
||||
:class="{ 'is-active': isDropdownOpen }"></i>
|
||||
</span>
|
||||
<div class="operation-header">
|
||||
<h2 class="page-title">{{ $t('header.providerManagement') }}</h2>
|
||||
<div class="right-operations">
|
||||
<el-input :placeholder="$t('providerManagement.searchPlaceholder')" v-model="searchName" class="search-input"
|
||||
@keyup.enter.native="handleSearch" clearable />
|
||||
<el-dropdown trigger="click" @command="handleSelectModelType" @visible-change="handleDropdownVisibleChange">
|
||||
<CustomButton>
|
||||
{{ $t('providerManagement.categoryFilter') }}{{ selectedModelTypeLabel }}<i class="el-icon-arrow-down el-icon--right"
|
||||
:class="{ 'rotate-down': DropdownVisible }"></i>
|
||||
</CustomButton>
|
||||
<el-dropdown-menu slot="dropdown">
|
||||
<el-dropdown-item command="">{{ $t('common.all') }}</el-dropdown-item>
|
||||
<el-dropdown-item v-for="item in translatedModelTypes" :key="item.value" :command="item.value">
|
||||
@@ -51,66 +22,60 @@
|
||||
</el-dropdown-item>
|
||||
</el-dropdown-menu>
|
||||
</el-dropdown>
|
||||
</template>
|
||||
<template slot-scope="scope">
|
||||
<el-tag :type="getModelTypeTag(scope.row.modelType)">
|
||||
{{ getModelTypeLabel(scope.row.modelType) }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column :label="$t('providerManagement.providerCode')" prop="providerCode" align="center" width="150"></el-table-column>
|
||||
<el-table-column :label="$t('common.name')" prop="name" align="center"></el-table-column>
|
||||
<el-table-column :label="$t('providerManagement.fieldConfig')" align="center">
|
||||
<template slot-scope="scope">
|
||||
<el-popover placement="top-start" width="400" trigger="hover">
|
||||
<div v-for="field in scope.row.fields" :key="field.key" class="field-item">
|
||||
<span class="field-label">{{ field.label }}:</span>
|
||||
<span class="field-type">{{ field.type }}</span>
|
||||
<span v-if="isSensitiveField(field.key)" class="sensitive-tag">{{ $t('common.sensitive') }}</span>
|
||||
</div>
|
||||
<el-button slot="reference" size="mini" type="text">{{ $t('providerManagement.viewFields') }}</el-button>
|
||||
</el-popover>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column :label="$t('common.sort')" prop="sort" align="center" width="80"></el-table-column>
|
||||
<el-table-column :label="$t('common.action')" align="center" width="180">
|
||||
<template slot-scope="scope">
|
||||
<el-button size="mini" type="text" @click="editProvider(scope.row)">{{ $t('common.edit') }}</el-button>
|
||||
<el-button size="mini" type="text" @click="deleteProvider(scope.row)">{{ $t('common.delete') }}</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 ? $t('common.deselectAll') : $t('common.selectAll') }}
|
||||
</el-button>
|
||||
<el-button size="mini" type="success" @click="showAddDialog">{{ $t('common.add') }}</el-button>
|
||||
<el-button size="mini" type="danger" icon="el-icon-delete" @click="deleteSelectedProviders">{{ $t('common.delete') }}
|
||||
</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="$t('common.perPage', { number: item })" :value="item">
|
||||
</el-option>
|
||||
</el-select>
|
||||
<button class="pagination-btn" :disabled="currentPage === 1" @click="goFirst">
|
||||
{{ $t('common.firstPage') }}
|
||||
</button>
|
||||
<button class="pagination-btn" :disabled="currentPage === 1" @click="goPrev">
|
||||
{{ $t('common.prevPage') }}
|
||||
</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">
|
||||
{{ $t('common.nextPage') }}
|
||||
</button>
|
||||
<span class="total-text">{{ $t('common.totalRecords', { number: total }) }}</span>
|
||||
<CustomButton icon="el-icon-search" type="confirm" @click="handleSearch">{{ $t('common.search') }}</CustomButton>
|
||||
</div>
|
||||
</div>
|
||||
<CustomTable
|
||||
ref="providersTable"
|
||||
:data="filteredProvidersList"
|
||||
:columns="tableColumns"
|
||||
:loading="loading"
|
||||
:show-selection="true"
|
||||
:show-operations="true"
|
||||
:operations-label="$t('common.action')"
|
||||
:total="total"
|
||||
:current-page="currentPage"
|
||||
:page-size="pageSize"
|
||||
:page-size-options="pageSizeOptions"
|
||||
@size-change="handlePageSizeChange"
|
||||
@page-change="goToPage"
|
||||
>
|
||||
<template slot="selection" slot-scope="scope">
|
||||
<el-checkbox v-model="scope.row.selected"></el-checkbox>
|
||||
</template>
|
||||
<template slot="modelType" slot-scope="scope">
|
||||
<el-tag :type="getModelTypeTag(scope.row.modelType)">
|
||||
{{ getModelTypeLabel(scope.row.modelType) }}
|
||||
</el-tag>
|
||||
</template>
|
||||
<template slot="fields" slot-scope="scope">
|
||||
<el-popover placement="top-start" width="400" trigger="hover">
|
||||
<div v-for="field in scope.row.fields" :key="field.key" class="field-item">
|
||||
<span class="field-label">{{ field.label }}:</span>
|
||||
<span class="field-type">{{ field.type }}</span>
|
||||
<span v-if="isSensitiveField(field.key)" class="sensitive-tag">{{ $t('common.sensitive') }}</span>
|
||||
</div>
|
||||
<el-button slot="reference" size="mini" type="text">{{ $t('providerManagement.viewFields') }}</el-button>
|
||||
</el-popover>
|
||||
</template>
|
||||
<template slot="operations" slot-scope="scope">
|
||||
<el-button size="mini" type="text" @click="editProvider(scope.row)">{{ $t('common.edit') }}</el-button>
|
||||
<el-button size="mini" type="text" @click="deleteProvider(scope.row)">{{ $t('common.delete') }}</el-button>
|
||||
</template>
|
||||
<template slot="footer-btns">
|
||||
<div class="ctrl_btn">
|
||||
<CustomButton :icon="isAllSelected ? 'el-icon-circle-close' : 'el-icon-circle-check'" size="small" @click="handleSelectAll">
|
||||
{{ isAllSelected ? $t('common.deselectAll') : $t('common.selectAll') }}
|
||||
</CustomButton>
|
||||
<CustomButton type="add" icon="el-icon-plus" size="small" @click="showAddDialog">
|
||||
{{ $t('common.add') }}
|
||||
</CustomButton>
|
||||
<CustomButton size="small" type="delete" icon="el-icon-delete" @click="deleteSelectedProviders">
|
||||
{{ $t('common.delete') }}
|
||||
</CustomButton>
|
||||
</div>
|
||||
</template>
|
||||
</CustomTable>
|
||||
</el-card>
|
||||
</div>
|
||||
</div>
|
||||
@@ -131,9 +96,11 @@ import Api from "@/apis/api";
|
||||
import HeaderBar from "@/components/HeaderBar.vue";
|
||||
import ProviderDialog from "@/components/ProviderDialog.vue";
|
||||
import VersionFooter from "@/components/VersionFooter.vue";
|
||||
import CustomButton from "@/components/CustomButton.vue";
|
||||
import CustomTable from "@/components/CustomTable.vue";
|
||||
|
||||
export default {
|
||||
components: { HeaderBar, ProviderDialog, VersionFooter },
|
||||
components: { HeaderBar, ProviderDialog, VersionFooter, CustomButton, CustomTable },
|
||||
data() {
|
||||
return {
|
||||
searchName: "",
|
||||
@@ -158,7 +125,7 @@ export default {
|
||||
dialogVisible: false,
|
||||
dialogTitle: "新增供应器",
|
||||
isAllSelected: false,
|
||||
isDropdownOpen: false,
|
||||
DropdownVisible: false,
|
||||
sensitive_keys: ["api_key", "personal_access_token", "access_token", "token", "secret", "access_key_secret", "secret_key"],
|
||||
providerForm: {
|
||||
id: null,
|
||||
@@ -168,10 +135,11 @@ export default {
|
||||
fields: [],
|
||||
sort: 0
|
||||
},
|
||||
DropdownVisible: false,
|
||||
tableColumns: []
|
||||
};
|
||||
},
|
||||
created() {
|
||||
this.initTableColumns();
|
||||
this.fetchProviders();
|
||||
},
|
||||
computed: {
|
||||
@@ -186,41 +154,43 @@ export default {
|
||||
const selectedType = this.modelTypes.find(item => item.value === this.searchModelType);
|
||||
return selectedType ? `(${this.$t(selectedType.labelKey)})` : "";
|
||||
},
|
||||
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;
|
||||
},
|
||||
filteredProvidersList() {
|
||||
return this.providersList;
|
||||
|
||||
// let list = this.providersList.filter(item => {
|
||||
// const nameMatch = item.name.toLowerCase().includes(this.searchName.toLowerCase());
|
||||
// const typeMatch = !this.searchModelType || item.model_type === this.searchModelType;
|
||||
// return nameMatch && typeMatch;
|
||||
// });
|
||||
|
||||
// list.sort((a, b) => a.sort - b.sort);
|
||||
|
||||
// // 分页处理
|
||||
// const start = (this.currentPage - 1) * this.pageSize;
|
||||
// return list.slice(start, start + this.pageSize);
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
initTableColumns() {
|
||||
this.tableColumns = [
|
||||
{
|
||||
prop: 'modelType',
|
||||
label: this.$t('providerManagement.category'),
|
||||
align: 'center',
|
||||
width: 200
|
||||
},
|
||||
{
|
||||
prop: 'providerCode',
|
||||
label: this.$t('providerManagement.providerCode'),
|
||||
align: 'center',
|
||||
width: 150
|
||||
},
|
||||
{
|
||||
prop: 'name',
|
||||
label: this.$t('common.name'),
|
||||
align: 'center'
|
||||
},
|
||||
{
|
||||
prop: 'fields',
|
||||
label: this.$t('providerManagement.fieldConfig'),
|
||||
align: 'center'
|
||||
},
|
||||
{
|
||||
prop: 'sort',
|
||||
label: this.$t('common.sort'),
|
||||
align: 'center',
|
||||
width: 80
|
||||
}
|
||||
];
|
||||
},
|
||||
fetchProviders() {
|
||||
this.loading = true;
|
||||
|
||||
@@ -255,7 +225,6 @@ export default {
|
||||
this.fetchProviders();
|
||||
},
|
||||
handleSelectModelType(value) {
|
||||
this.isDropdownOpen = false;
|
||||
this.searchModelType = value;
|
||||
this.handleSearch();
|
||||
},
|
||||
@@ -288,26 +257,23 @@ export default {
|
||||
handleSubmit({ form, done }) {
|
||||
this.loading = true;
|
||||
if (form.id) {
|
||||
// 编辑
|
||||
Api.model.updateModelProvider(form, ({ data }) => {
|
||||
|
||||
if (data.code === 0) {
|
||||
this.fetchProviders(); // 刷新表格
|
||||
this.fetchProviders();
|
||||
this.$message.success({
|
||||
message: this.$t('common.updateSuccess'),
|
||||
showClose: true
|
||||
});
|
||||
message: this.$t('common.updateSuccess'),
|
||||
showClose: true
|
||||
});
|
||||
}
|
||||
});
|
||||
} else {
|
||||
// 新增
|
||||
Api.model.addModelProvider(form, ({ data }) => {
|
||||
if (data.code === 0) {
|
||||
this.fetchProviders(); // 刷新表格
|
||||
this.fetchProviders();
|
||||
this.$message.success({
|
||||
message: this.$t('common.addSuccess'),
|
||||
showClose: true
|
||||
});
|
||||
message: this.$t('common.addSuccess'),
|
||||
showClose: true
|
||||
});
|
||||
this.total += 1;
|
||||
}
|
||||
});
|
||||
@@ -339,10 +305,8 @@ export default {
|
||||
const ids = providers.map(provider => provider.id);
|
||||
Api.model.deleteModelProviderByIds(ids, ({ data }) => {
|
||||
if (data.code === 0) {
|
||||
|
||||
this.isAllSelected = false;
|
||||
this.fetchProviders(); // 刷新表格
|
||||
|
||||
this.fetchProviders();
|
||||
this.$message.success({
|
||||
message: this.$t('common.deleteSuccess'),
|
||||
showClose: true
|
||||
@@ -390,57 +354,16 @@ export default {
|
||||
this.currentPage = 1;
|
||||
this.fetchProviders();
|
||||
},
|
||||
headerCellClassName({ columnIndex }) {
|
||||
if (columnIndex === 0) {
|
||||
return "custom-selection-header";
|
||||
}
|
||||
return "";
|
||||
},
|
||||
selectionCellClassName() {
|
||||
return "custom-selection-cell";
|
||||
},
|
||||
updateSelectionHeaderText() {
|
||||
// 确保表格已渲染
|
||||
this.$nextTick(() => {
|
||||
if (this.$refs.providersTable && this.$refs.providersTable.$el) {
|
||||
const headerCheckbox = this.$refs.providersTable.$el.querySelector('.custom-selection-header .el-checkbox .el-checkbox__label');
|
||||
if (headerCheckbox) {
|
||||
headerCheckbox.textContent = this.$t('modelConfig.select');
|
||||
}
|
||||
}
|
||||
});
|
||||
},
|
||||
mounted() {
|
||||
this.updateSelectionHeaderText();
|
||||
},
|
||||
updated() {
|
||||
this.updateSelectionHeaderText();
|
||||
},
|
||||
goFirst() {
|
||||
this.currentPage = 1;
|
||||
this.fetchProviders();
|
||||
},
|
||||
goPrev() {
|
||||
if (this.currentPage > 1) {
|
||||
this.currentPage--;
|
||||
this.fetchProviders();
|
||||
}
|
||||
},
|
||||
goNext() {
|
||||
if (this.currentPage < this.pageCount) {
|
||||
console.log("this.currentPage", this.currentPage);
|
||||
this.currentPage++;
|
||||
this.fetchProviders();
|
||||
}
|
||||
},
|
||||
goToPage(page) {
|
||||
this.currentPage = page;
|
||||
this.fetchProviders();
|
||||
if (page !== this.currentPage) {
|
||||
this.currentPage = page;
|
||||
this.fetchProviders();
|
||||
}
|
||||
},
|
||||
handleDropdownVisibleChange(visible) {
|
||||
this.DropdownVisible = visible;
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
@@ -453,32 +376,31 @@ export default {
|
||||
position: relative;
|
||||
flex-direction: column;
|
||||
background-size: cover;
|
||||
background: linear-gradient(to bottom right, #dce8ff, #e4eeff, #e6cbfd) center;
|
||||
background: #eff4ff;
|
||||
-webkit-background-size: cover;
|
||||
-o-background-size: cover;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.main-wrapper {
|
||||
// 顶部 63px 底部 35px 查询72px
|
||||
height: calc(100vh - 63px - 35px - 72px);
|
||||
margin: 0 22px;
|
||||
border-radius: 15px;
|
||||
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.1);
|
||||
// 顶部 63px 底部 35px
|
||||
height: calc(100vh - 63px - 35px);
|
||||
padding: 20px 22px 0;
|
||||
position: relative;
|
||||
background: rgba(237, 242, 255, 0.5);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.operation-bar {
|
||||
.operation-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 16px 24px;
|
||||
justify-content: space-between;
|
||||
padding: 0 0 16px 0;
|
||||
}
|
||||
|
||||
.page-title {
|
||||
font-weight: 500;
|
||||
font-size: 24px;
|
||||
margin: 0;
|
||||
}
|
||||
@@ -487,20 +409,14 @@ export default {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
margin-left: auto;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.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%;
|
||||
@@ -519,20 +435,17 @@ export default {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.el-card {
|
||||
border: none;
|
||||
}
|
||||
|
||||
.provider-card {
|
||||
background: white;
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
border: none;
|
||||
box-shadow: none;
|
||||
overflow: hidden;
|
||||
|
||||
::v-deep .el-card__body {
|
||||
padding: 15px;
|
||||
padding: 14px 20px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex: 1;
|
||||
@@ -540,284 +453,8 @@ export default {
|
||||
}
|
||||
}
|
||||
|
||||
.table_bottom {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-top: 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 {
|
||||
color: #909399;
|
||||
font-size: 14px;
|
||||
margin-left: 10px;
|
||||
}
|
||||
}
|
||||
|
||||
:deep(.transparent-table) {
|
||||
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 {
|
||||
background: white !important;
|
||||
color: black;
|
||||
}
|
||||
|
||||
&::before {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.el-table__body tr {
|
||||
background-color: white;
|
||||
|
||||
td {
|
||||
border-top: 1px solid rgba(0, 0, 0, 0.04);
|
||||
border-bottom: 1px solid rgba(0, 0, 0, 0.04);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
:deep(.el-checkbox__inner) {
|
||||
background-color: #ffffff !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;
|
||||
}
|
||||
|
||||
@media (min-width: 1144px) {
|
||||
.table_bottom {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-top: 40px;
|
||||
}
|
||||
|
||||
:deep(.transparent-table) {
|
||||
.el-table__body tr {
|
||||
td {
|
||||
padding-top: 16px;
|
||||
padding-bottom: 16px;
|
||||
}
|
||||
|
||||
&+tr {
|
||||
margin-top: 10px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
:deep(.el-table .el-button--text) {
|
||||
color: #7079aa;
|
||||
}
|
||||
|
||||
:deep(.el-table .el-button--text:hover) {
|
||||
color: #5a64b5;
|
||||
}
|
||||
|
||||
.el-button--success {
|
||||
background: #5bc98c;
|
||||
color: white;
|
||||
}
|
||||
|
||||
:deep(.el-table .cell) {
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
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;
|
||||
}
|
||||
|
||||
.field-item {
|
||||
@@ -843,40 +480,6 @@ export default {
|
||||
}
|
||||
}
|
||||
|
||||
.dropdown-trigger {
|
||||
font-size: 14px;
|
||||
color: #303133;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
&:hover {
|
||||
color: #409EFF;
|
||||
}
|
||||
}
|
||||
|
||||
.dropdown-trigger.active {
|
||||
color: #409EFF;
|
||||
}
|
||||
|
||||
.dropdown-arrow {
|
||||
display: inline-block;
|
||||
margin-left: 5px;
|
||||
width: 0;
|
||||
height: 0;
|
||||
border-left: 5px solid transparent;
|
||||
border-right: 5px solid transparent;
|
||||
border-top: 7px solid black;
|
||||
position: relative;
|
||||
transition: transform 0.3s ease;
|
||||
transform: rotate(0deg);
|
||||
|
||||
&.is-active {
|
||||
transform: rotate(180deg);
|
||||
border-top-color: #409EFF;
|
||||
}
|
||||
}
|
||||
|
||||
.rotate-down {
|
||||
transform: rotate(180deg);
|
||||
transition: transform 0.3s ease;
|
||||
@@ -886,41 +489,11 @@ export default {
|
||||
transition: transform 0.3s ease;
|
||||
}
|
||||
|
||||
.dropdown-trigger {
|
||||
font-size: 14px;
|
||||
color: #303133;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
&:hover {
|
||||
color: #409EFF;
|
||||
}
|
||||
:deep(.el-table .el-button--text) {
|
||||
color: #7079aa;
|
||||
}
|
||||
|
||||
.dropdown-trigger.active {
|
||||
color: #409EFF;
|
||||
:deep(.el-table .el-button--text:hover) {
|
||||
color: #5a64b5;
|
||||
}
|
||||
|
||||
/* 确保选择列标题样式正确 */
|
||||
.custom-selection-header {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
:deep(.custom-selection-header .el-checkbox) {
|
||||
display: flex !important;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
:deep(.custom-selection-header .el-checkbox__label) {
|
||||
position: relative;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
:deep(.custom-selection-cell) {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
</style>
|
||||
</style>
|
||||
|
||||