mirror of
https://github.com/xinnan-tech/xiaozhi-esp32-server.git
synced 2026-07-22 07:03:53 +08:00
feat: 新增智控台管理websocket服务器端【配置拉取】和【重启】
This commit is contained in:
@@ -71,10 +71,15 @@ public class RenExceptionHandler {
|
||||
List<ObjectError> allErrors = ex.getBindingResult().getAllErrors();
|
||||
String errorMsg = allErrors.stream()
|
||||
.filter(Objects::nonNull)
|
||||
.map(DefaultMessageSourceResolvable::getDefaultMessage)
|
||||
.map(err -> {
|
||||
String msg = err.getDefaultMessage();
|
||||
return (msg != null && !msg.trim().isEmpty()) ? msg : null;
|
||||
})
|
||||
.filter(Objects::nonNull)
|
||||
.findFirst()
|
||||
.orElse("");
|
||||
return new Result<Void>().error(400, errorMsg);
|
||||
.orElse("请求参数错误!");
|
||||
|
||||
return new Result<Void>().error(ErrorCode.PARAM_VALUE_NULL, errorMsg);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -21,6 +21,8 @@ public class DateUtils {
|
||||
* 时间格式(yyyy-MM-dd HH:mm:ss)
|
||||
*/
|
||||
public final static String DATE_TIME_PATTERN = "yyyy-MM-dd HH:mm:ss";
|
||||
public final static String DATE_TIME_MILLIS_PATTERN = "yyyy-MM-dd HH:mm:ss.SSS";
|
||||
|
||||
|
||||
/**
|
||||
* 日期格式化 日期格式为:yyyy-MM-dd
|
||||
@@ -63,6 +65,19 @@ public class DateUtils {
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
public static String getDateTimeNow() {
|
||||
return getDateTimeNow(DATE_TIME_PATTERN);
|
||||
}
|
||||
|
||||
public static String getDateTimeNow(String pattern) {
|
||||
return format(new Date(), pattern);
|
||||
}
|
||||
|
||||
public static String millsToSecond(long mills) {
|
||||
return String.format("%.3f", mills / 1000.0);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取简短的时间字符串:10秒前返回刚刚,多少秒前,几小时前,超过一周返回年月日时分秒
|
||||
* @param date
|
||||
|
||||
+115
@@ -0,0 +1,115 @@
|
||||
package xiaozhi.modules.sys.controller;
|
||||
|
||||
import com.fasterxml.jackson.core.JsonProcessingException;
|
||||
import com.fasterxml.jackson.databind.DeserializationFeature;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import jakarta.validation.Valid;
|
||||
import lombok.AllArgsConstructor;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.apache.shiro.authz.annotation.RequiresPermissions;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
import org.springframework.web.socket.WebSocketHttpHeaders;
|
||||
import xiaozhi.common.annotation.LogOperation;
|
||||
import xiaozhi.common.constant.Constant;
|
||||
import xiaozhi.common.exception.RenException;
|
||||
import xiaozhi.common.utils.Result;
|
||||
import xiaozhi.modules.sys.dto.EmitSeverActionDTO;
|
||||
import xiaozhi.modules.sys.dto.ServerActionPayloadDTO;
|
||||
import xiaozhi.modules.sys.dto.ServerActionResponseDTO;
|
||||
import xiaozhi.modules.sys.enums.ServerActionEnum;
|
||||
import xiaozhi.modules.sys.service.SysParamsService;
|
||||
import xiaozhi.modules.sys.utils.WebSocketClientManager;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
/**
|
||||
* 服务端管理控制器
|
||||
*/
|
||||
@RestController
|
||||
@RequestMapping("admin/server")
|
||||
@Tag(name = "服务端管理")
|
||||
@AllArgsConstructor
|
||||
public class ServerSideManageController
|
||||
{
|
||||
private final SysParamsService sysParamsService;
|
||||
private static final ObjectMapper objectMapper;
|
||||
static {
|
||||
objectMapper = new ObjectMapper();
|
||||
objectMapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
|
||||
}
|
||||
|
||||
@Operation(summary = "获取Ws服务端列表")
|
||||
@GetMapping("/server-list")
|
||||
@RequiresPermissions("sys:role:superAdmin")
|
||||
public Result<List<String>> getWsServerList() {
|
||||
String cachedWs = sysParamsService.getValue(Constant.SERVER_WEBSOCKET, true);
|
||||
return new Result<List<String>>().ok(Arrays.asList(cachedWs.split(";")));
|
||||
}
|
||||
|
||||
@Operation(summary = "通知python服务端更新配置")
|
||||
@PostMapping("/emit-action")
|
||||
@LogOperation("通知python服务端更新配置")
|
||||
// @RequiresPermissions("sys:role:superAdmin")
|
||||
public Result<Boolean> emitServerAction(@RequestBody @Valid EmitSeverActionDTO emitSeverActionDTO) {
|
||||
if (emitSeverActionDTO.getAction() == null) {
|
||||
throw new RenException("无效服务端操作");
|
||||
}
|
||||
String wsText = sysParamsService.getValue(Constant.SERVER_WEBSOCKET, true);
|
||||
if (StringUtils.isBlank(wsText)) {
|
||||
throw new RenException("未配置服务端WebSocket地址");
|
||||
}
|
||||
String targetWs = emitSeverActionDTO.getTargetWs();
|
||||
String[] wsList = wsText.split(";");
|
||||
// 找到需要发起的
|
||||
if (StringUtils.isBlank(targetWs) || !Arrays.asList(wsList).contains(targetWs)) {
|
||||
throw new RenException("目标WebSocket地址不存在");
|
||||
}
|
||||
return new Result<Boolean>().ok(emitServerActionByWs(targetWs, emitSeverActionDTO.getAction()));
|
||||
}
|
||||
|
||||
private Boolean emitServerActionByWs(String targetWsUri, ServerActionEnum actionEnum) {
|
||||
if (StringUtils.isBlank(targetWsUri) || actionEnum == null) {
|
||||
return false;
|
||||
}
|
||||
String serverSK = sysParamsService.getValue(Constant.SERVER_SECRET, true);
|
||||
WebSocketHttpHeaders headers = new WebSocketHttpHeaders();
|
||||
headers.add("Authorization", "Bearer " + serverSK);
|
||||
headers.add("device-id", serverSK);
|
||||
|
||||
try (WebSocketClientManager client = new WebSocketClientManager.Builder()
|
||||
.connectTimeout(3, TimeUnit.SECONDS)
|
||||
.maxSessionDuration(5, TimeUnit.SECONDS)
|
||||
.uri(targetWsUri)
|
||||
.headers(headers)
|
||||
.build()) {
|
||||
// 如果连接成功则发送一个json数据包并等待服务端响应
|
||||
client.sendJson(
|
||||
ServerActionPayloadDTO.build(
|
||||
actionEnum,
|
||||
Map.of("secret", serverSK)
|
||||
));
|
||||
// 等待服务端响应并持续监听信息
|
||||
client.listener((jsonText)-> {
|
||||
if (StringUtils.isBlank(jsonText)) {
|
||||
return false;
|
||||
}
|
||||
try {
|
||||
return ServerActionResponseDTO.isSuccess(objectMapper.readValue(jsonText, ServerActionResponseDTO.class));
|
||||
}
|
||||
catch (JsonProcessingException e) {
|
||||
return false;
|
||||
}
|
||||
});
|
||||
}
|
||||
catch (Exception e) {
|
||||
// 捕获全部错误,由全局异常处理器返回
|
||||
throw new RenException("WebSocket连接失败或连接超时");
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
+14
-14
@@ -111,7 +111,7 @@ public class SysParamsController {
|
||||
|
||||
/**
|
||||
* 验证WebSocket地址列表
|
||||
*
|
||||
*
|
||||
* @param urls WebSocket地址列表,以分号分隔
|
||||
* @return 验证结果
|
||||
*/
|
||||
@@ -143,6 +143,19 @@ public class SysParamsController {
|
||||
}
|
||||
}
|
||||
|
||||
@PostMapping("/delete")
|
||||
@Operation(summary = "删除")
|
||||
@LogOperation("删除")
|
||||
@RequiresPermissions("sys:role:superAdmin")
|
||||
public Result<Void> delete(@RequestBody String[] ids) {
|
||||
// 效验数据
|
||||
AssertUtils.isArrayEmpty(ids, "id");
|
||||
|
||||
sysParamsService.delete(ids);
|
||||
configService.getConfig(false);
|
||||
return new Result<Void>();
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证OTA地址
|
||||
*/
|
||||
@@ -182,17 +195,4 @@ public class SysParamsController {
|
||||
throw new RenException("OTA接口验证失败:" + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
@PostMapping("/delete")
|
||||
@Operation(summary = "删除")
|
||||
@LogOperation("删除")
|
||||
@RequiresPermissions("sys:role:superAdmin")
|
||||
public Result<Void> delete(@RequestBody String[] ids) {
|
||||
// 效验数据
|
||||
AssertUtils.isArrayEmpty(ids, "id");
|
||||
|
||||
sysParamsService.delete(ids);
|
||||
configService.getConfig(false);
|
||||
return new Result<Void>();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
package xiaozhi.modules.sys.dto;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import jakarta.validation.constraints.NotEmpty;
|
||||
import jakarta.validation.constraints.NotNull;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
import xiaozhi.modules.sys.enums.ServerActionEnum;
|
||||
|
||||
/**
|
||||
* 发送python服务端操作DTO
|
||||
*
|
||||
* @Author CAIXYPROMISE
|
||||
* @since 2025/5/15 4:55
|
||||
*/
|
||||
@Data
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public class EmitSeverActionDTO
|
||||
{
|
||||
@Schema(description = "目标ws地址")
|
||||
@NotEmpty(message = "目标ws地址不能为空")
|
||||
private String targetWs;
|
||||
|
||||
@Schema(description = "指定操作")
|
||||
@NotNull(message = "操作不能为空")
|
||||
private ServerActionEnum action;
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
package xiaozhi.modules.sys.dto;
|
||||
|
||||
import lombok.Data;
|
||||
import xiaozhi.modules.sys.enums.ServerActionEnum;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* 服务端动作DTO
|
||||
*
|
||||
* @Author CAIXYPROMISE
|
||||
* @since 2025/5/15 5:10
|
||||
*/
|
||||
@Data
|
||||
public class ServerActionPayloadDTO
|
||||
{
|
||||
/**
|
||||
* 类型(智控台发往服务端的都是server)
|
||||
*/
|
||||
private String type;
|
||||
/**
|
||||
* 动作
|
||||
*/
|
||||
private ServerActionEnum action;
|
||||
/**
|
||||
* 内容
|
||||
*/
|
||||
private Map<String, Object> content;
|
||||
|
||||
public static ServerActionPayloadDTO build(ServerActionEnum action, Map<String, Object> content) {
|
||||
ServerActionPayloadDTO serverActionPayloadDTO = new ServerActionPayloadDTO();
|
||||
serverActionPayloadDTO.setAction(action);
|
||||
serverActionPayloadDTO.setContent(content);
|
||||
serverActionPayloadDTO.setType("server");
|
||||
return serverActionPayloadDTO;
|
||||
}
|
||||
// 私有化
|
||||
private ServerActionPayloadDTO() {}
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
package xiaozhi.modules.sys.dto;
|
||||
|
||||
import lombok.Data;
|
||||
import xiaozhi.modules.sys.enums.ServerActionResponseEnum;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* 服务端动作响应体
|
||||
*/
|
||||
@Data
|
||||
public class ServerActionResponseDTO
|
||||
{
|
||||
private ServerActionResponseEnum status;
|
||||
private String message;
|
||||
private String type;
|
||||
private Map<String, Object> content; // 后续这个字段可以移除,并把这个类作为基类,针对业务写自己的content类型
|
||||
public static final String DEFAULT_TYPE_FORM_SERVER = "server";
|
||||
|
||||
public static Boolean isSuccess(ServerActionResponseDTO actionResponseDTO) {
|
||||
System.out.println(actionResponseDTO);
|
||||
if (actionResponseDTO == null) {
|
||||
return false;
|
||||
}
|
||||
if (actionResponseDTO.getStatus() == null || !actionResponseDTO.getStatus().equals(ServerActionResponseEnum.SUCCESS)) {
|
||||
return false;
|
||||
}
|
||||
Object actionType = actionResponseDTO.getContent().get("action");
|
||||
if (actionType == null) {
|
||||
return false;
|
||||
}
|
||||
return actionResponseDTO.getType() != null && actionResponseDTO.getType().equals(DEFAULT_TYPE_FORM_SERVER);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
package xiaozhi.modules.sys.enums;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonCreator;
|
||||
import com.fasterxml.jackson.annotation.JsonValue;
|
||||
import xiaozhi.common.exception.RenException;
|
||||
|
||||
/**
|
||||
* 服务端动作枚举
|
||||
*
|
||||
* @Author CAIXYPROMISE
|
||||
* @since 2025/5/15 4:57
|
||||
*/
|
||||
public enum ServerActionEnum
|
||||
{
|
||||
RESTART("restart"),
|
||||
UPDATE_CONFIG("update_config");
|
||||
|
||||
private final String value;
|
||||
|
||||
ServerActionEnum(String value)
|
||||
{
|
||||
this.value = value;
|
||||
}
|
||||
|
||||
@JsonValue
|
||||
public String getValue()
|
||||
{
|
||||
return value;
|
||||
}
|
||||
|
||||
@JsonCreator
|
||||
public static ServerActionEnum fromValue(String value)
|
||||
{
|
||||
for (ServerActionEnum action : ServerActionEnum.values())
|
||||
{
|
||||
if (action.value.equalsIgnoreCase(value))
|
||||
{
|
||||
return action;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
+49
@@ -0,0 +1,49 @@
|
||||
package xiaozhi.modules.sys.enums;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonCreator;
|
||||
import com.fasterxml.jackson.annotation.JsonValue;
|
||||
import lombok.Getter;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
|
||||
/**
|
||||
* 服务端调用响应枚举
|
||||
*
|
||||
* @Author CAIXYPROMISE
|
||||
* @since 2025/5/15 5:50
|
||||
*/
|
||||
public enum ServerActionResponseEnum
|
||||
{
|
||||
SUCCESS("success"), FAIL("fail");
|
||||
private final String value;
|
||||
|
||||
ServerActionResponseEnum(String value) {
|
||||
this.value = value;
|
||||
}
|
||||
|
||||
@JsonValue
|
||||
public String getValue()
|
||||
{
|
||||
return value;
|
||||
}
|
||||
|
||||
@JsonCreator
|
||||
public static ServerActionResponseEnum fromValue(String value) {
|
||||
ServerActionResponseEnum byValue = getByValue(value);
|
||||
if (byValue == null) {
|
||||
throw new IllegalArgumentException("Unknown enum value: " + value);
|
||||
}
|
||||
return byValue;
|
||||
}
|
||||
|
||||
public static ServerActionResponseEnum getByValue(String value) {
|
||||
if (StringUtils.isBlank(value)) {
|
||||
return null;
|
||||
}
|
||||
for (ServerActionResponseEnum action : ServerActionResponseEnum.values()) {
|
||||
if (action.value.equals(value)) {
|
||||
return action;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,315 @@
|
||||
package xiaozhi.modules.sys.utils;
|
||||
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.util.StopWatch;
|
||||
import org.springframework.web.socket.*;
|
||||
import org.springframework.web.socket.client.standard.StandardWebSocketClient;
|
||||
import org.springframework.web.socket.handler.AbstractWebSocketHandler;
|
||||
import xiaozhi.common.utils.DateUtils;
|
||||
|
||||
import java.io.Closeable;
|
||||
import java.io.IOException;
|
||||
import java.net.URI;
|
||||
import java.nio.ByteBuffer;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
import java.util.concurrent.*;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
import java.util.function.Consumer;
|
||||
import java.util.function.Predicate;
|
||||
|
||||
/**
|
||||
* WebSocketClientResource:支持 try-with-resources 模式
|
||||
*/
|
||||
@Slf4j
|
||||
public class WebSocketClientManager implements Closeable
|
||||
{
|
||||
private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper();
|
||||
|
||||
// 全局回调线程池
|
||||
private static final ExecutorService CALLBACK_EXECUTOR = Executors.newFixedThreadPool(Runtime.getRuntime().availableProcessors(), new ThreadFactory() {
|
||||
private final AtomicInteger cnt = new AtomicInteger();
|
||||
|
||||
public Thread newThread(Runnable r)
|
||||
{
|
||||
Thread t = new Thread(r, "ws-callback-" + cnt.getAndIncrement());
|
||||
t.setDaemon(true);
|
||||
return t;
|
||||
}
|
||||
});
|
||||
|
||||
private volatile WebSocketSession session;
|
||||
private final BlockingQueue<String> textMessageQueue;
|
||||
private final BlockingQueue<byte[]> binaryMessageQueue;
|
||||
private final CompletableFuture<Void> errorFuture;
|
||||
private final long maxSessionDuration;
|
||||
private final TimeUnit maxSessionDurationUnit;
|
||||
|
||||
private volatile Consumer<String> onText;
|
||||
private volatile Consumer<byte[]> onBinary;
|
||||
private volatile Consumer<Throwable> onError;
|
||||
|
||||
private final String uri;
|
||||
private final WebSocketHttpHeaders headers;
|
||||
private final long connectTimeout;
|
||||
private final TimeUnit connectUnit;
|
||||
private final int queueCapacity;
|
||||
|
||||
// 私有构造,仅由 Builder 调用
|
||||
private WebSocketClientManager(Builder b) {
|
||||
this.uri = b.uri;
|
||||
this.headers = b.headers != null ? b.headers : new WebSocketHttpHeaders();
|
||||
this.connectTimeout = b.connectTimeout;
|
||||
this.connectUnit = b.connectUnit;
|
||||
this.maxSessionDuration = b.maxSessionDuration;
|
||||
this.maxSessionDurationUnit = b.maxSessionDurationUnit;
|
||||
this.queueCapacity = b.queueCapacity;
|
||||
this.textMessageQueue = new LinkedBlockingQueue<>(queueCapacity);
|
||||
this.binaryMessageQueue = new LinkedBlockingQueue<>(queueCapacity);
|
||||
this.errorFuture = new CompletableFuture<>();
|
||||
}
|
||||
|
||||
public static WebSocketClientManager build(Builder b) throws InterruptedException, ExecutionException, TimeoutException, IOException {
|
||||
WebSocketClientManager ws = new WebSocketClientManager(b);
|
||||
StandardWebSocketClient client = new StandardWebSocketClient();
|
||||
CompletableFuture<WebSocketSession> future = client.execute(ws.new InternalHandler(b.uri), b.headers, URI.create(b.uri));
|
||||
WebSocketSession sess = future.get(b.connectTimeout, b.connectUnit);
|
||||
if (sess == null || !sess.isOpen())
|
||||
{
|
||||
throw new IOException("握手失败或会话未打开");
|
||||
}
|
||||
ws.session = sess;
|
||||
return ws;
|
||||
}
|
||||
|
||||
/**
|
||||
* 发送 Text
|
||||
*/
|
||||
public void sendText(String text) throws IOException {
|
||||
session.sendMessage(new TextMessage(text));
|
||||
}
|
||||
|
||||
public void sendBinary(byte[] data) throws IOException {
|
||||
session.sendMessage(new BinaryMessage(data));
|
||||
}
|
||||
|
||||
public void sendJson(Object payload) throws IOException {
|
||||
String json = OBJECT_MAPPER.writeValueAsString(payload);
|
||||
session.sendMessage(new TextMessage(json));
|
||||
}
|
||||
|
||||
|
||||
|
||||
private <T> List<T> listenerCustom(
|
||||
BlockingQueue<T> queue,
|
||||
Predicate<T> predicate)
|
||||
throws InterruptedException, TimeoutException, ExecutionException
|
||||
{
|
||||
List<T> collected = new ArrayList<>();
|
||||
long deadline = System.currentTimeMillis() + maxSessionDurationUnit.toMillis(maxSessionDuration);
|
||||
|
||||
while (true) {
|
||||
if (errorFuture.isDone()) {
|
||||
errorFuture.get();
|
||||
}
|
||||
|
||||
long remaining = deadline - System.currentTimeMillis();
|
||||
if (remaining <= 0) {
|
||||
throw new TimeoutException("等待批量消息超时");
|
||||
}
|
||||
|
||||
T msg = queue.poll(remaining, TimeUnit.MILLISECONDS);
|
||||
if (msg == null) {
|
||||
throw new TimeoutException("等待批量消息超时");
|
||||
}
|
||||
|
||||
collected.add(msg);
|
||||
if (predicate.test(msg)) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
close();
|
||||
return collected;
|
||||
}
|
||||
|
||||
/**
|
||||
* 同步接收多条消息,直到 predicate 为 true 或超时抛异常;
|
||||
* @return 返回监听期间的所有消息列表
|
||||
*/
|
||||
public List<String> listener(Predicate<String> predicate)
|
||||
throws InterruptedException, TimeoutException, ExecutionException
|
||||
{
|
||||
return listenerCustom(textMessageQueue, predicate);
|
||||
}
|
||||
|
||||
public List<byte[]> listenerBinary(Predicate<byte[]> predicate)
|
||||
throws InterruptedException, TimeoutException, ExecutionException
|
||||
{
|
||||
return listenerCustom(binaryMessageQueue, predicate);
|
||||
}
|
||||
|
||||
/**
|
||||
* 注册文本回调
|
||||
*/
|
||||
public WebSocketClientManager onText(Consumer<String> c) {
|
||||
this.onText = c;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* 注册二进制回调
|
||||
*/
|
||||
public WebSocketClientManager onBinary(Consumer<byte[]> c) {
|
||||
this.onBinary = c;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* 注册错误回调
|
||||
*/
|
||||
public WebSocketClientManager onError(Consumer<Throwable> c) {
|
||||
this.onError = c;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* 关闭会话,try-with-resources / finally 自动调用
|
||||
*/
|
||||
@Override
|
||||
public void close() {
|
||||
try {
|
||||
if (session != null && session.isOpen()) {
|
||||
session.close(CloseStatus.NORMAL);
|
||||
}
|
||||
}
|
||||
catch (IOException ignored) {}
|
||||
textMessageQueue.clear();
|
||||
binaryMessageQueue.clear();
|
||||
errorFuture.completeExceptionally(new IOException("WebSocket 已关闭"));
|
||||
}
|
||||
|
||||
private class InternalHandler extends AbstractWebSocketHandler {
|
||||
private final String targetUri;
|
||||
private final StopWatch stopWatch;
|
||||
|
||||
InternalHandler(String targetUri) {
|
||||
this.targetUri = targetUri;
|
||||
this.stopWatch = new StopWatch();
|
||||
}
|
||||
|
||||
/**
|
||||
* 连接建立时回调
|
||||
*/
|
||||
@Override
|
||||
public void afterConnectionEstablished(WebSocketSession session) {
|
||||
// 保存会话
|
||||
WebSocketClientManager.this.session = session;
|
||||
this.stopWatch.start();
|
||||
log.info("ws连接成功, 目标URI: {}, 连接时间: {}", targetUri, DateUtils.getDateTimeNow(DateUtils.DATE_TIME_MILLIS_PATTERN));
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理文本消息
|
||||
*/
|
||||
@Override
|
||||
protected void handleTextMessage(WebSocketSession session, TextMessage message) throws Exception {
|
||||
String payload = message.getPayload();
|
||||
// 入队
|
||||
textMessageQueue.offer(payload);
|
||||
// 回调用户注册的 onText
|
||||
if (onText != null) {
|
||||
CALLBACK_EXECUTOR.submit(() -> onText.accept(payload));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理二进制消息
|
||||
*/
|
||||
@Override
|
||||
protected void handleBinaryMessage(WebSocketSession session, BinaryMessage message) throws Exception {
|
||||
ByteBuffer buf = message.getPayload();
|
||||
byte[] data = new byte[buf.remaining()];
|
||||
buf.get(data);
|
||||
// 入队
|
||||
binaryMessageQueue.offer(data);
|
||||
// 回调用户注册的 onBinary
|
||||
if (onBinary != null) {
|
||||
CALLBACK_EXECUTOR.submit(() -> onBinary.accept(data));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 传输错误时回调
|
||||
*/
|
||||
@Override
|
||||
public void handleTransportError(WebSocketSession session, Throwable exception) throws Exception {
|
||||
super.handleTransportError(session, exception);
|
||||
// 保持原有逻辑:完成 errorFuture、回调 onError、关闭会话、异步通知连接失败
|
||||
errorFuture.completeExceptionally(exception);
|
||||
if (onError != null) {
|
||||
CALLBACK_EXECUTOR.submit(() -> onError.accept(exception));
|
||||
}
|
||||
session.close(CloseStatus.SERVER_ERROR);
|
||||
}
|
||||
|
||||
/**
|
||||
* 连接关闭时回调
|
||||
*/
|
||||
@Override
|
||||
public void afterConnectionClosed(WebSocketSession session, CloseStatus status) throws Exception {
|
||||
super.afterConnectionClosed(session, status);
|
||||
if (stopWatch.isRunning()) {
|
||||
stopWatch.stop();
|
||||
}
|
||||
log.info("ws连接关闭, 目标URI: {}, 关闭时间: {}, 连接总时长: {}s",
|
||||
targetUri, DateUtils.getDateTimeNow(DateUtils.DATE_TIME_MILLIS_PATTERN), DateUtils.millsToSecond(stopWatch.getTotalTimeMillis()));
|
||||
}
|
||||
}
|
||||
|
||||
public static class Builder {
|
||||
private String uri; // 目标 WS URI
|
||||
private long connectTimeout = 3; // 请求连接等待时间
|
||||
private TimeUnit connectUnit = TimeUnit.SECONDS; // 请求连接等待时间单位
|
||||
private long maxSessionDuration = 5; // 最大连线时间,默认5秒
|
||||
private TimeUnit maxSessionDurationUnit = TimeUnit.SECONDS; // 最大连线时间单位
|
||||
private int queueCapacity = 100; // 消息队列容量
|
||||
private WebSocketHttpHeaders headers; // 请求头
|
||||
|
||||
/**
|
||||
* 目标 WS URI
|
||||
*/
|
||||
public Builder uri(String uri) {
|
||||
this.uri = Objects.requireNonNull(uri);
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder headers(WebSocketHttpHeaders h) {
|
||||
this.headers = h;
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder connectTimeout(long t, TimeUnit u) {
|
||||
this.connectTimeout = t;
|
||||
this.connectUnit = u;
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder maxSessionDuration(long t, TimeUnit u) {
|
||||
this.maxSessionDuration = t;
|
||||
this.maxSessionDurationUnit = u;
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder queueCapacity(int c) {
|
||||
this.queueCapacity = c;
|
||||
return this;
|
||||
}
|
||||
|
||||
public WebSocketClientManager build() throws InterruptedException, ExecutionException, TimeoutException, IOException {
|
||||
return WebSocketClientManager.build(this);
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
@@ -130,5 +130,37 @@ export default {
|
||||
})
|
||||
}).send()
|
||||
},
|
||||
// 获取ws服务端列表
|
||||
getWsServerList(params, callback) {
|
||||
RequestService.sendRequest()
|
||||
.url(`${getServiceUrl()}/admin/server/server-list`)
|
||||
.method('GET')
|
||||
.success((res) => {
|
||||
RequestService.clearRequestTime()
|
||||
callback(res)
|
||||
})
|
||||
.fail((err) => {
|
||||
console.error('获取ws服务端列表失败:', err)
|
||||
RequestService.reAjaxFun(() => {
|
||||
this.getWsServerList(params, callback)
|
||||
})
|
||||
}).send();
|
||||
},
|
||||
// 发送ws服务器动作指令
|
||||
sendWsServerAction(data, callback) {
|
||||
RequestService.sendRequest()
|
||||
.url(`${getServiceUrl()}/admin/server/emit-action`)
|
||||
.method('POST')
|
||||
.data(data)
|
||||
.success((res) => {
|
||||
RequestService.clearRequestTime()
|
||||
callback(res)
|
||||
})
|
||||
.fail((err) => {
|
||||
RequestService.reAjaxFun(() => {
|
||||
this.sendWsServerAction(data, callback)
|
||||
})
|
||||
}).send();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -52,6 +52,9 @@
|
||||
<el-dropdown-item @click.native="goProviderManagement">
|
||||
供应器管理
|
||||
</el-dropdown-item>
|
||||
<el-dropdown-item @click.native="goServerSideManagement">
|
||||
服务端管理
|
||||
</el-dropdown-item>
|
||||
</el-dropdown-menu>
|
||||
</el-dropdown>
|
||||
</div>
|
||||
@@ -145,6 +148,9 @@ export default {
|
||||
goProviderManagement() {
|
||||
this.$router.push('/provider-management')
|
||||
},
|
||||
goServerSideManagement() {
|
||||
this.$router.push('/server-side-management')
|
||||
},
|
||||
// 获取用户信息
|
||||
fetchUserInfo() {
|
||||
userApi.getUserInfo(({ data }) => {
|
||||
|
||||
@@ -80,6 +80,18 @@ const routes = [
|
||||
title: '参数管理'
|
||||
}
|
||||
},
|
||||
|
||||
{
|
||||
path: '/server-side-management',
|
||||
name: 'ServerSideManager',
|
||||
component: function () {
|
||||
return import('../views/ServerSideManager.vue')
|
||||
},
|
||||
meta: {
|
||||
requiresAuth: true,
|
||||
title: '服务端管理'
|
||||
}
|
||||
},
|
||||
{
|
||||
path: '/ota-management',
|
||||
name: 'OtaManagement',
|
||||
|
||||
@@ -0,0 +1,597 @@
|
||||
<template>
|
||||
<div class="welcome">
|
||||
<HeaderBar/>
|
||||
|
||||
<div class="operation-bar">
|
||||
<h2 class="page-title">服务端管理</h2>
|
||||
<div class="right-operations">
|
||||
<el-input placeholder="请输入服务端ws地址查询" v-model="searchCode" 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" v-loading="loading"
|
||||
element-loading-text="拼命加载中" element-loading-spinner="el-icon-loading"
|
||||
element-loading-background="rgba(255, 255, 255, 0.7)"
|
||||
:header-cell-class-name="headerCellClassName">
|
||||
<el-table-column label="选择" align="center" width="120">
|
||||
<template slot-scope="scope">
|
||||
<el-checkbox v-model="scope.row.selected"></el-checkbox>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="ws地址" prop="address" align="center"></el-table-column>
|
||||
<el-table-column label="操作" prop="operator" align="center" show-overflow-tooltip>
|
||||
<template slot-scope="scope">
|
||||
<el-button size="medium" type="text" @click="emitAction(scope.row, actionMap.restart)">重启</el-button>
|
||||
<el-button size="medium" type="text" @click="emitAction(scope.row, actionMap.update_config)">更新配置</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
|
||||
<div class="table_bottom">
|
||||
<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>
|
||||
|
||||
|
||||
<el-footer>
|
||||
<version-footer/>
|
||||
</el-footer>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import Api from "@/apis/api";
|
||||
import HeaderBar from "@/components/HeaderBar.vue";
|
||||
import ParamDialog from "@/components/ParamDialog.vue";
|
||||
import VersionFooter from "@/components/VersionFooter.vue";
|
||||
|
||||
export default {
|
||||
components: {HeaderBar, ParamDialog, VersionFooter},
|
||||
data() {
|
||||
return {
|
||||
searchCode: "",
|
||||
paramsList: [],
|
||||
actionMap: {
|
||||
restart: {
|
||||
value: 'restart',
|
||||
title: "重启服务端",
|
||||
message: "确定要重启服务端吗?",
|
||||
confirmText: "重启",
|
||||
},
|
||||
update_config: {
|
||||
value: 'update_config',
|
||||
title: "更新配置",
|
||||
message: "确定要更新配置吗?",
|
||||
confirmText: "更新",
|
||||
}
|
||||
},
|
||||
currentPage: 1,
|
||||
loading: false,
|
||||
pageSize: 10,
|
||||
pageSizeOptions: [10, 20, 50, 100],
|
||||
total: 0,
|
||||
dialogVisible: false,
|
||||
dialogTitle: "新增参数",
|
||||
isAllSelected: false,
|
||||
sensitive_keys: ["api_key", "personal_access_token", "access_token", "token", "secret", "access_key_secret", "secret_key"],
|
||||
paramForm: {
|
||||
id: null,
|
||||
paramCode: "",
|
||||
paramValue: "",
|
||||
remark: ""
|
||||
},
|
||||
};
|
||||
},
|
||||
created() {
|
||||
this.fetchParams();
|
||||
},
|
||||
|
||||
computed: {
|
||||
pageCount() {
|
||||
return Math.ceil(this.total / this.pageSize);
|
||||
},
|
||||
visiblePages() {
|
||||
const pages = [];
|
||||
const maxVisible = 3;
|
||||
let start = Math.max(1, this.currentPage - 1);
|
||||
let end = Math.min(this.pageCount, start + maxVisible - 1);
|
||||
|
||||
if (end - start + 1 < maxVisible) {
|
||||
start = Math.max(1, end - maxVisible + 1);
|
||||
}
|
||||
|
||||
for (let i = start; i <= end; i++) {
|
||||
pages.push(i);
|
||||
}
|
||||
return pages;
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
handlePageSizeChange(val) {
|
||||
this.pageSize = val;
|
||||
this.currentPage = 1;
|
||||
this.fetchParams();
|
||||
},
|
||||
fetchParams() {
|
||||
this.loading = true;
|
||||
Api.admin.getWsServerList(
|
||||
{},
|
||||
({data}) => {
|
||||
this.loading = false;
|
||||
if (data.code === 0) {
|
||||
this.paramsList = data.data.map(item => ({address: item}));
|
||||
this.total = data.data.length;
|
||||
} else {
|
||||
this.$message.error({
|
||||
message: data.msg || '获取参数列表失败',
|
||||
showClose: true
|
||||
});
|
||||
}
|
||||
}
|
||||
);
|
||||
},
|
||||
handleSearch() {
|
||||
this.currentPage = 1;
|
||||
this.fetchParams();
|
||||
},
|
||||
emitAction(rowItem, actionItem) {
|
||||
if (actionItem === undefined || rowItem.address === undefined) {
|
||||
return;
|
||||
}
|
||||
// 弹开询问框
|
||||
this.$confirm(actionItem.message, actionItem.title, {
|
||||
confirmButtonText: actionItem.confirmText, // 确认按钮文本
|
||||
}).then(() => {
|
||||
// 用户点击了确认按钮
|
||||
Api.admin.sendWsServerAction({
|
||||
targetWs: rowItem.address,
|
||||
action: actionItem.value
|
||||
}, ({data}) => {
|
||||
if (data.code !== 0) {
|
||||
this.$message.error({
|
||||
message: data.msg || '操作失败',
|
||||
showClose: true
|
||||
});
|
||||
return;
|
||||
}
|
||||
this.$message.success({
|
||||
message: `${actionItem.title}成功`,
|
||||
showClose: true
|
||||
})
|
||||
})
|
||||
})
|
||||
},
|
||||
headerCellClassName({columnIndex}) {
|
||||
if (columnIndex === 0) {
|
||||
return "custom-selection-header";
|
||||
}
|
||||
return "";
|
||||
},
|
||||
goFirst() {
|
||||
this.currentPage = 1;
|
||||
this.fetchParams();
|
||||
},
|
||||
goPrev() {
|
||||
if (this.currentPage > 1) {
|
||||
this.currentPage--;
|
||||
this.fetchParams();
|
||||
}
|
||||
},
|
||||
goNext() {
|
||||
if (this.currentPage < this.pageCount) {
|
||||
this.currentPage++;
|
||||
this.fetchParams();
|
||||
}
|
||||
},
|
||||
goToPage(page) {
|
||||
this.currentPage = page;
|
||||
this.fetchParams();
|
||||
}
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.welcome {
|
||||
min-width: 900px;
|
||||
min-height: 506px;
|
||||
height: 100vh;
|
||||
display: flex;
|
||||
position: relative;
|
||||
flex-direction: column;
|
||||
background-size: cover;
|
||||
background: linear-gradient(to bottom right, #dce8ff, #e4eeff, #e6cbfd) center;
|
||||
-webkit-background-size: cover;
|
||||
-o-background-size: cover;
|
||||
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;
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
border: none;
|
||||
box-shadow: none;
|
||||
overflow: hidden;
|
||||
|
||||
::v-deep .el-card__body {
|
||||
padding: 15px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex: 1;
|
||||
overflow: hidden;
|
||||
}
|
||||
}
|
||||
|
||||
.table_bottom {
|
||||
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 {
|
||||
color: #909399;
|
||||
font-size: 14px;
|
||||
margin-left: 10px;
|
||||
}
|
||||
}
|
||||
|
||||
:deep(.transparent-table) {
|
||||
background: white;
|
||||
flex: 1;
|
||||
width: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
.el-table__body-wrapper {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
max-height: none !important;
|
||||
}
|
||||
|
||||
.el-table__header-wrapper {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.el-table__header th {
|
||||
background: white !important;
|
||||
color: black;
|
||||
}
|
||||
|
||||
&::before {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.el-table__body tr {
|
||||
background-color: white;
|
||||
|
||||
td {
|
||||
border-top: 1px solid rgba(0, 0, 0, 0.04);
|
||||
border-bottom: 1px solid rgba(0, 0, 0, 0.04);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
:deep(.el-checkbox__inner) {
|
||||
background-color: #eeeeee !important;
|
||||
border-color: #cccccc !important;
|
||||
}
|
||||
|
||||
:deep(.el-checkbox__inner:hover) {
|
||||
border-color: #cccccc !important;
|
||||
}
|
||||
|
||||
:deep(.el-checkbox__input.is-checked .el-checkbox__inner) {
|
||||
background-color: #5f70f3 !important;
|
||||
border-color: #5f70f3 !important;
|
||||
}
|
||||
|
||||
@media (min-width: 1144px) {
|
||||
.table_bottom {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-top: 40px;
|
||||
}
|
||||
|
||||
:deep(.transparent-table) {
|
||||
.el-table__body tr {
|
||||
td {
|
||||
padding-top: 16px;
|
||||
padding-bottom: 16px;
|
||||
}
|
||||
|
||||
& + tr {
|
||||
margin-top: 10px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
:deep(.el-table .el-button--text) {
|
||||
color: #7079aa;
|
||||
}
|
||||
|
||||
:deep(.el-table .el-button--text:hover) {
|
||||
color: #5a64b5;
|
||||
}
|
||||
|
||||
.el-button--success {
|
||||
background: #5bc98c;
|
||||
color: white;
|
||||
}
|
||||
|
||||
:deep(.el-table .cell) {
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.page-size-select {
|
||||
width: 100px;
|
||||
margin-right: 10px;
|
||||
|
||||
:deep(.el-input__inner) {
|
||||
height: 32px;
|
||||
line-height: 32px;
|
||||
border-radius: 4px;
|
||||
border: 1px solid #e4e7ed;
|
||||
background: #dee7ff;
|
||||
color: #606266;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
:deep(.el-input__suffix) {
|
||||
right: 6px;
|
||||
width: 15px;
|
||||
height: 20px;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
top: 6px;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
:deep(.el-input__suffix-inner) {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
:deep(.el-icon-arrow-up:before) {
|
||||
content: "";
|
||||
display: inline-block;
|
||||
border-left: 6px solid transparent;
|
||||
border-right: 6px solid transparent;
|
||||
border-top: 9px solid #606266;
|
||||
position: relative;
|
||||
transform: rotate(0deg);
|
||||
transition: transform 0.3s;
|
||||
}
|
||||
}
|
||||
|
||||
:deep(.el-table) {
|
||||
.el-table__body-wrapper {
|
||||
transition: height 0.3s ease;
|
||||
}
|
||||
}
|
||||
|
||||
.el-table {
|
||||
--table-max-height: calc(100vh - 40vh);
|
||||
max-height: var(--table-max-height);
|
||||
|
||||
.el-table__body-wrapper {
|
||||
max-height: calc(var(--table-max-height) - 40px);
|
||||
}
|
||||
}
|
||||
|
||||
:deep(.el-loading-mask) {
|
||||
background-color: rgba(255, 255, 255, 0.6) !important;
|
||||
backdrop-filter: blur(2px);
|
||||
}
|
||||
|
||||
:deep(.el-loading-spinner .circular) {
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
}
|
||||
|
||||
:deep(.el-loading-spinner .path) {
|
||||
stroke: #6b8cff;
|
||||
}
|
||||
|
||||
:deep(.el-loading-text) {
|
||||
color: #6b8cff !important;
|
||||
font-size: 14px;
|
||||
margin-top: 8px;
|
||||
}
|
||||
</style>
|
||||
@@ -273,9 +273,10 @@ class ConnectionHandler:
|
||||
await self.websocket.send(
|
||||
json.dumps(
|
||||
{
|
||||
"type": "server_response",
|
||||
"type": "server",
|
||||
"status": "success",
|
||||
"message": "服务器重启中...",
|
||||
"content": {'action': "restart"}
|
||||
}
|
||||
)
|
||||
)
|
||||
@@ -302,9 +303,10 @@ class ConnectionHandler:
|
||||
await self.websocket.send(
|
||||
json.dumps(
|
||||
{
|
||||
"type": "server_response",
|
||||
"type": "server",
|
||||
"status": "error",
|
||||
"message": f"Restart failed: {str(e)}",
|
||||
'content': {'action': "restart"}
|
||||
}
|
||||
)
|
||||
)
|
||||
|
||||
@@ -95,9 +95,10 @@ async def handleTextMessage(conn, message):
|
||||
await conn.websocket.send(
|
||||
json.dumps(
|
||||
{
|
||||
"type": "config_update_response",
|
||||
"type": "server",
|
||||
"status": "error",
|
||||
"message": "无法获取服务器实例",
|
||||
"content": {"action": "update_config"}
|
||||
}
|
||||
)
|
||||
)
|
||||
@@ -107,9 +108,10 @@ async def handleTextMessage(conn, message):
|
||||
await conn.websocket.send(
|
||||
json.dumps(
|
||||
{
|
||||
"type": "config_update_response",
|
||||
"type": "server",
|
||||
"status": "error",
|
||||
"message": "更新服务器配置失败",
|
||||
"content": {"action": "update_config"}
|
||||
}
|
||||
)
|
||||
)
|
||||
@@ -119,9 +121,10 @@ async def handleTextMessage(conn, message):
|
||||
await conn.websocket.send(
|
||||
json.dumps(
|
||||
{
|
||||
"type": "config_update_response",
|
||||
"type": "server",
|
||||
"status": "success",
|
||||
"message": "配置更新成功",
|
||||
"content": {"action": "update_config"}
|
||||
}
|
||||
)
|
||||
)
|
||||
@@ -130,9 +133,10 @@ async def handleTextMessage(conn, message):
|
||||
await conn.websocket.send(
|
||||
json.dumps(
|
||||
{
|
||||
"type": "config_update_response",
|
||||
"type": "server",
|
||||
"status": "error",
|
||||
"message": f"更新配置失败: {str(e)}",
|
||||
"content": {"action": "update_config"}
|
||||
}
|
||||
)
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user