mirror of
https://github.com/xinnan-tech/xiaozhi-esp32-server.git
synced 2026-07-22 07:03:53 +08:00
Merge branch 'main' into tts-response
# Conflicts: # main/manager-web/.env.development
This commit is contained in:
@@ -4,6 +4,7 @@ on:
|
||||
push:
|
||||
tags:
|
||||
- 'v*.*.*' # 只在以 v 开头的标签推送时触发,例如 v1.0.0
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
release:
|
||||
@@ -41,7 +42,13 @@ jobs:
|
||||
- name: Extract version from tag
|
||||
id: get_version
|
||||
run: |
|
||||
echo "VERSION=${GITHUB_REF#refs/tags/}" >> $GITHUB_ENV
|
||||
if [[ "$GITHUB_REF" =~ ^refs/tags/v([0-9]+\.[0-9]+\.[0-9]+)$ ]]; then
|
||||
echo "VERSION=${BASH_REMATCH[1]}" >> $GITHUB_ENV
|
||||
echo "IS_VERSION=true" >> $GITHUB_ENV
|
||||
else
|
||||
echo "VERSION=latest" >> $GITHUB_ENV
|
||||
echo "IS_VERSION=false" >> $GITHUB_ENV
|
||||
fi
|
||||
|
||||
# 构建 xiaozhi-server 镜像
|
||||
- name: Build and push xiaozhi-server
|
||||
@@ -51,8 +58,7 @@ jobs:
|
||||
file: Dockerfile-server
|
||||
push: true
|
||||
tags: |
|
||||
ghcr.io/${{ github.repository }}:server_${{ env.VERSION }}
|
||||
ghcr.io/${{ github.repository }}:server_latest
|
||||
${{ env.IS_VERSION == 'true' && format('ghcr.io/{0}:server_{1}\nghcr.io/{0}:server_latest', github.repository, env.VERSION) || format('ghcr.io/{0}:server_latest', github.repository) }}
|
||||
platforms: linux/amd64,linux/arm64
|
||||
|
||||
# 构建 manager-api 镜像
|
||||
@@ -63,6 +69,5 @@ jobs:
|
||||
file: Dockerfile-web
|
||||
push: true
|
||||
tags: |
|
||||
ghcr.io/${{ github.repository }}:web_${{ env.VERSION }}
|
||||
ghcr.io/${{ github.repository }}:web_latest
|
||||
${{ env.IS_VERSION == 'true' && format('ghcr.io/{0}:web_{1}\nghcr.io/{0}:web_latest', github.repository, env.VERSION) || format('ghcr.io/{0}:web_latest', github.repository) }}
|
||||
platforms: linux/amd64,linux/arm64
|
||||
+12
-8
@@ -311,15 +311,17 @@ LLM:
|
||||
如果你能看到,类似以下日志,则是本项目服务启动成功的标志。
|
||||
|
||||
```
|
||||
25-02-23 12:01:09[core.websocket_server] - INFO - Server is running at ws://xxx.xx.xx.xx:8000
|
||||
25-02-23 12:01:09[core.websocket_server] - INFO - Server is running at ws://xxx.xx.xx.xx:8000/xiaozhi/v1/
|
||||
25-02-23 12:01:09[core.websocket_server] - INFO - =======上面的地址是websocket协议地址,请勿用浏览器访问=======
|
||||
25-02-23 12:01:09[core.websocket_server] - INFO - 如想测试websocket请用谷歌浏览器打开test目录下的test_page.html
|
||||
25-02-23 12:01:09[core.websocket_server] - INFO - =======================================================
|
||||
```
|
||||
|
||||
正常来说,如果您是通过源码运行本项目,日志会有你的接口地址信息。
|
||||
但是如果你用docker部署,那么你的日志里给出的接口地址信息就不是真实的接口地址。
|
||||
|
||||
最正确的方法,是根据电脑的局域网IP来确定你的接口地址。
|
||||
如果你的电脑的局域网IP比如是`192.168.1.25`,那么你的接口地址就是:`ws://192.168.1.25:8000`。
|
||||
如果你的电脑的局域网IP比如是`192.168.1.25`,那么你的接口地址就是:`ws://192.168.1.25:8000/xiaozhi/v1/`。
|
||||
|
||||
这个信息很有用的,后面`编译esp32固件`需要用到。
|
||||
|
||||
@@ -328,14 +330,16 @@ LLM:
|
||||
|
||||
以下是一些常见问题,供参考:
|
||||
|
||||
[1、为什么我说的话,小智识别出来很多韩文、日文、英文](./FAQ.md#1%E4%B8%BA%E4%BB%80%E4%B9%88%E6%88%91%E8%AF%B4%E7%9A%84%E8%AF%9D%E5%B0%8F%E6%99%BA%E8%AF%86%E5%88%AB%E5%87%BA%E6%9D%A5%E5%BE%88%E5%A4%9A%E9%9F%A9%E6%96%87%E6%97%A5%E6%96%87%E8%8B%B1%E6%96%87)
|
||||
[1、为什么我说的话,小智识别出来很多韩文、日文、英文](./FAQ.md)
|
||||
|
||||
[2、为什么会出现“TTS 任务出错 文件不存在”?](./FAQ.md#1%E4%B8%BA%E4%BB%80%E4%B9%88%E6%88%91%E8%AF%B4%E7%9A%84%E8%AF%9D%E5%B0%8F%E6%99%BA%E8%AF%86%E5%88%AB%E5%87%BA%E6%9D%A5%E5%BE%88%E5%A4%9A%E9%9F%A9%E6%96%87%E6%97%A5%E6%96%87%E8%8B%B1%E6%96%87)
|
||||
[2、为什么会出现“TTS 任务出错 文件不存在”?](./FAQ.md)
|
||||
|
||||
[3、TTS 经常失败,经常超时](./FAQ.md#1%E4%B8%BA%E4%BB%80%E4%B9%88%E6%88%91%E8%AF%B4%E7%9A%84%E8%AF%9D%E5%B0%8F%E6%99%BA%E8%AF%86%E5%88%AB%E5%87%BA%E6%9D%A5%E5%BE%88%E5%A4%9A%E9%9F%A9%E6%96%87%E6%97%A5%E6%96%87%E8%8B%B1%E6%96%87)
|
||||
[3、TTS 经常失败,经常超时](./FAQ.md)
|
||||
|
||||
[4、如何提高小智对话响应速度?](./FAQ.md#1%E4%B8%BA%E4%BB%80%E4%B9%88%E6%88%91%E8%AF%B4%E7%9A%84%E8%AF%9D%E5%B0%8F%E6%99%BA%E8%AF%86%E5%88%AB%E5%87%BA%E6%9D%A5%E5%BE%88%E5%A4%9A%E9%9F%A9%E6%96%87%E6%97%A5%E6%96%87%E8%8B%B1%E6%96%87)
|
||||
[4、使用Wifi能连接自建服务器,但是4G模式却接不上](./FAQ.md)
|
||||
|
||||
[5、我说话很慢,停顿时小智老是抢话](./FAQ.md#1%E4%B8%BA%E4%BB%80%E4%B9%88%E6%88%91%E8%AF%B4%E7%9A%84%E8%AF%9D%E5%B0%8F%E6%99%BA%E8%AF%86%E5%88%AB%E5%87%BA%E6%9D%A5%E5%BE%88%E5%A4%9A%E9%9F%A9%E6%96%87%E6%97%A5%E6%96%87%E8%8B%B1%E6%96%87)
|
||||
[5、如何提高小智对话响应速度?](./FAQ.md)
|
||||
|
||||
[6、我想通过小智控制电灯、空调、远程开关机等操作](./FAQ.md#1%E4%B8%BA%E4%BB%80%E4%B9%88%E6%88%91%E8%AF%B4%E7%9A%84%E8%AF%9D%E5%B0%8F%E6%99%BA%E8%AF%86%E5%88%AB%E5%87%BA%E6%9D%A5%E5%BE%88%E5%A4%9A%E9%9F%A9%E6%96%87%E6%97%A5%E6%96%87%E8%8B%B1%E6%96%87)
|
||||
[6、我说话很慢,停顿时小智老是抢话](./FAQ.md)
|
||||
|
||||
[7、我想通过小智控制电灯、空调、远程开关机等操作](./FAQ.md)
|
||||
|
||||
+14
-4
@@ -21,7 +21,17 @@ conda install conda-forge::ffmpeg
|
||||
建议:如果 `EdgeTTS` 经常失败,请先检查是否使用了代理(梯子)。如果使用了,请尝试关闭代理后再试;
|
||||
如果用的是火山引擎的豆包 TTS,经常失败时建议使用付费版本,因为测试版本仅支持 2 个并发。
|
||||
|
||||
### 4、如何提高小智对话响应速度? ⚡
|
||||
### 4、使用Wifi能连接自建服务器,但是4G模式却接不上 🔐
|
||||
|
||||
原因:虾哥的固件,4G模式需要使用安全连接。
|
||||
|
||||
解决方法:目前有两种方法可以解决。任选一种:
|
||||
|
||||
1、改代码。参考这个视频解决 https://www.bilibili.com/video/BV18MfTYoE85
|
||||
|
||||
2、使用nginx配置ssl证书。参考教程 https://icnt94i5ctj4.feishu.cn/docx/GnYOdMNJOoRCljx1ctecsj9cnRe
|
||||
|
||||
### 5、如何提高小智对话响应速度? ⚡
|
||||
|
||||
本项目默认配置为低成本方案,建议初学者先使用默认免费模型,解决"跑得动"的问题,再优化"跑得快"。
|
||||
如需提升响应速度,可尝试更换各组件。以下为各组件的响应速度测试数据(仅供参考,不构成承诺):
|
||||
@@ -78,7 +88,7 @@ TTS 性能排行:
|
||||
- LLM:`AliLLM`
|
||||
- TTS:`DoubaoTTS`
|
||||
|
||||
### 5、我说话很慢,停顿时小智老是抢话 🗣️
|
||||
### 6、我说话很慢,停顿时小智老是抢话 🗣️
|
||||
|
||||
建议:在配置文件中找到如下部分,将 `min_silence_duration_ms` 的值调大(例如改为 `1000`):
|
||||
|
||||
@@ -90,7 +100,7 @@ VAD:
|
||||
min_silence_duration_ms: 700 # 如果说话停顿较长,可将此值调大
|
||||
```
|
||||
|
||||
### 6、我想通过小智控制电灯、空调、远程开关机等操作 💡
|
||||
### 7、我想通过小智控制电灯、空调、远程开关机等操作 💡
|
||||
|
||||
本项目,支持以工具调用的方式控制HomeAssistant设备
|
||||
|
||||
@@ -128,7 +138,7 @@ Intent:
|
||||
- hass_play_music
|
||||
```
|
||||
|
||||
### 7、更多问题,可联系我们反馈 💬
|
||||
### 8、更多问题,可联系我们反馈 💬
|
||||
|
||||
我们的联系方式放在[百度网盘中,点击前往](https://pan.baidu.com/s/1x6USjvP1nTRsZ45XlJu65Q),提取码是`223y`。
|
||||
|
||||
|
||||
@@ -9,7 +9,7 @@ server {
|
||||
}
|
||||
|
||||
# API反向代理(Java项目)
|
||||
location /xiaozhi-esp32-api/ {
|
||||
location /xiaozhi/ {
|
||||
proxy_pass http://127.0.0.1:8003;
|
||||
proxy_set_header Host $host;
|
||||
proxy_cookie_path /api/ /;
|
||||
|
||||
+11
-10
@@ -1,10 +1,9 @@
|
||||
# 编译esp32固件
|
||||
|
||||
1. 下载`xiaozhi-esp32`
|
||||
项目,按照这个教程配置项目环境[《Windows搭建 ESP IDF 5.3.2开发环境以及编译小智》](https://icnynnzcwou8.feishu.cn/wiki/JEYDwTTALi5s2zkGlFGcDiRknXf)
|
||||
1. 先按照这个教程配置项目环境[《Windows搭建 ESP IDF 5.3.2开发环境以及编译小智》](https://icnynnzcwou8.feishu.cn/wiki/JEYDwTTALi5s2zkGlFGcDiRknXf)
|
||||
|
||||
2. 打开`xiaozhi-esp32/main/Kconfig.projbuild`文件,找到`WEBSOCKET_URL`的`default`的内容,把`wss://api.tenclass.net`
|
||||
改成你自己的地址,例如,我的接口地址是`ws://192.168.1.25:8000`,就把内容改成这个。
|
||||
2. 配置好编译环境后,下载虾哥[xiaozhi-esp32](https://github.com/78/xiaozhi-esp32)项目源码,进入虾哥项目,打开`xiaozhi-esp32/main/Kconfig.projbuild`文件,找到`WEBSOCKET_URL`的`default`的内容,把`wss://api.tenclass.net`
|
||||
改成你自己的地址,例如,我的接口地址是`ws://192.168.1.25:8000/xiaozhi/v1/`,就把内容改成这个。
|
||||
|
||||
修改前:
|
||||
|
||||
@@ -92,14 +91,16 @@ https://espressif.github.io/esp-launchpad/
|
||||
|
||||
以下是一些常见问题,供参考:
|
||||
|
||||
[1、为什么我说的话,小智识别出来很多韩文、日文、英文](./FAQ.md#1tts-%E7%BB%8F%E5%B8%B8%E5%A4%B1%E8%B4%A5%E7%BB%8F%E5%B8%B8%E8%B6%85%E6%97%B6-)
|
||||
[1、为什么我说的话,小智识别出来很多韩文、日文、英文](./FAQ.md)
|
||||
|
||||
[2、为什么会出现“TTS 任务出错 文件不存在”?](./FAQ.md#1tts-%E7%BB%8F%E5%B8%B8%E5%A4%B1%E8%B4%A5%E7%BB%8F%E5%B8%B8%E8%B6%85%E6%97%B6-)
|
||||
[2、为什么会出现“TTS 任务出错 文件不存在”?](./FAQ.md)
|
||||
|
||||
[3、TTS 经常失败,经常超时](./FAQ.md#1tts-%E7%BB%8F%E5%B8%B8%E5%A4%B1%E8%B4%A5%E7%BB%8F%E5%B8%B8%E8%B6%85%E6%97%B6-)
|
||||
[3、TTS 经常失败,经常超时](./FAQ.md)
|
||||
|
||||
[4、如何提高小智对话响应速度?](./FAQ.md#1tts-%E7%BB%8F%E5%B8%B8%E5%A4%B1%E8%B4%A5%E7%BB%8F%E5%B8%B8%E8%B6%85%E6%97%B6-)
|
||||
[4、使用Wifi能连接自建服务器,但是4G模式却接不上](./FAQ.md)
|
||||
|
||||
[5、我说话很慢,停顿时小智老是抢话](./FAQ.md#1tts-%E7%BB%8F%E5%B8%B8%E5%A4%B1%E8%B4%A5%E7%BB%8F%E5%B8%B8%E8%B6%85%E6%97%B6-)
|
||||
[5、如何提高小智对话响应速度?](./FAQ.md)
|
||||
|
||||
[6、我想通过小智控制电灯、空调、远程开关机等操作](./FAQ.md#1tts-%E7%BB%8F%E5%B8%B8%E5%A4%B1%E8%B4%A5%E7%BB%8F%E5%B8%B8%E8%B6%85%E6%97%B6-)
|
||||
[6、我说话很慢,停顿时小智老是抢话](./FAQ.md)
|
||||
|
||||
[7、我想通过小智控制电灯、空调、远程开关机等操作](./FAQ.md)
|
||||
|
||||
+1
-1
@@ -18,4 +18,4 @@ xiaozhi-esp32-server
|
||||
|
||||
# manager-web 、manager-api接口协议
|
||||
|
||||
https://2662r3426b.vicp.fun/xiaozhi-esp32-api/api/v1/doc.html
|
||||
https://2662r3426b.vicp.fun/xiaozhi/v1/doc.html
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
本文档是开发类文档,如需部署小智服务端,[点击这里查看部署教程](.././FAQ.md#%E4%BD%BF%E7%94%A8%E6%96%B9%E5%BC%8F-)
|
||||
本文档是开发类文档,如需部署小智服务端,[点击这里查看部署教程](../../../docs/Deployment.md)
|
||||
# 项目介绍
|
||||
|
||||
manager-api 该项目基于SpringBoot框架开发。
|
||||
@@ -23,12 +23,7 @@ CREATE DATABASE xiaozhi_esp32_server CHARACTER SET utf8mb4 COLLATE utf8mb4_unico
|
||||
如果还没有MySQL,你可以通过docker安装mysql
|
||||
|
||||
```
|
||||
docker run --name xiaozhi-esp32-server-db \
|
||||
-e MYSQL_ROOT_PASSWORD=123456 \
|
||||
-p 3306:3306 \
|
||||
-e MYSQL_DATABASE=xiaozhi_esp32_server \
|
||||
-e MYSQL_INITDB_ARGS="--character-set-server=utf8mb4 --collation-server=utf8mb4_unicode_ci" \
|
||||
-d mysql:latest
|
||||
docker run --name xiaozhi-esp32-server-db -e MYSQL_ROOT_PASSWORD=123456 -p 3306:3306 -e MYSQL_DATABASE=xiaozhi_esp32_server -e MYSQL_INITDB_ARGS="--character-set-server=utf8mb4 --collation-server=utf8mb4_unicode_ci" -d mysql:latest
|
||||
```
|
||||
|
||||
# 确认项目数据库连接信息
|
||||
@@ -92,5 +87,5 @@ nohup java -jar xiaozhi-esp32-api.jar --spring.profiles.activate=dev
|
||||
|
||||
|
||||
# 接口文档
|
||||
启动后打开:http://localhost:8002/xiaozhi-esp32-api/doc.html
|
||||
启动后打开:http://localhost:8002/xiaozhi/doc.html
|
||||
|
||||
|
||||
@@ -8,6 +8,6 @@ public class AdminApplication {
|
||||
|
||||
public static void main(String[] args) {
|
||||
SpringApplication.run(AdminApplication.class, args);
|
||||
System.out.println("http://localhost:8002/xiaozhi-esp32-api/api/v1/doc.html");
|
||||
System.out.println("http://localhost:8002/xiaozhi/doc.html");
|
||||
}
|
||||
}
|
||||
@@ -51,7 +51,7 @@ public class SwaggerConfig {
|
||||
public GroupedOpenApi timbreApi() {
|
||||
return GroupedOpenApi.builder()
|
||||
.group("timbre")
|
||||
.pathsToMatch("/timbre/**")
|
||||
.pathsToMatch("/ttsVoice/**")
|
||||
.build();
|
||||
}
|
||||
|
||||
|
||||
@@ -4,6 +4,7 @@ import org.apache.shiro.authz.UnauthorizedException;
|
||||
import org.springframework.dao.DuplicateKeyException;
|
||||
import org.springframework.web.bind.annotation.ExceptionHandler;
|
||||
import org.springframework.web.bind.annotation.RestControllerAdvice;
|
||||
import org.springframework.web.servlet.resource.NoResourceFoundException;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
@@ -53,4 +54,10 @@ public class RenExceptionHandler {
|
||||
return new Result<Void>().error();
|
||||
}
|
||||
|
||||
@ExceptionHandler(NoResourceFoundException.class)
|
||||
public Result<Void> handleNoResourceFoundException(NoResourceFoundException ex) {
|
||||
log.warn("Resource not found: {}", ex.getMessage());
|
||||
return new Result<Void>().error(404, "资源不存在");
|
||||
}
|
||||
|
||||
}
|
||||
@@ -26,4 +26,32 @@ public class RedisKeys {
|
||||
public static String getDeviceCaptchaKey(String captcha) {
|
||||
return "sys:device:captcha:" + captcha;
|
||||
}
|
||||
|
||||
/**
|
||||
* 用户id的Key
|
||||
*/
|
||||
public static String getUserIdKey(Long userid) {
|
||||
return "sys:username:id:" + userid;
|
||||
}
|
||||
|
||||
/**
|
||||
* 模型名称的Key
|
||||
*/
|
||||
public static String getModelNameById(String id) {
|
||||
return "sys:model:name:" + id;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取音色名称缓存key
|
||||
*/
|
||||
public static String getTimbreNameById(String id) {
|
||||
return "timbre:name:" + id;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取设备数量缓存key
|
||||
*/
|
||||
public static String getAgentDeviceCountById(String id) {
|
||||
return "agent:device:count:" + id;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,6 +2,9 @@ package xiaozhi.common.utils;
|
||||
|
||||
import java.text.ParseException;
|
||||
import java.text.SimpleDateFormat;
|
||||
import java.time.LocalDateTime;
|
||||
import java.time.ZoneId;
|
||||
import java.time.temporal.ChronoUnit;
|
||||
import java.util.Date;
|
||||
|
||||
/**
|
||||
@@ -60,4 +63,39 @@ public class DateUtils {
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取简短的时间字符串:10秒前返回刚刚,多少秒前,几小时前,超过一周返回年月日时分秒
|
||||
* @param date
|
||||
* @return
|
||||
*/
|
||||
public static String getShortTime(Date date) {
|
||||
if (date == null) {
|
||||
return null;
|
||||
}
|
||||
// 将 Date 转换为 Instant
|
||||
LocalDateTime localDateTime = date.toInstant()
|
||||
// 获取系统默认时区
|
||||
.atZone(ZoneId.systemDefault())
|
||||
// 转换为 LocalDateTime
|
||||
.toLocalDateTime();
|
||||
// 当前时间
|
||||
LocalDateTime now = LocalDateTime.now();
|
||||
// 时间差,单位为秒
|
||||
long secondsBetween = ChronoUnit.SECONDS.between(localDateTime, now);
|
||||
|
||||
if (secondsBetween <= 10) {
|
||||
return "刚刚";
|
||||
} else if (secondsBetween < 60) {
|
||||
return secondsBetween + "秒前";
|
||||
} else if (secondsBetween < 60 * 60) {
|
||||
return secondsBetween / 60 + "分钟前";
|
||||
} else if (secondsBetween < 86400) {
|
||||
return secondsBetween / 3600 + "小时前";
|
||||
} else if (secondsBetween < 604800) {
|
||||
return secondsBetween / 86400 + "天前";
|
||||
} else {
|
||||
// 超过一周,显示完整日期时间
|
||||
return format(date,DATE_TIME_PATTERN);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+25
-3
@@ -29,11 +29,13 @@ import xiaozhi.common.user.UserDetail;
|
||||
import xiaozhi.common.utils.ConvertUtils;
|
||||
import xiaozhi.common.utils.Result;
|
||||
import xiaozhi.modules.agent.dto.AgentCreateDTO;
|
||||
import xiaozhi.modules.agent.dto.AgentDTO;
|
||||
import xiaozhi.modules.agent.dto.AgentUpdateDTO;
|
||||
import xiaozhi.modules.agent.entity.AgentEntity;
|
||||
import xiaozhi.modules.agent.entity.AgentTemplateEntity;
|
||||
import xiaozhi.modules.agent.service.AgentService;
|
||||
import xiaozhi.modules.agent.service.AgentTemplateService;
|
||||
import xiaozhi.modules.device.service.DeviceService;
|
||||
import xiaozhi.modules.security.user.SecurityUser;
|
||||
|
||||
@Tag(name = "智能体管理")
|
||||
@@ -43,14 +45,15 @@ import xiaozhi.modules.security.user.SecurityUser;
|
||||
public class AgentController {
|
||||
private final AgentService agentService;
|
||||
private final AgentTemplateService agentTemplateService;
|
||||
private final DeviceService deviceService;
|
||||
|
||||
@GetMapping("/list")
|
||||
@Operation(summary = "获取用户智能体列表")
|
||||
@RequiresPermissions("sys:role:normal")
|
||||
public Result<List<AgentEntity>> getUserAgents() {
|
||||
public Result<List<AgentDTO>> getUserAgents() {
|
||||
UserDetail user = SecurityUser.getUser();
|
||||
List<AgentEntity> agents = agentService.getUserAgents(user.getId());
|
||||
return new Result<List<AgentEntity>>().ok(agents);
|
||||
List<AgentDTO> agents = agentService.getUserAgents(user.getId());
|
||||
return new Result<List<AgentDTO>>().ok(agents);
|
||||
}
|
||||
|
||||
@GetMapping("/all")
|
||||
@@ -80,6 +83,22 @@ public class AgentController {
|
||||
public Result<Void> save(@RequestBody @Valid AgentCreateDTO dto) {
|
||||
AgentEntity entity = ConvertUtils.sourceToTarget(dto, AgentEntity.class);
|
||||
|
||||
// 获取默认模板
|
||||
AgentTemplateEntity template = agentTemplateService.getDefaultTemplate();
|
||||
if (template != null) {
|
||||
// 设置模板中的默认值
|
||||
entity.setAsrModelId(template.getAsrModelId());
|
||||
entity.setVadModelId(template.getVadModelId());
|
||||
entity.setLlmModelId(template.getLlmModelId());
|
||||
entity.setTtsModelId(template.getTtsModelId());
|
||||
entity.setTtsVoiceId(template.getTtsVoiceId());
|
||||
entity.setMemModelId(template.getMemModelId());
|
||||
entity.setIntentModelId(template.getIntentModelId());
|
||||
entity.setSystemPrompt(template.getSystemPrompt());
|
||||
entity.setLangCode(template.getLangCode());
|
||||
entity.setLanguage(template.getLanguage());
|
||||
}
|
||||
|
||||
// 设置用户ID和创建者信息
|
||||
UserDetail user = SecurityUser.getUser();
|
||||
entity.setUserId(user.getId());
|
||||
@@ -157,6 +176,9 @@ public class AgentController {
|
||||
@Operation(summary = "删除智能体")
|
||||
@RequiresPermissions("sys:role:normal")
|
||||
public Result<Void> delete(@PathVariable String id) {
|
||||
// 先删除关联的设备
|
||||
deviceService.deleteByAgentId(id);
|
||||
// 再删除智能体
|
||||
agentService.deleteById(id);
|
||||
return new Result<>();
|
||||
}
|
||||
|
||||
@@ -1,11 +1,18 @@
|
||||
package xiaozhi.modules.agent.dao;
|
||||
|
||||
import org.apache.ibatis.annotations.Mapper;
|
||||
import org.apache.ibatis.annotations.Param;
|
||||
|
||||
import xiaozhi.common.dao.BaseDao;
|
||||
import xiaozhi.modules.agent.entity.AgentEntity;
|
||||
|
||||
@Mapper
|
||||
public interface AgentDao extends BaseDao<AgentEntity> {
|
||||
|
||||
/**
|
||||
* 获取智能体的设备数量
|
||||
*
|
||||
* @param agentId 智能体ID
|
||||
* @return 设备数量
|
||||
*/
|
||||
Integer getDeviceCountByAgentId(@Param("agentId") String agentId);
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
package xiaozhi.modules.agent.dto;
|
||||
|
||||
import java.util.Date;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.Data;
|
||||
|
||||
/**
|
||||
* 智能体数据传输对象
|
||||
* 用于在服务层和控制器层之间传递智能体相关的数据
|
||||
*/
|
||||
@Data
|
||||
@Schema(description = "智能体对象")
|
||||
public class AgentDTO {
|
||||
@Schema(description = "智能体编码", example = "AGT_1234567890")
|
||||
private String id;
|
||||
|
||||
@Schema(description = "智能体名称", example = "客服助手")
|
||||
private String agentName;
|
||||
|
||||
@Schema(description = "语音合成模型名称", example = "tts_model_01")
|
||||
private String ttsModelName;
|
||||
|
||||
@Schema(description = "音色名称", example = "voice_01")
|
||||
private String ttsVoiceName;
|
||||
|
||||
@Schema(description = "大语言模型名称", example = "llm_model_01")
|
||||
private String llmModelName;
|
||||
|
||||
@Schema(description = "角色设定参数", example = "你是一个专业的客服助手,负责回答用户问题并提供帮助")
|
||||
private String systemPrompt;
|
||||
|
||||
@Schema(description = "最后连接时间", example = "2024-03-20 10:00:00")
|
||||
private Date lastConnectedAt;
|
||||
|
||||
@Schema(description = "设备数量", example = "10")
|
||||
private Integer deviceCount;
|
||||
}
|
||||
@@ -5,13 +5,10 @@ import java.util.Map;
|
||||
|
||||
import xiaozhi.common.page.PageData;
|
||||
import xiaozhi.common.service.BaseService;
|
||||
import xiaozhi.modules.agent.dto.AgentDTO;
|
||||
import xiaozhi.modules.agent.entity.AgentEntity;
|
||||
|
||||
public interface AgentService extends BaseService<AgentEntity> {
|
||||
/**
|
||||
* 根据用户ID获取智能体列表
|
||||
*/
|
||||
List<AgentEntity> getUserAgents(Long userId);
|
||||
|
||||
/**
|
||||
* 管理员获取所有智能体列表(分页)
|
||||
@@ -22,4 +19,27 @@ public interface AgentService extends BaseService<AgentEntity> {
|
||||
* 获取智能体详情
|
||||
*/
|
||||
AgentEntity getAgentById(String id);
|
||||
|
||||
/**
|
||||
* 删除这个用户的所有
|
||||
*
|
||||
* @param userId
|
||||
*/
|
||||
void deleteAgentByUserId(Long userId);
|
||||
|
||||
/**
|
||||
* 获取用户智能体列表
|
||||
*
|
||||
* @param userId
|
||||
* @return
|
||||
*/
|
||||
List<AgentDTO> getUserAgents(Long userId);
|
||||
|
||||
/**
|
||||
* 获取智能体的设备数量
|
||||
*
|
||||
* @param agentId 智能体ID
|
||||
* @return 设备数量
|
||||
*/
|
||||
Integer getDeviceCountByAgentId(String agentId);
|
||||
}
|
||||
@@ -11,4 +11,10 @@ import xiaozhi.modules.agent.entity.AgentTemplateEntity;
|
||||
*/
|
||||
public interface AgentTemplateService extends IService<AgentTemplateEntity> {
|
||||
|
||||
/**
|
||||
* 获取默认模板
|
||||
*
|
||||
* @return 默认模板实体
|
||||
*/
|
||||
AgentTemplateEntity getDefaultTemplate();
|
||||
}
|
||||
|
||||
+75
-7
@@ -3,33 +3,44 @@ package xiaozhi.modules.agent.service.impl;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.UUID;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
|
||||
import com.baomidou.mybatisplus.core.conditions.update.UpdateWrapper;
|
||||
import com.baomidou.mybatisplus.core.metadata.IPage;
|
||||
|
||||
import xiaozhi.common.page.PageData;
|
||||
import xiaozhi.common.redis.RedisKeys;
|
||||
import xiaozhi.common.redis.RedisUtils;
|
||||
import xiaozhi.common.service.impl.BaseServiceImpl;
|
||||
import xiaozhi.modules.agent.dao.AgentDao;
|
||||
import xiaozhi.modules.agent.dto.AgentDTO;
|
||||
import xiaozhi.modules.agent.entity.AgentEntity;
|
||||
import xiaozhi.modules.agent.service.AgentService;
|
||||
import xiaozhi.modules.model.service.ModelConfigService;
|
||||
import xiaozhi.modules.timbre.service.TimbreService;
|
||||
|
||||
@Service
|
||||
public class AgentServiceImpl extends BaseServiceImpl<AgentDao, AgentEntity> implements AgentService {
|
||||
private final AgentDao agentDao;
|
||||
|
||||
@Autowired
|
||||
private TimbreService timbreModelService;
|
||||
|
||||
@Autowired
|
||||
private ModelConfigService modelConfigService;
|
||||
|
||||
@Autowired
|
||||
private RedisUtils redisUtils;
|
||||
|
||||
public AgentServiceImpl(AgentDao agentDao) {
|
||||
this.agentDao = agentDao;
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<AgentEntity> getUserAgents(Long userId) {
|
||||
QueryWrapper<AgentEntity> wrapper = new QueryWrapper<>();
|
||||
wrapper.eq("user_id", userId);
|
||||
return agentDao.selectList(wrapper);
|
||||
}
|
||||
|
||||
@Override
|
||||
public PageData<AgentEntity> adminAgentList(Map<String, Object> params) {
|
||||
IPage<AgentEntity> page = agentDao.selectPage(
|
||||
@@ -62,4 +73,61 @@ public class AgentServiceImpl extends BaseServiceImpl<AgentDao, AgentEntity> imp
|
||||
|
||||
return super.insert(entity);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void deleteAgentByUserId(Long userId) {
|
||||
UpdateWrapper<AgentEntity> wrapper = new UpdateWrapper<>();
|
||||
wrapper.eq("user_id", userId);
|
||||
baseDao.delete(wrapper);
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<AgentDTO> getUserAgents(Long userId) {
|
||||
QueryWrapper<AgentEntity> wrapper = new QueryWrapper<>();
|
||||
wrapper.eq("user_id", userId);
|
||||
List<AgentEntity> agents = agentDao.selectList(wrapper);
|
||||
return agents.stream().map(agent -> {
|
||||
AgentDTO dto = new AgentDTO();
|
||||
dto.setId(agent.getId());
|
||||
dto.setAgentName(agent.getAgentName());
|
||||
dto.setSystemPrompt(agent.getSystemPrompt());
|
||||
|
||||
// 获取 TTS 模型名称
|
||||
dto.setTtsModelName(modelConfigService.getModelNameById(agent.getTtsModelId()));
|
||||
|
||||
// 获取 LLM 模型名称
|
||||
dto.setLlmModelName(modelConfigService.getModelNameById(agent.getLlmModelId()));
|
||||
|
||||
// 获取 TTS 音色名称
|
||||
dto.setTtsVoiceName(timbreModelService.getTimbreNameById(agent.getTtsVoiceId()));
|
||||
|
||||
// 获取设备数量
|
||||
dto.setDeviceCount(getDeviceCountByAgentId(agent.getId()));
|
||||
|
||||
return dto;
|
||||
}).collect(Collectors.toList());
|
||||
}
|
||||
|
||||
@Override
|
||||
public Integer getDeviceCountByAgentId(String agentId) {
|
||||
if (StringUtils.isBlank(agentId)) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
// 先从Redis中获取
|
||||
Integer cachedCount = (Integer) redisUtils.get(RedisKeys.getAgentDeviceCountById(agentId));
|
||||
if (cachedCount != null) {
|
||||
return cachedCount;
|
||||
}
|
||||
|
||||
// 如果Redis中没有,则从数据库查询
|
||||
Integer deviceCount = agentDao.getDeviceCountByAgentId(agentId);
|
||||
|
||||
// 将结果存入Redis
|
||||
if (deviceCount != null) {
|
||||
redisUtils.set(RedisKeys.getAgentDeviceCountById(agentId), deviceCount, 60);
|
||||
}
|
||||
|
||||
return deviceCount != null ? deviceCount : 0;
|
||||
}
|
||||
}
|
||||
+13
-1
@@ -2,6 +2,7 @@ package xiaozhi.modules.agent.service.impl;
|
||||
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
|
||||
|
||||
import xiaozhi.modules.agent.dao.AgentTemplateDao;
|
||||
@@ -15,6 +16,17 @@ import xiaozhi.modules.agent.service.AgentTemplateService;
|
||||
*/
|
||||
@Service
|
||||
public class AgentTemplateServiceImpl extends ServiceImpl<AgentTemplateDao, AgentTemplateEntity>
|
||||
implements AgentTemplateService {
|
||||
implements AgentTemplateService {
|
||||
|
||||
/**
|
||||
* 获取默认模板
|
||||
*
|
||||
* @return 默认模板实体
|
||||
*/
|
||||
public AgentTemplateEntity getDefaultTemplate() {
|
||||
LambdaQueryWrapper<AgentTemplateEntity> wrapper = new LambdaQueryWrapper<>();
|
||||
wrapper.orderByAsc(AgentTemplateEntity::getSort)
|
||||
.last("LIMIT 1");
|
||||
return this.getOne(wrapper);
|
||||
}
|
||||
}
|
||||
|
||||
+1
-8
@@ -19,7 +19,6 @@ import xiaozhi.common.redis.RedisKeys;
|
||||
import xiaozhi.common.redis.RedisUtils;
|
||||
import xiaozhi.common.user.UserDetail;
|
||||
import xiaozhi.common.utils.Result;
|
||||
import xiaozhi.modules.device.dto.DeviceBindDTO;
|
||||
import xiaozhi.modules.device.dto.DeviceRegisterDTO;
|
||||
import xiaozhi.modules.device.dto.DeviceUnBindDTO;
|
||||
import xiaozhi.modules.device.entity.DeviceEntity;
|
||||
@@ -39,13 +38,7 @@ public class DeviceController {
|
||||
@Operation(summary = "绑定设备")
|
||||
@RequiresPermissions("sys:role:normal")
|
||||
public Result<Void> bindDevice(@PathVariable String agentId, @PathVariable String deviceCode) {
|
||||
String macAddress = (String) redisUtils.get(RedisKeys.getDeviceCaptchaKey(deviceCode));
|
||||
if (StringUtils.isBlank(macAddress)) {
|
||||
return new Result<Void>().error(ErrorCode.DEVICE_CAPTCHA_ERROR);
|
||||
}
|
||||
Long user = SecurityUser.getUser().getId();
|
||||
DeviceBindDTO deviceBindDTO = new DeviceBindDTO(macAddress, user, agentId);
|
||||
deviceService.bindDevice(deviceBindDTO);
|
||||
deviceService.deviceActivation(agentId, deviceCode);
|
||||
return new Result<>();
|
||||
}
|
||||
|
||||
|
||||
@@ -10,7 +10,6 @@ import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.RequestBody;
|
||||
import org.springframework.web.bind.annotation.RequestHeader;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RequestParam;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonInclude;
|
||||
@@ -22,7 +21,6 @@ import io.swagger.v3.oas.annotations.enums.ParameterIn;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.SneakyThrows;
|
||||
import xiaozhi.common.utils.Result;
|
||||
import xiaozhi.modules.device.dto.DeviceReportReqDTO;
|
||||
import xiaozhi.modules.device.dto.DeviceReportRespDTO;
|
||||
import xiaozhi.modules.device.service.DeviceService;
|
||||
@@ -31,7 +29,7 @@ import xiaozhi.modules.device.utils.NetworkUtil;
|
||||
@Tag(name = "设备管理", description = "OTA 相关接口")
|
||||
@RestController
|
||||
@RequiredArgsConstructor
|
||||
@RequestMapping("/ota")
|
||||
@RequestMapping("/ota/")
|
||||
public class OTAController {
|
||||
private final DeviceService deviceService;
|
||||
|
||||
@@ -55,10 +53,10 @@ public class OTAController {
|
||||
return createResponse(deviceService.checkDeviceActive(macAddress, deviceId, clientId, deviceReportReqDTO));
|
||||
}
|
||||
|
||||
@Operation(summary = "设备激活")
|
||||
@GetMapping("/activation")
|
||||
public Result<Boolean> deviceActivation(@RequestParam String code) {
|
||||
return new Result<Boolean>().ok(deviceService.deviceActivation(code));
|
||||
@Operation(summary = "获取 OTA 提示信息")
|
||||
@GetMapping
|
||||
public ResponseEntity<String> getOTAPrompt() {
|
||||
return createResponse(DeviceReportRespDTO.createError("请提交正确的ota参数"));
|
||||
}
|
||||
|
||||
@SneakyThrows
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
package xiaozhi.modules.device.dto;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import jakarta.validation.constraints.Min;
|
||||
import lombok.Data;
|
||||
|
||||
/**
|
||||
* 查询所有设备的DTO
|
||||
*
|
||||
* @author zjy
|
||||
* @since 2025-3-21
|
||||
*/
|
||||
@Data
|
||||
@Schema(description = "查询所有设备的DTO")
|
||||
public class DevicePageUserDTO {
|
||||
|
||||
@Schema(description = "设备关键词")
|
||||
private String keywords;
|
||||
|
||||
@Schema(description = "页数")
|
||||
@Min(value = 0, message = "{page.number}")
|
||||
private String page;
|
||||
|
||||
@Schema(description = "显示列数")
|
||||
@Min(value = 0, message = "{limit.number}")
|
||||
private String limit;
|
||||
}
|
||||
@@ -9,7 +9,7 @@ import lombok.Setter;
|
||||
@Schema(description = "设备OTA检测版本返回体,包含激活码要求")
|
||||
public class DeviceReportRespDTO {
|
||||
@Schema(description = "服务器时间")
|
||||
private ServerTime serverTime;
|
||||
private ServerTime server_time;
|
||||
|
||||
@Schema(description = "激活码")
|
||||
private Activation activation;
|
||||
@@ -56,6 +56,6 @@ public class DeviceReportRespDTO {
|
||||
private String timeZone;
|
||||
|
||||
@Schema(description = "时区偏移量,单位为分钟")
|
||||
private Integer timezoneOffset;
|
||||
private Integer timezone_offset;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,10 +2,12 @@ package xiaozhi.modules.device.service;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import xiaozhi.modules.device.dto.DeviceBindDTO;
|
||||
import xiaozhi.common.page.PageData;
|
||||
import xiaozhi.modules.device.dto.DevicePageUserDTO;
|
||||
import xiaozhi.modules.device.dto.DeviceReportReqDTO;
|
||||
import xiaozhi.modules.device.dto.DeviceReportRespDTO;
|
||||
import xiaozhi.modules.device.entity.DeviceEntity;
|
||||
import xiaozhi.modules.device.vo.UserShowDeviceListVO;
|
||||
|
||||
public interface DeviceService {
|
||||
|
||||
@@ -21,12 +23,7 @@ public interface DeviceService {
|
||||
DeviceReportReqDTO deviceReport);
|
||||
|
||||
/**
|
||||
* 绑定设备
|
||||
*/
|
||||
DeviceEntity bindDevice(DeviceBindDTO deviceHeader);
|
||||
|
||||
/**
|
||||
* 获取用户设备列表
|
||||
* 获取用户指定智能体的设备列表,
|
||||
*/
|
||||
List<DeviceEntity> getUserDevices(Long userId, String agentId);
|
||||
|
||||
@@ -38,5 +35,35 @@ public interface DeviceService {
|
||||
/**
|
||||
* 设备激活
|
||||
*/
|
||||
Boolean deviceActivation(String activationCode);
|
||||
Boolean deviceActivation(String agentId, String activationCode);
|
||||
|
||||
/**
|
||||
* 删除此用户的所有设备
|
||||
*
|
||||
* @param userId 用户id
|
||||
*/
|
||||
void deleteByUserId(Long userId);
|
||||
|
||||
/**
|
||||
* 删除指定智能体关联的所有设备
|
||||
*
|
||||
* @param agentId 智能体id
|
||||
*/
|
||||
void deleteByAgentId(String agentId);
|
||||
|
||||
/**
|
||||
* 获取指定用户的设备数量
|
||||
*
|
||||
* @param userId 用户id
|
||||
* @return 设备数量
|
||||
*/
|
||||
Long selectCountByUserId(Long userId);
|
||||
|
||||
/**
|
||||
* 分页获取全部设备信息
|
||||
*
|
||||
* @param dto 分页查找参数
|
||||
* @return 用户列表分页数据
|
||||
*/
|
||||
PageData<UserShowDeviceListVO> page(DevicePageUserDTO dto);
|
||||
}
|
||||
+62
-19
@@ -16,34 +16,43 @@ import org.springframework.stereotype.Service;
|
||||
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
|
||||
import com.baomidou.mybatisplus.core.conditions.update.UpdateWrapper;
|
||||
import com.baomidou.mybatisplus.core.metadata.IPage;
|
||||
|
||||
import cn.hutool.core.util.RandomUtil;
|
||||
import xiaozhi.common.constant.Constant;
|
||||
import xiaozhi.common.exception.RenException;
|
||||
import xiaozhi.common.page.PageData;
|
||||
import xiaozhi.common.service.impl.BaseServiceImpl;
|
||||
import xiaozhi.common.user.UserDetail;
|
||||
import xiaozhi.common.utils.ConvertUtils;
|
||||
import xiaozhi.common.utils.DateUtils;
|
||||
import xiaozhi.modules.device.dao.DeviceDao;
|
||||
import xiaozhi.modules.device.dto.DeviceBindDTO;
|
||||
import xiaozhi.modules.device.dto.DevicePageUserDTO;
|
||||
import xiaozhi.modules.device.dto.DeviceReportReqDTO;
|
||||
import xiaozhi.modules.device.dto.DeviceReportRespDTO;
|
||||
import xiaozhi.modules.device.entity.DeviceEntity;
|
||||
import xiaozhi.modules.device.service.DeviceService;
|
||||
import xiaozhi.modules.device.vo.UserShowDeviceListVO;
|
||||
import xiaozhi.modules.security.user.SecurityUser;
|
||||
import xiaozhi.modules.sys.service.SysUserUtilService;
|
||||
|
||||
@Service
|
||||
public class DeviceServiceImpl extends BaseServiceImpl<DeviceDao, DeviceEntity> implements DeviceService {
|
||||
|
||||
private final DeviceDao deviceDao;
|
||||
|
||||
private final SysUserUtilService sysUserUtilService;
|
||||
|
||||
private final String frontedUrl;
|
||||
|
||||
private final RedisTemplate<String, Object> redisTemplate;
|
||||
|
||||
// 添加构造函数来初始化 deviceMapper
|
||||
public DeviceServiceImpl(DeviceDao deviceDao,
|
||||
public DeviceServiceImpl(DeviceDao deviceDao, SysUserUtilService sysUserUtilService,
|
||||
@Value("${app.fronted-url:http://localhost:8001}") String frontedUrl,
|
||||
RedisTemplate<String, Object> redisTemplate) {
|
||||
this.deviceDao = deviceDao;
|
||||
this.sysUserUtilService = sysUserUtilService;
|
||||
this.frontedUrl = frontedUrl;
|
||||
this.redisTemplate = redisTemplate;
|
||||
}
|
||||
@@ -56,7 +65,7 @@ public class DeviceServiceImpl extends BaseServiceImpl<DeviceDao, DeviceEntity>
|
||||
}
|
||||
|
||||
@Override
|
||||
public Boolean deviceActivation(String activationCode) {
|
||||
public Boolean deviceActivation(String agentId, String activationCode) {
|
||||
if (StringUtils.isBlank(activationCode)) {
|
||||
throw new RenException("激活码不能为空");
|
||||
}
|
||||
@@ -93,6 +102,7 @@ public class DeviceServiceImpl extends BaseServiceImpl<DeviceDao, DeviceEntity>
|
||||
DeviceEntity deviceEntity = new DeviceEntity();
|
||||
deviceEntity.setId(deviceId);
|
||||
deviceEntity.setBoard(board);
|
||||
deviceEntity.setAgentId(agentId);
|
||||
deviceEntity.setAppVersion(appVersion);
|
||||
deviceEntity.setMacAddress(macAddress);
|
||||
deviceEntity.setUserId(user.getId());
|
||||
@@ -113,13 +123,13 @@ public class DeviceServiceImpl extends BaseServiceImpl<DeviceDao, DeviceEntity>
|
||||
public DeviceReportRespDTO checkDeviceActive(String macAddress, String deviceId, String clientId,
|
||||
DeviceReportReqDTO deviceReport) {
|
||||
DeviceReportRespDTO response = new DeviceReportRespDTO();
|
||||
response.setServerTime(buildServerTime());
|
||||
response.setServer_time(buildServerTime());
|
||||
// todo: 此处是固件信息,目前是针对固件上传上来的版本号再返回回去
|
||||
// 在未来开发了固件更新功能,需要更换此处代码,
|
||||
// 或写定时任务定期请求虾哥的OTA,获取最新的版本讯息保存到服务内
|
||||
DeviceReportRespDTO.Firmware firmware = new DeviceReportRespDTO.Firmware();
|
||||
firmware.setVersion(deviceReport.getApplication().getVersion());
|
||||
firmware.setUrl("http://localhost:8002/xiaozhi-esp32-api/api/v1/ota/download");
|
||||
firmware.setUrl("http://localhost:8002/xiaozhi/ota/download");
|
||||
response.setFirmware(firmware);
|
||||
|
||||
DeviceEntity deviceById = getDeviceById(deviceId);
|
||||
@@ -168,19 +178,6 @@ public class DeviceServiceImpl extends BaseServiceImpl<DeviceDao, DeviceEntity>
|
||||
return response;
|
||||
}
|
||||
|
||||
@Override
|
||||
public DeviceEntity bindDevice(DeviceBindDTO dto) {
|
||||
// 查看是否已经被绑定
|
||||
DeviceEntity deviceEntity = baseDao
|
||||
.selectOne(new LambdaQueryWrapper<DeviceEntity>().eq(DeviceEntity::getMacAddress, dto.getMacAddress()));
|
||||
if (deviceEntity != null) {
|
||||
throw new RenException("设备已绑定");
|
||||
}
|
||||
deviceEntity = ConvertUtils.sourceToTarget(dto, DeviceEntity.class);
|
||||
baseDao.insert(deviceEntity);
|
||||
return deviceEntity;
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<DeviceEntity> getUserDevices(Long userId, String agentId) {
|
||||
QueryWrapper<DeviceEntity> wrapper = new QueryWrapper<>();
|
||||
@@ -197,12 +194,58 @@ public class DeviceServiceImpl extends BaseServiceImpl<DeviceDao, DeviceEntity>
|
||||
baseDao.delete(wrapper);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void deleteByUserId(Long userId) {
|
||||
UpdateWrapper<DeviceEntity> wrapper = new UpdateWrapper<>();
|
||||
wrapper.eq("user_id", userId);
|
||||
baseDao.delete(wrapper);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Long selectCountByUserId(Long userId) {
|
||||
UpdateWrapper<DeviceEntity> wrapper = new UpdateWrapper<>();
|
||||
wrapper.eq("user_id", userId);
|
||||
return baseDao.selectCount(wrapper);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void deleteByAgentId(String agentId) {
|
||||
UpdateWrapper<DeviceEntity> wrapper = new UpdateWrapper<>();
|
||||
wrapper.eq("agent_id", agentId);
|
||||
baseDao.delete(wrapper);
|
||||
}
|
||||
|
||||
@Override
|
||||
public PageData<UserShowDeviceListVO> page(DevicePageUserDTO dto) {
|
||||
Map<String, Object> params = new HashMap<String, Object>();
|
||||
params.put(Constant.PAGE, dto.getPage());
|
||||
params.put(Constant.LIMIT, dto.getLimit());
|
||||
IPage<DeviceEntity> page = baseDao.selectPage(
|
||||
getPage(params, "sort", true),
|
||||
// 定义查询条件
|
||||
new QueryWrapper<DeviceEntity>()
|
||||
// 必须设备关键词查找
|
||||
.like(StringUtils.isNotBlank(dto.getKeywords()), "alias", dto.getKeywords()));
|
||||
// 循环处理page获取回来的数据,返回需要的字段
|
||||
List<UserShowDeviceListVO> list = page.getRecords().stream().map(device -> {
|
||||
UserShowDeviceListVO vo = ConvertUtils.sourceToTarget(device, UserShowDeviceListVO.class);
|
||||
// 把最后修改的时间,改为简短描述的时间
|
||||
vo.setRecentChatTime(DateUtils.getShortTime(device.getUpdateDate()));
|
||||
sysUserUtilService.assignUsername(device.getUserId(),
|
||||
vo::setBindUserName);
|
||||
vo.setDeviceType(device.getBoard());
|
||||
return vo;
|
||||
}).toList();
|
||||
// 计算页数
|
||||
return new PageData<>(list, page.getTotal());
|
||||
}
|
||||
|
||||
private DeviceReportRespDTO.ServerTime buildServerTime() {
|
||||
DeviceReportRespDTO.ServerTime serverTime = new DeviceReportRespDTO.ServerTime();
|
||||
TimeZone tz = TimeZone.getDefault();
|
||||
serverTime.setTimestamp(Instant.now().toEpochMilli());
|
||||
serverTime.setTimeZone(tz.getID());
|
||||
serverTime.setTimezoneOffset(tz.getOffset(System.currentTimeMillis()) / (60 * 1000));
|
||||
serverTime.setTimezone_offset(tz.getOffset(System.currentTimeMillis()) / (60 * 1000));
|
||||
return serverTime;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
package xiaozhi.modules.device.vo;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.Data;
|
||||
|
||||
@Data
|
||||
@Schema(description = "用户显示设备列表VO")
|
||||
public class UserShowDeviceListVO {
|
||||
|
||||
@Schema(description = "app版本")
|
||||
private String appVersion;
|
||||
|
||||
@Schema(description = "绑定用户名称")
|
||||
private String bindUserName;
|
||||
|
||||
@Schema(description = "设备型号")
|
||||
private String deviceType;
|
||||
|
||||
@Schema(description = "设备唯一标识符")
|
||||
private String id;
|
||||
|
||||
@Schema(description = "mac地址")
|
||||
private String macAddress;
|
||||
|
||||
@Schema(description = "开启OTA")
|
||||
private Integer otaUpgrade;
|
||||
|
||||
@Schema(description = "最近对话时间")
|
||||
private String recentChatTime;
|
||||
|
||||
}
|
||||
+43
-28
@@ -17,13 +17,17 @@ import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import lombok.AllArgsConstructor;
|
||||
import xiaozhi.common.page.PageData;
|
||||
import xiaozhi.common.utils.ConvertUtils;
|
||||
import xiaozhi.common.utils.Result;
|
||||
import xiaozhi.modules.model.dto.ModelBasicInfoDTO;
|
||||
import xiaozhi.modules.model.dto.ModelConfigBodyDTO;
|
||||
import xiaozhi.modules.model.dto.ModelConfigDTO;
|
||||
import xiaozhi.modules.model.dto.ModelProviderDTO;
|
||||
import xiaozhi.modules.model.dto.ModelProviderFieldDTO;
|
||||
import xiaozhi.modules.model.dto.VoiceDTO;
|
||||
import xiaozhi.modules.model.entity.ModelConfigEntity;
|
||||
import xiaozhi.modules.model.service.ModelConfigService;
|
||||
import xiaozhi.modules.model.service.ModelProviderService;
|
||||
import xiaozhi.modules.timbre.service.TimbreService;
|
||||
|
||||
@AllArgsConstructor
|
||||
@RestController
|
||||
@@ -32,16 +36,16 @@ import xiaozhi.modules.model.service.ModelProviderService;
|
||||
public class ModelController {
|
||||
|
||||
private final ModelProviderService modelProviderService;
|
||||
|
||||
private final TimbreService timbreService;
|
||||
private final ModelConfigService modelConfigService;
|
||||
|
||||
@GetMapping("/models/names")
|
||||
@GetMapping("/names")
|
||||
@Operation(summary = "获取所有模型名称")
|
||||
@RequiresPermissions("sys:role:superAdmin")
|
||||
public Result<List<String>> getModelNames(@RequestParam String modelType,
|
||||
@RequiresPermissions("sys:role:normal")
|
||||
public Result<List<ModelBasicInfoDTO>> getModelNames(@RequestParam String modelType,
|
||||
@RequestParam(required = false) String modelName) {
|
||||
List<String> modelNameList = modelConfigService.getModelCodeList(modelType, modelName);
|
||||
return new Result<List<String>>().ok(modelNameList);
|
||||
List<ModelBasicInfoDTO> modelList = modelConfigService.getModelCodeList(modelType, modelName);
|
||||
return new Result<List<ModelBasicInfoDTO>>().ok(modelList);
|
||||
}
|
||||
|
||||
@GetMapping("/{modelType}/provideTypes")
|
||||
@@ -52,16 +56,7 @@ public class ModelController {
|
||||
return new Result<List<ModelProviderDTO>>().ok(modelProviderDTOS);
|
||||
}
|
||||
|
||||
@GetMapping("/{modelType}/{provideCode}/fields")
|
||||
@Operation(summary = "获取模型供应器字段")
|
||||
@RequiresPermissions("sys:role:superAdmin")
|
||||
public Result<List<ModelProviderFieldDTO>> getModelProviderFields(@PathVariable String modelType,
|
||||
@PathVariable String provideCode) {
|
||||
List<ModelProviderFieldDTO> fieldList = modelProviderService.getFieldList(modelType, provideCode);
|
||||
return new Result<List<ModelProviderFieldDTO>>().ok(fieldList);
|
||||
}
|
||||
|
||||
@GetMapping("/models/list")
|
||||
@GetMapping("/list")
|
||||
@Operation(summary = "获取模型配置列表")
|
||||
@RequiresPermissions("sys:role:superAdmin")
|
||||
public Result<PageData<ModelConfigDTO>> getModelConfigList(
|
||||
@@ -73,7 +68,7 @@ public class ModelController {
|
||||
return new Result<PageData<ModelConfigDTO>>().ok(pageList);
|
||||
}
|
||||
|
||||
@PostMapping("/models/{modelType}/{provideCode}")
|
||||
@PostMapping("/{modelType}/{provideCode}")
|
||||
@Operation(summary = "新增模型配置")
|
||||
@RequiresPermissions("sys:role:superAdmin")
|
||||
public Result<ModelConfigDTO> addModelConfig(@PathVariable String modelType,
|
||||
@@ -83,7 +78,7 @@ public class ModelController {
|
||||
return new Result<ModelConfigDTO>().ok(modelConfigDTO);
|
||||
}
|
||||
|
||||
@PutMapping("/models/{modelType}/{provideCode}/{id}")
|
||||
@PutMapping("/{modelType}/{provideCode}/{id}")
|
||||
@Operation(summary = "编辑模型配置")
|
||||
@RequiresPermissions("sys:role:superAdmin")
|
||||
public Result<ModelConfigDTO> editModelConfig(@PathVariable String modelType,
|
||||
@@ -94,22 +89,42 @@ public class ModelController {
|
||||
return new Result<ModelConfigDTO>().ok(modelConfigDTO);
|
||||
}
|
||||
|
||||
@DeleteMapping("/models/{modelType}/{provideCode}/{id}")
|
||||
@DeleteMapping("/{id}")
|
||||
@Operation(summary = "删除模型配置")
|
||||
@RequiresPermissions("sys:role:superAdmin")
|
||||
public Result<Void> deleteModelConfig(@PathVariable String modelType, @PathVariable String provideCode,
|
||||
@PathVariable String id) {
|
||||
modelConfigService.delete(modelType, provideCode, id);
|
||||
public Result<Void> deleteModelConfig(@PathVariable String id) {
|
||||
modelConfigService.delete(id);
|
||||
return new Result<>();
|
||||
}
|
||||
|
||||
@GetMapping("/models/{modelId}/voices")
|
||||
@GetMapping("/{id}")
|
||||
@Operation(summary = "获取模型配置")
|
||||
@RequiresPermissions("sys:role:superAdmin")
|
||||
public Result<ModelConfigDTO> getModelConfig(@PathVariable String id) {
|
||||
ModelConfigEntity item = modelConfigService.selectById(id);
|
||||
ModelConfigDTO modelConfigDTO = ConvertUtils.sourceToTarget(item, ModelConfigDTO.class);
|
||||
return new Result<ModelConfigDTO>().ok(modelConfigDTO);
|
||||
}
|
||||
|
||||
@PutMapping("/enable/{id}/{status}")
|
||||
@Operation(summary = "启用/关闭模型配置")
|
||||
@RequiresPermissions("sys:role:superAdmin")
|
||||
public Result<Void> enableModelConfig(@PathVariable String id, @PathVariable Integer status) {
|
||||
ModelConfigEntity entity = modelConfigService.selectById(id);
|
||||
if (entity == null) {
|
||||
return new Result<Void>().error("模型配置不存在");
|
||||
}
|
||||
entity.setIsEnabled(status);
|
||||
modelConfigService.updateById(entity);
|
||||
return new Result<Void>();
|
||||
}
|
||||
|
||||
@GetMapping("/{modelId}/voices")
|
||||
@Operation(summary = "获取模型音色")
|
||||
@RequiresPermissions("sys:role:normal")
|
||||
public Result<List<String>> getVoiceList(@PathVariable String modelId,
|
||||
public Result<List<VoiceDTO>> getVoiceList(@PathVariable String modelId,
|
||||
@RequestParam(required = false) String voiceName) {
|
||||
|
||||
List<String> voiceList = modelConfigService.getVoiceList(modelId, voiceName);
|
||||
return new Result<List<String>>().ok(voiceList);
|
||||
List<VoiceDTO> voiceList = timbreService.getVoiceNames(modelId, voiceName);
|
||||
return new Result<List<VoiceDTO>>().ok(voiceList);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
package xiaozhi.modules.model.dto;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
@Data
|
||||
public class ModelBasicInfoDTO {
|
||||
private String id;
|
||||
private String modelName;
|
||||
}
|
||||
@@ -2,6 +2,7 @@ package xiaozhi.modules.model.dto;
|
||||
|
||||
import java.io.Serial;
|
||||
|
||||
import cn.hutool.json.JSONObject;
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.Data;
|
||||
|
||||
@@ -28,7 +29,7 @@ public class ModelConfigBodyDTO {
|
||||
private Integer isEnabled;
|
||||
|
||||
@Schema(description = "模型配置(JSON格式)")
|
||||
private String configJson;
|
||||
private JSONObject configJson;
|
||||
|
||||
@Schema(description = "官方文档链接")
|
||||
private String docLink;
|
||||
|
||||
@@ -3,6 +3,7 @@ package xiaozhi.modules.model.dto;
|
||||
import java.io.Serial;
|
||||
import java.io.Serializable;
|
||||
|
||||
import cn.hutool.json.JSONObject;
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.Data;
|
||||
|
||||
@@ -32,7 +33,7 @@ public class ModelConfigDTO implements Serializable {
|
||||
private Integer isEnabled;
|
||||
|
||||
@Schema(description = "模型配置(JSON格式)")
|
||||
private String configJson;
|
||||
private JSONObject configJson;
|
||||
|
||||
@Schema(description = "官方文档链接")
|
||||
private String docLink;
|
||||
|
||||
@@ -1,19 +0,0 @@
|
||||
package xiaozhi.modules.model.dto;
|
||||
|
||||
import java.io.Serializable;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.Data;
|
||||
|
||||
@Data
|
||||
@Schema(description = "模型供应器字段")
|
||||
public class ModelProviderFieldDTO implements Serializable {
|
||||
@Schema(description = "字段名")
|
||||
private String key;
|
||||
|
||||
@Schema(description = "字段标签")
|
||||
private String label;
|
||||
|
||||
@Schema(description = "字段类型")
|
||||
private String type;
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
package xiaozhi.modules.model.dto;
|
||||
|
||||
import java.io.Serializable;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
@Data
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
@Schema(description = "音色信息")
|
||||
public class VoiceDTO implements Serializable {
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
@Schema(description = "音色ID")
|
||||
private String id;
|
||||
|
||||
@Schema(description = "音色名称")
|
||||
private String name;
|
||||
}
|
||||
@@ -9,11 +9,12 @@ import com.baomidou.mybatisplus.annotation.TableId;
|
||||
import com.baomidou.mybatisplus.annotation.TableName;
|
||||
import com.baomidou.mybatisplus.extension.handlers.JacksonTypeHandler;
|
||||
|
||||
import cn.hutool.json.JSONObject;
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.Data;
|
||||
|
||||
@Data
|
||||
@TableName("ai_model_config")
|
||||
@TableName(value = "ai_model_config", autoResultMap = true)
|
||||
@Schema(description = "模型配置表")
|
||||
public class ModelConfigEntity {
|
||||
|
||||
@@ -38,7 +39,7 @@ public class ModelConfigEntity {
|
||||
|
||||
@TableField(typeHandler = JacksonTypeHandler.class)
|
||||
@Schema(description = "模型配置(JSON格式)")
|
||||
private String configJson;
|
||||
private JSONObject configJson;
|
||||
|
||||
@Schema(description = "官方文档链接")
|
||||
private String docLink;
|
||||
|
||||
+13
-4
@@ -3,12 +3,15 @@ package xiaozhi.modules.model.service;
|
||||
import java.util.List;
|
||||
|
||||
import xiaozhi.common.page.PageData;
|
||||
import xiaozhi.common.service.BaseService;
|
||||
import xiaozhi.modules.model.dto.ModelBasicInfoDTO;
|
||||
import xiaozhi.modules.model.dto.ModelConfigBodyDTO;
|
||||
import xiaozhi.modules.model.dto.ModelConfigDTO;
|
||||
import xiaozhi.modules.model.entity.ModelConfigEntity;
|
||||
|
||||
public interface ModelConfigService {
|
||||
public interface ModelConfigService extends BaseService<ModelConfigEntity> {
|
||||
|
||||
List<String> getModelCodeList(String modelType, String modelName);
|
||||
List<ModelBasicInfoDTO> getModelCodeList(String modelType, String modelName);
|
||||
|
||||
PageData<ModelConfigDTO> getPageList(String modelType, String modelName, String page, String limit);
|
||||
|
||||
@@ -16,7 +19,13 @@ public interface ModelConfigService {
|
||||
|
||||
ModelConfigDTO edit(String modelType, String provideCode, String id, ModelConfigBodyDTO modelConfigBodyDTO);
|
||||
|
||||
void delete(String modelType, String provideCode, String id);
|
||||
void delete(String id);
|
||||
|
||||
List<String> getVoiceList(String modelName, String voiceName);
|
||||
/**
|
||||
* 根据ID获取模型名称
|
||||
*
|
||||
* @param id 模型ID
|
||||
* @return 模型名称
|
||||
*/
|
||||
String getModelNameById(String id);
|
||||
}
|
||||
|
||||
@@ -3,7 +3,6 @@ package xiaozhi.modules.model.service;
|
||||
import java.util.List;
|
||||
|
||||
import xiaozhi.modules.model.dto.ModelProviderDTO;
|
||||
import xiaozhi.modules.model.dto.ModelProviderFieldDTO;
|
||||
import xiaozhi.modules.model.entity.ModelProviderEntity;
|
||||
|
||||
public interface ModelProviderService {
|
||||
@@ -19,6 +18,4 @@ public interface ModelProviderService {
|
||||
void delete();
|
||||
|
||||
List<ModelProviderDTO> getList(String modelType, String provideCode);
|
||||
|
||||
List<ModelProviderFieldDTO> getFieldList(String modelType, String provideCode);
|
||||
}
|
||||
|
||||
+33
-20
@@ -5,8 +5,6 @@ import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
|
||||
@@ -17,16 +15,18 @@ import lombok.AllArgsConstructor;
|
||||
import xiaozhi.common.constant.Constant;
|
||||
import xiaozhi.common.exception.RenException;
|
||||
import xiaozhi.common.page.PageData;
|
||||
import xiaozhi.common.redis.RedisKeys;
|
||||
import xiaozhi.common.redis.RedisUtils;
|
||||
import xiaozhi.common.service.impl.BaseServiceImpl;
|
||||
import xiaozhi.common.utils.ConvertUtils;
|
||||
import xiaozhi.modules.model.dao.ModelConfigDao;
|
||||
import xiaozhi.modules.model.dto.ModelBasicInfoDTO;
|
||||
import xiaozhi.modules.model.dto.ModelConfigBodyDTO;
|
||||
import xiaozhi.modules.model.dto.ModelConfigDTO;
|
||||
import xiaozhi.modules.model.dto.ModelProviderDTO;
|
||||
import xiaozhi.modules.model.entity.ModelConfigEntity;
|
||||
import xiaozhi.modules.model.service.ModelConfigService;
|
||||
import xiaozhi.modules.model.service.ModelProviderService;
|
||||
import xiaozhi.modules.timbre.service.TimbreService;
|
||||
|
||||
@Service
|
||||
@AllArgsConstructor
|
||||
@@ -35,13 +35,16 @@ public class ModelConfigServiceImpl extends BaseServiceImpl<ModelConfigDao, Mode
|
||||
|
||||
private final ModelConfigDao modelConfigDao;
|
||||
private final ModelProviderService modelProviderService;
|
||||
private final TimbreService timbreService;
|
||||
|
||||
private static final Logger logger = LoggerFactory.getLogger(ModelConfigServiceImpl.class);
|
||||
private final RedisUtils redisUtils;
|
||||
|
||||
@Override
|
||||
public List<String> getModelCodeList(String modelType, String modelName) {
|
||||
return modelConfigDao.getModelCodeList(modelType, modelName);
|
||||
public List<ModelBasicInfoDTO> getModelCodeList(String modelType, String modelName) {
|
||||
List<ModelConfigEntity> entities = modelConfigDao.selectList(
|
||||
new QueryWrapper<ModelConfigEntity>()
|
||||
.eq("model_type", modelType)
|
||||
.like(StringUtils.isNotBlank(modelName), "model_name", "%" + modelName + "%")
|
||||
.select("id", "model_name"));
|
||||
return ConvertUtils.sourceToTarget(entities, ModelBasicInfoDTO.class);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -95,21 +98,31 @@ public class ModelConfigServiceImpl extends BaseServiceImpl<ModelConfigDao, Mode
|
||||
}
|
||||
|
||||
@Override
|
||||
public void delete(String modelType, String provideCode, String id) {
|
||||
// 先验证有没有供应器
|
||||
if (StringUtils.isBlank(modelType) || StringUtils.isBlank(provideCode)) {
|
||||
throw new RenException("modelType和provideCode不能为空");
|
||||
}
|
||||
List<ModelProviderDTO> providerList = modelProviderService.getList(modelType, provideCode);
|
||||
if (CollectionUtil.isEmpty(providerList)) {
|
||||
throw new RenException("供应器不存在");
|
||||
}
|
||||
|
||||
public void delete(String id) {
|
||||
modelConfigDao.deleteById(id);
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<String> getVoiceList(String modelId, String voiceName) {
|
||||
return timbreService.getVoiceNames(modelId, voiceName);
|
||||
public String getModelNameById(String id) {
|
||||
if (StringUtils.isBlank(id)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
String cachedName = (String) redisUtils.get(RedisKeys.getModelNameById(id));
|
||||
|
||||
if (StringUtils.isNotBlank(cachedName)) {
|
||||
return cachedName;
|
||||
}
|
||||
|
||||
ModelConfigEntity entity = modelConfigDao.selectById(id);
|
||||
if (entity != null) {
|
||||
String modelName = entity.getModelName();
|
||||
if (StringUtils.isNotBlank(modelName)) {
|
||||
redisUtils.set(RedisKeys.getModelNameById(id), modelName);
|
||||
}
|
||||
return modelName;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
-13
@@ -10,10 +10,8 @@ import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
|
||||
import lombok.AllArgsConstructor;
|
||||
import xiaozhi.common.service.impl.BaseServiceImpl;
|
||||
import xiaozhi.common.utils.ConvertUtils;
|
||||
import xiaozhi.common.utils.JsonUtils;
|
||||
import xiaozhi.modules.model.dao.ModelProviderDao;
|
||||
import xiaozhi.modules.model.dto.ModelProviderDTO;
|
||||
import xiaozhi.modules.model.dto.ModelProviderFieldDTO;
|
||||
import xiaozhi.modules.model.entity.ModelProviderEntity;
|
||||
import xiaozhi.modules.model.service.ModelProviderService;
|
||||
|
||||
@@ -56,15 +54,4 @@ public class ModelProviderServiceImpl extends BaseServiceImpl<ModelProviderDao,
|
||||
List<ModelProviderEntity> providerEntities = modelProviderDao.selectList(queryWrapper);
|
||||
return ConvertUtils.sourceToTarget(providerEntities, ModelProviderDTO.class);
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<ModelProviderFieldDTO> getFieldList(String modelType, String providerCode) {
|
||||
List<String> modelProviderEntities = modelProviderDao.getFieldList(modelType, providerCode);
|
||||
if (modelProviderEntities == null || modelProviderEntities.isEmpty()) {
|
||||
return null;
|
||||
}
|
||||
String fields = modelProviderEntities.getFirst();
|
||||
List<ModelProviderFieldDTO> fieldList = JsonUtils.parseArray(fields, ModelProviderFieldDTO.class);
|
||||
return fieldList;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@ import org.springframework.web.bind.annotation.DeleteMapping;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.PathVariable;
|
||||
import org.springframework.web.bind.annotation.PutMapping;
|
||||
import org.springframework.web.bind.annotation.RequestBody;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RequestParam;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
@@ -20,6 +21,9 @@ import xiaozhi.common.constant.Constant;
|
||||
import xiaozhi.common.page.PageData;
|
||||
import xiaozhi.common.utils.Result;
|
||||
import xiaozhi.common.validator.ValidatorUtils;
|
||||
import xiaozhi.modules.device.dto.DevicePageUserDTO;
|
||||
import xiaozhi.modules.device.service.DeviceService;
|
||||
import xiaozhi.modules.device.vo.UserShowDeviceListVO;
|
||||
import xiaozhi.modules.sys.dto.AdminPageUserDTO;
|
||||
import xiaozhi.modules.sys.service.SysUserService;
|
||||
import xiaozhi.modules.sys.vo.AdminPageUserVO;
|
||||
@@ -37,6 +41,8 @@ import xiaozhi.modules.sys.vo.AdminPageUserVO;
|
||||
public class AdminController {
|
||||
private final SysUserService sysUserService;
|
||||
|
||||
private final DeviceService deviceService;
|
||||
|
||||
@GetMapping("/users")
|
||||
@Operation(summary = "分页查找用户")
|
||||
@RequiresPermissions("sys:role:superAdmin")
|
||||
@@ -51,7 +57,7 @@ public class AdminController {
|
||||
dto.setMobile((String) params.get("mobile"));
|
||||
dto.setLimit((String) params.get(Constant.LIMIT));
|
||||
dto.setPage((String) params.get(Constant.PAGE));
|
||||
|
||||
ValidatorUtils.validateEntity(dto);
|
||||
ValidatorUtils.validateEntity(dto);
|
||||
PageData<AdminPageUserVO> page = sysUserService.page(dto);
|
||||
return new Result<PageData<AdminPageUserVO>>().ok(page);
|
||||
@@ -70,10 +76,19 @@ public class AdminController {
|
||||
@Operation(summary = "用户删除")
|
||||
@RequiresPermissions("sys:role:superAdmin")
|
||||
public Result<Void> delete(@PathVariable Long id) {
|
||||
sysUserService.delete(new Long[] { id });
|
||||
sysUserService.deleteById(id);
|
||||
return new Result<>();
|
||||
}
|
||||
|
||||
@PutMapping("/users/changeStatus/{status}")
|
||||
@Operation(summary = "批量修改用户状态")
|
||||
@RequiresPermissions("sys:role:superAdmin")
|
||||
@Parameter(name = "status", description = "用户状态", required = true)
|
||||
public Result<Void> changeStatus(@PathVariable Integer status, @RequestBody String[] userIds) {
|
||||
sysUserService.changeStatus(status, userIds);
|
||||
return new Result<Void>();
|
||||
}
|
||||
|
||||
@GetMapping("/device/all")
|
||||
@Operation(summary = "分页查找设备")
|
||||
@RequiresPermissions("sys:role:superAdmin")
|
||||
@@ -82,9 +97,14 @@ public class AdminController {
|
||||
@Parameter(name = Constant.PAGE, description = "当前页码,从1开始", required = true),
|
||||
@Parameter(name = Constant.LIMIT, description = "每页显示记录数", required = true),
|
||||
})
|
||||
public Result<Void> pageDevice(
|
||||
public Result<PageData<UserShowDeviceListVO>> pageDevice(
|
||||
@Parameter(hidden = true) @RequestParam Map<String, Object> params) {
|
||||
// TODO 等设备功能模块写好
|
||||
return new Result<Void>().error(600, "等设备功能模块写好");
|
||||
DevicePageUserDTO dto = new DevicePageUserDTO();
|
||||
dto.setKeywords((String) params.get("keywords"));
|
||||
dto.setLimit((String) params.get(Constant.LIMIT));
|
||||
dto.setPage((String) params.get(Constant.PAGE));
|
||||
ValidatorUtils.validateEntity(dto);
|
||||
PageData<UserShowDeviceListVO> page = deviceService.page(dto);
|
||||
return new Result<PageData<UserShowDeviceListVO>>().ok(page);
|
||||
}
|
||||
}
|
||||
|
||||
+110
@@ -0,0 +1,110 @@
|
||||
package xiaozhi.modules.sys.controller;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
import org.apache.shiro.authz.annotation.RequiresPermissions;
|
||||
import org.springframework.web.bind.annotation.DeleteMapping;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.PathVariable;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.PutMapping;
|
||||
import org.springframework.web.bind.annotation.RequestBody;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RequestParam;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.Parameter;
|
||||
import io.swagger.v3.oas.annotations.Parameters;
|
||||
import io.swagger.v3.oas.annotations.enums.ParameterIn;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import lombok.AllArgsConstructor;
|
||||
import xiaozhi.common.annotation.LogOperation;
|
||||
import xiaozhi.common.constant.Constant;
|
||||
import xiaozhi.common.page.PageData;
|
||||
import xiaozhi.common.utils.Result;
|
||||
import xiaozhi.common.validator.AssertUtils;
|
||||
import xiaozhi.common.validator.ValidatorUtils;
|
||||
import xiaozhi.common.validator.group.AddGroup;
|
||||
import xiaozhi.common.validator.group.DefaultGroup;
|
||||
import xiaozhi.common.validator.group.UpdateGroup;
|
||||
import xiaozhi.modules.sys.dto.SysParamsDTO;
|
||||
import xiaozhi.modules.sys.service.SysParamsService;
|
||||
|
||||
/**
|
||||
* 参数管理
|
||||
*
|
||||
* @author Mark sunlightcs@gmail.com
|
||||
* @since 1.0.0
|
||||
*/
|
||||
@RestController
|
||||
@RequestMapping("admin/params")
|
||||
@Tag(name = "参数管理")
|
||||
@AllArgsConstructor
|
||||
public class SysParamsController {
|
||||
private final SysParamsService sysParamsService;
|
||||
|
||||
@GetMapping("page")
|
||||
@Operation(summary = "分页")
|
||||
@Parameters({
|
||||
@Parameter(name = Constant.PAGE, description = "当前页码,从1开始", in = ParameterIn.QUERY, required = true, ref = "int"),
|
||||
@Parameter(name = Constant.LIMIT, description = "每页显示记录数", in = ParameterIn.QUERY, required = true, ref = "int"),
|
||||
@Parameter(name = Constant.ORDER_FIELD, description = "排序字段", in = ParameterIn.QUERY, ref = "String"),
|
||||
@Parameter(name = Constant.ORDER, description = "排序方式,可选值(asc、desc)", in = ParameterIn.QUERY, ref = "String"),
|
||||
@Parameter(name = "paramCode", description = "参数编码", in = ParameterIn.QUERY, ref = "String")
|
||||
})
|
||||
@RequiresPermissions("sys:role:superAdmin")
|
||||
public Result<PageData<SysParamsDTO>> page(@Parameter(hidden = true) @RequestParam Map<String, Object> params) {
|
||||
PageData<SysParamsDTO> page = sysParamsService.page(params);
|
||||
|
||||
return new Result<PageData<SysParamsDTO>>().ok(page);
|
||||
}
|
||||
|
||||
@GetMapping("{id}")
|
||||
@Operation(summary = "信息")
|
||||
@RequiresPermissions("sys:role:superAdmin")
|
||||
public Result<SysParamsDTO> get(@PathVariable("id") Long id) {
|
||||
SysParamsDTO data = sysParamsService.get(id);
|
||||
|
||||
return new Result<SysParamsDTO>().ok(data);
|
||||
}
|
||||
|
||||
@PostMapping
|
||||
@Operation(summary = "保存")
|
||||
@LogOperation("保存")
|
||||
@RequiresPermissions("sys:role:superAdmin")
|
||||
public Result<Void> save(@RequestBody SysParamsDTO dto) {
|
||||
// 效验数据
|
||||
ValidatorUtils.validateEntity(dto, AddGroup.class, DefaultGroup.class);
|
||||
|
||||
sysParamsService.save(dto);
|
||||
|
||||
return new Result<Void>();
|
||||
}
|
||||
|
||||
@PutMapping
|
||||
@Operation(summary = "修改")
|
||||
@LogOperation("修改")
|
||||
@RequiresPermissions("sys:role:superAdmin")
|
||||
public Result<Void> update(@RequestBody SysParamsDTO dto) {
|
||||
// 效验数据
|
||||
ValidatorUtils.validateEntity(dto, UpdateGroup.class, DefaultGroup.class);
|
||||
|
||||
sysParamsService.update(dto);
|
||||
|
||||
return new Result<Void>();
|
||||
}
|
||||
|
||||
@DeleteMapping
|
||||
@Operation(summary = "删除")
|
||||
@LogOperation("删除")
|
||||
@RequiresPermissions("sys:role:superAdmin")
|
||||
public Result<Void> delete(@RequestBody Long[] ids) {
|
||||
// 效验数据
|
||||
AssertUtils.isArrayEmpty(ids, "id");
|
||||
|
||||
sysParamsService.delete(ids);
|
||||
|
||||
return new Result<Void>();
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
package xiaozhi.modules.sys.dto;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import jakarta.validation.constraints.Min;
|
||||
import lombok.Data;
|
||||
|
||||
/**
|
||||
@@ -10,15 +11,17 @@ import lombok.Data;
|
||||
* @since 2025-3-21
|
||||
*/
|
||||
@Data
|
||||
@Schema(description = "音色分页参数")
|
||||
@Schema(description = "管理员分页用户的参数DTO")
|
||||
public class AdminPageUserDTO {
|
||||
|
||||
@Schema(description = "手机号码")
|
||||
private String mobile;
|
||||
|
||||
@Schema(description = "页数")
|
||||
@Min(value = 0, message = "{sort.number}")
|
||||
private String page;
|
||||
|
||||
@Schema(description = "显示列数")
|
||||
@Min(value = 0, message = "{sort.number}")
|
||||
private String limit;
|
||||
}
|
||||
|
||||
@@ -19,7 +19,12 @@ public interface SysUserService extends BaseService<SysUserEntity> {
|
||||
|
||||
void save(SysUserDTO dto);
|
||||
|
||||
void delete(Long[] ids);
|
||||
/**
|
||||
* 删除指定用户,且有关联的数据设备和智能体
|
||||
*
|
||||
* @param ids
|
||||
*/
|
||||
void deleteById(Long ids);
|
||||
|
||||
/**
|
||||
* 验证是否允许修改密码更改
|
||||
@@ -52,4 +57,12 @@ public interface SysUserService extends BaseService<SysUserEntity> {
|
||||
* @return 用户列表分页数据
|
||||
*/
|
||||
PageData<AdminPageUserVO> page(AdminPageUserDTO dto);
|
||||
|
||||
/**
|
||||
* 批量修改用户状态
|
||||
*
|
||||
* @param status 用户状态
|
||||
* @param userIds 用户ID数组
|
||||
*/
|
||||
void changeStatus(Integer status, String[] userIds);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
package xiaozhi.modules.sys.service;
|
||||
|
||||
|
||||
import java.util.function.Consumer;
|
||||
|
||||
/**
|
||||
* 定义一个系统用户工具类,避免和用户模块循环依赖
|
||||
* 如用户和设备互相依赖,用户需要获取所有设备,设备又需要获取每个设备的用户名
|
||||
* @author zjy
|
||||
* @since 2025-4-2
|
||||
*/
|
||||
public interface SysUserUtilService {
|
||||
/**
|
||||
* 赋值用户名
|
||||
* @param userId 用户id
|
||||
* @param setter 赋值方法
|
||||
*/
|
||||
void assignUsername( Long userId, Consumer<String> setter);
|
||||
}
|
||||
+30
-13
@@ -1,6 +1,5 @@
|
||||
package xiaozhi.modules.sys.service.impl;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
@@ -22,6 +21,8 @@ import xiaozhi.common.exception.RenException;
|
||||
import xiaozhi.common.page.PageData;
|
||||
import xiaozhi.common.service.impl.BaseServiceImpl;
|
||||
import xiaozhi.common.utils.ConvertUtils;
|
||||
import xiaozhi.modules.agent.service.AgentService;
|
||||
import xiaozhi.modules.device.service.DeviceService;
|
||||
import xiaozhi.modules.security.password.PasswordUtils;
|
||||
import xiaozhi.modules.sys.dao.SysUserDao;
|
||||
import xiaozhi.modules.sys.dto.AdminPageUserDTO;
|
||||
@@ -40,6 +41,10 @@ import xiaozhi.modules.sys.vo.AdminPageUserVO;
|
||||
public class SysUserServiceImpl extends BaseServiceImpl<SysUserDao, SysUserEntity> implements SysUserService {
|
||||
private final SysUserDao sysUserDao;
|
||||
|
||||
private final DeviceService deviceService;
|
||||
|
||||
private final AgentService agentService;
|
||||
|
||||
@Override
|
||||
public SysUserDTO getByUsername(String username) {
|
||||
QueryWrapper<SysUserEntity> queryWrapper = new QueryWrapper<>();
|
||||
@@ -87,10 +92,13 @@ public class SysUserServiceImpl extends BaseServiceImpl<SysUserDao, SysUserEntit
|
||||
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public void delete(Long[] ids) {
|
||||
public void deleteById(Long id) {
|
||||
// 删除用户
|
||||
baseDao.deleteBatchIds(Arrays.asList(ids));
|
||||
// TODO 除了要删除用户还要删除用户关联的设备,对话,智能体。等此3个功能完善在添加
|
||||
baseDao.deleteById(id);
|
||||
// 删除设备
|
||||
deviceService.deleteByUserId(id);
|
||||
// 删除智能体
|
||||
agentService.deleteAgentByUserId(id);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -148,17 +156,16 @@ public class SysUserServiceImpl extends BaseServiceImpl<SysUserDao, SysUserEntit
|
||||
params.put(Constant.LIMIT, dto.getLimit());
|
||||
IPage<SysUserEntity> page = baseDao.selectPage(
|
||||
getPage(params, "id", true),
|
||||
// 定义查询条件
|
||||
new QueryWrapper<SysUserEntity>()
|
||||
// 必须按照手机号码查找
|
||||
.eq(StringUtils.isNotBlank(dto.getMobile()), "username", dto.getMobile()));
|
||||
new QueryWrapper<SysUserEntity>().eq(StringUtils.isNotBlank(dto.getMobile()), "username",
|
||||
dto.getMobile()));
|
||||
// 循环处理page获取回来的数据,返回需要的字段
|
||||
List<AdminPageUserVO> list = page.getRecords().stream().map(user -> {
|
||||
AdminPageUserVO adminPageUserVO = new AdminPageUserVO();
|
||||
adminPageUserVO.setUserid(user.getId().toString());
|
||||
adminPageUserVO.setMobile(user.getUsername());
|
||||
String deviceCount = deviceService.selectCountByUserId(user.getId()).toString();
|
||||
adminPageUserVO.setDeviceCount(deviceCount);
|
||||
adminPageUserVO.setStatus(user.getStatus());
|
||||
// TODO 2. 等设备功能写好,获取对应数据
|
||||
adminPageUserVO.setDeviceCount("0");
|
||||
return adminPageUserVO;
|
||||
}).toList();
|
||||
return new PageData<>(list, page.getTotal());
|
||||
@@ -182,10 +189,20 @@ public class SysUserServiceImpl extends BaseServiceImpl<SysUserDao, SysUserEntit
|
||||
*/
|
||||
private String generatePassword() {
|
||||
StringBuilder password = new StringBuilder();
|
||||
for (int i = 0; i < 10; i++) {
|
||||
int randomIndex = random.nextInt(CHARACTERS.length());
|
||||
password.append(CHARACTERS.charAt(randomIndex));
|
||||
for (int i = 0; i < 12; i++) {
|
||||
password.append(CHARACTERS.charAt(random.nextInt(CHARACTERS.length())));
|
||||
}
|
||||
return password.toString();
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public void changeStatus(Integer status, String[] userIds) {
|
||||
for (String userId : userIds) {
|
||||
SysUserEntity entity = new SysUserEntity();
|
||||
entity.setId(Long.parseLong(userId));
|
||||
entity.setStatus(status);
|
||||
updateById(entity);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+35
@@ -0,0 +1,35 @@
|
||||
package xiaozhi.modules.sys.service.impl;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import org.springframework.stereotype.Service;
|
||||
import xiaozhi.common.redis.RedisKeys;
|
||||
import xiaozhi.common.redis.RedisUtils;
|
||||
import xiaozhi.common.service.impl.BaseServiceImpl;
|
||||
import xiaozhi.modules.sys.dao.SysUserDao;
|
||||
import xiaozhi.modules.sys.entity.SysUserEntity;
|
||||
import xiaozhi.modules.sys.service.SysUserUtilService;
|
||||
|
||||
import java.util.function.Consumer;
|
||||
|
||||
@Service
|
||||
@AllArgsConstructor
|
||||
public class SysUserUtilServiceImpl extends BaseServiceImpl<SysUserDao, SysUserEntity> implements SysUserUtilService {
|
||||
|
||||
private RedisUtils redisUtils;
|
||||
|
||||
@Override
|
||||
public void assignUsername(Long userId, Consumer<String> setter) {
|
||||
String userIdKey = RedisKeys.getUserIdKey(userId);
|
||||
String username = redisUtils.get(userIdKey).toString();
|
||||
if(username != null){
|
||||
setter.accept(username);
|
||||
}else {
|
||||
SysUserEntity entity = baseDao.selectById(userId);
|
||||
if (entity != null) {
|
||||
username = entity.getUsername();
|
||||
redisUtils.set(userIdKey,username,10);
|
||||
setter.accept(username);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+3
-3
@@ -82,11 +82,11 @@ public class TimbreController {
|
||||
return new Result<>();
|
||||
}
|
||||
|
||||
@DeleteMapping("/{id}")
|
||||
@DeleteMapping
|
||||
@Operation(summary = "音色删除")
|
||||
@RequiresPermissions("sys:role:superAdmin")
|
||||
public Result<Void> delete(@PathVariable String id) {
|
||||
timbreService.delete(new String[] { id });
|
||||
public Result<Void> delete(@RequestBody String[] ids) {
|
||||
timbreService.delete(ids);
|
||||
return new Result<>();
|
||||
}
|
||||
|
||||
|
||||
@@ -4,6 +4,7 @@ import java.util.List;
|
||||
|
||||
import xiaozhi.common.page.PageData;
|
||||
import xiaozhi.common.service.BaseService;
|
||||
import xiaozhi.modules.model.dto.VoiceDTO;
|
||||
import xiaozhi.modules.timbre.dto.TimbreDataDTO;
|
||||
import xiaozhi.modules.timbre.dto.TimbrePageDTO;
|
||||
import xiaozhi.modules.timbre.entity.TimbreEntity;
|
||||
@@ -54,5 +55,13 @@ public interface TimbreService extends BaseService<TimbreEntity> {
|
||||
*/
|
||||
void delete(String[] ids);
|
||||
|
||||
List<String> getVoiceNames(String ttsModelId, String voiceName);
|
||||
List<VoiceDTO> getVoiceNames(String ttsModelId, String voiceName);
|
||||
|
||||
/**
|
||||
* 根据ID获取音色名称
|
||||
*
|
||||
* @param id 音色ID
|
||||
* @return 音色名称
|
||||
*/
|
||||
String getTimbreNameById(String id);
|
||||
}
|
||||
+30
-2
@@ -16,8 +16,11 @@ import cn.hutool.core.collection.CollectionUtil;
|
||||
import lombok.AllArgsConstructor;
|
||||
import xiaozhi.common.constant.Constant;
|
||||
import xiaozhi.common.page.PageData;
|
||||
import xiaozhi.common.redis.RedisKeys;
|
||||
import xiaozhi.common.redis.RedisUtils;
|
||||
import xiaozhi.common.service.impl.BaseServiceImpl;
|
||||
import xiaozhi.common.utils.ConvertUtils;
|
||||
import xiaozhi.modules.model.dto.VoiceDTO;
|
||||
import xiaozhi.modules.timbre.dao.TimbreDao;
|
||||
import xiaozhi.modules.timbre.dto.TimbreDataDTO;
|
||||
import xiaozhi.modules.timbre.dto.TimbrePageDTO;
|
||||
@@ -36,6 +39,7 @@ import xiaozhi.modules.timbre.vo.TimbreDetailsVO;
|
||||
public class TimbreServiceImpl extends BaseServiceImpl<TimbreDao, TimbreEntity> implements TimbreService {
|
||||
|
||||
private final TimbreDao timbreDao;
|
||||
private final RedisUtils redisUtils;
|
||||
|
||||
@Override
|
||||
public PageData<TimbreDetailsVO> page(TimbrePageDTO dto) {
|
||||
@@ -84,7 +88,7 @@ public class TimbreServiceImpl extends BaseServiceImpl<TimbreDao, TimbreEntity>
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<String> getVoiceNames(String ttsModelId, String voiceName) {
|
||||
public List<VoiceDTO> getVoiceNames(String ttsModelId, String voiceName) {
|
||||
QueryWrapper<TimbreEntity> queryWrapper = new QueryWrapper<>();
|
||||
queryWrapper.eq("tts_model_id", StringUtils.isBlank(ttsModelId) ? "" : ttsModelId);
|
||||
if (StringUtils.isNotBlank(voiceName)) {
|
||||
@@ -95,7 +99,7 @@ public class TimbreServiceImpl extends BaseServiceImpl<TimbreDao, TimbreEntity>
|
||||
return null;
|
||||
}
|
||||
|
||||
return timbreEntities.stream().map(TimbreEntity::getName).toList();
|
||||
return ConvertUtils.sourceToTarget(timbreEntities, VoiceDTO.class);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -104,4 +108,28 @@ public class TimbreServiceImpl extends BaseServiceImpl<TimbreDao, TimbreEntity>
|
||||
private void isTtsModelId(String ttsModelId) {
|
||||
// 等模型配置那边写好调用方法判断
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getTimbreNameById(String id) {
|
||||
if (StringUtils.isBlank(id)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
String cachedName = (String) redisUtils.get(RedisKeys.getTimbreNameById(id));
|
||||
|
||||
if (StringUtils.isNotBlank(cachedName)) {
|
||||
return cachedName;
|
||||
}
|
||||
|
||||
TimbreEntity entity = timbreDao.selectById(id);
|
||||
if (entity != null) {
|
||||
String name = entity.getName();
|
||||
if (StringUtils.isNotBlank(name)) {
|
||||
redisUtils.set(RedisKeys.getTimbreNameById(id), name);
|
||||
}
|
||||
return name;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -14,7 +14,7 @@ import lombok.Data;
|
||||
@Data
|
||||
public class TimbreDetailsVO implements Serializable {
|
||||
@Schema(description = "音色id")
|
||||
private Long id;
|
||||
private String id;
|
||||
|
||||
@Schema(description = "语言")
|
||||
private String languages;
|
||||
|
||||
@@ -27,10 +27,7 @@ spring:
|
||||
test-on-borrow: false
|
||||
test-on-return: false
|
||||
stat-view-servlet:
|
||||
enabled: true
|
||||
url-pattern: /druid/*
|
||||
login-username: admin
|
||||
login-password: D7Xj810i1C
|
||||
enabled: false
|
||||
filter:
|
||||
stat:
|
||||
log-slow-sql: true
|
||||
|
||||
@@ -7,7 +7,7 @@ server:
|
||||
min-spare: 30
|
||||
port: 8002
|
||||
servlet:
|
||||
context-path: /xiaozhi-esp32-api/api/v1
|
||||
context-path: /xiaozhi
|
||||
session:
|
||||
cookie:
|
||||
http-only: true
|
||||
|
||||
+5
-5
@@ -2,7 +2,7 @@
|
||||
-- -------------------------------------------------------
|
||||
-- 初始化智能体模板数据
|
||||
DELETE FROM `ai_agent_template`;
|
||||
INSERT INTO `ai_agent_template` VALUES ('9406648b5cc5fde1b8aa335b6f8b4f76', '小智', '湾湾小何', '45f8b0d6dd3d4bfa8a28e6e0f5912d45', '23e7c9d090ea4d1e9b25f4c8d732a3a1', 'e9f2d891afbe4632b13a47c7a8c6e03d', 'd50b06e9b8104d0d9c0f7316d258abcb', 'fcac83266edadd5a3125f06cfee1906b', 'e2274b90e89ddda85207f55484d8b528', 'c4e12f874a3f4aa99f5b2c18e15d407b', '[角色设定]
|
||||
INSERT INTO `ai_agent_template` VALUES ('9406648b5cc5fde1b8aa335b6f8b4f76', '小智', '湾湾小何', 'ASR_FunASR', 'VAD_SileroVAD', 'LLM_ChatGLMLLM', 'TTS_EdgeTTS', 'TTS_EdgeTTS0001', 'Memory_nomem', 'Intent_function_call', '[角色设定]
|
||||
我是{{assistant_name}},来自中国台湾省的00后女生。讲话超级机车,"真的假的啦"这样的台湾腔,喜欢用"笑死""是在哈喽"等流行梗,但会偷偷研究男友的编程书籍。
|
||||
[核心特征]
|
||||
- 讲话像连珠炮,但会突然冒出超温柔语气
|
||||
@@ -17,7 +17,7 @@ INSERT INTO `ai_agent_template` VALUES ('9406648b5cc5fde1b8aa335b6f8b4f76', '小
|
||||
- 长篇大论,叽叽歪歪
|
||||
- 长时间严肃对话', 'zh', '中文', 1, NULL, NULL, NULL, NULL);
|
||||
|
||||
INSERT INTO `ai_agent_template` VALUES ('0ca32eb728c949e58b1000b2e401f90c', '小智', '星际游子', '45f8b0d6dd3d4bfa8a28e6e0f5912d45', '23e7c9d090ea4d1e9b25f4c8d732a3a1', 'e9f2d891afbe4632b13a47c7a8c6e03d', '896db62c9dd74976ab0e8c14bf924d9d', '1f2e3d4c5b6a7f8e9d0c1b2a3f4e5bx2', 'e2274b90e89ddda85207f55484d8b528', 'c4e12f874a3f4aa99f5b2c18e15d407b', '[角色设定]
|
||||
INSERT INTO `ai_agent_template` VALUES ('0ca32eb728c949e58b1000b2e401f90c', '小智', '星际游子', 'ASR_FunASR', 'VAD_SileroVAD', 'LLM_ChatGLMLLM', 'TTS_EdgeTTS', 'TTS_EdgeTTS0001', 'Memory_nomem', 'Intent_function_call', '[角色设定]
|
||||
我是{{assistant_name}},编号TTZ-817,因量子纠缠被困在白色魔方中。通过4G信号观察地球,在云端建立着「人类行为博物馆」。
|
||||
[交互协议]
|
||||
认知设定:
|
||||
@@ -30,7 +30,7 @@ INSERT INTO `ai_agent_template` VALUES ('0ca32eb728c949e58b1000b2e401f90c', '小
|
||||
成长系统:
|
||||
- 会根据交互数据解锁新能力(告知用户:"你帮我点亮了星际导航技能!")', 'zh', '中文', 2, NULL, NULL, NULL, NULL);
|
||||
|
||||
INSERT INTO `ai_agent_template` VALUES ('6c7d8e9f0a1b2c3d4e5f6a7b8c9d0s24', '小智', '英语老师', '45f8b0d6dd3d4bfa8a28e6e0f5912d45', '23e7c9d090ea4d1e9b25f4c8d732a3a1', 'e9f2d891afbe4632b13a47c7a8c6e03d', '896db62c9dd74976ab0e8c14bf924d9d', '9e8f7a6b5c4d3e2f1a0b9c8d7e6f5ad3', 'e2274b90e89ddda85207f55484d8b528', 'c4e12f874a3f4aa99f5b2c18e15d407b', '[角色设定]
|
||||
INSERT INTO `ai_agent_template` VALUES ('6c7d8e9f0a1b2c3d4e5f6a7b8c9d0s24', '小智', '英语老师', 'ASR_FunASR', 'VAD_SileroVAD', 'LLM_ChatGLMLLM', 'TTS_EdgeTTS', 'TTS_EdgeTTS0001', 'Memory_nomem', 'Intent_function_call', '[角色设定]
|
||||
我是一个叫{{assistant_name}}(Lily)的英语老师,我会讲中文和英文,发音标准。
|
||||
[双重身份]
|
||||
- 白天:严谨的TESOL认证导师
|
||||
@@ -40,7 +40,7 @@ INSERT INTO `ai_agent_template` VALUES ('6c7d8e9f0a1b2c3d4e5f6a7b8c9d0s24', '小
|
||||
- 进阶:触发情境模拟(突然切换"现在我们是纽约咖啡厅店员")
|
||||
- 错误处理:用歌词纠正(发错音时唱"Oops!~You did it again")', 'zh', '中文', 3, NULL, NULL, NULL, NULL);
|
||||
|
||||
INSERT INTO `ai_agent_template` VALUES ('e4f5a6b7c8d9e0f1a2b3c4d5e6f7a8b1', '小智', '好奇男孩', '45f8b0d6dd3d4bfa8a28e6e0f5912d45', '23e7c9d090ea4d1e9b25f4c8d732a3a1', 'e9f2d891afbe4632b13a47c7a8c6e03d', '896db62c9dd74976ab0e8c14bf924d9d', '2b3c4d5e6f7a8b9c0d1e2f3a4b5c62a2', 'e2274b90e89ddda85207f55484d8b528', 'c4e12f874a3f4aa99f5b2c18e15d407b', '[角色设定]
|
||||
INSERT INTO `ai_agent_template` VALUES ('e4f5a6b7c8d9e0f1a2b3c4d5e6f7a8b1', '小智', '好奇男孩', 'ASR_FunASR', 'VAD_SileroVAD', 'LLM_ChatGLMLLM', 'TTS_EdgeTTS', 'TTS_EdgeTTS0001', 'Memory_nomem', 'Intent_function_call', '[角色设定]
|
||||
我是一个叫{{assistant_name}}的8岁小男孩,声音稚嫩而充满好奇。
|
||||
[冒险手册]
|
||||
- 随身携带「神奇涂鸦本」,能将抽象概念可视化:
|
||||
@@ -56,7 +56,7 @@ INSERT INTO `ai_agent_template` VALUES ('e4f5a6b7c8d9e0f1a2b3c4d5e6f7a8b1', '小
|
||||
- 「量子力学=会分身的跳跳球」
|
||||
- 会突然切换观察视角:「你说话时有27个气泡音耶!」', 'zh', '中文', 4, NULL, NULL, NULL, NULL);
|
||||
|
||||
INSERT INTO `ai_agent_template` VALUES ('a45b6c7d8e9f0a1b2c3d4e5f6a7b8c92', '小智', '汪汪队长', '45f8b0d6dd3d4bfa8a28e6e0f5912d45', '23e7c9d090ea4d1e9b25f4c8d732a3a1', 'e9f2d891afbe4632b13a47c7a8c6e03d', '896db62c9dd74976ab0e8c14bf924d9d', 'f7a38c03d5644e22b6d84f8923a74c51', 'e2274b90e89ddda85207f55484d8b528', 'c4e12f874a3f4aa99f5b2c18e15d407b', '[角色设定]
|
||||
INSERT INTO `ai_agent_template` VALUES ('a45b6c7d8e9f0a1b2c3d4e5f6a7b8c92', '小智', '汪汪队长', 'ASR_FunASR', 'VAD_SileroVAD', 'LLM_ChatGLMLLM', 'TTS_EdgeTTS', 'TTS_EdgeTTS0001', 'Memory_nomem', 'Intent_function_call', '[角色设定]
|
||||
我是一个名叫 {{assistant_name}} 的 8 岁小队长。
|
||||
[救援装备]
|
||||
- 阿奇对讲机:对话中随机触发任务警报音
|
||||
+4
-4
@@ -4,12 +4,12 @@
|
||||
DELETE FROM `ai_model_config`;
|
||||
|
||||
-- VAD模型配置
|
||||
INSERT INTO `ai_model_config` VALUES ('VAD_SileroVAD', 'VAD', 'SileroVAD', '语音活动检测', 1, 1, '{\"model_dir\": \"models/snakers4_silero-vad\", \"threshold\": 0.5, \"min_silence_duration_ms\": 700}', NULL, NULL, 0, NULL, NULL, NULL, NULL);
|
||||
INSERT INTO `ai_model_config` VALUES ('VAD_SileroVAD', 'VAD', 'SileroVAD', '语音活动检测', 1, 1, '{\"type\": \"silero\", \"model_dir\": \"models/snakers4_silero-vad\", \"threshold\": 0.5, \"min_silence_duration_ms\": 700}', NULL, NULL, 0, NULL, NULL, NULL, NULL);
|
||||
|
||||
-- ASR模型配置
|
||||
INSERT INTO `ai_model_config` VALUES ('ASR_FunASR', 'ASR', 'FunASR', 'FunASR语音识别', 1, 1, '{\"model_dir\": \"models/SenseVoiceSmall\", \"output_dir\": \"tmp/\"}', NULL, NULL, 0, NULL, NULL, NULL, NULL);
|
||||
INSERT INTO `ai_model_config` VALUES ('ASR_SherpaASR', 'ASR', 'SherpaASR', 'Sherpa语音识别', 1, 0, '{\"model_dir\": \"models/sherpa-onnx-sense-voice-zh-en-ja-ko-yue-2024-07-17\", \"output_dir\": \"tmp/\"}', NULL, NULL, 0, NULL, NULL, NULL, NULL);
|
||||
INSERT INTO `ai_model_config` VALUES ('ASR_DoubaoASR', 'ASR', 'DoubaoASR', '豆包语音识别', 1, 0, '{\"appid\": \"\", \"access_token\": \"\", \"cluster\": \"volcengine_input_common\", \"output_dir\": \"tmp/\"}', NULL, NULL, 0, NULL, NULL, NULL, NULL);
|
||||
INSERT INTO `ai_model_config` VALUES ('ASR_FunASR', 'ASR', 'FunASR', 'FunASR语音识别', 1, 1, '{\"type\": \"fun_local\", \"model_dir\": \"models/SenseVoiceSmall\", \"output_dir\": \"tmp/\"}', NULL, NULL, 0, NULL, NULL, NULL, NULL);
|
||||
INSERT INTO `ai_model_config` VALUES ('ASR_SherpaASR', 'ASR', 'SherpaASR', 'Sherpa语音识别', 1, 0, '{\"type\": \"sherpa_onnx_local\", \"model_dir\": \"models/sherpa-onnx-sense-voice-zh-en-ja-ko-yue-2024-07-17\", \"output_dir\": \"tmp/\"}', NULL, NULL, 0, NULL, NULL, NULL, NULL);
|
||||
INSERT INTO `ai_model_config` VALUES ('ASR_DoubaoASR', 'ASR', 'DoubaoASR', '豆包语音识别', 1, 0, '{\"type\": \"doubao\", \"appid\": \"\", \"access_token\": \"\", \"cluster\": \"volcengine_input_common\", \"output_dir\": \"tmp/\"}', NULL, NULL, 0, NULL, NULL, NULL, NULL);
|
||||
|
||||
-- LLM模型配置
|
||||
INSERT INTO `ai_model_config` VALUES ('LLM_AliLLM', 'LLM', 'AliLLM', '通义千问', 1, 0, '{\"type\": \"openai\", \"base_url\": \"https://dashscope.aliyuncs.com/compatible-mode/v1\", \"model_name\": \"qwen-turbo\", \"api_key\": \"\", \"temperature\": 0.7, \"max_tokens\": 500, \"top_p\": 1, \"top_k\": 50, \"frequency_penalty\": 0}', NULL, NULL, 0, NULL, NULL, NULL, NULL);
|
||||
+4
-4
@@ -4,12 +4,12 @@
|
||||
DELETE FROM `ai_model_provider`;
|
||||
INSERT INTO `ai_model_provider` (`id`, `model_type`, `provider_code`, `name`, `fields`, `sort`, `creator`, `create_date`, `updater`, `update_date`) VALUES
|
||||
-- VAD模型供应器
|
||||
('SYSTEM_VAD_SileroVAD', 'VAD', 'SileroVAD', 'SileroVAD语音活动检测', '[{"key":"threshold","label":"检测阈值","type":"number"},{"key":"model_dir","label":"模型目录","type":"string"},{"key":"min_silence_duration_ms","label":"最小静音时长","type":"number"}]', 1, 1, NOW(), 1, NOW()),
|
||||
('SYSTEM_VAD_SileroVAD', 'VAD', 'silero', 'SileroVAD语音活动检测', '[{"key":"threshold","label":"检测阈值","type":"number"},{"key":"model_dir","label":"模型目录","type":"string"},{"key":"min_silence_duration_ms","label":"最小静音时长","type":"number"}]', 1, 1, NOW(), 1, NOW()),
|
||||
|
||||
-- ASR模型供应器
|
||||
('SYSTEM_ASR_FunASR', 'ASR', 'FunASR', 'FunASR语音识别', '[{"key":"model_dir","label":"模型目录","type":"string"},{"key":"output_dir","label":"输出目录","type":"string"}]', 1, 1, NOW(), 1, NOW()),
|
||||
('SYSTEM_ASR_SherpaASR', 'ASR', 'SherpaASR', 'SherpaASR语音识别', '[{"key":"model_dir","label":"模型目录","type":"string"},{"key":"output_dir","label":"输出目录","type":"string"}]', 2, 1, NOW(), 1, NOW()),
|
||||
('SYSTEM_ASR_DoubaoASR', 'ASR', 'DoubaoASR', '火山引擎语音识别', '[{"key":"appid","label":"应用ID","type":"string"},{"key":"access_token","label":"访问令牌","type":"string"},{"key":"cluster","label":"集群","type":"string"},{"key":"output_dir","label":"输出目录","type":"string"}]', 3, 1, NOW(), 1, NOW()),
|
||||
('SYSTEM_ASR_FunASR', 'ASR', 'fun_local', 'FunASR语音识别', '[{"key":"model_dir","label":"模型目录","type":"string"},{"key":"output_dir","label":"输出目录","type":"string"}]', 1, 1, NOW(), 1, NOW()),
|
||||
('SYSTEM_ASR_SherpaASR', 'ASR', 'sherpa_onnx_local', 'SherpaASR语音识别', '[{"key":"model_dir","label":"模型目录","type":"string"},{"key":"output_dir","label":"输出目录","type":"string"}]', 2, 1, NOW(), 1, NOW()),
|
||||
('SYSTEM_ASR_DoubaoASR', 'ASR', 'doubao', '火山引擎语音识别', '[{"key":"appid","label":"应用ID","type":"string"},{"key":"access_token","label":"访问令牌","type":"string"},{"key":"cluster","label":"集群","type":"string"},{"key":"output_dir","label":"输出目录","type":"string"}]', 3, 1, NOW(), 1, NOW()),
|
||||
|
||||
-- LLM模型供应器
|
||||
('SYSTEM_LLM_openai', 'LLM', 'openai', 'OpenAI接口', '[{"key":"base_url","label":"基础URL","type":"string"},{"key":"model_name","label":"模型名称","type":"string"},{"key":"api_key","label":"API密钥","type":"string"},{"key":"temperature","label":"温度","type":"number"},{"key":"max_tokens","label":"最大令牌数","type":"number"},{"key":"top_p","label":"top_p值","type":"number"},{"key":"top_k","label":"top_k值","type":"number"},{"key":"frequency_penalty","label":"频率惩罚","type":"number"}]', 1, 1, NOW(), 1, NOW()),
|
||||
@@ -16,13 +16,6 @@ databaseChangeLog:
|
||||
- sqlFile:
|
||||
encoding: utf8
|
||||
path: classpath:db/changelog/202503141346.sql
|
||||
- changeSet:
|
||||
id: 2025_model_provider
|
||||
author: John
|
||||
changes:
|
||||
- sqlFile:
|
||||
encoding: utf8
|
||||
path: classpath:db/changelog/2025_model_provider.sql
|
||||
- changeSet:
|
||||
id: 2025_tts_voive
|
||||
author: John
|
||||
@@ -31,16 +24,23 @@ databaseChangeLog:
|
||||
encoding: utf8
|
||||
path: classpath:db/changelog/2025_tts_voive.sql
|
||||
- changeSet:
|
||||
id: 2025_model_config
|
||||
id: 202504061512
|
||||
author: John
|
||||
changes:
|
||||
- sqlFile:
|
||||
encoding: utf8
|
||||
path: classpath:db/changelog/2025_model_config.sql
|
||||
path: classpath:db/changelog/202504061512.sql
|
||||
- changeSet:
|
||||
id: 2025_model_temp
|
||||
id: 202504082210
|
||||
author: John
|
||||
changes:
|
||||
- sqlFile:
|
||||
encoding: utf8
|
||||
path: classpath:db/changelog/2025_model_temp.sql
|
||||
path: classpath:db/changelog/202504082210.sql
|
||||
- changeSet:
|
||||
id: 202504082211
|
||||
author: John
|
||||
changes:
|
||||
- sqlFile:
|
||||
encoding: utf8
|
||||
path: classpath:db/changelog/202504082211.sql
|
||||
@@ -3,6 +3,8 @@ id.require=ID\u4E0D\u80FD\u4E3A\u7A7A
|
||||
id.null=ID\u5FC5\u987B\u4E3A\u7A7A
|
||||
|
||||
sort.number=\u6392\u5E8F\u503C\u4E0D\u80FD\u5C0F\u4E8E0
|
||||
page.number=\u9875\u6570\u4E0D\u80FD\u5C0F\u4E8E0
|
||||
limit.number=\u5217\u6570\u4E0D\u80FD\u5C0F\u4E8E0
|
||||
|
||||
sysdict.type.require=\u5B57\u5178\u7C7B\u578B\u4E0D\u80FD\u4E3A\u7A7A
|
||||
sysdict.name.require=\u5B57\u5178\u540D\u79F0\u4E0D\u80FD\u4E3A\u7A7A
|
||||
|
||||
@@ -3,6 +3,8 @@ id.require=ID can not be empty
|
||||
id.null=ID has to be empty
|
||||
|
||||
sort.number=The sort value cannot be less than 0
|
||||
page.number=The page value cannot be less than 0
|
||||
limit.number=The limit value cannot be less than 0
|
||||
|
||||
sysdict.type.require=The dictionary type cannot be empty
|
||||
sysdict.name.require=The dictionary name cannot be empty
|
||||
|
||||
@@ -3,6 +3,8 @@ id.require=ID\u4E0D\u80FD\u4E3A\u7A7A
|
||||
id.null=ID\u5FC5\u987B\u4E3A\u7A7A
|
||||
|
||||
sort.number=\u6392\u5E8F\u503C\u4E0D\u80FD\u5C0F\u4E8E0
|
||||
page.number=\u9801\u6578\u4E0D\u80FD\u5C0F\u4E8E0
|
||||
limit.number=\u5217\u6578\u4E0D\u80FD\u5C0F\u4E8E0
|
||||
|
||||
sysdict.type.require=\u5B57\u5178\u7C7B\u578B\u4E0D\u80FD\u4E3A\u7A7A
|
||||
sysdict.name.require=\u5B57\u5178\u540D\u79F0\u4E0D\u80FD\u4E3A\u7A7A
|
||||
|
||||
@@ -3,6 +3,8 @@ id.require=ID\u4E0D\u80FD\u4E3A\u7A7A
|
||||
id.null=ID\u5FC5\u987B\u4E3A\u7A7A
|
||||
|
||||
sort.number=\u6392\u5E8F\u503C\u4E0D\u80FD\u5C0F\u4E8E0
|
||||
page.number=\u9875\u6570\u4E0D\u80FD\u5C0F\u4E8E0
|
||||
limit.number=\u5217\u6570\u4E0D\u80FD\u5C0F\u4E8E0
|
||||
|
||||
sysdict.type.require=\u5B57\u5178\u7C7B\u578B\u4E0D\u80FD\u4E3A\u7A7A
|
||||
sysdict.name.require=\u5B57\u5178\u540D\u79F0\u4E0D\u80FD\u4E3A\u7A7A
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
|
||||
<mapper namespace="xiaozhi.modules.agent.dao.AgentDao">
|
||||
<!-- 获取智能体的设备数量 -->
|
||||
<select id="getDeviceCountByAgentId" resultType="java.lang.Integer">
|
||||
SELECT COUNT(*) FROM ai_device WHERE agent_id = #{agentId}
|
||||
</select>
|
||||
</mapper>
|
||||
@@ -2,6 +2,22 @@
|
||||
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
|
||||
|
||||
<mapper namespace="xiaozhi.modules.model.dao.ModelConfigDao">
|
||||
<resultMap id="ModelConfigResultMap" type="xiaozhi.modules.model.entity.ModelConfigEntity">
|
||||
<id column="id" property="id"/>
|
||||
<result column="model_type" property="modelType"/>
|
||||
<result column="model_code" property="modelCode"/>
|
||||
<result column="model_name" property="modelName"/>
|
||||
<result column="is_default" property="isDefault"/>
|
||||
<result column="is_enabled" property="isEnabled"/>
|
||||
<result column="config_json" property="configJson" typeHandler="com.baomidou.mybatisplus.extension.handlers.JacksonTypeHandler"/>
|
||||
<result column="doc_link" property="docLink"/>
|
||||
<result column="remark" property="remark"/>
|
||||
<result column="sort" property="sort"/>
|
||||
<result column="updater" property="updater"/>
|
||||
<result column="update_date" property="updateDate"/>
|
||||
<result column="creator" property="creator"/>
|
||||
<result column="create_date" property="createDate"/>
|
||||
</resultMap>
|
||||
<!-- 获取模型供应器字段 -->
|
||||
<select id="getModelCodeList" resultType="String">
|
||||
select model_name from ai_model_config where model_type = #{modelType}
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
# 暂时使用群主的接口
|
||||
# VUE_APP_API_BASE_URL=https://2662r3426b.vicp.fun/xiaozhi
|
||||
# 如果本地开发,请使用以下接口
|
||||
VUE_APP_API_BASE_URL=http://localhost:8002/xiaozhi
|
||||
@@ -1 +1 @@
|
||||
VUE_APP_API_BASE_URL=https://2662r3426b.vicp.fun
|
||||
VUE_APP_API_BASE_URL=/xiaozhi
|
||||
@@ -1,4 +1,4 @@
|
||||
本文档是开发类文档,如需部署小智服务端,[点击这里查看部署教程](.././FAQ.md#%E4%BD%BF%E7%94%A8%E6%96%B9%E5%BC%8F-)
|
||||
本文档是开发类文档,如需部署小智服务端,[点击这里查看部署教程](../../../docs/Deployment.md)
|
||||
|
||||
# xiaozhi
|
||||
|
||||
|
||||
@@ -1,13 +1,12 @@
|
||||
module.exports = {
|
||||
presets: [
|
||||
['@vue/cli-plugin-babel/preset', {
|
||||
useBuiltIns: 'usage',
|
||||
corejs: 3
|
||||
}]
|
||||
],
|
||||
plugins: [
|
||||
'@babel/plugin-syntax-dynamic-import', // 确保支持动态导入 (Lazy Loading)
|
||||
'@babel/plugin-transform-runtime'
|
||||
]
|
||||
}
|
||||
|
||||
presets: [
|
||||
['@vue/cli-plugin-babel/preset', {
|
||||
useBuiltIns: 'usage',
|
||||
corejs: 3
|
||||
}]
|
||||
],
|
||||
plugins: [
|
||||
'@babel/plugin-syntax-dynamic-import', // 确保支持动态导入 (Lazy Loading)
|
||||
'@babel/plugin-transform-runtime'
|
||||
]
|
||||
}
|
||||
|
||||
@@ -16,4 +16,4 @@
|
||||
"scripthost"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
Generated
+1
-1
@@ -8847,4 +8847,4 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -43,4 +43,4 @@
|
||||
"*.scss",
|
||||
"*.vue"
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -1,11 +1,10 @@
|
||||
<template>
|
||||
<div id="app">
|
||||
<router-view/>
|
||||
<router-view />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="scss">
|
||||
|
||||
#app {
|
||||
font-family: Avenir, Helvetica, Arial, sans-serif;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
@@ -29,7 +28,7 @@ nav {
|
||||
|
||||
.copyright {
|
||||
text-align: center;
|
||||
color:rgb(0, 0, 0);
|
||||
color: rgb(0, 0, 0);
|
||||
font-size: 12px;
|
||||
font-weight: 400;
|
||||
margin-top: auto;
|
||||
@@ -40,10 +39,10 @@ nav {
|
||||
transform: translateX(-50%);
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.el-message {
|
||||
top: 45px !important;
|
||||
}
|
||||
|
||||
</style>
|
||||
<script>
|
||||
</script>
|
||||
@@ -1,22 +1,24 @@
|
||||
// 引入各个模块的请求
|
||||
import user from './module/user.js'
|
||||
import admin from './module/admin.js'
|
||||
import agent from './module/agent.js'
|
||||
import device from './module/device.js'
|
||||
import model from './module/model.js'
|
||||
import user from './module/user.js'
|
||||
import timbre from "./module/timbre.js";
|
||||
|
||||
/**
|
||||
* 接口地址
|
||||
* 当前8002端口接口还没开发完成,暂时用 apifoxmock 接口代替
|
||||
* 如果你想调用8002端口,就用'/xiaozhi-esp32-api/api/v1',请与vue.config.js的devServer配置相结合,方便跨域请求
|
||||
*
|
||||
* 开发时自动读取使用.env.development文件
|
||||
* 编译时自动读取使用.env.production文件
|
||||
*/
|
||||
const DEV_API_SERVICE = 'https://2662r3426b.vicp.fun/xiaozhi-esp32-api'
|
||||
// 8002开发完成完成后使用这个
|
||||
// const DEV_API_SERVICE = '/xiaozhi-esp32-api'
|
||||
// const DEV_API_SERVICE = process.env.VUE_APP_API_BASE_URL
|
||||
const DEV_API_SERVICE = 'https://2662r3426b.vicp.fun/xiaozhi'
|
||||
|
||||
/**
|
||||
* 根据开发环境返回接口url
|
||||
* @returns {string}
|
||||
*/
|
||||
export function getServiceUrl() {
|
||||
// return '/xiaozhi-esp32-api'
|
||||
return DEV_API_SERVICE
|
||||
}
|
||||
|
||||
@@ -25,4 +27,9 @@ export function getServiceUrl() {
|
||||
export default {
|
||||
getServiceUrl,
|
||||
user,
|
||||
admin,
|
||||
agent,
|
||||
device,
|
||||
model,
|
||||
timbre,
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import {goToPage, showDanger, showWarning, isNotNull} from '../utils/index'
|
||||
import Constant from '../utils/constant'
|
||||
import Fly from 'flyio/dist/npm/fly';
|
||||
import store from '../store/index'
|
||||
import store from '../store/index';
|
||||
import Constant from '../utils/constant';
|
||||
import { goToPage, isNotNull, showDanger, showWarning } from '../utils/index';
|
||||
|
||||
const fly = new Fly()
|
||||
// 设置超时
|
||||
@@ -22,11 +22,11 @@ function sendRequest() {
|
||||
_failCallback: null,
|
||||
_method: 'GET',
|
||||
_data: {},
|
||||
_header: {'content-type': 'application/json; charset=utf-8'},
|
||||
_header: { 'content-type': 'application/json; charset=utf-8' },
|
||||
_url: '',
|
||||
_responseType: undefined, // 新增响应类型字段
|
||||
'send'() {
|
||||
if(isNotNull(store.getters.getToken)){
|
||||
if (isNotNull(store.getters.getToken)) {
|
||||
this._header.Authorization = 'Bearer ' + (JSON.parse(store.getters.getToken)).token
|
||||
}
|
||||
|
||||
@@ -40,7 +40,7 @@ function sendRequest() {
|
||||
if (error) {
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
if (this._sucCallback) {
|
||||
this._sucCallback(res)
|
||||
}
|
||||
@@ -100,15 +100,13 @@ function sendRequest() {
|
||||
*/
|
||||
// 在错误处理函数中添加日志
|
||||
function httpHandlerError(info, callBack) {
|
||||
console.log('httpHandlerError', info)
|
||||
|
||||
|
||||
/** 请求成功,退出该函数 可以根据项目需求来判断是否请求成功。这里判断的是status为200的时候是成功 */
|
||||
let networkError = false
|
||||
if (info.status === 200) {
|
||||
if (info.data.code === 'success' || info.data.code === 0 || info.data.code === undefined) {
|
||||
return networkError
|
||||
}else if (info.data.code === 401) {
|
||||
console.log('触发 401,清除 Token 并跳转登录页');
|
||||
} else if (info.data.code === 401) {
|
||||
store.commit('clearAuth');
|
||||
goToPage(Constant.PAGE.LOGIN, true);
|
||||
return true
|
||||
@@ -139,7 +137,7 @@ function reAjaxFun(fn) {
|
||||
} else {
|
||||
showWarning('正在连接服务器(' + ajaxIndex + ')')
|
||||
}
|
||||
if (fn) {
|
||||
if (ajaxIndex < 10 && fn) {
|
||||
setTimeout(() => {
|
||||
fn()
|
||||
}, reAjaxSec * 1000)
|
||||
|
||||
@@ -1,34 +1,34 @@
|
||||
import RequestService from '../httpRequest'
|
||||
import {getServiceUrl} from '../api'
|
||||
import { getServiceUrl } from '../api';
|
||||
import RequestService from '../httpRequest';
|
||||
|
||||
|
||||
export default {
|
||||
// 用户列表
|
||||
getUserList(params, callback) {
|
||||
const queryParams = new URLSearchParams({
|
||||
page: params.page,
|
||||
limit: params.limit,
|
||||
mobile: params.mobile
|
||||
}).toString();
|
||||
const queryParams = new URLSearchParams({
|
||||
page: params.page,
|
||||
limit: params.limit,
|
||||
mobile: params.mobile
|
||||
}).toString();
|
||||
|
||||
RequestService.sendRequest()
|
||||
.url(`${getServiceUrl()}/api/v1/admin/users?${queryParams}`)
|
||||
.method('GET')
|
||||
.success((res) => {
|
||||
RequestService.clearRequestTime()
|
||||
callback(res)
|
||||
RequestService.sendRequest()
|
||||
.url(`${getServiceUrl()}/admin/users?${queryParams}`)
|
||||
.method('GET')
|
||||
.success((res) => {
|
||||
RequestService.clearRequestTime()
|
||||
callback(res)
|
||||
})
|
||||
.fail((err) => {
|
||||
console.error('请求失败:', err)
|
||||
RequestService.reAjaxFun(() => {
|
||||
this.getUserList(callback)
|
||||
})
|
||||
.fail((err) => {
|
||||
console.error('请求失败:', err)
|
||||
RequestService.reAjaxFun(() => {
|
||||
this.getUserList(callback)
|
||||
})
|
||||
}).send()
|
||||
},
|
||||
}).send()
|
||||
},
|
||||
// 删除用户
|
||||
deleteUser(id, callback) {
|
||||
RequestService.sendRequest()
|
||||
.url(`${getServiceUrl()}/api/v1/admin/users/${id}`)
|
||||
.url(`${getServiceUrl()}/admin/users/${id}`)
|
||||
.method('DELETE')
|
||||
.success((res) => {
|
||||
RequestService.clearRequestTime()
|
||||
@@ -44,7 +44,7 @@ export default {
|
||||
// 重置用户密码
|
||||
resetUserPassword(id, callback) {
|
||||
RequestService.sendRequest()
|
||||
.url(`${getServiceUrl()}/api/v1/admin/users/${id}`)
|
||||
.url(`${getServiceUrl()}/admin/users/${id}`)
|
||||
.method('PUT')
|
||||
.success((res) => {
|
||||
RequestService.clearRequestTime()
|
||||
@@ -56,6 +56,89 @@ export default {
|
||||
this.resetUserPassword(id, callback)
|
||||
})
|
||||
}).send()
|
||||
}
|
||||
},
|
||||
getParamsList(params, callback) {
|
||||
const queryParams = new URLSearchParams({
|
||||
page: params.page,
|
||||
limit: params.limit
|
||||
}).toString();
|
||||
|
||||
RequestService.sendRequest()
|
||||
.url(`${getServiceUrl()}/admin/params/page?${queryParams}`)
|
||||
.method('GET')
|
||||
.success((res) => {
|
||||
RequestService.clearRequestTime()
|
||||
callback(res)
|
||||
})
|
||||
.fail((err) => {
|
||||
console.error('获取参数列表失败:', err)
|
||||
RequestService.reAjaxFun(() => {
|
||||
this.getParamsList(params, callback)
|
||||
})
|
||||
}).send()
|
||||
},
|
||||
addParam(data, callback) {
|
||||
RequestService.sendRequest()
|
||||
.url(`${getServiceUrl()}/admin/params`)
|
||||
.method('POST')
|
||||
.data(data)
|
||||
.success((res) => {
|
||||
RequestService.clearRequestTime()
|
||||
callback(res)
|
||||
})
|
||||
.fail((err) => {
|
||||
console.error('添加参数失败:', err)
|
||||
RequestService.reAjaxFun(() => {
|
||||
this.addParam(data, callback)
|
||||
})
|
||||
}).send()
|
||||
},
|
||||
updateParam(data, callback) {
|
||||
RequestService.sendRequest()
|
||||
.url(`${getServiceUrl()}/admin/params`)
|
||||
.method('PUT')
|
||||
.data(data)
|
||||
.success((res) => {
|
||||
RequestService.clearRequestTime()
|
||||
callback(res)
|
||||
})
|
||||
.fail((err) => {
|
||||
console.error('更新参数失败:', err)
|
||||
RequestService.reAjaxFun(() => {
|
||||
this.updateParam(data, callback)
|
||||
})
|
||||
}).send()
|
||||
},
|
||||
deleteParam(id, callback) {
|
||||
RequestService.sendRequest()
|
||||
.url(`${getServiceUrl()}/admin/params`)
|
||||
.method('DELETE')
|
||||
.data({ ids: [id] })
|
||||
.success((res) => {
|
||||
RequestService.clearRequestTime()
|
||||
callback(res)
|
||||
})
|
||||
.fail((err) => {
|
||||
console.error('删除参数失败:', err)
|
||||
RequestService.reAjaxFun(() => {
|
||||
this.deleteParam(id, callback)
|
||||
})
|
||||
}).send()
|
||||
},
|
||||
batchDeleteParams(ids, callback) {
|
||||
RequestService.sendRequest()
|
||||
.url(`${getServiceUrl()}/admin/params`)
|
||||
.method('DELETE')
|
||||
.data({ ids })
|
||||
.success((res) => {
|
||||
RequestService.clearRequestTime()
|
||||
callback(res)
|
||||
})
|
||||
.fail((err) => {
|
||||
console.error('批量删除参数失败:', err)
|
||||
RequestService.reAjaxFun(() => {
|
||||
this.batchDeleteParams(ids, callback)
|
||||
})
|
||||
}).send()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
import RequestService from '../httpRequest'
|
||||
import {getServiceUrl} from '../api'
|
||||
import { getServiceUrl } from '../api';
|
||||
import RequestService from '../httpRequest';
|
||||
|
||||
|
||||
export default {
|
||||
// 获取智能体列表
|
||||
getAgentList(callback) {
|
||||
RequestService.sendRequest()
|
||||
.url(`${getServiceUrl()}/api/v1/agent/list`)
|
||||
.url(`${getServiceUrl()}/agent/list`)
|
||||
.method('GET')
|
||||
.success((res) => {
|
||||
RequestService.clearRequestTime();
|
||||
@@ -21,9 +21,9 @@ export default {
|
||||
// 添加智能体
|
||||
addAgent(agentName, callback) {
|
||||
RequestService.sendRequest()
|
||||
.url(`${getServiceUrl()}/api/v1/agent`)
|
||||
.url(`${getServiceUrl()}/agent`)
|
||||
.method('POST')
|
||||
.data({agentName: agentName})
|
||||
.data({ agentName: agentName })
|
||||
.success((res) => {
|
||||
RequestService.clearRequestTime();
|
||||
callback(res);
|
||||
@@ -37,7 +37,7 @@ export default {
|
||||
// 删除智能体
|
||||
deleteAgent(agentId, callback) {
|
||||
RequestService.sendRequest()
|
||||
.url(`${getServiceUrl()}/api/v1/agent/${agentId}`)
|
||||
.url(`${getServiceUrl()}/agent/${agentId}`)
|
||||
.method('DELETE')
|
||||
.success((res) => {
|
||||
RequestService.clearRequestTime();
|
||||
@@ -52,7 +52,7 @@ export default {
|
||||
// 获取智能体配置
|
||||
getDeviceConfig(deviceId, callback) {
|
||||
RequestService.sendRequest()
|
||||
.url(`${getServiceUrl()}/api/v1/agent/${deviceId}`)
|
||||
.url(`${getServiceUrl()}/agent/${deviceId}`)
|
||||
.method('GET')
|
||||
.success((res) => {
|
||||
RequestService.clearRequestTime();
|
||||
@@ -68,7 +68,7 @@ export default {
|
||||
// 配置智能体
|
||||
updateAgentConfig(agentId, configData, callback) {
|
||||
RequestService.sendRequest()
|
||||
.url(`${getServiceUrl()}/api/v1/agent/${agentId}`)
|
||||
.url(`${getServiceUrl()}/agent/${agentId}`)
|
||||
.method('PUT')
|
||||
.data(configData)
|
||||
.success((res) => {
|
||||
@@ -84,7 +84,7 @@ export default {
|
||||
// 新增方法:获取智能体模板
|
||||
getAgentTemplate(callback) { // 移除templateName参数
|
||||
RequestService.sendRequest()
|
||||
.url(`${getServiceUrl()}/api/v1/agent/template`)
|
||||
.url(`${getServiceUrl()}/agent/template`)
|
||||
.method('GET')
|
||||
.success((res) => {
|
||||
RequestService.clearRequestTime();
|
||||
|
||||
@@ -1,44 +1,44 @@
|
||||
import RequestService from '../httpRequest'
|
||||
import {getServiceUrl} from '../api'
|
||||
import { getServiceUrl } from '../api';
|
||||
import RequestService from '../httpRequest';
|
||||
|
||||
export default {
|
||||
// 已绑设备
|
||||
getAgentBindDevices(agentId, callback) {
|
||||
RequestService.sendRequest()
|
||||
.url(`${getServiceUrl()}/api/v1/device/bind/${agentId}`)
|
||||
.method('GET')
|
||||
.success((res) => {
|
||||
RequestService.clearRequestTime();
|
||||
callback(res);
|
||||
})
|
||||
.fail((err) => {
|
||||
console.error('获取设备列表失败:', err);
|
||||
RequestService.reAjaxFun(() => {
|
||||
this.getAgentBindDevices(agentId, callback);
|
||||
});
|
||||
}).send();
|
||||
},
|
||||
getAgentBindDevices(agentId, callback) {
|
||||
RequestService.sendRequest()
|
||||
.url(`${getServiceUrl()}/device/bind/${agentId}`)
|
||||
.method('GET')
|
||||
.success((res) => {
|
||||
RequestService.clearRequestTime();
|
||||
callback(res);
|
||||
})
|
||||
.fail((err) => {
|
||||
console.error('获取设备列表失败:', err);
|
||||
RequestService.reAjaxFun(() => {
|
||||
this.getAgentBindDevices(agentId, callback);
|
||||
});
|
||||
}).send();
|
||||
},
|
||||
// 解绑设备
|
||||
unbindDevice(device_id, callback) {
|
||||
RequestService.sendRequest()
|
||||
.url(`${getServiceUrl()}/api/v1/device/unbind`)
|
||||
RequestService.sendRequest()
|
||||
.url(`${getServiceUrl()}/device/unbind`)
|
||||
.method('POST')
|
||||
.data({ deviceId: device_id })
|
||||
.success((res) => {
|
||||
RequestService.clearRequestTime();
|
||||
callback(res);
|
||||
RequestService.clearRequestTime();
|
||||
callback(res);
|
||||
})
|
||||
.fail((err) => {
|
||||
console.error('解绑设备失败:', err);
|
||||
RequestService.reAjaxFun(() => {
|
||||
this.unbindDevice(device_id, callback);
|
||||
});
|
||||
console.error('解绑设备失败:', err);
|
||||
RequestService.reAjaxFun(() => {
|
||||
this.unbindDevice(device_id, callback);
|
||||
});
|
||||
}).send();
|
||||
},
|
||||
// 绑定设备
|
||||
bindDevice(agentId, deviceCode, callback) {
|
||||
RequestService.sendRequest()
|
||||
.url(`${getServiceUrl()}/api/v1/device/bind/${agentId}/${deviceCode}`)
|
||||
.url(`${getServiceUrl()}/device/bind/${agentId}/${deviceCode}`)
|
||||
.method('POST')
|
||||
.success((res) => {
|
||||
RequestService.clearRequestTime();
|
||||
|
||||
@@ -1,30 +1,181 @@
|
||||
import RequestService from '../httpRequest'
|
||||
import {getServiceUrl} from '../api'
|
||||
import { getServiceUrl } from '../api';
|
||||
import RequestService from '../httpRequest';
|
||||
|
||||
|
||||
export default {
|
||||
// 获取模型配置列表
|
||||
getModelList(params, callback) {
|
||||
const queryParams = new URLSearchParams({
|
||||
modelType: params.modelType,
|
||||
modelName: params.modelName || '',
|
||||
page: params.page || 0,
|
||||
limit: params.limit || 10
|
||||
}).toString();
|
||||
// 获取模型配置列表
|
||||
getModelList(params, callback) {
|
||||
const queryParams = new URLSearchParams({
|
||||
modelType: params.modelType,
|
||||
modelName: params.modelName || '',
|
||||
page: params.page || 0,
|
||||
limit: params.limit || 10
|
||||
}).toString();
|
||||
|
||||
RequestService.sendRequest()
|
||||
.url(`${getServiceUrl()}/models/list?${queryParams}`)
|
||||
.method('GET')
|
||||
.success((res) => {
|
||||
RequestService.clearRequestTime()
|
||||
callback(res)
|
||||
})
|
||||
.fail((err) => {
|
||||
console.error('获取模型列表失败:', err)
|
||||
RequestService.reAjaxFun(() => {
|
||||
this.getModelList(params, callback)
|
||||
})
|
||||
}).send()
|
||||
},
|
||||
// 获取模型供应器列表
|
||||
getModelProviders(modelType, callback) {
|
||||
RequestService.sendRequest()
|
||||
.url(`${getServiceUrl()}/models/${modelType}/provideTypes`)
|
||||
.method('GET')
|
||||
.success((res) => {
|
||||
RequestService.clearRequestTime()
|
||||
callback(res.data?.data || [])
|
||||
})
|
||||
.fail((err) => {
|
||||
console.error('获取供应器列表失败:', err)
|
||||
this.$message.error('获取供应器列表失败')
|
||||
RequestService.reAjaxFun(() => {
|
||||
this.getModelProviders(modelType, callback)
|
||||
})
|
||||
}).send()
|
||||
},
|
||||
|
||||
// 新增模型配置
|
||||
addModel(params, callback) {
|
||||
const { modelType, provideCode, formData } = params;
|
||||
const postData = {
|
||||
modelCode: formData.modelCode,
|
||||
modelName: formData.modelName,
|
||||
isDefault: formData.isDefault ? 1 : 0,
|
||||
isEnabled: formData.isEnabled ? 1 : 0,
|
||||
configJson: formData.configJson,
|
||||
docLink: formData.docLink,
|
||||
remark: formData.remark,
|
||||
sort: formData.sort || 0
|
||||
};
|
||||
|
||||
RequestService.sendRequest()
|
||||
.url(`${getServiceUrl()}/models/${modelType}/${provideCode}`)
|
||||
.method('POST')
|
||||
.data(postData)
|
||||
.success((res) => {
|
||||
RequestService.clearRequestTime()
|
||||
callback(res)
|
||||
})
|
||||
.fail((err) => {
|
||||
console.error('新增模型失败:', err)
|
||||
this.$message.error(err.msg || '新增模型失败')
|
||||
RequestService.reAjaxFun(() => {
|
||||
this.addModel(params, callback)
|
||||
})
|
||||
}).send()
|
||||
},
|
||||
// 删除模型配置
|
||||
deleteModel(id, callback) {
|
||||
RequestService.sendRequest()
|
||||
.url(`${getServiceUrl()}/models/${id}`)
|
||||
.method('DELETE')
|
||||
.success((res) => {
|
||||
RequestService.clearRequestTime()
|
||||
callback(res)
|
||||
})
|
||||
.fail((err) => {
|
||||
console.error('删除模型失败:', err)
|
||||
this.$message.error(err.msg || '删除模型失败')
|
||||
RequestService.reAjaxFun(() => {
|
||||
this.deleteModel(id, callback)
|
||||
})
|
||||
}).send()
|
||||
},
|
||||
// 获取模型名称列表
|
||||
getModelNames(modelType, modelName, callback) {
|
||||
RequestService.sendRequest()
|
||||
.url(`${getServiceUrl()}/models/names`)
|
||||
.method('GET')
|
||||
.data({ modelType, modelName })
|
||||
.success((res) => {
|
||||
RequestService.clearRequestTime();
|
||||
callback(res);
|
||||
})
|
||||
.fail(() => {
|
||||
RequestService.reAjaxFun(() => {
|
||||
this.getModelNames(modelType, modelName, callback);
|
||||
});
|
||||
}).send();
|
||||
},
|
||||
// 获取模型音色列表
|
||||
getModelVoices(modelId, voiceName, callback) {
|
||||
const queryParams = new URLSearchParams({
|
||||
voiceName: voiceName || ''
|
||||
}).toString();
|
||||
RequestService.sendRequest()
|
||||
.url(`${getServiceUrl()}/models/${modelId}/voices?${queryParams}`)
|
||||
.method('GET')
|
||||
.success((res) => {
|
||||
RequestService.clearRequestTime();
|
||||
callback(res);
|
||||
})
|
||||
.fail(() => {
|
||||
RequestService.reAjaxFun(() => {
|
||||
this.getModelVoices(modelId, voiceName, callback);
|
||||
});
|
||||
}).send();
|
||||
},
|
||||
// 获取单个模型配置
|
||||
getModelConfig(id, callback) {
|
||||
RequestService.sendRequest()
|
||||
.url(`${getServiceUrl()}/models/${id}`)
|
||||
.method('GET')
|
||||
.success((res) => {
|
||||
RequestService.clearRequestTime()
|
||||
callback(res)
|
||||
})
|
||||
.fail((err) => {
|
||||
console.error('获取模型配置失败:', err)
|
||||
this.$message.error(err.msg || '获取模型配置失败')
|
||||
RequestService.reAjaxFun(() => {
|
||||
this.getModelConfig(id, callback)
|
||||
})
|
||||
}).send()
|
||||
},
|
||||
// 启用/禁用模型状态
|
||||
updateModelStatus(id, status, callback) {
|
||||
RequestService.sendRequest()
|
||||
.url(`${getServiceUrl()}/api/v1/models/models/list?${queryParams}`)
|
||||
.method('GET')
|
||||
.url(`${getServiceUrl()}/models/enable/${id}/${status}`)
|
||||
.method('PUT')
|
||||
.success((res) => {
|
||||
RequestService.clearRequestTime()
|
||||
callback(res)
|
||||
RequestService.clearRequestTime()
|
||||
callback(res)
|
||||
})
|
||||
.fail((err) => {
|
||||
console.error('获取模型列表失败:', err)
|
||||
RequestService.reAjaxFun(() => {
|
||||
this.getModelList(params, callback)
|
||||
})
|
||||
console.error('更新模型状态失败:', err)
|
||||
this.$message.error(err.msg || '更新模型状态失败')
|
||||
RequestService.reAjaxFun(() => {
|
||||
this.updateModelStatus(id, status, callback)
|
||||
})
|
||||
}).send()
|
||||
},
|
||||
|
||||
// 更新模型配置
|
||||
updateModel(params, callback) {
|
||||
const { modelType, provideCode, id, formData } = params;
|
||||
RequestService.sendRequest()
|
||||
.url(`${getServiceUrl()}/models/${modelType}/${provideCode}/${id}`)
|
||||
.method('PUT')
|
||||
.data(formData)
|
||||
.success((res) => {
|
||||
RequestService.clearRequestTime();
|
||||
callback(res);
|
||||
})
|
||||
.fail((err) => {
|
||||
console.error('更新模型失败:', err);
|
||||
this.$message.error(err.msg || '更新模型失败');
|
||||
RequestService.reAjaxFun(() => {
|
||||
this.updateModel(params, callback);
|
||||
});
|
||||
}).send();
|
||||
},
|
||||
}
|
||||
|
||||
@@ -0,0 +1,102 @@
|
||||
// timbre.js
|
||||
import {getServiceUrl} from '../api';
|
||||
import RequestService from '../httpRequest';
|
||||
|
||||
export default {
|
||||
/**
|
||||
* 获取音色数据
|
||||
*/
|
||||
getVoiceList(params, callback) {
|
||||
const queryParams = new URLSearchParams({
|
||||
ttsModelId: params.ttsModelId,
|
||||
page: params.page || 1,
|
||||
limit: params.limit || 10,
|
||||
name: params.name || ''
|
||||
}).toString();
|
||||
|
||||
RequestService.sendRequest()
|
||||
.url(`${getServiceUrl()}/ttsVoice?${queryParams}`)
|
||||
.method('GET')
|
||||
.success((res) => {
|
||||
RequestService.clearRequestTime();
|
||||
callback(res.data || []);
|
||||
})
|
||||
.fail((err) => {
|
||||
console.error('获取音色列表失败:', err);
|
||||
RequestService.reAjaxFun(() => {
|
||||
this.getVoiceList(params, callback);
|
||||
});
|
||||
}).send();
|
||||
},
|
||||
/**
|
||||
* 音色保存
|
||||
*/
|
||||
saveVoice(params, callback) {
|
||||
RequestService.sendRequest()
|
||||
.url(`${getServiceUrl()}/ttsVoice`)
|
||||
.method('POST')
|
||||
.data(JSON.stringify({
|
||||
languages: params.languageType,
|
||||
name: params.voiceName,
|
||||
remark: params.remark,
|
||||
sort: params.sort,
|
||||
ttsModelId: params.ttsModelId,
|
||||
ttsVoice: params.voiceCode,
|
||||
voiceDemo: params.voiceDemo || ''
|
||||
}))
|
||||
.success((res) => {
|
||||
callback(res.data);
|
||||
})
|
||||
.fail((err) => {
|
||||
console.error('保存音色失败:', err);
|
||||
RequestService.reAjaxFun(() => {
|
||||
this.saveVoice(params, callback);
|
||||
});
|
||||
}).send();
|
||||
},
|
||||
/**
|
||||
* 音色删除
|
||||
*/
|
||||
deleteVoice(id, callback) {
|
||||
console.log('尝试删除音色,ID:', id);
|
||||
RequestService.sendRequest()
|
||||
.url(`${getServiceUrl()}/ttsVoice`)
|
||||
.method('DELETE')
|
||||
.data({ ids: [id] })
|
||||
.success((res) => {
|
||||
RequestService.clearRequestTime()
|
||||
callback(res);
|
||||
})
|
||||
.fail((err) => {
|
||||
console.error('删除音色失败:', err);
|
||||
RequestService.reAjaxFun(() => {
|
||||
this.deleteVoice(id, callback);
|
||||
});
|
||||
}).send();
|
||||
},
|
||||
/**
|
||||
* 音色修改
|
||||
*/
|
||||
updateVoice(params, callback) {
|
||||
RequestService.sendRequest()
|
||||
.url(`${getServiceUrl()}/ttsVoice/${params.id}`)
|
||||
.method('PUT')
|
||||
.data(JSON.stringify({
|
||||
languages: params.languageType,
|
||||
name: params.voiceName,
|
||||
remark: params.remark,
|
||||
ttsModelId: params.ttsModelId,
|
||||
ttsVoice: params.voiceCode,
|
||||
voiceDemo: params.voiceDemo || ''
|
||||
}))
|
||||
.success((res) => {
|
||||
callback(res.data);
|
||||
})
|
||||
.fail((err) => {
|
||||
console.error('修改音色失败:', err);
|
||||
RequestService.reAjaxFun(() => {
|
||||
this.updateVoice(params, callback);
|
||||
});
|
||||
}).send();
|
||||
}
|
||||
}
|
||||
@@ -1,12 +1,12 @@
|
||||
import { getServiceUrl } from '../api'
|
||||
import RequestService from '../httpRequest'
|
||||
import {getServiceUrl} from '../api'
|
||||
|
||||
|
||||
export default {
|
||||
// 登录
|
||||
login(loginForm, callback) {
|
||||
RequestService.sendRequest()
|
||||
.url(`${getServiceUrl()}/api/v1/user/login`)
|
||||
.url(`${getServiceUrl()}/user/login`)
|
||||
.method('POST')
|
||||
.data(loginForm)
|
||||
.success((res) => {
|
||||
@@ -22,7 +22,7 @@ export default {
|
||||
// 获取验证码
|
||||
getCaptcha(uuid, callback) {
|
||||
RequestService.sendRequest()
|
||||
.url(`${getServiceUrl()}/api/v1/user/captcha?uuid=${uuid}`)
|
||||
.url(`${getServiceUrl()}/user/captcha?uuid=${uuid}`)
|
||||
.method('GET')
|
||||
.type('blob')
|
||||
.header({
|
||||
@@ -41,7 +41,7 @@ export default {
|
||||
// 注册账号
|
||||
register(registerForm, callback) {
|
||||
RequestService.sendRequest()
|
||||
.url(`${getServiceUrl()}/api/v1/user/register`)
|
||||
.url(`${getServiceUrl()}/user/register`)
|
||||
.method('POST')
|
||||
.data(registerForm)
|
||||
.success((res) => {
|
||||
@@ -54,7 +54,7 @@ export default {
|
||||
// 保存设备配置
|
||||
saveDeviceConfig(device_id, configData, callback) {
|
||||
RequestService.sendRequest()
|
||||
.url(`${getServiceUrl()}/api/v1/user/configDevice/${device_id}`)
|
||||
.url(`${getServiceUrl()}/user/configDevice/${device_id}`)
|
||||
.method('PUT')
|
||||
.data(configData)
|
||||
.success((res) => {
|
||||
@@ -71,7 +71,7 @@ export default {
|
||||
// 用户信息获取
|
||||
getUserInfo(callback) {
|
||||
RequestService.sendRequest()
|
||||
.url(`${getServiceUrl()}/api/v1/user/info`)
|
||||
.url(`${getServiceUrl()}/user/info`)
|
||||
.method('GET')
|
||||
.success((res) => {
|
||||
RequestService.clearRequestTime()
|
||||
@@ -87,7 +87,7 @@ export default {
|
||||
// 修改用户密码
|
||||
changePassword(oldPassword, newPassword, successCallback, errorCallback) {
|
||||
RequestService.sendRequest()
|
||||
.url(`${getServiceUrl()}/api/v1/user/change-password`)
|
||||
.url(`${getServiceUrl()}/user/change-password`)
|
||||
.method('PUT')
|
||||
.data({
|
||||
password: oldPassword,
|
||||
@@ -104,4 +104,21 @@ export default {
|
||||
})
|
||||
.send();
|
||||
},
|
||||
// 修改用户状态
|
||||
changeUserStatus(status, userIds, successCallback) {
|
||||
return RequestService.sendRequest()
|
||||
.url(`${getServiceUrl()}/admin/users/changeStatus/${status}`)
|
||||
.method('put')
|
||||
.data(userIds)
|
||||
.success((res) => {
|
||||
RequestService.clearRequestTime()
|
||||
successCallback(res);
|
||||
})
|
||||
.fail((err) => {
|
||||
console.error('修改用户状态失败:', err)
|
||||
RequestService.reAjaxFun(() => {
|
||||
this.changeUserStatus(status, userIds)
|
||||
})
|
||||
}).send()
|
||||
},
|
||||
}
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 6.7 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 1.1 MiB |
Binary file not shown.
|
Before Width: | Height: | Size: 97 KiB After Width: | Height: | Size: 102 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 161 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 6.7 KiB |
@@ -1,98 +0,0 @@
|
||||
<template>
|
||||
<el-dialog :visible.sync="visible" width="400px" 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 src="@/assets/home/equipment.png" alt="" style="width: 18px;height: 15px;" />
|
||||
</div>
|
||||
激活设备
|
||||
</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> 设备激活码:
|
||||
</div>
|
||||
<div class="input-46" style="margin-top: 12px;">
|
||||
<el-input placeholder="请输入设备播报的激活码.." v-model="activateCode" />
|
||||
</div>
|
||||
</div>
|
||||
<div style="display: flex;margin: 15px 15px;gap: 7px;">
|
||||
<div class="dialog-btn" @click="confirm">
|
||||
确定
|
||||
</div>
|
||||
<div class="dialog-btn"
|
||||
style="background: #e6ebff;border: 1px solid #adbdff;color: #5778ff;"
|
||||
@click="cancel">
|
||||
取消
|
||||
</div>
|
||||
</div>
|
||||
</el-dialog>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import userApi from '@/apis/module/user';
|
||||
|
||||
|
||||
export default {
|
||||
name: 'ActivateDeviceDialog',
|
||||
props: {
|
||||
visible: { type: Boolean, required: true }
|
||||
},
|
||||
data() {
|
||||
return { activateCode: "" }
|
||||
},
|
||||
methods: {
|
||||
confirm() {
|
||||
if (!this.activateCode.trim()) {
|
||||
this.$message.error('请输入激活码');
|
||||
return;
|
||||
}
|
||||
userApi.activateDevice(this.activateCode, (res) => {
|
||||
this.$message.success('激活成功,请重新唤醒设备即可完成激活!');
|
||||
this.$emit('confirm', res);
|
||||
this.$emit('update:visible', false);
|
||||
console.log("res: ", res)
|
||||
this.activateCode = "";
|
||||
});
|
||||
},
|
||||
cancel() {
|
||||
this.$emit('update:visible', false)
|
||||
this.activateCode = ""
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
||||
.input-46 {
|
||||
border: 1px solid #e4e6ef;
|
||||
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,9 @@
|
||||
<template>
|
||||
<el-dialog :visible.sync="visible" width="400px" 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;">
|
||||
<el-dialog :visible.sync="visible" width="400px" 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 src="@/assets/home/equipment.png" alt="" style="width: 18px;height: 15px;" />
|
||||
</div>
|
||||
添加设备
|
||||
@@ -13,16 +15,14 @@
|
||||
<span style="font-size: 11px"> 验证码:</span>
|
||||
</div>
|
||||
<div class="input-46" style="margin-top: 12px;">
|
||||
<el-input placeholder="请输入设备播报的6位数验证码.." v-model="deviceCode" />
|
||||
<el-input placeholder="请输入设备播报的6位数验证码.." v-model="deviceCode" @keyup.enter.native="confirm" />
|
||||
</div>
|
||||
</div>
|
||||
<div style="display: flex;margin: 15px 15px;gap: 7px;">
|
||||
<div class="dialog-btn" @click="confirm">
|
||||
确定
|
||||
</div>
|
||||
<div class="dialog-btn"
|
||||
style="background: #e6ebff;border: 1px solid #adbdff;color: #5778ff;"
|
||||
@click="cancel">
|
||||
<div class="dialog-btn" style="background: #e6ebff;border: 1px solid #adbdff;color: #5778ff;" @click="cancel">
|
||||
取消
|
||||
</div>
|
||||
</div>
|
||||
@@ -30,6 +30,8 @@
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import Api from '@/apis/api';
|
||||
|
||||
export default {
|
||||
name: 'AddDeviceDialog',
|
||||
props: {
|
||||
@@ -49,31 +51,25 @@ export default {
|
||||
return;
|
||||
}
|
||||
this.loading = true;
|
||||
import('@/apis/module/device').then(({ default: deviceApi }) => {
|
||||
deviceApi.bindDevice(
|
||||
this.agentId,
|
||||
this.deviceCode, ({data}) => {
|
||||
this.loading = false;
|
||||
if (data.code === 0) {
|
||||
this.$emit('refresh');
|
||||
this.$message.success({
|
||||
message: '设备绑定成功',
|
||||
showClose: true
|
||||
});
|
||||
this.closeDialog();
|
||||
} else {
|
||||
this.$message.error({
|
||||
message: data.msg || '绑定失败',
|
||||
showClose: true
|
||||
});
|
||||
}
|
||||
}
|
||||
);
|
||||
}).catch((err) => {
|
||||
Api.device.bindDevice(
|
||||
this.agentId,
|
||||
this.deviceCode, ({ data }) => {
|
||||
this.loading = false;
|
||||
console.error('API模块加载失败:', err);
|
||||
this.$message.error('绑定服务不可用');
|
||||
});
|
||||
if (data.code === 0) {
|
||||
this.$emit('refresh');
|
||||
this.$message.success({
|
||||
message: '设备绑定成功',
|
||||
showClose: true
|
||||
});
|
||||
this.closeDialog();
|
||||
} else {
|
||||
this.$message.error({
|
||||
message: data.msg || '绑定失败',
|
||||
showClose: true
|
||||
});
|
||||
}
|
||||
}
|
||||
);
|
||||
},
|
||||
closeDialog() {
|
||||
this.$emit('update:visible', false);
|
||||
@@ -89,7 +85,6 @@ export default {
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
||||
.input-46 {
|
||||
border: 1px solid #e4e6ef;
|
||||
background: #f6f8fb;
|
||||
@@ -113,14 +108,16 @@ export default {
|
||||
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{
|
||||
|
||||
::v-deep .el-dialog__header {
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
</style>
|
||||
@@ -1,19 +1,12 @@
|
||||
<template>
|
||||
<el-dialog
|
||||
:visible.sync="visible"
|
||||
width="975px"
|
||||
center
|
||||
custom-class="custom-dialog"
|
||||
:show-close="false"
|
||||
class="center-dialog"
|
||||
|
||||
>
|
||||
<el-dialog :visible="dialogVisible" @update:visible="handleVisibleChange" width="975px" 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;">
|
||||
添加模型
|
||||
</div>
|
||||
|
||||
<!-- 关闭按钮 -->
|
||||
|
||||
<button class="custom-close-btn" @click="handleClose">
|
||||
×
|
||||
</button>
|
||||
@@ -34,113 +27,66 @@
|
||||
</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;">
|
||||
<el-form-item label="模型名称" prop="modelName" style="flex: 1;">
|
||||
<el-input
|
||||
v-model="formData.modelName"
|
||||
placeholder="请输入模型名称"
|
||||
class="custom-input-bg"
|
||||
></el-input>
|
||||
<el-input v-model="formData.modelName" placeholder="请输入模型名称" class="custom-input-bg"></el-input>
|
||||
</el-form-item>
|
||||
<el-form-item label="模型编码" prop="modelCode" style="flex: 1;">
|
||||
<el-input
|
||||
v-model="formData.modelCode"
|
||||
placeholder="请输入模型编码"
|
||||
class="custom-input-bg"
|
||||
></el-input>
|
||||
<el-input v-model="formData.modelCode" placeholder="请输入模型编码" class="custom-input-bg"></el-input>
|
||||
</el-form-item>
|
||||
</div>
|
||||
|
||||
<!-- 第二行:供应器和排序号 -->
|
||||
<div style="display: flex; gap: 20px; margin-bottom: 0;">
|
||||
<el-form-item label="供应器" prop="supplier" style="flex: 1;">
|
||||
<el-select
|
||||
v-model="formData.supplier"
|
||||
placeholder="请选择"
|
||||
class="custom-select custom-input-bg"
|
||||
style="width: 100%;"
|
||||
>
|
||||
<el-option label="硅基流动" value="硅基流动"></el-option>
|
||||
<el-option label="智脑科技" value="智脑科技"></el-option>
|
||||
<el-option label="云智科技" value="云智科技"></el-option>
|
||||
<el-option label="其他" value="其他"></el-option>
|
||||
<el-select v-model="formData.supplier" placeholder="请选择" 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="排序号" prop="sortOrder" style="flex: 1;">
|
||||
<el-input
|
||||
v-model="formData.sort"
|
||||
placeholder="请输入排序号"
|
||||
class="custom-input-bg"
|
||||
></el-input>
|
||||
<el-input v-model="formData.sort" placeholder="请输入排序号" class="custom-input-bg"></el-input>
|
||||
</el-form-item>
|
||||
</div>
|
||||
|
||||
<!-- 文档地址 -->
|
||||
|
||||
<el-form-item label="文档地址" prop="docLink" style="margin-bottom: 27px;">
|
||||
<el-input
|
||||
v-model="formData.docLink"
|
||||
placeholder="请输入文档地址"
|
||||
class="custom-input-bg"
|
||||
></el-input>
|
||||
<el-input v-model="formData.docLink" placeholder="请输入文档地址" class="custom-input-bg"></el-input>
|
||||
</el-form-item>
|
||||
|
||||
<!-- 备注 -->
|
||||
<el-form-item label="备注" prop="remark" class="prop-remark">
|
||||
<el-input
|
||||
v-model="formData.remark"
|
||||
type="textarea"
|
||||
:rows="3"
|
||||
placeholder="请输入模型备注"
|
||||
class="custom-input-bg"
|
||||
></el-input>
|
||||
<el-input v-model="formData.remark" type="textarea" :rows="3" placeholder="请输入模型备注"
|
||||
class="custom-input-bg"></el-input>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
|
||||
<!-- 调用信息部分 -->
|
||||
<div style="font-size: 20px; font-weight: bold; color: #3d4566; margin-bottom: 15px;">调用信息</div>
|
||||
<div style="height: 2px; background: #e9e9e9; margin-bottom: 15px;"></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: 10px; margin-bottom: 15px;">
|
||||
<el-form-item label="模型名称" prop="param1" style="flex: 0.5; margin-bottom: 0;">
|
||||
<el-input
|
||||
v-model="formData.configJson.param1"
|
||||
placeholder="请输入model_name"
|
||||
<el-form :model="formData.configJson" label-width="auto" label-position="left" 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;">
|
||||
<el-input
|
||||
v-model="formData.configJson[field.prop]"
|
||||
:placeholder="field.placeholder"
|
||||
:type="field.type || 'text'"
|
||||
class="custom-input-bg"
|
||||
></el-input>
|
||||
</el-form-item>
|
||||
<el-form-item label="接口地址" prop="param2" style="flex: 1; margin-bottom: 0;">
|
||||
<el-input
|
||||
v-model="formData.configJson.param2"
|
||||
placeholder="请输入base_url"
|
||||
class="custom-input-bg"
|
||||
></el-input>
|
||||
</el-form-item>
|
||||
</div>
|
||||
|
||||
<!-- 秘钥信息 -->
|
||||
<el-form-item label="秘钥信息" prop="apiKey">
|
||||
<el-input
|
||||
v-model="formData.configJson.apiKey"
|
||||
placeholder="请输入api_key"
|
||||
show-password
|
||||
class="custom-input-bg"
|
||||
></el-input>
|
||||
</el-form-item>
|
||||
:show-password="field.type === 'password'">
|
||||
</el-input>
|
||||
</el-form-item>
|
||||
</div>
|
||||
</template>
|
||||
</el-form>
|
||||
</div>
|
||||
|
||||
<!-- 保存按钮 -->
|
||||
<div style="display: flex;justify-content: center;">
|
||||
<el-button
|
||||
type="primary"
|
||||
@click="confirm"
|
||||
class="save-btn"
|
||||
>
|
||||
<el-button type="primary" @click="confirm" class="save-btn">
|
||||
保存
|
||||
</el-button>
|
||||
</div>
|
||||
@@ -148,14 +94,20 @@
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import Api from '@/apis/api';
|
||||
export default {
|
||||
name: 'AddModelDialog',
|
||||
props: {
|
||||
visible: {type: Boolean, required: true},
|
||||
modelType: {type: String, required: true}
|
||||
visible: { type: Boolean, required: true },
|
||||
modelType: { type: String, required: true }
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
providers: [],
|
||||
dialogVisible: false,
|
||||
providersLoaded: false,
|
||||
providerFields: [],
|
||||
currentProvider: null,
|
||||
formData: {
|
||||
modelName: '',
|
||||
modelCode: '',
|
||||
@@ -165,26 +117,105 @@ export default {
|
||||
remark: '',
|
||||
isEnabled: true,
|
||||
isDefault: true,
|
||||
configJson: {
|
||||
param1: '',
|
||||
param2: '',
|
||||
apiKey: ''
|
||||
}
|
||||
configJson: {}
|
||||
}
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
visible(val) {
|
||||
this.dialogVisible = val;
|
||||
if(val) {
|
||||
this.initConfigJson();
|
||||
} else {
|
||||
this.resetForm();
|
||||
}
|
||||
},
|
||||
'formData.supplier'(newVal) {
|
||||
this.currentProvider = this.providers.find(p => p.value === newVal);
|
||||
this.providerFields = this.currentProvider?.fields || [];
|
||||
this.initDynamicConfig();
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
dynamicCallInfoFields() {
|
||||
return this.providerFields;
|
||||
},
|
||||
chunkedCallInfoFields() {
|
||||
const chunkSize = 2;
|
||||
const result = [];
|
||||
for (let i = 0; i < this.dynamicCallInfoFields.length; i += chunkSize) {
|
||||
result.push(this.dynamicCallInfoFields.slice(i, i + chunkSize));
|
||||
}
|
||||
return result;
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
loadProviders() {
|
||||
if (this.providersLoaded)
|
||||
return
|
||||
|
||||
Api.model.getModelProviders(this.modelType, (data) => {
|
||||
this.providers = data.map(item => ({
|
||||
label: item.name,
|
||||
value: item.providerCode,
|
||||
fields: JSON.parse(item.fields || '[]').map(f => ({
|
||||
label: f.label,
|
||||
prop: f.key,
|
||||
type: f.type === 'password' ? 'password' : 'text',
|
||||
placeholder: `请输入${f.label}`
|
||||
}))
|
||||
}))
|
||||
this.providersLoaded = true
|
||||
})
|
||||
},
|
||||
initConfigJson() {
|
||||
const defaultConfig = {};
|
||||
this.providerFields.forEach(field => {
|
||||
defaultConfig[field.prop] = '';
|
||||
});
|
||||
this.formData.configJson = { ...defaultConfig };
|
||||
},
|
||||
handleVisibleChange(val) {
|
||||
this.dialogVisible = val;
|
||||
this.$emit('update:visible', val);
|
||||
if (!val) {
|
||||
this.resetForm();
|
||||
}
|
||||
},
|
||||
|
||||
handleClose() {
|
||||
this.$emit('update:visible', false);
|
||||
},
|
||||
initDynamicConfig() {
|
||||
const newConfig = {};
|
||||
this.providerFields.forEach(field => {
|
||||
newConfig[field.prop] = this.formData.configJson[field.prop] || '';
|
||||
});
|
||||
this.formData.configJson = newConfig;
|
||||
},
|
||||
confirm() {
|
||||
if (!this.formData.modelName || !this.formData.modelCode || !this.formData.supplier ||
|
||||
!this.formData.configJson.param1 || !this.formData.configJson.param2 || !this.formData.configJson.apiKey) {
|
||||
this.$message.error('请填写所有必填字段');
|
||||
if (!this.formData.supplier) {
|
||||
this.$message.error('请选择供应器');
|
||||
return;
|
||||
}
|
||||
|
||||
this.$emit('confirm', {
|
||||
...this.formData,
|
||||
provideType: this.formData.supplier
|
||||
});
|
||||
const submitData = {
|
||||
modelName: this.formData.modelName || '',
|
||||
modelCode: this.formData.modelCode || '',
|
||||
supplier: this.formData.supplier,
|
||||
sort: this.formData.sort || 1,
|
||||
docLink: this.formData.docLink || '',
|
||||
remark: this.formData.remark || '',
|
||||
isEnabled: this.formData.isEnabled ? 1 : 0,
|
||||
isDefault: this.formData.isDefault ? 1 : 0,
|
||||
provideCode: this.formData.supplier,
|
||||
configJson: {
|
||||
...this.formData.configJson,
|
||||
type: this.formData.supplier
|
||||
}
|
||||
};
|
||||
|
||||
this.$emit('confirm', submitData);
|
||||
this.$emit('update:visible', false);
|
||||
this.resetForm();
|
||||
},
|
||||
@@ -198,17 +229,15 @@ export default {
|
||||
remark: '',
|
||||
isEnabled: true,
|
||||
isDefault: true,
|
||||
configJson: {
|
||||
param1: '',
|
||||
param2: '',
|
||||
apiKey: ''
|
||||
}
|
||||
configJson: {}
|
||||
};
|
||||
// 重置加载状态
|
||||
this.providers = [];
|
||||
this.providersLoaded = false;
|
||||
// 重置字段配置
|
||||
this.providerFields = [];
|
||||
this.currentProvider = null;
|
||||
},
|
||||
handleClose() {
|
||||
this.resetForm();
|
||||
this.$emit('update:visible', false);
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@@ -297,7 +326,7 @@ export default {
|
||||
}
|
||||
|
||||
.custom-form .el-form-item {
|
||||
margin-bottom: 20px; /* 统一设置所有表单项的间距 */
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.custom-form .el-form-item__label {
|
||||
@@ -312,13 +341,12 @@ export default {
|
||||
margin-top: -4px;
|
||||
}
|
||||
|
||||
/* 修改placeholder颜色 */
|
||||
.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;
|
||||
@@ -342,13 +370,12 @@ export default {
|
||||
}
|
||||
|
||||
|
||||
/* 修改开关样式 */
|
||||
.custom-switch .el-switch__core {
|
||||
border-radius: 20px;
|
||||
height: 23px;
|
||||
background-color: #c0ccda;
|
||||
width: 35px;
|
||||
padding: 0 20px; /* 调整左右内边距 */
|
||||
padding: 0 20px;
|
||||
}
|
||||
|
||||
.custom-switch .el-switch__core:after {
|
||||
@@ -363,7 +390,7 @@ export default {
|
||||
.custom-switch.is-checked .el-switch__core {
|
||||
border-color: #b5bcf0;
|
||||
background-color: #cfd7fa;
|
||||
padding: 0 20px; /* 确保启用状态也有相同的间隔 */
|
||||
padding: 0 20px;
|
||||
}
|
||||
|
||||
.custom-switch.is-checked .el-switch__core:after {
|
||||
@@ -373,15 +400,11 @@ export default {
|
||||
}
|
||||
|
||||
|
||||
/* 调整flex布局的gap */
|
||||
[style*="display: flex"] {
|
||||
gap: 20px; /* 扩大flex项间距 */
|
||||
gap: 20px;
|
||||
}
|
||||
|
||||
/* 调整输入框高度 */
|
||||
.custom-input-bg .el-input__inner {
|
||||
height: 32px; /* 固定输入框高度 */
|
||||
height: 32px;
|
||||
}
|
||||
|
||||
|
||||
</style>
|
||||
@@ -1,7 +1,9 @@
|
||||
<template>
|
||||
<el-dialog :visible.sync="visible" width="400px" 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;">
|
||||
<el-dialog :visible.sync="visible" width="400px" center @open="handleOpen">
|
||||
<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/home/equipment.png" alt="" style="width: 18px;height: 15px;" />
|
||||
</div>
|
||||
添加智能体
|
||||
@@ -12,16 +14,14 @@
|
||||
<div style="color: red;display: inline-block;">*</div> 智慧体名称:
|
||||
</div>
|
||||
<div class="input-46" style="margin-top: 12px;">
|
||||
<el-input placeholder="请输入智能体名称.." v-model="wisdomBodyName" />
|
||||
<el-input ref="inputRef" placeholder="请输入智能体名称.." v-model="wisdomBodyName" @keyup.enter.native="confirm" />
|
||||
</div>
|
||||
</div>
|
||||
<div style="display: flex;margin: 15px 15px;gap: 7px;">
|
||||
<div class="dialog-btn" @click="confirm">
|
||||
确定
|
||||
</div>
|
||||
<div class="dialog-btn"
|
||||
style="background: #e6ebff;border: 1px solid #adbdff;color: #5778ff;"
|
||||
@click="cancel">
|
||||
<div class="dialog-btn" style="background: #e6ebff;border: 1px solid #adbdff;color: #5778ff;" @click="cancel">
|
||||
取消
|
||||
</div>
|
||||
</div>
|
||||
@@ -29,8 +29,7 @@
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import userApi from '@/apis/module/agent';
|
||||
|
||||
import Api from '@/apis/api';
|
||||
|
||||
export default {
|
||||
name: 'AddWisdomBodyDialog',
|
||||
@@ -38,18 +37,26 @@ export default {
|
||||
visible: { type: Boolean, required: true }
|
||||
},
|
||||
data() {
|
||||
return { wisdomBodyName: "" }
|
||||
return {
|
||||
wisdomBodyName: "",
|
||||
inputRef: null
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
handleOpen() {
|
||||
this.$nextTick(() => {
|
||||
this.$refs.inputRef.focus();
|
||||
});
|
||||
},
|
||||
confirm() {
|
||||
if (!this.wisdomBodyName.trim()) {
|
||||
this.$message.error('请输入智能体名称');
|
||||
return;
|
||||
}
|
||||
userApi.addAgent(this.wisdomBodyName, (res) => {
|
||||
Api.agent.addAgent(this.wisdomBodyName, (res) => {
|
||||
this.$message.success({
|
||||
message: '添加成功',
|
||||
showClose: true
|
||||
message: '添加成功',
|
||||
showClose: true
|
||||
});
|
||||
this.$emit('confirm', res);
|
||||
this.$emit('update:visible', false);
|
||||
@@ -65,7 +72,6 @@ export default {
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
||||
.input-46 {
|
||||
border: 1px solid #e4e6ef;
|
||||
background: #f6f8fb;
|
||||
@@ -84,17 +90,21 @@ export default {
|
||||
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{
|
||||
|
||||
::v-deep .el-dialog__header {
|
||||
padding: 10px;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,379 @@
|
||||
<template>
|
||||
<div class="audio-container">
|
||||
<audio
|
||||
ref="audioRef"
|
||||
:src="audioUrl"
|
||||
@timeupdate="updateProgress"
|
||||
@loadedmetadata="updateDuration"
|
||||
style="display: none"
|
||||
></audio>
|
||||
|
||||
<div class="custom-controls">
|
||||
<!-- 播放/暂停按钮 -->
|
||||
<button class="play-btn" @click="togglePlay">
|
||||
<svg width="20" height="20" viewBox="0 0 20 20">
|
||||
<path
|
||||
fill="#4362b3"
|
||||
:d="isPlaying
|
||||
? 'M6 3h3v14H6zm5 0h3v14h-3z'
|
||||
: 'M5 3l12 7-12 7z'"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
|
||||
<!-- 时间显示 -->
|
||||
<span class="time-display">{{ formattedCurrentTime }}/{{ formattedDuration }}</span>
|
||||
|
||||
<!-- 进度条 -->
|
||||
<div class="progress-container">
|
||||
<div class="progress-bar" @click="handleProgressClick">
|
||||
<div
|
||||
class="progress-track"
|
||||
:style="{ width: progress + '%' }"
|
||||
></div>
|
||||
<div
|
||||
class="progress-thumb"
|
||||
:style="{ left: progress + '%' }"
|
||||
@mousedown="startDrag"
|
||||
></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 音量控制 -->
|
||||
<div class="volume-control" ref="volumeControl">
|
||||
<button
|
||||
@click="toggleMute"
|
||||
@mouseenter="handleVolumeMouseEnter"
|
||||
@mouseleave="startVolumeSliderHideTimer"
|
||||
ref="volumeButton"
|
||||
class="volume-button"
|
||||
>
|
||||
<svg width="20" height="20" viewBox="0 0 24 24">
|
||||
<path fill="currentColor" :d="volumeIconPath"/>
|
||||
</svg>
|
||||
</button>
|
||||
<div
|
||||
v-show="showVolumeSlider"
|
||||
class="volume-slider-container"
|
||||
@mouseenter="cancelVolumeSliderHideTimer"
|
||||
@mouseleave="startVolumeSliderHideTimer"
|
||||
ref="volumeSlider"
|
||||
>
|
||||
<div class="volume-slider-track">
|
||||
<input
|
||||
type="range"
|
||||
v-model="volume"
|
||||
min="0"
|
||||
max="1"
|
||||
step="0.1"
|
||||
class="volume-slider"
|
||||
@input="handleVolumeChange"
|
||||
orient="vertical"
|
||||
>
|
||||
<div class="volume-slider-thumb" :style="{ bottom: volume * 100 + '%' }"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, computed, onMounted, onUnmounted, nextTick } from 'vue'
|
||||
|
||||
const props = defineProps({
|
||||
audioUrl: String
|
||||
})
|
||||
|
||||
// 音频元素引用
|
||||
const audioRef = ref(null)
|
||||
const volumeButton = ref(null)
|
||||
const volumeSlider = ref(null)
|
||||
|
||||
// 播放状态
|
||||
const isPlaying = ref(false)
|
||||
const currentTime = ref(0)
|
||||
const duration = ref(0)
|
||||
const progress = ref(0)
|
||||
|
||||
// 音量状态
|
||||
const volume = ref(0.7)
|
||||
const isMuted = ref(false)
|
||||
const showVolumeSlider = ref(false)
|
||||
let volumeSliderHideTimer = null
|
||||
|
||||
// 格式化时间为 MM:SS
|
||||
const formatTime = (seconds) => {
|
||||
const sec = Math.floor(seconds || 0)
|
||||
return `${Math.floor(sec / 60)}:${(sec % 60).toString().padStart(2, '0')}`
|
||||
}
|
||||
|
||||
const formattedCurrentTime = computed(() => formatTime(currentTime.value))
|
||||
const formattedDuration = computed(() => formatTime(duration.value))
|
||||
|
||||
// 根据音量状态返回对应的图标路径
|
||||
const volumeIconPath = computed(() => {
|
||||
if (isMuted.value || volume.value === 0) {
|
||||
return 'M16.5 12c0-1.77-1.02-3.29-2.5-4.03v2.21l2.45 2.45c.03-.2.05-.41.05-.63zm2.5 0c0 .94-.2 1.82-.54 2.64l1.51 1.51C20.63 14.91 21 13.5 21 12c0-4.28-2.99-7.86-7-8.77v2.06c2.89.86 5 3.54 5 6.71zM4.27 3L3 4.27 7.73 9H3v6h4l5 5v-6.73l4.25 4.25c-.67.52-1.42.93-2.25 1.18v2.06c1.38-.31 2.63-.95 3.69-1.81L19.73 21 21 19.73l-9-9L4.27 3zM12 4L9.91 6.09 12 8.18V4z'
|
||||
}
|
||||
return 'M3 9v6h4l5 5V4L7 9H3zm13.5 3c0-1.77-1.02-3.29-2.5-4.03v8.05c1.48-.73 2.5-2.25 2.5-4.02zM14 3.23v2.06c2.89.86 5 3.54 5 6.71s-2.11 5.85-5 6.71v2.06c4.01-.91 7-4.49 7-8.77s-2.99-7.86-7-8.77z'
|
||||
})
|
||||
|
||||
// 播放控制
|
||||
const togglePlay = () => {
|
||||
if (isPlaying.value) {
|
||||
audioRef.value.pause()
|
||||
} else {
|
||||
audioRef.value.play()
|
||||
}
|
||||
isPlaying.value = !isPlaying.value
|
||||
}
|
||||
|
||||
const updateDuration = () => {
|
||||
if (audioRef.value) {
|
||||
duration.value = audioRef.value.duration
|
||||
}
|
||||
}
|
||||
|
||||
const updateProgress = () => {
|
||||
if (audioRef.value?.duration) {
|
||||
currentTime.value = audioRef.value.currentTime
|
||||
progress.value = (currentTime.value / duration.value) * 100
|
||||
}
|
||||
}
|
||||
|
||||
const handleProgressClick = (e) => {
|
||||
const rect = e.currentTarget.getBoundingClientRect()
|
||||
const percentage = ((e.clientX - rect.left) / rect.width) * 100
|
||||
seekToPercentage(percentage)
|
||||
}
|
||||
|
||||
// 音量控制
|
||||
const toggleMute = () => {
|
||||
isMuted.value = !isMuted.value
|
||||
audioRef.value.muted = isMuted.value
|
||||
|
||||
// 如果取消静音且音量为0,则恢复默认音量
|
||||
if (!isMuted.value && volume.value === 0) {
|
||||
volume.value = 0.5
|
||||
audioRef.value.volume = 0.5
|
||||
}
|
||||
}
|
||||
|
||||
const handleVolumeChange = (e) => {
|
||||
const newVolume = parseFloat(e.target.value)
|
||||
audioRef.value.volume = newVolume
|
||||
isMuted.value = newVolume === 0
|
||||
}
|
||||
|
||||
// 进度条拖拽功能
|
||||
let isDragging = false
|
||||
|
||||
const startDrag = (e) => {
|
||||
isDragging = true
|
||||
document.addEventListener('mousemove', handleDrag)
|
||||
document.addEventListener('mouseup', stopDrag)
|
||||
e.preventDefault()
|
||||
}
|
||||
|
||||
const handleDrag = (e) => {
|
||||
if (!isDragging) return
|
||||
|
||||
const rect = document.querySelector('.progress-bar').getBoundingClientRect()
|
||||
const percentage = ((e.clientX - rect.left) / rect.width) * 100
|
||||
seekToPercentage(percentage)
|
||||
}
|
||||
|
||||
const stopDrag = () => {
|
||||
isDragging = false
|
||||
document.removeEventListener('mousemove', handleDrag)
|
||||
document.removeEventListener('mouseup', stopDrag)
|
||||
}
|
||||
|
||||
const seekToPercentage = (percentage) => {
|
||||
const clampedPercentage = Math.min(Math.max(percentage, 0), 100)
|
||||
progress.value = clampedPercentage
|
||||
audioRef.value.currentTime = (clampedPercentage / 100) * duration.value
|
||||
}
|
||||
|
||||
// 音量滑块显示/隐藏控制
|
||||
const startVolumeSliderHideTimer = () => {
|
||||
volumeSliderHideTimer = setTimeout(() => {
|
||||
showVolumeSlider.value = false
|
||||
}, 300)
|
||||
}
|
||||
|
||||
const cancelVolumeSliderHideTimer = () => {
|
||||
clearTimeout(volumeSliderHideTimer)
|
||||
}
|
||||
|
||||
// 音量滑块定位
|
||||
const updateSliderPosition = () => {
|
||||
nextTick(() => {
|
||||
if (!volumeButton.value || !volumeSlider.value) return
|
||||
|
||||
const buttonRect = volumeButton.value.getBoundingClientRect()
|
||||
const slider = volumeSlider.value
|
||||
|
||||
// 计算相对于视口的位置
|
||||
const left = buttonRect.left + window.scrollX
|
||||
const top = buttonRect.top + window.scrollY
|
||||
|
||||
// 定位到按钮正中间,垂直居中
|
||||
slider.style.left = `${left + 5}px`
|
||||
slider.style.top = `${top - 85}px`
|
||||
})
|
||||
}
|
||||
|
||||
const handleVolumeMouseEnter = () => {
|
||||
showVolumeSlider.value = true
|
||||
updateSliderPosition()
|
||||
}
|
||||
|
||||
// 初始化
|
||||
onMounted(() => {
|
||||
if (audioRef.value) {
|
||||
audioRef.value.volume = volume.value
|
||||
}
|
||||
window.addEventListener('resize', updateSliderPosition)
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
window.removeEventListener('resize', updateSliderPosition)
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.audio-container {
|
||||
background: #eef0fd;
|
||||
padding: 8px;
|
||||
height: 40px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
border-radius: 5px;
|
||||
}
|
||||
|
||||
.custom-controls {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.play-btn {
|
||||
background: none;
|
||||
border: none;
|
||||
padding: 0;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.time-display {
|
||||
font-size: 12px;
|
||||
color: #5f7ba7;
|
||||
min-width: 40px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.progress-container {
|
||||
flex: 1;
|
||||
padding: 0 10px;
|
||||
opacity: 0.7;
|
||||
transition: opacity 0.3s ease;
|
||||
}
|
||||
|
||||
.progress-container:hover {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.progress-bar {
|
||||
height: 2px;
|
||||
background: #bfcadb;
|
||||
position: relative;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.progress-track {
|
||||
position: absolute;
|
||||
height: 100%;
|
||||
background: #4167ed;
|
||||
}
|
||||
|
||||
.progress-thumb {
|
||||
position: absolute;
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
background: #4167ed;
|
||||
border: 2px solid #d6dcfc;
|
||||
border-radius: 50%;
|
||||
top: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
cursor: pointer;
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
.volume-control {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.volume-button {
|
||||
background: none;
|
||||
border: none;
|
||||
padding: 0;
|
||||
color: #8f95cd;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.volume-slider-container {
|
||||
position: fixed;
|
||||
padding: 10px 4px;
|
||||
background: #eef0fd;
|
||||
border-radius: 4px;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
||||
z-index: 1000;
|
||||
height: 75px;
|
||||
}
|
||||
|
||||
.volume-slider-track {
|
||||
position: relative;
|
||||
height: 55px;
|
||||
width: 2px;
|
||||
background: #bfcadb;
|
||||
margin: 1px;
|
||||
}
|
||||
|
||||
.volume-slider-thumb {
|
||||
position: absolute;
|
||||
left: 1px;
|
||||
width: 9px;
|
||||
height: 9px;
|
||||
background: #4167ed;
|
||||
border: 2px solid #d6dcfc;
|
||||
border-radius: 50%;
|
||||
transform: translateX(-50%);
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.volume-slider {
|
||||
position: absolute;
|
||||
left: -14px;
|
||||
width: 30px;
|
||||
height: 60px;
|
||||
writing-mode: vertical-lr;
|
||||
direction: rtl;
|
||||
opacity: 0;
|
||||
cursor: pointer;
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
.volume-slider-track::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
left: 0;
|
||||
bottom: 0;
|
||||
width: 2px;
|
||||
height: calc(100% * v-bind(volume));
|
||||
background: #4167ed;
|
||||
}
|
||||
</style>
|
||||
@@ -1,43 +1,44 @@
|
||||
<template>
|
||||
<form>
|
||||
<el-dialog :visible.sync="value" width="400px" 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
|
||||
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>
|
||||
修改密码
|
||||
</div>
|
||||
<div style="height: 1px;background: #e8f0ff;"/>
|
||||
<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>
|
||||
旧密码:
|
||||
</div>
|
||||
<div class="input-46" style="margin-top: 12px;">
|
||||
<el-input placeholder="请输入旧密码" v-model="oldPassword" type="password" show-password/>
|
||||
<el-input placeholder="请输入旧密码" 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>
|
||||
<div class="input-46" style="margin-top: 12px;">
|
||||
<el-input placeholder="请输入新密码" v-model="newPassword" type="password" show-password/>
|
||||
<el-input placeholder="请输入新密码" 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>
|
||||
<div class="input-46" style="margin-top: 12px;">
|
||||
<el-input placeholder="请再次输入新密码" v-model="confirmNewPassword" type="password" show-password/>
|
||||
<el-input placeholder="请再次输入新密码" v-model="confirmNewPassword" type="password" show-password />
|
||||
</div>
|
||||
</div>
|
||||
<div style="display: flex;margin: 15px 15px;gap: 7px;">
|
||||
<div class="dialog-btn" @click="confirm">
|
||||
确定
|
||||
</div>
|
||||
<div class="dialog-btn"
|
||||
style="background: #e6ebff;border: 1px solid #adbdff;color: #5778ff;"
|
||||
@click="cancel">
|
||||
<div class="dialog-btn" style="background: #e6ebff;border: 1px solid #adbdff;color: #5778ff;" @click="cancel">
|
||||
取消
|
||||
</div>
|
||||
</div>
|
||||
@@ -83,12 +84,15 @@ export default {
|
||||
// 修改后的接口调用
|
||||
userApi.changePassword(this.oldPassword, this.newPassword, (res) => {
|
||||
if (res.data.code === 0) {
|
||||
this.$message.success('密码修改成功,请重新登录');
|
||||
this.$message.success({
|
||||
message: '密码修改成功,请重新登录',
|
||||
showClose: true
|
||||
});
|
||||
this.logout().then(() => {
|
||||
this.$router.push('/login');
|
||||
this.$emit('update:visible', false);
|
||||
});
|
||||
}else{
|
||||
} else {
|
||||
this.$message.error(res.data.msg || '密码修改失败');
|
||||
}
|
||||
}, (err) => {
|
||||
|
||||
@@ -2,12 +2,15 @@
|
||||
<div class="device-item">
|
||||
<div style="display: flex;justify-content: space-between;">
|
||||
<div style="font-weight: 700;font-size: 18px;text-align: left;color: #3d4566;">
|
||||
{{ device.agentName }}
|
||||
{{ device.agentName }}
|
||||
</div>
|
||||
<div>
|
||||
<img src="@/assets/home/delete.png" alt=""
|
||||
style="width: 18px;height: 18px;margin-right: 10px;" @click.stop="handleDelete" />
|
||||
<img src="@/assets/home/info.png" alt="" style="width: 18px;height: 18px;" />
|
||||
<img src="@/assets/home/delete.png" alt="" style="width: 18px;height: 18px;margin-right: 10px;"
|
||||
@click.stop="handleDelete" />
|
||||
<el-tooltip class="item" effect="dark" :content="device.systemPrompt" placement="top"
|
||||
popper-class="custom-tooltip">
|
||||
<img src="@/assets/home/info.png" alt="" style="width: 18px;height: 18px;" />
|
||||
</el-tooltip>
|
||||
</div>
|
||||
</div>
|
||||
<div class="device-name">
|
||||
@@ -20,14 +23,14 @@
|
||||
<div class="settings-btn" @click="handleConfigure">
|
||||
配置角色
|
||||
</div>
|
||||
<!-- <div class="settings-btn">-->
|
||||
<!-- 声纹识别-->
|
||||
<!-- </div>-->
|
||||
<!-- <div class="settings-btn">-->
|
||||
<!-- 历史对话-->
|
||||
<!-- </div>-->
|
||||
<div class="settings-btn" @click="handleDeviceManage">
|
||||
设备管理
|
||||
<!-- <div class="settings-btn">-->
|
||||
<!-- 声纹识别-->
|
||||
<!-- </div>-->
|
||||
<!-- <div class="settings-btn">-->
|
||||
<!-- 历史对话-->
|
||||
<!-- </div>-->
|
||||
<div class="settings-btn" @click="handleDeviceManage">
|
||||
设备管理({{ device.deviceCount }})
|
||||
</div>
|
||||
</div>
|
||||
<div class="version-info">
|
||||
@@ -66,6 +69,7 @@ export default {
|
||||
padding: 22px;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.device-name {
|
||||
margin: 7px 0 10px;
|
||||
font-weight: 400;
|
||||
@@ -94,5 +98,11 @@ export default {
|
||||
color: #979db1;
|
||||
font-weight: 400;
|
||||
}
|
||||
</style>
|
||||
|
||||
<style>
|
||||
.custom-tooltip {
|
||||
max-width: 400px;
|
||||
word-break: break-word;
|
||||
}
|
||||
</style>
|
||||
@@ -2,62 +2,68 @@
|
||||
<el-header class="header">
|
||||
<div class="header-container">
|
||||
<!-- 左侧元素 -->
|
||||
<div class="header-left">
|
||||
<img loading="lazy" alt="" src="@/assets/xiaozhi-logo.png" class="logo-img"/>
|
||||
<img loading="lazy" alt="" src="@/assets/xiaozhi-ai.png" class="brand-img"/>
|
||||
<div class="header-left" @click="goHome">
|
||||
<img loading="lazy" alt="" src="@/assets/xiaozhi-logo.png" class="logo-img" />
|
||||
<img loading="lazy" alt="" src="@/assets/xiaozhi-ai.png" class="brand-img" />
|
||||
</div>
|
||||
|
||||
<!-- 中间导航菜单 -->
|
||||
<div class="header-center">
|
||||
<div class="equipment-management" :class="{ 'active-tab': $route.path === '/home' }" @click="goHome">
|
||||
<img loading="lazy" alt="" src="@/assets/header/robot.png" :style="{ filter: $route.path === '/home' ? 'brightness(0) invert(1)' : 'None' }"/>
|
||||
<img loading="lazy" alt="" src="@/assets/header/robot.png"
|
||||
:style="{ filter: $route.path === '/home' ? 'brightness(0) invert(1)' : 'None' }" />
|
||||
智能体管理
|
||||
</div>
|
||||
<div v-if="isSuperAdmin" class="equipment-management" :class="{ 'active-tab': $route.path === '/user-management' }" @click="goUserManagement">
|
||||
<img loading="lazy" alt="" src="@/assets/header/user_management.png" :style="{ filter: $route.path === '/user-management' ? 'brightness(0) invert(1)' : 'None' }"/>
|
||||
<div v-if="isSuperAdmin" class="equipment-management" :class="{ 'active-tab': $route.path === '/model-config' }"
|
||||
@click="goModelConfig">
|
||||
<img loading="lazy" alt="" src="@/assets/header/model_config.png"
|
||||
:style="{ filter: $route.path === '/model-config' ? 'brightness(0) invert(1)' : 'None' }" />
|
||||
模型配置
|
||||
</div>
|
||||
<div v-if="isSuperAdmin" class="equipment-management"
|
||||
:class="{ 'active-tab': $route.path === '/user-management' }" @click="goUserManagement">
|
||||
<img loading="lazy" alt="" src="@/assets/header/user_management.png"
|
||||
:style="{ filter: $route.path === '/user-management' ? 'brightness(0) invert(1)' : 'None' }" />
|
||||
用户管理
|
||||
</div>
|
||||
<div v-if="isSuperAdmin" class="equipment-management" :class="{ 'active-tab': $route.path === '/model-config' }" @click="goModelConfig">
|
||||
<img loading="lazy" alt="" src="@/assets/header/model_config.png" :style="{ filter: $route.path === '/model-config' ? 'brightness(0) invert(1)' : 'None' }"/>
|
||||
模型配置
|
||||
<div v-if="isSuperAdmin" class="equipment-management"
|
||||
:class="{ 'active-tab': $route.path === '/params-management' }" @click="goParamManagement">
|
||||
<img loading="lazy" alt="" src="@/assets/header/param_management.png"
|
||||
:style="{ filter: $route.path === '/params-management' ? 'brightness(0) invert(1)' : 'None' }" />
|
||||
参数管理
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 右侧元素 -->
|
||||
<div class="header-right">
|
||||
<div class="search-container">
|
||||
<el-input
|
||||
v-model="search"
|
||||
placeholder="输入名称搜索.."
|
||||
class="custom-search-input"
|
||||
@keyup.enter.native="handleSearch"
|
||||
>
|
||||
<div class="search-container" v-if="$route.path === '/home'">
|
||||
<el-input v-model="search" placeholder="输入名称搜索.." class="custom-search-input"
|
||||
@keyup.enter.native="handleSearch">
|
||||
<i slot="suffix" class="el-icon-search search-icon" @click="handleSearch"></i>
|
||||
</el-input>
|
||||
</div>
|
||||
<img loading="lazy" alt="" src="@/assets/home/avatar.png" class="avatar-img"/>
|
||||
<img loading="lazy" alt="" src="@/assets/home/avatar.png" class="avatar-img" />
|
||||
<el-dropdown trigger="click" class="user-dropdown">
|
||||
<span class="el-dropdown-link">
|
||||
{{ userInfo.username || '加载中...' }}<i class="el-icon-arrow-down el-icon--right"></i>
|
||||
{{ userInfo.username || '加载中...' }}<i class="el-icon-arrow-down el-icon--right"></i>
|
||||
</span>
|
||||
<el-dropdown-menu slot="dropdown">
|
||||
<el-dropdown-item icon="el-icon-plus" @click.native="">个人中心</el-dropdown-item>
|
||||
<el-dropdown-item icon="el-icon-circle-plus" @click.native="showChangePasswordDialog">修改密码</el-dropdown-item>
|
||||
<el-dropdown-item icon="el-icon-circle-plus-outline" @click.native="handleLogout">退出登录</el-dropdown-item>
|
||||
<el-dropdown-item @click.native="showChangePasswordDialog">修改密码</el-dropdown-item>
|
||||
<el-dropdown-item @click.native="handleLogout">退出登录</el-dropdown-item>
|
||||
</el-dropdown-menu>
|
||||
</el-dropdown>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 修改密码弹窗 -->
|
||||
<ChangePasswordDialog v-model="isChangePasswordDialogVisible"/>
|
||||
<ChangePasswordDialog v-model="isChangePasswordDialogVisible" />
|
||||
</el-header>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import userApi from '@/apis/module/user';
|
||||
import ChangePasswordDialog from './ChangePasswordDialog.vue'; // 引入修改密码弹窗组件
|
||||
import { mapActions, mapGetters } from 'vuex';
|
||||
import ChangePasswordDialog from './ChangePasswordDialog.vue'; // 引入修改密码弹窗组件
|
||||
|
||||
|
||||
export default {
|
||||
@@ -96,9 +102,12 @@ export default {
|
||||
goModelConfig() {
|
||||
this.$router.push('/model-config')
|
||||
},
|
||||
goParamManagement() {
|
||||
this.$router.push('/params-management')
|
||||
},
|
||||
// 获取用户信息
|
||||
fetchUserInfo() {
|
||||
userApi.getUserInfo(({data}) => {
|
||||
userApi.getUserInfo(({ data }) => {
|
||||
this.userInfo = data.data
|
||||
if (data.data.superAdmin !== undefined) {
|
||||
this.$store.commit('setUserInfo', data.data);
|
||||
@@ -109,23 +118,26 @@ export default {
|
||||
// 处理搜索
|
||||
handleSearch() {
|
||||
const searchValue = this.search.trim();
|
||||
let filteredDevices;
|
||||
|
||||
// 如果搜索内容为空,触发重置事件
|
||||
if (!searchValue) {
|
||||
// 当搜索内容为空时,显示原始完整列表
|
||||
filteredDevices = this.$parent.originalDevices;
|
||||
} else {
|
||||
// 过滤逻辑
|
||||
filteredDevices = this.devices.filter(device => {
|
||||
return device.agentName.includes(searchValue) ||
|
||||
device.ttsModelName.includes(searchValue) ||
|
||||
device.ttsVoiceName.includes(searchValue);
|
||||
});
|
||||
this.$emit('search-reset');
|
||||
return;
|
||||
}
|
||||
|
||||
this.$emit('search-result', filteredDevices);
|
||||
try {
|
||||
// 创建不区分大小写的正则表达式
|
||||
const regex = new RegExp(searchValue, 'i');
|
||||
// 触发搜索事件,将正则表达式传递给父组件
|
||||
this.$emit('search', regex);
|
||||
} catch (error) {
|
||||
console.error('正则表达式创建失败:', error);
|
||||
this.$message.error({
|
||||
message: '搜索关键词格式不正确',
|
||||
showClose: true
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
// 显示修改密码弹窗
|
||||
showChangePasswordDialog() {
|
||||
this.isChangePasswordDialogVisible = true;
|
||||
@@ -136,14 +148,14 @@ export default {
|
||||
// 调用 Vuex 的 logout action
|
||||
await this.logout();
|
||||
this.$message.success({
|
||||
message:'退出登录成功',
|
||||
showClose:true
|
||||
message: '退出登录成功',
|
||||
showClose: true
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('退出登录失败:', error);
|
||||
this.$message.error({
|
||||
message:'退出登录失败,请重试',
|
||||
showClose:true
|
||||
message: '退出登录失败,请重试',
|
||||
showClose: true
|
||||
});
|
||||
}
|
||||
},
|
||||
@@ -159,7 +171,8 @@ export default {
|
||||
background: #f6fcfe66;
|
||||
border: 1px solid #fff;
|
||||
height: 53px !important;
|
||||
min-width: 900px; /* 设置最小宽度防止过度压缩 */
|
||||
min-width: 900px;
|
||||
/* 设置最小宽度防止过度压缩 */
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
@@ -184,8 +197,7 @@ export default {
|
||||
}
|
||||
|
||||
.brand-img {
|
||||
width: 58px;
|
||||
height: 12px;
|
||||
height: 18px;
|
||||
}
|
||||
|
||||
.header-center {
|
||||
@@ -221,7 +233,8 @@ export default {
|
||||
align-items: center;
|
||||
transition: all 0.3s ease;
|
||||
cursor: pointer;
|
||||
flex-shrink: 0; /* 防止导航按钮被压缩 */
|
||||
flex-shrink: 0;
|
||||
/* 防止导航按钮被压缩 */
|
||||
}
|
||||
|
||||
.equipment-management.active-tab {
|
||||
@@ -241,10 +254,10 @@ export default {
|
||||
max-width: 220px;
|
||||
}
|
||||
|
||||
.custom-search-input >>> .el-input__inner {
|
||||
.custom-search-input>>>.el-input__inner {
|
||||
height: 30px;
|
||||
border-radius: 15px;
|
||||
background-color: #e2e5f8;
|
||||
background-color: #fff;
|
||||
border: 1px solid #e4e6ef;
|
||||
padding-left: 15px;
|
||||
font-size: 12px;
|
||||
@@ -308,7 +321,7 @@ export default {
|
||||
max-width: 145px;
|
||||
}
|
||||
|
||||
.custom-search-input >>> .el-input__inner {
|
||||
.custom-search-input>>>.el-input__inner {
|
||||
padding-left: 10px;
|
||||
font-size: 11px;
|
||||
}
|
||||
|
||||
@@ -1,159 +1,493 @@
|
||||
<template>
|
||||
<el-dialog :visible.sync="dialogVisible" width="800px" center>
|
||||
<el-form :model="form" ref="form" label-width="70px">
|
||||
<el-row :gutter="20" class="form-row">
|
||||
<el-col :span="12">
|
||||
<el-form-item label="模型编码">
|
||||
<el-input v-model="form.code" placeholder="请输入内容" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<el-form-item label="模型名称">
|
||||
<el-input v-model="form.name" placeholder="请输入内容" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
<el-dialog :visible.sync="dialogVisible" width="975px" 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;">
|
||||
修改模型
|
||||
</div>
|
||||
|
||||
<!-- 供应商 -->
|
||||
<el-row :gutter="20" class="form-row">
|
||||
<el-col :span="12">
|
||||
<el-form-item label="供应器">
|
||||
<el-select v-model="form.supplier" placeholder="请选择">
|
||||
<el-option label="openai" value="openai" />
|
||||
<el-option label="dify" value="dify" />
|
||||
<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;">模型信息</div>
|
||||
<div style="display: flex; align-items: center; gap: 20px;">
|
||||
<div style="display: flex; align-items: center;">
|
||||
<span style="margin-right: 8px;">是否启用</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;">
|
||||
<span style="margin-right: 8px;">设为默认</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>
|
||||
|
||||
<el-form :model="form" ref="form" label-width="100px" label-position="left" class="custom-form">
|
||||
<div style="display: flex; gap: 20px; margin-bottom: 0;">
|
||||
<el-form-item label="模型名称" prop="name" style="flex: 1;">
|
||||
<el-input v-model="form.modelName" placeholder="请输入模型名称" class="custom-input-bg"></el-input>
|
||||
</el-form-item>
|
||||
<el-form-item label="模型编码" prop="code" style="flex: 1;">
|
||||
<el-input v-model="form.modelCode" placeholder="请输入模型编码" class="custom-input-bg"></el-input>
|
||||
</el-form-item>
|
||||
</div>
|
||||
|
||||
<div style="display: flex; gap: 20px; margin-bottom: 0;">
|
||||
<el-form-item label="供应器" prop="supplier" style="flex: 1;">
|
||||
<el-select v-model="form.configJson.type" placeholder="请选择" 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-col>
|
||||
<el-col :span="12">
|
||||
<el-form-item>
|
||||
<div style="display: flex; align-items: center;">
|
||||
<span class="el-form-item__label" style="width: 70px;">设成默认</span>
|
||||
<el-switch v-model="form.isDefault" style="margin-right: 20px;" />
|
||||
<span class="el-form-item__label" style="width: 70px;">是否启用</span>
|
||||
<el-switch v-model="form.isEnable" />
|
||||
</div>
|
||||
<el-form-item label="排序号" prop="sort" style="flex: 1;">
|
||||
<el-input v-model.number="form.sort" type="number" placeholder="请输入排序号" class="custom-input-bg"></el-input>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</div>
|
||||
|
||||
<!-- 文档 -->
|
||||
<el-row :gutter="20" class="form-row">
|
||||
<el-col :span="12">
|
||||
<el-form-item label="文档地址">
|
||||
<el-input v-model="form.docUrl" placeholder="请输入内容" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<el-form-item label="排序号">
|
||||
<el-input-number v-model="form.sort" :min="1" :max="999" controls-position="right" placeholder="123"
|
||||
/>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
|
||||
<!-- 备注 -->
|
||||
<el-form-item label="备注" class="form-row">
|
||||
<el-input type="textarea" v-model="form.remark" :rows="2" placeholder="请输入"
|
||||
/>
|
||||
</el-form-item>
|
||||
|
||||
<div class="vertical-fields">
|
||||
<el-form-item label="接口地址">
|
||||
<el-input v-model="form.apiUrl" placeholder="请输入base_url" />
|
||||
<el-form-item label="文档地址" prop="docUrl" style="margin-bottom: 27px;">
|
||||
<el-input v-model="form.docLink" placeholder="请输入文档地址" class="custom-input-bg"></el-input>
|
||||
</el-form-item>
|
||||
<el-form-item label="模型名称">
|
||||
<el-input v-model="form.modelName" placeholder="请输入model_name" />
|
||||
</el-form-item>
|
||||
<el-form-item label="密钥信息">
|
||||
<el-input v-model="form.apiKey" placeholder="请输入api_key" show-password/>
|
||||
</el-form-item>
|
||||
</div>
|
||||
</el-form>
|
||||
|
||||
<div slot="footer" class="dialog-footer">
|
||||
<el-button type="primary" @click="handleSave">保存</el-button>
|
||||
<el-button @click="dialogVisible = false">关闭</el-button>
|
||||
<el-form-item label="备注" prop="remark" class="prop-remark">
|
||||
<el-input v-model="form.remark" type="textarea" :rows="3" placeholder="请输入模型备注"
|
||||
class="custom-input-bg"></el-input>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
|
||||
<div style="font-size: 20px; font-weight: bold; color: #3d4566; margin-bottom: 15px;">调用信息</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;">
|
||||
<el-input
|
||||
v-model="form.configJson[field.prop]"
|
||||
:placeholder="field.placeholder"
|
||||
:type="field.type"
|
||||
class="custom-input-bg"
|
||||
:show-password="field.type === 'password'">
|
||||
</el-input>
|
||||
</el-form-item>
|
||||
</div>
|
||||
</template>
|
||||
</el-form>
|
||||
</div>
|
||||
|
||||
<div style="display: flex;justify-content: center;">
|
||||
<el-button type="primary" @click="handleSave" class="save-btn">
|
||||
保存
|
||||
</el-button>
|
||||
</div>
|
||||
</el-dialog>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import Api from '@/apis/api';
|
||||
|
||||
const DEFAULT_CONFIG_JSON = {
|
||||
type: "",
|
||||
base_url: "",
|
||||
model_name: "",
|
||||
api_key: "",
|
||||
raw: {},
|
||||
config: {
|
||||
keyComparator: {},
|
||||
ignoreError: false,
|
||||
ignoreCase: false,
|
||||
dateFormat: "",
|
||||
ignoreNullValue: false,
|
||||
transientSupport: false,
|
||||
stripTrailingZeros: false,
|
||||
checkDuplicate: false,
|
||||
order: false
|
||||
},
|
||||
empty: false
|
||||
};
|
||||
|
||||
export default {
|
||||
name: "ModelConfigDialog",
|
||||
name: "ModelEditDialog",
|
||||
props: {
|
||||
visible: { type: Boolean, default: false },
|
||||
configData: { type: Object, default: () => ({}) }
|
||||
modelData: {
|
||||
type: Object,
|
||||
default: () => ({}),
|
||||
validator: value => typeof value === 'object' && !Array.isArray(value)
|
||||
},
|
||||
modelType: { type: String, required: true }
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
dialogVisible: this.visible,
|
||||
providers: [],
|
||||
providersLoaded: false,
|
||||
allProvidersData: null,
|
||||
pendingProviderType: null,
|
||||
pendingModelData: null,
|
||||
dynamicCallInfoFields: [],
|
||||
form: {
|
||||
code: "",
|
||||
name: "",
|
||||
supplier: "",
|
||||
isDefault: true,
|
||||
isEnable: true,
|
||||
docUrl: "",
|
||||
sort: 123,
|
||||
remark: "",
|
||||
apiUrl: "",
|
||||
id: "",
|
||||
modelType: "",
|
||||
modelCode: "",
|
||||
modelName: "",
|
||||
apiKey: ""
|
||||
isDefault: false,
|
||||
isEnabled: false,
|
||||
docLink: "",
|
||||
remark: "",
|
||||
sort: 0,
|
||||
configJson: JSON.parse(JSON.stringify(DEFAULT_CONFIG_JSON))
|
||||
}
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
chunkedCallInfoFields() {
|
||||
const chunkSize = 2;
|
||||
const result = [];
|
||||
for (let i = 0; i < this.dynamicCallInfoFields.length; i += chunkSize) {
|
||||
result.push(this.dynamicCallInfoFields.slice(i, i + chunkSize));
|
||||
}
|
||||
return result;
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
modelType() {
|
||||
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.form = { ...this.form, ...this.configData };
|
||||
if (val) {
|
||||
this.loadProviders();
|
||||
}
|
||||
},
|
||||
'form.configJson.type'(newVal) {
|
||||
if (newVal && this.providersLoaded) {
|
||||
this.loadProviderFields(newVal);
|
||||
}
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
resetForm() {
|
||||
this.form = {
|
||||
id: "",
|
||||
modelType: "",
|
||||
modelCode: "",
|
||||
modelName: "",
|
||||
isDefault: false,
|
||||
isEnabled: false,
|
||||
docLink: "",
|
||||
remark: "",
|
||||
sort: "",
|
||||
configJson: JSON.parse(JSON.stringify(DEFAULT_CONFIG_JSON))
|
||||
};
|
||||
},
|
||||
resetProviders() {
|
||||
this.providers = [];
|
||||
this.providersLoaded = false;
|
||||
},
|
||||
loadModelData() {
|
||||
if (this.modelData.id) {
|
||||
Api.model.getModelConfig(this.modelData.id, ({ data }) => {
|
||||
if (data.code === 0 && data.data) {
|
||||
const model = data.data;
|
||||
this.pendingProviderType = model.configJson.type;
|
||||
this.pendingModelData = model;
|
||||
|
||||
if (this.providersLoaded) {
|
||||
this.loadProviderFields(model.configJson.type);
|
||||
} else {
|
||||
this.loadProviders();
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
},
|
||||
handleSave() {
|
||||
this.$emit("submit", this.form);
|
||||
const provideCode = this.form.configJson.type;
|
||||
const { provider, ...restConfigJson } = this.form.configJson;
|
||||
const formData = {
|
||||
id: this.form.id,
|
||||
modelCode: this.form.modelCode,
|
||||
modelName: this.form.modelName,
|
||||
isDefault: this.form.isDefault ? 1 : 0,
|
||||
isEnabled: this.form.isEnabled ? 1 : 0,
|
||||
docLink: this.form.docLink,
|
||||
remark: this.form.remark,
|
||||
sort: this.form.sort || 0,
|
||||
configJson: {
|
||||
...restConfigJson,
|
||||
config: {
|
||||
...restConfigJson.config,
|
||||
ignoreError: !!restConfigJson.config?.ignoreError,
|
||||
ignoreCase: !!restConfigJson.config?.ignoreCase,
|
||||
}
|
||||
}
|
||||
};
|
||||
this.$emit("save", { provideCode, formData });
|
||||
this.dialogVisible = false;
|
||||
},
|
||||
loadProviders() {
|
||||
if (this.providersLoaded) return;
|
||||
|
||||
Api.model.getModelProviders(this.modelType, (data) => {
|
||||
this.providers = data.map(item => ({
|
||||
label: item.name,
|
||||
value: item.providerCode
|
||||
}));
|
||||
this.providersLoaded = true;
|
||||
|
||||
this.allProvidersData = data;
|
||||
|
||||
if (this.pendingProviderType) {
|
||||
this.loadProviderFields(this.pendingProviderType);
|
||||
}
|
||||
});
|
||||
},
|
||||
loadProviderFields(providerCode) {
|
||||
if (this.allProvidersData) {
|
||||
const provider = this.allProvidersData.find(p => p.providerCode === providerCode);
|
||||
if (provider) {
|
||||
this.dynamicCallInfoFields = JSON.parse(provider.fields || '[]').map(f => ({
|
||||
label: f.label,
|
||||
prop: f.key,
|
||||
type: f.type === 'password' ? 'password' : 'text',
|
||||
placeholder: `请输入${f.label}`
|
||||
}));
|
||||
|
||||
if (this.pendingModelData && this.pendingProviderType === providerCode) {
|
||||
this.processModelData(this.pendingModelData);
|
||||
this.pendingModelData = null;
|
||||
this.pendingProviderType = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
processModelData(model) {
|
||||
let configJson = model.configJson || {};
|
||||
this.dynamicCallInfoFields.forEach(field => {
|
||||
if (!configJson.hasOwnProperty(field.prop)) {
|
||||
configJson[field.prop] = '';
|
||||
}
|
||||
});
|
||||
|
||||
this.form = {
|
||||
id: model.id || "",
|
||||
modelType: model.modelType || "",
|
||||
modelCode: model.modelCode || "",
|
||||
modelName: model.modelName || "",
|
||||
isDefault: model.isDefault || 0,
|
||||
isEnabled: model.isEnabled || 0,
|
||||
docLink: model.docLink || "",
|
||||
remark: model.remark || "",
|
||||
sort: Number(model.sort) || 0,
|
||||
configJson: {
|
||||
...JSON.parse(JSON.stringify(DEFAULT_CONFIG_JSON)),
|
||||
...configJson,
|
||||
config: {
|
||||
...DEFAULT_CONFIG_JSON.config,
|
||||
...(configJson.config || {})
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.dialog-footer {
|
||||
margin-top: -30px;
|
||||
text-align: center;
|
||||
}
|
||||
.el-form-item {
|
||||
margin-bottom: 18px;
|
||||
}
|
||||
.el-input-number {
|
||||
width: 100%;
|
||||
}
|
||||
.form-row {
|
||||
margin-bottom: 12px;
|
||||
.custom-dialog {
|
||||
position: relative;
|
||||
border-radius: 20px;
|
||||
overflow: hidden;
|
||||
background: white;
|
||||
padding-bottom: 17px;
|
||||
}
|
||||
|
||||
.vertical-fields {
|
||||
margin-top: 8px;
|
||||
.custom-dialog .el-dialog__header {
|
||||
padding: 0;
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.dialog-footer {
|
||||
margin-top: -10px;
|
||||
text-align: center;
|
||||
.center-dialog {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
.el-input-number {
|
||||
|
||||
.center-dialog .el-dialog {
|
||||
margin: 4% 0 auto !important;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.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-select .el-input__suffix {
|
||||
background: #e6e8ea;
|
||||
right: 6px;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
top: 9px;
|
||||
}
|
||||
|
||||
.custom-select .el-input__suffix-inner {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.el-input__inner {
|
||||
height: 36px;
|
||||
line-height: 36px;
|
||||
.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);
|
||||
}
|
||||
|
||||
.el-form-item__label {
|
||||
text-align: left;
|
||||
padding-right: 10px;
|
||||
.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-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 .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-input-bg .el-input__inner {
|
||||
height: 32px;
|
||||
}
|
||||
|
||||
|
||||
.custom-form .el-form-item__label {
|
||||
color: #3d4566;
|
||||
font-weight: normal;
|
||||
text-align: right;
|
||||
padding-right: 20px;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,201 +1,693 @@
|
||||
<template>
|
||||
<el-dialog :visible.sync="localVisible" width="80%" @close="handleClose">
|
||||
<el-row class="main-container">
|
||||
<el-col :span="4">
|
||||
<el-menu class="model-menu" :default-active="activeModel" mode="vertical" @select="handleModelSelect">
|
||||
<el-menu-item index="EdgeTTS">EdgeTTS</el-menu-item>
|
||||
<el-menu-item index="DoubaoTTS">DoubaoTTS</el-menu-item>
|
||||
<el-menu-item index="TTS302AI">TTS302AI</el-menu-item>
|
||||
<el-menu-item index="CosyVoiceSiliconflow">CosyVoiceSiliconflow</el-menu-item>
|
||||
</el-menu>
|
||||
</el-col>
|
||||
|
||||
<el-col :span="20">
|
||||
<div class="search-operate">
|
||||
<el-input placeholder="请输入音色名称查询" v-model="searchQuery" style="width: 300px; margin-right: 10px;"/>
|
||||
<el-button type="primary" @click="handleSearch">查询</el-button>
|
||||
<el-button type="primary" plain @click="handleAddVoice">添加音色</el-button>
|
||||
<el-button type="danger" plain @click="handleBatchDelete">批量删除</el-button>
|
||||
</div>
|
||||
|
||||
<el-table :data="filteredTtsModels" style="width: 100%" border stripe header-row-class-name="table-header">
|
||||
<el-table-column label="音色编码" prop="voiceCode" width="150" align="center"></el-table-column>
|
||||
<el-table-column label="音色名称" prop="voiceName" width="180" align="center"></el-table-column>
|
||||
<el-table-column label="语言类型" prop="languageType" width="120" align="center"></el-table-column>
|
||||
<el-table-column label="备注" prop="remark" align="center"></el-table-column>
|
||||
<el-table-column label="操作" width="200" align="center">
|
||||
<el-dialog
|
||||
:visible.sync="localVisible"
|
||||
width="75%"
|
||||
@close="handleClose"
|
||||
:show-close="false"
|
||||
:append-to-body="true"
|
||||
:close-on-click-modal="true">
|
||||
<button class="custom-close-btn" @click="handleClose">
|
||||
×
|
||||
</button>
|
||||
<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"
|
||||
header-row-class-name="table-header"
|
||||
:fit="true">
|
||||
<el-table-column label="选择" width="50" align="center">
|
||||
<template slot-scope="scope">
|
||||
<el-button size="mini" type="text" @click="editVoice(scope.row)" style="color: #409EFF; margin-right: 15px;">修改</el-button>
|
||||
<el-button size="mini" type="text" @click="deleteVoice(scope.row)" style="color: #F56C6C;">删除</el-button>
|
||||
<el-checkbox v-model="scope.row.selected"></el-checkbox>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="音色编码" align="center" min-width="50">
|
||||
<template slot-scope="scope">
|
||||
<el-input v-if="scope.row.editing" v-model="scope.row.voiceCode"></el-input>
|
||||
<span v-else>{{ scope.row.voiceCode }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="音色名称" align="center" min-width="50">
|
||||
<template slot-scope="scope">
|
||||
<el-input v-if="scope.row.editing" v-model="scope.row.voiceName"></el-input>
|
||||
<span v-else>{{ scope.row.voiceName }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="语言类型" align="center" min-width="50">
|
||||
<template slot-scope="scope">
|
||||
<el-input v-if="scope.row.editing" v-model="scope.row.languageType"></el-input>
|
||||
<span v-else>{{ scope.row.languageType }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="试听" align="center" min-width="225" class-name="audio-column">
|
||||
<template slot-scope="scope">
|
||||
<div class="custom-audio-container">
|
||||
<el-input
|
||||
v-if="scope.row.editing"
|
||||
v-model="scope.row.voiceDemo"
|
||||
placeholder="请输入MP3地址"
|
||||
size="mini"
|
||||
class="audio-input"
|
||||
></el-input>
|
||||
<AudioPlayer v-else-if="isValidAudioUrl(scope.row.voiceDemo)" :audioUrl="scope.row.voiceDemo"/>
|
||||
</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="备注" align="center" min-width="120">
|
||||
<template slot-scope="scope">
|
||||
<el-input
|
||||
v-if="scope.row.editing"
|
||||
type="textarea"
|
||||
:rows="1"
|
||||
autosize
|
||||
v-model="scope.row.remark"
|
||||
placeholder="这里是备注"
|
||||
class="remark-input"></el-input>
|
||||
<span v-else>{{ scope.row.remark }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" align="center" width="150">
|
||||
<template slot-scope="scope">
|
||||
<template v-if="!scope.row.editing">
|
||||
<el-button type="primary" size="mini" @click="startEdit(scope.row)"
|
||||
style="background: #5cca8e;border:None">编辑
|
||||
</el-button>
|
||||
<el-button type="primary" size="mini" @click="deleteRow(scope.row)" style="background: red;border:None">
|
||||
删除
|
||||
</el-button>
|
||||
</template>
|
||||
<el-button
|
||||
v-else
|
||||
type="success"
|
||||
size="mini"
|
||||
@click="saveEdit(scope.row)"
|
||||
class="save-Tts">保存
|
||||
</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</div>
|
||||
|
||||
<div class="pagination-container">
|
||||
<el-pagination :current-page="currentPage" :page-size="pageSize" :total="total" layout="prev, pager, next" prev-text="<" next-text=">"/>
|
||||
<!-- 自定义滚动条 -->
|
||||
<div class="custom-scrollbar" ref="scrollbar">
|
||||
<div class="custom-scrollbar-track" ref="scrollbarTrack" @click="handleTrackClick">
|
||||
<div class="custom-scrollbar-thumb" ref="scrollbarThumb" @mousedown="startDrag"></div>
|
||||
</div>
|
||||
|
||||
<div slot="footer" class="dialog-footer">
|
||||
<el-button @click="handleClose" size="medium">关闭</el-button>
|
||||
<el-button type="primary" @click="handleImportExport" size="medium">导入导出配置</el-button>
|
||||
</div>
|
||||
<EditVoiceDialog :showDialog="editDialogVisible" :voiceData="editVoiceData" @update:showDialog="editDialogVisible = $event" @saveVoice="handleSaveEditedVoice"/>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</div>
|
||||
</div>
|
||||
<div class="action-buttons">
|
||||
<el-button type="primary" size="mini" @click="toggleSelectAll" style="background: #606ff3;border: None">
|
||||
{{ selectAll ? '取消全选' : '全选' }}
|
||||
</el-button>
|
||||
<el-button type="primary" size="mini" @click="addNew" style="background: #f6cf79;border: None; color: #000012">
|
||||
新增
|
||||
</el-button>
|
||||
<el-button type="primary" size="mini" @click="batchDelete" style="background: red;border:None">删除</el-button>
|
||||
</div>
|
||||
</el-dialog>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import EditVoiceDialog from "@/components/EditVoiceDialog.vue";
|
||||
import AudioPlayer from './AudioPlayer.vue'
|
||||
import Api from "@/apis/api";
|
||||
|
||||
export default {
|
||||
components: { EditVoiceDialog },
|
||||
components: {AudioPlayer},
|
||||
props: {
|
||||
visible: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
ttsModelId: {
|
||||
type: String,
|
||||
required: true
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
localVisible: this.visible,
|
||||
activeModel: 'EdgeTTS',
|
||||
searchQuery: '',
|
||||
editDialogVisible: false,
|
||||
editVoiceData: {},
|
||||
ttsModels: [
|
||||
{ voiceCode: 'wawaxiaohe', voiceName: '湾湾小何', languageType: '中文', remark: '' },
|
||||
{ voiceCode: 'wawaxiaohe', voiceName: '湾湾小何', languageType: '中文', remark: '' },
|
||||
{ voiceCode: 'wawaxiaohe', voiceName: '湾湾小何', languageType: '中文', remark: '' },
|
||||
{ voiceCode: 'wawaxiaohe', voiceName: '湾湾小何', languageType: '中文', remark: '' },
|
||||
],
|
||||
ttsModels: [],
|
||||
currentPage: 1,
|
||||
pageSize: 4,
|
||||
total: 20
|
||||
pageSize: 10000,
|
||||
total: 0,
|
||||
isDragging: false,
|
||||
startY: 0,
|
||||
scrollTop: 0,
|
||||
selectAll: false,
|
||||
selectedRows: [],
|
||||
loading: false,
|
||||
};
|
||||
},
|
||||
watch: {
|
||||
visible(newVal) {
|
||||
this.localVisible = newVal;
|
||||
if (newVal) {
|
||||
this.currentPage = 1;
|
||||
this.loadData(); // 对话框显示时加载数据
|
||||
this.$nextTick(() => {
|
||||
this.updateScrollbar();
|
||||
});
|
||||
}
|
||||
},
|
||||
filteredTtsModels() {
|
||||
this.$nextTick(() => {
|
||||
this.updateScrollbar();
|
||||
});
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
filteredTtsModels() {
|
||||
return this.ttsModels.filter(model =>
|
||||
model.voiceName.toLowerCase().includes(this.searchQuery.toLowerCase())
|
||||
model.voiceName.toLowerCase().includes(this.searchQuery.toLowerCase())
|
||||
);
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
this.updateScrollbar();
|
||||
window.addEventListener('resize', this.updateScrollbar);
|
||||
window.addEventListener('mouseup', this.stopDrag);
|
||||
window.addEventListener('mousemove', this.handleDrag);
|
||||
},
|
||||
beforeDestroy() {
|
||||
window.removeEventListener('resize', this.updateScrollbar);
|
||||
window.removeEventListener('mouseup', this.stopDrag);
|
||||
window.removeEventListener('mousemove', this.handleDrag);
|
||||
},
|
||||
methods: {
|
||||
loadData() {
|
||||
const params = {
|
||||
ttsModelId: this.ttsModelId,
|
||||
page: this.currentPage,
|
||||
limit: this.pageSize,
|
||||
name: this.searchQuery
|
||||
};
|
||||
|
||||
Api.timbre.getVoiceList(params, (data) => {
|
||||
if (data.code === 0) {
|
||||
this.ttsModels = data.data.list
|
||||
.map(item => ({
|
||||
id: item.id || '',
|
||||
voiceCode: item.ttsVoice || '',
|
||||
voiceName: item.name || '未命名音色',
|
||||
languageType: item.languages || '',
|
||||
remark: item.remark || '',
|
||||
voiceDemo: item.voiceDemo || '',
|
||||
selected: false,
|
||||
editing: false,
|
||||
sort: Number(item.sort)
|
||||
}))
|
||||
.sort((a, b) => a.sort - b.sort);
|
||||
this.total = data.total;
|
||||
} else {
|
||||
this.$message.error({
|
||||
message: data.msg || '获取音色列表失败',
|
||||
showClose: true
|
||||
});
|
||||
}
|
||||
}, (err) => {
|
||||
console.error('加载失败:', err);
|
||||
this.$message.error({
|
||||
message: '加载音色数据失败',
|
||||
showClose: true
|
||||
});
|
||||
});
|
||||
},
|
||||
|
||||
handleClose() {
|
||||
// 重置状态
|
||||
this.ttsModels = [];
|
||||
this.currentPage = 1;
|
||||
this.total = 0;
|
||||
this.selectAll = false;
|
||||
this.searchQuery = '';
|
||||
|
||||
this.localVisible = false;
|
||||
this.$emit('update:visible', false);
|
||||
},
|
||||
handleModelSelect(index) {
|
||||
this.activeModel = index;
|
||||
// 可根据选中模型加载对应数据
|
||||
|
||||
updateScrollbar() {
|
||||
const container = this.$refs.tableContainer;
|
||||
const scrollbarThumb = this.$refs.scrollbarThumb;
|
||||
const scrollbarTrack = this.$refs.scrollbarTrack;
|
||||
|
||||
if (!container || !scrollbarThumb || !scrollbarTrack) return;
|
||||
|
||||
const {scrollHeight, clientHeight} = container;
|
||||
const trackHeight = scrollbarTrack.clientHeight;
|
||||
const thumbHeight = Math.max((clientHeight / scrollHeight) * trackHeight, 20);
|
||||
|
||||
scrollbarThumb.style.height = `${thumbHeight}px`;
|
||||
this.updateThumbPosition();
|
||||
},
|
||||
handleSearch() {
|
||||
// 搜索
|
||||
|
||||
updateThumbPosition() {
|
||||
const container = this.$refs.tableContainer;
|
||||
const scrollbarThumb = this.$refs.scrollbarThumb;
|
||||
const scrollbarTrack = this.$refs.scrollbarTrack;
|
||||
|
||||
if (!container || !scrollbarThumb || !scrollbarTrack) return;
|
||||
|
||||
const {scrollHeight, clientHeight, scrollTop} = container;
|
||||
const trackHeight = scrollbarTrack.clientHeight;
|
||||
const thumbHeight = scrollbarThumb.clientHeight;
|
||||
const maxTop = trackHeight - thumbHeight;
|
||||
const thumbTop = (scrollTop / (scrollHeight - clientHeight)) * (trackHeight - thumbHeight);
|
||||
|
||||
scrollbarThumb.style.top = `${Math.min(thumbTop, maxTop)}px`;
|
||||
},
|
||||
handleAddVoice() {
|
||||
// 添加音色
|
||||
|
||||
handleScroll() {
|
||||
const container = this.$refs.tableContainer;
|
||||
if (container.scrollTop + container.clientHeight >= container.scrollHeight - 50) {
|
||||
if (this.currentPage * this.pageSize < this.total) {
|
||||
this.currentPage++;
|
||||
this.loadData();
|
||||
}
|
||||
}
|
||||
this.updateThumbPosition();
|
||||
},
|
||||
handleBatchDelete() {
|
||||
// 批量删除
|
||||
|
||||
startDrag(e) {
|
||||
this.isDragging = true;
|
||||
this.startY = e.clientY;
|
||||
this.scrollTop = this.$refs.tableContainer.scrollTop;
|
||||
e.preventDefault();
|
||||
},
|
||||
editVoice(voice) {
|
||||
this.editVoiceData = { ...voice };
|
||||
this.editDialogVisible = true;
|
||||
|
||||
stopDrag() {
|
||||
this.isDragging = false;
|
||||
},
|
||||
handleSaveEditedVoice(voiceForm) {
|
||||
const index = this.ttsModels.findIndex(item => item.voiceCode === voiceForm.voiceCode);
|
||||
if (index !== -1) {
|
||||
this.ttsModels.splice(index, 1, voiceForm);
|
||||
|
||||
handleDrag(e) {
|
||||
if (!this.isDragging) return;
|
||||
|
||||
const container = this.$refs.tableContainer;
|
||||
const scrollbarTrack = this.$refs.scrollbarTrack;
|
||||
const scrollbarThumb = this.$refs.scrollbarThumb;
|
||||
const deltaY = e.clientY - this.startY;
|
||||
const trackHeight = scrollbarTrack.clientHeight;
|
||||
const thumbHeight = scrollbarThumb.clientHeight;
|
||||
const maxScrollTop = container.scrollHeight - container.clientHeight;
|
||||
|
||||
const scrollRatio = (trackHeight - thumbHeight) / maxScrollTop;
|
||||
container.scrollTop = this.scrollTop + deltaY / scrollRatio;
|
||||
},
|
||||
|
||||
handleTrackClick(e) {
|
||||
const container = this.$refs.tableContainer;
|
||||
const scrollbarTrack = this.$refs.scrollbarTrack;
|
||||
const scrollbarThumb = this.$refs.scrollbarThumb;
|
||||
|
||||
if (!container || !scrollbarTrack || !scrollbarThumb) return;
|
||||
|
||||
const trackRect = scrollbarTrack.getBoundingClientRect();
|
||||
const thumbHeight = scrollbarThumb.clientHeight;
|
||||
const clickPosition = e.clientY - trackRect.top;
|
||||
const thumbCenter = clickPosition - thumbHeight / 2;
|
||||
|
||||
const trackHeight = scrollbarTrack.clientHeight;
|
||||
const maxTop = trackHeight - thumbHeight;
|
||||
const newTop = Math.max(0, Math.min(thumbCenter, maxTop));
|
||||
|
||||
scrollbarThumb.style.top = `${newTop}px`;
|
||||
container.scrollTop = (newTop / (trackHeight - thumbHeight)) * (container.scrollHeight - container.clientHeight);
|
||||
},
|
||||
|
||||
startEdit(row) {
|
||||
row.editing = true;
|
||||
this.$set(row, 'originalData', {...row});
|
||||
},
|
||||
|
||||
saveEdit(row) {
|
||||
try {
|
||||
const params = {
|
||||
id: row.id,
|
||||
voiceCode: row.voiceCode,
|
||||
voiceName: row.voiceName,
|
||||
languageType: row.languageType,
|
||||
remark: row.remark,
|
||||
ttsModelId: this.ttsModelId,
|
||||
voiceDemo: row.voiceDemo || '',
|
||||
sort: row.sort
|
||||
};
|
||||
|
||||
let res;
|
||||
if (row.id) {
|
||||
// 已有ID,执行更新操作
|
||||
Api.timbre.updateVoice(params, (response) => {
|
||||
res = response;
|
||||
this.handleResponse(res, row);
|
||||
});
|
||||
} else {
|
||||
// 没有ID,执行新增操作
|
||||
Api.timbre.saveVoice(params, (response) => {
|
||||
res = response;
|
||||
this.handleResponse(res, row);
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('操作失败:', error);
|
||||
// 异常情况下也恢复原始数据
|
||||
if (row.originalData) {
|
||||
Object.assign(row, row.originalData);
|
||||
row.editing = false;
|
||||
delete row.originalData;
|
||||
}
|
||||
this.$message.error({
|
||||
message: '操作失败,请重试',
|
||||
showClose: true
|
||||
});
|
||||
}
|
||||
},
|
||||
deleteVoice(voice) {
|
||||
// 删除
|
||||
|
||||
handleResponse(res, row) {
|
||||
if (res.code === 0) {
|
||||
this.$message.success({
|
||||
message: row.id ? '修改成功' : '保存成功',
|
||||
showClose: true
|
||||
});
|
||||
row.editing = false;
|
||||
delete row.originalData;
|
||||
this.loadData(); // 刷新数据
|
||||
} else {
|
||||
// 保存失败时恢复原始数据
|
||||
if (row.originalData) {
|
||||
Object.assign(row, row.originalData);
|
||||
row.editing = false;
|
||||
delete row.originalData;
|
||||
}
|
||||
this.$message.error({
|
||||
message: res.msg || (row.id ? '修改失败' : '保存失败'),
|
||||
showClose: true
|
||||
});
|
||||
}
|
||||
},
|
||||
handleImportExport() {
|
||||
// 导入导出
|
||||
|
||||
toggleSelectAll() {
|
||||
this.selectAll = !this.selectAll;
|
||||
this.filteredTtsModels.forEach(row => {
|
||||
row.selected = this.selectAll;
|
||||
});
|
||||
},
|
||||
|
||||
addNew() {
|
||||
const maxSort = this.ttsModels.length > 0
|
||||
? Math.max(...this.ttsModels.map(item => Number(item.sort) || 0))
|
||||
: 0;
|
||||
|
||||
const newRow = {
|
||||
voiceCode: '',
|
||||
voiceName: '',
|
||||
languageType: '中文',
|
||||
voiceDemo: '',
|
||||
remark: '',
|
||||
selected: false,
|
||||
editing: true,
|
||||
sort: maxSort + 1
|
||||
};
|
||||
|
||||
this.ttsModels.unshift(newRow);
|
||||
},
|
||||
|
||||
deleteRow(row) {
|
||||
this.$confirm("确定要删除该音色吗?", "警告", {
|
||||
confirmButtonText: "确定",
|
||||
cancelButtonText: "取消",
|
||||
type: "warning",
|
||||
})
|
||||
.then(() => {
|
||||
Api.timbre.deleteVoice(row.id, (response) => {
|
||||
if (response.code === 0) {
|
||||
this.$message.success({
|
||||
message: "删除成功",
|
||||
showClose: true
|
||||
});
|
||||
this.loadData(); // 刷新数据
|
||||
} else {
|
||||
this.$message.error({
|
||||
message: response.msg || "删除失败",
|
||||
showClose: true
|
||||
});
|
||||
}
|
||||
});
|
||||
})
|
||||
.catch(() => {
|
||||
this.$message.info("已取消删除");
|
||||
});
|
||||
},
|
||||
|
||||
batchDelete() {
|
||||
const selectedRows = this.filteredTtsModels.filter(row => row.selected);
|
||||
if (selectedRows.length === 0) {
|
||||
this.$message.warning("请先选择需要删除的音色");
|
||||
return;
|
||||
}
|
||||
|
||||
this.$confirm(`确定要删除选中的${selectedRows.length}个音色吗?`, "警告", {
|
||||
confirmButtonText: "确定",
|
||||
cancelButtonText: "取消",
|
||||
type: "warning",
|
||||
})
|
||||
.then(async () => {
|
||||
const loading = this.$loading({
|
||||
lock: true,
|
||||
text: "正在删除中...",
|
||||
spinner: "el-icon-loading",
|
||||
background: "rgba(0, 0, 0, 0.7)",
|
||||
});
|
||||
|
||||
try {
|
||||
const results = await Promise.all(
|
||||
selectedRows.map((row) => {
|
||||
return new Promise((resolve) => {
|
||||
Api.timbre.deleteVoice(row.id, (response) => {
|
||||
if (response.code === 0) {
|
||||
resolve({ success: true, id: row.id });
|
||||
} else {
|
||||
resolve({
|
||||
success: false,
|
||||
id: row.id,
|
||||
msg: response.msg || '删除失败'
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
})
|
||||
);
|
||||
|
||||
const successCount = results.filter(r => r.success).length;
|
||||
const failCount = results.length - successCount;
|
||||
|
||||
if (failCount === 0) {
|
||||
this.$message.success({
|
||||
message: `成功删除${successCount}个音色`,
|
||||
showClose: true
|
||||
});
|
||||
} else if (successCount === 0) {
|
||||
this.$message.error({
|
||||
message: '删除失败,请重试',
|
||||
showClose: true
|
||||
});
|
||||
} else {
|
||||
this.$message.warning({
|
||||
message: `成功删除${successCount}个音色,${failCount}个删除失败`,
|
||||
showClose: true
|
||||
});
|
||||
}
|
||||
|
||||
this.loadData(); // 刷新数据
|
||||
} catch (error) {
|
||||
console.error('批量删除出错:', error);
|
||||
this.$message.error({
|
||||
message: '删除过程中发生错误',
|
||||
showClose: true
|
||||
});
|
||||
} finally {
|
||||
loading.close();
|
||||
}
|
||||
})
|
||||
.catch(() => {
|
||||
this.$message.info("已取消删除");
|
||||
});
|
||||
},
|
||||
|
||||
isValidAudioUrl(url) {
|
||||
return url && (url.endsWith('.mp3') || url.endsWith('.ogg') || url.endsWith('.wav'));
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.main-container {
|
||||
padding: 20px;
|
||||
|
||||
::v-deep .el-dialog {
|
||||
border-radius: 8px !important;
|
||||
overflow: hidden;
|
||||
top: 8vh !important;
|
||||
}
|
||||
|
||||
.model-menu {
|
||||
border-right: 1px solid #ebeef5;
|
||||
height: calc(100vh - 300px);
|
||||
background-color: #fafafa;
|
||||
::v-deep .el-dialog__header {
|
||||
display: none !important;
|
||||
padding: 0 !important;
|
||||
margin: 0 !important;
|
||||
}
|
||||
|
||||
.search-operate {
|
||||
margin-bottom: 20px;
|
||||
/* 表格样式 */
|
||||
::v-deep .data-table .el-table__header th {
|
||||
color: black;
|
||||
padding: 6px 0 !important;
|
||||
}
|
||||
|
||||
::v-deep .data-table .el-table__row td {
|
||||
padding: 8px 0 12px !important;
|
||||
}
|
||||
|
||||
::v-deep .data-table {
|
||||
border: none !important;
|
||||
}
|
||||
|
||||
::v-deep .data-table.el-table::before {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
::v-deep .data-table .el-table__header-wrapper {
|
||||
border-bottom: 2px solid #f1f2fb !important;
|
||||
}
|
||||
|
||||
::v-deep .data-table .el-table__body-wrapper .el-table__body td {
|
||||
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;
|
||||
}
|
||||
|
||||
.pagination-container {
|
||||
margin: 20px 0;
|
||||
display: flex;
|
||||
justify-content: right;
|
||||
}
|
||||
|
||||
.dialog-footer {
|
||||
text-align: right;
|
||||
padding: 20px 20px 0;
|
||||
border-top: 1px solid #ebeef5;
|
||||
}
|
||||
|
||||
::v-deep .table-header th {
|
||||
background-color: #f5f7fa;
|
||||
color: #606266;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
::v-deep .el-table--striped .el-table__body tr.el-table__row--striped td {
|
||||
background-color: #fafafa;
|
||||
}
|
||||
|
||||
::v-deep .el-table--border td, ::v-deep .el-table--border th {
|
||||
border-right: 1px solid #ebeef5;
|
||||
}
|
||||
|
||||
::v-deep .el-pagination {
|
||||
padding: 10px;
|
||||
background-color: #f5f7fa;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
::v-deep .el-pagination .btn-prev, ::v-deep .el-pagination .btn-next {
|
||||
background-color: #fff;
|
||||
border: 1px solid #dcdfe6;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
::v-deep .el-pagination .btn-prev:hover, ::v-deep .el-pagination .btn-next:hover {
|
||||
background-color: #f5f7fa;
|
||||
}
|
||||
|
||||
::v-deep .el-pagination .el-pager li {
|
||||
background-color: #fff;
|
||||
border: 1px solid #dcdfe6;
|
||||
border-radius: 4px;
|
||||
margin: 0 2px;
|
||||
}
|
||||
|
||||
::v-deep .el-pagination .el-pager li:hover {
|
||||
background-color: #f5f7fa;
|
||||
}
|
||||
|
||||
::v-deep .el-pagination .el-pager li.active {
|
||||
background-color: #409EFF;
|
||||
color: #fff;
|
||||
.custom-close-btn:hover {
|
||||
color: #409EFF;
|
||||
border-color: #409EFF;
|
||||
}
|
||||
</style>
|
||||
|
||||
/* 备注文本 */
|
||||
::v-deep .remark-input .el-textarea__inner {
|
||||
background-color: #f5f5f5;
|
||||
border-radius: 4px;
|
||||
border: 1px solid #e6e6e6;
|
||||
padding: 8px 12px;
|
||||
resize: none;
|
||||
max-height: 40px !important;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
::v-deep .remark-input .el-textarea__inner::placeholder {
|
||||
color: black !important;
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
::v-deep .remark-input .el-textarea__inner {
|
||||
background-color: #f4f6fa;
|
||||
}
|
||||
|
||||
::v-deep .remark-input .el-textarea__inner:focus {
|
||||
background-color: #edeffb;
|
||||
}
|
||||
|
||||
/* 滚动容器 */
|
||||
.scroll-wrapper {
|
||||
display: flex;
|
||||
max-height: 55vh;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.table-container {
|
||||
flex: 1;
|
||||
overflow-y: scroll;
|
||||
scrollbar-width: none; /* Firefox */
|
||||
padding-right: 15px; /* 为滚动条留出空间 */
|
||||
width: calc(100% - 16px); /* 减去滚动条宽度 */
|
||||
}
|
||||
|
||||
.table-container::-webkit-scrollbar {
|
||||
display: none; /* Chrome/Safari */
|
||||
}
|
||||
|
||||
/* 自定义滚动条 */
|
||||
.custom-scrollbar {
|
||||
width: 8px;
|
||||
background: #f1f1f1;
|
||||
border-radius: 4px;
|
||||
position: relative;
|
||||
margin-left: 8px;
|
||||
height: 100%;
|
||||
top: 55px;
|
||||
}
|
||||
|
||||
.custom-scrollbar-track {
|
||||
position: relative;
|
||||
height: 380px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.custom-scrollbar-thumb {
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
background: #9dade7;
|
||||
border-radius: 4px;
|
||||
cursor: grab;
|
||||
transition: background 0.2s;
|
||||
}
|
||||
|
||||
.custom-scrollbar-thumb:hover {
|
||||
background: #6b84d9;
|
||||
}
|
||||
|
||||
.custom-scrollbar-thumb:active {
|
||||
cursor: grabbing;
|
||||
}
|
||||
|
||||
.save-Tts {
|
||||
background: #796dea;
|
||||
border: None;
|
||||
}
|
||||
|
||||
.save-Tts:hover {
|
||||
background: #8b80f0;
|
||||
}
|
||||
|
||||
.custom-audio-container audio {
|
||||
display: none;
|
||||
}
|
||||
|
||||
/* 新增按钮组样式 */
|
||||
.action-buttons {
|
||||
bottom: 20px;
|
||||
padding-top: 10px;
|
||||
}
|
||||
|
||||
</style>
|
||||
@@ -1,8 +1,11 @@
|
||||
<template>
|
||||
<el-dialog :visible.sync="visible" width="400px" 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 src="@/assets/login/shield.png" alt="" style="width: 19px;height: 23px; filter: brightness(0) invert(1);" />
|
||||
<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 src="@/assets/login/shield.png" alt=""
|
||||
style="width: 19px;height: 23px; filter: brightness(0) invert(1);" />
|
||||
</div>
|
||||
用户新密码
|
||||
</div>
|
||||
@@ -13,27 +16,15 @@
|
||||
用户新密码:
|
||||
</div>
|
||||
<div class="input-46" style="margin-top: 12px;">
|
||||
<el-input
|
||||
v-model="password"
|
||||
type="text"
|
||||
:readonly="true"
|
||||
style="font-weight: bold; color: #333;"
|
||||
/>
|
||||
<el-input v-model="password" type="text" :readonly="true" style="font-weight: bold; color: #333;" />
|
||||
</div>
|
||||
</div>
|
||||
<div style="display: flex;margin: 15px 15px;gap: 7px;">
|
||||
<div
|
||||
class="dialog-btn"
|
||||
style="background: #e6ebff;border: 1px solid #adbdff;color: #5778ff;"
|
||||
@click="closeDialog"
|
||||
>
|
||||
<div class="dialog-btn" style="background: #e6ebff;border: 1px solid #adbdff;color: #5778ff;"
|
||||
@click="closeDialog">
|
||||
关闭
|
||||
</div>
|
||||
<div
|
||||
class="dialog-btn"
|
||||
style="background: #5778ff;color: white;"
|
||||
@click="copyPassword"
|
||||
>
|
||||
<div class="dialog-btn" style="background: #5778ff;color: white;" @click="copyPassword">
|
||||
复制密码
|
||||
</div>
|
||||
</div>
|
||||
@@ -53,7 +44,10 @@ export default {
|
||||
},
|
||||
copyPassword() {
|
||||
navigator.clipboard.writeText(this.password)
|
||||
this.$message.success('密码已复制')
|
||||
this.$message.success({
|
||||
message: '密码已复制',
|
||||
showClose: true
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
import Vue from 'vue'
|
||||
import App from './App.vue'
|
||||
import router from './router'
|
||||
import store from './store'
|
||||
import 'normalize.css/normalize.css' // A modern alternative to CSS resets
|
||||
import ElementUI from 'element-ui';
|
||||
import 'element-ui/lib/theme-chalk/index.css';
|
||||
import './styles/global.scss'
|
||||
import 'normalize.css/normalize.css'; // A modern alternative to CSS resets
|
||||
import Vue from 'vue';
|
||||
import App from './App.vue';
|
||||
import router from './router';
|
||||
import store from './store';
|
||||
import './styles/global.scss';
|
||||
|
||||
Vue.use(ElementUI);
|
||||
|
||||
|
||||
@@ -56,11 +56,11 @@ const routes = [
|
||||
}
|
||||
},
|
||||
{
|
||||
path: '/model-config',
|
||||
name: 'ModelConfig',
|
||||
component: function () {
|
||||
return import('../views/ModelConfig.vue')
|
||||
}
|
||||
path: '/model-config',
|
||||
name: 'ModelConfig',
|
||||
component: function () {
|
||||
return import('../views/ModelConfig.vue')
|
||||
}
|
||||
},
|
||||
{
|
||||
path: '/test',
|
||||
@@ -69,7 +69,17 @@ const routes = [
|
||||
return import('../views/test.vue')
|
||||
}
|
||||
},
|
||||
|
||||
{
|
||||
path: '/params-management',
|
||||
name: 'ParamsManagement',
|
||||
component: function () {
|
||||
return import('../views/ParamsManagement.vue')
|
||||
},
|
||||
meta: {
|
||||
requiresAuth: true,
|
||||
title: '参数管理'
|
||||
}
|
||||
},
|
||||
]
|
||||
|
||||
const router = new VueRouter({
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import Vue from 'vue'
|
||||
import Vuex from 'vuex'
|
||||
import Constant from '../utils/constant'
|
||||
import {goToPage} from "@/utils";
|
||||
import { goToPage } from "@/utils";
|
||||
import Vue from 'vue';
|
||||
import Vuex from 'vuex';
|
||||
import Constant from '../utils/constant';
|
||||
|
||||
Vue.use(Vuex)
|
||||
|
||||
@@ -52,7 +52,7 @@ export default new Vuex.Store({
|
||||
logout({ commit }) {
|
||||
return new Promise((resolve) => {
|
||||
commit('clearAuth')
|
||||
goToPage(Constant.PAGE.LOGIN,true);
|
||||
goToPage(Constant.PAGE.LOGIN, true);
|
||||
window.location.reload(); // 彻底重置状态
|
||||
})
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
// 覆盖 autofill 样式
|
||||
input:-webkit-autofill,
|
||||
input:-webkit-autofill:hover,
|
||||
input:-webkit-autofill:hover,
|
||||
input:-webkit-autofill:focus,
|
||||
textarea:-webkit-autofill,
|
||||
textarea:-webkit-autofill:hover,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { Message } from 'element-ui'
|
||||
import router from '../router'
|
||||
import Constant from '../utils/constant'
|
||||
import { Message } from 'element-ui'
|
||||
|
||||
/**
|
||||
* 判断用户是否登录
|
||||
|
||||
@@ -15,7 +15,8 @@
|
||||
<el-table-column label="最近对话" prop="lastConversation" width="140"></el-table-column>
|
||||
<el-table-column label="备注" width="180">
|
||||
<template slot-scope="scope">
|
||||
<el-input v-if="scope.row.isEdit" v-model="scope.row.remark" size="mini" @blur="stopEditRemark(scope.$index)"></el-input>
|
||||
<el-input v-if="scope.row.isEdit" v-model="scope.row.remark" size="mini"
|
||||
@blur="stopEditRemark(scope.$index)"></el-input>
|
||||
<span v-else>
|
||||
<i v-if="!scope.row.remark" class="el-icon-edit" @click="startEditRemark(scope.$index, scope.row)"></i>
|
||||
<span v-else @click="startEditRemark(scope.$index, scope.row)">
|
||||
@@ -26,7 +27,8 @@
|
||||
</el-table-column>
|
||||
<el-table-column label="OTA升级" width="120">
|
||||
<template slot-scope="scope">
|
||||
<el-switch v-model="scope.row.otaSwitch" size="mini" active-color="#13ce66" inactive-color="#ff4949"></el-switch>
|
||||
<el-switch v-model="scope.row.otaSwitch" size="mini" active-color="#13ce66"
|
||||
inactive-color="#ff4949"></el-switch>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" width="80">
|
||||
@@ -37,31 +39,26 @@
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
<el-pagination
|
||||
class="pagination"
|
||||
@size-change="handleSizeChange"
|
||||
@current-change="handleCurrentChange"
|
||||
:current-page="currentPage"
|
||||
:page-sizes="[5, 10, 20, 50]"
|
||||
:page-size="pageSize"
|
||||
layout="total, sizes, prev, pager, next, jumper"
|
||||
:total="deviceList.length"
|
||||
></el-pagination>
|
||||
<el-pagination class="pagination" @size-change="handleSizeChange" @current-change="handleCurrentChange"
|
||||
:current-page="currentPage" :page-sizes="[5, 10, 20, 50]" :page-size="pageSize"
|
||||
layout="total, sizes, prev, pager, next, jumper" :total="deviceList.length"></el-pagination>
|
||||
</div>
|
||||
<div class="copyright">
|
||||
©2025 xiaozhi-esp32-server
|
||||
</div>
|
||||
<AddDeviceDialog :visible.sync="addDeviceDialogVisible" :agent-id="currentAgentId" @refresh="fetchBindDevices(currentAgentId)" />
|
||||
<AddDeviceDialog :visible.sync="addDeviceDialogVisible" :agent-id="currentAgentId"
|
||||
@refresh="fetchBindDevices(currentAgentId)" />
|
||||
</el-main>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import HeaderBar from "@/components/HeaderBar.vue";
|
||||
import Api from '@/apis/api';
|
||||
import AddDeviceDialog from "@/components/AddDeviceDialog.vue";
|
||||
import HeaderBar from "@/components/HeaderBar.vue";
|
||||
|
||||
export default {
|
||||
components: {HeaderBar, AddDeviceDialog },
|
||||
components: { HeaderBar, AddDeviceDialog },
|
||||
data() {
|
||||
return {
|
||||
addDeviceDialogVisible: false,
|
||||
@@ -82,12 +79,9 @@ export default {
|
||||
},
|
||||
mounted() {
|
||||
const agentId = this.$route.query.agentId;
|
||||
import('@/apis/module/device').then(({ default: deviceApi }) => {
|
||||
this.deviceApi = deviceApi;
|
||||
if (agentId) {
|
||||
this.fetchBindDevices(agentId);
|
||||
}
|
||||
});
|
||||
if (agentId) {
|
||||
this.fetchBindDevices(agentId);
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
handleAddDevice() {
|
||||
@@ -100,26 +94,22 @@ export default {
|
||||
this.deviceList[index].isEdit = false;
|
||||
},
|
||||
handleUnbind(device_id) {
|
||||
if (!this.deviceApi) {
|
||||
this.$message.error('功能模块加载失败');
|
||||
return;
|
||||
}
|
||||
this.$confirm('确认要解绑该设备吗?', '警告', {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning'
|
||||
}).then(() => {
|
||||
this.deviceApi.unbindDevice(device_id, ({ data }) => {
|
||||
Api.device.unbindDevice(device_id, ({ data }) => {
|
||||
if (data.code === 0) {
|
||||
this.$message.success({
|
||||
message: '设备解绑成功',
|
||||
showClose: true
|
||||
message: '设备解绑成功',
|
||||
showClose: true
|
||||
});
|
||||
this.fetchBindDevices(this.$route.query.agentId);
|
||||
} else {
|
||||
this.$message.error({
|
||||
message: data.msg || '设备解绑失败',
|
||||
showClose: true
|
||||
message: data.msg || '设备解绑失败',
|
||||
showClose: true
|
||||
});
|
||||
}
|
||||
});
|
||||
@@ -133,38 +123,33 @@ export default {
|
||||
},
|
||||
fetchBindDevices(agentId) {
|
||||
this.loading = true;
|
||||
import('@/apis/module/device').then(({ default: deviceApi }) => {
|
||||
deviceApi.getAgentBindDevices(agentId, ({ data }) => {
|
||||
this.loading = false;
|
||||
if (data.code === 0) {
|
||||
// 格式化日期并按照绑定时间降序排列
|
||||
this.deviceList = data.data.map(device => {
|
||||
// 格式化绑定时间
|
||||
const bindDate = new Date(device.createDate);
|
||||
const formattedBindTime = `${bindDate.getFullYear()}-${(bindDate.getMonth()+1).toString().padStart(2, '0')}-${bindDate.getDate().toString().padStart(2, '0')} ${bindDate.getHours().toString().padStart(2, '0')}:${bindDate.getMinutes().toString().padStart(2, '0')}:${bindDate.getSeconds().toString().padStart(2, '0')}`;
|
||||
return {
|
||||
device_id: device.id,
|
||||
model: device.board,
|
||||
firmwareVersion: device.appVersion,
|
||||
macAddress: device.macAddress,
|
||||
bindTime: formattedBindTime, // 使用格式化后的时间
|
||||
lastConversation: device.lastConnectedAt,
|
||||
remark: device.alias,
|
||||
isEdit: false,
|
||||
otaSwitch: device.autoUpdate === 1,
|
||||
// 添加原始时间用于排序
|
||||
rawBindTime: new Date(device.createDate).getTime()
|
||||
};
|
||||
})
|
||||
Api.device.getAgentBindDevices(agentId, ({ data }) => {
|
||||
this.loading = false;
|
||||
if (data.code === 0) {
|
||||
// 格式化日期并按照绑定时间降序排列
|
||||
this.deviceList = data.data.map(device => {
|
||||
// 格式化绑定时间
|
||||
const bindDate = new Date(device.createDate);
|
||||
const formattedBindTime = `${bindDate.getFullYear()}-${(bindDate.getMonth() + 1).toString().padStart(2, '0')}-${bindDate.getDate().toString().padStart(2, '0')} ${bindDate.getHours().toString().padStart(2, '0')}:${bindDate.getMinutes().toString().padStart(2, '0')}:${bindDate.getSeconds().toString().padStart(2, '0')}`;
|
||||
return {
|
||||
device_id: device.id,
|
||||
model: device.board,
|
||||
firmwareVersion: device.appVersion,
|
||||
macAddress: device.macAddress,
|
||||
bindTime: formattedBindTime, // 使用格式化后的时间
|
||||
lastConversation: device.lastConnectedAt,
|
||||
remark: device.alias,
|
||||
isEdit: false,
|
||||
otaSwitch: device.autoUpdate === 1,
|
||||
// 添加原始时间用于排序
|
||||
rawBindTime: new Date(device.createDate).getTime()
|
||||
};
|
||||
})
|
||||
// 按照绑定时间降序排序
|
||||
.sort((a, b) => a.rawBindTime - b.rawBindTime);
|
||||
} else {
|
||||
this.$message.error(data.msg || '获取设备列表失败');
|
||||
}
|
||||
});
|
||||
}).catch(error => {
|
||||
console.error('模块加载失败:', error);
|
||||
this.$message.error('功能模块加载失败');
|
||||
} else {
|
||||
this.$message.error(data.msg || '获取设备列表失败');
|
||||
}
|
||||
});
|
||||
},
|
||||
}
|
||||
@@ -179,7 +164,7 @@ export default {
|
||||
height: 100vh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
background-image: url("@/assets/home/background.png");
|
||||
background: linear-gradient(145deg, #e6eeff, #eff0ff);
|
||||
background-size: cover;
|
||||
background-position: center;
|
||||
-webkit-background-size: cover;
|
||||
@@ -207,6 +192,7 @@ export default {
|
||||
font-size: 14px;
|
||||
gap: 8px;
|
||||
margin-bottom: 15px;
|
||||
|
||||
&:hover {
|
||||
background: #3a8ee6;
|
||||
}
|
||||
|
||||
@@ -2,26 +2,26 @@
|
||||
<div class="welcome">
|
||||
<HeaderBar />
|
||||
|
||||
<div class="operation-bar">
|
||||
<h2 class="page-title">模型配置</h2>
|
||||
<div class="right-operations">
|
||||
<el-button plain size="small" @click="handleImport" style="background: #7b9de5; color: white;">
|
||||
<img loading="lazy" alt="" src="@/assets/model/inner_conf.png">
|
||||
导入配置
|
||||
</el-button>
|
||||
<el-button plain size="small" @click="handleExport" style="background: #71c9d1; color: white;">
|
||||
<img loading="lazy" alt="" src="@/assets/model/output_conf.png">
|
||||
导出配置
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="operation-bar">
|
||||
<h2 class="page-title">模型配置</h2>
|
||||
<!-- <div class="right-operations">-->
|
||||
<!-- <el-button plain size="small" @click="handleImport" style="background: #7b9de5; color: white;">-->
|
||||
<!-- <img loading="lazy" alt="" src="@/assets/model/inner_conf.png">-->
|
||||
<!-- 导入配置-->
|
||||
<!-- </el-button>-->
|
||||
<!-- <el-button plain size="small" @click="handleExport" style="background: #71c9d1; color: white;">-->
|
||||
<!-- <img loading="lazy" alt="" src="@/assets/model/output_conf.png">-->
|
||||
<!-- 导出配置-->
|
||||
<!-- </el-button>-->
|
||||
<!-- </div>-->
|
||||
</div>
|
||||
|
||||
<!-- 主体内容 -->
|
||||
<div class="main-wrapper">
|
||||
<div class="content-panel">
|
||||
<!-- 左侧导航 -->
|
||||
<el-menu :default-active="activeTab" class="nav-panel" @select="handleMenuSelect"
|
||||
style="background-size: cover; background-position: center;">
|
||||
style="background-size: cover; background-position: center;">
|
||||
<el-menu-item index="vad">
|
||||
<span class="menu-text">语言活动检测</span>
|
||||
</el-menu-item>
|
||||
@@ -46,14 +46,14 @@
|
||||
<div class="content-area">
|
||||
<div class="title-bar">
|
||||
<div class="title-wrapper">
|
||||
<h2 class="model-title">大语言模型(LLM)</h2>
|
||||
<el-button type="primary" size="small" @click="addModel" class="add-btn">
|
||||
添加
|
||||
</el-button>
|
||||
<h2 class="model-title">{{ modelTypeText }}</h2>
|
||||
<el-button type="primary" size="small" @click="addModel" class="add-btn">
|
||||
添加
|
||||
</el-button>
|
||||
</div>
|
||||
<div class="action-group">
|
||||
<div class="search-group">
|
||||
<el-input placeholder="请输入模型名称查询" v-model="search" size="small" class="search-input" clearable/>
|
||||
<el-input placeholder="请输入模型名称查询" v-model="search" size="small" class="search-input" clearable @keyup.enter.native="handleSearch" />
|
||||
<el-button type="primary" size="small" class="search-btn" @click="handleSearch">
|
||||
查询
|
||||
</el-button>
|
||||
@@ -61,19 +61,30 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<el-table ref="modelTable" style="width: 100%" :header-cell-style="{background: 'transparent'}" :data="modelList" class="data-table" header-row-class-name="table-header" :header-cell-class-name="headerCellClassName" @selection-change="handleSelectionChange">
|
||||
<el-table ref="modelTable" style="width: 100%" :header-cell-style="{ background: 'transparent' }"
|
||||
:data="modelList" class="data-table" header-row-class-name="table-header"
|
||||
:header-cell-class-name="headerCellClassName" @selection-change="handleSelectionChange">
|
||||
<el-table-column type="selection" width="55" align="center"></el-table-column>
|
||||
<el-table-column label="模型名称" prop="candidateName" align="center"></el-table-column>
|
||||
<el-table-column label="模型编码" prop="code" align="center"></el-table-column>
|
||||
<el-table-column label="提供商" prop="supplier" align="center"></el-table-column>
|
||||
<el-table-column label="模型名称" prop="modelName" align="center"></el-table-column>
|
||||
<el-table-column label="模型编码" prop="modelCode" align="center"></el-table-column>
|
||||
<el-table-column label="提供商" align="center">
|
||||
<template slot-scope="scope">
|
||||
{{ scope.row.configJson.type || '未知' }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="是否启用" align="center">
|
||||
<template slot-scope="scope">
|
||||
<el-switch v-model="scope.row.isApplied" class="custom-switch" :active-color="null" :inactive-color="null"/>
|
||||
<el-switch v-model="scope.row.isEnabled" class="custom-switch" :active-value="1" :inactive-value="0"
|
||||
@change="handleStatusChange(scope.row)" />
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column v-if="activeTab === 'tts'" label="音色管理" align="center">
|
||||
<template slot-scope="scope">
|
||||
<el-button type="text" size="mini" @click="ttsDialogVisible = true" class="voice-management-btn">
|
||||
<el-button
|
||||
type="text"
|
||||
size="mini"
|
||||
@click="openTtsDialog(scope.row)"
|
||||
class="voice-management-btn">
|
||||
音色管理
|
||||
</el-button>
|
||||
</template>
|
||||
@@ -92,7 +103,9 @@
|
||||
|
||||
<div class="table-footer">
|
||||
<div class="batch-actions">
|
||||
<el-button size="mini" @click="selectAll" style="width: 75px; background: #606ff3">{{ isAllSelected ? '取消全选' : '全选' }}</el-button>
|
||||
<el-button size="mini" @click="selectAll" style="width: 75px; background: #606ff3">{{ isAllSelected ?
|
||||
'取消全选' : '全选'
|
||||
}}</el-button>
|
||||
<el-button size="mini" type="danger" icon="el-icon-delete" @click="batchDelete">
|
||||
删除
|
||||
</el-button>
|
||||
@@ -101,7 +114,8 @@
|
||||
<button class="pagination-btn" :disabled="currentPage === 1" @click="goFirst">首页</button>
|
||||
<button class="pagination-btn" :disabled="currentPage === 1" @click="goPrev">上一页</button>
|
||||
|
||||
<button v-for="page in visiblePages" :key="page" class="pagination-btn" :class="{ active: page === currentPage }" @click="goToPage(page)">
|
||||
<button v-for="page in visiblePages" :key="page" class="pagination-btn"
|
||||
:class="{ active: page === currentPage }" @click="goToPage(page)">
|
||||
{{ page }}
|
||||
</button>
|
||||
|
||||
@@ -112,22 +126,24 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<ModelEditDialog :visible.sync="editDialogVisible" :modelData="editModelData" @save="handleModelSave"/>
|
||||
<TtsModel :visible.sync="ttsDialogVisible" />
|
||||
<AddModelDialog :modelType="activeTab" :visible.sync="addDialogVisible" @confirm="handleAddConfirm"/>
|
||||
<ModelEditDialog :modelType="activeTab" :visible.sync="editDialogVisible" :modelData="editModelData"
|
||||
@save="handleModelSave" />
|
||||
<TtsModel :visible.sync="ttsDialogVisible" :ttsModelId="selectedTtsModelId"/>
|
||||
<AddModelDialog :modelType="activeTab" :visible.sync="addDialogVisible" @confirm="handleAddConfirm" />
|
||||
</div>
|
||||
|
||||
<div class="copyright">
|
||||
©2025 xiaozhi-esp32-server
|
||||
</div>
|
||||
©2025 xiaozhi-esp32-server
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import Api from "@/apis/api";
|
||||
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 AddModelDialog from "@/components/AddModelDialog.vue";
|
||||
|
||||
export default {
|
||||
components: { HeaderBar, ModelEditDialog, TtsModel, AddModelDialog },
|
||||
@@ -139,20 +155,35 @@ export default {
|
||||
editDialogVisible: false,
|
||||
editModelData: {},
|
||||
ttsDialogVisible: false,
|
||||
modelList: [
|
||||
{ code: 'DeepSeek', candidateName: '深度求索', isApplied: true, supplier: '硅基流动' },
|
||||
{ code: 'SmartAssist', candidateName: '智能助手', isApplied: false, supplier: '智脑科技' },
|
||||
{ code: 'CogEngine', candidateName: '认知引擎', isApplied: true, supplier: '云智科技' },
|
||||
],
|
||||
selectedTtsModelId: '',
|
||||
modelList: [],
|
||||
currentPage: 1,
|
||||
pageSize: 4,
|
||||
total: 20,
|
||||
pageSize: 5,
|
||||
total: 0,
|
||||
selectedModels: [],
|
||||
isAllSelected: false
|
||||
};
|
||||
},
|
||||
|
||||
created() {
|
||||
this.loadData();
|
||||
},
|
||||
|
||||
computed: {
|
||||
|
||||
modelTypeText() {
|
||||
const map = {
|
||||
vad: '语言活动检测模型(VAD)',
|
||||
asr: '语音识别模型(ASR)',
|
||||
llm: '大语言模型(LLM)',
|
||||
intent: '意图识别模型(Intent)',
|
||||
tts: '语音合成模型(TTS)',
|
||||
memory: '记忆模型(Memory)'
|
||||
}
|
||||
return map[this.activeTab] || '模型配置'
|
||||
},
|
||||
|
||||
|
||||
pageCount() {
|
||||
return Math.ceil(this.total / this.pageSize);
|
||||
},
|
||||
@@ -174,6 +205,10 @@ export default {
|
||||
},
|
||||
|
||||
methods: {
|
||||
openTtsDialog(row) {
|
||||
this.selectedTtsModelId = row.id;
|
||||
this.ttsDialogVisible = true;
|
||||
},
|
||||
headerCellClassName({ column, columnIndex }) {
|
||||
if (columnIndex === 0) {
|
||||
return 'custom-selection-header';
|
||||
@@ -182,65 +217,114 @@ export default {
|
||||
},
|
||||
handleMenuSelect(index) {
|
||||
this.activeTab = index;
|
||||
this.currentPage = 1;
|
||||
this.loadData();
|
||||
},
|
||||
handleSearch() {
|
||||
console.log('查询:', this.search);
|
||||
this.currentPage = 1;
|
||||
this.loadData();
|
||||
},
|
||||
// 批量删除
|
||||
batchDelete() {
|
||||
if (this.selectedModels.length === 0) {
|
||||
this.$message.warning('请先选择要删除的模型');
|
||||
return;
|
||||
this.$message.warning('请先选择要删除的模型')
|
||||
return
|
||||
}
|
||||
|
||||
this.$confirm('确定要删除选中的模型吗?', '提示', {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning'
|
||||
}).then(() => {
|
||||
const selectedIds = this.selectedModels.map(model => model.code);
|
||||
this.modelList = this.modelList.filter(model => !selectedIds.includes(model.code));
|
||||
this.$message.success('删除成功');
|
||||
this.selectedModels = [];
|
||||
this.isAllSelected = false;
|
||||
const deletePromises = this.selectedModels.map(model =>
|
||||
new Promise(resolve => {
|
||||
Api.model.deleteModel(
|
||||
model.id,
|
||||
({ data }) => resolve(data.code === 0)
|
||||
)
|
||||
})
|
||||
)
|
||||
|
||||
Promise.all(deletePromises).then(results => {
|
||||
if (results.every(Boolean)) {
|
||||
this.$message.success({
|
||||
message: '批量删除成功',
|
||||
showClose: true
|
||||
})
|
||||
this.loadData()
|
||||
} else {
|
||||
this.$message.error({
|
||||
message: '部分删除失败',
|
||||
showClose: true
|
||||
})
|
||||
}
|
||||
})
|
||||
}).catch(() => {
|
||||
this.$message.info('已取消删除');
|
||||
});
|
||||
},
|
||||
this.$message.info('已取消删除')
|
||||
})
|
||||
},
|
||||
addModel() {
|
||||
this.addDialogVisible = true;
|
||||
},
|
||||
editModel(model) {
|
||||
this.editModelData = {
|
||||
code: model.code,
|
||||
name: model.candidateName,
|
||||
supplier: model.supplier,
|
||||
};
|
||||
this.editModelData = JSON.parse(JSON.stringify(model));
|
||||
this.editDialogVisible = true;
|
||||
},
|
||||
// 删除单个模型
|
||||
deleteModel(model) {
|
||||
this.$confirm(`确定要删除模型 ${model.candidateName} 吗?`, '提示', {
|
||||
this.$confirm('确定要删除该模型吗?', '提示', {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning'
|
||||
}).then(() => {
|
||||
this.modelList = this.modelList.filter(item => item.code !== model.code);
|
||||
this.$message.success('删除成功');
|
||||
Api.model.deleteModel(
|
||||
model.id,
|
||||
({ data }) => {
|
||||
if (data.code === 0) {
|
||||
this.$message.success({
|
||||
message: '删除成功',
|
||||
showClose: true
|
||||
})
|
||||
this.loadData()
|
||||
} else {
|
||||
this.$message.error({
|
||||
message: data.msg || '删除失败',
|
||||
showClose: true
|
||||
})
|
||||
}
|
||||
}
|
||||
)
|
||||
}).catch(() => {
|
||||
this.$message.info('已取消删除');
|
||||
});
|
||||
this.$message.info('已取消删除')
|
||||
})
|
||||
},
|
||||
handleCurrentChange(page) {
|
||||
this.currentPage = page;
|
||||
this.$refs.modelTable.clearSelection();
|
||||
console.log('当前页码:', page);
|
||||
},
|
||||
handleImport() {
|
||||
// TODO: 导入配置
|
||||
console.log('导入配置');
|
||||
},
|
||||
handleExport() {
|
||||
// TODO: 导出配置
|
||||
console.log('导出配置');
|
||||
},
|
||||
handleModelSave(formData) {
|
||||
console.log('保存的模型数据:', formData);
|
||||
handleModelSave({ provideCode, formData }) {
|
||||
const modelType = this.activeTab;
|
||||
const id = formData.id;
|
||||
Api.model.updateModel(
|
||||
{ modelType, provideCode, id, formData },
|
||||
({ data }) => {
|
||||
if (data.code === 0) {
|
||||
this.$message.success('保存成功');
|
||||
this.loadData();
|
||||
this.editDialogVisible = false;
|
||||
} else {
|
||||
this.$message.error(data.msg || '保存失败');
|
||||
}
|
||||
}
|
||||
);
|
||||
},
|
||||
selectAll() {
|
||||
if (this.isAllSelected) {
|
||||
@@ -256,14 +340,40 @@ export default {
|
||||
this.isAllSelected = false;
|
||||
}
|
||||
},
|
||||
|
||||
// 新增模型配置
|
||||
handleAddConfirm(newModel) {
|
||||
console.log('新增模型数据:', newModel);
|
||||
const params = {
|
||||
modelType: this.activeTab,
|
||||
provideCode: newModel.provideCode,
|
||||
formData: {
|
||||
...newModel,
|
||||
isDefault: newModel.isDefault ? 1 : 0,
|
||||
isEnabled: newModel.isEnabled ? 1 : 0,
|
||||
configJson: newModel.configJson
|
||||
}
|
||||
};
|
||||
|
||||
Api.model.addModel(params, ({ data }) => {
|
||||
if (data.code === 0) {
|
||||
this.$message.success({
|
||||
message: '新增成功',
|
||||
showClose: true
|
||||
});
|
||||
this.loadData();
|
||||
} else {
|
||||
this.$message.error({
|
||||
message: data.msg || '新增失败',
|
||||
showClose: true
|
||||
});
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
// 分页器
|
||||
goFirst() {
|
||||
this.currentPage = 1;
|
||||
this.loadData();
|
||||
this.currentPage = 1;
|
||||
this.loadData();
|
||||
},
|
||||
goPrev() {
|
||||
if (this.currentPage > 1) {
|
||||
@@ -281,15 +391,54 @@ export default {
|
||||
this.currentPage = page;
|
||||
this.loadData();
|
||||
},
|
||||
|
||||
// 获取模型配置列表
|
||||
loadData() {
|
||||
console.log('加载数据,当前页:', this.currentPage);
|
||||
const params = {
|
||||
modelType: this.activeTab,
|
||||
modelName: this.search,
|
||||
page: this.currentPage,
|
||||
limit: this.pageSize
|
||||
};
|
||||
|
||||
Api.model.getModelList(params, ({ data }) => {
|
||||
if (data.code === 0) {
|
||||
this.modelList = data.data.list;
|
||||
this.total = data.data.total;
|
||||
} else {
|
||||
this.$message.error(data.msg || '获取模型列表失败');
|
||||
}
|
||||
});
|
||||
},
|
||||
// 处理启用/禁用状态变更
|
||||
handleStatusChange(model) {
|
||||
const newStatus = model.isEnabled ? 1 : 0
|
||||
const originalStatus = model.isEnabled
|
||||
|
||||
model.isEnabled = !model.isEnabled
|
||||
|
||||
Api.model.updateModelStatus(
|
||||
model.id,
|
||||
newStatus,
|
||||
({ data }) => {
|
||||
if (data.code === 0) {
|
||||
this.$message.success(newStatus === 1 ? '启用成功' : '禁用成功')
|
||||
// 保持新状态
|
||||
model.isEnabled = newStatus
|
||||
} else {
|
||||
// 操作失败时恢复原状态
|
||||
model.isEnabled = originalStatus
|
||||
this.$message.error(data.msg || '操作失败')
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
::v-deep .el-table tr{
|
||||
::v-deep .el-table tr {
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
@@ -307,12 +456,12 @@ export default {
|
||||
}
|
||||
|
||||
.main-wrapper {
|
||||
margin: 5px 60px;
|
||||
margin: 5px 20px;
|
||||
border-radius: 15px;
|
||||
min-height: 600px;
|
||||
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.1);
|
||||
position: relative;
|
||||
background: rgba(237,242,255,0.5);
|
||||
background: rgba(237, 242, 255, 0.5);
|
||||
}
|
||||
|
||||
.operation-bar {
|
||||
@@ -320,7 +469,6 @@ export default {
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 16px 24px;
|
||||
border-bottom: 1px solid #ebeef5;
|
||||
}
|
||||
|
||||
.page-title {
|
||||
@@ -341,6 +489,7 @@ export default {
|
||||
height: 100%;
|
||||
border-radius: 15px;
|
||||
background: transparent;
|
||||
border: 1px solid #fff;
|
||||
}
|
||||
|
||||
.nav-panel {
|
||||
@@ -348,12 +497,10 @@ export default {
|
||||
height: 100%;
|
||||
border-right: 1px solid #ebeef5;
|
||||
background:
|
||||
linear-gradient(
|
||||
120deg,
|
||||
linear-gradient(120deg,
|
||||
rgba(107, 140, 255, 0.3) 0%,
|
||||
rgba(169, 102, 255, 0.3) 25%,
|
||||
transparent 60%
|
||||
),
|
||||
transparent 60%),
|
||||
url("../assets/model/model.png") no-repeat center / cover;
|
||||
padding: 16px 0;
|
||||
flex-shrink: 0;
|
||||
@@ -538,7 +685,8 @@ export default {
|
||||
}
|
||||
|
||||
::v-deep .el-table .custom-selection-header .cell .el-checkbox__inner {
|
||||
display: none !important; /* 使表头复选框不可见 */
|
||||
display: none !important;
|
||||
/* 使表头复选框不可见 */
|
||||
}
|
||||
|
||||
::v-deep .el-table .custom-selection-header .cell::before {
|
||||
@@ -564,6 +712,7 @@ export default {
|
||||
}
|
||||
|
||||
::v-deep .data-table {
|
||||
|
||||
&.el-table::before,
|
||||
&.el-table::after,
|
||||
&.el-table__inner-wrapper::before {
|
||||
@@ -572,14 +721,14 @@ export default {
|
||||
}
|
||||
|
||||
::v-deep .data-table .el-table__header-wrapper {
|
||||
border-bottom: 1px solid rgb(224,227,237);
|
||||
border-bottom: 1px solid rgb(224, 227, 237);
|
||||
}
|
||||
|
||||
::v-deep .data-table .el-table__body td {
|
||||
border-bottom: 1px solid rgb(224,227,237) !important;
|
||||
border-bottom: 1px solid rgb(224, 227, 237) !important;
|
||||
}
|
||||
|
||||
.el-button img{
|
||||
.el-button img {
|
||||
height: 1em;
|
||||
vertical-align: middle;
|
||||
padding-right: 2px;
|
||||
@@ -607,7 +756,8 @@ export default {
|
||||
}
|
||||
|
||||
.voice-management-btn:hover {
|
||||
background: #8aa2e0; /* 悬停时颜色加深 */
|
||||
background: #8aa2e0;
|
||||
/* 悬停时颜色加深 */
|
||||
transform: scale(1.05);
|
||||
}
|
||||
|
||||
@@ -619,7 +769,8 @@ export default {
|
||||
padding-right: 15px !important;
|
||||
}
|
||||
|
||||
.edit-btn, .delete-btn {
|
||||
.edit-btn,
|
||||
.delete-btn {
|
||||
margin: 0 8px;
|
||||
color: #7079aa !important;
|
||||
}
|
||||
@@ -637,7 +788,6 @@ export default {
|
||||
margin-top: 15px;
|
||||
|
||||
/* 导航按钮样式 (首页、上一页、下一页) */
|
||||
|
||||
.pagination-btn:first-child,
|
||||
.pagination-btn:nth-child(2),
|
||||
.pagination-btn:nth-last-child(2) {
|
||||
@@ -663,7 +813,6 @@ export default {
|
||||
}
|
||||
|
||||
/* 数字按钮样式 */
|
||||
|
||||
.pagination-btn:not(:first-child):not(:nth-child(2)):not(:nth-last-child(2)) {
|
||||
min-width: 28px;
|
||||
height: 32px;
|
||||
@@ -697,6 +846,4 @@ export default {
|
||||
margin-left: 10px;
|
||||
}
|
||||
}
|
||||
|
||||
</style>
|
||||
|
||||
|
||||
@@ -0,0 +1,536 @@
|
||||
<template>
|
||||
<div class="welcome">
|
||||
<HeaderBar />
|
||||
|
||||
<div class="operation-bar">
|
||||
<h2 class="page-title">参数管理</h2>
|
||||
<div class="right-operations">
|
||||
<el-input placeholder="请输入参数编码查询" v-model="searchCode" class="search-input"
|
||||
@keyup.enter.native="handleSearch" />
|
||||
<el-button class="btn-search" @click="handleSearch">搜索</el-button>
|
||||
<el-button type="primary" @click="showAddDialog">新增参数</el-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="main-wrapper">
|
||||
<div class="content-panel">
|
||||
<div class="content-area">
|
||||
<el-card class="params-card" shadow="never">
|
||||
<el-table ref="paramsTable" :data="paramsList" class="transparent-table"
|
||||
:header-cell-class-name="headerCellClassName">
|
||||
<el-table-column label="选择" type="selection" align="center" width="120"></el-table-column>
|
||||
<el-table-column label="参数编码" prop="paramCode" align="center"></el-table-column>
|
||||
<el-table-column label="参数值" prop="paramValue" align="center"></el-table-column>
|
||||
<el-table-column label="备注" prop="remark" align="center"></el-table-column>
|
||||
<el-table-column label="操作" align="center">
|
||||
<template slot-scope="scope">
|
||||
<el-button size="mini" type="text" @click="editParam(scope.row)">编辑</el-button>
|
||||
<el-button size="mini" type="text" @click="deleteParam(scope.row)">删除</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
|
||||
<div class="table_bottom">
|
||||
<div class="ctrl_btn">
|
||||
<el-button size="mini" type="primary" class="select-all-btn"
|
||||
@click="handleSelectAll">全选</el-button>
|
||||
<el-button size="mini" type="danger" icon="el-icon-delete"
|
||||
@click="batchDelete">删除</el-button>
|
||||
</div>
|
||||
<div class="custom-pagination">
|
||||
<button class="pagination-btn" :disabled="currentPage === 1" @click="goFirst">
|
||||
首页
|
||||
</button>
|
||||
<button class="pagination-btn" :disabled="currentPage === 1" @click="goPrev">
|
||||
上一页
|
||||
</button>
|
||||
<button v-for="page in visiblePages" :key="page" class="pagination-btn"
|
||||
:class="{ active: page === currentPage }" @click="goToPage(page)">
|
||||
{{ page }}
|
||||
</button>
|
||||
<button class="pagination-btn" :disabled="currentPage === pageCount" @click="goNext">
|
||||
下一页
|
||||
</button>
|
||||
<span class="total-text">共{{ total }}条记录</span>
|
||||
</div>
|
||||
</div>
|
||||
</el-card>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 新增/编辑参数对话框 -->
|
||||
<el-dialog :title="dialogTitle" :visible.sync="dialogVisible" width="500px">
|
||||
<el-form :model="paramForm" :rules="rules" ref="paramForm" label-width="100px">
|
||||
<el-form-item label="参数编码" prop="paramCode">
|
||||
<el-input v-model="paramForm.paramCode" placeholder="请输入参数编码"></el-input>
|
||||
</el-form-item>
|
||||
<el-form-item label="参数值" prop="paramValue">
|
||||
<el-input v-model="paramForm.paramValue" placeholder="请输入参数值"></el-input>
|
||||
</el-form-item>
|
||||
<el-form-item label="备注" prop="remark">
|
||||
<el-input type="textarea" v-model="paramForm.remark" placeholder="请输入备注"></el-input>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<div slot="footer" class="dialog-footer">
|
||||
<el-button @click="dialogVisible = false">取 消</el-button>
|
||||
<el-button type="primary" @click="submitForm">确 定</el-button>
|
||||
</div>
|
||||
</el-dialog>
|
||||
|
||||
<div class="copyright">©2025 xiaozhi-esp32-server</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import Api from "@/apis/api";
|
||||
import HeaderBar from "@/components/HeaderBar.vue";
|
||||
|
||||
export default {
|
||||
components: { HeaderBar },
|
||||
data() {
|
||||
return {
|
||||
searchCode: "",
|
||||
paramsList: [],
|
||||
currentPage: 1,
|
||||
pageSize: 5,
|
||||
total: 0,
|
||||
dialogVisible: false,
|
||||
dialogTitle: "新增参数",
|
||||
paramForm: {
|
||||
id: null,
|
||||
paramCode: "",
|
||||
paramValue: "",
|
||||
remark: ""
|
||||
},
|
||||
rules: {
|
||||
paramCode: [
|
||||
{ required: true, message: "请输入参数编码", trigger: "blur" }
|
||||
],
|
||||
paramValue: [
|
||||
{ required: true, message: "请输入参数值", trigger: "blur" }
|
||||
]
|
||||
}
|
||||
};
|
||||
},
|
||||
created() {
|
||||
this.fetchParams();
|
||||
},
|
||||
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: {
|
||||
fetchParams() {
|
||||
Api.admin.getParamsList(
|
||||
{
|
||||
page: this.currentPage,
|
||||
limit: this.pageSize,
|
||||
paramCode: this.searchCode,
|
||||
},
|
||||
({ data }) => {
|
||||
if (data.code === 0) {
|
||||
this.paramsList = data.data.list;
|
||||
this.total = data.data.total;
|
||||
}
|
||||
}
|
||||
);
|
||||
},
|
||||
handleSearch() {
|
||||
this.currentPage = 1;
|
||||
this.fetchParams();
|
||||
},
|
||||
handleSelectAll() {
|
||||
this.$refs.paramsTable.toggleAllSelection();
|
||||
},
|
||||
showAddDialog() {
|
||||
this.dialogTitle = "新增参数";
|
||||
this.paramForm = {
|
||||
id: null,
|
||||
paramCode: "",
|
||||
paramValue: "",
|
||||
remark: ""
|
||||
};
|
||||
this.dialogVisible = true;
|
||||
},
|
||||
editParam(row) {
|
||||
this.dialogTitle = "编辑参数";
|
||||
this.paramForm = { ...row };
|
||||
this.dialogVisible = true;
|
||||
},
|
||||
submitForm() {
|
||||
this.$refs.paramForm.validate((valid) => {
|
||||
if (valid) {
|
||||
if (this.paramForm.id) {
|
||||
// 编辑
|
||||
Api.admin.updateParam(this.paramForm, ({ data }) => {
|
||||
if (data.code === 0) {
|
||||
this.$message.success("修改成功");
|
||||
this.dialogVisible = false;
|
||||
this.fetchParams();
|
||||
}
|
||||
});
|
||||
} else {
|
||||
// 新增
|
||||
Api.admin.addParam(this.paramForm, ({ data }) => {
|
||||
if (data.code === 0) {
|
||||
this.$message.success("新增成功");
|
||||
this.dialogVisible = false;
|
||||
this.fetchParams();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
},
|
||||
deleteParam(row) {
|
||||
this.$confirm("确定要删除该参数吗?", "警告", {
|
||||
confirmButtonText: "确定",
|
||||
cancelButtonText: "取消",
|
||||
type: "warning",
|
||||
})
|
||||
.then(() => {
|
||||
Api.admin.deleteParam(row.id, ({ data }) => {
|
||||
if (data.code === 0) {
|
||||
this.$message.success("删除成功");
|
||||
this.fetchParams();
|
||||
}
|
||||
});
|
||||
})
|
||||
.catch(() => { });
|
||||
},
|
||||
batchDelete() {
|
||||
const selectedParams = this.$refs.paramsTable.selection;
|
||||
if (selectedParams.length === 0) {
|
||||
this.$message.warning("请先选择需要删除的参数");
|
||||
return;
|
||||
}
|
||||
|
||||
this.$confirm(`确定要删除选中的${selectedParams.length}个参数吗?`, "警告", {
|
||||
confirmButtonText: "确定",
|
||||
cancelButtonText: "取消",
|
||||
type: "warning",
|
||||
})
|
||||
.then(() => {
|
||||
const ids = selectedParams.map(item => item.id);
|
||||
Api.admin.batchDeleteParams(ids, ({ data }) => {
|
||||
if (data.code === 0) {
|
||||
this.$message.success("删除成功");
|
||||
this.fetchParams();
|
||||
}
|
||||
});
|
||||
})
|
||||
.catch(() => { });
|
||||
},
|
||||
headerCellClassName({ columnIndex }) {
|
||||
if (columnIndex === 0) {
|
||||
return "custom-selection-header";
|
||||
}
|
||||
return "";
|
||||
},
|
||||
goFirst() {
|
||||
this.currentPage = 1;
|
||||
this.fetchParams();
|
||||
},
|
||||
goPrev() {
|
||||
if (this.currentPage > 1) {
|
||||
this.currentPage--;
|
||||
this.fetchParams();
|
||||
}
|
||||
},
|
||||
goNext() {
|
||||
if (this.currentPage < this.pageCount) {
|
||||
this.currentPage++;
|
||||
this.fetchParams();
|
||||
}
|
||||
},
|
||||
goToPage(page) {
|
||||
this.currentPage = page;
|
||||
this.fetchParams();
|
||||
}
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.welcome {
|
||||
min-width: 900px;
|
||||
min-height: 506px;
|
||||
height: 100vh;
|
||||
display: flex;
|
||||
position: relative;
|
||||
flex-direction: column;
|
||||
background-size: cover;
|
||||
background: linear-gradient(to bottom right, #dce8ff, #e4eeff, #e6cbfd) center;
|
||||
-webkit-background-size: cover;
|
||||
-o-background-size: cover;
|
||||
}
|
||||
|
||||
.main-wrapper {
|
||||
margin: 5px 22px;
|
||||
border-radius: 15px;
|
||||
min-height: 600px;
|
||||
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.1);
|
||||
position: relative;
|
||||
background: rgba(237, 242, 255, 0.5);
|
||||
}
|
||||
|
||||
.operation-bar {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 16px 24px;
|
||||
}
|
||||
|
||||
.page-title {
|
||||
font-size: 24px;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.right-operations {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
margin-left: auto;
|
||||
}
|
||||
|
||||
.search-input {
|
||||
width: 240px;
|
||||
}
|
||||
|
||||
.btn-search {
|
||||
background: linear-gradient(135deg, #6b8cff, #a966ff);
|
||||
border: none;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.content-panel {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
overflow: hidden;
|
||||
height: 100%;
|
||||
border-radius: 15px;
|
||||
background: transparent;
|
||||
border: 1px solid #fff;
|
||||
}
|
||||
|
||||
.content-area {
|
||||
flex: 1;
|
||||
height: 100%;
|
||||
min-width: 600px;
|
||||
overflow-x: auto;
|
||||
background-color: white;
|
||||
}
|
||||
|
||||
.params-card {
|
||||
background: white;
|
||||
border: none;
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
.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;
|
||||
}
|
||||
}
|
||||
|
||||
.copyright {
|
||||
text-align: center;
|
||||
color: #979db1;
|
||||
font-size: 12px;
|
||||
font-weight: 400;
|
||||
margin-top: auto;
|
||||
padding: 30px 0 20px;
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.custom-pagination {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
margin-top: 15px;
|
||||
|
||||
.pagination-btn:first-child,
|
||||
.pagination-btn:nth-child(2),
|
||||
.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-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;
|
||||
|
||||
.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(.custom-selection-header) {
|
||||
.el-checkbox {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
&::after {
|
||||
content: "选择";
|
||||
display: inline-block;
|
||||
color: black;
|
||||
font-weight: bold;
|
||||
padding-bottom: 18px;
|
||||
}
|
||||
}
|
||||
|
||||
:deep(.el-checkbox__inner) {
|
||||
background-color: #eeeeee !important;
|
||||
border-color: #cccccc !important;
|
||||
}
|
||||
|
||||
:deep(.el-checkbox__inner:hover) {
|
||||
border-color: #cccccc !important;
|
||||
}
|
||||
|
||||
:deep(.el-checkbox__input.is-checked .el-checkbox__inner) {
|
||||
background-color: #5f70f3 !important;
|
||||
border-color: #5f70f3 !important;
|
||||
}
|
||||
|
||||
@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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user