Merge branch 'master' of https://gitee.com/zhijiantianya/ruoyi-vue-pro into feature/wechat-mp
# Conflicts: # yudao-server/src/main/resources/application.yamlplp
commit
b9246d1543
@ -0,0 +1,37 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project xmlns="http://maven.apache.org/POM/4.0.0"
|
||||||
|
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||||
|
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||||
|
<parent>
|
||||||
|
<groupId>cn.iocoder.boot</groupId>
|
||||||
|
<artifactId>yudao-framework</artifactId>
|
||||||
|
<version>${revision}</version>
|
||||||
|
</parent>
|
||||||
|
<modelVersion>4.0.0</modelVersion>
|
||||||
|
<artifactId>yudao-spring-boot-starter-websocket</artifactId>
|
||||||
|
<packaging>jar</packaging>
|
||||||
|
|
||||||
|
<name>${project.artifactId}</name>
|
||||||
|
<description>WebSocket</description>
|
||||||
|
<url>https://github.com/YunaiV/ruoyi-vue-pro</url>
|
||||||
|
|
||||||
|
|
||||||
|
<dependencies>
|
||||||
|
|
||||||
|
<dependency>
|
||||||
|
<groupId>cn.iocoder.boot</groupId>
|
||||||
|
<artifactId>yudao-common</artifactId>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
|
<dependency>
|
||||||
|
<groupId>cn.iocoder.boot</groupId>
|
||||||
|
<artifactId>yudao-spring-boot-starter-security</artifactId>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.springframework.boot</groupId>
|
||||||
|
<artifactId>spring-boot-starter-websocket</artifactId>
|
||||||
|
</dependency>
|
||||||
|
</dependencies>
|
||||||
|
|
||||||
|
</project>
|
||||||
@ -0,0 +1,14 @@
|
|||||||
|
package cn.iocoder.yudao.framework.websocket.config;
|
||||||
|
|
||||||
|
import cn.iocoder.yudao.framework.websocket.core.UserHandshakeInterceptor;
|
||||||
|
import org.springframework.boot.context.properties.EnableConfigurationProperties;
|
||||||
|
import org.springframework.context.annotation.Bean;
|
||||||
|
import org.springframework.web.socket.server.HandshakeInterceptor;
|
||||||
|
|
||||||
|
@EnableConfigurationProperties(WebSocketProperties.class)
|
||||||
|
public class WebSocketHandlerConfig {
|
||||||
|
@Bean
|
||||||
|
public HandshakeInterceptor handshakeInterceptor() {
|
||||||
|
return new UserHandshakeInterceptor();
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,29 @@
|
|||||||
|
package cn.iocoder.yudao.framework.websocket.config;
|
||||||
|
|
||||||
|
import lombok.Data;
|
||||||
|
import org.springframework.boot.context.properties.ConfigurationProperties;
|
||||||
|
import org.springframework.validation.annotation.Validated;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* WebSocket 配置项
|
||||||
|
*
|
||||||
|
* @author xingyu4j
|
||||||
|
*/
|
||||||
|
@ConfigurationProperties("yudao.websocket")
|
||||||
|
@Data
|
||||||
|
@Validated
|
||||||
|
public class WebSocketProperties {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 路径
|
||||||
|
*/
|
||||||
|
private String path = "";
|
||||||
|
/**
|
||||||
|
* 默认最多允许同时在线用户数
|
||||||
|
*/
|
||||||
|
private int maxOnlineCount = 0;
|
||||||
|
/**
|
||||||
|
* 是否保存session
|
||||||
|
*/
|
||||||
|
private boolean sessionMap = true;
|
||||||
|
}
|
||||||
@ -0,0 +1,34 @@
|
|||||||
|
package cn.iocoder.yudao.framework.websocket.config;
|
||||||
|
|
||||||
|
import org.springframework.boot.autoconfigure.AutoConfiguration;
|
||||||
|
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
|
||||||
|
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
|
||||||
|
import org.springframework.boot.context.properties.EnableConfigurationProperties;
|
||||||
|
import org.springframework.context.annotation.Bean;
|
||||||
|
import org.springframework.web.socket.WebSocketHandler;
|
||||||
|
import org.springframework.web.socket.config.annotation.WebSocketConfigurer;
|
||||||
|
import org.springframework.web.socket.server.HandshakeInterceptor;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* WebSocket 自动配置
|
||||||
|
*
|
||||||
|
* @author xingyu4j
|
||||||
|
*/
|
||||||
|
@AutoConfiguration
|
||||||
|
// 允许使用 yudao.websocket.enable=false 禁用websocket
|
||||||
|
@ConditionalOnProperty(prefix = "yudao.websocket", value = "enable", matchIfMissing = true)
|
||||||
|
@EnableConfigurationProperties(WebSocketProperties.class)
|
||||||
|
public class YudaoWebSocketAutoConfiguration {
|
||||||
|
@Bean
|
||||||
|
@ConditionalOnMissingBean
|
||||||
|
public WebSocketConfigurer webSocketConfigurer(List<HandshakeInterceptor> handshakeInterceptor,
|
||||||
|
WebSocketHandler webSocketHandler,
|
||||||
|
WebSocketProperties webSocketProperties) {
|
||||||
|
|
||||||
|
return registry -> registry
|
||||||
|
.addHandler(webSocketHandler, webSocketProperties.getPath())
|
||||||
|
.addInterceptors(handshakeInterceptor.toArray(new HandshakeInterceptor[0]));
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,24 @@
|
|||||||
|
package cn.iocoder.yudao.framework.websocket.core;
|
||||||
|
|
||||||
|
import cn.iocoder.yudao.framework.security.core.LoginUser;
|
||||||
|
import cn.iocoder.yudao.framework.security.core.util.SecurityFrameworkUtils;
|
||||||
|
import org.springframework.http.server.ServerHttpRequest;
|
||||||
|
import org.springframework.http.server.ServerHttpResponse;
|
||||||
|
import org.springframework.web.socket.WebSocketHandler;
|
||||||
|
import org.springframework.web.socket.server.HandshakeInterceptor;
|
||||||
|
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
public class UserHandshakeInterceptor implements HandshakeInterceptor {
|
||||||
|
@Override
|
||||||
|
public boolean beforeHandshake(ServerHttpRequest request, ServerHttpResponse response, WebSocketHandler wsHandler, Map<String, Object> attributes) throws Exception {
|
||||||
|
LoginUser loginUser = SecurityFrameworkUtils.getLoginUser();
|
||||||
|
attributes.put(WebSocketKeyDefine.LOGIN_USER, loginUser);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void afterHandshake(ServerHttpRequest request, ServerHttpResponse response, WebSocketHandler wsHandler, Exception exception) {
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,9 @@
|
|||||||
|
package cn.iocoder.yudao.framework.websocket.core;
|
||||||
|
|
||||||
|
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
@Data
|
||||||
|
public class WebSocketKeyDefine {
|
||||||
|
public static final String LOGIN_USER ="LOGIN_USER";
|
||||||
|
}
|
||||||
@ -0,0 +1,24 @@
|
|||||||
|
package cn.iocoder.yudao.framework.websocket.core;
|
||||||
|
|
||||||
|
import lombok.Data;
|
||||||
|
import lombok.experimental.Accessors;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
@Data
|
||||||
|
@Accessors(chain = true)
|
||||||
|
public class WebSocketMessageDO {
|
||||||
|
/**
|
||||||
|
* 接收消息的seesion
|
||||||
|
*/
|
||||||
|
private List<Object> seesionKeyList;
|
||||||
|
/**
|
||||||
|
* 发送消息
|
||||||
|
*/
|
||||||
|
private String msgText;
|
||||||
|
|
||||||
|
public static WebSocketMessageDO build(List<Object> seesionKeyList, String msgText) {
|
||||||
|
return new WebSocketMessageDO().setMsgText(msgText).setSeesionKeyList(seesionKeyList);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@ -0,0 +1,36 @@
|
|||||||
|
package cn.iocoder.yudao.framework.websocket.core;
|
||||||
|
|
||||||
|
import org.springframework.web.socket.WebSocketSession;
|
||||||
|
|
||||||
|
import java.util.Collection;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.Set;
|
||||||
|
import java.util.concurrent.ConcurrentHashMap;
|
||||||
|
|
||||||
|
public final class WebSocketSessionHandler {
|
||||||
|
private WebSocketSessionHandler() {
|
||||||
|
}
|
||||||
|
|
||||||
|
private static final Map<String, WebSocketSession> SESSION_MAP = new ConcurrentHashMap<>();
|
||||||
|
|
||||||
|
public static void addSession(Object sessionKey, WebSocketSession session) {
|
||||||
|
SESSION_MAP.put(sessionKey.toString(), session);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void removeSession(Object sessionKey) {
|
||||||
|
SESSION_MAP.remove(sessionKey.toString());
|
||||||
|
}
|
||||||
|
|
||||||
|
public static WebSocketSession getSession(Object sessionKey) {
|
||||||
|
return SESSION_MAP.get(sessionKey.toString());
|
||||||
|
}
|
||||||
|
|
||||||
|
public static Collection<WebSocketSession> getSessions() {
|
||||||
|
return SESSION_MAP.values();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static Set<String> getSessionKeys() {
|
||||||
|
return SESSION_MAP.keySet();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@ -0,0 +1,31 @@
|
|||||||
|
package cn.iocoder.yudao.framework.websocket.core;
|
||||||
|
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.springframework.web.socket.TextMessage;
|
||||||
|
import org.springframework.web.socket.WebSocketSession;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
|
||||||
|
@Slf4j
|
||||||
|
public class WebSocketUtils {
|
||||||
|
public static boolean sendMessage(WebSocketSession seesion, String message) {
|
||||||
|
if (seesion == null) {
|
||||||
|
log.error("seesion 不存在");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (seesion.isOpen()) {
|
||||||
|
try {
|
||||||
|
seesion.sendMessage(new TextMessage(message));
|
||||||
|
} catch (IOException e) {
|
||||||
|
log.error("WebSocket 消息发送异常 Session={} | msg= {} | exception={}", seesion, message, e);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static boolean sendMessage(Object sessionKey, String message) {
|
||||||
|
WebSocketSession session = WebSocketSessionHandler.getSession(sessionKey);
|
||||||
|
return sendMessage(session, message);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,49 @@
|
|||||||
|
package cn.iocoder.yudao.framework.websocket.core;
|
||||||
|
|
||||||
|
import cn.iocoder.yudao.framework.security.core.LoginUser;
|
||||||
|
import org.springframework.web.socket.CloseStatus;
|
||||||
|
import org.springframework.web.socket.WebSocketHandler;
|
||||||
|
import org.springframework.web.socket.WebSocketSession;
|
||||||
|
import org.springframework.web.socket.handler.WebSocketHandlerDecorator;
|
||||||
|
|
||||||
|
public class YudaoWebSocketHandlerDecorator extends WebSocketHandlerDecorator {
|
||||||
|
public YudaoWebSocketHandlerDecorator(WebSocketHandler delegate) {
|
||||||
|
super(delegate);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* websocket 连接时执行的动作
|
||||||
|
* @param session websocket session 对象
|
||||||
|
* @throws Exception 异常对象
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public void afterConnectionEstablished(final WebSocketSession session) throws Exception {
|
||||||
|
Object sessionKey = sessionKeyGen(session);
|
||||||
|
WebSocketSessionHandler.addSession(sessionKey, session);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* websocket 关闭连接时执行的动作
|
||||||
|
* @param session websocket session 对象
|
||||||
|
* @param closeStatus 关闭状态对象
|
||||||
|
* @throws Exception 异常对象
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public void afterConnectionClosed(final WebSocketSession session, CloseStatus closeStatus) throws Exception {
|
||||||
|
Object sessionKey = sessionKeyGen(session);
|
||||||
|
WebSocketSessionHandler.removeSession(sessionKey);
|
||||||
|
}
|
||||||
|
|
||||||
|
public Object sessionKeyGen(WebSocketSession webSocketSession) {
|
||||||
|
|
||||||
|
Object obj = webSocketSession.getAttributes().get(WebSocketKeyDefine.LOGIN_USER);
|
||||||
|
|
||||||
|
if (obj instanceof LoginUser) {
|
||||||
|
LoginUser loginUser = (LoginUser) obj;
|
||||||
|
// userId 作为唯一区分
|
||||||
|
return String.valueOf(loginUser.getId());
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1 @@
|
|||||||
|
package cn.iocoder.yudao.framework.websocket;
|
||||||
@ -0,0 +1 @@
|
|||||||
|
cn.iocoder.yudao.framework.websocket.config.YudaoWebSocketAutoConfiguration
|
||||||
@ -0,0 +1,45 @@
|
|||||||
|
package cn.iocoder.yudao.module.infra.websocket;
|
||||||
|
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
|
||||||
|
import java.util.concurrent.Semaphore;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 信号量相关处理
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
@Slf4j
|
||||||
|
public class SemaphoreUtils {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取信号量
|
||||||
|
*
|
||||||
|
* @param semaphore
|
||||||
|
* @return
|
||||||
|
*/
|
||||||
|
public static boolean tryAcquire(Semaphore semaphore) {
|
||||||
|
boolean flag = false;
|
||||||
|
|
||||||
|
try {
|
||||||
|
flag = semaphore.tryAcquire();
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("获取信号量异常", e);
|
||||||
|
}
|
||||||
|
|
||||||
|
return flag;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 释放信号量
|
||||||
|
*
|
||||||
|
* @param semaphore
|
||||||
|
*/
|
||||||
|
public static void release(Semaphore semaphore) {
|
||||||
|
|
||||||
|
try {
|
||||||
|
semaphore.release();
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("释放信号量异常", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,16 @@
|
|||||||
|
package cn.iocoder.yudao.module.infra.websocket;
|
||||||
|
|
||||||
|
import org.springframework.context.annotation.Bean;
|
||||||
|
import org.springframework.context.annotation.Configuration;
|
||||||
|
import org.springframework.web.socket.server.standard.ServerEndpointExporter;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* websocket 配置
|
||||||
|
*/
|
||||||
|
@Configuration
|
||||||
|
public class WebSocketConfig {
|
||||||
|
@Bean
|
||||||
|
public ServerEndpointExporter serverEndpointExporter() {
|
||||||
|
return new ServerEndpointExporter();
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,86 @@
|
|||||||
|
package cn.iocoder.yudao.module.infra.websocket;
|
||||||
|
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.springframework.stereotype.Component;
|
||||||
|
|
||||||
|
import javax.websocket.*;
|
||||||
|
import javax.websocket.server.ServerEndpoint;
|
||||||
|
import java.util.concurrent.Semaphore;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* websocket 消息处理
|
||||||
|
*/
|
||||||
|
@Component
|
||||||
|
@ServerEndpoint("/websocket/message")
|
||||||
|
@Slf4j
|
||||||
|
public class WebSocketServer {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 默认最多允许同时在线用户数100
|
||||||
|
*/
|
||||||
|
public static int socketMaxOnlineCount = 100;
|
||||||
|
|
||||||
|
private static final Semaphore SOCKET_SEMAPHORE = new Semaphore(socketMaxOnlineCount);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 连接建立成功调用的方法
|
||||||
|
*/
|
||||||
|
@OnOpen
|
||||||
|
public void onOpen(Session session) throws Exception {
|
||||||
|
// 尝试获取信号量
|
||||||
|
boolean semaphoreFlag = SemaphoreUtils.tryAcquire(SOCKET_SEMAPHORE);
|
||||||
|
if (!semaphoreFlag) {
|
||||||
|
// 未获取到信号量
|
||||||
|
log.error("当前在线人数超过限制数:{}", socketMaxOnlineCount);
|
||||||
|
WebSocketUsers.sendMessage(session, "当前在线人数超过限制数:" + socketMaxOnlineCount);
|
||||||
|
session.close();
|
||||||
|
} else {
|
||||||
|
String userId = WebSocketUsers.getParam("userId", session);
|
||||||
|
if (userId != null) {
|
||||||
|
// 添加用户
|
||||||
|
WebSocketUsers.addSession(userId, session);
|
||||||
|
log.info("用户【userId={}】建立连接,当前连接用户总数:{}", userId, WebSocketUsers.getUsers().size());
|
||||||
|
WebSocketUsers.sendMessage(session, "接收内容:连接成功");
|
||||||
|
} else {
|
||||||
|
WebSocketUsers.sendMessage(session, "接收内容:连接失败");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 连接关闭时处理
|
||||||
|
*/
|
||||||
|
@OnClose
|
||||||
|
public void onClose(Session session) {
|
||||||
|
log.info("用户【sessionId={}】关闭连接!", session.getId());
|
||||||
|
// 移除用户
|
||||||
|
WebSocketUsers.removeSession(session);
|
||||||
|
// 获取到信号量则需释放
|
||||||
|
SemaphoreUtils.release(SOCKET_SEMAPHORE);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 抛出异常时处理
|
||||||
|
*/
|
||||||
|
@OnError
|
||||||
|
public void onError(Session session, Throwable exception) throws Exception {
|
||||||
|
if (session.isOpen()) {
|
||||||
|
// 关闭连接
|
||||||
|
session.close();
|
||||||
|
}
|
||||||
|
String sessionId = session.getId();
|
||||||
|
log.info("用户【sessionId={}】连接异常!异常信息:{}", sessionId, exception);
|
||||||
|
// 移出用户
|
||||||
|
WebSocketUsers.removeSession(session);
|
||||||
|
// 获取到信号量则需释放
|
||||||
|
SemaphoreUtils.release(SOCKET_SEMAPHORE);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 收到客户端消息时调用的方法
|
||||||
|
*/
|
||||||
|
@OnMessage
|
||||||
|
public void onMessage(Session session, String message) {
|
||||||
|
WebSocketUsers.sendMessage(session, "接收内容:" + message);
|
||||||
|
}
|
||||||
|
}
|
||||||
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,3 @@
|
|||||||
|
import XTable from './src/XTable.vue'
|
||||||
|
|
||||||
|
export { XTable }
|
||||||
@ -0,0 +1,81 @@
|
|||||||
|
// 修改样式变量
|
||||||
|
//@import 'vxe-table/styles/variable.scss';
|
||||||
|
|
||||||
|
/*font*/
|
||||||
|
$vxe-font-color: #e5e7eb;
|
||||||
|
// $vxe-font-size: 14px !default;
|
||||||
|
// $vxe-font-size-medium: 16px !default;
|
||||||
|
// $vxe-font-size-small: 14px !default;
|
||||||
|
// $vxe-font-size-mini: 12px !default;
|
||||||
|
|
||||||
|
/*color*/
|
||||||
|
$vxe-primary-color: #409eff !default;
|
||||||
|
$vxe-success-color: #67c23a !default;
|
||||||
|
$vxe-info-color: #909399 !default;
|
||||||
|
$vxe-warning-color: #e6a23c !default;
|
||||||
|
$vxe-danger-color: #f56c6c !default;
|
||||||
|
$vxe-disabled-color: #bfbfbf !default;
|
||||||
|
$vxe-primary-disabled-color: #c0c4cc !default;
|
||||||
|
|
||||||
|
/*loading*/
|
||||||
|
$vxe-loading-color: $vxe-primary-color !default;
|
||||||
|
$vxe-loading-background-color: #1d1e1f !default;
|
||||||
|
$vxe-loading-z-index: 999 !default;
|
||||||
|
|
||||||
|
/*icon*/
|
||||||
|
$vxe-icon-font-family: Verdana, Arial, Tahoma !default;
|
||||||
|
$vxe-icon-background-color: #e5e7eb !default;
|
||||||
|
|
||||||
|
/*toolbar*/
|
||||||
|
$vxe-toolbar-background-color: #1d1e1f !default;
|
||||||
|
$vxe-toolbar-button-border: #dcdfe6 !default;
|
||||||
|
$vxe-toolbar-custom-active-background-color: #d9dadb !default;
|
||||||
|
$vxe-toolbar-panel-background-color: #e5e7eb !default;
|
||||||
|
|
||||||
|
$vxe-table-font-color: #e5e7eb;
|
||||||
|
$vxe-table-header-background-color: #1d1e1f;
|
||||||
|
$vxe-table-body-background-color: #141414;
|
||||||
|
$vxe-table-row-striped-background-color: #1d1d1d;
|
||||||
|
$vxe-table-row-hover-background-color: #1d1e1f;
|
||||||
|
$vxe-table-row-hover-striped-background-color: #1e1e1e;
|
||||||
|
$vxe-table-footer-background-color: #1d1e1f;
|
||||||
|
$vxe-table-row-current-background-color: #302d2d;
|
||||||
|
$vxe-table-column-current-background-color: #302d2d;
|
||||||
|
$vxe-table-column-hover-background-color: #302d2d;
|
||||||
|
$vxe-table-row-hover-current-background-color: #302d2d;
|
||||||
|
$vxe-table-row-checkbox-checked-background-color: #3e3c37 !default;
|
||||||
|
$vxe-table-row-hover-checkbox-checked-background-color: #615a4a !default;
|
||||||
|
$vxe-table-menu-background-color: #1d1e1f;
|
||||||
|
$vxe-table-border-width: 1px !default;
|
||||||
|
$vxe-table-border-color: #4c4d4f !default;
|
||||||
|
$vxe-table-fixed-left-scrolling-box-shadow: 8px 0px 10px -5px rgba(0, 0, 0, 0.12) !default;
|
||||||
|
$vxe-table-fixed-right-scrolling-box-shadow: -8px 0px 10px -5px rgba(0, 0, 0, 0.12) !default;
|
||||||
|
|
||||||
|
$vxe-form-background-color: #141414;
|
||||||
|
|
||||||
|
/*pager*/
|
||||||
|
$vxe-pager-background-color: #1d1e1f !default;
|
||||||
|
$vxe-pager-perfect-background-color: #262727 !default;
|
||||||
|
$vxe-pager-perfect-button-background-color: #a7a3a3 !default;
|
||||||
|
|
||||||
|
$vxe-input-background-color: #141414;
|
||||||
|
$vxe-input-border-color: #4c4d4f !default;
|
||||||
|
|
||||||
|
$vxe-select-option-hover-background-color: #262626 !default;
|
||||||
|
$vxe-select-panel-background-color: #141414 !default;
|
||||||
|
$vxe-select-empty-color: #262626 !default;
|
||||||
|
$vxe-optgroup-title-color: #909399 !default;
|
||||||
|
|
||||||
|
/*button*/
|
||||||
|
$vxe-button-default-background-color: #262626;
|
||||||
|
$vxe-button-dropdown-panel-background-color: #141414;
|
||||||
|
|
||||||
|
/*modal*/
|
||||||
|
$vxe-modal-header-background-color: #141414;
|
||||||
|
$vxe-modal-body-background-color: #141414;
|
||||||
|
$vxe-modal-border-color: #3b3b3b;
|
||||||
|
|
||||||
|
/*pulldown*/
|
||||||
|
$vxe-pulldown-panel-background-color: #262626 !default;
|
||||||
|
|
||||||
|
@import 'vxe-table/styles/index';
|
||||||
@ -0,0 +1,6 @@
|
|||||||
|
@import 'vxe-table/styles/variable.scss';
|
||||||
|
@import 'vxe-table/styles/modules.scss';
|
||||||
|
// @import './theme/light.scss';
|
||||||
|
i {
|
||||||
|
border-color: initial;
|
||||||
|
}
|
||||||
@ -0,0 +1,16 @@
|
|||||||
|
// 修改样式变量
|
||||||
|
// /*font*/
|
||||||
|
// $vxe-font-size: 12px !default;
|
||||||
|
// $vxe-font-size-medium: 16px !default;
|
||||||
|
// $vxe-font-size-small: 14px !default;
|
||||||
|
// $vxe-font-size-mini: 12px !default;
|
||||||
|
/*color*/
|
||||||
|
$vxe-primary-color: #409eff !default;
|
||||||
|
$vxe-success-color: #67c23a !default;
|
||||||
|
$vxe-info-color: #909399 !default;
|
||||||
|
$vxe-warning-color: #e6a23c !default;
|
||||||
|
$vxe-danger-color: #f56c6c !default;
|
||||||
|
$vxe-disabled-color: #bfbfbf !default;
|
||||||
|
$vxe-primary-disabled-color: #c0c4cc !default;
|
||||||
|
|
||||||
|
@import 'vxe-table/styles/index';
|
||||||
@ -0,0 +1,25 @@
|
|||||||
|
import { CrudSchema } from '@/hooks/web/useCrudSchemas'
|
||||||
|
import type { VxeGridProps, VxeGridPropTypes, VxeTablePropTypes } from 'vxe-table'
|
||||||
|
|
||||||
|
export type XTableProps<D = any> = VxeGridProps<D> & {
|
||||||
|
allSchemas?: CrudSchema
|
||||||
|
height?: number // 高度 默认730
|
||||||
|
topActionSlots?: boolean // 是否开启表格内顶部操作栏插槽
|
||||||
|
treeConfig?: VxeTablePropTypes.TreeConfig // 树形表单配置
|
||||||
|
isList?: boolean // 是否不带分页的list
|
||||||
|
getListApi?: Function // 获取列表接口
|
||||||
|
getAllListApi?: Function // 获取全部数据接口 用于 vxe 导出
|
||||||
|
deleteApi?: Function // 删除接口
|
||||||
|
exportListApi?: Function // 导出接口
|
||||||
|
exportName?: string // 导出文件夹名称
|
||||||
|
params?: any // 其他查询参数
|
||||||
|
pagination?: boolean | VxeGridPropTypes.PagerConfig // 分页配置参数
|
||||||
|
toolBar?: boolean | VxeGridPropTypes.ToolbarConfig // 右侧工具栏配置参数
|
||||||
|
}
|
||||||
|
export type XColumns = VxeGridPropTypes.Columns
|
||||||
|
|
||||||
|
export type VxeTableColumn = {
|
||||||
|
field: string
|
||||||
|
title?: string
|
||||||
|
children?: VxeTableColumn[]
|
||||||
|
} & Recordable
|
||||||
@ -0,0 +1,32 @@
|
|||||||
|
import { ref, unref } from 'vue'
|
||||||
|
import { XTableProps } from '@/components/XTable/src/type'
|
||||||
|
|
||||||
|
export interface tableMethod {
|
||||||
|
reload: () => void
|
||||||
|
setProps: (props: XTableProps) => void
|
||||||
|
deleteData: (ids: string | number) => void
|
||||||
|
exportList: (fileName?: string) => void
|
||||||
|
}
|
||||||
|
|
||||||
|
export const useXTable = (props: XTableProps): [Function, tableMethod] => {
|
||||||
|
const tableRef = ref<Nullable<tableMethod>>(null)
|
||||||
|
|
||||||
|
const register = (instance) => {
|
||||||
|
tableRef.value = instance
|
||||||
|
props && instance.setProps(props)
|
||||||
|
}
|
||||||
|
const getInstance = (): tableMethod => {
|
||||||
|
const table = unref(tableRef)
|
||||||
|
if (!table) {
|
||||||
|
console.error('表格实例不存在')
|
||||||
|
}
|
||||||
|
return table as tableMethod
|
||||||
|
}
|
||||||
|
const methods: tableMethod = {
|
||||||
|
reload: () => getInstance().reload(),
|
||||||
|
setProps: (props) => getInstance().setProps(props),
|
||||||
|
deleteData: (ids: string | number) => getInstance().deleteData(ids),
|
||||||
|
exportList: (fileName?: string) => getInstance().exportList(fileName)
|
||||||
|
}
|
||||||
|
return [register, methods]
|
||||||
|
}
|
||||||
@ -0,0 +1,34 @@
|
|||||||
|
import { VXETable } from 'vxe-table'
|
||||||
|
import { ElImage, ElLink } from 'element-plus'
|
||||||
|
|
||||||
|
// 图片渲染
|
||||||
|
VXETable.renderer.add('XPreview', {
|
||||||
|
// 默认显示模板
|
||||||
|
renderDefault(_renderOpts, params) {
|
||||||
|
const { row, column } = params
|
||||||
|
if (row.type.indexOf('image/') === 0) {
|
||||||
|
return (
|
||||||
|
<ElImage
|
||||||
|
style="width: 80px; height: 50px"
|
||||||
|
src={row[column.field]}
|
||||||
|
key={row[column.field]}
|
||||||
|
preview-src-list={[row[column.field]]}
|
||||||
|
fit="contain"
|
||||||
|
lazy
|
||||||
|
></ElImage>
|
||||||
|
)
|
||||||
|
} else if (row.type.indexOf('video/') === 0) {
|
||||||
|
return (
|
||||||
|
<video>
|
||||||
|
<source src={row[column.field]}></source>
|
||||||
|
</video>
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
return (
|
||||||
|
<ElLink href={row[column.field]} target="_blank">
|
||||||
|
{row[column.field]}
|
||||||
|
</ElLink>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
@ -0,0 +1,118 @@
|
|||||||
|
<template>
|
||||||
|
<div class="flex">
|
||||||
|
<el-card class="w-1/2" :gutter="12" shadow="always">
|
||||||
|
<template #header>
|
||||||
|
<div class="card-header">
|
||||||
|
<span>连接</span>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<div class="flex items-center">
|
||||||
|
<span class="text-lg font-medium mr-4"> 连接状态: </span>
|
||||||
|
<el-tag :color="getTagColor">{{ status }}</el-tag>
|
||||||
|
</div>
|
||||||
|
<hr class="my-4" />
|
||||||
|
|
||||||
|
<div class="flex">
|
||||||
|
<el-input v-model="server" disabled>
|
||||||
|
<template #prepend> 服务地址 </template>
|
||||||
|
</el-input>
|
||||||
|
<el-button :type="getIsOpen ? 'danger' : 'primary'" @click="toggle">
|
||||||
|
{{ getIsOpen ? '关闭连接' : '开启连接' }}
|
||||||
|
</el-button>
|
||||||
|
</div>
|
||||||
|
<p class="text-lg font-medium mt-4">设置</p>
|
||||||
|
<hr class="my-4" />
|
||||||
|
<el-input
|
||||||
|
v-model="sendValue"
|
||||||
|
:autosize="{ minRows: 2, maxRows: 4 }"
|
||||||
|
type="textarea"
|
||||||
|
:disabled="!getIsOpen"
|
||||||
|
clearable
|
||||||
|
/>
|
||||||
|
<el-button type="primary" block class="mt-4" :disabled="!getIsOpen" @click="handlerSend">
|
||||||
|
发送
|
||||||
|
</el-button>
|
||||||
|
</el-card>
|
||||||
|
<el-card class="w-1/2" :gutter="12" shadow="always">
|
||||||
|
<template #header>
|
||||||
|
<div class="card-header">
|
||||||
|
<span>消息记录</span>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<div class="max-h-80 overflow-auto">
|
||||||
|
<ul>
|
||||||
|
<li v-for="item in getList" class="mt-2" :key="item.time">
|
||||||
|
<div class="flex items-center">
|
||||||
|
<span class="mr-2 text-primary font-medium">收到消息:</span>
|
||||||
|
<span>{{ dayjs(item.time).format('YYYY-MM-DD HH:mm:ss') }}</span>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
{{ item.res }}
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</el-card>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { computed, reactive, ref, watchEffect } from 'vue'
|
||||||
|
import { ElCard, ElInput, ElTag } from 'element-plus'
|
||||||
|
import { useWebSocket } from '@vueuse/core'
|
||||||
|
import dayjs from 'dayjs'
|
||||||
|
import { useUserStore } from '@/store/modules/user'
|
||||||
|
|
||||||
|
const userStore = useUserStore()
|
||||||
|
|
||||||
|
const sendValue = ref('')
|
||||||
|
|
||||||
|
const server = ref(
|
||||||
|
(import.meta.env.VITE_BASE_URL + '/websocket/message').replace('http', 'ws') +
|
||||||
|
'?userId=' +
|
||||||
|
userStore.getUser.id
|
||||||
|
)
|
||||||
|
|
||||||
|
const state = reactive({
|
||||||
|
recordList: [] as { id: number; time: number; res: string }[]
|
||||||
|
})
|
||||||
|
|
||||||
|
const { status, data, send, close, open } = useWebSocket(server.value, {
|
||||||
|
autoReconnect: false,
|
||||||
|
heartbeat: true
|
||||||
|
})
|
||||||
|
|
||||||
|
watchEffect(() => {
|
||||||
|
if (data.value) {
|
||||||
|
try {
|
||||||
|
const res = JSON.parse(data.value)
|
||||||
|
state.recordList.push(res)
|
||||||
|
} catch (error) {
|
||||||
|
state.recordList.push({
|
||||||
|
res: data.value,
|
||||||
|
id: Math.ceil(Math.random() * 1000),
|
||||||
|
time: new Date().getTime()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const getIsOpen = computed(() => status.value === 'OPEN')
|
||||||
|
const getTagColor = computed(() => (getIsOpen.value ? 'success' : 'red'))
|
||||||
|
|
||||||
|
const getList = computed(() => {
|
||||||
|
return [...state.recordList].reverse()
|
||||||
|
})
|
||||||
|
|
||||||
|
function handlerSend() {
|
||||||
|
send(sendValue.value)
|
||||||
|
sendValue.value = ''
|
||||||
|
}
|
||||||
|
|
||||||
|
function toggle() {
|
||||||
|
if (getIsOpen.value) {
|
||||||
|
close()
|
||||||
|
} else {
|
||||||
|
open()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
@ -0,0 +1,92 @@
|
|||||||
|
<template>
|
||||||
|
<div class="app-container">
|
||||||
|
<el-form label-width="120px">
|
||||||
|
<el-row type="flex" :gutter="0">
|
||||||
|
<el-col :sm="12">
|
||||||
|
<el-form-item label="WebSocket地址" size="small">
|
||||||
|
<el-input v-model="url" type="text"/>
|
||||||
|
</el-form-item>
|
||||||
|
</el-col>
|
||||||
|
<el-col :offset="1">
|
||||||
|
<el-form-item label="" label-width="0px" size="small">
|
||||||
|
<el-button @click="connect" type="primary" :disabled="ws&&ws.readyState===1">
|
||||||
|
{{ ws && ws.readyState === 1 ? "已连接" : "连接" }}
|
||||||
|
</el-button>
|
||||||
|
<el-button @click="exit" type="danger">断开</el-button>
|
||||||
|
</el-form-item>
|
||||||
|
</el-col>
|
||||||
|
</el-row>
|
||||||
|
<el-form-item label="发送内容" size="small">
|
||||||
|
<el-input type="textarea" v-model="message" :rows="5"/>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="" size="small">
|
||||||
|
<el-button type="success" @click="send">发送消息</el-button>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="接收内容" size="small">
|
||||||
|
<el-input type="textarea" v-model="content" :rows="12" disabled/>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="" size="small">
|
||||||
|
<el-button type="info" @click="content=''">清空消息</el-button>
|
||||||
|
</el-form-item>
|
||||||
|
</el-form>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import store from "@/store";
|
||||||
|
import {getNowDateTime} from "@/utils/ruoyi";
|
||||||
|
|
||||||
|
export default {
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
url: process.env.VUE_APP_BASE_API + "/websocket/message",
|
||||||
|
message: "",
|
||||||
|
content: "",
|
||||||
|
ws: null,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
created() {
|
||||||
|
this.url = this.url.replace("http", "ws")
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
connect() {
|
||||||
|
if (!'WebSocket' in window) {
|
||||||
|
this.$modal.msgError("您的浏览器不支持WebSocket");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const userId = store.getters.userId;
|
||||||
|
this.ws = new WebSocket(this.url + "?userId=" + userId);
|
||||||
|
const self = this;
|
||||||
|
this.ws.onopen = function (event) {
|
||||||
|
self.content = self.content + "\n**********************连接开始**********************\n";
|
||||||
|
};
|
||||||
|
this.ws.onmessage = function (event) {
|
||||||
|
self.content = self.content + "接收时间:" + getNowDateTime() + "\n" + event.data + "\n";
|
||||||
|
};
|
||||||
|
this.ws.onclose = function (event) {
|
||||||
|
self.content = self.content + "**********************连接关闭**********************\n";
|
||||||
|
};
|
||||||
|
this.ws.error = function (event) {
|
||||||
|
self.content = self.content + "**********************连接异常**********************\n";
|
||||||
|
};
|
||||||
|
},
|
||||||
|
exit() {
|
||||||
|
if (this.ws) {
|
||||||
|
this.ws.close();
|
||||||
|
this.ws = null;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
send() {
|
||||||
|
if (!this.ws || this.ws.readyState !== 1) {
|
||||||
|
this.$modal.msgError("未连接到服务器");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!this.message) {
|
||||||
|
this.$modal.msgError("请输入发送内容");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.ws.send(this.message);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
};
|
||||||
|
</script>
|
||||||
Loading…
Reference in New Issue