Merge branch 'main' into tts-response

# Conflicts:
#	main/manager-web/.env.development
This commit is contained in:
hrz
2025-04-09 21:46:59 +08:00
129 changed files with 8298 additions and 2083 deletions
+10 -5
View File
@@ -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
View File
@@ -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
View File
@@ -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`。
+1 -1
View File
@@ -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
View File
@@ -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
View File
@@ -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
+3 -8
View File
@@ -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);
}
}
}
@@ -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();
}
@@ -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;
}
}
@@ -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);
}
}
@@ -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);
}
@@ -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;
}
@@ -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;
@@ -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);
}
@@ -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;
}
}
@@ -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);
}
}
@@ -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);
}
@@ -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);
}
}
}
@@ -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);
}
}
}
}
@@ -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);
}
@@ -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
@@ -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-8174G信号观察地球
[]
@@ -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,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,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}
+4
View File
@@ -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
View File
@@ -1 +1 @@
VUE_APP_API_BASE_URL=https://2662r3426b.vicp.fun
VUE_APP_API_BASE_URL=/xiaozhi
+1 -1
View File
@@ -1,4 +1,4 @@
本文档是开发类文档,如需部署小智服务端,[点击这里查看部署教程](.././FAQ.md#%E4%BD%BF%E7%94%A8%E6%96%B9%E5%BC%8F-)
本文档是开发类文档,如需部署小智服务端,[点击这里查看部署教程](../../../docs/Deployment.md)
# xiaozhi
+11 -12
View File
@@ -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'
]
}
+1 -1
View File
@@ -16,4 +16,4 @@
"scripthost"
]
}
}
}
+1 -1
View File
@@ -8847,4 +8847,4 @@
}
}
}
}
}
+1 -1
View File
@@ -43,4 +43,4 @@
"*.scss",
"*.vue"
]
}
}
+3 -4
View File
@@ -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>
+15 -8
View File
@@ -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,
}
+9 -11
View File
@@ -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)
+106 -23
View File
@@ -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()
}
}
+9 -9
View File
@@ -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();
+26 -26
View File
@@ -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();
+170 -19
View File
@@ -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();
},
}
+102
View File
@@ -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();
}
}
+24 -7
View File
@@ -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>
+145 -122
View File
@@ -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) => {
+22 -12
View File
@@ -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>
+60 -47
View File
@@ -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>
+626 -134
View File
@@ -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
})
}
}
}
+6 -6
View File
@@ -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);
+16 -6
View File
@@ -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({
+5 -5
View File
@@ -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 -1
View File
@@ -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 -1
View File
@@ -1,6 +1,6 @@
import { Message } from 'element-ui'
import router from '../router'
import Constant from '../utils/constant'
import { Message } from 'element-ui'
/**
* 判断用户是否登录
+47 -61
View File
@@ -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;
}
+233 -86
View File
@@ -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