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