mirror of
https://github.com/xinnan-tech/xiaozhi-esp32-server.git
synced 2026-07-23 15:43:54 +08:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
783f17e9e8 | ||
|
|
9e305c2840 | ||
|
|
78fa5d3110 | ||
|
|
13c0a966ec | ||
|
|
d987619946 | ||
|
|
8fb9498fe6 | ||
|
|
b8f64a6693 | ||
|
|
e31bb047bf | ||
|
|
6897b813a6 | ||
|
|
83f86406b5 | ||
|
|
dde237ff0d | ||
|
|
6a4887e252 | ||
|
|
d945b94910 | ||
|
|
bc7dcf791d | ||
|
|
66c71fe3b7 | ||
|
|
16a7aeec3b | ||
|
|
94605345e5 | ||
|
|
ec9fa32c81 | ||
|
|
264aa15281 | ||
|
|
180a6ff824 | ||
|
|
5904ee1571 | ||
|
|
bc99341f85 |
@@ -164,3 +164,4 @@ main/manager-api/.vscode
|
|||||||
# Ignore webpack cache directory
|
# Ignore webpack cache directory
|
||||||
main/manager-web/.webpack_cache/
|
main/manager-web/.webpack_cache/
|
||||||
main/xiaozhi-server/mysql
|
main/xiaozhi-server/mysql
|
||||||
|
uploadfile
|
||||||
|
|||||||
+10
-1
@@ -4,7 +4,16 @@
|
|||||||
先按照这个教程配置项目环境[《Windows搭建 ESP IDF 5.3.2开发环境以及编译小智》](https://icnynnzcwou8.feishu.cn/wiki/JEYDwTTALi5s2zkGlFGcDiRknXf)
|
先按照这个教程配置项目环境[《Windows搭建 ESP IDF 5.3.2开发环境以及编译小智》](https://icnynnzcwou8.feishu.cn/wiki/JEYDwTTALi5s2zkGlFGcDiRknXf)
|
||||||
|
|
||||||
## 第2步 打开配置文件
|
## 第2步 打开配置文件
|
||||||
配置好编译环境后,下载虾哥[xiaozhi-esp32](https://github.com/78/xiaozhi-esp32)项目源码,进入虾哥项目,打开`xiaozhi-esp32/main/Kconfig.projbuild`文件。
|
配置好编译环境后,下载虾哥iaozhi-esp32项目源码,
|
||||||
|
|
||||||
|
从这里下载虾哥[xiaozhi-esp32项目源码](https://github.com/78/xiaozhi-esp32/archive/refs/tags/v1.6.0.zip)。
|
||||||
|
|
||||||
|
从这里下载虾哥[xiaozhi-esp32项目源码](https://github.com/78/xiaozhi-esp32/archive/refs/tags/v1.6.0.zip)。
|
||||||
|
|
||||||
|
从这里下载虾哥[xiaozhi-esp32项目源码](https://github.com/78/xiaozhi-esp32/archive/refs/tags/v1.6.0.zip)。
|
||||||
|
|
||||||
|
下载后,解压缩包,打开`xiaozhi-esp32/main/Kconfig.projbuild`文件。
|
||||||
|
|
||||||
|
|
||||||
## 第3步 修改WEBSOCKET地址
|
## 第3步 修改WEBSOCKET地址
|
||||||
找到`WEBSOCKET_URL`的`default`的内容,把`wss://api.tenclass.net/xiaozhi/v1/`
|
找到`WEBSOCKET_URL`的`default`的内容,把`wss://api.tenclass.net/xiaozhi/v1/`
|
||||||
|
|||||||
@@ -84,6 +84,11 @@ public interface Constant {
|
|||||||
*/
|
*/
|
||||||
String SERVER_SECRET = "server.secret";
|
String SERVER_SECRET = "server.secret";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* websocket地址
|
||||||
|
*/
|
||||||
|
String SERVER_WEBSOCKET = "server.websocket";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 是否允许用户注册
|
* 是否允许用户注册
|
||||||
*/
|
*/
|
||||||
@@ -167,5 +172,5 @@ public interface Constant {
|
|||||||
/**
|
/**
|
||||||
* 版本号
|
* 版本号
|
||||||
*/
|
*/
|
||||||
public static final String VERSION = "0.3.8";
|
public static final String VERSION = "0.3.10";
|
||||||
}
|
}
|
||||||
@@ -80,6 +80,20 @@ public class RedisKeys {
|
|||||||
* 获取版本号Key
|
* 获取版本号Key
|
||||||
*/
|
*/
|
||||||
public static String getVersionKey() {
|
public static String getVersionKey() {
|
||||||
return "system:version";
|
return "sys:version";
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* OTA固件ID的Key
|
||||||
|
*/
|
||||||
|
public static String getOtaIdKey(String uuid) {
|
||||||
|
return "ota:id:" + uuid;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* OTA固件下载次数的Key
|
||||||
|
*/
|
||||||
|
public static String getOtaDownloadCountKey(String uuid) {
|
||||||
|
return "ota:download:count:" + uuid;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+1
-1
@@ -291,7 +291,7 @@ public class ConfigServiceImpl implements ConfigService {
|
|||||||
|
|
||||||
result.put("selected_module", selectedModule);
|
result.put("selected_module", selectedModule);
|
||||||
if (StringUtils.isNotBlank(prompt)) {
|
if (StringUtils.isNotBlank(prompt)) {
|
||||||
prompt = prompt.replace("{{assistant_name}}", "小智");
|
prompt = prompt.replace("{{assistant_name}}", StringUtils.isBlank(assistantName) ? "小智" : assistantName);
|
||||||
}
|
}
|
||||||
result.put("prompt", prompt);
|
result.put("prompt", prompt);
|
||||||
}
|
}
|
||||||
|
|||||||
+30
-3
@@ -15,25 +15,32 @@ import org.springframework.web.bind.annotation.RestController;
|
|||||||
import com.fasterxml.jackson.annotation.JsonInclude;
|
import com.fasterxml.jackson.annotation.JsonInclude;
|
||||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||||
|
|
||||||
|
import io.swagger.v3.oas.annotations.Hidden;
|
||||||
import io.swagger.v3.oas.annotations.Operation;
|
import io.swagger.v3.oas.annotations.Operation;
|
||||||
import io.swagger.v3.oas.annotations.Parameter;
|
import io.swagger.v3.oas.annotations.Parameter;
|
||||||
import io.swagger.v3.oas.annotations.enums.ParameterIn;
|
import io.swagger.v3.oas.annotations.enums.ParameterIn;
|
||||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||||
import lombok.RequiredArgsConstructor;
|
import lombok.RequiredArgsConstructor;
|
||||||
import lombok.SneakyThrows;
|
import lombok.SneakyThrows;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import xiaozhi.common.constant.Constant;
|
||||||
import xiaozhi.modules.device.dto.DeviceReportReqDTO;
|
import xiaozhi.modules.device.dto.DeviceReportReqDTO;
|
||||||
import xiaozhi.modules.device.dto.DeviceReportRespDTO;
|
import xiaozhi.modules.device.dto.DeviceReportRespDTO;
|
||||||
|
import xiaozhi.modules.device.entity.DeviceEntity;
|
||||||
import xiaozhi.modules.device.service.DeviceService;
|
import xiaozhi.modules.device.service.DeviceService;
|
||||||
import xiaozhi.modules.device.utils.NetworkUtil;
|
import xiaozhi.modules.device.utils.NetworkUtil;
|
||||||
|
import xiaozhi.modules.sys.service.SysParamsService;
|
||||||
|
|
||||||
@Tag(name = "设备管理", description = "OTA 相关接口")
|
@Tag(name = "设备管理", description = "OTA 相关接口")
|
||||||
|
@Slf4j
|
||||||
@RestController
|
@RestController
|
||||||
@RequiredArgsConstructor
|
@RequiredArgsConstructor
|
||||||
@RequestMapping("/ota/")
|
@RequestMapping("/ota/")
|
||||||
public class OTAController {
|
public class OTAController {
|
||||||
private final DeviceService deviceService;
|
private final DeviceService deviceService;
|
||||||
|
private final SysParamsService sysParamsService;
|
||||||
|
|
||||||
@Operation(summary = "检查 OTA 版本和设备激活状态")
|
@Operation(summary = "OTA版本和设备激活状态检查")
|
||||||
@PostMapping
|
@PostMapping
|
||||||
public ResponseEntity<String> checkOTAVersion(
|
public ResponseEntity<String> checkOTAVersion(
|
||||||
@RequestBody DeviceReportReqDTO deviceReportReqDTO,
|
@RequestBody DeviceReportReqDTO deviceReportReqDTO,
|
||||||
@@ -54,9 +61,29 @@ public class OTAController {
|
|||||||
return createResponse(deviceService.checkDeviceActive(macAddress, clientId, deviceReportReqDTO));
|
return createResponse(deviceService.checkDeviceActive(macAddress, clientId, deviceReportReqDTO));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Operation(summary = "设备快速检查激活状态")
|
||||||
|
@PostMapping("activate")
|
||||||
|
public ResponseEntity<String> activateDevice(
|
||||||
|
@Parameter(name = "Device-Id", description = "设备唯一标识", required = true, in = ParameterIn.HEADER) @RequestHeader("Device-Id") String deviceId,
|
||||||
|
@Parameter(name = "Client-Id", description = "客户端标识", required = false, in = ParameterIn.HEADER) @RequestHeader(value = "Client-Id", required = false) String clientId) {
|
||||||
|
if (StringUtils.isBlank(deviceId)) {
|
||||||
|
return ResponseEntity.status(202).build();
|
||||||
|
}
|
||||||
|
DeviceEntity device = deviceService.getDeviceByMacAddress(deviceId);
|
||||||
|
if (device == null) {
|
||||||
|
return ResponseEntity.status(202).build();
|
||||||
|
}
|
||||||
|
return ResponseEntity.ok("success");
|
||||||
|
}
|
||||||
|
|
||||||
@GetMapping
|
@GetMapping
|
||||||
public ResponseEntity<String> getOTAPrompt() {
|
@Hidden
|
||||||
return createResponse(DeviceReportRespDTO.createError("请提交正确的ota参数"));
|
public ResponseEntity<String> getOTA() {
|
||||||
|
String wsUrl = sysParamsService.getValue(Constant.SERVER_WEBSOCKET, true);
|
||||||
|
if (StringUtils.isBlank(wsUrl) || wsUrl.equals("null")) {
|
||||||
|
return ResponseEntity.ok("OTA接口不正常,缺少websocket地址");
|
||||||
|
}
|
||||||
|
return ResponseEntity.ok("OTA接口运行正常,websocket集群数量:" + wsUrl.split(";").length);
|
||||||
}
|
}
|
||||||
|
|
||||||
@SneakyThrows
|
@SneakyThrows
|
||||||
|
|||||||
+291
@@ -0,0 +1,291 @@
|
|||||||
|
package xiaozhi.modules.device.controller;
|
||||||
|
|
||||||
|
import java.io.File;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.nio.file.Files;
|
||||||
|
import java.nio.file.Path;
|
||||||
|
import java.nio.file.Paths;
|
||||||
|
import java.security.MessageDigest;
|
||||||
|
import java.security.NoSuchAlgorithmException;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.UUID;
|
||||||
|
|
||||||
|
import org.apache.commons.lang3.StringUtils;
|
||||||
|
import org.apache.shiro.authz.annotation.RequiresPermissions;
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
import org.springframework.http.HttpHeaders;
|
||||||
|
import org.springframework.http.HttpStatus;
|
||||||
|
import org.springframework.http.MediaType;
|
||||||
|
import org.springframework.http.ResponseEntity;
|
||||||
|
import org.springframework.web.bind.annotation.DeleteMapping;
|
||||||
|
import org.springframework.web.bind.annotation.GetMapping;
|
||||||
|
import org.springframework.web.bind.annotation.PathVariable;
|
||||||
|
import org.springframework.web.bind.annotation.PostMapping;
|
||||||
|
import org.springframework.web.bind.annotation.PutMapping;
|
||||||
|
import org.springframework.web.bind.annotation.RequestBody;
|
||||||
|
import org.springframework.web.bind.annotation.RequestMapping;
|
||||||
|
import org.springframework.web.bind.annotation.RequestParam;
|
||||||
|
import org.springframework.web.bind.annotation.RestController;
|
||||||
|
import org.springframework.web.multipart.MultipartFile;
|
||||||
|
|
||||||
|
import io.swagger.v3.oas.annotations.Operation;
|
||||||
|
import io.swagger.v3.oas.annotations.Parameter;
|
||||||
|
import io.swagger.v3.oas.annotations.Parameters;
|
||||||
|
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import xiaozhi.common.constant.Constant;
|
||||||
|
import xiaozhi.common.page.PageData;
|
||||||
|
import xiaozhi.common.redis.RedisKeys;
|
||||||
|
import xiaozhi.common.redis.RedisUtils;
|
||||||
|
import xiaozhi.common.utils.Result;
|
||||||
|
import xiaozhi.common.validator.ValidatorUtils;
|
||||||
|
import xiaozhi.modules.device.entity.OtaEntity;
|
||||||
|
import xiaozhi.modules.device.service.OtaService;
|
||||||
|
|
||||||
|
@Tag(name = "设备管理", description = "OTA 相关接口")
|
||||||
|
@Slf4j
|
||||||
|
@RestController
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
@RequestMapping("/otaMag")
|
||||||
|
public class OTAMagController {
|
||||||
|
private static final Logger logger = LoggerFactory.getLogger(OTAController.class);
|
||||||
|
private final OtaService otaService;
|
||||||
|
private final RedisUtils redisUtils;
|
||||||
|
|
||||||
|
@GetMapping
|
||||||
|
@Operation(summary = "分页查询 OTA 固件信息")
|
||||||
|
@Parameters({
|
||||||
|
@Parameter(name = Constant.PAGE, description = "当前页码,从1开始", required = true),
|
||||||
|
@Parameter(name = Constant.LIMIT, description = "每页显示记录数", required = true)
|
||||||
|
})
|
||||||
|
@RequiresPermissions("sys:role:superAdmin")
|
||||||
|
public Result<PageData<OtaEntity>> page(@Parameter(hidden = true) @RequestParam Map<String, Object> params) {
|
||||||
|
ValidatorUtils.validateEntity(params);
|
||||||
|
PageData<OtaEntity> page = otaService.page(params);
|
||||||
|
return new Result<PageData<OtaEntity>>().ok(page);
|
||||||
|
}
|
||||||
|
|
||||||
|
@GetMapping("{id}")
|
||||||
|
@Operation(summary = "信息 OTA 固件信息")
|
||||||
|
@RequiresPermissions("sys:role:superAdmin")
|
||||||
|
public Result<OtaEntity> get(@PathVariable("id") String id) {
|
||||||
|
OtaEntity data = otaService.selectById(id);
|
||||||
|
return new Result<OtaEntity>().ok(data);
|
||||||
|
}
|
||||||
|
|
||||||
|
@PostMapping
|
||||||
|
@Operation(summary = "保存 OTA 固件信息")
|
||||||
|
@RequiresPermissions("sys:role:superAdmin")
|
||||||
|
public Result<Void> save(@RequestBody OtaEntity entity) {
|
||||||
|
if (entity == null) {
|
||||||
|
return new Result<Void>().error("固件信息不能为空");
|
||||||
|
}
|
||||||
|
if (StringUtils.isBlank(entity.getFirmwareName())) {
|
||||||
|
return new Result<Void>().error("固件名称不能为空");
|
||||||
|
}
|
||||||
|
if (StringUtils.isBlank(entity.getType())) {
|
||||||
|
return new Result<Void>().error("固件类型不能为空");
|
||||||
|
}
|
||||||
|
if (StringUtils.isBlank(entity.getVersion())) {
|
||||||
|
return new Result<Void>().error("版本号不能为空");
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
otaService.save(entity);
|
||||||
|
return new Result<Void>();
|
||||||
|
} catch (RuntimeException e) {
|
||||||
|
return new Result<Void>().error(e.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@DeleteMapping("/{id}")
|
||||||
|
@Operation(summary = "OTA 删除")
|
||||||
|
@RequiresPermissions("sys:role:superAdmin")
|
||||||
|
public Result<Void> delete(@PathVariable("id") String[] ids) {
|
||||||
|
if (ids == null || ids.length == 0) {
|
||||||
|
return new Result<Void>().error("删除的固件ID不能为空");
|
||||||
|
}
|
||||||
|
otaService.delete(ids);
|
||||||
|
return new Result<Void>();
|
||||||
|
}
|
||||||
|
|
||||||
|
@PutMapping("/{id}")
|
||||||
|
@Operation(summary = "修改 OTA 固件信息")
|
||||||
|
@RequiresPermissions("sys:role:superAdmin")
|
||||||
|
public Result<?> update(@PathVariable("id") String id, @RequestBody OtaEntity entity) {
|
||||||
|
if (entity == null) {
|
||||||
|
return new Result<>().error("固件信息不能为空");
|
||||||
|
}
|
||||||
|
entity.setId(id);
|
||||||
|
try {
|
||||||
|
otaService.update(entity);
|
||||||
|
return new Result<>();
|
||||||
|
} catch (RuntimeException e) {
|
||||||
|
return new Result<>().error(e.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@GetMapping("/getDownloadUrl/{id}")
|
||||||
|
@Operation(summary = "获取 OTA 固件下载链接")
|
||||||
|
@RequiresPermissions("sys:role:superAdmin")
|
||||||
|
public Result<String> getDownloadUrl(@PathVariable("id") String id) {
|
||||||
|
String uuid = UUID.randomUUID().toString();
|
||||||
|
redisUtils.set(RedisKeys.getOtaIdKey(uuid), id);
|
||||||
|
return new Result<String>().ok(uuid);
|
||||||
|
}
|
||||||
|
|
||||||
|
@GetMapping("/download/{uuid}")
|
||||||
|
@Operation(summary = "下载固件文件")
|
||||||
|
public ResponseEntity<byte[]> downloadFirmware(@PathVariable("uuid") String uuid) {
|
||||||
|
String id = (String) redisUtils.get(RedisKeys.getOtaIdKey(uuid));
|
||||||
|
if (StringUtils.isBlank(id)) {
|
||||||
|
return ResponseEntity.notFound().build();
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查下载次数
|
||||||
|
String downloadCountKey = RedisKeys.getOtaDownloadCountKey(uuid);
|
||||||
|
Integer downloadCount = (Integer) redisUtils.get(downloadCountKey);
|
||||||
|
if (downloadCount == null) {
|
||||||
|
downloadCount = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 如果下载次数超过3次,返回404
|
||||||
|
if (downloadCount >= 3) {
|
||||||
|
redisUtils.delete(downloadCountKey);
|
||||||
|
redisUtils.delete(RedisKeys.getOtaIdKey(uuid));
|
||||||
|
logger.warn("Download limit exceeded for UUID: {}", uuid);
|
||||||
|
return ResponseEntity.notFound().build();
|
||||||
|
}
|
||||||
|
|
||||||
|
redisUtils.set(downloadCountKey, downloadCount + 1);
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 获取固件信息
|
||||||
|
OtaEntity otaEntity = otaService.selectById(id);
|
||||||
|
if (otaEntity == null || StringUtils.isBlank(otaEntity.getFirmwarePath())) {
|
||||||
|
logger.warn("Firmware not found or path is empty for ID: {}", id);
|
||||||
|
return ResponseEntity.notFound().build();
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取文件路径 - 确保路径是绝对路径或正确的相对路径
|
||||||
|
String firmwarePath = otaEntity.getFirmwarePath();
|
||||||
|
Path path;
|
||||||
|
|
||||||
|
// 检查是否是绝对路径
|
||||||
|
if (Paths.get(firmwarePath).isAbsolute()) {
|
||||||
|
path = Paths.get(firmwarePath);
|
||||||
|
} else {
|
||||||
|
// 如果是相对路径,则从当前工作目录解析
|
||||||
|
path = Paths.get(System.getProperty("user.dir"), firmwarePath);
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.info("Attempting to download firmware for ID: {}, DB path: {}, resolved path: {}",
|
||||||
|
id, firmwarePath, path.toAbsolutePath());
|
||||||
|
|
||||||
|
if (!Files.exists(path) || !Files.isRegularFile(path)) {
|
||||||
|
// 尝试直接从firmware目录下查找文件名
|
||||||
|
String fileName = new File(firmwarePath).getName();
|
||||||
|
Path altPath = Paths.get(System.getProperty("user.dir"), "firmware", fileName);
|
||||||
|
|
||||||
|
logger.info("File not found at primary path, trying alternative path: {}", altPath.toAbsolutePath());
|
||||||
|
|
||||||
|
if (Files.exists(altPath) && Files.isRegularFile(altPath)) {
|
||||||
|
path = altPath;
|
||||||
|
} else {
|
||||||
|
logger.error("Firmware file not found at either path: {} or {}",
|
||||||
|
path.toAbsolutePath(), altPath.toAbsolutePath());
|
||||||
|
return ResponseEntity.notFound().build();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 读取文件内容
|
||||||
|
byte[] fileContent = Files.readAllBytes(path);
|
||||||
|
|
||||||
|
// 设置响应头
|
||||||
|
String originalFilename = otaEntity.getType() + "_" + otaEntity.getVersion();
|
||||||
|
if (firmwarePath.contains(".")) {
|
||||||
|
String extension = firmwarePath.substring(firmwarePath.lastIndexOf("."));
|
||||||
|
originalFilename += extension;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 清理文件名,移除不安全字符
|
||||||
|
String safeFilename = originalFilename.replaceAll("[^a-zA-Z0-9._-]", "_");
|
||||||
|
|
||||||
|
logger.info("Providing download for firmware ID: {}, filename: {}, size: {} bytes",
|
||||||
|
id, safeFilename, fileContent.length);
|
||||||
|
|
||||||
|
return ResponseEntity.ok()
|
||||||
|
.contentType(MediaType.APPLICATION_OCTET_STREAM)
|
||||||
|
.header(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=\"" + safeFilename + "\"")
|
||||||
|
.body(fileContent);
|
||||||
|
} catch (IOException e) {
|
||||||
|
logger.error("Error reading firmware file for ID: {}", id, e);
|
||||||
|
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).build();
|
||||||
|
} catch (Exception e) {
|
||||||
|
logger.error("Unexpected error during firmware download for ID: {}", id, e);
|
||||||
|
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).build();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@PostMapping("/upload")
|
||||||
|
@Operation(summary = "上传固件文件")
|
||||||
|
@RequiresPermissions("sys:role:superAdmin")
|
||||||
|
public Result<String> uploadFirmware(@RequestParam("file") MultipartFile file) {
|
||||||
|
if (file.isEmpty()) {
|
||||||
|
return new Result<String>().error("上传文件不能为空");
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查文件扩展名
|
||||||
|
String originalFilename = file.getOriginalFilename();
|
||||||
|
if (originalFilename == null) {
|
||||||
|
return new Result<String>().error("文件名不能为空");
|
||||||
|
}
|
||||||
|
|
||||||
|
String extension = originalFilename.substring(originalFilename.lastIndexOf(".")).toLowerCase();
|
||||||
|
if (!extension.equals(".bin") && !extension.equals(".apk")) {
|
||||||
|
return new Result<String>().error("只允许上传.bin和.apk格式的文件");
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 计算文件的MD5值
|
||||||
|
String md5 = calculateMD5(file);
|
||||||
|
|
||||||
|
// 设置存储路径
|
||||||
|
String uploadDir = "uploadfile";
|
||||||
|
Path uploadPath = Paths.get(uploadDir);
|
||||||
|
|
||||||
|
// 如果目录不存在,创建目录
|
||||||
|
if (!Files.exists(uploadPath)) {
|
||||||
|
Files.createDirectories(uploadPath);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 使用MD5作为文件名,固定使用.bin扩展名
|
||||||
|
String uniqueFileName = md5 + extension;
|
||||||
|
Path filePath = uploadPath.resolve(uniqueFileName);
|
||||||
|
|
||||||
|
// 检查文件是否已存在
|
||||||
|
if (Files.exists(filePath)) {
|
||||||
|
return new Result<String>().ok(filePath.toString());
|
||||||
|
}
|
||||||
|
|
||||||
|
// 保存文件
|
||||||
|
Files.copy(file.getInputStream(), filePath);
|
||||||
|
|
||||||
|
// 返回文件路径
|
||||||
|
return new Result<String>().ok(filePath.toString());
|
||||||
|
} catch (IOException | NoSuchAlgorithmException e) {
|
||||||
|
return new Result<String>().error("文件上传失败:" + e.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private String calculateMD5(MultipartFile file) throws IOException, NoSuchAlgorithmException {
|
||||||
|
MessageDigest md = MessageDigest.getInstance("MD5");
|
||||||
|
byte[] digest = md.digest(file.getBytes());
|
||||||
|
StringBuilder sb = new StringBuilder();
|
||||||
|
for (byte b : digest) {
|
||||||
|
sb.append(String.format("%02x", b));
|
||||||
|
}
|
||||||
|
return sb.toString();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
package xiaozhi.modules.device.dao;
|
||||||
|
|
||||||
|
import org.apache.ibatis.annotations.Mapper;
|
||||||
|
|
||||||
|
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||||
|
|
||||||
|
import xiaozhi.modules.device.entity.OtaEntity;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* OTA固件管理
|
||||||
|
*/
|
||||||
|
@Mapper
|
||||||
|
public interface OtaDao extends BaseMapper<OtaEntity> {
|
||||||
|
|
||||||
|
}
|
||||||
@@ -19,6 +19,9 @@ public class DeviceReportRespDTO {
|
|||||||
|
|
||||||
@Schema(description = "固件版本信息")
|
@Schema(description = "固件版本信息")
|
||||||
private Firmware firmware;
|
private Firmware firmware;
|
||||||
|
|
||||||
|
@Schema(description = "WebSocket配置")
|
||||||
|
private Websocket websocket;
|
||||||
|
|
||||||
@Getter
|
@Getter
|
||||||
@Setter
|
@Setter
|
||||||
@@ -44,6 +47,8 @@ public class DeviceReportRespDTO {
|
|||||||
@Schema(description = "激活码信息: 激活地址")
|
@Schema(description = "激活码信息: 激活地址")
|
||||||
private String message;
|
private String message;
|
||||||
|
|
||||||
|
@Schema(description = "挑战码")
|
||||||
|
private String challenge;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Getter
|
@Getter
|
||||||
@@ -58,4 +63,11 @@ public class DeviceReportRespDTO {
|
|||||||
@Schema(description = "时区偏移量,单位为分钟")
|
@Schema(description = "时区偏移量,单位为分钟")
|
||||||
private Integer timezone_offset;
|
private Integer timezone_offset;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Getter
|
||||||
|
@Setter
|
||||||
|
public static class Websocket {
|
||||||
|
@Schema(description = "WebSocket服务器地址")
|
||||||
|
private String url;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,61 @@
|
|||||||
|
package xiaozhi.modules.device.entity;
|
||||||
|
|
||||||
|
import java.util.Date;
|
||||||
|
|
||||||
|
import com.baomidou.mybatisplus.annotation.FieldFill;
|
||||||
|
import com.baomidou.mybatisplus.annotation.IdType;
|
||||||
|
import com.baomidou.mybatisplus.annotation.TableField;
|
||||||
|
import com.baomidou.mybatisplus.annotation.TableId;
|
||||||
|
import com.baomidou.mybatisplus.annotation.TableName;
|
||||||
|
|
||||||
|
import io.swagger.v3.oas.annotations.media.Schema;
|
||||||
|
import lombok.Data;
|
||||||
|
import lombok.EqualsAndHashCode;
|
||||||
|
|
||||||
|
@Data
|
||||||
|
@EqualsAndHashCode(callSuper = false)
|
||||||
|
@TableName("ai_ota")
|
||||||
|
@Schema(description = "固件信息")
|
||||||
|
public class OtaEntity {
|
||||||
|
|
||||||
|
@TableId(type = IdType.ASSIGN_UUID)
|
||||||
|
@Schema(description = "ID")
|
||||||
|
private String id;
|
||||||
|
|
||||||
|
@Schema(description = "固件名称")
|
||||||
|
private String firmwareName;
|
||||||
|
|
||||||
|
@Schema(description = "固件类型")
|
||||||
|
private String type;
|
||||||
|
|
||||||
|
@Schema(description = "版本号")
|
||||||
|
private String version;
|
||||||
|
|
||||||
|
@Schema(description = "文件大小(字节)")
|
||||||
|
private Long size;
|
||||||
|
|
||||||
|
@Schema(description = "备注/说明")
|
||||||
|
private String remark;
|
||||||
|
|
||||||
|
@Schema(description = "固件路径")
|
||||||
|
private String firmwarePath;
|
||||||
|
|
||||||
|
@Schema(description = "排序")
|
||||||
|
private Integer sort;
|
||||||
|
|
||||||
|
@Schema(description = "更新者")
|
||||||
|
@TableField(fill = FieldFill.UPDATE)
|
||||||
|
private Long updater;
|
||||||
|
|
||||||
|
@Schema(description = "更新时间")
|
||||||
|
@TableField(fill = FieldFill.UPDATE)
|
||||||
|
private Date updateDate;
|
||||||
|
|
||||||
|
@Schema(description = "创建者")
|
||||||
|
@TableField(fill = FieldFill.INSERT)
|
||||||
|
private Long creator;
|
||||||
|
|
||||||
|
@Schema(description = "创建时间")
|
||||||
|
@TableField(fill = FieldFill.INSERT)
|
||||||
|
private Date createDate;
|
||||||
|
}
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
package xiaozhi.modules.device.service;
|
||||||
|
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
import xiaozhi.common.page.PageData;
|
||||||
|
import xiaozhi.common.service.BaseService;
|
||||||
|
import xiaozhi.modules.device.entity.OtaEntity;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* OTA固件管理
|
||||||
|
*/
|
||||||
|
public interface OtaService extends BaseService<OtaEntity> {
|
||||||
|
PageData<OtaEntity> page(Map<String, Object> params);
|
||||||
|
|
||||||
|
boolean save(OtaEntity entity);
|
||||||
|
|
||||||
|
void update(OtaEntity entity);
|
||||||
|
|
||||||
|
void delete(String[] ids);
|
||||||
|
}
|
||||||
+30
-3
@@ -18,6 +18,7 @@ import com.baomidou.mybatisplus.core.conditions.update.UpdateWrapper;
|
|||||||
import com.baomidou.mybatisplus.core.metadata.IPage;
|
import com.baomidou.mybatisplus.core.metadata.IPage;
|
||||||
|
|
||||||
import cn.hutool.core.util.RandomUtil;
|
import cn.hutool.core.util.RandomUtil;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
import xiaozhi.common.constant.Constant;
|
import xiaozhi.common.constant.Constant;
|
||||||
import xiaozhi.common.exception.RenException;
|
import xiaozhi.common.exception.RenException;
|
||||||
import xiaozhi.common.page.PageData;
|
import xiaozhi.common.page.PageData;
|
||||||
@@ -36,6 +37,7 @@ import xiaozhi.modules.security.user.SecurityUser;
|
|||||||
import xiaozhi.modules.sys.service.SysParamsService;
|
import xiaozhi.modules.sys.service.SysParamsService;
|
||||||
import xiaozhi.modules.sys.service.SysUserUtilService;
|
import xiaozhi.modules.sys.service.SysUserUtilService;
|
||||||
|
|
||||||
|
@Slf4j
|
||||||
@Service
|
@Service
|
||||||
public class DeviceServiceImpl extends BaseServiceImpl<DeviceDao, DeviceEntity> implements DeviceService {
|
public class DeviceServiceImpl extends BaseServiceImpl<DeviceDao, DeviceEntity> implements DeviceService {
|
||||||
|
|
||||||
@@ -43,18 +45,18 @@ public class DeviceServiceImpl extends BaseServiceImpl<DeviceDao, DeviceEntity>
|
|||||||
|
|
||||||
private final SysUserUtilService sysUserUtilService;
|
private final SysUserUtilService sysUserUtilService;
|
||||||
|
|
||||||
private final String frontedUrl;
|
|
||||||
|
|
||||||
private final RedisTemplate<String, Object> redisTemplate;
|
private final RedisTemplate<String, Object> redisTemplate;
|
||||||
|
|
||||||
|
private final SysParamsService sysParamsService;
|
||||||
|
|
||||||
// 添加构造函数来初始化 deviceMapper
|
// 添加构造函数来初始化 deviceMapper
|
||||||
public DeviceServiceImpl(DeviceDao deviceDao, SysUserUtilService sysUserUtilService,
|
public DeviceServiceImpl(DeviceDao deviceDao, SysUserUtilService sysUserUtilService,
|
||||||
SysParamsService sysParamsService,
|
SysParamsService sysParamsService,
|
||||||
RedisTemplate<String, Object> redisTemplate) {
|
RedisTemplate<String, Object> redisTemplate) {
|
||||||
this.deviceDao = deviceDao;
|
this.deviceDao = deviceDao;
|
||||||
this.sysUserUtilService = sysUserUtilService;
|
this.sysUserUtilService = sysUserUtilService;
|
||||||
this.frontedUrl = sysParamsService.getValue(Constant.SERVER_FRONTED_URL, true);
|
|
||||||
this.redisTemplate = redisTemplate;
|
this.redisTemplate = redisTemplate;
|
||||||
|
this.sysParamsService = sysParamsService;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -132,6 +134,27 @@ public class DeviceServiceImpl extends BaseServiceImpl<DeviceDao, DeviceEntity>
|
|||||||
firmware.setUrl("http://localhost:8002/xiaozhi/ota/download");
|
firmware.setUrl("http://localhost:8002/xiaozhi/ota/download");
|
||||||
response.setFirmware(firmware);
|
response.setFirmware(firmware);
|
||||||
|
|
||||||
|
// 添加WebSocket配置
|
||||||
|
DeviceReportRespDTO.Websocket websocket = new DeviceReportRespDTO.Websocket();
|
||||||
|
// 从系统参数获取WebSocket URL,如果未配置则使用默认值
|
||||||
|
String wsUrl = sysParamsService.getValue(Constant.SERVER_WEBSOCKET, true);
|
||||||
|
if (StringUtils.isBlank(wsUrl) || wsUrl.equals("null")) {
|
||||||
|
log.error("WebSocket URL is not configured");
|
||||||
|
wsUrl = "ws://xiaozhi.server.com:8000/xiaozhi/v1/";
|
||||||
|
websocket.setUrl(wsUrl);
|
||||||
|
} else {
|
||||||
|
String[] wsUrls = wsUrl.split("\\;");
|
||||||
|
if (wsUrls.length > 0) {
|
||||||
|
// 随机选择一个WebSocket URL
|
||||||
|
websocket.setUrl(wsUrls[RandomUtil.randomInt(0, wsUrls.length)]);
|
||||||
|
} else {
|
||||||
|
log.error("WebSocket URL list is empty");
|
||||||
|
websocket.setUrl("ws://xiaozhi.server.com:8000/xiaozhi/v1/");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
response.setWebsocket(websocket);
|
||||||
|
|
||||||
DeviceEntity deviceById = getDeviceById(macAddress);
|
DeviceEntity deviceById = getDeviceById(macAddress);
|
||||||
if (deviceById != null) { // 如果设备存在,则更新上次连接时间
|
if (deviceById != null) { // 如果设备存在,则更新上次连接时间
|
||||||
deviceById.setLastConnectedAt(new Date());
|
deviceById.setLastConnectedAt(new Date());
|
||||||
@@ -250,11 +273,15 @@ public class DeviceServiceImpl extends BaseServiceImpl<DeviceDao, DeviceEntity>
|
|||||||
|
|
||||||
if (StringUtils.isNotBlank(cachedCode)) {
|
if (StringUtils.isNotBlank(cachedCode)) {
|
||||||
code.setCode(cachedCode);
|
code.setCode(cachedCode);
|
||||||
|
String frontedUrl = sysParamsService.getValue(Constant.SERVER_FRONTED_URL, true);
|
||||||
code.setMessage(frontedUrl + "\n" + cachedCode);
|
code.setMessage(frontedUrl + "\n" + cachedCode);
|
||||||
|
code.setChallenge(deviceId);
|
||||||
} else {
|
} else {
|
||||||
String newCode = RandomUtil.randomNumbers(6);
|
String newCode = RandomUtil.randomNumbers(6);
|
||||||
code.setCode(newCode);
|
code.setCode(newCode);
|
||||||
|
String frontedUrl = sysParamsService.getValue(Constant.SERVER_FRONTED_URL, true);
|
||||||
code.setMessage(frontedUrl + "\n" + newCode);
|
code.setMessage(frontedUrl + "\n" + newCode);
|
||||||
|
code.setChallenge(deviceId);
|
||||||
|
|
||||||
Map<String, Object> dataMap = new HashMap<>();
|
Map<String, Object> dataMap = new HashMap<>();
|
||||||
dataMap.put("id", deviceId);
|
dataMap.put("id", deviceId);
|
||||||
|
|||||||
+74
@@ -0,0 +1,74 @@
|
|||||||
|
package xiaozhi.modules.device.service.impl;
|
||||||
|
|
||||||
|
import java.util.Arrays;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
|
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
|
||||||
|
import com.baomidou.mybatisplus.core.metadata.IPage;
|
||||||
|
|
||||||
|
import io.micrometer.common.util.StringUtils;
|
||||||
|
import xiaozhi.common.page.PageData;
|
||||||
|
import xiaozhi.common.service.impl.BaseServiceImpl;
|
||||||
|
import xiaozhi.modules.device.dao.OtaDao;
|
||||||
|
import xiaozhi.modules.device.entity.OtaEntity;
|
||||||
|
import xiaozhi.modules.device.service.OtaService;
|
||||||
|
|
||||||
|
@Service
|
||||||
|
public class OtaServiceImpl extends BaseServiceImpl<OtaDao, OtaEntity> implements OtaService {
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public PageData<OtaEntity> page(Map<String, Object> params) {
|
||||||
|
IPage<OtaEntity> page = baseDao.selectPage(
|
||||||
|
getPage(params, "update_date", true),
|
||||||
|
getWrapper(params));
|
||||||
|
|
||||||
|
return new PageData<>(page.getRecords(), page.getTotal());
|
||||||
|
}
|
||||||
|
|
||||||
|
private QueryWrapper<OtaEntity> getWrapper(Map<String, Object> params) {
|
||||||
|
String firmwareName = (String) params.get("firmwareName");
|
||||||
|
|
||||||
|
QueryWrapper<OtaEntity> wrapper = new QueryWrapper<>();
|
||||||
|
wrapper.like(StringUtils.isNotBlank(firmwareName), "firmware_name", firmwareName);
|
||||||
|
|
||||||
|
return wrapper;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void update(OtaEntity entity) {
|
||||||
|
// 检查是否存在相同名称、类型和版本的固件(排除当前记录)
|
||||||
|
QueryWrapper<OtaEntity> queryWrapper = new QueryWrapper<OtaEntity>()
|
||||||
|
.eq("firmware_name", entity.getFirmwareName())
|
||||||
|
.eq("type", entity.getType())
|
||||||
|
.eq("version", entity.getVersion())
|
||||||
|
.ne("id", entity.getId()); // 排除当前记录
|
||||||
|
|
||||||
|
if (baseDao.selectCount(queryWrapper) > 0) {
|
||||||
|
throw new RuntimeException("已存在相同名称、类型和版本的固件,请修改后重试");
|
||||||
|
}
|
||||||
|
|
||||||
|
baseDao.updateById(entity);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void delete(String[] ids) {
|
||||||
|
baseDao.deleteBatchIds(Arrays.asList(ids));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean save(OtaEntity entity) {
|
||||||
|
// 检查是否存在相同名称、类型和版本的固件
|
||||||
|
QueryWrapper<OtaEntity> queryWrapper = new QueryWrapper<OtaEntity>()
|
||||||
|
.eq("firmware_name", entity.getFirmwareName())
|
||||||
|
.eq("type", entity.getType())
|
||||||
|
.eq("version", entity.getVersion());
|
||||||
|
|
||||||
|
if (baseDao.selectCount(queryWrapper) > 0) {
|
||||||
|
throw new RuntimeException("已存在相同名称、类型和版本的固件,请勿重复添加");
|
||||||
|
}
|
||||||
|
|
||||||
|
return baseDao.insert(entity) > 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -69,6 +69,7 @@ public class ShiroConfig {
|
|||||||
*/
|
*/
|
||||||
Map<String, String> filterMap = new LinkedHashMap<>();
|
Map<String, String> filterMap = new LinkedHashMap<>();
|
||||||
filterMap.put("/ota/**", "anon");
|
filterMap.put("/ota/**", "anon");
|
||||||
|
filterMap.put("/otaMag/download/**", "anon");
|
||||||
filterMap.put("/webjars/**", "anon");
|
filterMap.put("/webjars/**", "anon");
|
||||||
filterMap.put("/druid/**", "anon");
|
filterMap.put("/druid/**", "anon");
|
||||||
filterMap.put("/v3/api-docs/**", "anon");
|
filterMap.put("/v3/api-docs/**", "anon");
|
||||||
|
|||||||
+35
@@ -2,6 +2,7 @@ package xiaozhi.modules.sys.controller;
|
|||||||
|
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
|
||||||
|
import org.apache.commons.lang3.StringUtils;
|
||||||
import org.apache.shiro.authz.annotation.RequiresPermissions;
|
import org.apache.shiro.authz.annotation.RequiresPermissions;
|
||||||
import org.springframework.web.bind.annotation.GetMapping;
|
import org.springframework.web.bind.annotation.GetMapping;
|
||||||
import org.springframework.web.bind.annotation.PathVariable;
|
import org.springframework.web.bind.annotation.PathVariable;
|
||||||
@@ -20,6 +21,7 @@ import io.swagger.v3.oas.annotations.tags.Tag;
|
|||||||
import lombok.AllArgsConstructor;
|
import lombok.AllArgsConstructor;
|
||||||
import xiaozhi.common.annotation.LogOperation;
|
import xiaozhi.common.annotation.LogOperation;
|
||||||
import xiaozhi.common.constant.Constant;
|
import xiaozhi.common.constant.Constant;
|
||||||
|
import xiaozhi.common.exception.RenException;
|
||||||
import xiaozhi.common.page.PageData;
|
import xiaozhi.common.page.PageData;
|
||||||
import xiaozhi.common.utils.Result;
|
import xiaozhi.common.utils.Result;
|
||||||
import xiaozhi.common.validator.AssertUtils;
|
import xiaozhi.common.validator.AssertUtils;
|
||||||
@@ -30,6 +32,7 @@ import xiaozhi.common.validator.group.UpdateGroup;
|
|||||||
import xiaozhi.modules.config.service.ConfigService;
|
import xiaozhi.modules.config.service.ConfigService;
|
||||||
import xiaozhi.modules.sys.dto.SysParamsDTO;
|
import xiaozhi.modules.sys.dto.SysParamsDTO;
|
||||||
import xiaozhi.modules.sys.service.SysParamsService;
|
import xiaozhi.modules.sys.service.SysParamsService;
|
||||||
|
import xiaozhi.modules.sys.utils.WebSocketValidator;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 参数管理
|
* 参数管理
|
||||||
@@ -91,11 +94,43 @@ public class SysParamsController {
|
|||||||
// 效验数据
|
// 效验数据
|
||||||
ValidatorUtils.validateEntity(dto, UpdateGroup.class, DefaultGroup.class);
|
ValidatorUtils.validateEntity(dto, UpdateGroup.class, DefaultGroup.class);
|
||||||
|
|
||||||
|
// 验证WebSocket地址列表
|
||||||
|
validateWebSocketUrls(dto.getParamCode(), dto.getParamValue());
|
||||||
|
|
||||||
sysParamsService.update(dto);
|
sysParamsService.update(dto);
|
||||||
configService.getConfig(false);
|
configService.getConfig(false);
|
||||||
return new Result<Void>();
|
return new Result<Void>();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 验证WebSocket地址列表
|
||||||
|
*
|
||||||
|
* @param urls WebSocket地址列表,以分号分隔
|
||||||
|
* @return 验证结果
|
||||||
|
*/
|
||||||
|
private void validateWebSocketUrls(String paramCode, String urls) {
|
||||||
|
if (!paramCode.equals(Constant.SERVER_WEBSOCKET)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
String[] wsUrls = urls.split("\\;");
|
||||||
|
if (wsUrls.length == 0) {
|
||||||
|
throw new RenException("WebSocket地址列表不能为空");
|
||||||
|
}
|
||||||
|
for (String url : wsUrls) {
|
||||||
|
if (StringUtils.isNotBlank(url)) {
|
||||||
|
// 验证WebSocket地址格式
|
||||||
|
if (!WebSocketValidator.validateUrlFormat(url)) {
|
||||||
|
throw new RenException("WebSocket地址格式不正确: " + url);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 测试WebSocket连接
|
||||||
|
if (!WebSocketValidator.testConnection(url)) {
|
||||||
|
throw new RenException("WebSocket连接测试失败: " + url);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@PostMapping("/delete")
|
@PostMapping("/delete")
|
||||||
@Operation(summary = "删除")
|
@Operation(summary = "删除")
|
||||||
@LogOperation("删除")
|
@LogOperation("删除")
|
||||||
|
|||||||
@@ -0,0 +1,46 @@
|
|||||||
|
package xiaozhi.modules.sys.utils;
|
||||||
|
|
||||||
|
import java.util.concurrent.CompletableFuture;
|
||||||
|
|
||||||
|
import org.springframework.web.socket.CloseStatus;
|
||||||
|
import org.springframework.web.socket.WebSocketHandler;
|
||||||
|
import org.springframework.web.socket.WebSocketMessage;
|
||||||
|
import org.springframework.web.socket.WebSocketSession;
|
||||||
|
|
||||||
|
public class WebSocketTestHandler implements WebSocketHandler {
|
||||||
|
private final CompletableFuture<Boolean> future;
|
||||||
|
|
||||||
|
public WebSocketTestHandler(CompletableFuture<Boolean> future) {
|
||||||
|
this.future = future;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void afterConnectionEstablished(WebSocketSession session) {
|
||||||
|
future.complete(true);
|
||||||
|
try {
|
||||||
|
session.close();
|
||||||
|
} catch (Exception e) {
|
||||||
|
// 忽略关闭异常
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void handleMessage(WebSocketSession session, WebSocketMessage<?> message) {
|
||||||
|
// 不需要处理消息
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void handleTransportError(WebSocketSession session, Throwable exception) {
|
||||||
|
future.complete(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void afterConnectionClosed(WebSocketSession session, CloseStatus closeStatus) {
|
||||||
|
// 连接关闭时不做任何处理
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean supportsPartialMessages() {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,58 @@
|
|||||||
|
package xiaozhi.modules.sys.utils;
|
||||||
|
|
||||||
|
import java.net.URI;
|
||||||
|
import java.util.concurrent.CompletableFuture;
|
||||||
|
import java.util.concurrent.TimeUnit;
|
||||||
|
import java.util.regex.Pattern;
|
||||||
|
|
||||||
|
import org.apache.commons.lang3.StringUtils;
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
import org.springframework.web.socket.client.WebSocketClient;
|
||||||
|
import org.springframework.web.socket.client.standard.StandardWebSocketClient;
|
||||||
|
|
||||||
|
public class WebSocketValidator {
|
||||||
|
private static final Logger logger = LoggerFactory.getLogger(WebSocketValidator.class);
|
||||||
|
|
||||||
|
// WebSocket URL正则表达式
|
||||||
|
private static final Pattern WS_URL_PATTERN = Pattern
|
||||||
|
.compile("^wss?://[\\w.-]+(?:\\.[\\w.-]+)*(?::\\d+)?(?:/[\\w.-]*)*$");
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 验证WebSocket地址格式
|
||||||
|
*
|
||||||
|
* @param url WebSocket地址
|
||||||
|
* @return 是否有效
|
||||||
|
*/
|
||||||
|
public static boolean validateUrlFormat(String url) {
|
||||||
|
if (StringUtils.isBlank(url)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return WS_URL_PATTERN.matcher(url.trim()).matches();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 测试WebSocket连接
|
||||||
|
*
|
||||||
|
* @param url WebSocket地址
|
||||||
|
* @return 是否可连接
|
||||||
|
*/
|
||||||
|
public static boolean testConnection(String url) {
|
||||||
|
if (!validateUrlFormat(url)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
WebSocketClient client = new StandardWebSocketClient();
|
||||||
|
CompletableFuture<Boolean> future = new CompletableFuture<>();
|
||||||
|
|
||||||
|
client.doHandshake(new WebSocketTestHandler(future), null, URI.create(url));
|
||||||
|
|
||||||
|
// 等待最多5秒获取连接结果
|
||||||
|
return future.get(5, TimeUnit.SECONDS);
|
||||||
|
} catch (Exception e) {
|
||||||
|
logger.error("WebSocket连接测试失败: {}", url, e);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -24,7 +24,6 @@ public class TimbreDataDTO {
|
|||||||
private String name;
|
private String name;
|
||||||
|
|
||||||
@Schema(description = "备注")
|
@Schema(description = "备注")
|
||||||
@NotBlank(message = "{timbre.remark.require}")
|
|
||||||
private String remark;
|
private String remark;
|
||||||
|
|
||||||
@Schema(description = "排序")
|
@Schema(description = "排序")
|
||||||
@@ -40,6 +39,5 @@ public class TimbreDataDTO {
|
|||||||
private String ttsVoice;
|
private String ttsVoice;
|
||||||
|
|
||||||
@Schema(description = "音频播放地址")
|
@Schema(description = "音频播放地址")
|
||||||
@NotBlank(message = "{timbre.voiceDemo.require}")
|
|
||||||
private String voiceDemo;
|
private String voiceDemo;
|
||||||
}
|
}
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
-- 对0.3.0版本之前的参数进行修改
|
|
||||||
update `sys_params` set param_value = '.mp3;.wav;.p3' where param_code = 'plugins.play_music.music_ext';
|
|
||||||
update `ai_model_config` set config_json = '{\"type\": \"intent_llm\", \"llm\": \"LLM_ChatGLMLLM\"}' where id = 'Intent_intent_llm';
|
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
-- 本文件用于初始化固件信息表,无需手动执行,在项目启动时会自动执行
|
||||||
|
-- -------------------------------------------------------
|
||||||
|
-- 初始化固件信息表
|
||||||
|
drop table if exists `ai_ota`;
|
||||||
|
CREATE TABLE `ai_ota` (
|
||||||
|
`id` varchar(32) NOT NULL COMMENT 'ID',
|
||||||
|
`firmware_name` varchar(100) DEFAULT NULL COMMENT '固件名称',
|
||||||
|
`type` varchar(50) DEFAULT NULL COMMENT '固件类型',
|
||||||
|
`version` varchar(50) DEFAULT NULL COMMENT '版本号',
|
||||||
|
`size` bigint DEFAULT NULL COMMENT '文件大小(字节)',
|
||||||
|
`remark` varchar(500) DEFAULT NULL COMMENT '备注/说明',
|
||||||
|
`firmware_path` varchar(255) DEFAULT NULL COMMENT '固件路径',
|
||||||
|
`sort` int unsigned DEFAULT '0' COMMENT '排序',
|
||||||
|
`updater` bigint DEFAULT NULL COMMENT '更新者',
|
||||||
|
`update_date` datetime DEFAULT NULL COMMENT '更新时间',
|
||||||
|
`creator` bigint DEFAULT NULL COMMENT '创建者',
|
||||||
|
`create_date` datetime DEFAULT NULL COMMENT '创建时间',
|
||||||
|
PRIMARY KEY (`id`)
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='固件信息表';
|
||||||
@@ -1,2 +0,0 @@
|
|||||||
delete from `ai_model_config` where id = 'Intent_function_call';
|
|
||||||
INSERT INTO `ai_model_config` VALUES ('Intent_function_call', 'Intent', 'function_call', '函数调用意图识别', 0, 1, '{\"type\": \"function_call\", \"functions\": \"change_role;get_weather;get_news;play_music\"}', NULL, NULL, 3, NULL, NULL, NULL, NULL);
|
|
||||||
@@ -0,0 +1,88 @@
|
|||||||
|
-- 删除无用模型供应器
|
||||||
|
delete from `ai_model_provider` where id = 'SYSTEM_LLM_doubao';
|
||||||
|
delete from `ai_model_provider` where id = 'SYSTEM_LLM_chatglm';
|
||||||
|
delete from `ai_model_provider` where id = 'SYSTEM_TTS_302ai';
|
||||||
|
delete from `ai_model_provider` where id = 'SYSTEM_TTS_gizwits';
|
||||||
|
|
||||||
|
-- 添加模型供应器
|
||||||
|
delete from `ai_model_provider` where id = 'SYSTEM_ASR_TencentASR';
|
||||||
|
INSERT INTO `ai_model_provider` (`id`, `model_type`, `provider_code`, `name`, `fields`, `sort`, `creator`, `create_date`, `updater`, `update_date`) VALUES
|
||||||
|
('SYSTEM_ASR_TencentASR', 'ASR', 'tencent', '腾讯语音识别', '[{"key":"appid","label":"应用ID","type":"string"},{"key":"secret_id","label":"Secret ID","type":"string"},{"key":"secret_key","label":"Secret Key","type":"string"},{"key":"output_dir","label":"输出目录","type":"string"}]', 4, 1, NOW(), 1, NOW());
|
||||||
|
|
||||||
|
-- 添加腾讯语音合成模型供应器
|
||||||
|
delete from `ai_model_provider` where id = 'SYSTEM_TTS_TencentTTS';
|
||||||
|
INSERT INTO `ai_model_provider` (`id`, `model_type`, `provider_code`, `name`, `fields`, `sort`, `creator`, `create_date`, `updater`, `update_date`) VALUES
|
||||||
|
('SYSTEM_TTS_TencentTTS', 'TTS', 'tencent', '腾讯语音合成', '[{"key":"appid","label":"应用ID","type":"string"},{"key":"secret_id","label":"Secret ID","type":"string"},{"key":"secret_key","label":"Secret Key","type":"string"},{"key":"output_dir","label":"输出目录","type":"string"},{"key":"region","label":"区域","type":"string"},{"key":"voice","label":"音色ID","type":"string"}]', 5, 1, NOW(), 1, NOW());
|
||||||
|
|
||||||
|
|
||||||
|
-- 添加edge音色
|
||||||
|
delete from `ai_tts_voice` where id in ('TTS_EdgeTTS0001', 'TTS_EdgeTTS0002', 'TTS_EdgeTTS0003', 'TTS_EdgeTTS0004', 'TTS_EdgeTTS0005', 'TTS_EdgeTTS0006', 'TTS_EdgeTTS0007', 'TTS_EdgeTTS0008', 'TTS_EdgeTTS0009', 'TTS_EdgeTTS0010', 'TTS_EdgeTTS0011');
|
||||||
|
INSERT INTO `ai_tts_voice` VALUES
|
||||||
|
('TTS_EdgeTTS0001', 'TTS_EdgeTTS', 'EdgeTTS女声-晓晓', 'zh-CN-XiaoxiaoNeural', '普通话', NULL, NULL, 1, NULL, NULL, NULL, NULL),
|
||||||
|
('TTS_EdgeTTS0002', 'TTS_EdgeTTS', 'EdgeTTS男声-云扬', 'zh-CN-YunyangNeural', '普通话', NULL, NULL, 1, NULL, NULL, NULL, NULL),
|
||||||
|
('TTS_EdgeTTS0003', 'TTS_EdgeTTS', 'EdgeTTS女声-晓伊', 'zh-CN-XiaoyiNeural', '普通话', NULL, NULL, 1, NULL, NULL, NULL, NULL),
|
||||||
|
('TTS_EdgeTTS0004', 'TTS_EdgeTTS', 'EdgeTTS男声-云健', 'zh-CN-YunjianNeural', '普通话', NULL, NULL, 1, NULL, NULL, NULL, NULL),
|
||||||
|
('TTS_EdgeTTS0005', 'TTS_EdgeTTS', 'EdgeTTS男声-云希', 'zh-CN-YunxiNeural', '普通话', NULL, NULL, 1, NULL, NULL, NULL, NULL),
|
||||||
|
('TTS_EdgeTTS0006', 'TTS_EdgeTTS', 'EdgeTTS男声-云夏', 'zh-CN-YunxiaNeural', '普通话', NULL, NULL, 1, NULL, NULL, NULL, NULL),
|
||||||
|
('TTS_EdgeTTS0007', 'TTS_EdgeTTS', 'EdgeTTS女声-辽宁小贝', 'zh-CN-liaoning-XiaobeiNeural', '辽宁', NULL, NULL, 1, NULL, NULL, NULL, NULL),
|
||||||
|
('TTS_EdgeTTS0008', 'TTS_EdgeTTS', 'EdgeTTS女声-陕西小妮', 'zh-CN-shaanxi-XiaoniNeural', '陕西', NULL, NULL, 1, NULL, NULL, NULL, NULL),
|
||||||
|
('TTS_EdgeTTS0009', 'TTS_EdgeTTS', 'EdgeTTS女声-香港海佳', 'zh-HK-HiuGaaiNeural', '粤语', 'General', 'Friendly, Positive', 1, NULL, NULL, NULL, NULL),
|
||||||
|
('TTS_EdgeTTS0010', 'TTS_EdgeTTS', 'EdgeTTS女声-香港海曼', 'zh-HK-HiuMaanNeural', '粤语', 'General', 'Friendly, Positive', 1, NULL, NULL, NULL, NULL),
|
||||||
|
('TTS_EdgeTTS0011', 'TTS_EdgeTTS', 'EdgeTTS男声-香港万龙', 'zh-HK-WanLungNeural', '粤语', 'General', 'Friendly, Positive', 1, NULL, NULL, NULL, NULL);
|
||||||
|
|
||||||
|
-- DoubaoTTS音色
|
||||||
|
delete from `ai_tts_voice` where id in ('TTS_DoubaoTTS0001', 'TTS_DoubaoTTS0002', 'TTS_DoubaoTTS0003', 'TTS_DoubaoTTS0004', 'TTS_DoubaoTTS0005');
|
||||||
|
INSERT INTO `ai_tts_voice` VALUES ('TTS_DoubaoTTS0001', 'TTS_DoubaoTTS', '通用女声', 'BV001_streaming', '普通话', 'https://lf3-speech.bytetos.com/obj/speech-tts-external/portal/Portal_Demo_BV001.mp3', NULL, 3, NULL, NULL, NULL, NULL);
|
||||||
|
INSERT INTO `ai_tts_voice` VALUES ('TTS_DoubaoTTS0002', 'TTS_DoubaoTTS', '通用男声', 'BV002_streaming', '普通话', 'https://lf3-speech.bytetos.com/obj/speech-tts-external/portal/Portal_Demo_BV002.mp3', NULL, 2, NULL, NULL, NULL, NULL);
|
||||||
|
INSERT INTO `ai_tts_voice` VALUES ('TTS_DoubaoTTS0003', 'TTS_DoubaoTTS', '阳光男生', 'BV056_streaming', '普通话', 'https://lf3-speech.bytetos.com/obj/speech-tts-external/portal/Portal_Demo_BV056.mp3', NULL, 4, NULL, NULL, NULL, NULL);
|
||||||
|
INSERT INTO `ai_tts_voice` VALUES ('TTS_DoubaoTTS0004', 'TTS_DoubaoTTS', '奶气萌娃', 'BV051_streaming', '普通话', 'https://lf3-speech.bytetos.com/obj/speech-tts-external/portal/Portal_Demo_BV051.mp3', NULL, 5, NULL, NULL, NULL, NULL);
|
||||||
|
INSERT INTO `ai_tts_voice` VALUES ('TTS_DoubaoTTS0005', 'TTS_DoubaoTTS', '湾湾小何', 'zh_female_wanwanxiaohe_moon_bigtts', '普通话', 'https://lf3-static.bytednsdoc.com/obj/eden-cn/lm_hz_ihsph/ljhwZthlaukjlkulzlp/portal/bigtts/%E6%B9%BE%E6%B9%BE%E5%B0%8F%E4%BD%95.mp3', NULL, 6, NULL, NULL, NULL, NULL);
|
||||||
|
|
||||||
|
-- 修正CosyVoiceSiliconflow音色
|
||||||
|
delete from `ai_tts_voice` where id in ('TTS_CosyVoiceSiliconflow0001', 'TTS_CosyVoiceSiliconflow0002');
|
||||||
|
INSERT INTO `ai_tts_voice` VALUES ('TTS_CosyVoiceSiliconflow0001', 'TTS_CosyVoiceSiliconflow', 'CosyVoice男声', 'FunAudioLLM/CosyVoice2-0.5B:alex', '中文', NULL, NULL, 6, NULL, NULL, NULL, NULL);
|
||||||
|
INSERT INTO `ai_tts_voice` VALUES ('TTS_CosyVoiceSiliconflow0002', 'TTS_CosyVoiceSiliconflow', 'CosyVoice女声', 'FunAudioLLM/CosyVoice2-0.5B:bella', '中文', NULL, NULL, 6, NULL, NULL, NULL, NULL);
|
||||||
|
|
||||||
|
-- CozeCnTTS音色
|
||||||
|
delete from `ai_tts_voice` where id = 'TTS_CozeCnTTS0001';
|
||||||
|
INSERT INTO `ai_tts_voice` VALUES ('TTS_CozeCnTTS0001', 'TTS_CozeCnTTS', 'CozeCn音色', '7426720361733046281', '中文', NULL, NULL, 7, NULL, NULL, NULL, NULL);
|
||||||
|
|
||||||
|
-- MinimaxTTS音色
|
||||||
|
delete from `ai_tts_voice` where id = 'TTS_MinimaxTTS0001';
|
||||||
|
INSERT INTO `ai_tts_voice` VALUES ('TTS_MinimaxTTS0001', 'TTS_MinimaxTTS', 'Minimax少女音', 'female-shaonv', '中文', NULL, NULL, 8, NULL, NULL, NULL, NULL);
|
||||||
|
|
||||||
|
-- AliyunTTS音色
|
||||||
|
delete from `ai_tts_voice` where id = 'TTS_AliyunTTS0001';
|
||||||
|
INSERT INTO `ai_tts_voice` VALUES ('TTS_AliyunTTS0001', 'TTS_AliyunTTS', '阿里云小云', 'xiaoyun', '中文', NULL, NULL, 9, NULL, NULL, NULL, NULL);
|
||||||
|
|
||||||
|
-- TTS302AI音色
|
||||||
|
delete from `ai_tts_voice` where id = 'TTS_TTS302AI0001';
|
||||||
|
INSERT INTO `ai_tts_voice` VALUES ('TTS_TTS302AI0001', 'TTS_TTS302AI', '湾湾小何', 'zh_female_wanwanxiaohe_moon_bigtts', '中文', 'https://lf3-static.bytednsdoc.com/obj/eden-cn/lm_hz_ihsph/ljhwZthlaukjlkulzlp/portal/bigtts/%E6%B9%BE%E6%B9%BE%E5%B0%8F%E4%BD%95.mp3', NULL, 10, NULL, NULL, NULL, NULL);
|
||||||
|
|
||||||
|
-- GizwitsTTS音色
|
||||||
|
delete from `ai_tts_voice` where id = 'TTS_GizwitsTTS0001';
|
||||||
|
INSERT INTO `ai_tts_voice` VALUES ('TTS_GizwitsTTS0001', 'TTS_GizwitsTTS', '机智云湾湾', 'zh_female_wanwanxiaohe_moon_bigtts', '中文', 'https://lf3-static.bytednsdoc.com/obj/eden-cn/lm_hz_ihsph/ljhwZthlaukjlkulzlp/portal/bigtts/%E6%B9%BE%E6%B9%BE%E5%B0%8F%E4%BD%95.mp3', NULL, 11, NULL, NULL, NULL, NULL);
|
||||||
|
|
||||||
|
-- ACGNTTS音色
|
||||||
|
delete from `ai_tts_voice` where id = 'TTS_ACGNTTS0001';
|
||||||
|
INSERT INTO `ai_tts_voice` VALUES ('TTS_ACGNTTS0001', 'TTS_ACGNTTS', 'ACG音色', '1695', '中文', NULL, NULL, 12, NULL, NULL, NULL, NULL);
|
||||||
|
|
||||||
|
-- OpenAITTS音色
|
||||||
|
delete from `ai_tts_voice` where id = 'TTS_OpenAITTS0001';
|
||||||
|
INSERT INTO `ai_tts_voice` VALUES ('TTS_OpenAITTS0001', 'TTS_OpenAITTS', 'OpenAI男声', 'onyx', '中文', NULL, NULL, 13, NULL, NULL, NULL, NULL);
|
||||||
|
|
||||||
|
-- 添加腾讯语音合成音色
|
||||||
|
delete from `ai_tts_voice` where id = 'TTS_TencentTTS0001';
|
||||||
|
INSERT INTO `ai_tts_voice` VALUES ('TTS_TencentTTS0001', 'TTS_TencentTTS', '智瑜', '101001', '中文', NULL, NULL, 1, NULL, NULL, NULL, NULL);
|
||||||
|
|
||||||
|
-- 其他音色
|
||||||
|
delete from `ai_tts_voice` where id = 'TTS_FishSpeech0000';
|
||||||
|
INSERT INTO `ai_tts_voice` VALUES ('TTS_FishSpeech0000', 'TTS_FishSpeech', '', '', '中文', '', NULL, 8, NULL, NULL, NULL, NULL);
|
||||||
|
|
||||||
|
delete from `ai_tts_voice` where id = 'TTS_GPT_SOVITS_V20000';
|
||||||
|
INSERT INTO `ai_tts_voice` VALUES ('TTS_GPT_SOVITS_V20000', 'TTS_GPT_SOVITS_V2', '', '', '中文', '', NULL, 8, NULL, NULL, NULL, NULL);
|
||||||
|
|
||||||
|
delete from `ai_tts_voice` where id in ('TTS_GPT_SOVITS_V30000', 'TTS_CustomTTS0000');
|
||||||
|
INSERT INTO `ai_tts_voice` VALUES ('TTS_GPT_SOVITS_V30000', 'TTS_GPT_SOVITS_V3', '', '', '中文', '', NULL, 8, NULL, NULL, NULL, NULL);
|
||||||
|
INSERT INTO `ai_tts_voice` VALUES ('TTS_CustomTTS0000', 'TTS_CustomTTS', '', '', '中文', '', NULL, 8, NULL, NULL, NULL, NULL);
|
||||||
|
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
-- 删除server.ip和server.port,改成在xiaozhi-server中配置,方便运行多个websocket
|
||||||
|
delete from `sys_params` where id = 100;
|
||||||
|
delete from `sys_params` where id = 101;
|
||||||
|
|
||||||
|
INSERT INTO `sys_params` (id, param_code, param_value, value_type, param_type, remark) VALUES (106, 'server.websocket', 'null', 'string', 1, 'websocket地址,多个用;分隔');
|
||||||
|
|
||||||
@@ -1,44 +0,0 @@
|
|||||||
-- 本文件用于初始化音色数据,无需手动执行,在项目启动时会自动执行
|
|
||||||
-- ----------------------------------------------------
|
|
||||||
|
|
||||||
-- 初始化音色数据
|
|
||||||
DELETE FROM `ai_tts_voice`;
|
|
||||||
|
|
||||||
-- EdgeTTS音色
|
|
||||||
INSERT INTO `ai_tts_voice` VALUES ('TTS_EdgeTTS0001', 'TTS_EdgeTTS', 'EdgeTTS女声', 'zh-CN-XiaoxiaoNeural', '中文', 'https://lf3-static.bytednsdoc.com/obj/eden-cn/lm_hz_ihsph/ljhwZthlaukjlkulzlp/portal/bigtts/EdgeTTS.mp3', NULL, 1, NULL, NULL, NULL, NULL);
|
|
||||||
|
|
||||||
-- DoubaoTTS音色
|
|
||||||
INSERT INTO `ai_tts_voice` VALUES ('TTS_DoubaoTTS0001', 'TTS_DoubaoTTS', '通用女声', 'BV001_streaming', '中文', 'https://lf3-speech.bytetos.com/obj/speech-tts-external/portal/Portal_Demo_BV001.mp3', NULL, 3, NULL, NULL, NULL, NULL);
|
|
||||||
INSERT INTO `ai_tts_voice` VALUES ('TTS_DoubaoTTS0002', 'TTS_DoubaoTTS', '通用男声', 'BV002_streaming', '中文', 'https://lf3-speech.bytetos.com/obj/speech-tts-external/portal/Portal_Demo_BV002.mp3', NULL, 2, NULL, NULL, NULL, NULL);
|
|
||||||
INSERT INTO `ai_tts_voice` VALUES ('TTS_DoubaoTTS0003', 'TTS_DoubaoTTS', '阳光男生', 'BV056_streaming', '中文', 'https://lf3-speech.bytetos.com/obj/speech-tts-external/portal/Portal_Demo_BV056.mp3', NULL, 4, NULL, NULL, NULL, NULL);
|
|
||||||
INSERT INTO `ai_tts_voice` VALUES ('TTS_DoubaoTTS0004', 'TTS_DoubaoTTS', '奶气萌娃', 'BV051_streaming', '中文', 'https://lf3-speech.bytetos.com/obj/speech-tts-external/portal/Portal_Demo_BV051.mp3', NULL, 5, NULL, NULL, NULL, NULL);
|
|
||||||
|
|
||||||
-- CosyVoiceSiliconflow音色
|
|
||||||
INSERT INTO `ai_tts_voice` VALUES ('TTS_CosyVoiceSiliconflow0001', 'TTS_CosyVoiceSiliconflow', 'CosyVoice男声', 'alex', '中文', 'https://example.com/cosyvoice/alex.mp3', NULL, 6, NULL, NULL, NULL, NULL);
|
|
||||||
|
|
||||||
-- CozeCnTTS音色
|
|
||||||
INSERT INTO `ai_tts_voice` VALUES ('TTS_CozeCnTTS0001', 'TTS_CozeCnTTS', 'CozeCn音色', '7426720361733046281', '中文', 'https://example.com/cozecn/voice.mp3', NULL, 7, NULL, NULL, NULL, NULL);
|
|
||||||
|
|
||||||
-- MinimaxTTS音色
|
|
||||||
INSERT INTO `ai_tts_voice` VALUES ('TTS_MinimaxTTS0001', 'TTS_MinimaxTTS', 'Minimax少女音', 'female-shaonv', '中文', 'https://example.com/minimax/female-shaonv.mp3', NULL, 8, NULL, NULL, NULL, NULL);
|
|
||||||
|
|
||||||
-- AliyunTTS音色
|
|
||||||
INSERT INTO `ai_tts_voice` VALUES ('TTS_AliyunTTS0001', 'TTS_AliyunTTS', '阿里云小云', 'xiaoyun', '中文', 'https://example.com/aliyun/xiaoyun.mp3', NULL, 9, NULL, NULL, NULL, NULL);
|
|
||||||
|
|
||||||
-- TTS302AI音色
|
|
||||||
INSERT INTO `ai_tts_voice` VALUES ('TTS_TTS302AI0001', 'TTS_TTS302AI', '湾湾小何', 'zh_female_wanwanxiaohe_moon_bigtts', '中文', 'https://example.com/302ai/wanwanxiaohe.mp3', NULL, 10, NULL, NULL, NULL, NULL);
|
|
||||||
|
|
||||||
-- GizwitsTTS音色
|
|
||||||
INSERT INTO `ai_tts_voice` VALUES ('TTS_GizwitsTTS0001', 'TTS_GizwitsTTS', '机智云湾湾', 'zh_female_wanwanxiaohe_moon_bigtts', '中文', 'https://example.com/gizwits/wanwanxiaohe.mp3', NULL, 11, NULL, NULL, NULL, NULL);
|
|
||||||
|
|
||||||
-- ACGNTTS音色
|
|
||||||
INSERT INTO `ai_tts_voice` VALUES ('TTS_ACGNTTS0001', 'TTS_ACGNTTS', 'ACG音色', '1695', '中文', 'https://example.com/acgn/voice.mp3', NULL, 12, NULL, NULL, NULL, NULL);
|
|
||||||
|
|
||||||
-- OpenAITTS音色
|
|
||||||
INSERT INTO `ai_tts_voice` VALUES ('TTS_OpenAITTS0001', 'TTS_OpenAITTS', 'OpenAI男声', 'onyx', '中文', 'https://example.com/openai/onyx.mp3', NULL, 13, NULL, NULL, NULL, NULL);
|
|
||||||
|
|
||||||
-- 其他音色
|
|
||||||
INSERT INTO `ai_tts_voice` VALUES ('TTS_FishSpeech0000', 'TTS_FishSpeech', '', '', '中文', '', NULL, 8, NULL, NULL, NULL, NULL);
|
|
||||||
INSERT INTO `ai_tts_voice` VALUES ('TTS_GPT_SOVITS_V20000', 'TTS_GPT_SOVITS_V2', '', '', '中文', '', NULL, 8, NULL, NULL, NULL, NULL);
|
|
||||||
INSERT INTO `ai_tts_voice` VALUES ('TTS_GPT_SOVITS_V30000', 'TTS_GPT_SOVITS_V3', '', '', '中文', '', NULL, 8, NULL, NULL, NULL, NULL);
|
|
||||||
INSERT INTO `ai_tts_voice` VALUES ('TTS_CustomTTS0000', 'TTS_CustomTTS', '', '', '中文', '', NULL, 8, NULL, NULL, NULL, NULL);
|
|
||||||
@@ -16,13 +16,6 @@ databaseChangeLog:
|
|||||||
- sqlFile:
|
- sqlFile:
|
||||||
encoding: utf8
|
encoding: utf8
|
||||||
path: classpath:db/changelog/202503141346.sql
|
path: classpath:db/changelog/202503141346.sql
|
||||||
- changeSet:
|
|
||||||
id: 2025_tts_voive
|
|
||||||
author: John
|
|
||||||
changes:
|
|
||||||
- sqlFile:
|
|
||||||
encoding: utf8
|
|
||||||
path: classpath:db/changelog/2025_tts_voive.sql
|
|
||||||
- changeSet:
|
- changeSet:
|
||||||
id: 202504082211
|
id: 202504082211
|
||||||
author: John
|
author: John
|
||||||
@@ -65,3 +58,24 @@ databaseChangeLog:
|
|||||||
- sqlFile:
|
- sqlFile:
|
||||||
encoding: utf8
|
encoding: utf8
|
||||||
path: classpath:db/changelog/202504181536.sql
|
path: classpath:db/changelog/202504181536.sql
|
||||||
|
- changeSet:
|
||||||
|
id: 202504180758
|
||||||
|
author: jiangkunyin
|
||||||
|
changes:
|
||||||
|
- sqlFile:
|
||||||
|
encoding: utf8
|
||||||
|
path: classpath:db/changelog/202504180758.sql
|
||||||
|
- changeSet:
|
||||||
|
id: 202504221135
|
||||||
|
author: John
|
||||||
|
changes:
|
||||||
|
- sqlFile:
|
||||||
|
encoding: utf8
|
||||||
|
path: classpath:db/changelog/202504221135.sql
|
||||||
|
- changeSet:
|
||||||
|
id: 202504221646
|
||||||
|
author: John
|
||||||
|
changes:
|
||||||
|
- sqlFile:
|
||||||
|
encoding: utf8
|
||||||
|
path: classpath:db/changelog/202504221646.sql
|
||||||
@@ -28,10 +28,8 @@ sysuser.uuid.require=\u552F\u4E00\u6807\u8BC6\u4E0D\u80FD\u4E3A\u7A7A
|
|||||||
|
|
||||||
timbre.languages.require=\u97F3\u8272\u7684\u8BED\u8A00\u4E0D\u53EF\u4EE5\u4E3A\u7A7A
|
timbre.languages.require=\u97F3\u8272\u7684\u8BED\u8A00\u4E0D\u53EF\u4EE5\u4E3A\u7A7A
|
||||||
timbre.name.require=\u97F3\u8272\u7684\u540D\u79F0\u4E0D\u53EF\u4EE5\u4E3A\u7A7A
|
timbre.name.require=\u97F3\u8272\u7684\u540D\u79F0\u4E0D\u53EF\u4EE5\u4E3A\u7A7A
|
||||||
timbre.remark.require=\u97F3\u8272\u7684\u5907\u6CE8\u4E0D\u53EF\u4EE5\u4E3A\u7A7A
|
|
||||||
timbre.ttsModelId.require=\u97F3\u8272\u7684tts\u4E3B\u952E\u4E0D\u53EF\u4EE5\u4E3A\u7A7A
|
timbre.ttsModelId.require=\u97F3\u8272\u7684tts\u4E3B\u952E\u4E0D\u53EF\u4EE5\u4E3A\u7A7A
|
||||||
timbre.ttsVoice.require=\u97F3\u8272\u7684\u7F16\u7801\u4E0D\u53EF\u4EE5\u4E3A\u7A7A
|
timbre.ttsVoice.require=\u97F3\u8272\u7684\u7F16\u7801\u4E0D\u53EF\u4EE5\u4E3A\u7A7A
|
||||||
timbre.voiceDemo.require=\u97F3\u8272\u7684\u64AD\u653EDemo\u4E0D\u53EF\u4EE5\u4E3A\u7A7A
|
|
||||||
|
|
||||||
ota.device.not.found=\u8BBE\u5907\u672A\u627E\u5230
|
ota.device.not.found=\u8BBE\u5907\u672A\u627E\u5230
|
||||||
ota.device.need.bind={0}
|
ota.device.need.bind={0}
|
||||||
|
|||||||
@@ -27,10 +27,8 @@ sysuser.uuid.require=The unique identifier cannot be empty
|
|||||||
|
|
||||||
timbre.languages.require=The language of the timbre cannot be empty
|
timbre.languages.require=The language of the timbre cannot be empty
|
||||||
timbre.name.require=The name of the timbre cannot be empty
|
timbre.name.require=The name of the timbre cannot be empty
|
||||||
timbre.remark.require=The remark of the timbre cannot be empty
|
|
||||||
timbre.ttsModelId.require=The TTS model ID of the timbre cannot be empty
|
timbre.ttsModelId.require=The TTS model ID of the timbre cannot be empty
|
||||||
timbre.ttsVoice.require=The TTS voice of the timbre cannot be empty
|
timbre.ttsVoice.require=The TTS voice of the timbre cannot be empty
|
||||||
timbre.voiceDemo.require=The voice demo of the timbre cannot be empty
|
|
||||||
|
|
||||||
ota.device.not.found=Device not found
|
ota.device.not.found=Device not found
|
||||||
ota.device.need.bind={0}
|
ota.device.need.bind={0}
|
||||||
@@ -27,10 +27,8 @@ sysuser.uuid.require=\u552F\u4E00\u6807\u8BC6\u4E0D\u80FD\u4E3A\u7A7A
|
|||||||
|
|
||||||
timbre.languages.require=\u97F3\u8272\u7684\u8A9E\u8A00\u4E0D\u53EF\u4EE5\u70BA\u7A7A
|
timbre.languages.require=\u97F3\u8272\u7684\u8A9E\u8A00\u4E0D\u53EF\u4EE5\u70BA\u7A7A
|
||||||
timbre.name.require=\u97F3\u8272\u7684\u540D\u7A31\u4E0D\u53EF\u4EE5\u70BA\u7A7A
|
timbre.name.require=\u97F3\u8272\u7684\u540D\u7A31\u4E0D\u53EF\u4EE5\u70BA\u7A7A
|
||||||
timbre.remark.require=\u97F3\u8272\u7684\u5099\u6CE8\u4E0D\u53EF\u4EE5\u70BA\u7A7A
|
|
||||||
timbre.ttsModelId.require=\u97F3\u8272\u7684tts\u4E3B\u9375\u4E0D\u53EF\u4EE5\u70BA\u7A7A
|
timbre.ttsModelId.require=\u97F3\u8272\u7684tts\u4E3B\u9375\u4E0D\u53EF\u4EE5\u70BA\u7A7A
|
||||||
timbre.ttsVoice.require=\u97F3\u8272\u7684\u7DE8\u78BC\u4E0D\u53EF\u4EE5\u70BA\u7A7A
|
timbre.ttsVoice.require=\u97F3\u8272\u7684\u7DE8\u78BC\u4E0D\u53EF\u4EE5\u70BA\u7A7A
|
||||||
timbre.voiceDemo.require=\u97F3\u8272\u7684\u64AD\u653E\u5730\u5740\u4E0D\u53EF\u4EE5\u70BA\u7A7A
|
|
||||||
|
|
||||||
ota.device.not.found=\u8A2D\u5099\u672A\u627E\u5230
|
ota.device.not.found=\u8A2D\u5099\u672A\u627E\u5230
|
||||||
ota.device.need.bind={0}
|
ota.device.need.bind={0}
|
||||||
@@ -27,10 +27,8 @@ sysuser.uuid.require=\u552F\u4E00\u6807\u8BC6\u4E0D\u80FD\u4E3A\u7A7A
|
|||||||
|
|
||||||
timbre.languages.require=\u97F3\u8272\u7684\u8BED\u8A00\u4E0D\u53EF\u4EE5\u4E3A\u7A7A
|
timbre.languages.require=\u97F3\u8272\u7684\u8BED\u8A00\u4E0D\u53EF\u4EE5\u4E3A\u7A7A
|
||||||
timbre.name.require=\u97F3\u8272\u7684\u540D\u79F0\u4E0D\u53EF\u4EE5\u4E3A\u7A7A
|
timbre.name.require=\u97F3\u8272\u7684\u540D\u79F0\u4E0D\u53EF\u4EE5\u4E3A\u7A7A
|
||||||
timbre.remark.require=\u97F3\u8272\u7684\u5907\u6CE8\u4E0D\u53EF\u4EE5\u4E3A\u7A7A
|
|
||||||
timbre.ttsModelId.require=\u97F3\u8272\u7684tts\u4E3B\u952E\u4E0D\u53EF\u4EE5\u4E3A\u7A7A
|
timbre.ttsModelId.require=\u97F3\u8272\u7684tts\u4E3B\u952E\u4E0D\u53EF\u4EE5\u4E3A\u7A7A
|
||||||
timbre.ttsVoice.require=\u97F3\u8272\u7684\u7F16\u7801\u4E0D\u53EF\u4EE5\u4E3A\u7A7A
|
timbre.ttsVoice.require=\u97F3\u8272\u7684\u7F16\u7801\u4E0D\u53EF\u4EE5\u4E3A\u7A7A
|
||||||
timbre.voiceDemo.require=\u97F3\u8272\u7684\u64AD\u653EDemo\u4E0D\u53EF\u4EE5\u4E3A\u7A7A
|
|
||||||
|
|
||||||
ota.device.not.found=\u8A2D\u5099\u672A\u627E\u5230
|
ota.device.not.found=\u8A2D\u5099\u672A\u627E\u5230
|
||||||
ota.device.need.bind={0}
|
ota.device.need.bind={0}
|
||||||
File diff suppressed because one or more lines are too long
@@ -5,6 +5,7 @@ import device from './module/device.js'
|
|||||||
import model from './module/model.js'
|
import model from './module/model.js'
|
||||||
import timbre from "./module/timbre.js"
|
import timbre from "./module/timbre.js"
|
||||||
import user from './module/user.js'
|
import user from './module/user.js'
|
||||||
|
import ota from './module/ota.js'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 接口地址
|
* 接口地址
|
||||||
@@ -31,4 +32,5 @@ export default {
|
|||||||
device,
|
device,
|
||||||
model,
|
model,
|
||||||
timbre,
|
timbre,
|
||||||
|
ota
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,123 @@
|
|||||||
|
import { getServiceUrl } from '../api';
|
||||||
|
import RequestService from '../httpRequest';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
// 分页查询OTA固件信息
|
||||||
|
getOtaList(params, callback) {
|
||||||
|
RequestService.sendRequest()
|
||||||
|
.url(`${getServiceUrl()}/otaMag`)
|
||||||
|
.method('GET')
|
||||||
|
.data(params)
|
||||||
|
.success((res) => {
|
||||||
|
RequestService.clearRequestTime();
|
||||||
|
callback(res);
|
||||||
|
})
|
||||||
|
.fail((err) => {
|
||||||
|
console.error('获取OTA固件列表失败:', err);
|
||||||
|
RequestService.reAjaxFun(() => {
|
||||||
|
this.getOtaList(params, callback);
|
||||||
|
});
|
||||||
|
}).send();
|
||||||
|
},
|
||||||
|
// 获取单个OTA固件信息
|
||||||
|
getOtaInfo(id, callback) {
|
||||||
|
RequestService.sendRequest()
|
||||||
|
.url(`${getServiceUrl()}/otaMag/${id}`)
|
||||||
|
.method('GET')
|
||||||
|
.success((res) => {
|
||||||
|
RequestService.clearRequestTime();
|
||||||
|
callback(res);
|
||||||
|
})
|
||||||
|
.fail((err) => {
|
||||||
|
console.error('获取OTA固件信息失败:', err);
|
||||||
|
RequestService.reAjaxFun(() => {
|
||||||
|
this.getOtaInfo(id, callback);
|
||||||
|
});
|
||||||
|
}).send();
|
||||||
|
},
|
||||||
|
// 保存OTA固件信息
|
||||||
|
saveOta(entity, callback) {
|
||||||
|
RequestService.sendRequest()
|
||||||
|
.url(`${getServiceUrl()}/otaMag`)
|
||||||
|
.method('POST')
|
||||||
|
.data(entity)
|
||||||
|
.success((res) => {
|
||||||
|
RequestService.clearRequestTime();
|
||||||
|
callback(res);
|
||||||
|
})
|
||||||
|
.fail((err) => {
|
||||||
|
console.error('保存OTA固件信息失败:', err);
|
||||||
|
RequestService.reAjaxFun(() => {
|
||||||
|
this.saveOta(entity, callback);
|
||||||
|
});
|
||||||
|
}).send();
|
||||||
|
},
|
||||||
|
// 更新OTA固件信息
|
||||||
|
updateOta(id, entity, callback) {
|
||||||
|
RequestService.sendRequest()
|
||||||
|
.url(`${getServiceUrl()}/otaMag/${id}`)
|
||||||
|
.method('PUT')
|
||||||
|
.data(entity)
|
||||||
|
.success((res) => {
|
||||||
|
RequestService.clearRequestTime();
|
||||||
|
callback(res);
|
||||||
|
})
|
||||||
|
.fail((err) => {
|
||||||
|
console.error('更新OTA固件信息失败:', err);
|
||||||
|
RequestService.reAjaxFun(() => {
|
||||||
|
this.updateOta(id, entity, callback);
|
||||||
|
});
|
||||||
|
}).send();
|
||||||
|
},
|
||||||
|
// 删除OTA固件
|
||||||
|
deleteOta(id, callback) {
|
||||||
|
RequestService.sendRequest()
|
||||||
|
.url(`${getServiceUrl()}/otaMag/${id}`)
|
||||||
|
.method('DELETE')
|
||||||
|
.success((res) => {
|
||||||
|
RequestService.clearRequestTime();
|
||||||
|
callback(res);
|
||||||
|
})
|
||||||
|
.fail((err) => {
|
||||||
|
console.error('删除OTA固件失败:', err);
|
||||||
|
RequestService.reAjaxFun(() => {
|
||||||
|
this.deleteOta(id, callback);
|
||||||
|
});
|
||||||
|
}).send();
|
||||||
|
},
|
||||||
|
// 上传固件文件
|
||||||
|
uploadFirmware(file, callback) {
|
||||||
|
const formData = new FormData();
|
||||||
|
formData.append('file', file);
|
||||||
|
RequestService.sendRequest()
|
||||||
|
.url(`${getServiceUrl()}/otaMag/upload`)
|
||||||
|
.method('POST')
|
||||||
|
.data(formData)
|
||||||
|
.success((res) => {
|
||||||
|
RequestService.clearRequestTime();
|
||||||
|
callback(res);
|
||||||
|
})
|
||||||
|
.fail((err) => {
|
||||||
|
console.error('上传固件文件失败:', err);
|
||||||
|
RequestService.reAjaxFun(() => {
|
||||||
|
this.uploadFirmware(file, callback);
|
||||||
|
});
|
||||||
|
}).send();
|
||||||
|
},
|
||||||
|
// 获取固件下载链接
|
||||||
|
getDownloadUrl(id, callback) {
|
||||||
|
RequestService.sendRequest()
|
||||||
|
.url(`${getServiceUrl()}/otaMag/getDownloadUrl/${id}`)
|
||||||
|
.method('GET')
|
||||||
|
.success((res) => {
|
||||||
|
RequestService.clearRequestTime();
|
||||||
|
callback(res);
|
||||||
|
})
|
||||||
|
.fail((err) => {
|
||||||
|
console.error('获取下载链接失败:', err);
|
||||||
|
RequestService.reAjaxFun(() => {
|
||||||
|
this.getDownloadUrl(id, callback);
|
||||||
|
});
|
||||||
|
}).send();
|
||||||
|
}
|
||||||
|
}
|
||||||
Binary file not shown.
|
After Width: | Height: | Size: 7.6 KiB |
@@ -77,7 +77,12 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div style="display: flex;justify-content: center;">
|
<div style="display: flex;justify-content: center;">
|
||||||
<el-button type="primary" @click="confirm" class="save-btn">
|
<el-button
|
||||||
|
type="primary"
|
||||||
|
@click="confirm"
|
||||||
|
class="save-btn"
|
||||||
|
:loading="saving"
|
||||||
|
:disabled="saving">
|
||||||
保存
|
保存
|
||||||
</el-button>
|
</el-button>
|
||||||
</div>
|
</div>
|
||||||
@@ -94,6 +99,7 @@ export default {
|
|||||||
},
|
},
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
|
saving: false,
|
||||||
providers: [],
|
providers: [],
|
||||||
dialogVisible: false,
|
dialogVisible: false,
|
||||||
providersLoaded: false,
|
providersLoaded: false,
|
||||||
@@ -175,6 +181,7 @@ export default {
|
|||||||
},
|
},
|
||||||
|
|
||||||
handleClose() {
|
handleClose() {
|
||||||
|
this.saving = false;
|
||||||
this.$emit('update:visible', false);
|
this.$emit('update:visible', false);
|
||||||
},
|
},
|
||||||
initDynamicConfig() {
|
initDynamicConfig() {
|
||||||
@@ -185,8 +192,11 @@ export default {
|
|||||||
this.formData.configJson = newConfig;
|
this.formData.configJson = newConfig;
|
||||||
},
|
},
|
||||||
confirm() {
|
confirm() {
|
||||||
|
this.saving = true;
|
||||||
|
|
||||||
if (!this.formData.supplier) {
|
if (!this.formData.supplier) {
|
||||||
this.$message.error('请选择供应器');
|
this.$message.error('请选择供应器');
|
||||||
|
this.saving = false;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -206,11 +216,18 @@ export default {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
this.$emit('confirm', submitData);
|
try {
|
||||||
this.$emit('update:visible', false);
|
this.$emit('confirm', submitData);
|
||||||
this.resetForm();
|
this.$emit('update:visible', false);
|
||||||
|
this.resetForm();
|
||||||
|
} catch (e) {
|
||||||
|
console.error(e);
|
||||||
|
} finally {
|
||||||
|
this.saving = false;
|
||||||
|
}
|
||||||
},
|
},
|
||||||
resetForm() {
|
resetForm() {
|
||||||
|
this.saving = false;
|
||||||
this.formData = {
|
this.formData = {
|
||||||
modelName: '',
|
modelName: '',
|
||||||
modelCode: '',
|
modelCode: '',
|
||||||
|
|||||||
@@ -11,7 +11,7 @@
|
|||||||
<div style="height: 1px;background: #e8f0ff;" />
|
<div style="height: 1px;background: #e8f0ff;" />
|
||||||
<div style="margin: 22px 15px;">
|
<div style="margin: 22px 15px;">
|
||||||
<div style="font-weight: 400;font-size: 14px;text-align: left;color: #3d4566;">
|
<div style="font-weight: 400;font-size: 14px;text-align: left;color: #3d4566;">
|
||||||
<div style="color: red;display: inline-block;">*</div> 智慧体名称:
|
<div style="color: red;display: inline-block;">*</div> 智能体名称:
|
||||||
</div>
|
</div>
|
||||||
<div class="input-46" style="margin-top: 12px;">
|
<div class="input-46" style="margin-top: 12px;">
|
||||||
<el-input ref="inputRef" placeholder="请输入智能体名称.." v-model="wisdomBodyName" @keyup.enter.native="confirm" />
|
<el-input ref="inputRef" placeholder="请输入智能体名称.." v-model="wisdomBodyName" @keyup.enter.native="confirm" />
|
||||||
|
|||||||
@@ -23,12 +23,6 @@
|
|||||||
<div class="settings-btn" @click="handleConfigure">
|
<div class="settings-btn" @click="handleConfigure">
|
||||||
配置角色
|
配置角色
|
||||||
</div>
|
</div>
|
||||||
<!-- <div class="settings-btn">-->
|
|
||||||
<!-- 声纹识别-->
|
|
||||||
<!-- </div>-->
|
|
||||||
<!-- <div class="settings-btn">-->
|
|
||||||
<!-- 历史对话-->
|
|
||||||
<!-- </div>-->
|
|
||||||
<div class="settings-btn" @click="handleDeviceManage">
|
<div class="settings-btn" @click="handleDeviceManage">
|
||||||
设备管理({{ device.deviceCount }})
|
设备管理({{ device.deviceCount }})
|
||||||
</div>
|
</div>
|
||||||
@@ -80,7 +74,7 @@ export default {
|
|||||||
|
|
||||||
.settings-btn {
|
.settings-btn {
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
font-size: 10px;
|
font-size: 12px;
|
||||||
color: #5778ff;
|
color: #5778ff;
|
||||||
background: #e6ebff;
|
background: #e6ebff;
|
||||||
width: auto;
|
width: auto;
|
||||||
@@ -95,7 +89,7 @@ export default {
|
|||||||
display: flex;
|
display: flex;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
margin-top: 15px;
|
margin-top: 15px;
|
||||||
font-size: 10px;
|
font-size: 12px;
|
||||||
color: #979db1;
|
color: #979db1;
|
||||||
font-weight: 400;
|
font-weight: 400;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,136 @@
|
|||||||
|
<template>
|
||||||
|
<el-dialog :title="title" :visible.sync="visible" width="500px" @close="handleClose">
|
||||||
|
<el-form ref="form" :model="form" :rules="rules" label-width="100px">
|
||||||
|
<el-form-item label="固件名称" prop="firmwareName">
|
||||||
|
<el-input v-model="form.firmwareName" placeholder="请输入固件名称(板子+版本号)"></el-input>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="固件类型" prop="type">
|
||||||
|
<el-select v-model="form.type" placeholder="请选择固件类型" style="width: 100%;" filterable>
|
||||||
|
<el-option v-for="item in firmwareTypes" :key="item.key" :label="item.name" :value="item.key"></el-option>
|
||||||
|
</el-select>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="版本号" prop="version">
|
||||||
|
<el-input v-model="form.version" placeholder="请输入版本号(x.x.x格式)"></el-input>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="固件文件" prop="firmwarePath">
|
||||||
|
<el-upload class="upload-demo" action="#" :http-request="handleUpload" :before-upload="beforeUpload"
|
||||||
|
:accept="'.bin,.apk'" :limit="1" :multiple="false" :auto-upload="true">
|
||||||
|
<el-button size="small" type="primary">点击上传</el-button>
|
||||||
|
<div slot="tip" class="el-upload__tip">只能上传固件文件(.bin/.apk),且不超过100MB</div>
|
||||||
|
</el-upload>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="备注" prop="remark">
|
||||||
|
<el-input type="textarea" v-model="form.remark" placeholder="请输入备注信息"></el-input>
|
||||||
|
</el-form-item>
|
||||||
|
</el-form>
|
||||||
|
<div slot="footer" class="dialog-footer">
|
||||||
|
<el-button @click="handleCancel">取 消</el-button>
|
||||||
|
<el-button type="primary" @click="handleSubmit">确 定</el-button>
|
||||||
|
</div>
|
||||||
|
</el-dialog>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import Api from '@/apis/api';
|
||||||
|
import { FIRMWARE_TYPES } from '@/utils';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'FirmwareDialog',
|
||||||
|
props: {
|
||||||
|
visible: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false
|
||||||
|
},
|
||||||
|
title: {
|
||||||
|
type: String,
|
||||||
|
default: ''
|
||||||
|
},
|
||||||
|
form: {
|
||||||
|
type: Object,
|
||||||
|
default: () => ({})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
firmwareTypes: FIRMWARE_TYPES,
|
||||||
|
rules: {
|
||||||
|
firmwareName: [
|
||||||
|
{ required: true, message: '请输入固件名称(板子+版本号)', trigger: 'blur' }
|
||||||
|
],
|
||||||
|
type: [
|
||||||
|
{ required: true, message: '请选择固件类型', trigger: 'change' }
|
||||||
|
],
|
||||||
|
version: [
|
||||||
|
{ required: true, message: '请输入版本号', trigger: 'blur' },
|
||||||
|
{ pattern: /^\d+\.\d+\.\d+$/, message: '版本号格式不正确,请输入x.x.x格式', trigger: 'blur' }
|
||||||
|
],
|
||||||
|
firmwarePath: [
|
||||||
|
{ required: false, message: '请上传固件文件', trigger: 'change' }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
handleClose() {
|
||||||
|
this.$refs.form.clearValidate();
|
||||||
|
this.$emit('cancel');
|
||||||
|
},
|
||||||
|
handleCancel() {
|
||||||
|
this.$refs.form.clearValidate();
|
||||||
|
this.$emit('cancel');
|
||||||
|
},
|
||||||
|
handleSubmit() {
|
||||||
|
this.$refs.form.validate(valid => {
|
||||||
|
if (valid) {
|
||||||
|
// 如果是新增模式且没有上传文件,则提示错误
|
||||||
|
if (!this.form.id && !this.form.firmwarePath) {
|
||||||
|
this.$message.error('请上传固件文件')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
// 提交成功后将关闭对话框的逻辑交给父组件处理
|
||||||
|
this.$emit('submit', this.form)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
},
|
||||||
|
beforeUpload(file) {
|
||||||
|
const isValidSize = file.size / 1024 / 1024 < 100
|
||||||
|
const isValidType = ['.bin', '.apk'].some(ext => file.name.toLowerCase().endsWith(ext))
|
||||||
|
|
||||||
|
if (!isValidType) {
|
||||||
|
this.$message.error('只能上传.bin/.apk格式的固件文件!')
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if (!isValidSize) {
|
||||||
|
this.$message.error('固件文件大小不能超过100MB!')
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
},
|
||||||
|
handleUpload(options) {
|
||||||
|
const { file } = options
|
||||||
|
Api.ota.uploadFirmware(file, (res) => {
|
||||||
|
res = res.data
|
||||||
|
if (res.code === 0) {
|
||||||
|
this.form.firmwarePath = res.data
|
||||||
|
this.form.size = file.size
|
||||||
|
this.$message.success('固件文件上传成功')
|
||||||
|
} else {
|
||||||
|
this.$message.error(res.msg || '文件上传失败')
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.upload-demo {
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
|
||||||
|
.el-upload__tip {
|
||||||
|
line-height: 1.2;
|
||||||
|
padding-top: 5px;
|
||||||
|
color: #909399;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -32,6 +32,12 @@
|
|||||||
:style="{ filter: $route.path === '/params-management' ? 'brightness(0) invert(1)' : 'None' }" />
|
:style="{ filter: $route.path === '/params-management' ? 'brightness(0) invert(1)' : 'None' }" />
|
||||||
参数管理
|
参数管理
|
||||||
</div>
|
</div>
|
||||||
|
<div v-if="isSuperAdmin" class="equipment-management"
|
||||||
|
:class="{ 'active-tab': $route.path === '/ota-management' }" @click="goOtaManagement">
|
||||||
|
<img loading="lazy" alt="" src="@/assets/header/firmware_update.png"
|
||||||
|
:style="{ filter: $route.path === '/ota-management' ? 'brightness(0) invert(1)' : 'None' }" />
|
||||||
|
OTA管理
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 右侧元素 -->
|
<!-- 右侧元素 -->
|
||||||
@@ -105,6 +111,9 @@ export default {
|
|||||||
goParamManagement() {
|
goParamManagement() {
|
||||||
this.$router.push('/params-management')
|
this.$router.push('/params-management')
|
||||||
},
|
},
|
||||||
|
goOtaManagement() {
|
||||||
|
this.$router.push('/ota-management')
|
||||||
|
},
|
||||||
// 获取用户信息
|
// 获取用户信息
|
||||||
fetchUserInfo() {
|
fetchUserInfo() {
|
||||||
userApi.getUserInfo(({ data }) => {
|
userApi.getUserInfo(({ data }) => {
|
||||||
@@ -170,7 +179,7 @@ export default {
|
|||||||
.header {
|
.header {
|
||||||
background: #f6fcfe66;
|
background: #f6fcfe66;
|
||||||
border: 1px solid #fff;
|
border: 1px solid #fff;
|
||||||
height: 53px !important;
|
height: 63px !important;
|
||||||
min-width: 900px;
|
min-width: 900px;
|
||||||
/* 设置最小宽度防止过度压缩 */
|
/* 设置最小宽度防止过度压缩 */
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
@@ -197,7 +206,7 @@ export default {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.brand-img {
|
.brand-img {
|
||||||
height: 18px;
|
height: 20px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.header-center {
|
.header-center {
|
||||||
@@ -219,13 +228,13 @@ export default {
|
|||||||
|
|
||||||
.equipment-management {
|
.equipment-management {
|
||||||
padding: 0 9px;
|
padding: 0 9px;
|
||||||
width: 82px;
|
width: px;
|
||||||
height: 24px;
|
height: 30px;
|
||||||
border-radius: 12px;
|
border-radius: 15px;
|
||||||
background: #deeafe;
|
background: #deeafe;
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
font-size: 10px;
|
font-size: 14px;
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
gap: 7px;
|
gap: 7px;
|
||||||
color: #3d4566;
|
color: #3d4566;
|
||||||
@@ -235,6 +244,7 @@ export default {
|
|||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
/* 防止导航按钮被压缩 */
|
/* 防止导航按钮被压缩 */
|
||||||
|
padding: 0px 15px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.equipment-management.active-tab {
|
.equipment-management.active-tab {
|
||||||
|
|||||||
@@ -76,7 +76,12 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div style="display: flex;justify-content: center;">
|
<div style="display: flex;justify-content: center;">
|
||||||
<el-button type="primary" @click="handleSave" class="save-btn">
|
<el-button
|
||||||
|
type="primary"
|
||||||
|
@click="handleSave"
|
||||||
|
class="save-btn"
|
||||||
|
:loading="saving"
|
||||||
|
:disabled="saving">
|
||||||
保存
|
保存
|
||||||
</el-button>
|
</el-button>
|
||||||
</div>
|
</div>
|
||||||
@@ -102,6 +107,7 @@ export default {
|
|||||||
dialogVisible: this.visible,
|
dialogVisible: this.visible,
|
||||||
providers: [],
|
providers: [],
|
||||||
providersLoaded: false,
|
providersLoaded: false,
|
||||||
|
saving: false,
|
||||||
allProvidersData: null,
|
allProvidersData: null,
|
||||||
pendingProviderType: null,
|
pendingProviderType: null,
|
||||||
pendingModelData: null,
|
pendingModelData: null,
|
||||||
@@ -195,6 +201,8 @@ export default {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
handleSave() {
|
handleSave() {
|
||||||
|
this.saving = true; // 开始保存加载
|
||||||
|
|
||||||
const provideCode = this.form.configJson.type;
|
const provideCode = this.form.configJson.type;
|
||||||
const formData = {
|
const formData = {
|
||||||
id: this.modelData.id,
|
id: this.modelData.id,
|
||||||
@@ -209,8 +217,19 @@ export default {
|
|||||||
...this.form.configJson,
|
...this.form.configJson,
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
this.$emit("save", { provideCode, formData });
|
|
||||||
this.dialogVisible = false;
|
this.$emit("save", {
|
||||||
|
provideCode,
|
||||||
|
formData,
|
||||||
|
done: () => {
|
||||||
|
this.saving = false; // 保存完成后回调
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// 如果父组件不处理done回调,3秒后自动关闭加载状态
|
||||||
|
setTimeout(() => {
|
||||||
|
this.saving = false;
|
||||||
|
}, 3000);
|
||||||
},
|
},
|
||||||
loadProviders() {
|
loadProviders() {
|
||||||
if (this.providersLoaded) return;
|
if (this.providersLoaded) return;
|
||||||
|
|||||||
@@ -40,7 +40,12 @@
|
|||||||
</el-form>
|
</el-form>
|
||||||
|
|
||||||
<div class="dialog-footer">
|
<div class="dialog-footer">
|
||||||
<el-button type="primary" @click="submit" class="save-btn">
|
<el-button
|
||||||
|
type="primary"
|
||||||
|
@click="submit"
|
||||||
|
class="save-btn"
|
||||||
|
:loading="saving"
|
||||||
|
:disabled="saving">
|
||||||
保存
|
保存
|
||||||
</el-button>
|
</el-button>
|
||||||
<el-button @click="cancel" class="cancel-btn">
|
<el-button @click="cancel" class="cancel-btn">
|
||||||
@@ -76,6 +81,7 @@ export default {
|
|||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
dialogKey: Date.now(),
|
dialogKey: Date.now(),
|
||||||
|
saving: false,
|
||||||
valueTypeOptions: [
|
valueTypeOptions: [
|
||||||
{ value: 'string', label: '字符串(string)' },
|
{ value: 'string', label: '字符串(string)' },
|
||||||
{ value: 'number', label: '数字(number)' },
|
{ value: 'number', label: '数字(number)' },
|
||||||
@@ -100,11 +106,22 @@ export default {
|
|||||||
submit() {
|
submit() {
|
||||||
this.$refs.form.validate((valid) => {
|
this.$refs.form.validate((valid) => {
|
||||||
if (valid) {
|
if (valid) {
|
||||||
this.$emit('submit', this.form);
|
this.saving = true; // 开始加载
|
||||||
|
this.$emit('submit', {
|
||||||
|
form: this.form,
|
||||||
|
done: () => {
|
||||||
|
this.saving = false; // 加载完成
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
this.saving = false;
|
||||||
|
}, 3000);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
cancel() {
|
cancel() {
|
||||||
|
this.saving = false; // 取消时重置状态
|
||||||
this.$emit('cancel');
|
this.$emit('cancel');
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -14,13 +14,16 @@
|
|||||||
class="table-container"
|
class="table-container"
|
||||||
ref="tableContainer"
|
ref="tableContainer"
|
||||||
@scroll="handleScroll">
|
@scroll="handleScroll">
|
||||||
<el-table
|
<el-table
|
||||||
v-loading="loading"
|
v-loading="loading"
|
||||||
:data="filteredTtsModels"
|
:data="filteredTtsModels"
|
||||||
style="width: 100%;"
|
style="width: 100%;"
|
||||||
class="data-table"
|
class="data-table"
|
||||||
header-row-class-name="table-header"
|
header-row-class-name="table-header"
|
||||||
:fit="true">
|
:fit="true"
|
||||||
|
element-loading-text="拼命加载中"
|
||||||
|
element-loading-spinner="el-icon-loading"
|
||||||
|
element-loading-background="rgba(0, 0, 0, 0.8)">
|
||||||
<el-table-column label="选择" width="50" align="center">
|
<el-table-column label="选择" width="50" align="center">
|
||||||
<template slot-scope="scope">
|
<template slot-scope="scope">
|
||||||
<el-checkbox v-model="scope.row.selected"></el-checkbox>
|
<el-checkbox v-model="scope.row.selected"></el-checkbox>
|
||||||
@@ -187,28 +190,28 @@ export default {
|
|||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
loadData() {
|
loadData() {
|
||||||
|
this.loading = true;
|
||||||
const params = {
|
const params = {
|
||||||
ttsModelId: this.ttsModelId,
|
ttsModelId: this.ttsModelId,
|
||||||
page: this.currentPage,
|
page: this.currentPage,
|
||||||
limit: this.pageSize,
|
limit: this.pageSize,
|
||||||
name: this.searchQuery
|
name: this.searchQuery
|
||||||
};
|
};
|
||||||
|
|
||||||
Api.timbre.getVoiceList(params, (data) => {
|
Api.timbre.getVoiceList(params, (data) => {
|
||||||
if (data.code === 0) {
|
if (data.code === 0) {
|
||||||
this.ttsModels = data.data.list
|
this.ttsModels = data.data.list
|
||||||
.map(item => ({
|
.map(item => ({
|
||||||
id: item.id || '',
|
id: item.id || '',
|
||||||
voiceCode: item.ttsVoice || '',
|
voiceCode: item.ttsVoice || '',
|
||||||
voiceName: item.name || '未命名音色',
|
voiceName: item.name || '未命名音色',
|
||||||
languageType: item.languages || '',
|
languageType: item.languages || '',
|
||||||
remark: item.remark || '',
|
remark: item.remark || '',
|
||||||
voiceDemo: item.voiceDemo || '',
|
voiceDemo: item.voiceDemo || '',
|
||||||
selected: false,
|
selected: false,
|
||||||
editing: false,
|
editing: false,
|
||||||
sort: Number(item.sort)
|
sort: Number(item.sort)
|
||||||
}))
|
}))
|
||||||
.sort((a, b) => a.sort - b.sort);
|
.sort((a, b) => a.sort - b.sort);
|
||||||
this.total = data.total;
|
this.total = data.total;
|
||||||
} else {
|
} else {
|
||||||
this.$message.error({
|
this.$message.error({
|
||||||
@@ -216,12 +219,14 @@ export default {
|
|||||||
showClose: true
|
showClose: true
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
this.loading = false;
|
||||||
}, (err) => {
|
}, (err) => {
|
||||||
console.error('加载失败:', err);
|
console.error('加载失败:', err);
|
||||||
this.$message.error({
|
this.$message.error({
|
||||||
message: '加载音色数据失败',
|
message: '加载音色数据失败',
|
||||||
showClose: true
|
showClose: true
|
||||||
});
|
});
|
||||||
|
this.loading = false;
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|||||||
@@ -62,13 +62,6 @@ const routes = [
|
|||||||
return import('../views/ModelConfig.vue')
|
return import('../views/ModelConfig.vue')
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
|
||||||
path: '/test',
|
|
||||||
name: 'TestServer',
|
|
||||||
component: function () {
|
|
||||||
return import('../views/test.vue')
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
path: '/params-management',
|
path: '/params-management',
|
||||||
name: 'ParamsManagement',
|
name: 'ParamsManagement',
|
||||||
@@ -80,6 +73,17 @@ const routes = [
|
|||||||
title: '参数管理'
|
title: '参数管理'
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: '/ota-management',
|
||||||
|
name: 'OtaManagement',
|
||||||
|
component: function () {
|
||||||
|
return import('../views/OtaManagement.vue')
|
||||||
|
},
|
||||||
|
meta: {
|
||||||
|
requiresAuth: true,
|
||||||
|
title: 'OTA管理'
|
||||||
|
}
|
||||||
|
},
|
||||||
]
|
]
|
||||||
|
|
||||||
const router = new VueRouter({
|
const router = new VueRouter({
|
||||||
|
|||||||
@@ -0,0 +1,21 @@
|
|||||||
|
// 日期格式化函数
|
||||||
|
export function formatDate(date) {
|
||||||
|
if (!date) return '';
|
||||||
|
const d = new Date(date);
|
||||||
|
const year = d.getFullYear();
|
||||||
|
const month = String(d.getMonth() + 1).padStart(2, '0');
|
||||||
|
const day = String(d.getDate()).padStart(2, '0');
|
||||||
|
const hour = String(d.getHours()).padStart(2, '0');
|
||||||
|
const minute = String(d.getMinutes()).padStart(2, '0');
|
||||||
|
const second = String(d.getSeconds()).padStart(2, '0');
|
||||||
|
return `${year}-${month}-${day} ${hour}:${minute}:${second}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 文件大小格式化函数
|
||||||
|
export function formatFileSize(bytes) {
|
||||||
|
if (!bytes || bytes === 0) return '0 B';
|
||||||
|
const units = ['B', 'KB', 'MB', 'GB', 'TB'];
|
||||||
|
const k = 1024;
|
||||||
|
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
||||||
|
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + units[i];
|
||||||
|
}
|
||||||
@@ -136,3 +136,63 @@ export function getUUID() {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 固件类型选项
|
||||||
|
*/
|
||||||
|
export const FIRMWARE_TYPES = [
|
||||||
|
{ "key": "bread-compact-wifi", "name": "面包板新版接线(WiFi)" },
|
||||||
|
{ "key": "bread-compact-wifi-lcd", "name": "面包板新版接线(WiFi)+ LCD" },
|
||||||
|
{ "key": "bread-compact-ml307", "name": "面包板新版接线(ML307 AT)" },
|
||||||
|
{ "key": "bread-compact-esp32", "name": "面包板(WiFi) ESP32 DevKit" },
|
||||||
|
{ "key": "bread-compact-esp32-lcd", "name": "面包板(WiFi+ LCD) ESP32 DevKit" },
|
||||||
|
{ "key": "df-k10", "name": "DFRobot 行空板 k10" },
|
||||||
|
{ "key": "esp32-cgc", "name": "ESP32 CGC" },
|
||||||
|
{ "key": "esp-box-3", "name": "ESP BOX 3" },
|
||||||
|
{ "key": "esp-box", "name": "ESP BOX" },
|
||||||
|
{ "key": "esp-box-lite", "name": "ESP BOX Lite" },
|
||||||
|
{ "key": "kevin-box-1", "name": "Kevin Box 1" },
|
||||||
|
{ "key": "kevin-box-2", "name": "Kevin Box 2" },
|
||||||
|
{ "key": "kevin-c3", "name": "Kevin C3" },
|
||||||
|
{ "key": "kevin-sp-v3-dev", "name": "Kevin SP V3开发板" },
|
||||||
|
{ "key": "kevin-sp-v4-dev", "name": "Kevin SP V4开发板" },
|
||||||
|
{ "key": "kevin-yuying-313lcd", "name": "鱼鹰科技3.13LCD开发板" },
|
||||||
|
{ "key": "lichuang-dev", "name": "立创·实战派ESP32-S3开发板" },
|
||||||
|
{ "key": "lichuang-c3-dev", "name": "立创·实战派ESP32-C3开发板" },
|
||||||
|
{ "key": "magiclick-2p4", "name": "神奇按钮 Magiclick_2.4" },
|
||||||
|
{ "key": "magiclick-2p5", "name": "神奇按钮 Magiclick_2.5" },
|
||||||
|
{ "key": "magiclick-c3", "name": "神奇按钮 Magiclick_C3" },
|
||||||
|
{ "key": "magiclick-c3-v2", "name": "神奇按钮 Magiclick_C3_v2" },
|
||||||
|
{ "key": "m5stack-core-s3", "name": "M5Stack CoreS3" },
|
||||||
|
{ "key": "atoms3-echo-base", "name": "AtomS3 + Echo Base" },
|
||||||
|
{ "key": "atoms3r-echo-base", "name": "AtomS3R + Echo Base" },
|
||||||
|
{ "key": "atoms3r-cam-m12-echo-base", "name": "AtomS3R CAM/M12 + Echo Base" },
|
||||||
|
{ "key": "atommatrix-echo-base", "name": "AtomMatrix + Echo Base" },
|
||||||
|
{ "key": "xmini-c3", "name": "虾哥 Mini C3" },
|
||||||
|
{ "key": "esp32s3-korvo2-v3", "name": "ESP32S3_KORVO2_V3开发板" },
|
||||||
|
{ "key": "esp-sparkbot", "name": "ESP-SparkBot开发板" },
|
||||||
|
{ "key": "esp-spot-s3", "name": "ESP-Spot-S3" },
|
||||||
|
{ "key": "esp32-s3-touch-amoled-1.8", "name": "Waveshare ESP32-S3-Touch-AMOLED-1.8" },
|
||||||
|
{ "key": "esp32-s3-touch-lcd-1.85c", "name": "Waveshare ESP32-S3-Touch-LCD-1.85C" },
|
||||||
|
{ "key": "esp32-s3-touch-lcd-1.85", "name": "Waveshare ESP32-S3-Touch-LCD-1.85" },
|
||||||
|
{ "key": "esp32-s3-touch-lcd-1.46", "name": "Waveshare ESP32-S3-Touch-LCD-1.46" },
|
||||||
|
{ "key": "esp32-s3-touch-lcd-3.5", "name": "Waveshare ESP32-S3-Touch-LCD-3.5" },
|
||||||
|
{ "key": "tudouzi", "name": "土豆子" },
|
||||||
|
{ "key": "lilygo-t-circle-s3", "name": "LILYGO T-Circle-S3" },
|
||||||
|
{ "key": "lilygo-t-cameraplus-s3", "name": "LILYGO T-CameraPlus-S3" },
|
||||||
|
{ "key": "movecall-moji-esp32s3", "name": "Movecall Moji 小智AI衍生版" },
|
||||||
|
{ "key": "movecall-cuican-esp32s3", "name": "Movecall CuiCan 璀璨·AI吊坠" },
|
||||||
|
{ "key": "atk-dnesp32s3", "name": "正点原子DNESP32S3开发板" },
|
||||||
|
{ "key": "atk-dnesp32s3-box", "name": "正点原子DNESP32S3-BOX" },
|
||||||
|
{ "key": "du-chatx", "name": "嘟嘟开发板CHATX(wifi)" },
|
||||||
|
{ "key": "taiji-pi-s3", "name": "太极小派esp32s3" },
|
||||||
|
{ "key": "xingzhi-cube-0.85tft-wifi", "name": "无名科技星智0.85(WIFI)" },
|
||||||
|
{ "key": "xingzhi-cube-0.85tft-ml307", "name": "无名科技星智0.85(ML307)" },
|
||||||
|
{ "key": "xingzhi-cube-0.96oled-wifi", "name": "无名科技星智0.96(WIFI)" },
|
||||||
|
{ "key": "xingzhi-cube-0.96oled-ml307", "name": "无名科技星智0.96(ML307)" },
|
||||||
|
{ "key": "xingzhi-cube-1.54tft-wifi", "name": "无名科技星智1.54(WIFI)" },
|
||||||
|
{ "key": "xingzhi-cube-1.54tft-ml307", "name": "无名科技星智1.54(ML307)" },
|
||||||
|
{ "key": "sensecap-watcher", "name": "SenseCAP Watcher" },
|
||||||
|
{ "key": "doit-s3-aibox", "name": "四博智联AI陪伴盒子" },
|
||||||
|
{ "key": "mixgo-nova", "name": "元控·青春" }
|
||||||
|
]
|
||||||
|
|
||||||
|
|||||||
@@ -5,8 +5,8 @@
|
|||||||
<div class="operation-bar">
|
<div class="operation-bar">
|
||||||
<h2 class="page-title">设备管理</h2>
|
<h2 class="page-title">设备管理</h2>
|
||||||
<div class="right-operations">
|
<div class="right-operations">
|
||||||
<el-input placeholder="请输入设备型号或Mac地址查询" v-model="searchKeyword"
|
<el-input placeholder="请输入设备型号或Mac地址查询" v-model="searchKeyword" class="search-input"
|
||||||
class="search-input" @keyup.enter.native="handleSearch" clearable />
|
@keyup.enter.native="handleSearch" clearable />
|
||||||
<el-button class="btn-search" @click="handleSearch">搜索</el-button>
|
<el-button class="btn-search" @click="handleSearch">搜索</el-button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -18,11 +18,22 @@
|
|||||||
<el-table
|
<el-table
|
||||||
ref="deviceTable"
|
ref="deviceTable"
|
||||||
:data="paginatedDeviceList"
|
:data="paginatedDeviceList"
|
||||||
@selection-change="handleSelectionChange"
|
|
||||||
class="transparent-table"
|
class="transparent-table"
|
||||||
:header-cell-class-name="headerCellClassName">
|
:header-cell-class-name="headerCellClassName"
|
||||||
<el-table-column type="selection" align="center" width="120"></el-table-column>
|
v-loading="loading"
|
||||||
<el-table-column label="设备型号" prop="model" align="center"></el-table-column>
|
element-loading-text="拼命加载中"
|
||||||
|
element-loading-spinner="el-icon-loading"
|
||||||
|
element-loading-background="rgba(255, 255, 255, 0.7)">
|
||||||
|
<el-table-column label="选择" align="center" width="120">
|
||||||
|
<template slot-scope="scope">
|
||||||
|
<el-checkbox v-model="scope.row.selected"></el-checkbox>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column label="设备型号" prop="model" align="center">
|
||||||
|
<template slot-scope="scope">
|
||||||
|
{{ getFirmwareTypeName(scope.row.model) }}
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
<el-table-column label="固件版本" prop="firmwareVersion" align="center" ></el-table-column>
|
<el-table-column label="固件版本" prop="firmwareVersion" align="center" ></el-table-column>
|
||||||
<el-table-column label="Mac地址" prop="macAddress" align="center"></el-table-column>
|
<el-table-column label="Mac地址" prop="macAddress" align="center"></el-table-column>
|
||||||
<el-table-column label="绑定时间" prop="bindTime" align="center"></el-table-column>
|
<el-table-column label="绑定时间" prop="bindTime" align="center"></el-table-column>
|
||||||
@@ -32,7 +43,8 @@
|
|||||||
<el-input v-if="scope.row.isEdit" v-model="scope.row.remark" size="mini"
|
<el-input v-if="scope.row.isEdit" v-model="scope.row.remark" size="mini"
|
||||||
@blur="stopEditRemark(scope.$index)"></el-input>
|
@blur="stopEditRemark(scope.$index)"></el-input>
|
||||||
<span v-else>
|
<span v-else>
|
||||||
<i v-if="!scope.row.remark" class="el-icon-edit" @click="startEditRemark(scope.$index, scope.row)"></i>
|
<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)">
|
<span v-else @click="startEditRemark(scope.$index, scope.row)">
|
||||||
{{ scope.row.remark }}
|
{{ scope.row.remark }}
|
||||||
</span>
|
</span>
|
||||||
@@ -56,32 +68,23 @@
|
|||||||
|
|
||||||
<div class="table_bottom">
|
<div class="table_bottom">
|
||||||
<div class="ctrl_btn">
|
<div class="ctrl_btn">
|
||||||
<el-button size="mini" type="primary" class="select-all-btn" @click="toggleAllSelection">
|
<el-button size="mini" type="primary" class="select-all-btn" @click="handleSelectAll">
|
||||||
{{ isAllSelected ? '取消全选' : '全选' }}
|
{{ isAllSelected ? '取消全选' : '全选' }}
|
||||||
</el-button>
|
</el-button>
|
||||||
<el-button type="success" size="mini" class="add-device-btn" @click="handleAddDevice">
|
<el-button type="success" size="mini" class="add-device-btn" @click="handleAddDevice">
|
||||||
新增
|
新增
|
||||||
</el-button>
|
</el-button>
|
||||||
<el-button size="mini" type="danger" icon="el-icon-delete"
|
<el-button size="mini" type="danger" icon="el-icon-delete" @click="deleteSelected">解绑</el-button>
|
||||||
@click="deleteSelected">解绑</el-button>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="custom-pagination">
|
<div class="custom-pagination">
|
||||||
<el-select v-model="pageSize" @change="handlePageSizeChange" class="page-size-select">
|
<el-select v-model="pageSize" @change="handlePageSizeChange" class="page-size-select">
|
||||||
<el-option
|
<el-option v-for="item in pageSizeOptions" :key="item" :label="`${item}条/页`" :value="item">
|
||||||
v-for="item in pageSizeOptions"
|
</el-option>
|
||||||
:key="item"
|
|
||||||
:label="`${item}条/页`"
|
|
||||||
:value="item">
|
|
||||||
</el-option>
|
|
||||||
</el-select>
|
</el-select>
|
||||||
<button class="pagination-btn" :disabled="currentPage === 1" @click="goFirst">首页</button>
|
<button class="pagination-btn" :disabled="currentPage === 1" @click="goFirst">首页</button>
|
||||||
<button class="pagination-btn" :disabled="currentPage === 1" @click="goPrev">上一页</button>
|
<button class="pagination-btn" :disabled="currentPage === 1" @click="goPrev">上一页</button>
|
||||||
<button
|
<button v-for="page in visiblePages" :key="page" class="pagination-btn"
|
||||||
v-for="page in visiblePages"
|
:class="{ active: page === currentPage }" @click="goToPage(page)">
|
||||||
:key="page"
|
|
||||||
class="pagination-btn"
|
|
||||||
:class="{ active: page === currentPage }"
|
|
||||||
@click="goToPage(page)">
|
|
||||||
{{ page }}
|
{{ page }}
|
||||||
</button>
|
</button>
|
||||||
<button class="pagination-btn" :disabled="currentPage === pageCount" @click="goNext">下一页</button>
|
<button class="pagination-btn" :disabled="currentPage === pageCount" @click="goNext">下一页</button>
|
||||||
@@ -103,6 +106,7 @@
|
|||||||
import Api from '@/apis/api';
|
import Api from '@/apis/api';
|
||||||
import AddDeviceDialog from "@/components/AddDeviceDialog.vue";
|
import AddDeviceDialog from "@/components/AddDeviceDialog.vue";
|
||||||
import HeaderBar from "@/components/HeaderBar.vue";
|
import HeaderBar from "@/components/HeaderBar.vue";
|
||||||
|
import { FIRMWARE_TYPES } from "@/utils";
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
components: { HeaderBar, AddDeviceDialog },
|
components: { HeaderBar, AddDeviceDialog },
|
||||||
@@ -120,7 +124,6 @@ export default {
|
|||||||
deviceList: [],
|
deviceList: [],
|
||||||
loading: false,
|
loading: false,
|
||||||
userApi: null,
|
userApi: null,
|
||||||
|
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
@@ -136,7 +139,10 @@ export default {
|
|||||||
paginatedDeviceList() {
|
paginatedDeviceList() {
|
||||||
const start = (this.currentPage - 1) * this.pageSize;
|
const start = (this.currentPage - 1) * this.pageSize;
|
||||||
const end = start + this.pageSize;
|
const end = start + this.pageSize;
|
||||||
return this.filteredDeviceList.slice(start, end);
|
return this.filteredDeviceList.slice(start, end).map(item => ({
|
||||||
|
...item,
|
||||||
|
selected: false
|
||||||
|
}));
|
||||||
},
|
},
|
||||||
pageCount() {
|
pageCount() {
|
||||||
return Math.ceil(this.filteredDeviceList.length / this.pageSize);
|
return Math.ceil(this.filteredDeviceList.length / this.pageSize);
|
||||||
@@ -173,15 +179,16 @@ export default {
|
|||||||
this.currentPage = 1;
|
this.currentPage = 1;
|
||||||
},
|
},
|
||||||
|
|
||||||
handleSelectionChange(val) {
|
handleSelectAll() {
|
||||||
this.selectedDevices = val;
|
this.isAllSelected = !this.isAllSelected;
|
||||||
this.isAllSelected = val.length === this.paginatedDeviceList.length;
|
this.paginatedDeviceList.forEach(row => {
|
||||||
},
|
row.selected = this.isAllSelected;
|
||||||
toggleAllSelection() {
|
});
|
||||||
this.$refs.deviceTable.toggleAllSelection();
|
this.selectedDevices = this.paginatedDeviceList.filter(device => device.selected);
|
||||||
},
|
},
|
||||||
|
|
||||||
deleteSelected() {
|
deleteSelected() {
|
||||||
|
this.selectedDevices = this.paginatedDeviceList.filter(device => device.selected);
|
||||||
if (this.selectedDevices.length === 0) {
|
if (this.selectedDevices.length === 0) {
|
||||||
this.$message.warning({
|
this.$message.warning({
|
||||||
message: '请至少选择一条记录',
|
message: '请至少选择一条记录',
|
||||||
@@ -301,7 +308,7 @@ export default {
|
|||||||
rawBindTime: new Date(device.createDate).getTime()
|
rawBindTime: new Date(device.createDate).getTime()
|
||||||
};
|
};
|
||||||
})
|
})
|
||||||
.sort((a, b) => a.rawBindTime - b.rawBindTime);
|
.sort((a, b) => a.rawBindTime - b.rawBindTime);
|
||||||
this.activeSearchKeyword = "";
|
this.activeSearchKeyword = "";
|
||||||
this.searchKeyword = "";
|
this.searchKeyword = "";
|
||||||
} else {
|
} else {
|
||||||
@@ -309,12 +316,16 @@ export default {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
headerCellClassName({columnIndex}) {
|
headerCellClassName({ columnIndex }) {
|
||||||
if (columnIndex === 0) {
|
if (columnIndex === 0) {
|
||||||
return "custom-selection-header";
|
return "custom-selection-header";
|
||||||
}
|
}
|
||||||
return "";
|
return "";
|
||||||
}
|
},
|
||||||
|
getFirmwareTypeName(type) {
|
||||||
|
const firmwareType = FIRMWARE_TYPES.find(item => item.key === type);
|
||||||
|
return firmwareType ? firmwareType.name : type;
|
||||||
|
},
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
@@ -383,12 +394,12 @@ export default {
|
|||||||
transition: border-color 0.2s;
|
transition: border-color 0.2s;
|
||||||
}
|
}
|
||||||
|
|
||||||
::v-deep .page-size-select{
|
::v-deep .page-size-select {
|
||||||
width: 100px;
|
width: 100px;
|
||||||
margin-right: 8px;
|
margin-right: 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
::v-deep .page-size-select .el-input__inner{
|
::v-deep .page-size-select .el-input__inner {
|
||||||
height: 32px;
|
height: 32px;
|
||||||
line-height: 32px;
|
line-height: 32px;
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
@@ -397,7 +408,8 @@ export default {
|
|||||||
color: #606266;
|
color: #606266;
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
}
|
}
|
||||||
::v-deep .page-size-select .el-input__suffix{
|
|
||||||
|
::v-deep .page-size-select .el-input__suffix {
|
||||||
right: 6px;
|
right: 6px;
|
||||||
width: 15px;
|
width: 15px;
|
||||||
height: 20px;
|
height: 20px;
|
||||||
@@ -408,13 +420,14 @@ export default {
|
|||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
}
|
}
|
||||||
|
|
||||||
::v-deep .page-size-select .el-input__suffix-inner{
|
::v-deep .page-size-select .el-input__suffix-inner {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
::v-deep .page-size-select .el-icon-arrow-up:before{
|
|
||||||
|
::v-deep .page-size-select .el-icon-arrow-up:before {
|
||||||
content: "";
|
content: "";
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
border-left: 6px solid transparent;
|
border-left: 6px solid transparent;
|
||||||
@@ -459,6 +472,13 @@ export default {
|
|||||||
flex: 1;
|
flex: 1;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
::v-deep .el-card__body {
|
||||||
|
padding: 15px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
flex: 1;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
.table_bottom {
|
.table_bottom {
|
||||||
display: flex;
|
display: flex;
|
||||||
@@ -468,6 +488,7 @@ export default {
|
|||||||
padding-bottom: 10px;
|
padding-bottom: 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
.ctrl_btn {
|
.ctrl_btn {
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: 8px;
|
gap: 8px;
|
||||||
@@ -613,13 +634,7 @@ export default {
|
|||||||
display: none !important;
|
display: none !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
:deep(.custom-selection-header::after) {
|
|
||||||
content: "选择";
|
|
||||||
display: inline-block;
|
|
||||||
color: black;
|
|
||||||
font-weight: bold;
|
|
||||||
padding-bottom: 18px;
|
|
||||||
}
|
|
||||||
|
|
||||||
:deep(.el-table .el-button--text) {
|
:deep(.el-table .el-button--text) {
|
||||||
color: #7079aa;
|
color: #7079aa;
|
||||||
@@ -656,4 +671,25 @@ export default {
|
|||||||
padding-bottom: 16px;
|
padding-bottom: 16px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
:deep(.el-checkbox__inner) {
|
||||||
|
background-color: #eeeeee !important;
|
||||||
|
border-color: #cccccc !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.el-checkbox__inner:hover) {
|
||||||
|
border-color: #cccccc !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.el-checkbox__input.is-checked .el-checkbox__inner) {
|
||||||
|
background-color: #5f70f3 !important;
|
||||||
|
border-color: #5f70f3 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
::v-deep .el-table--border::after,
|
||||||
|
::v-deep .el-table--group::after,
|
||||||
|
::v-deep .el-table::before {
|
||||||
|
display: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -44,12 +44,22 @@
|
|||||||
<!-- 右侧内容 -->
|
<!-- 右侧内容 -->
|
||||||
<div class="content-area">
|
<div class="content-area">
|
||||||
<el-card class="model-card" shadow="never">
|
<el-card class="model-card" shadow="never">
|
||||||
<el-table ref="modelTable" style="width: 100%" :header-cell-style="{ background: 'transparent' }"
|
<el-table
|
||||||
:data="modelList" class="data-table" header-row-class-name="table-header"
|
ref="modelTable"
|
||||||
:header-cell-class-name="headerCellClassName" @selection-change="handleSelectionChange">
|
style="width: 100%"
|
||||||
|
v-loading="loading"
|
||||||
|
element-loading-text="拼命加载中"
|
||||||
|
element-loading-spinner="el-icon-loading"
|
||||||
|
element-loading-background="rgba(255, 255, 255, 0.7)"
|
||||||
|
: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 type="selection" width="55" align="center"></el-table-column>
|
||||||
|
<el-table-column label="模型ID" prop="id" align="center"></el-table-column>
|
||||||
<el-table-column label="模型名称" prop="modelName" 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">
|
<el-table-column label="提供商" align="center">
|
||||||
<template slot-scope="scope">
|
<template slot-scope="scope">
|
||||||
{{ scope.row.configJson.type || '未知' }}
|
{{ scope.row.configJson.type || '未知' }}
|
||||||
@@ -86,41 +96,37 @@
|
|||||||
</el-table-column>
|
</el-table-column>
|
||||||
</el-table>
|
</el-table>
|
||||||
<div class="table-footer">
|
<div class="table-footer">
|
||||||
<div class="batch-actions">
|
<div class="batch-actions">
|
||||||
<el-button size="mini" type="primary" @click="selectAll">
|
<el-button size="mini" type="primary" @click="selectAll">
|
||||||
{{ isAllSelected ?
|
{{ isAllSelected ?
|
||||||
'取消全选' : '全选' }}
|
'取消全选' : '全选' }}
|
||||||
</el-button>
|
</el-button>
|
||||||
<el-button type="success" size="mini" @click="addModel" class="add-btn">
|
<el-button type="success" size="mini" @click="addModel" class="add-btn">
|
||||||
新增
|
新增
|
||||||
</el-button>
|
</el-button>
|
||||||
<el-button size="mini" type="danger" icon="el-icon-delete" @click="batchDelete">
|
<el-button size="mini" type="danger" icon="el-icon-delete" @click="batchDelete">
|
||||||
删除
|
删除
|
||||||
</el-button>
|
</el-button>
|
||||||
</div>
|
</div>
|
||||||
<div class="custom-pagination">
|
<div class="custom-pagination">
|
||||||
|
|
||||||
<el-select v-model="pageSize" @change="handlePageSizeChange" class="page-size-select">
|
<el-select v-model="pageSize" @change="handlePageSizeChange" class="page-size-select">
|
||||||
<el-option
|
<el-option v-for="item in pageSizeOptions" :key="item" :label="`${item}条/页`" :value="item">
|
||||||
v-for="item in pageSizeOptions"
|
|
||||||
:key="item"
|
|
||||||
:label="`${item}条/页`"
|
|
||||||
:value="item">
|
|
||||||
</el-option>
|
</el-option>
|
||||||
</el-select>
|
</el-select>
|
||||||
|
|
||||||
<button class="pagination-btn" :disabled="currentPage === 1" @click="goFirst">首页</button>
|
<button class="pagination-btn" :disabled="currentPage === 1" @click="goFirst">首页</button>
|
||||||
<button class="pagination-btn" :disabled="currentPage === 1" @click="goPrev">上一页</button>
|
<button class="pagination-btn" :disabled="currentPage === 1" @click="goPrev">上一页</button>
|
||||||
|
|
||||||
<button v-for="page in visiblePages" :key="page" class="pagination-btn"
|
<button v-for="page in visiblePages" :key="page" class="pagination-btn"
|
||||||
:class="{ active: page === currentPage }" @click="goToPage(page)">
|
:class="{ active: page === currentPage }" @click="goToPage(page)">
|
||||||
{{ page }}
|
{{ page }}
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<button class="pagination-btn" :disabled="currentPage === pageCount" @click="goNext">下一页</button>
|
<button class="pagination-btn" :disabled="currentPage === pageCount" @click="goNext">下一页</button>
|
||||||
<span class="total-text">共{{ total }}条记录</span>
|
<span class="total-text">共{{ total }}条记录</span>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
</el-card>
|
</el-card>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -157,7 +163,8 @@ export default {
|
|||||||
pageSize: 10,
|
pageSize: 10,
|
||||||
total: 0,
|
total: 0,
|
||||||
selectedModels: [],
|
selectedModels: [],
|
||||||
isAllSelected: false
|
isAllSelected: false,
|
||||||
|
loading: false
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -301,9 +308,10 @@ export default {
|
|||||||
this.currentPage = page;
|
this.currentPage = page;
|
||||||
this.$refs.modelTable.clearSelection();
|
this.$refs.modelTable.clearSelection();
|
||||||
},
|
},
|
||||||
handleModelSave({ provideCode, formData }) {
|
handleModelSave({ provideCode, formData, done }) {
|
||||||
const modelType = this.activeTab;
|
const modelType = this.activeTab;
|
||||||
const id = formData.id;
|
const id = formData.id;
|
||||||
|
|
||||||
Api.model.updateModel(
|
Api.model.updateModel(
|
||||||
{ modelType, provideCode, id, formData },
|
{ modelType, provideCode, id, formData },
|
||||||
({ data }) => {
|
({ data }) => {
|
||||||
@@ -314,6 +322,7 @@ export default {
|
|||||||
} else {
|
} else {
|
||||||
this.$message.error(data.msg || '保存失败');
|
this.$message.error(data.msg || '保存失败');
|
||||||
}
|
}
|
||||||
|
done && done(); // 调用done回调关闭加载状态
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
@@ -385,6 +394,7 @@ export default {
|
|||||||
|
|
||||||
// 获取模型配置列表
|
// 获取模型配置列表
|
||||||
loadData() {
|
loadData() {
|
||||||
|
this.loading = true; // 开始加载
|
||||||
const params = {
|
const params = {
|
||||||
modelType: this.activeTab,
|
modelType: this.activeTab,
|
||||||
modelName: this.search,
|
modelName: this.search,
|
||||||
@@ -393,6 +403,7 @@ export default {
|
|||||||
};
|
};
|
||||||
|
|
||||||
Api.model.getModelList(params, ({ data }) => {
|
Api.model.getModelList(params, ({ data }) => {
|
||||||
|
this.loading = false; // 结束加载
|
||||||
if (data.code === 0) {
|
if (data.code === 0) {
|
||||||
this.modelList = data.data.list;
|
this.modelList = data.data.list;
|
||||||
this.total = data.data.total;
|
this.total = data.data.total;
|
||||||
@@ -592,12 +603,12 @@ export default {
|
|||||||
transition: border-color 0.2s;
|
transition: border-color 0.2s;
|
||||||
}
|
}
|
||||||
|
|
||||||
::v-deep .page-size-select{
|
::v-deep .page-size-select {
|
||||||
width: 100px;
|
width: 100px;
|
||||||
margin-right: 8px;
|
margin-right: 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
::v-deep .page-size-select .el-input__inner{
|
::v-deep .page-size-select .el-input__inner {
|
||||||
height: 32px;
|
height: 32px;
|
||||||
line-height: 32px;
|
line-height: 32px;
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
@@ -606,7 +617,8 @@ export default {
|
|||||||
color: #606266;
|
color: #606266;
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
}
|
}
|
||||||
::v-deep .page-size-select .el-input__suffix{
|
|
||||||
|
::v-deep .page-size-select .el-input__suffix {
|
||||||
right: 6px;
|
right: 6px;
|
||||||
width: 15px;
|
width: 15px;
|
||||||
height: 20px;
|
height: 20px;
|
||||||
@@ -617,13 +629,14 @@ export default {
|
|||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
}
|
}
|
||||||
|
|
||||||
::v-deep .page-size-select .el-input__suffix-inner{
|
::v-deep .page-size-select .el-input__suffix-inner {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
::v-deep .page-size-select .el-icon-arrow-up:before{
|
|
||||||
|
::v-deep .page-size-select .el-icon-arrow-up:before {
|
||||||
content: "";
|
content: "";
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
border-left: 6px solid transparent;
|
border-left: 6px solid transparent;
|
||||||
@@ -884,7 +897,7 @@ export default {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.model-card{
|
.model-card {
|
||||||
background: white;
|
background: white;
|
||||||
flex: 1;
|
flex: 1;
|
||||||
display: flex;
|
display: flex;
|
||||||
@@ -894,7 +907,7 @@ export default {
|
|||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
.model-card ::v-deep .el-card__body{
|
.model-card ::v-deep .el-card__body {
|
||||||
padding: 0;
|
padding: 0;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
@@ -912,4 +925,21 @@ export default {
|
|||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
::v-deep .el-loading-mask {
|
||||||
|
background-color: rgba(255, 255, 255, 0.6) !important;
|
||||||
|
backdrop-filter: blur(2px);
|
||||||
|
}
|
||||||
|
::v-deep .el-loading-spinner .circular {
|
||||||
|
width: 28px;
|
||||||
|
height: 28px;
|
||||||
|
}
|
||||||
|
::v-deep .el-loading-spinner .path {
|
||||||
|
stroke: #6b8cff;
|
||||||
|
}
|
||||||
|
::v-deep .el-loading-text {
|
||||||
|
color: #6b8cff !important;
|
||||||
|
font-size: 14px;
|
||||||
|
margin-top: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -0,0 +1,643 @@
|
|||||||
|
<template>
|
||||||
|
<div class="welcome">
|
||||||
|
<HeaderBar />
|
||||||
|
|
||||||
|
<div class="operation-bar">
|
||||||
|
<h2 class="page-title">固件管理</h2>
|
||||||
|
<div class="right-operations">
|
||||||
|
<el-input placeholder="请输入固件名称查询" v-model="searchName" class="search-input"
|
||||||
|
@keyup.enter.native="handleSearch" clearable />
|
||||||
|
<el-button class="btn-search" @click="handleSearch">搜索</el-button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="main-wrapper">
|
||||||
|
<div class="content-panel">
|
||||||
|
<div class="content-area">
|
||||||
|
<el-card class="params-card" shadow="never">
|
||||||
|
<el-table ref="paramsTable" :data="paramsList" class="transparent-table"
|
||||||
|
:header-cell-class-name="headerCellClassName">
|
||||||
|
<el-table-column label="选择" align="center" width="120">
|
||||||
|
<template slot-scope="scope">
|
||||||
|
<el-checkbox v-model="scope.row.selected"></el-checkbox>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column label="固件名称" prop="firmwareName" align="center"></el-table-column>
|
||||||
|
<el-table-column label="固件类型" prop="type" align="center">
|
||||||
|
<template slot-scope="scope">
|
||||||
|
{{ getFirmwareTypeName(scope.row.type) }}
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column label="版本号" prop="version" align="center"></el-table-column>
|
||||||
|
<el-table-column label="文件大小" prop="size" align="center">
|
||||||
|
<template slot-scope="scope">
|
||||||
|
{{ formatFileSize(scope.row.size) }}
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column label="备注" prop="remark" align="center"
|
||||||
|
show-overflow-tooltip></el-table-column>
|
||||||
|
<el-table-column label="创建时间" prop="createDate" align="center">
|
||||||
|
<template slot-scope="scope">
|
||||||
|
{{ formatDate(scope.row.createDate) }}
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column label="操作" align="center">
|
||||||
|
<template slot-scope="scope">
|
||||||
|
<el-button size="mini" type="text"
|
||||||
|
@click="downloadFirmware(scope.row)">下载</el-button>
|
||||||
|
<el-button size="mini" type="text" @click="editParam(scope.row)">编辑</el-button>
|
||||||
|
<el-button size="mini" type="text" @click="deleteParam(scope.row)">删除</el-button>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
</el-table>
|
||||||
|
|
||||||
|
<div class="table_bottom">
|
||||||
|
<div class="ctrl_btn">
|
||||||
|
<el-button size="mini" type="primary" class="select-all-btn" @click="handleSelectAll">
|
||||||
|
{{ isAllSelected ? '取消全选' : '全选' }}
|
||||||
|
</el-button>
|
||||||
|
<el-button size="mini" type="success" @click="showAddDialog"
|
||||||
|
style="background: #5bc98c;border: None;">新增</el-button>
|
||||||
|
<el-button size="mini" type="danger" icon="el-icon-delete"
|
||||||
|
@click="deleteSelectedParams">删除</el-button>
|
||||||
|
</div>
|
||||||
|
<div class="custom-pagination">
|
||||||
|
<el-select v-model="pageSize" @change="handlePageSizeChange" class="page-size-select">
|
||||||
|
<el-option v-for="item in pageSizeOptions" :key="item" :label="`${item}条/页`"
|
||||||
|
:value="item">
|
||||||
|
</el-option>
|
||||||
|
</el-select>
|
||||||
|
<button class="pagination-btn" :disabled="currentPage === 1" @click="goFirst">
|
||||||
|
首页
|
||||||
|
</button>
|
||||||
|
<button class="pagination-btn" :disabled="currentPage === 1" @click="goPrev">
|
||||||
|
上一页
|
||||||
|
</button>
|
||||||
|
<button v-for="page in visiblePages" :key="page" class="pagination-btn"
|
||||||
|
:class="{ active: page === currentPage }" @click="goToPage(page)">
|
||||||
|
{{ page }}
|
||||||
|
</button>
|
||||||
|
<button class="pagination-btn" :disabled="currentPage === pageCount" @click="goNext">
|
||||||
|
下一页
|
||||||
|
</button>
|
||||||
|
<span class="total-text">共{{ total }}条记录</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</el-card>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 新增/编辑固件对话框 -->
|
||||||
|
<firmware-dialog :title="dialogTitle" :visible.sync="dialogVisible" :form="firmwareForm" @submit="handleSubmit"
|
||||||
|
@cancel="dialogVisible = false" />
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import Api from "@/apis/api";
|
||||||
|
import FirmwareDialog from "@/components/FirmwareDialog.vue";
|
||||||
|
import HeaderBar from "@/components/HeaderBar.vue";
|
||||||
|
import { FIRMWARE_TYPES } from "@/utils";
|
||||||
|
import { formatDate, formatFileSize } from "@/utils/format";
|
||||||
|
|
||||||
|
export default {
|
||||||
|
components: { HeaderBar, FirmwareDialog },
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
searchName: "",
|
||||||
|
paramsList: [],
|
||||||
|
firmwareList: [],
|
||||||
|
currentPage: 1,
|
||||||
|
pageSize: 10,
|
||||||
|
pageSizeOptions: [10, 20, 50, 100],
|
||||||
|
total: 0,
|
||||||
|
dialogVisible: false,
|
||||||
|
dialogTitle: "新增固件",
|
||||||
|
isAllSelected: false,
|
||||||
|
firmwareForm: {
|
||||||
|
id: null,
|
||||||
|
firmwareName: "",
|
||||||
|
type: "",
|
||||||
|
version: "",
|
||||||
|
size: 0,
|
||||||
|
remark: "",
|
||||||
|
firmwarePath: ""
|
||||||
|
},
|
||||||
|
};
|
||||||
|
},
|
||||||
|
created() {
|
||||||
|
this.fetchFirmwareList();
|
||||||
|
},
|
||||||
|
|
||||||
|
computed: {
|
||||||
|
pageCount() {
|
||||||
|
return Math.ceil(this.total / this.pageSize);
|
||||||
|
},
|
||||||
|
visiblePages() {
|
||||||
|
const pages = [];
|
||||||
|
const maxVisible = 3;
|
||||||
|
let start = Math.max(1, this.currentPage - 1);
|
||||||
|
let end = Math.min(this.pageCount, start + maxVisible - 1);
|
||||||
|
|
||||||
|
if (end - start + 1 < maxVisible) {
|
||||||
|
start = Math.max(1, end - maxVisible + 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (let i = start; i <= end; i++) {
|
||||||
|
pages.push(i);
|
||||||
|
}
|
||||||
|
return pages;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
handlePageSizeChange(val) {
|
||||||
|
this.pageSize = val;
|
||||||
|
this.currentPage = 1;
|
||||||
|
this.fetchFirmwareList();
|
||||||
|
},
|
||||||
|
fetchFirmwareList() {
|
||||||
|
const params = {
|
||||||
|
pageNum: this.currentPage,
|
||||||
|
pageSize: this.pageSize,
|
||||||
|
firmwareName: this.searchName || "",
|
||||||
|
orderField: "create_date",
|
||||||
|
order: "desc"
|
||||||
|
};
|
||||||
|
Api.ota.getOtaList(params, (res) => {
|
||||||
|
res = res.data
|
||||||
|
if (res.code === 0) {
|
||||||
|
this.firmwareList = res.data.list.map(item => ({
|
||||||
|
...item,
|
||||||
|
selected: false
|
||||||
|
}));
|
||||||
|
this.paramsList = this.firmwareList;
|
||||||
|
this.total = res.data.total || 0;
|
||||||
|
} else {
|
||||||
|
this.firmwareList = [];
|
||||||
|
this.paramsList = [];
|
||||||
|
this.total = 0;
|
||||||
|
this.$message.error({
|
||||||
|
message: res?.data?.msg || '获取固件列表失败',
|
||||||
|
showClose: true
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
handleSearch() {
|
||||||
|
this.currentPage = 1;
|
||||||
|
this.fetchFirmwareList();
|
||||||
|
},
|
||||||
|
handleSelectAll() {
|
||||||
|
this.isAllSelected = !this.isAllSelected;
|
||||||
|
this.firmwareList.forEach(row => {
|
||||||
|
row.selected = this.isAllSelected;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
showAddDialog() {
|
||||||
|
this.dialogTitle = "新增固件";
|
||||||
|
// 完全重置表单数据
|
||||||
|
this.firmwareForm = {
|
||||||
|
id: null,
|
||||||
|
firmwareName: "",
|
||||||
|
type: "",
|
||||||
|
version: "",
|
||||||
|
size: 0,
|
||||||
|
remark: "",
|
||||||
|
firmwarePath: ""
|
||||||
|
};
|
||||||
|
this.$nextTick(() => {
|
||||||
|
// 重置表单的校验状态
|
||||||
|
if (this.$refs.firmwareDialog && this.$refs.firmwareDialog.$refs.form) {
|
||||||
|
this.$refs.firmwareDialog.$refs.form.clearValidate();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
this.dialogVisible = true;
|
||||||
|
},
|
||||||
|
editParam(row) {
|
||||||
|
this.dialogTitle = "编辑固件";
|
||||||
|
this.firmwareForm = { ...row };
|
||||||
|
this.dialogVisible = true;
|
||||||
|
},
|
||||||
|
handleSubmit(form) {
|
||||||
|
if (form.id) {
|
||||||
|
// 编辑
|
||||||
|
Api.ota.updateOta(form.id, form, (res) => {
|
||||||
|
res = res.data;
|
||||||
|
if (res.code === 0) {
|
||||||
|
this.$message.success({
|
||||||
|
message: "修改成功",
|
||||||
|
showClose: true
|
||||||
|
});
|
||||||
|
this.dialogVisible = false;
|
||||||
|
this.fetchFirmwareList();
|
||||||
|
} else {
|
||||||
|
this.$message.error({
|
||||||
|
message: res.msg || "修改失败",
|
||||||
|
showClose: true
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
// 新增
|
||||||
|
Api.ota.saveOta(form, (res) => {
|
||||||
|
res = res.data;
|
||||||
|
if (res.code === 0) {
|
||||||
|
this.$message.success({
|
||||||
|
message: "新增成功",
|
||||||
|
showClose: true
|
||||||
|
});
|
||||||
|
this.dialogVisible = false;
|
||||||
|
this.fetchFirmwareList();
|
||||||
|
} else {
|
||||||
|
this.$message.error({
|
||||||
|
message: res.msg || "新增失败",
|
||||||
|
showClose: true
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
deleteSelectedParams() {
|
||||||
|
const selectedRows = this.firmwareList.filter(row => row.selected);
|
||||||
|
if (selectedRows.length === 0) {
|
||||||
|
this.$message.warning({
|
||||||
|
message: "请先选择需要删除的固件",
|
||||||
|
showClose: true
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.deleteParam(selectedRows);
|
||||||
|
},
|
||||||
|
deleteParam(row) {
|
||||||
|
// 处理单个参数或参数数组
|
||||||
|
const params = Array.isArray(row) ? row : [row];
|
||||||
|
|
||||||
|
if (Array.isArray(row) && row.length === 0) {
|
||||||
|
this.$message.warning({
|
||||||
|
message: "请先选择需要删除的参数",
|
||||||
|
showClose: true
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const paramCount = params.length;
|
||||||
|
this.$confirm(`确定要删除选中的${paramCount}个固件吗?`, '警告', {
|
||||||
|
confirmButtonText: '确定',
|
||||||
|
cancelButtonText: '取消',
|
||||||
|
type: 'warning',
|
||||||
|
distinguishCancelAndClose: true
|
||||||
|
}).then(() => {
|
||||||
|
const ids = params.map(param => param.id);
|
||||||
|
if (ids.some(id => !id)) {
|
||||||
|
this.$message.error({
|
||||||
|
message: '存在无效的参数ID',
|
||||||
|
showClose: true
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Api.ota.deleteOta(ids, (res) => {
|
||||||
|
res = res.data;
|
||||||
|
if (res.code === 0) {
|
||||||
|
this.$message.success({
|
||||||
|
message: `成功删除${paramCount}个固件`,
|
||||||
|
showClose: true
|
||||||
|
});
|
||||||
|
this.fetchFirmwareList();
|
||||||
|
} else {
|
||||||
|
this.$message.error({
|
||||||
|
message: res.msg || '删除失败,请重试',
|
||||||
|
showClose: true
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}).catch(action => {
|
||||||
|
if (action === 'cancel') {
|
||||||
|
this.$message({
|
||||||
|
type: 'info',
|
||||||
|
message: '已取消删除操作',
|
||||||
|
duration: 1000
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
this.$message({
|
||||||
|
type: 'info',
|
||||||
|
message: '操作已关闭',
|
||||||
|
duration: 1000
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
headerCellClassName({ columnIndex }) {
|
||||||
|
if (columnIndex === 0) {
|
||||||
|
return "custom-selection-header";
|
||||||
|
}
|
||||||
|
return "";
|
||||||
|
},
|
||||||
|
goFirst() {
|
||||||
|
this.currentPage = 1;
|
||||||
|
this.fetchFirmwareList();
|
||||||
|
},
|
||||||
|
goPrev() {
|
||||||
|
if (this.currentPage > 1) {
|
||||||
|
this.currentPage--;
|
||||||
|
this.fetchFirmwareList();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
goNext() {
|
||||||
|
if (this.currentPage < this.pageCount) {
|
||||||
|
this.currentPage++;
|
||||||
|
this.fetchFirmwareList();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
goToPage(page) {
|
||||||
|
this.currentPage = page;
|
||||||
|
this.fetchFirmwareList();
|
||||||
|
},
|
||||||
|
downloadFirmware(firmware) {
|
||||||
|
if (!firmware || !firmware.id) {
|
||||||
|
this.$message.error('固件信息不完整');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// 先获取下载链接
|
||||||
|
Api.ota.getDownloadUrl(firmware.id, (res) => {
|
||||||
|
if (res.data.code === 0) {
|
||||||
|
const uuid = res.data.data;
|
||||||
|
const baseUrl = process.env.VUE_APP_API_BASE_URL || '';
|
||||||
|
window.open(`${window.location.origin}${baseUrl}/otaMag/download/${uuid}`);
|
||||||
|
} else {
|
||||||
|
this.$message.error('获取下载链接失败');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
formatDate,
|
||||||
|
formatFileSize,
|
||||||
|
getFirmwareTypeName(type) {
|
||||||
|
const firmwareType = FIRMWARE_TYPES.find(item => item.key === type);
|
||||||
|
return firmwareType ? firmwareType.name : type;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.welcome {
|
||||||
|
min-width: 900px;
|
||||||
|
min-height: 506px;
|
||||||
|
height: 100vh;
|
||||||
|
display: flex;
|
||||||
|
position: relative;
|
||||||
|
flex-direction: column;
|
||||||
|
background-size: cover;
|
||||||
|
background: linear-gradient(to bottom right, #dce8ff, #e4eeff, #e6cbfd) center;
|
||||||
|
-webkit-background-size: cover;
|
||||||
|
-o-background-size: cover;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.main-wrapper {
|
||||||
|
margin: 5px 22px;
|
||||||
|
border-radius: 15px;
|
||||||
|
min-height: calc(100vh - 24vh);
|
||||||
|
height: auto;
|
||||||
|
max-height: 80vh;
|
||||||
|
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.1);
|
||||||
|
position: relative;
|
||||||
|
background: rgba(237, 242, 255, 0.5);
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.operation-bar {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
padding: 16px 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.page-title {
|
||||||
|
font-size: 24px;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.right-operations {
|
||||||
|
display: flex;
|
||||||
|
gap: 10px;
|
||||||
|
margin-left: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-input {
|
||||||
|
width: 240px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-search {
|
||||||
|
background: linear-gradient(135deg, #6b8cff, #a966ff);
|
||||||
|
border: none;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.content-panel {
|
||||||
|
flex: 1;
|
||||||
|
display: flex;
|
||||||
|
overflow: hidden;
|
||||||
|
height: 100%;
|
||||||
|
border-radius: 15px;
|
||||||
|
background: transparent;
|
||||||
|
border: 1px solid #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.content-area {
|
||||||
|
flex: 1;
|
||||||
|
height: 100%;
|
||||||
|
min-width: 600px;
|
||||||
|
overflow: auto;
|
||||||
|
background-color: white;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.params-card {
|
||||||
|
background: white;
|
||||||
|
border: none;
|
||||||
|
box-shadow: none;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
flex: 1;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.table_bottom {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
margin-top: 10px;
|
||||||
|
padding-bottom: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ctrl_btn {
|
||||||
|
display: flex;
|
||||||
|
gap: 8px;
|
||||||
|
padding-left: 26px;
|
||||||
|
|
||||||
|
.el-button {
|
||||||
|
min-width: 72px;
|
||||||
|
height: 32px;
|
||||||
|
padding: 7px 12px 7px 10px;
|
||||||
|
font-size: 12px;
|
||||||
|
border-radius: 4px;
|
||||||
|
line-height: 1;
|
||||||
|
font-weight: 500;
|
||||||
|
border: none;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
box-shadow: 0 2px 6px rgba(0, 0, 0, 0.1);
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
transform: translateY(-1px);
|
||||||
|
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.15);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.el-button--primary {
|
||||||
|
background: #5f70f3;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.el-button--danger {
|
||||||
|
background: #fd5b63;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.custom-pagination {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 10px;
|
||||||
|
|
||||||
|
.el-select {
|
||||||
|
margin-right: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pagination-btn:first-child,
|
||||||
|
.pagination-btn:nth-child(2),
|
||||||
|
.pagination-btn:nth-last-child(2),
|
||||||
|
.pagination-btn:nth-child(3) {
|
||||||
|
min-width: 60px;
|
||||||
|
height: 32px;
|
||||||
|
padding: 0 12px;
|
||||||
|
border-radius: 4px;
|
||||||
|
border: 1px solid #e4e7ed;
|
||||||
|
background: #dee7ff;
|
||||||
|
color: #606266;
|
||||||
|
font-size: 14px;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background: #d7dce6;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:disabled {
|
||||||
|
opacity: 0.6;
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.pagination-btn:not(:first-child):not(:nth-child(3)):not(:nth-child(2)):not(:nth-last-child(2)) {
|
||||||
|
min-width: 28px;
|
||||||
|
height: 32px;
|
||||||
|
padding: 0;
|
||||||
|
border-radius: 4px;
|
||||||
|
border: 1px solid transparent;
|
||||||
|
background: transparent;
|
||||||
|
color: #606266;
|
||||||
|
font-size: 14px;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background: rgba(245, 247, 250, 0.3);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.pagination-btn.active {
|
||||||
|
background: #5f70f3 !important;
|
||||||
|
color: #ffffff !important;
|
||||||
|
border-color: #5f70f3 !important;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background: #6d7cf5 !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.total-text {
|
||||||
|
margin-left: 10px;
|
||||||
|
color: #606266;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.page-size-select {
|
||||||
|
width: 100px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.custom-selection-header {
|
||||||
|
text-align: center !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.transparent-table) {
|
||||||
|
background-color: transparent;
|
||||||
|
|
||||||
|
.el-table__header-wrapper {
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.el-table__header th {
|
||||||
|
background-color: white !important;
|
||||||
|
color: black;
|
||||||
|
font-weight: 600;
|
||||||
|
height: 40px;
|
||||||
|
padding: 8px 0;
|
||||||
|
font-size: 14px;
|
||||||
|
border-bottom: 1px solid #e4e7ed;
|
||||||
|
}
|
||||||
|
|
||||||
|
.el-table__body-wrapper {
|
||||||
|
td {
|
||||||
|
background-color: transparent;
|
||||||
|
border-bottom: 1px solid #e4e7ed;
|
||||||
|
padding: 8px 0;
|
||||||
|
height: 40px;
|
||||||
|
color: #606266;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.el-table__row:hover>td {
|
||||||
|
background-color: #f5f7fa !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.el-table .el-button--text) {
|
||||||
|
color: #7079aa !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.el-table .el-button--text:hover) {
|
||||||
|
color: #5a64b5 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.el-checkbox__inner) {
|
||||||
|
background-color: #eeeeee !important;
|
||||||
|
border-color: #cccccc !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.el-checkbox__inner:hover) {
|
||||||
|
border-color: #cccccc !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.el-checkbox__input.is-checked .el-checkbox__inner) {
|
||||||
|
background-color: #5f70f3 !important;
|
||||||
|
border-color: #5f70f3 !important;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -15,8 +15,15 @@
|
|||||||
<div class="content-panel">
|
<div class="content-panel">
|
||||||
<div class="content-area">
|
<div class="content-area">
|
||||||
<el-card class="params-card" shadow="never">
|
<el-card class="params-card" shadow="never">
|
||||||
<el-table ref="paramsTable" :data="paramsList" class="transparent-table"
|
<el-table
|
||||||
:header-cell-class-name="headerCellClassName">
|
ref="paramsTable"
|
||||||
|
:data="paramsList"
|
||||||
|
class="transparent-table"
|
||||||
|
v-loading="loading"
|
||||||
|
element-loading-text="拼命加载中"
|
||||||
|
element-loading-spinner="el-icon-loading"
|
||||||
|
element-loading-background="rgba(255, 255, 255, 0.7)"
|
||||||
|
:header-cell-class-name="headerCellClassName">
|
||||||
<el-table-column label="选择" align="center" width="120">
|
<el-table-column label="选择" align="center" width="120">
|
||||||
<template slot-scope="scope">
|
<template slot-scope="scope">
|
||||||
<el-checkbox v-model="scope.row.selected"></el-checkbox>
|
<el-checkbox v-model="scope.row.selected"></el-checkbox>
|
||||||
@@ -92,6 +99,7 @@ export default {
|
|||||||
searchCode: "",
|
searchCode: "",
|
||||||
paramsList: [],
|
paramsList: [],
|
||||||
currentPage: 1,
|
currentPage: 1,
|
||||||
|
loading: false,
|
||||||
pageSize: 10,
|
pageSize: 10,
|
||||||
pageSizeOptions: [10, 20, 50, 100],
|
pageSizeOptions: [10, 20, 50, 100],
|
||||||
total: 0,
|
total: 0,
|
||||||
@@ -138,27 +146,29 @@ export default {
|
|||||||
this.fetchParams();
|
this.fetchParams();
|
||||||
},
|
},
|
||||||
fetchParams() {
|
fetchParams() {
|
||||||
Api.admin.getParamsList(
|
this.loading = true;
|
||||||
{
|
Api.admin.getParamsList(
|
||||||
page: this.currentPage,
|
{
|
||||||
limit: this.pageSize,
|
page: this.currentPage,
|
||||||
paramCode: this.searchCode,
|
limit: this.pageSize,
|
||||||
},
|
paramCode: this.searchCode,
|
||||||
({ data }) => {
|
},
|
||||||
if (data.code === 0) {
|
({ data }) => {
|
||||||
this.paramsList = data.data.list.map(item => ({
|
this.loading = false;
|
||||||
...item,
|
if (data.code === 0) {
|
||||||
selected: false
|
this.paramsList = data.data.list.map(item => ({
|
||||||
}));
|
...item,
|
||||||
this.total = data.data.total;
|
selected: false
|
||||||
} else {
|
}));
|
||||||
this.$message.error({
|
this.total = data.data.total;
|
||||||
message: data.msg || '获取参数列表失败',
|
} else {
|
||||||
showClose: true
|
this.$message.error({
|
||||||
});
|
message: data.msg || '获取参数列表失败',
|
||||||
}
|
showClose: true
|
||||||
}
|
});
|
||||||
);
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
},
|
},
|
||||||
handleSearch() {
|
handleSearch() {
|
||||||
this.currentPage = 1;
|
this.currentPage = 1;
|
||||||
@@ -185,32 +195,35 @@ export default {
|
|||||||
this.paramForm = { ...row };
|
this.paramForm = { ...row };
|
||||||
this.dialogVisible = true;
|
this.dialogVisible = true;
|
||||||
},
|
},
|
||||||
handleSubmit(form) {
|
|
||||||
if (form.id) {
|
handleSubmit({ form, done }) {
|
||||||
// 编辑
|
if (form.id) {
|
||||||
Api.admin.updateParam(form, ({ data }) => {
|
// 编辑
|
||||||
if (data.code === 0) {
|
Api.admin.updateParam(form, ({ data }) => {
|
||||||
this.$message.success({
|
if (data.code === 0) {
|
||||||
message:"修改成功",
|
this.$message.success({
|
||||||
showClose:true
|
message:"修改成功",
|
||||||
});
|
showClose:true
|
||||||
this.dialogVisible = false;
|
|
||||||
this.fetchParams();
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
} else {
|
this.dialogVisible = false;
|
||||||
// 新增
|
this.fetchParams();
|
||||||
Api.admin.addParam(form, ({ data }) => {
|
}
|
||||||
if (data.code === 0) {
|
done && done();
|
||||||
this.$message.success({
|
});
|
||||||
message:"新增成功",
|
} else {
|
||||||
showClose:true
|
// 新增
|
||||||
});
|
Api.admin.addParam(form, ({ data }) => {
|
||||||
this.dialogVisible = false;
|
if (data.code === 0) {
|
||||||
this.fetchParams();
|
this.$message.success({
|
||||||
}
|
message:"新增成功",
|
||||||
|
showClose:true
|
||||||
});
|
});
|
||||||
}
|
this.dialogVisible = false;
|
||||||
|
this.fetchParams();
|
||||||
|
}
|
||||||
|
done && done();
|
||||||
|
});
|
||||||
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
deleteSelectedParams() {
|
deleteSelectedParams() {
|
||||||
@@ -389,13 +402,21 @@ export default {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.params-card {
|
.params-card {
|
||||||
background: white;
|
background: white;
|
||||||
border: none;
|
flex: 1;
|
||||||
box-shadow: none;
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
border: none;
|
||||||
|
box-shadow: none;
|
||||||
|
overflow: hidden;
|
||||||
|
|
||||||
|
::v-deep .el-card__body {
|
||||||
|
padding: 15px;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
flex: 1;
|
flex: 1;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.table_bottom {
|
.table_bottom {
|
||||||
@@ -656,4 +677,25 @@ export default {
|
|||||||
max-height: calc(var(--table-max-height) - 40px);
|
max-height: calc(var(--table-max-height) - 40px);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
:deep(.el-loading-mask) {
|
||||||
|
background-color: rgba(255, 255, 255, 0.6) !important;
|
||||||
|
backdrop-filter: blur(2px);
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.el-loading-spinner .circular) {
|
||||||
|
width: 28px;
|
||||||
|
height: 28px;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.el-loading-spinner .path) {
|
||||||
|
stroke: #6b8cff;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.el-loading-text) {
|
||||||
|
color: #6b8cff !important;
|
||||||
|
font-size: 14px;
|
||||||
|
margin-top: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -15,7 +15,14 @@
|
|||||||
<div class="content-panel">
|
<div class="content-panel">
|
||||||
<div class="content-area">
|
<div class="content-area">
|
||||||
<el-card class="user-card" shadow="never">
|
<el-card class="user-card" shadow="never">
|
||||||
<el-table ref="userTable" :data="userList" class="transparent-table">
|
<el-table
|
||||||
|
ref="userTable"
|
||||||
|
:data="userList"
|
||||||
|
class="transparent-table"
|
||||||
|
v-loading="loading"
|
||||||
|
element-loading-text="拼命加载中"
|
||||||
|
element-loading-spinner="el-icon-loading"
|
||||||
|
element-loading-background="rgba(255, 255, 255, 0.7)">
|
||||||
<el-table-column label="选择" align="center" width="120">
|
<el-table-column label="选择" align="center" width="120">
|
||||||
<template slot-scope="scope">
|
<template slot-scope="scope">
|
||||||
<el-checkbox v-model="scope.row.selected"></el-checkbox>
|
<el-checkbox v-model="scope.row.selected"></el-checkbox>
|
||||||
@@ -103,7 +110,8 @@ export default {
|
|||||||
currentPage: 1,
|
currentPage: 1,
|
||||||
pageSize: 10,
|
pageSize: 10,
|
||||||
total: 0,
|
total: 0,
|
||||||
isAllSelected: false
|
isAllSelected: false,
|
||||||
|
loading: false,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
created() {
|
created() {
|
||||||
@@ -137,22 +145,24 @@ export default {
|
|||||||
},
|
},
|
||||||
|
|
||||||
fetchUsers() {
|
fetchUsers() {
|
||||||
Api.admin.getUserList(
|
this.loading = true;
|
||||||
{
|
Api.admin.getUserList(
|
||||||
page: this.currentPage,
|
{
|
||||||
limit: this.pageSize,
|
page: this.currentPage,
|
||||||
mobile: this.searchPhone,
|
limit: this.pageSize,
|
||||||
},
|
mobile: this.searchPhone,
|
||||||
({ data }) => {
|
},
|
||||||
if (data.code === 0) {
|
({ data }) => {
|
||||||
this.userList = data.data.list.map(item => ({
|
this.loading = false; // 结束加载
|
||||||
...item,
|
if (data.code === 0) {
|
||||||
selected: false
|
this.userList = data.data.list.map(item => ({
|
||||||
}));
|
...item,
|
||||||
this.total = data.data.total;
|
selected: false
|
||||||
}
|
}));
|
||||||
}
|
this.total = data.data.total;
|
||||||
);
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
},
|
},
|
||||||
handleSearch() {
|
handleSearch() {
|
||||||
this.currentPage = 1;
|
this.currentPage = 1;
|
||||||
@@ -415,12 +425,20 @@ export default {
|
|||||||
|
|
||||||
.user-card {
|
.user-card {
|
||||||
background: white;
|
background: white;
|
||||||
border: none;
|
flex: 1;
|
||||||
box-shadow: none;
|
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
flex: 1;
|
border: none;
|
||||||
|
box-shadow: none;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
|
|
||||||
|
::v-deep .el-card__body {
|
||||||
|
padding: 15px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
flex: 1;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.table_bottom {
|
.table_bottom {
|
||||||
@@ -682,5 +700,21 @@ export default {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
:deep(.el-loading-mask) {
|
||||||
|
background-color: rgba(255, 255, 255, 0.6) !important;
|
||||||
|
backdrop-filter: blur(2px);
|
||||||
|
}
|
||||||
|
:deep(.el-loading-spinner .circular) {
|
||||||
|
width: 28px;
|
||||||
|
height: 28px;
|
||||||
|
}
|
||||||
|
:deep(.el-loading-spinner .path) {
|
||||||
|
stroke: #6b8cff;
|
||||||
|
}
|
||||||
|
:deep(.el-loading-text) {
|
||||||
|
color: #6b8cff !important;
|
||||||
|
font-size: 14px;
|
||||||
|
margin-top: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -31,8 +31,30 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="device-list-container">
|
<div class="device-list-container">
|
||||||
<DeviceItem v-for="(item, index) in devices" :key="index" :device="item" @configure="goToRoleConfig"
|
<template v-if="isLoading">
|
||||||
@deviceManage="handleDeviceManage" @delete="handleDeleteAgent" />
|
<div
|
||||||
|
v-for="i in skeletonCount"
|
||||||
|
:key="'skeleton-'+i"
|
||||||
|
class="skeleton-item"
|
||||||
|
>
|
||||||
|
<div class="skeleton-image"></div>
|
||||||
|
<div class="skeleton-content">
|
||||||
|
<div class="skeleton-line"></div>
|
||||||
|
<div class="skeleton-line-short"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template v-else>
|
||||||
|
<DeviceItem
|
||||||
|
v-for="(item, index) in devices"
|
||||||
|
:key="index"
|
||||||
|
:device="item"
|
||||||
|
@configure="goToRoleConfig"
|
||||||
|
@deviceManage="handleDeviceManage"
|
||||||
|
@delete="handleDeleteAgent"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<AddWisdomBodyDialog :visible.sync="addDeviceDialogVisible" @confirm="handleWisdomBodyAdded" />
|
<AddWisdomBodyDialog :visible.sync="addDeviceDialogVisible" @confirm="handleWisdomBodyAdded" />
|
||||||
@@ -60,7 +82,9 @@ export default {
|
|||||||
devices: [],
|
devices: [],
|
||||||
originalDevices: [],
|
originalDevices: [],
|
||||||
isSearching: false,
|
isSearching: false,
|
||||||
searchRegex: null
|
searchRegex: null,
|
||||||
|
isLoading: true,
|
||||||
|
skeletonCount: localStorage.getItem('skeletonCount') || 8,
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -109,12 +133,26 @@ export default {
|
|||||||
},
|
},
|
||||||
// 获取智能体列表
|
// 获取智能体列表
|
||||||
fetchAgentList() {
|
fetchAgentList() {
|
||||||
|
this.isLoading = true;
|
||||||
Api.agent.getAgentList(({ data }) => {
|
Api.agent.getAgentList(({ data }) => {
|
||||||
this.originalDevices = data.data.map(item => ({
|
if (data?.data) {
|
||||||
...item,
|
this.originalDevices = data.data.map(item => ({
|
||||||
agentId: item.id // 字段映射
|
...item,
|
||||||
}));
|
agentId: item.id
|
||||||
this.handleSearchReset(); // 重置搜索状态
|
}));
|
||||||
|
|
||||||
|
// 动态设置骨架屏数量(可选)
|
||||||
|
this.skeletonCount = Math.min(
|
||||||
|
Math.max(this.originalDevices.length, 3), // 最少3个
|
||||||
|
10 // 最多10个
|
||||||
|
);
|
||||||
|
|
||||||
|
this.handleSearchReset();
|
||||||
|
}
|
||||||
|
this.isLoading = false;
|
||||||
|
}, (error) => {
|
||||||
|
console.error('Failed to fetch agent list:', error);
|
||||||
|
this.isLoading = false;
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
// 删除智能体
|
// 删除智能体
|
||||||
@@ -199,7 +237,7 @@ export default {
|
|||||||
|
|
||||||
.hi-hint {
|
.hi-hint {
|
||||||
font-weight: 400;
|
font-weight: 400;
|
||||||
font-size: 10px;
|
font-size: 12px;
|
||||||
text-align: left;
|
text-align: left;
|
||||||
color: #818cae;
|
color: #818cae;
|
||||||
margin-left: 75px;
|
margin-left: 75px;
|
||||||
@@ -220,7 +258,7 @@ export default {
|
|||||||
border-radius: 17px;
|
border-radius: 17px;
|
||||||
background: #5778ff;
|
background: #5778ff;
|
||||||
color: #fff;
|
color: #fff;
|
||||||
font-size: 10px;
|
font-size: 14px;
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
line-height: 34px;
|
line-height: 34px;
|
||||||
@@ -262,6 +300,65 @@ export default {
|
|||||||
/* 居中显示 */
|
/* 居中显示 */
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* 骨架屏动画 */
|
||||||
|
@keyframes shimmer {
|
||||||
|
100% { transform: translateX(100%); }
|
||||||
|
}
|
||||||
|
|
||||||
|
.skeleton-item {
|
||||||
|
background: #fff;
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 20px;
|
||||||
|
height: 120px;
|
||||||
|
position: relative;
|
||||||
|
overflow: hidden;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.skeleton-image {
|
||||||
|
width: 80px;
|
||||||
|
height: 80px;
|
||||||
|
background: #f0f2f5;
|
||||||
|
border-radius: 4px;
|
||||||
|
float: left;
|
||||||
|
position: relative;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.skeleton-content {
|
||||||
|
margin-left: 100px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.skeleton-line {
|
||||||
|
height: 16px;
|
||||||
|
background: #f0f2f5;
|
||||||
|
border-radius: 4px;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
width: 70%;
|
||||||
|
position: relative;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.skeleton-line-short {
|
||||||
|
height: 12px;
|
||||||
|
background: #f0f2f5;
|
||||||
|
border-radius: 4px;
|
||||||
|
width: 50%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.skeleton-item::after {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 50%;
|
||||||
|
height: 100%;
|
||||||
|
background: linear-gradient(
|
||||||
|
90deg,
|
||||||
|
rgba(255,255,255,0),
|
||||||
|
rgba(255,255,255,0.3),
|
||||||
|
rgba(255,255,255,0)
|
||||||
|
);
|
||||||
|
animation: shimmer 1.5s infinite;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
@@ -15,6 +15,9 @@
|
|||||||
<img loading="lazy" src="@/assets/home/setting-user.png" alt="">
|
<img loading="lazy" src="@/assets/home/setting-user.png" alt="">
|
||||||
</div>
|
</div>
|
||||||
<span class="header-title">{{ form.agentName }}</span>
|
<span class="header-title">{{ form.agentName }}</span>
|
||||||
|
<button class="custom-close-btn" @click="goToHome">
|
||||||
|
×
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="divider"></div>
|
<div class="divider"></div>
|
||||||
|
|
||||||
@@ -165,6 +168,9 @@ export default {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
|
goToHome() {
|
||||||
|
this.$router.push('/home');
|
||||||
|
},
|
||||||
saveConfig() {
|
saveConfig() {
|
||||||
const configData = {
|
const configData = {
|
||||||
agentCode: this.form.agentCode,
|
agentCode: this.form.agentCode,
|
||||||
@@ -423,6 +429,7 @@ export default {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.config-header {
|
.config-header {
|
||||||
|
position: relative;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 13px;
|
gap: 13px;
|
||||||
@@ -560,8 +567,35 @@ export default {
|
|||||||
background: none;
|
background: none;
|
||||||
position: absolute;
|
position: absolute;
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
bottom: -10px;
|
bottom: -10%;
|
||||||
right: 21px;
|
right: 3%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.custom-close-btn {
|
||||||
|
position: absolute;
|
||||||
|
top: 25%;
|
||||||
|
right: 0;
|
||||||
|
transform: translateY(-50%);
|
||||||
|
width: 35px;
|
||||||
|
height: 35px;
|
||||||
|
border-radius: 50%;
|
||||||
|
border: 2px solid #cfcfcf;
|
||||||
|
background: none;
|
||||||
|
font-size: 30px;
|
||||||
|
font-weight: lighter;
|
||||||
|
color: #cfcfcf;
|
||||||
|
cursor: pointer;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
z-index: 1;
|
||||||
|
padding: 0;
|
||||||
|
outline: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.custom-close-btn:hover {
|
||||||
|
color: #409EFF;
|
||||||
|
border-color: #409EFF;
|
||||||
}
|
}
|
||||||
|
|
||||||
</style>
|
</style>
|
||||||
@@ -1,400 +0,0 @@
|
|||||||
<template>
|
|
||||||
<div class="container">
|
|
||||||
<h1 class="title">XiaoZhi ESP32 Server 测试助手</h1>
|
|
||||||
<div class="chat-container" ref="chatContainer">
|
|
||||||
<div v-for="(message, index) in messages" :key="index"
|
|
||||||
:class="['message', message.role === 'user' ? 'user' : 'assistant']">
|
|
||||||
<span>{{ message.content }}</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="controls">
|
|
||||||
<button @click="toggleRecording" :disabled="wsStatus !== 'connected'" :class="{ recording: isRecording }">
|
|
||||||
{{ isRecording ? '停止录音' : '开始录音' }}
|
|
||||||
</button>
|
|
||||||
<p>WebSocket: {{ wsStatus }}</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script>
|
|
||||||
import { OpusDecoder } from 'opus-decoder';
|
|
||||||
import Recorder from 'opus-recorder';
|
|
||||||
|
|
||||||
export default {
|
|
||||||
name: 'TestPage',
|
|
||||||
data() {
|
|
||||||
return {
|
|
||||||
messages: [],
|
|
||||||
wsStatus: 'disconnected',
|
|
||||||
isRecording: false,
|
|
||||||
ws: null,
|
|
||||||
recorder: null,
|
|
||||||
stream: null,
|
|
||||||
audioContext: null,
|
|
||||||
sourceNode: null,
|
|
||||||
audioDecoder: null,
|
|
||||||
audioBufferQueue: [], // 当前句子的音频缓冲区
|
|
||||||
playbackQueue: [], // 播放队列,存储待播放的句子
|
|
||||||
isPlaying: false,
|
|
||||||
}
|
|
||||||
},
|
|
||||||
methods: {
|
|
||||||
connectWebSocket() {
|
|
||||||
this.ws = new WebSocket('ws://192.168.3.97:8000');//修改服务端地址
|
|
||||||
this.ws.binaryType = 'arraybuffer';
|
|
||||||
this.ws.onopen = () => {
|
|
||||||
this.wsStatus = 'connected';
|
|
||||||
console.log('WebSocket 连接成功');
|
|
||||||
this.ws.send(JSON.stringify({ type: 'auth', 'device-id': 'test-device' }));
|
|
||||||
this.audioDecoder = new OpusDecoder({ sampleRate: 16000, channels: 1 });
|
|
||||||
};
|
|
||||||
this.ws.onmessage = async (event) => {
|
|
||||||
if (typeof event.data === 'string') {
|
|
||||||
const msg = JSON.parse(event.data);
|
|
||||||
console.log('收到文本消息:', msg);
|
|
||||||
|
|
||||||
if (msg.type === 'stt') {
|
|
||||||
this.messages.push({ role: 'user', content: msg.text });
|
|
||||||
this.scrollToBottom();
|
|
||||||
} else if (msg.type === 'llm') {
|
|
||||||
this.messages.push({ role: 'assistant', content: msg.text });
|
|
||||||
this.scrollToBottom();
|
|
||||||
} else if (msg.type === 'tts') {
|
|
||||||
if (msg.state === 'sentence_start') {
|
|
||||||
this.audioBufferQueue = [];
|
|
||||||
const lastMessage = this.messages[this.messages.length - 1];
|
|
||||||
if (!lastMessage || lastMessage.content !== msg.text) {
|
|
||||||
this.messages.push({ role: 'assistant', content: msg.text });
|
|
||||||
this.scrollToBottom();
|
|
||||||
}
|
|
||||||
} else if (msg.state === 'sentence_end') {
|
|
||||||
if (this.audioBufferQueue.length > 0) {
|
|
||||||
this.playbackQueue.push([...this.audioBufferQueue]);
|
|
||||||
this.audioBufferQueue = [];
|
|
||||||
this.playNextInQueue();
|
|
||||||
}
|
|
||||||
} else if (msg.state === 'stop') {
|
|
||||||
console.log('TTS 任务结束');
|
|
||||||
if (this.audioBufferQueue.length > 0) {
|
|
||||||
this.playbackQueue.push([...this.audioBufferQueue]);
|
|
||||||
this.audioBufferQueue = [];
|
|
||||||
this.playNextInQueue();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else if (msg.type === 'hello') {
|
|
||||||
console.log('收到服务器初始化消息:', msg);
|
|
||||||
}
|
|
||||||
} else if (event.data instanceof ArrayBuffer) {
|
|
||||||
console.log('收到音频帧,大小:', event.data.byteLength, '字节');
|
|
||||||
const opusFrame = new Uint8Array(event.data);
|
|
||||||
const frameHead = Array.from(opusFrame.slice(0, 8))
|
|
||||||
.map(b => b.toString(16).padStart(2, '0'))
|
|
||||||
.join(' ');
|
|
||||||
console.log('音频帧前8字节:', frameHead);
|
|
||||||
|
|
||||||
try {
|
|
||||||
const decoded = this.audioDecoder.decodeFrame(opusFrame);
|
|
||||||
console.log('解码结果:', decoded);
|
|
||||||
if (decoded && decoded.channelData && decoded.channelData[0]) {
|
|
||||||
const pcmData = decoded.channelData[0];
|
|
||||||
if (pcmData.length > 0) {
|
|
||||||
this.audioBufferQueue.push(pcmData);
|
|
||||||
console.log('解码音频帧,PCM 数据长度:', pcmData.length);
|
|
||||||
if (this.audioBufferQueue.length >= 5 && this.playbackQueue.length === 0 && !this.isPlaying) {
|
|
||||||
this.playbackQueue.push([...this.audioBufferQueue]);
|
|
||||||
this.audioBufferQueue = [];
|
|
||||||
this.playNextInQueue();
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
console.warn('解码成功,但 PCM 数据长度为 0');
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
console.warn('解码结果无效:', decoded);
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
console.error('Opus 解码错误:', e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
this.ws.onerror = (error) => {
|
|
||||||
console.error('WebSocket 错误:', error);
|
|
||||||
this.wsStatus = 'error';
|
|
||||||
};
|
|
||||||
this.ws.onclose = () => {
|
|
||||||
this.wsStatus = 'disconnected';
|
|
||||||
console.log('WebSocket 断开');
|
|
||||||
setTimeout(this.connectWebSocket, 1000);
|
|
||||||
};
|
|
||||||
},
|
|
||||||
scrollToBottom() {
|
|
||||||
this.$nextTick(() => {
|
|
||||||
if (this.$refs.chatContainer) {
|
|
||||||
this.$refs.chatContainer.scrollTop = this.$refs.chatContainer.scrollHeight;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
},
|
|
||||||
async playNextInQueue() {
|
|
||||||
if (this.isPlaying || this.playbackQueue.length === 0) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
this.isPlaying = true;
|
|
||||||
|
|
||||||
if (!this.audioContext || this.audioContext.state === 'closed') {
|
|
||||||
this.audioContext = new (window.AudioContext || window.webkitAudioContext)({ sampleRate: 16000 });
|
|
||||||
}
|
|
||||||
|
|
||||||
const pcmBuffers = this.playbackQueue.shift();
|
|
||||||
const totalLength = pcmBuffers.reduce((sum, pcm) => sum + pcm.length, 0);
|
|
||||||
if (totalLength === 0) {
|
|
||||||
console.error('音频缓冲区总长度为 0,无法播放');
|
|
||||||
this.isPlaying = false;
|
|
||||||
this.playNextInQueue();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const mergedPcm = new Float32Array(totalLength);
|
|
||||||
let offset = 0;
|
|
||||||
for (const pcm of pcmBuffers) {
|
|
||||||
mergedPcm.set(pcm, offset);
|
|
||||||
offset += pcm.length;
|
|
||||||
}
|
|
||||||
|
|
||||||
const audioBuffer = this.audioContext.createBuffer(1, totalLength, 16000);
|
|
||||||
audioBuffer.getChannelData(0).set(mergedPcm);
|
|
||||||
|
|
||||||
const source = this.audioContext.createBufferSource();
|
|
||||||
source.buffer = audioBuffer;
|
|
||||||
source.connect(this.audioContext.destination);
|
|
||||||
source.onended = () => {
|
|
||||||
this.isPlaying = false;
|
|
||||||
console.log('音频播放结束');
|
|
||||||
this.playNextInQueue();
|
|
||||||
};
|
|
||||||
source.start();
|
|
||||||
console.log('开始播放音频,总长度:', totalLength);
|
|
||||||
},
|
|
||||||
stripOggContainer(data) {
|
|
||||||
let arrayBuffer;
|
|
||||||
if (data instanceof ArrayBuffer) {
|
|
||||||
arrayBuffer = data;
|
|
||||||
} else if (data.buffer instanceof ArrayBuffer) {
|
|
||||||
arrayBuffer = data.buffer;
|
|
||||||
} else {
|
|
||||||
console.error('输入数据类型不支持:', data);
|
|
||||||
return [data];
|
|
||||||
}
|
|
||||||
|
|
||||||
const dataView = new DataView(arrayBuffer);
|
|
||||||
const frames = [];
|
|
||||||
let offset = 0;
|
|
||||||
|
|
||||||
while (offset < arrayBuffer.byteLength) {
|
|
||||||
if (dataView.getUint32(offset) === 0x4F676753) { // 'OggS'
|
|
||||||
const segmentTableLength = dataView.getUint8(offset + 26);
|
|
||||||
const segmentTableOffset = offset + 27;
|
|
||||||
let segmentOffset = segmentTableOffset + segmentTableLength;
|
|
||||||
|
|
||||||
for (let i = 0; i < segmentTableLength; i++) {
|
|
||||||
const segmentLength = dataView.getUint8(segmentTableOffset + i);
|
|
||||||
const segmentData = arrayBuffer.slice(segmentOffset, segmentOffset + segmentLength);
|
|
||||||
segmentOffset += segmentLength;
|
|
||||||
|
|
||||||
const header = new TextDecoder().decode(segmentData.slice(0, 8));
|
|
||||||
if (header === 'OpusHead' || header === 'OpusTags') {
|
|
||||||
console.log(`跳过 ${header},大小: ${segmentLength} 字节`);
|
|
||||||
} else if (segmentLength >= 50 && segmentLength <= 300) {
|
|
||||||
frames.push(segmentData);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
offset = segmentOffset;
|
|
||||||
} else {
|
|
||||||
const remainingLength = arrayBuffer.byteLength - offset;
|
|
||||||
if (remainingLength >= 50 && remainingLength <= 300) {
|
|
||||||
frames.push(arrayBuffer.slice(offset));
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (frames.length === 0) {
|
|
||||||
console.warn('未找到有效裸 Opus 帧,返回原始数据');
|
|
||||||
return [arrayBuffer];
|
|
||||||
}
|
|
||||||
console.log('剥离后找到', frames.length, '个裸 Opus 帧');
|
|
||||||
return frames;
|
|
||||||
},
|
|
||||||
async initRecorder() {
|
|
||||||
console.log('开始初始化录音');
|
|
||||||
try {
|
|
||||||
if (this.stream) {
|
|
||||||
this.stream.getTracks().forEach(track => track.stop());
|
|
||||||
}
|
|
||||||
this.stream = await navigator.mediaDevices.getUserMedia({ audio: true });
|
|
||||||
console.log('获取麦克风权限成功,stream:', this.stream);
|
|
||||||
|
|
||||||
if (!this.audioContext || this.audioContext.state === 'closed') {
|
|
||||||
this.audioContext = new (window.AudioContext || window.webkitAudioContext)({ sampleRate: 16000 });
|
|
||||||
}
|
|
||||||
this.sourceNode = this.audioContext.createMediaStreamSource(this.stream);
|
|
||||||
|
|
||||||
this.recorder = new Recorder({
|
|
||||||
encoderPath: '/encoderWorker.min.js',
|
|
||||||
sampleRate: 16000,
|
|
||||||
numberOfChannels: 1,
|
|
||||||
frameSize: 960,
|
|
||||||
encoderApplication: 2048,
|
|
||||||
encoderFrameSize: 60,
|
|
||||||
encoderBitRate: 24000,
|
|
||||||
monitorGain: 0,
|
|
||||||
sourceNode: this.sourceNode,
|
|
||||||
ogg: false,
|
|
||||||
streamPages: false,
|
|
||||||
maxFramesPerPage: 1,
|
|
||||||
});
|
|
||||||
|
|
||||||
this.recorder.ondataavailable = (data) => {
|
|
||||||
console.log('录音数据可用:', data.byteLength, '类型:', data.constructor.name);
|
|
||||||
const frames = this.stripOggContainer(data);
|
|
||||||
frames.forEach((frame, index) => {
|
|
||||||
const frameHead = Array.from(new Uint8Array(frame.slice(0, 8)))
|
|
||||||
.map(b => b.toString(16).padStart(2, '0'))
|
|
||||||
.join(' ');
|
|
||||||
console.log(`帧 ${index} 大小: ${frame.byteLength} 字节,前8字节: ${frameHead}`);
|
|
||||||
|
|
||||||
if (frame.byteLength < 50 || frame.byteLength > 300) {
|
|
||||||
console.warn(`帧 ${index} 大小异常: ${frame.byteLength} 字节,预期 50-300 字节`);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.ws && this.ws.readyState === WebSocket.OPEN) {
|
|
||||||
this.ws.send(frame);
|
|
||||||
console.log('发送裸 Opus 帧:', frame.byteLength);
|
|
||||||
} else {
|
|
||||||
console.warn('WebSocket 未连接,跳过发送');
|
|
||||||
}
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
this.recorder.onstart = () => {
|
|
||||||
console.log('录音已启动');
|
|
||||||
};
|
|
||||||
|
|
||||||
this.recorder.onstop = () => {
|
|
||||||
console.log('录音停止');
|
|
||||||
this.stream.getTracks().forEach(track => track.stop());
|
|
||||||
this.stream = null;
|
|
||||||
this.sourceNode = null;
|
|
||||||
this.scrollToBottom();
|
|
||||||
};
|
|
||||||
|
|
||||||
console.log('Recorder 初始化成功');
|
|
||||||
} catch (err) {
|
|
||||||
console.error('初始化录音失败:', err);
|
|
||||||
alert('无法访问麦克风或录音初始化失败,请检查权限');
|
|
||||||
}
|
|
||||||
},
|
|
||||||
async toggleRecording() {
|
|
||||||
console.log('点击 toggleRecording,当前状态:', this.isRecording, 'WebSocket 状态:', this.wsStatus);
|
|
||||||
if (!this.recorder) {
|
|
||||||
await this.initRecorder();
|
|
||||||
if (!this.recorder) {
|
|
||||||
console.error('recorder 初始化失败');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (this.isRecording) {
|
|
||||||
console.log('停止录音');
|
|
||||||
this.recorder.stop();
|
|
||||||
this.isRecording = false;
|
|
||||||
} else {
|
|
||||||
try {
|
|
||||||
console.log('开始录音');
|
|
||||||
await this.initRecorder();
|
|
||||||
await this.recorder.start();
|
|
||||||
console.log('录音开始后,状态:', this.recorder.state);
|
|
||||||
this.isRecording = true;
|
|
||||||
} catch (err) {
|
|
||||||
console.error('录音启动失败:', err);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
},
|
|
||||||
mounted() {
|
|
||||||
console.log('组件挂载,初始化 WebSocket');
|
|
||||||
this.connectWebSocket();
|
|
||||||
},
|
|
||||||
destroyed() {
|
|
||||||
if (this.ws) this.ws.close();
|
|
||||||
if (this.stream) this.stream.getTracks().forEach(track => track.stop());
|
|
||||||
if (this.recorder) this.recorder.stop();
|
|
||||||
if (this.audioContext) this.audioContext.close();
|
|
||||||
if (this.audioDecoder) this.audioDecoder.destroy();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style scoped>
|
|
||||||
.container {
|
|
||||||
max-width: 800px;
|
|
||||||
margin: 0 auto;
|
|
||||||
padding: 20px;
|
|
||||||
font-family: Arial, sans-serif;
|
|
||||||
}
|
|
||||||
|
|
||||||
.title {
|
|
||||||
text-align: center;
|
|
||||||
font-size: 24px;
|
|
||||||
margin-bottom: 20px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.chat-container {
|
|
||||||
height: 60vh;
|
|
||||||
overflow-y: auto;
|
|
||||||
border: 1px solid #ccc;
|
|
||||||
padding: 10px;
|
|
||||||
background: #f9f9f9;
|
|
||||||
border-radius: 5px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.message {
|
|
||||||
margin: 10px 0;
|
|
||||||
padding: 8px 12px;
|
|
||||||
border-radius: 5px;
|
|
||||||
max-width: 70%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.user {
|
|
||||||
background: #007bff;
|
|
||||||
color: white;
|
|
||||||
margin-left: auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
.assistant {
|
|
||||||
background: #e9ecef;
|
|
||||||
color: black;
|
|
||||||
}
|
|
||||||
|
|
||||||
.controls {
|
|
||||||
text-align: center;
|
|
||||||
margin-top: 20px;
|
|
||||||
}
|
|
||||||
|
|
||||||
button {
|
|
||||||
padding: 10px 20px;
|
|
||||||
font-size: 16px;
|
|
||||||
cursor: pointer;
|
|
||||||
background: #007bff;
|
|
||||||
color: white;
|
|
||||||
border: none;
|
|
||||||
border-radius: 5px;
|
|
||||||
}
|
|
||||||
|
|
||||||
button:disabled {
|
|
||||||
background: #ccc;
|
|
||||||
cursor: not-allowed;
|
|
||||||
}
|
|
||||||
|
|
||||||
button.recording {
|
|
||||||
background: #dc3545;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
@@ -69,6 +69,11 @@ def get_config_from_api(config):
|
|||||||
"url": config["manager-api"].get("url", ""),
|
"url": config["manager-api"].get("url", ""),
|
||||||
"secret": config["manager-api"].get("secret", ""),
|
"secret": config["manager-api"].get("secret", ""),
|
||||||
}
|
}
|
||||||
|
if config.get("server"):
|
||||||
|
config_data["server"] = {
|
||||||
|
"ip": config["server"].get("ip", ""),
|
||||||
|
"port": config["server"].get("port", ""),
|
||||||
|
}
|
||||||
return config_data
|
return config_data
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import sys
|
|||||||
from loguru import logger
|
from loguru import logger
|
||||||
from config.config_loader import load_config
|
from config.config_loader import load_config
|
||||||
|
|
||||||
SERVER_VERSION = "0.3.8"
|
SERVER_VERSION = "0.3.10"
|
||||||
|
|
||||||
|
|
||||||
def get_module_abbreviation(module_name, module_dict):
|
def get_module_abbreviation(module_name, module_dict):
|
||||||
|
|||||||
@@ -43,6 +43,14 @@ def check_config_file():
|
|||||||
missing_keys = find_missing_keys(new_config, old_config)
|
missing_keys = find_missing_keys(new_config, old_config)
|
||||||
read_config_from_api = old_config.get("read_config_from_api", False)
|
read_config_from_api = old_config.get("read_config_from_api", False)
|
||||||
if read_config_from_api:
|
if read_config_from_api:
|
||||||
|
old_config_origin = read_config(old_config_file)
|
||||||
|
if old_config_origin.get("selected_module") is not None:
|
||||||
|
missing_keys_str = "\n".join(f"- {key}" for key in missing_keys)
|
||||||
|
error_msg = "您的配置文件好像既包含智控台的配置又包含本地配置:\n"
|
||||||
|
error_msg += "\n建议您:\n"
|
||||||
|
error_msg += "1、将根目录的config_from_api.yaml文件复制到data下,重命名为.config.yaml\n"
|
||||||
|
error_msg += "2、按教程配置好接口地址和密钥\n"
|
||||||
|
raise ValueError(error_msg)
|
||||||
return
|
return
|
||||||
|
|
||||||
if missing_keys:
|
if missing_keys:
|
||||||
|
|||||||
@@ -5,6 +5,9 @@
|
|||||||
# 先启动manager-api和manager-web,注册一个账号,第一个注册的账号为管理员
|
# 先启动manager-api和manager-web,注册一个账号,第一个注册的账号为管理员
|
||||||
# 使用管理员,进入【参数管理】页面,找到【server.secret】,复制它到参数值,注意每次从零部署,server.secret都会变化
|
# 使用管理员,进入【参数管理】页面,找到【server.secret】,复制它到参数值,注意每次从零部署,server.secret都会变化
|
||||||
# 打开本data目录下的.config.yaml文件,修改manager-api.secret为刚才复制出来的server.secret
|
# 打开本data目录下的.config.yaml文件,修改manager-api.secret为刚才复制出来的server.secret
|
||||||
|
server:
|
||||||
|
ip: 0.0.0.0
|
||||||
|
port: 8000
|
||||||
manager-api:
|
manager-api:
|
||||||
# 你的manager-api的地址,最好使用局域网ip
|
# 你的manager-api的地址,最好使用局域网ip
|
||||||
url: http://127.0.0.1:8002/xiaozhi
|
url: http://127.0.0.1:8002/xiaozhi
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import openai
|
import openai
|
||||||
|
from openai.types import CompletionUsage
|
||||||
from config.logger import setup_logging
|
from config.logger import setup_logging
|
||||||
from core.utils.util import check_model_key
|
from core.utils.util import check_model_key
|
||||||
from core.providers.llm.base import LLMProviderBase
|
from core.providers.llm.base import LLMProviderBase
|
||||||
@@ -70,7 +71,17 @@ class LLMProvider(LLMProviderBase):
|
|||||||
)
|
)
|
||||||
|
|
||||||
for chunk in stream:
|
for chunk in stream:
|
||||||
yield chunk.choices[0].delta.content, chunk.choices[0].delta.tool_calls
|
# 检查是否存在有效的choice且content不为空
|
||||||
|
if getattr(chunk, "choices", None):
|
||||||
|
yield chunk.choices[0].delta.content, chunk.choices[0].delta.tool_calls
|
||||||
|
# 存在 CompletionUsage 消息时,生成 Token 消耗 log
|
||||||
|
elif isinstance(getattr(chunk, 'usage', None), CompletionUsage):
|
||||||
|
usage_info = getattr(chunk, 'usage', None)
|
||||||
|
logger.bind(tag=TAG).info(
|
||||||
|
f"Token 消耗:输入 {getattr(usage_info, 'prompt_tokens', '未知')},"
|
||||||
|
f"输出 {getattr(usage_info, 'completion_tokens', '未知')},"
|
||||||
|
f"共计 {getattr(usage_info, 'total_tokens', '未知')}"
|
||||||
|
)
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.bind(tag=TAG).error(f"Error in function call streaming: {e}")
|
logger.bind(tag=TAG).error(f"Error in function call streaming: {e}")
|
||||||
|
|||||||
@@ -15,7 +15,10 @@ logger = setup_logging()
|
|||||||
class TTSProvider(TTSProviderBase):
|
class TTSProvider(TTSProviderBase):
|
||||||
def __init__(self, config, delete_audio_file):
|
def __init__(self, config, delete_audio_file):
|
||||||
super().__init__(config, delete_audio_file)
|
super().__init__(config, delete_audio_file)
|
||||||
self.appid = config.get("appid")
|
if config.get("appid"):
|
||||||
|
self.appid = int(config.get("appid"))
|
||||||
|
else:
|
||||||
|
self.appid = ""
|
||||||
self.access_token = config.get("access_token")
|
self.access_token = config.get("access_token")
|
||||||
self.cluster = config.get("cluster")
|
self.cluster = config.get("cluster")
|
||||||
|
|
||||||
|
|||||||
@@ -5,6 +5,10 @@ import requests
|
|||||||
import shutil
|
import shutil
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from core.providers.tts.base import TTSProviderBase
|
from core.providers.tts.base import TTSProviderBase
|
||||||
|
from config.logger import setup_logging
|
||||||
|
|
||||||
|
TAG = __name__
|
||||||
|
logger = setup_logging()
|
||||||
|
|
||||||
|
|
||||||
class TTSProvider(TTSProviderBase):
|
class TTSProvider(TTSProviderBase):
|
||||||
@@ -54,6 +58,7 @@ class TTSProvider(TTSProviderBase):
|
|||||||
|
|
||||||
resp = requests.request("POST", url, data=payload)
|
resp = requests.request("POST", url, data=payload)
|
||||||
if resp.status_code != 200:
|
if resp.status_code != 200:
|
||||||
|
logger.bind(tag=TAG).error(f"TTS请求失败: {resp.text}")
|
||||||
return None
|
return None
|
||||||
resp_json = resp.json()
|
resp_json = resp.json()
|
||||||
try:
|
try:
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ class WebSocketServer:
|
|||||||
|
|
||||||
async def start(self):
|
async def start(self):
|
||||||
server_config = self.config["server"]
|
server_config = self.config["server"]
|
||||||
host = server_config["ip"]
|
host = server_config.get("ip", "0.0.0.0")
|
||||||
port = int(server_config.get("port", 8000))
|
port = int(server_config.get("port", 8000))
|
||||||
|
|
||||||
self.logger.bind(tag=TAG).info(
|
self.logger.bind(tag=TAG).info(
|
||||||
|
|||||||
@@ -47,6 +47,10 @@ services:
|
|||||||
- SPRING_DATASOURCE_DRUID_PASSWORD=123456
|
- SPRING_DATASOURCE_DRUID_PASSWORD=123456
|
||||||
- SPRING_DATA_REDIS_HOST=xiaozhi-esp32-server-redis
|
- SPRING_DATA_REDIS_HOST=xiaozhi-esp32-server-redis
|
||||||
- SPRING_DATA_REDIS_PORT=6379
|
- SPRING_DATA_REDIS_PORT=6379
|
||||||
|
volumes:
|
||||||
|
# 配置文件目录
|
||||||
|
- ./uploadfile:/app/uploadfile
|
||||||
|
|
||||||
xiaozhi-esp32-server-db:
|
xiaozhi-esp32-server-db:
|
||||||
image: mysql:latest
|
image: mysql:latest
|
||||||
container_name: xiaozhi-esp32-server-db
|
container_name: xiaozhi-esp32-server-db
|
||||||
@@ -59,7 +63,7 @@ services:
|
|||||||
networks:
|
networks:
|
||||||
- default
|
- default
|
||||||
expose:
|
expose:
|
||||||
- "3306:3306"
|
- 3306
|
||||||
volumes:
|
volumes:
|
||||||
- ./mysql/data:/var/lib/mysql
|
- ./mysql/data:/var/lib/mysql
|
||||||
environment:
|
environment:
|
||||||
|
|||||||
@@ -25,3 +25,4 @@ sherpa_onnx==1.11.0
|
|||||||
mcp==1.4.1
|
mcp==1.4.1
|
||||||
cnlunar==0.2.0
|
cnlunar==0.2.0
|
||||||
PySocks==1.7.1
|
PySocks==1.7.1
|
||||||
|
dashscope==1.23.1
|
||||||
Reference in New Issue
Block a user