Merge branch 'master' of https://gitee.com/zhijiantianya/ruoyi-vue-pro into feature/wechat-mp

# Conflicts:
#	yudao-server/src/main/resources/application.yaml
This commit is contained in:
YunaiV 2023-01-07 18:57:04 +08:00
commit b9246d1543
96 changed files with 2990 additions and 2468 deletions

View File

@ -188,17 +188,17 @@ ps核心功能已经实现正在对接微信小程序中...
| 框架 | 说明 | 版本 | 学习指南 |
|---------------------------------------------------------------------------------------------|------------------|-------------|----------------------------------------------------------------|
| [Spring Boot](https://spring.io/projects/spring-boot) | 应用开发框架 | 2.7.6 | [文档](https://github.com/YunaiV/SpringBoot-Labs) |
| [Spring Boot](https://spring.io/projects/spring-boot) | 应用开发框架 | 2.7.7 | [文档](https://github.com/YunaiV/SpringBoot-Labs) |
| [MySQL](https://www.mysql.com/cn/) | 数据库服务器 | 5.7 / 8.0+ | |
| [Druid](https://github.com/alibaba/druid) | JDBC 连接池、监控组件 | 1.2.15 | [文档](http://www.iocoder.cn/Spring-Boot/datasource-pool/?yudao) |
| [MyBatis Plus](https://mp.baomidou.com/) | MyBatis 增强工具包 | 3.5.2 | [文档](http://www.iocoder.cn/Spring-Boot/MyBatis/?yudao) |
| [Dynamic Datasource](https://dynamic-datasource.com/) | 动态数据源 | 3.6.0 | [文档](http://www.iocoder.cn/Spring-Boot/datasource-pool/?yudao) |
| [MyBatis Plus](https://mp.baomidou.com/) | MyBatis 增强工具包 | 3.5.3 | [文档](http://www.iocoder.cn/Spring-Boot/MyBatis/?yudao) |
| [Dynamic Datasource](https://dynamic-datasource.com/) | 动态数据源 | 3.6.1 | [文档](http://www.iocoder.cn/Spring-Boot/datasource-pool/?yudao) |
| [Redis](https://redis.io/) | key-value 数据库 | 5.0 / 6.0 | |
| [Redisson](https://github.com/redisson/redisson) | Redis 客户端 | 3.18.0 | [文档](http://www.iocoder.cn/Spring-Boot/Redis/?yudao) |
| [Spring MVC](https://github.com/spring-projects/spring-framework/tree/master/spring-webmvc) | MVC 框架 | 5.3.24 | [文档](http://www.iocoder.cn/SpringMVC/MVC/?yudao) |
| [Spring Security](https://github.com/spring-projects/spring-security) | Spring 安全框架 | 5.7.5 | [文档](http://www.iocoder.cn/Spring-Boot/Spring-Security/?yudao) |
| [Hibernate Validator](https://github.com/hibernate/hibernate-validator) | 参数校验组件 | 6.2.5 | [文档](http://www.iocoder.cn/Spring-Boot/Validation/?yudao) |
| [Flowable](https://github.com/flowable/flowable-engine) | 工作流引擎 | 6.7.2 | [文档](https://doc.iocoder.cn/bpm/) |
| [Flowable](https://github.com/flowable/flowable-engine) | 工作流引擎 | 6.8.0 | [文档](https://doc.iocoder.cn/bpm/) |
| [Quartz](https://github.com/quartz-scheduler) | 任务调度组件 | 2.3.2 | [文档](http://www.iocoder.cn/Spring-Boot/Job/?yudao) |
| [Knife4j](https://gitee.com/xiaoym/knife4j) | Swagger 增强 UI 实现 | 3.0.3 | [文档](http://www.iocoder.cn/Spring-Boot/Swagger/?yudao) |
| [Resilience4j](https://github.com/resilience4j/resilience4j) | 服务保障组件 | 1.7.1 | [文档](http://www.iocoder.cn/Spring-Boot/Resilience4j/?yudao) |
@ -222,8 +222,8 @@ ps核心功能已经实现正在对接微信小程序中...
| 框架 | 说明 | 版本 |
|----------------------------------------------------------------------|:------------:|:------:|
| [Vue](https://staging-cn.vuejs.org/) | Vue 框架 | 3.2.45 |
| [Vite](https://cn.vitejs.dev//) | 开发与构建工具 | 4.0.3 |
| [Element Plus](https://element-plus.org/zh-CN/) | Element Plus | 2.2.27 |
| [Vite](https://cn.vitejs.dev//) | 开发与构建工具 | 4.0.4 |
| [Element Plus](https://element-plus.org/zh-CN/) | Element Plus | 2.2.28 |
| [TypeScript](https://www.typescriptlang.org/docs/) | TypeScript | 4.9.4 |
| [pinia](https://pinia.vuejs.org/) | vuex5 | 2.0.28 |
| [vue-i18n](https://kazupon.github.io/vue-i18n/zh/introduction.html/) | 国际化 | 9.2.2 |

View File

@ -30,7 +30,7 @@
<url>https://github.com/YunaiV/ruoyi-vue-pro</url>
<properties>
<revision>1.6.5-snapshot</revision>
<revision>1.6.6-snapshot</revision>
<!-- Maven 相关 -->
<java.version>1.8</java.version>
<maven.compiler.source>${java.version}</maven.compiler.source>

View File

@ -262,5 +262,6 @@ INSERT INTO `system_menu` VALUES (1266, '客户端更新', 'system:oauth2-client
INSERT INTO `system_menu` VALUES (1267, '客户端删除', 'system:oauth2-client:delete', 3, 4, 1263, '', '', '', 0, b'1', b'1', '', '2022-05-10 16:26:33', '1', '2022-05-11 00:31:33', b'0');
INSERT INTO `system_menu` VALUES (1281, '可视化报表', '', 1, 12, 0, '/visualization', 'ep:histogram', NULL, 0, b'1', b'1', '1', '2022-07-10 20:22:15', '1', '2022-07-10 20:33:30', b'0');
INSERT INTO `system_menu` VALUES (1282, '积木报表', '', 2, 1, 1281, 'jimu-report', 'ep:histogram', 'visualization/jmreport/index', 0, b'1', b'1', '1', '2022-07-10 20:26:36', '1', '2022-07-28 21:17:34', b'0');
INSERT INTO `system_menu` VALUES (1283, 'webSocket连接', '', 2, 14, 2, 'webSocket', 'ep:turn-off', 'infra/webSocket/index', 0, b'1', b'1', '1', '2023-01-01 11:43:04', '1', '2023-01-01 11:43:04', b'0');
SET FOREIGN_KEY_CHECKS = 1;

View File

@ -1710,6 +1710,8 @@ INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_i
INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `status`, `visible`, `keep_alive`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (1267, '客户端删除', 'system:oauth2-client:delete', 3, 4, 1263, '', '', '', 0, b'1', b'1', '', '2022-05-10 16:26:33', '1', '2022-05-11 00:31:33', b'0');
INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `status`, `visible`, `keep_alive`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (1281, '可视化报表', '', 1, 12, 0, '/visualization', 'chart', NULL, 0, b'1', b'1', '1', '2022-07-10 20:22:15', '1', '2022-07-10 20:33:30', b'0');
INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `status`, `visible`, `keep_alive`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (1282, '积木报表', '', 2, 1, 1281, 'jimu-report', 'example', 'visualization/jmreport/index', 0, b'1', b'1', '1', '2022-07-10 20:26:36', '1', '2022-07-28 21:17:34', b'0');
INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `status`, `visible`, `keep_alive`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (1283, 'webSocket连接', '', 2, 14, 2, 'webSocket', 'message', 'infra/webSocket/index', 0, b'1', b'1', '1', '2023-01-01 11:43:04', '1', '2023-01-01 11:43:04', b'0');
COMMIT;
-- ----------------------------

View File

@ -1344,6 +1344,7 @@ CREATE TABLE `jimu_report_share` (
`last_update_time` datetime NULL DEFAULT NULL COMMENT '最后更新时间',
`term_of_validity` varchar(1) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '有效期(0:永久有效1:1天2:7天)',
`status` varchar(1) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '是否过期(0未过期1已过期)',
`preview_lock_status` varchar(1) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '密码锁状态',
PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '积木报表预览权限表' ROW_FORMAT = Dynamic;

View File

@ -14,18 +14,18 @@
<url>https://github.com/YunaiV/ruoyi-vue-pro</url>
<properties>
<revision>1.6.5-snapshot</revision>
<revision>1.6.6-snapshot</revision>
<!-- 统一依赖管理 -->
<spring.boot.version>2.7.6</spring.boot.version>
<spring.boot.version>2.7.7</spring.boot.version>
<!-- Web 相关 -->
<knife4j.version>3.0.3</knife4j.version>
<swagger-annotations.version>1.6.8</swagger-annotations.version>
<servlet.versoin>2.5</servlet.versoin>
<!-- DB 相关 -->
<druid.version>1.2.15</druid.version>
<mybatis-plus.version>3.5.2</mybatis-plus.version>
<mybatis-plus-generator.version>3.5.2</mybatis-plus-generator.version>
<dynamic-datasource.version>3.6.0</dynamic-datasource.version>
<mybatis-plus.version>3.5.3.1</mybatis-plus.version>
<mybatis-plus-generator.version>3.5.3.1</mybatis-plus-generator.version>
<dynamic-datasource.version>3.6.1</dynamic-datasource.version>
<redisson.version>3.18.0</redisson.version>
<!-- 服务保障相关 -->
<lock4j.version>2.2.3</lock4j.version>
@ -37,14 +37,14 @@
<!-- Test 测试相关 -->
<podam.version>7.2.11.RELEASE</podam.version>
<jedis-mock.version>1.0.5</jedis-mock.version>
<mockito-inline.version>4.8.0</mockito-inline.version>
<mockito-inline.version>4.11.0</mockito-inline.version>
<!-- Bpm 工作流相关 -->
<flowable.version>6.7.2</flowable.version>
<flowable.version>6.8.0</flowable.version>
<!-- 工具类相关 -->
<lombok.version>1.18.24</lombok.version>
<mapstruct.version>1.5.3.Final</mapstruct.version>
<hutool.version>5.8.10</hutool.version>
<easyexcel.verion>3.1.3</easyexcel.verion>
<hutool.version>5.8.11</hutool.version>
<easyexcel.verion>3.1.4</easyexcel.verion>
<velocity.version>2.3</velocity.version>
<screw.version>1.0.5</screw.version>
<fastjson.version>1.2.83</fastjson.version>
@ -55,7 +55,7 @@
<jsch.version>0.1.55</jsch.version>
<tika-core.version>2.6.0</tika-core.version>
<aj-captcha.version>1.3.0</aj-captcha.version>
<netty-all.version>4.1.85.Final</netty-all.version>
<netty-all.version>4.1.86.Final</netty-all.version>
<ip2region.version>2.6.6</ip2region.version>
<!-- 三方云服务相关 -->
<okio.version>3.0.0</okio.version>
@ -63,7 +63,7 @@
<minio.version>8.4.6</minio.version>
<aliyun-java-sdk-core.version>4.6.3</aliyun-java-sdk-core.version>
<aliyun-java-sdk-dysmsapi.version>2.2.1</aliyun-java-sdk-dysmsapi.version>
<tencentcloud-sdk-java.version>3.1.637</tencentcloud-sdk-java.version>
<tencentcloud-sdk-java.version>3.1.660</tencentcloud-sdk-java.version>
<justauth.version>1.4.0</justauth.version>
<jimureport.version>1.5.6</jimureport.version>
<xercesImpl.version>2.12.2</xercesImpl.version>
@ -602,6 +602,12 @@
<artifactId>xercesImpl</artifactId>
<version>${xercesImpl.version}</version>
</dependency>
<!-- SpringBoot Websocket -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-websocket</artifactId>
<version>${spring.boot.version}</version>
</dependency>
</dependencies>
</dependencyManagement>

View File

@ -21,7 +21,7 @@
<maven.compiler.target>8</maven.compiler.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<!-- 统一依赖管理 -->
<spring.boot.version>2.7.6</spring.boot.version>
<spring.boot.version>2.7.7</spring.boot.version>
</properties>
<dependencyManagement>
@ -52,7 +52,7 @@
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-all</artifactId>
<version>5.8.10</version>
<version>5.8.11</version>
</dependency>
<dependency>

View File

@ -21,7 +21,7 @@
<maven.compiler.target>8</maven.compiler.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<!-- 统一依赖管理 -->
<spring.boot.version>2.7.6</spring.boot.version>
<spring.boot.version>2.7.7</spring.boot.version>
</properties>
<dependencyManagement>
@ -52,7 +52,7 @@
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-all</artifactId>
<version>5.8.10</version>
<version>5.8.11</version>
</dependency>
<dependency>

View File

@ -40,6 +40,7 @@
<module>yudao-spring-boot-starter-flowable</module>
<module>yudao-spring-boot-starter-captcha</module>
<module>yudao-spring-boot-starter-websocket</module>
</modules>
<artifactId>yudao-framework</artifactId>

View File

@ -52,7 +52,7 @@
<dependency>
<groupId>com.alipay.sdk</groupId>
<artifactId>alipay-sdk-java</artifactId>
<version>4.35.0.ALL</version>
<version>4.35.9.ALL</version>
<exclusions>
<exclusion>
<groupId>org.bouncycastle</groupId>

View File

@ -9,10 +9,11 @@ import io.minio.*;
import java.io.ByteArrayInputStream;
import static cn.iocoder.yudao.framework.file.core.client.s3.S3FileClientConfig.ENDPOINT_ALIYUN;
import static cn.iocoder.yudao.framework.file.core.client.s3.S3FileClientConfig.ENDPOINT_TENCENT;
/**
* 基于 S3 协议的文件客户端实现 MinIO阿里云腾讯云七牛云华为云等云服务
*
* <p>
* S3 协议的客户端采用亚马逊提供的 software.amazon.awssdk.s3
*
* @author 芋道源码
@ -78,6 +79,11 @@ public class S3FileClient extends AbstractFileClient<S3FileClientConfig> {
.replaceAll("-internal", "")// 去除内网 Endpoint 的后缀
.replaceAll("https://", "");
}
// 腾讯云必须有 region否则会报错
if (config.getEndpoint().contains(ENDPOINT_TENCENT)) {
return StrUtil.subAfter(config.getEndpoint(), ".cos.", false)
.replaceAll("." + ENDPOINT_TENCENT, ""); // 去除 Endpoint
}
return null;
}

View File

@ -19,6 +19,7 @@ public class S3FileClientConfig implements FileClientConfig {
public static final String ENDPOINT_QINIU = "qiniucs.com";
public static final String ENDPOINT_ALIYUN = "aliyuncs.com";
public static final String ENDPOINT_TENCENT = "myqcloud.com";
/**
* 节点地址

View File

@ -129,6 +129,8 @@ public class YudaoWebSecurityConfigurerAdapter {
.antMatchers(buildAppApi("/**")).permitAll()
// 1.5 验证码captcha 允许匿名访问
.antMatchers("/captcha/get", "/captcha/check").permitAll()
// 1.6 webSocket 允许匿名访问
.antMatchers("/websocket/message").permitAll()
// 每个项目的自定义规则
.and().authorizeRequests(registry -> // 下面循环设置自定义规则
authorizeRequestsCustomizers.forEach(customizer -> customizer.customize(registry)))

View File

@ -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>

View File

@ -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();
}
}

View File

@ -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;
}

View File

@ -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]));
}
}

View File

@ -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) {
}
}

View File

@ -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";
}

View File

@ -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);
}
}

View File

@ -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();
}
}

View File

@ -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);
}
}

View File

@ -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;
}
}

View File

@ -0,0 +1 @@
package cn.iocoder.yudao.framework.websocket;

View File

@ -0,0 +1 @@
cn.iocoder.yudao.framework.websocket.config.YudaoWebSocketAutoConfiguration

View File

@ -111,6 +111,12 @@
<groupId>cn.iocoder.boot</groupId>
<artifactId>yudao-spring-boot-starter-file</artifactId>
</dependency>
<!-- WebSocket -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-websocket</artifactId>
</dependency>
</dependencies>
</project>

View File

@ -75,7 +75,7 @@ public class ConfigController {
if (config == null) {
return null;
}
if (config.getVisible()) {
if (!config.getVisible()) {
throw ServiceExceptionUtil.exception(ErrorCodeConstants.CONFIG_GET_VALUE_ERROR_IF_VISIBLE);
}
return success(config.getValue());

View File

@ -1,11 +1,14 @@
package cn.iocoder.yudao.module.infra.dal.mysql.file;
import cn.hutool.core.collection.CollUtil;
import cn.iocoder.yudao.framework.file.core.client.db.DBFileContentFrameworkDAO;
import cn.iocoder.yudao.module.infra.dal.dataobject.file.FileContentDO;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import org.springframework.stereotype.Repository;
import javax.annotation.Resource;
import java.util.List;
import java.util.Optional;
@Repository
public class FileContentDAOImpl implements DBFileContentFrameworkDAO {
@ -27,9 +30,11 @@ public class FileContentDAOImpl implements DBFileContentFrameworkDAO {
@Override
public byte[] selectContent(Long configId, String path) {
FileContentDO fileContentDO = fileContentMapper.selectOne(
buildQuery(configId, path).select(FileContentDO::getContent));
return fileContentDO != null ? fileContentDO.getContent() : null;
List<FileContentDO> list = fileContentMapper.selectList(
buildQuery(configId, path).select(FileContentDO::getContent).orderByDesc(FileContentDO::getId));
return Optional.ofNullable(CollUtil.getFirst(list))
.map(FileContentDO::getContent)
.orElse(null);
}
private LambdaQueryWrapper<FileContentDO> buildQuery(Long configId, String path) {

View File

@ -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);
}
}
}

View File

@ -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();
}
}

View File

@ -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);
}
}

View File

@ -0,0 +1,178 @@
package cn.iocoder.yudao.module.infra.websocket;
import cn.hutool.core.map.MapUtil;
import cn.hutool.core.util.StrUtil;
import lombok.extern.slf4j.Slf4j;
import org.bouncycastle.util.Strings;
import javax.validation.constraints.NotNull;
import javax.websocket.Session;
import java.io.IOException;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
/**
* websocket 客户端用户
*/
@Slf4j
public class WebSocketUsers {
/**
* 用户集
* TODO 需要登录用户的session
*/
private static final Map<String, Session> SESSION_MAP = new ConcurrentHashMap<>();
/**
* 存储用户
*
* @param userId 唯一键
* @param session 用户信息
*/
public static void addSession(String userId, Session session) {
SESSION_MAP.put(userId, session);
}
/**
* 移除用户
*
* @param session 用户信息
* @return 移除结果
*/
public static boolean removeSession(Session session) {
String key = null;
boolean flag = SESSION_MAP.containsValue(session);
if (flag) {
Set<Map.Entry<String, Session>> entries = SESSION_MAP.entrySet();
for (Map.Entry<String, Session> entry : entries) {
Session value = entry.getValue();
if (value.equals(session)) {
key = entry.getKey();
break;
}
}
} else {
return true;
}
return removeSession(key);
}
/**
* 移出用户
*
* @param userId 用户id
*/
public static boolean removeSession(String userId) {
log.info("用户【userId={}】退出", userId);
Session remove = SESSION_MAP.remove(userId);
if (remove != null) {
boolean containsValue = SESSION_MAP.containsValue(remove);
log.info("用户【userId={}】退出{},当前连接用户总数:{}", userId, containsValue ? "失败" : "成功", SESSION_MAP.size());
return containsValue;
} else {
return true;
}
}
/**
* 获取在线用户列表
*
* @return 返回用户集合
*/
public static Map<String, Session> getUsers() {
return SESSION_MAP;
}
/**
* 向所有在线人发送消息
*
* @param message 消息内容
*/
public static void sendMessageToAll(String message) {
SESSION_MAP.forEach((userId, session) -> {
if (session.isOpen()) {
sendMessage(session, message);
}
});
}
/**
* 异步发送文本消息
*
* @param session 用户session
* @param message 消息内容
*/
public static void sendMessageAsync(Session session, String message) {
if (session.isOpen()) {
// TODO 需要加synchronized锁synchronized(session)单个session创建线程
session.getAsyncRemote().sendText(message);
} else {
log.warn("用户【session={}】不在线", session.getId());
}
}
/**
* 同步发送文本消息
*
* @param session 用户session
* @param message 消息内容
*/
public static void sendMessage(Session session, String message) {
try {
if (session.isOpen()) {
// TODO 需要加synchronized锁synchronized(session)单个session创建线程
session.getBasicRemote().sendText(message);
} else {
log.warn("用户【session={}】不在线", session.getId());
}
} catch (IOException e) {
log.error("发送消息异常", e);
}
}
/**
* 根据用户id发送消息
*
* @param userId 用户id
* @param message 消息内容
*/
public static void sendMessage(String userId, String message) {
Session session = SESSION_MAP.get(userId);
//判断是否存在该用户的session并且是否在线
if (session == null || !session.isOpen()) {
return;
}
sendMessage(session, message);
}
/**
* 获取session中的指定参数值
*
* @param key 参数key
* @param session 用户session
*/
public static String getParam(@NotNull String key, Session session) {
//TODO 目前只针对获取一个key的值后期根据情况拓展多个 或者直接在onClose onOpen上获取参数
String value = null;
Map<String, List<String>> parameters = session.getRequestParameterMap();
if (MapUtil.isNotEmpty(parameters)) {
value = parameters.get(key).get(0);
} else {
String queryString = session.getQueryString();
if (!StrUtil.isEmpty(queryString)) {
String[] params = Strings.split(queryString, '&');
for (String paramPair : params) {
String[] nameValues = Strings.split(paramPair, '=');
if (key.equals(nameValues[0])) {
value = nameValues[1];
}
}
}
}
return value;
}
}

View File

@ -1,7 +1,7 @@
<template>
<ContentWrap>
<!-- 列表 -->
<vxe-grid ref="xGrid" v-bind="gridOptions" class="xtable-scrollbar">
<XTable @register="registerTable">
<template #toolbar_buttons>
<!-- 操作:新增 -->
<XButton
@ -17,7 +17,7 @@
preIcon="ep:download"
:title="t('action.export')"
v-hasPermi="['${permissionPrefix}:export']"
@click="handleExport()"
@click="exportList('${table.classComment}.xls')"
/>
</template>
<template #actionbtns_default="{ row }">
@ -40,10 +40,10 @@
preIcon="ep:delete"
:title="t('action.del')"
v-hasPermi="['${permissionPrefix}:delete']"
@click="handleDelete(row.id)"
@click="deleteData(row.id)"
/>
</template>
</vxe-grid>
</XTable>
</ContentWrap>
<!-- 弹窗 -->
<XModal id="${classNameVar}Model" :loading="modelLoading" v-model="modelVisible" :title="modelTitle">
@ -79,8 +79,7 @@
import { ref, unref } from 'vue'
import { useI18n } from '@/hooks/web/useI18n'
import { useMessage } from '@/hooks/web/useMessage'
import { useVxeGrid } from '@/hooks/web/useVxeGrid'
import { VxeGridInstance } from 'vxe-table'
import { useXTable } from '@/hooks/web/useXTable'
import { FormExpose } from '@/components/Form'
// 业务相关的 import
import { rules, allSchemas } from './${classNameVar}.data'
@ -90,8 +89,7 @@ const { t } = useI18n() // 国际化
const message = useMessage() // 消息弹窗
// 列表相关的变量
const xGrid = ref<VxeGridInstance>() // 列表 Grid Ref
const { gridOptions, getList, deleteData, exportList } = useVxeGrid<${simpleClassName}Api.${simpleClassName}VO>({
const [registerTable, { reload, deleteData, exportList }] = useXTable({
allSchemas: allSchemas,
getListApi: ${simpleClassName}Api.get${simpleClassName}PageApi,
deleteApi: ${simpleClassName}Api.delete${simpleClassName}Api,
@ -121,11 +119,6 @@ const handleCreate = () => {
modelLoading.value = false
}
// 导出操作
const handleExport = async () => {
await exportList(xGrid, '${table.classComment}.xls')
}
// 修改操作
const handleUpdate = async (rowId: number) => {
setDialogTile('update')
@ -143,11 +136,6 @@ const handleDetail = async (rowId: number) => {
modelLoading.value = false
}
// 删除操作
const handleDelete = async (rowId: number) => {
await deleteData(xGrid, rowId)
}
// 提交按钮
const submitForm = async () => {
const elForm = unref(formRef)?.getElFormRef()
@ -169,7 +157,7 @@ const submitForm = async () => {
} finally {
actionLoading.value = false
// 刷新列表
await getList(xGrid)
await reload()
}
}
})

View File

@ -1,5 +1,6 @@
package cn.iocoder.yudao.module.visualization.framework.jmreport.config;
import cn.iocoder.yudao.framework.security.config.SecurityProperties;
import cn.iocoder.yudao.module.system.api.oauth2.OAuth2TokenApi;
import cn.iocoder.yudao.module.visualization.framework.jmreport.core.service.JmReportTokenServiceImpl;
import org.jeecg.modules.jmreport.api.JmReportTokenServiceI;
@ -18,8 +19,8 @@ public class JmReportConfiguration {
@Bean
@SuppressWarnings("SpringJavaInjectionPointsAutowiringInspection")
public JmReportTokenServiceI jmReportTokenService(OAuth2TokenApi oAuth2TokenApi) {
return new JmReportTokenServiceImpl(oAuth2TokenApi);
public JmReportTokenServiceI jmReportTokenService(OAuth2TokenApi oAuth2TokenApi, SecurityProperties securityProperties) {
return new JmReportTokenServiceImpl(oAuth2TokenApi, securityProperties);
}
}

View File

@ -1,7 +1,10 @@
package cn.iocoder.yudao.module.visualization.framework.jmreport.core.service;
import cn.hutool.core.util.ObjectUtil;
import cn.hutool.core.util.StrUtil;
import cn.iocoder.yudao.framework.common.exception.ServiceException;
import cn.iocoder.yudao.framework.common.util.servlet.ServletUtils;
import cn.iocoder.yudao.framework.security.config.SecurityProperties;
import cn.iocoder.yudao.framework.security.core.LoginUser;
import cn.iocoder.yudao.framework.security.core.util.SecurityFrameworkUtils;
import cn.iocoder.yudao.framework.tenant.core.context.TenantContextHolder;
@ -10,6 +13,10 @@ import cn.iocoder.yudao.module.system.api.oauth2.OAuth2TokenApi;
import cn.iocoder.yudao.module.system.api.oauth2.dto.OAuth2AccessTokenCheckRespDTO;
import lombok.RequiredArgsConstructor;
import org.jeecg.modules.jmreport.api.JmReportTokenServiceI;
import org.springframework.http.HttpHeaders;
import javax.servlet.http.HttpServletRequest;
import java.util.Objects;
/**
* {@link JmReportTokenServiceI} 实现类提供积木报表的 Token 校验用户信息的查询等功能
@ -19,8 +26,37 @@ import org.jeecg.modules.jmreport.api.JmReportTokenServiceI;
@RequiredArgsConstructor
public class JmReportTokenServiceImpl implements JmReportTokenServiceI {
/**
* 积木 token head
*/
private static final String JM_TOKEN_HEADER = "X-Access-Token";
/**
* auth 相关格式
*/
private static final String AUTHORIZATION_FORMAT = SecurityFrameworkUtils.AUTHORIZATION_BEARER + " %s";
private final OAuth2TokenApi oauth2TokenApi;
private final SecurityProperties securityProperties;
/**
* 自定义 API 数据集appian自定义 Header解决 Token 传递
* 参考 <a href="http://report.jeecg.com/2222224">api数据集token机制详解</a> 文档
*
* @return head
*/
@Override
public HttpHeaders customApiHeader() {
// 读取积木标标系统的 token
HttpServletRequest request = ServletUtils.getRequest();
String token = request.getHeader(JM_TOKEN_HEADER);
// 设置到 yudao 系统的 token
HttpHeaders headers = new HttpHeaders();
headers.add(securityProperties.getTokenHeader(), String.format(AUTHORIZATION_FORMAT, token));
return headers;
}
/**
* 校验 Token 是否有效即验证通过
*
@ -29,8 +65,40 @@ public class JmReportTokenServiceImpl implements JmReportTokenServiceI {
*/
@Override
public Boolean verifyToken(String token) {
Long userId = SecurityFrameworkUtils.getLoginUserId();
if (!Objects.isNull(userId)) {
return true;
}
return buildLoginUserByToken(token) != null;
}
/**
* 获得用户编号
* <p>
* 虽然方法名获得的是 username实际对应到项目中是用户编号
*
* @param token JmReport 前端传递的 token
* @return 用户编号
*/
@Override
public String getUsername(String token) {
Long userId = SecurityFrameworkUtils.getLoginUserId();
if (ObjectUtil.isNotNull(userId)) {
return String.valueOf(userId);
}
LoginUser user = buildLoginUserByToken(token);
return user == null ? null : String.valueOf(user.getId());
}
/**
* 基于 token 构建登录用户
*
* @param token token
* @return 返回 token 对应的用户信息
*/
private LoginUser buildLoginUserByToken(String token) {
if (StrUtil.isEmpty(token)) {
return false;
return null;
}
// TODO 如下的实现不算特别优雅主要咱是不想搞的太复杂所以参考对应的 Filter 先实现了
@ -41,7 +109,7 @@ public class JmReportTokenServiceImpl implements JmReportTokenServiceI {
try {
OAuth2AccessTokenCheckRespDTO accessToken = oauth2TokenApi.checkAccessToken(token);
if (accessToken == null) {
return false;
return null;
}
user = new LoginUser().setId(accessToken.getUserId()).setUserType(accessToken.getUserType())
.setTenantId(accessToken.getTenantId()).setScopes(accessToken.getScopes());
@ -49,7 +117,7 @@ public class JmReportTokenServiceImpl implements JmReportTokenServiceI {
// do nothing如果报错说明认证失败则返回 false 即可
}
if (user == null) {
return false;
return null;
}
SecurityFrameworkUtils.setLoginUser(user, WebFrameworkUtils.getRequest());
@ -57,21 +125,7 @@ public class JmReportTokenServiceImpl implements JmReportTokenServiceI {
// 目的基于 LoginUser 获得到的租户编号设置到 Tenant 上下文避免查询数据库时的报错
TenantContextHolder.setIgnore(false);
TenantContextHolder.setTenantId(user.getTenantId());
return true;
}
/**
* 获得用户编号
*
* 虽然方法名获得的是 username实际对应到项目中是用户编号
*
* @param token JmReport 前端传递的 token
* @return 用户编号
*/
@Override
public String getUsername(String token) {
Long userId = SecurityFrameworkUtils.getLoginUserId();
return userId != null ? String.valueOf(userId) : null;
return user;
}
}

View File

@ -111,7 +111,7 @@
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<version>2.7.6</version> <!-- 如果 spring.boot.version 版本修改,则这里也要跟着修改 -->
<version>2.7.7</version> <!-- 如果 spring.boot.version 版本修改,则这里也要跟着修改 -->
<configuration>
<fork>true</fork>
</configuration>

View File

@ -93,6 +93,11 @@ yudao:
permit-all_urls:
- /admin-ui/** # /resources/admin-ui 目录下的静态资源
- /admin-api/mp/open/** # 微信公众号开放平台,微信回调接口,不需要登录
websocket:
enable: true # websocket的开关
path: /websocket/message # 路径
maxOnlineCount: 0 # 最大连接人数
sessionMap: true # 保存sessionMap
swagger:
title: 管理后台
description: 提供管理员管理的所有功能

View File

@ -26,19 +26,19 @@
### 前端依赖
| 框架 | 说明 | 版本 |
| --- | --- | --- |
| 框架 | 说明 | 版本 |
| --- | --- |--------|
| [Vue](https://staging-cn.vuejs.org/) | vue 框架 | 3.2.45 |
| [Vite](https://cn.vitejs.dev//) | 开发与构建工具 | 4.0.3 |
| [Element Plus](https://element-plus.org/zh-CN/) | Element Plus | 2.2.27 |
| [TypeScript](https://www.typescriptlang.org/docs/) | JavaScript 的超集 | 4.9.4 |
| [Vite](https://cn.vitejs.dev//) | 开发与构建工具 | 4.0.4 |
| [Element Plus](https://element-plus.org/zh-CN/) | Element Plus | 2.2.28 |
| [TypeScript](https://www.typescriptlang.org/docs/) | JavaScript 的超集 | 4.9.4 |
| [pinia](https://pinia.vuejs.org/) | Vue 存储库 替代 vuex5 | 2.0.28 |
| [vueuse](https://vueuse.org/) | 常用工具集 | 9.8.2 |
| [vxe-table](https://vxetable.cn/) | vue 最强表单 | 4.3.7 |
| [vue-i18n](https://kazupon.github.io/vue-i18n/zh/introduction.html/) | 国际化 | 9.2.2 |
| [vue-router](https://router.vuejs.org/) | vue 路由 | 4.1.6 |
| [windicss](https://cn.windicss.org/) | 下一代工具优先的 CSS 框架 | 3.5.6 |
| [iconify](https://icon-sets.iconify.design/) | 在线图标库 | 3.0.0 |
| [vueuse](https://vueuse.org/) | 常用工具集 | 9.10.0 |
| [vxe-table](https://vxetable.cn/) | vue 最强表单 | 4.3.7 |
| [vue-i18n](https://kazupon.github.io/vue-i18n/zh/introduction.html/) | 国际化 | 9.2.2 |
| [vue-router](https://router.vuejs.org/) | vue 路由 | 4.1.6 |
| [windicss](https://cn.windicss.org/) | 下一代工具优先的 CSS 框架 | 3.5.6 |
| [iconify](https://icon-sets.iconify.design/) | 在线图标库 | 3.0.1 |
| [wangeditor](https://www.wangeditor.com/) | 富文本编辑器 | 5.1.23 |
### 推荐 VScode 开发,插件如下

View File

@ -1,6 +1,6 @@
{
"name": "yudao-ui-admin-vue3",
"version": "1.6.5.1879",
"version": "1.6.6-snapshot.1901",
"description": "基于vue3、vite4、element-plus、typesScript",
"author": "xingyu",
"private": false,
@ -25,18 +25,18 @@
},
"dependencies": {
"@iconify/iconify": "^3.0.1",
"@vueuse/core": "^9.9.0",
"@vueuse/core": "^9.10.0",
"@wangeditor/editor": "^5.1.23",
"@wangeditor/editor-for-vue": "^5.1.10",
"@zxcvbn-ts/core": "^2.1.0",
"animate.css": "^4.1.1",
"axios": "^1.2.1",
"axios": "^1.2.2",
"cropperjs": "^1.5.13",
"crypto-js": "^4.1.1",
"dayjs": "^1.11.7",
"echarts": "^5.4.1",
"echarts-wordcloud": "^2.1.0",
"element-plus": "2.2.27",
"element-plus": "2.2.28",
"intro.js": "^6.0.0",
"jsencrypt": "^3.3.1",
"lodash-es": "^4.17.21",
@ -55,27 +55,27 @@
"xe-utils": "^3.5.7"
},
"devDependencies": {
"@commitlint/cli": "^17.3.0",
"@commitlint/config-conventional": "^17.3.0",
"@iconify/json": "^2.1.157",
"@commitlint/cli": "^17.4.0",
"@commitlint/config-conventional": "^17.4.0",
"@iconify/json": "^2.2.2",
"@intlify/unplugin-vue-i18n": "^0.8.1",
"@purge-icons/generated": "^0.9.0",
"@types/intro.js": "^5.1.0",
"@types/lodash-es": "^4.17.6",
"@types/node": "^18.11.17",
"@types/node": "^18.11.18",
"@types/nprogress": "^0.2.0",
"@types/qrcode": "^1.5.0",
"@types/qs": "^6.9.7",
"@typescript-eslint/eslint-plugin": "^5.47.0",
"@typescript-eslint/parser": "^5.47.0",
"@typescript-eslint/eslint-plugin": "^5.48.0",
"@typescript-eslint/parser": "^5.48.0",
"@vitejs/plugin-legacy": "^3.0.1",
"@vitejs/plugin-vue": "^4.0.0",
"@vitejs/plugin-vue-jsx": "^3.0.0",
"autoprefixer": "^10.4.13",
"consola": "^2.15.3",
"eslint": "^8.30.0",
"eslint-config-prettier": "^8.5.0",
"eslint-define-config": "^1.12.0",
"eslint": "^8.31.0",
"eslint-config-prettier": "^8.6.0",
"eslint-define-config": "^1.13.0",
"eslint-plugin-prettier": "^4.2.1",
"eslint-plugin-vue": "^9.8.0",
"lint-staged": "^13.1.0",
@ -84,9 +84,9 @@
"postcss-scss": "^4.0.6",
"prettier": "^2.8.1",
"rimraf": "^3.0.2",
"rollup": "^3.8.1",
"rollup": "^3.9.1",
"sass": "^1.57.1",
"stylelint": "^14.16.0",
"stylelint": "^14.16.1",
"stylelint-config-html": "^1.1.0",
"stylelint-config-prettier": "^9.0.4",
"stylelint-config-recommended": "^9.0.0",
@ -94,7 +94,7 @@
"stylelint-order": "^5.0.0",
"terser": "^5.16.1",
"typescript": "4.9.4",
"vite": "4.0.3",
"vite": "4.0.4",
"vite-plugin-compression": "^0.5.1",
"vite-plugin-ejs": "^1.6.4",
"vite-plugin-eslint": "^1.8.1",
@ -104,7 +104,7 @@
"vite-plugin-svg-icons": "^2.0.1",
"vite-plugin-vue-setup-extend": "^0.4.0",
"vite-plugin-windicss": "^1.8.10",
"vue-tsc": "^1.0.17",
"vue-tsc": "^1.0.22",
"windicss": "^3.5.6"
},
"engines": {

File diff suppressed because it is too large Load Diff

View File

@ -353,7 +353,6 @@ const select = ref()
watch(
() => select.value,
() => {
console.info(select.value)
if (select.value == 'custom') {
open()
} else {

View File

@ -0,0 +1,3 @@
import XTable from './src/XTable.vue'
export { XTable }

View File

@ -0,0 +1,335 @@
<template>
<VxeGrid v-bind="getProps" ref="xGrid" :class="`${prefixCls}`" class="xtable-scrollbar">
<template #[item]="data" v-for="item in Object.keys($slots)" :key="item">
<slot :name="item" v-bind="data || {}"></slot>
</template>
</VxeGrid>
</template>
<script lang="ts" setup name="XTable">
import { computed, PropType, ref, unref, useAttrs, watch } from 'vue'
import { SizeType, VxeGridInstance } from 'vxe-table'
import { useAppStore } from '@/store/modules/app'
import { useDesign } from '@/hooks/web/useDesign'
import { XTableProps } from './type'
import { isBoolean, isFunction } from '@/utils/is'
import { useMessage } from '@/hooks/web/useMessage'
import download from '@/utils/download'
import { useI18n } from '@/hooks/web/useI18n'
const { t } = useI18n()
const message = useMessage() //
const appStore = useAppStore()
const { getPrefixCls } = useDesign()
const prefixCls = getPrefixCls('x-vxe-table')
const attrs = useAttrs()
const emit = defineEmits(['register'])
watch(
() => appStore.getIsDark,
() => {
if (appStore.getIsDark == true) {
import('./style/dark.scss')
}
if (appStore.getIsDark == false) {
import('./style/light.scss')
}
},
{ immediate: true }
)
const currentSize = computed(() => {
let resSize: SizeType = 'small'
const appsize = appStore.getCurrentSize
switch (appsize) {
case 'large':
resSize = 'medium'
break
case 'default':
resSize = 'small'
break
case 'small':
resSize = 'mini'
break
}
return resSize
})
const props = defineProps({
options: {
type: Object as PropType<XTableProps>,
default: () => {}
}
})
const innerProps = ref<Partial<XTableProps>>()
const getProps = computed(() => {
const options = innerProps.value || props.options
options.size = currentSize as any
options.height = 700
getOptionInitConfig(options)
getColumnsConfig(options)
getProxyConfig(options)
getPageConfig(options)
getToolBarConfig(options)
// console.log(options);
return {
...options,
...attrs
}
})
const xGrid = ref<VxeGridInstance>() // Grid Ref
let proxyForm = false
const getOptionInitConfig = (options: XTableProps) => {
options.size = currentSize as any
options.rowConfig = {
isCurrent: true, //
isHover: true //
}
}
// columns
const getColumnsConfig = (options: XTableProps) => {
const { allSchemas } = options
if (!allSchemas) return
if (allSchemas.printSchema) {
options.printConfig = {
columns: allSchemas.printSchema
}
}
if (allSchemas.formSchema) {
proxyForm = true
options.formConfig = {
enabled: true,
titleWidth: 100,
titleAlign: 'right',
items: allSchemas.searchSchema
}
}
if (allSchemas.tableSchema) {
options.columns = allSchemas.tableSchema
}
}
//
const getProxyConfig = (options: XTableProps) => {
const { getListApi, proxyConfig, data, isList } = options
if (proxyConfig || data) return
if (getListApi && isFunction(getListApi) && !isList) {
if (!isList) {
options.proxyConfig = {
seq: true, //
form: proxyForm, // reload
props: { result: 'list', total: 'total' },
ajax: {
query: async ({ page, form }) => {
let queryParams: any = Object.assign({}, JSON.parse(JSON.stringify(form)))
if (options.params) {
queryParams = Object.assign(queryParams, options.params)
}
if (!options?.treeConfig) {
queryParams.pageSize = page.pageSize
queryParams.pageNo = page.currentPage
}
return new Promise(async (resolve) => {
resolve(await getListApi(queryParams))
})
},
delete: ({ body }) => {
return new Promise(async (resolve) => {
if (options.deleteApi) {
resolve(await options.deleteApi(JSON.stringify(body)))
} else {
Promise.reject('未设置deleteApi')
}
})
},
queryAll: ({ form }) => {
const queryParams = Object.assign({}, JSON.parse(JSON.stringify(form)))
return new Promise(async (resolve) => {
if (options.getAllListApi) {
resolve(await options.getAllListApi(queryParams))
} else {
resolve(await getListApi(queryParams))
}
})
}
}
}
} else {
options.proxyConfig = {
seq: true, //
form: true, // reload
props: { result: 'data' },
ajax: {
query: ({ form }) => {
let queryParams: any = Object.assign({}, JSON.parse(JSON.stringify(form)))
if (options?.params) {
queryParams = Object.assign(queryParams, options.params)
}
return new Promise(async (resolve) => {
resolve(await getListApi(queryParams))
})
}
}
}
}
}
if (options.exportListApi) {
options.exportConfig = {
filename: options?.exportName,
//
type: 'csv',
//
modes: options?.getAllListApi ? ['current', 'all'] : ['current'],
columns: options?.allSchemas?.printSchema
}
}
}
//
const getPageConfig = (options: XTableProps) => {
const { pagination, pagerConfig, treeConfig } = options
if (treeConfig) {
options.treeConfig = options.treeConfig
return
}
if (pagerConfig) return
if (pagination) {
if (isBoolean(pagination)) {
options.pagerConfig = {
border: false, //
background: true, //
perfect: false, //
pageSize: 10, //
pagerCount: 7, //
autoHidden: false, //
pageSizes: [5, 10, 20, 30, 50, 100], //
layouts: [
'PrevJump',
'PrevPage',
'JumpNumber',
'NextPage',
'NextJump',
'Sizes',
'FullJump',
'Total'
]
}
return
}
options.pagerConfig = pagination
} else {
if (pagination != false) {
options.pagerConfig = {
border: false, //
background: true, //
perfect: false, //
pageSize: 10, //
pagerCount: 7, //
autoHidden: false, //
pageSizes: [5, 10, 20, 30, 50, 100], //
layouts: [
'PrevJump',
'PrevPage',
'JumpNumber',
'NextPage',
'NextJump',
'Sizes',
'FullJump',
'Total'
]
}
}
}
}
// tool bar
const getToolBarConfig = (options: XTableProps) => {
const { toolBar, toolbarConfig, topActionSlots } = options
if (toolbarConfig) return
if (toolBar) {
if (!isBoolean(toolBar)) {
options.toolbarConfig = toolBar
return
}
} else if (!topActionSlots) {
options.toolbarConfig = {
slots: { buttons: 'toolbar_buttons' }
}
}
}
//
const reload = () => {
const g = unref(xGrid)
if (!g) {
return
}
g.commitProxy('query')
}
//
const deleteData = async (ids: string | number) => {
const g = unref(xGrid)
if (!g) {
return
}
const options = innerProps.value || props.options
if (!options.deleteApi) {
console.error('未传入delListApi')
return
}
return new Promise(async () => {
message.delConfirm().then(async () => {
await (options?.deleteApi && options?.deleteApi(ids))
message.success(t('common.delSuccess'))
//
reload()
})
})
}
//
const exportList = async (fileName?: string) => {
const g = unref(xGrid)
if (!g) {
return
}
const options = innerProps.value || props.options
if (!options?.exportListApi) {
console.error('未传入exportListApi')
return
}
const queryParams = Object.assign({}, JSON.parse(JSON.stringify(g.getProxyInfo()?.form)))
message.exportConfirm().then(async () => {
const res = await (options?.exportListApi && options?.exportListApi(queryParams))
download.excel(res as unknown as Blob, fileName ? fileName : 'excel.xls')
})
}
//
const getSearchData = () => {
const g = unref(xGrid)
if (!g) {
return
}
const queryParams = Object.assign({}, JSON.parse(JSON.stringify(g.getProxyInfo()?.form)))
return queryParams
}
const setProps = (prop: Partial<XTableProps>) => {
innerProps.value = { ...unref(innerProps), ...prop }
}
defineExpose({ reload, Ref: xGrid, getSearchData, deleteData, exportList })
emit('register', { reload, getSearchData, setProps, deleteData, exportList })
</script>
<style lang="scss">
@import './style/index.scss';
</style>

View File

@ -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';

View File

@ -0,0 +1,6 @@
@import 'vxe-table/styles/variable.scss';
@import 'vxe-table/styles/modules.scss';
// @import './theme/light.scss';
i {
border-color: initial;
}

View File

@ -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';

View File

@ -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

View File

@ -4,6 +4,7 @@ import { Form } from '@/components/Form'
import { Table } from '@/components/Table'
import { Search } from '@/components/Search'
import { XModal } from '@/components/XModal'
import { XTable } from '@/components/XTable'
import { XButton, XTextButton } from '@/components/XButton'
import { DictTag } from '@/components/DictTag'
import { ContentWrap } from '@/components/ContentWrap'
@ -15,6 +16,7 @@ export const setupGlobCom = (app: App<Element>): void => {
app.component('Table', Table)
app.component('Search', Search)
app.component('XModal', XModal)
app.component('XTable', XTable)
app.component('XButton', XButton)
app.component('XTextButton', XTextButton)
app.component('DictTag', DictTag)

View File

@ -165,7 +165,7 @@ const filterSearchSchema = (crudSchema: VxeCrudSchema): VxeFormItemProps[] => {
// 添加搜索按钮
const buttons: VxeFormItemProps = {
span: 24,
align: 'center',
align: 'right',
collapseNode: searchSchema.length > spanLength,
itemRender: {
name: '$buttons',

View File

@ -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]
}

View File

@ -26,10 +26,10 @@ import '@/styles/index.scss'
import '@/plugins/animate.css'
// 路由
import { setupRouter } from './router'
import router, { setupRouter } from '@/router'
// 权限
import { setupAuth } from './directives'
import { setupAuth } from '@/directives'
import { createApp } from 'vue'
@ -53,6 +53,8 @@ const setupAll = async () => {
setupAuth(app)
await router.isReady()
app.mount('#app')
}

View File

@ -1,9 +1,7 @@
import { App, unref, watch } from 'vue'
import { App, unref } from 'vue'
import XEUtils from 'xe-utils'
import './index.scss'
import './renderer'
import { i18n } from '@/plugins/vueI18n'
import { useAppStore } from '@/store/modules/app'
import zhCN from 'vxe-table/lib/locale/lang/zh-CN'
import enUS from 'vxe-table/lib/locale/lang/en-US'
import {
@ -46,21 +44,6 @@ import {
Table
} from 'vxe-table'
const appStore = useAppStore()
watch(
() => appStore.getIsDark,
() => {
if (appStore.getIsDark) {
import('./theme/dark.scss')
} else {
import('./theme/light.scss')
}
},
{
deep: true,
immediate: true
}
)
// 全局默认参数
VXETable.setup({
size: 'medium', // 全局尺寸

View File

@ -4,3 +4,4 @@ import './dict'
import './html'
import './link'
import './img'
import './preview'

View File

@ -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>
)
}
}
})

View File

@ -6,15 +6,11 @@ import { isRelogin } from '@/config/axios/service'
import { getAccessToken } from '@/utils/auth'
import { useTitle } from '@/hooks/web/useTitle'
import { useNProgress } from '@/hooks/web/useNProgress'
import { CACHE_KEY, useCache } from '@/hooks/web/useCache'
import { usePageLoading } from '@/hooks/web/usePageLoading'
import { useDictStoreWithOut } from '@/store/modules/dict'
import { useUserStoreWithOut } from '@/store/modules/user'
import { usePermissionStoreWithOut } from '@/store/modules/permission'
import { getInfoApi } from '@/api/login'
import { listSimpleDictDataApi } from '@/api/system/dict/dict.data'
const { wsCache } = useCache()
const { start, done } = useNProgress()
@ -50,10 +46,8 @@ router.beforeEach(async (to, from, next) => {
const dictStore = useDictStoreWithOut()
const userStore = useUserStoreWithOut()
const permissionStore = usePermissionStoreWithOut()
const dictMap = wsCache.get(CACHE_KEY.DICT_CACHE)
if (!dictMap) {
const res = await listSimpleDictDataApi()
dictStore.setDictMap(res)
if (!dictStore.getIsSetDict) {
dictStore.setDictMap()
}
if (!userStore.getIsSetUser) {
isRelogin.show = true

View File

@ -3,6 +3,7 @@ import { store } from '../index'
import { DictDataVO } from '@/api/system/dict/types'
import { CACHE_KEY, useCache } from '@/hooks/web/useCache'
const { wsCache } = useCache('sessionStorage')
import { listSimpleDictDataApi } from '@/api/system/dict/dict.data'
export interface DictValueType {
value: any
@ -16,45 +17,54 @@ export interface DictTypeType {
}
export interface DictState {
dictMap: Map<string, any>
isSetDict: boolean
}
export const useDictStore = defineStore('dict', {
state: (): DictState => ({
dictMap: new Map<string, any>()
dictMap: new Map<string, any>(),
isSetDict: false
}),
getters: {
getDictMap(): Recordable {
const dictMap = wsCache.get(CACHE_KEY.DICT_CACHE)
return dictMap ? dictMap : this.dictMap
},
getHasDictData(): boolean {
if (this.dictMap.size > 0) {
return true
} else {
return false
if (dictMap) {
this.dictMap = dictMap
}
return this.dictMap
},
getIsSetDict(): boolean {
return this.isSetDict
}
},
actions: {
setDictMap(dictMap: Recordable) {
// 设置数据
const dictDataMap = new Map<string, any>()
dictMap.forEach((dictData: DictDataVO) => {
// 获得 dictType 层级
const enumValueObj = dictDataMap[dictData.dictType]
if (!enumValueObj) {
dictDataMap[dictData.dictType] = []
}
// 处理 dictValue 层级
dictDataMap[dictData.dictType].push({
value: dictData.value,
label: dictData.label,
colorType: dictData.colorType,
cssClass: dictData.cssClass
async setDictMap() {
const dictMap = wsCache.get(CACHE_KEY.DICT_CACHE)
if (dictMap) {
this.dictMap = dictMap
this.isSetDict = true
} else {
const res = await listSimpleDictDataApi()
// 设置数据
const dictDataMap = new Map<string, any>()
res.forEach((dictData: DictDataVO) => {
// 获得 dictType 层级
const enumValueObj = dictDataMap[dictData.dictType]
if (!enumValueObj) {
dictDataMap[dictData.dictType] = []
}
// 处理 dictValue 层级
dictDataMap[dictData.dictType].push({
value: dictData.value,
label: dictData.label,
colorType: dictData.colorType,
cssClass: dictData.cssClass
})
})
})
this.dictMap = dictDataMap
wsCache.set(CACHE_KEY.DICT_CACHE, dictDataMap, { exp: 60 }) // 60 秒 过期
this.dictMap = dictDataMap
this.isSetDict = true
wsCache.set(CACHE_KEY.DICT_CACHE, dictDataMap, { exp: 60 }) // 60 秒 过期
}
}
}
})

View File

@ -36,45 +36,30 @@ export const formatToken = (token: string): string => {
}
// ========== 账号相关 ==========
const UsernameKey = 'USERNAME'
const PasswordKey = 'PASSWORD'
const RememberMeKey = 'REMEMBER_ME'
const LoginFormKey = 'LOGINFORM'
export const getUsername = () => {
return wsCache.get(UsernameKey)
export type LoginFormType = {
tenantName: string
username: string
password: string
rememberMe: boolean
}
export const setUsername = (username: string) => {
wsCache.set(UsernameKey, username, { exp: 30 * 24 * 60 * 60 })
export const getLoginForm = () => {
const loginForm: LoginFormType = wsCache.get(LoginFormKey)
if (loginForm) {
loginForm.password = decrypt(loginForm.password) as string
}
return loginForm
}
export const removeUsername = () => {
wsCache.delete(UsernameKey)
export const setLoginForm = (loginForm: LoginFormType) => {
loginForm.password = encrypt(loginForm.password) as string
wsCache.set(LoginFormKey, loginForm, { exp: 30 * 24 * 60 * 60 })
}
export const getPassword = () => {
const password = wsCache.get(PasswordKey)
return password ? decrypt(password) : undefined
}
export const setPassword = (password: string) => {
wsCache.set(PasswordKey, encrypt(password), { exp: 30 * 24 * 60 * 60 })
}
export const removePassword = () => {
wsCache.delete(PasswordKey)
}
export const getRememberMe = () => {
return wsCache.get(RememberMeKey) === true
}
export const setRememberMe = (rememberMe: boolean) => {
wsCache.set(RememberMeKey, rememberMe, { exp: 30 * 24 * 60 * 60 })
}
export const removeRememberMe = () => {
wsCache.delete(RememberMeKey)
export const removeLoginForm = () => {
wsCache.delete(LoginFormKey)
}
// ========== 租户相关 ==========

View File

@ -148,7 +148,6 @@ import { useIcon } from '@/hooks/web/useIcon'
import { useMessage } from '@/hooks/web/useMessage'
import { required } from '@/utils/formRules'
import * as authUtil from '@/utils/auth'
import { decrypt } from '@/utils/jsencrypt'
import { Verify } from '@/components/Verifition'
import { usePermissionStore } from '@/store/modules/permission'
import * as LoginApi from '@/api/login'
@ -180,10 +179,6 @@ const loginData = reactive({
isShowPassword: false,
captchaEnable: import.meta.env.VITE_APP_CAPTCHA_ENABLE,
tenantEnable: import.meta.env.VITE_APP_TENANT_ENABLE,
token: '',
loading: {
signIn: false
},
loginForm: {
tenantName: '芋道源码',
username: 'admin',
@ -194,22 +189,10 @@ const loginData = reactive({
})
const socialList = [
{
icon: 'ant-design:github-filled',
type: 0
},
{
icon: 'ant-design:wechat-filled',
type: 30
},
{
icon: 'ant-design:alipay-circle-filled',
type: 0
},
{
icon: 'ant-design:dingtalk-circle-filled',
type: 20
}
{ icon: 'ant-design:github-filled', type: 0 },
{ icon: 'ant-design:wechat-filled', type: 30 },
{ icon: 'ant-design:alipay-circle-filled', type: 0 },
{ icon: 'ant-design:dingtalk-circle-filled', type: 20 }
]
//
@ -232,18 +215,15 @@ const getTenantId = async () => {
}
//
const getCookie = () => {
const username = authUtil.getUsername()
const password = authUtil.getPassword()
? decrypt(authUtil.getPassword() as unknown as string)
: undefined
const rememberMe = authUtil.getRememberMe()
const tenantName = authUtil.getTenantName()
loginData.loginForm = {
...loginData.loginForm,
username: username ? username : loginData.loginForm.username,
password: password ? password : loginData.loginForm.password,
rememberMe: rememberMe ? true : false,
tenantName: tenantName ? tenantName : loginData.loginForm.tenantName
const loginForm = authUtil.getLoginForm()
if (loginForm) {
loginData.loginForm = {
...loginData.loginForm,
username: loginForm.username ? loginForm.username : loginData.loginForm.username,
password: loginForm.password ? loginForm.password : loginData.loginForm.password,
rememberMe: loginForm.rememberMe ? true : false,
tenantName: loginForm.tenantName ? loginForm.tenantName : loginData.loginForm.tenantName
}
}
}
//
@ -266,15 +246,9 @@ const handleLogin = async (params) => {
background: 'rgba(0, 0, 0, 0.7)'
})
if (loginData.loginForm.rememberMe) {
authUtil.setUsername(loginData.loginForm.username)
authUtil.setPassword(loginData.loginForm.password)
authUtil.setRememberMe(loginData.loginForm.rememberMe)
authUtil.setTenantName(loginData.loginForm.tenantName)
authUtil.setLoginForm(loginData.loginForm)
} else {
authUtil.removeUsername()
authUtil.removePassword()
authUtil.removeRememberMe()
authUtil.removeTenantName()
authUtil.removeLoginForm()
}
authUtil.setToken(res)
if (!redirect.value) {

View File

@ -1,7 +1,7 @@
<template>
<ContentWrap>
<!-- 列表 -->
<vxe-grid ref="xGrid" v-bind="gridOptions" class="xtable-scrollbar">
<XTable @register="registerTable">
<template #duration_default="{ row }">
<span>{{ row.duration + 'ms' }}</span>
</template>
@ -17,7 +17,7 @@
@click="handleDetail(row)"
/>
</template>
</vxe-grid>
</XTable>
</ContentWrap>
<XModal v-model="dialogVisible" :title="dialogTitle">
<!-- 对话框(详情) -->
@ -38,15 +38,14 @@
<script setup lang="ts" name="ApiAccessLog">
import { ref } from 'vue'
import { useI18n } from '@/hooks/web/useI18n'
import { useVxeGrid } from '@/hooks/web/useVxeGrid'
import { VxeGridInstance } from 'vxe-table'
import { useXTable } from '@/hooks/web/useXTable'
import { allSchemas } from './apiAccessLog.data'
import * as ApiAccessLogApi from '@/api/infra/apiAccessLog'
const { t } = useI18n() //
//
const xGrid = ref<VxeGridInstance>() // Grid Ref
const { gridOptions } = useVxeGrid<ApiAccessLogApi.ApiAccessLogVO>({
const [registerTable] = useXTable({
allSchemas: allSchemas,
topActionSlots: false,
getListApi: ApiAccessLogApi.getApiAccessLogPageApi

View File

@ -1,14 +1,14 @@
<template>
<ContentWrap>
<!-- 列表 -->
<vxe-grid ref="xGrid" v-bind="gridOptions" class="xtable-scrollbar">
<XTable @register="registerTable">
<!-- 操作导出 -->
<template #toolbar_buttons>
<XButton
type="warning"
preIcon="ep:download"
:title="t('action.export')"
@click="handleExport()"
@click="exportList('错误数据.xls')"
/>
</template>
<template #duration_default="{ row }">
@ -40,7 +40,7 @@
@click="handleProcessClick(row, InfraApiErrorLogProcessStatusEnum.IGNORE, '已忽略')"
/>
</template>
</vxe-grid>
</XTable>
</ContentWrap>
<XModal v-model="dialogVisible" :title="dialogTitle">
<!-- 对话框(详情) -->
@ -54,18 +54,17 @@
<script setup lang="ts" name="ApiErrorLog">
import { ref } from 'vue'
import { useI18n } from '@/hooks/web/useI18n'
import { useVxeGrid } from '@/hooks/web/useVxeGrid'
import { VxeGridInstance } from 'vxe-table'
import { useXTable } from '@/hooks/web/useXTable'
import { allSchemas } from './apiErrorLog.data'
import * as ApiErrorLogApi from '@/api/infra/apiErrorLog'
import { InfraApiErrorLogProcessStatusEnum } from '@/utils/constants'
import { useMessage } from '@/hooks/web/useMessage'
const message = useMessage()
const { t } = useI18n() //
const message = useMessage()
// ========== ==========
const xGrid = ref<VxeGridInstance>() // Grid Ref
const { gridOptions, getList, exportList } = useVxeGrid<ApiErrorLogApi.ApiErrorLogVO>({
const [registerTable, { reload, exportList }] = useXTable({
allSchemas: allSchemas,
getListApi: ApiErrorLogApi.getApiErrorLogPageApi,
exportListApi: ApiErrorLogApi.exportApiErrorLogApi
@ -82,10 +81,7 @@ const handleDetail = (row: ApiErrorLogApi.ApiErrorLogVO) => {
dialogTitle.value = t('action.detail')
dialogVisible.value = true
}
//
const handleExport = async () => {
await exportList(xGrid, '错误数据.xls')
}
//
const handleProcessClick = (
row: ApiErrorLogApi.ApiErrorLogVO,
@ -100,7 +96,7 @@ const handleProcessClick = (
})
.finally(async () => {
//
await getList(xGrid)
await reload()
})
.catch(() => {})
}

View File

@ -1,7 +1,7 @@
<template>
<ContentWrap>
<!-- 列表 -->
<vxe-grid ref="xGrid" v-bind="gridOptions" class="xtable-scrollbar">
<XTable @register="registerTable">
<template #toolbar_buttons>
<!-- 操作导入 -->
<XButton
@ -32,7 +32,7 @@
preIcon="ep:delete"
:title="t('action.del')"
v-hasPermi="['infra:codegen:delete']"
@click="handleDelete(row.id)"
@click="deleteData(row.id)"
/>
<!-- 操作同步 -->
<XTextButton
@ -49,20 +49,19 @@
@click="handleGenTable(row)"
/>
</template>
</vxe-grid>
</XTable>
</ContentWrap>
<!-- 弹窗导入表 -->
<ImportTable ref="importRef" @ok="handleQuery()" />
<ImportTable ref="importRef" @ok="reload()" />
<!-- 弹窗预览代码 -->
<Preview ref="previewRef" />
</template>
<script setup lang="ts" name="Codegen">
import { ref } from 'vue'
import { useRouter } from 'vue-router'
import { VxeGridInstance } from 'vxe-table'
import { useI18n } from '@/hooks/web/useI18n'
import { useMessage } from '@/hooks/web/useMessage'
import { useVxeGrid } from '@/hooks/web/useVxeGrid'
import { useXTable } from '@/hooks/web/useXTable'
import download from '@/utils/download'
import * as CodegenApi from '@/api/infra/codegen'
import { CodegenTableVO } from '@/api/infra/codegen/types'
@ -73,8 +72,7 @@ const { t } = useI18n() // 国际化
const message = useMessage() //
const { push } = useRouter() //
//
const xGrid = ref<VxeGridInstance>() // Grid Ref
const { gridOptions, getList, deleteData } = useVxeGrid<CodegenTableVO>({
const [registerTable, { reload, deleteData }] = useXTable({
allSchemas: allSchemas,
getListApi: CodegenApi.getCodegenTablePageApi,
deleteApi: CodegenApi.deleteCodegenTableApi
@ -105,17 +103,10 @@ const handleSynchDb = (row: CodegenTableVO) => {
message.success('同步成功')
})
}
//
const handleGenTable = async (row: CodegenTableVO) => {
const res = await CodegenApi.downloadCodegenApi(row.id)
download.zip(res, 'codegen-' + row.className + '.zip')
}
//
const handleDelete = async (rowId: number) => {
await deleteData(xGrid, rowId)
}
//
const handleQuery = async () => {
await getList(xGrid)
}
</script>

View File

@ -1,7 +1,7 @@
<template>
<ContentWrap>
<!-- 列表 -->
<vxe-grid ref="xGrid" v-bind="gridOptions" class="xtable-scrollbar">
<XTable @register="registerTable">
<template #toolbar_buttons>
<!-- 操作新增 -->
<XButton
@ -17,7 +17,7 @@
preIcon="ep:download"
:title="t('action.export')"
v-hasPermi="['infra:config:export']"
@click="handleExport()"
@click="exportList('配置.xls')"
/>
</template>
<template #visible_default="{ row }">
@ -43,10 +43,10 @@
preIcon="ep:delete"
:title="t('action.del')"
v-hasPermi="['infra:config:delete']"
@click="handleDelete(row.id)"
@click="deleteData(row.id)"
/>
</template>
</vxe-grid>
</XTable>
</ContentWrap>
<XModal v-model="dialogVisible" :title="dialogTitle">
@ -87,8 +87,7 @@
import { ref, unref } from 'vue'
import { useI18n } from '@/hooks/web/useI18n'
import { useMessage } from '@/hooks/web/useMessage'
import { useVxeGrid } from '@/hooks/web/useVxeGrid'
import { VxeGridInstance } from 'vxe-table'
import { useXTable } from '@/hooks/web/useXTable'
import { FormExpose } from '@/components/Form'
// import
import * as ConfigApi from '@/api/infra/config'
@ -97,8 +96,7 @@ import { rules, allSchemas } from './config.data'
const { t } = useI18n() //
const message = useMessage() //
//
const xGrid = ref<VxeGridInstance>() // Grid Ref
const { gridOptions, getList, deleteData, exportList } = useVxeGrid<ConfigApi.ConfigVO>({
const [registerTable, { reload, deleteData, exportList }] = useXTable({
allSchemas: allSchemas,
getListApi: ConfigApi.getConfigPageApi,
deleteApi: ConfigApi.deleteConfigApi,
@ -125,11 +123,6 @@ const handleCreate = () => {
setDialogTile('create')
}
//
const handleExport = async () => {
await exportList(xGrid, '配置.xls')
}
//
const handleUpdate = async (rowId: number) => {
setDialogTile('update')
@ -145,11 +138,6 @@ const handleDetail = async (rowId: number) => {
detailData.value = res
}
//
const handleDelete = async (rowId: number) => {
await deleteData(xGrid, rowId)
}
//
const submitForm = async () => {
const elForm = unref(formRef)?.getElFormRef()
@ -171,7 +159,7 @@ const submitForm = async () => {
} finally {
actionLoading.value = false
//
await getList(xGrid)
await reload()
}
}
})

View File

@ -1,7 +1,7 @@
<template>
<ContentWrap>
<!-- 列表 -->
<vxe-grid ref="xGrid" v-bind="gridOptions" class="xtable-scrollbar">
<XTable @register="registerTable">
<template #toolbar_buttons>
<XButton
type="primary"
@ -31,10 +31,10 @@
preIcon="ep:delete"
:title="t('action.del')"
v-hasPermi="['infra:data-source-config:delete']"
@click="handleDelete(row.id)"
@click="deleteData(row.id)"
/>
</template>
</vxe-grid>
</XTable>
</ContentWrap>
<XModal v-model="dialogVisible" :title="dialogTitle">
<!-- 对话框(添加 / 修改) -->
@ -69,9 +69,8 @@
// import
import { ref, unref } from 'vue'
import { useI18n } from '@/hooks/web/useI18n'
import { useXTable } from '@/hooks/web/useXTable'
import { useMessage } from '@/hooks/web/useMessage'
import { useVxeGrid } from '@/hooks/web/useVxeGrid'
import { VxeGridInstance } from 'vxe-table'
import { FormExpose } from '@/components/Form'
// import
import * as DataSourceConfiggApi from '@/api/infra/dataSourceConfig'
@ -80,8 +79,7 @@ import { rules, allSchemas } from './dataSourceConfig.data'
const { t } = useI18n() //
const message = useMessage() //
//
const xGrid = ref<VxeGridInstance>() // Grid Ref
const { gridOptions, getList, deleteData } = useVxeGrid<DataSourceConfiggApi.DataSourceConfigVO>({
const [registerTable, { reload, deleteData }] = useXTable({
allSchemas: allSchemas,
isList: true,
getListApi: DataSourceConfiggApi.getDataSourceConfigListApi,
@ -123,11 +121,6 @@ const handleDetail = async (rowId: number) => {
setDialogTile('detail')
}
//
const handleDelete = async (rowId: number) => {
await deleteData(xGrid, rowId)
}
//
const submitForm = async () => {
const elForm = unref(formRef)?.getElFormRef()
@ -149,7 +142,7 @@ const submitForm = async () => {
} finally {
loading.value = false
//
await getList(xGrid)
await reload()
}
}
})

View File

@ -1,7 +1,7 @@
<template>
<ContentWrap>
<!-- 列表 -->
<vxe-grid ref="xGrid" v-bind="gridOptions" class="xtable-scrollbar">
<XTable @register="registerTable">
<template #toolbar_buttons>
<!-- 操作新增 -->
<XButton
@ -41,10 +41,10 @@
preIcon="ep:delete"
:title="t('action.del')"
v-hasPermi="['infra:file-config:delete']"
@click="handleDelete(row.id)"
@click="deleteData(row.id)"
/>
</template>
</vxe-grid>
</XTable>
</ContentWrap>
<XModal v-model="dialogVisible" :title="dialogTitle">
<!-- 对话框(添加 / 修改) -->
@ -173,8 +173,7 @@ import {
} from 'element-plus'
import { useI18n } from '@/hooks/web/useI18n'
import { useMessage } from '@/hooks/web/useMessage'
import { useVxeGrid } from '@/hooks/web/useVxeGrid'
import { VxeGridInstance } from 'vxe-table'
import { useXTable } from '@/hooks/web/useXTable'
// import
import * as FileConfigApi from '@/api/infra/fileConfig'
import { rules, allSchemas } from './fileConfig.data'
@ -183,8 +182,7 @@ import { getIntDictOptions, DICT_TYPE } from '@/utils/dict'
const { t } = useI18n() //
const message = useMessage() //
//
const xGrid = ref<VxeGridInstance>() // Grid Ref
const { gridOptions, getList, deleteData } = useVxeGrid<FileConfigApi.FileConfigVO>({
const [registerTable, { reload, deleteData }] = useXTable({
allSchemas: allSchemas,
getListApi: FileConfigApi.getFileConfigPageApi,
deleteApi: FileConfigApi.deleteFileConfigApi
@ -276,7 +274,7 @@ const handleMaster = (row: FileConfigApi.FileConfigVO) => {
.confirm('是否确认修改配置【 ' + row.name + ' 】为主配置?', t('common.reminder'))
.then(async () => {
await FileConfigApi.updateFileConfigMasterApi(row.id)
await getList(xGrid)
await reload()
})
}
@ -285,11 +283,6 @@ const handleTest = async (rowId: number) => {
message.alert('测试通过,上传文件成功!访问地址:' + res)
}
//
const handleDelete = async (rowId: number) => {
await deleteData(xGrid, rowId)
}
//
const submitForm = async (formEl: FormInstance | undefined) => {
if (!formEl) return
@ -308,7 +301,7 @@ const submitForm = async (formEl: FormInstance | undefined) => {
dialogVisible.value = false
} finally {
actionLoading.value = false
await getList(xGrid)
await reload()
}
}
})

View File

@ -23,7 +23,7 @@ const crudSchemas = reactive<VxeCrudSchema>({
field: 'url',
table: {
cellRender: {
name: 'XImg'
name: 'XPreview'
}
}
},

View File

@ -1,7 +1,7 @@
<template>
<ContentWrap>
<!-- 列表 -->
<vxe-grid ref="xGrid" v-bind="gridOptions" class="xtable-scrollbar">
<XTable @register="registerTable">
<template #toolbar_buttons>
<XButton
type="primary"
@ -21,10 +21,10 @@
preIcon="ep:delete"
:title="t('action.del')"
v-hasPermi="['infra:file:delete']"
@click="handleDelete(row.id)"
@click="deleteData(row.id)"
/>
</template>
</vxe-grid>
</XTable>
</ContentWrap>
<XModal v-model="dialogVisible" :title="dialogTitle">
<!-- 对话框(详情) -->
@ -85,8 +85,7 @@
import { ref, unref } from 'vue'
import { useI18n } from '@/hooks/web/useI18n'
import { useMessage } from '@/hooks/web/useMessage'
import { useVxeGrid } from '@/hooks/web/useVxeGrid'
import { VxeGridInstance } from 'vxe-table'
import { useXTable } from '@/hooks/web/useXTable'
import { ElUpload, ElImage, UploadInstance, UploadRawFile } from 'element-plus'
// import
import { allSchemas } from './fileList.data'
@ -98,8 +97,7 @@ const { t } = useI18n() // 国际化
const message = useMessage() //
//
const xGrid = ref<VxeGridInstance>() // Grid Ref
const { gridOptions, getList, deleteData } = useVxeGrid<FileApi.FileVO>({
const [registerTable, { reload, deleteData }] = useXTable({
allSchemas: allSchemas,
getListApi: FileApi.getFilePageApi,
deleteApi: FileApi.deleteFileApi
@ -145,7 +143,7 @@ const handleFileSuccess = async (response: any): Promise<void> => {
message.success('上传成功')
uploadDialogVisible.value = false
uploadDisabled.value = false
await getList(xGrid)
await reload()
}
//
const handleExceed = (): void => {
@ -164,11 +162,6 @@ const handleDetail = (row: FileApi.FileVO) => {
dialogVisible.value = true
}
//
const handleDelete = async (rowId: number) => {
await deleteData(xGrid, rowId)
}
// ========== ==========
const handleCopy = async (text: string) => {
const { copy, copied, isSupported } = useClipboard({ source: text })

View File

@ -1,14 +1,14 @@
<template>
<ContentWrap>
<!-- 列表 -->
<vxe-grid ref="xGrid" v-bind="gridOptions" class="xtable-scrollbar">
<XTable @register="registerTable">
<template #toolbar_buttons>
<XButton
type="warning"
preIcon="ep:download"
:title="t('action.export')"
v-hasPermi="['infra:job:export']"
@click="handleExport()"
@click="exportList('定时任务详情.xls')"
/>
</template>
<template #beginTime_default="{ row }">
@ -29,7 +29,7 @@
@click="handleDetail(row)"
/>
</template>
</vxe-grid>
</XTable>
</ContentWrap>
<XModal v-model="dialogVisible" :title="dialogTitle">
<!-- 对话框(详情) -->
@ -51,15 +51,13 @@
import { ref } from 'vue'
import dayjs from 'dayjs'
import { useI18n } from '@/hooks/web/useI18n'
import { useVxeGrid } from '@/hooks/web/useVxeGrid'
import { VxeGridInstance } from 'vxe-table'
import { useXTable } from '@/hooks/web/useXTable'
import * as JobLogApi from '@/api/infra/jobLog'
import { allSchemas } from './jobLog.data'
const { t } = useI18n() //
//
const xGrid = ref<VxeGridInstance>() // Grid Ref
const { gridOptions, exportList } = useVxeGrid<JobLogApi.JobLogVO>({
const [registerTable, { exportList }] = useXTable({
allSchemas: allSchemas,
getListApi: JobLogApi.getJobLogPageApi,
exportListApi: JobLogApi.exportJobLogApi
@ -79,8 +77,4 @@ const handleDetail = async (row: JobLogApi.JobLogVO) => {
dialogTitle.value = t('action.detail')
dialogVisible.value = true
}
//
const handleExport = async () => {
await exportList(xGrid, '定时任务详情.xls')
}
</script>

View File

@ -1,7 +1,7 @@
<template>
<ContentWrap>
<!-- 列表 -->
<vxe-grid ref="xGrid" v-bind="gridOptions" class="xtable-scrollbar">
<XTable @register="registerTable">
<template #toolbar_buttons>
<!-- 操作新增 -->
<XButton
@ -17,14 +17,14 @@
preIcon="ep:download"
:title="t('action.export')"
v-hasPermi="['infra:job:export']"
@click="handleExport()"
@click="exportList('定时任务.xls')"
/>
<XButton
type="info"
preIcon="ep:zoom-in"
title="执行日志"
v-hasPermi="['infra:job:query']"
@click="handleJobLog"
@click="handleJobLog()"
/>
</template>
<template #actionbtns_default="{ row }">
@ -46,7 +46,7 @@
preIcon="ep:delete"
:title="t('action.del')"
v-hasPermi="['infra:job:delete']"
@click="handleDelete(row.id)"
@click="deleteData(row.id)"
/>
<el-dropdown class="p-0.5" v-hasPermi="['infra:job:trigger', 'infra:job:query']">
<XTextButton :title="t('action.more')" postIcon="ep:arrow-down" />
@ -83,7 +83,7 @@
</template>
</el-dropdown>
</template>
</vxe-grid>
</XTable>
</ContentWrap>
<XModal v-model="dialogVisible" :title="dialogTitle">
<!-- 对话框(添加 / 修改) -->
@ -134,8 +134,7 @@ import { useRouter } from 'vue-router'
import { ElDropdown, ElDropdownMenu, ElDropdownItem } from 'element-plus'
import { useI18n } from '@/hooks/web/useI18n'
import { useMessage } from '@/hooks/web/useMessage'
import { useVxeGrid } from '@/hooks/web/useVxeGrid'
import { VxeGridInstance } from 'vxe-table'
import { useXTable } from '@/hooks/web/useXTable'
import { FormExpose } from '@/components/Form'
import { Crontab } from '@/components/Crontab'
import * as JobApi from '@/api/infra/job'
@ -147,8 +146,7 @@ const message = useMessage() // 消息弹窗
const { push } = useRouter()
//
const xGrid = ref<VxeGridInstance>() // Grid Ref
const { gridOptions, getList, deleteData, exportList } = useVxeGrid<JobApi.JobVO>({
const [registerTable, { reload, deleteData, exportList }] = useXTable({
allSchemas: allSchemas,
getListApi: JobApi.getJobPageApi,
deleteApi: JobApi.deleteJobApi,
@ -181,11 +179,6 @@ const handleCreate = () => {
setDialogTile('create')
}
//
const handleExport = async () => {
await exportList(xGrid, '定时任务.xls')
}
//
const handleUpdate = async (rowId: number) => {
setDialogTile('update')
@ -250,10 +243,6 @@ const parseTime = (time) => {
return time_str
}
//
const handleDelete = async (rowId: number) => {
await deleteData(xGrid, rowId)
}
const handleChangeStatus = async (row: JobApi.JobVO) => {
const text = row.status === InfraJobStatusEnum.STOP ? '开启' : '关闭'
const status =
@ -267,7 +256,7 @@ const handleChangeStatus = async (row: JobApi.JobVO) => {
: InfraJobStatusEnum.STOP
await JobApi.updateJobStatusApi(row.id, status)
message.success(text + '成功')
await getList(xGrid)
await reload()
})
.catch(() => {
row.status =
@ -277,7 +266,7 @@ const handleChangeStatus = async (row: JobApi.JobVO) => {
})
}
//
const handleJobLog = (rowId: number) => {
const handleJobLog = (rowId?: number) => {
if (rowId) {
push('/job/job-log?id=' + rowId)
} else {
@ -289,7 +278,7 @@ const handleRun = (row: JobApi.JobVO) => {
message.confirm('确认要立即执行一次' + row.name + '?', t('common.reminder')).then(async () => {
await JobApi.runJobApi(row.id)
message.success('执行成功')
await getList(xGrid)
await reload()
})
}
//
@ -312,7 +301,7 @@ const submitForm = async () => {
dialogVisible.value = false
} finally {
actionLoading.value = false
await getList(xGrid)
await reload()
}
}
})

View File

@ -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>

View File

@ -1,7 +1,7 @@
<template>
<ContentWrap>
<!-- 列表 -->
<vxe-grid ref="xGrid" v-bind="gridOptions" class="xtable-scrollbar">
<XTable @register="registerTable">
<template #toolbar_buttons>
<!-- 操作新增 -->
<XButton
@ -17,7 +17,7 @@
preIcon="ep:download"
:title="t('action.export')"
v-hasPermi="['pay:app:export']"
@click="handleExport()"
@click="exportList('应用信息.xls')"
/>
</template>
<template #actionbtns_default="{ row }">
@ -40,10 +40,10 @@
preIcon="ep:delete"
:title="t('action.del')"
v-hasPermi="['pay:app:delete']"
@click="handleDelete(row.id)"
@click="deleteData(row.id)"
/>
</template>
</vxe-grid>
</XTable>
</ContentWrap>
<XModal v-model="dialogVisible" :title="dialogTitle">
@ -79,8 +79,7 @@
import { ref, unref } from 'vue'
import { useI18n } from '@/hooks/web/useI18n'
import { useMessage } from '@/hooks/web/useMessage'
import { useVxeGrid } from '@/hooks/web/useVxeGrid'
import { VxeGridInstance } from 'vxe-table'
import { useXTable } from '@/hooks/web/useXTable'
import { FormExpose } from '@/components/Form'
import { rules, allSchemas } from './app.data'
import * as AppApi from '@/api/pay/app'
@ -89,8 +88,7 @@ const { t } = useI18n() // 国际化
const message = useMessage() //
//
const xGrid = ref<VxeGridInstance>() // Grid Ref
const { gridOptions, getList, deleteData, exportList } = useVxeGrid<AppApi.AppVO>({
const [registerTable, { reload, deleteData, exportList }] = useXTable({
allSchemas: allSchemas,
getListApi: AppApi.getAppPageApi,
deleteApi: AppApi.deleteAppApi,
@ -117,11 +115,6 @@ const handleCreate = () => {
setDialogTile('create')
}
//
const handleExport = async () => {
await exportList(xGrid, '应用信息.xls')
}
//
const handleUpdate = async (rowId: number) => {
setDialogTile('update')
@ -137,11 +130,6 @@ const handleDetail = async (rowId: number) => {
detailData.value = res
}
//
const handleDelete = async (rowId: number) => {
await deleteData(xGrid, rowId)
}
//
const submitForm = async () => {
const elForm = unref(formRef)?.getElFormRef()
@ -163,7 +151,7 @@ const submitForm = async () => {
} finally {
actionLoading.value = false
//
await getList(xGrid)
await reload()
}
}
})

View File

@ -1,7 +1,7 @@
<template>
<ContentWrap>
<!-- 列表 -->
<vxe-grid ref="xGrid" v-bind="gridOptions" class="xtable-scrollbar">
<XTable @register="registerTable">
<template #toolbar_buttons>
<!-- 操作新增 -->
<XButton
@ -17,7 +17,7 @@
preIcon="ep:download"
:title="t('action.export')"
v-hasPermi="['pay:merchant:export']"
@click="handleExport()"
@click="exportList('商户列表.xls')"
/>
</template>
<template #actionbtns_default="{ row }">
@ -40,10 +40,10 @@
preIcon="ep:delete"
:title="t('action.del')"
v-hasPermi="['pay:merchant:delete']"
@click="handleDelete(row.id)"
@click="deleteData(row.id)"
/>
</template>
</vxe-grid>
</XTable>
</ContentWrap>
<XModal v-model="dialogVisible" :title="dialogTitle">
<!-- 对话框(添加 / 修改) -->
@ -78,8 +78,7 @@
import { ref, unref } from 'vue'
import { useI18n } from '@/hooks/web/useI18n'
import { useMessage } from '@/hooks/web/useMessage'
import { useVxeGrid } from '@/hooks/web/useVxeGrid'
import { VxeGridInstance } from 'vxe-table'
import { useXTable } from '@/hooks/web/useXTable'
import { FormExpose } from '@/components/Form'
import { rules, allSchemas } from './merchant.data'
import * as MerchantApi from '@/api/pay/merchant'
@ -87,8 +86,7 @@ import * as MerchantApi from '@/api/pay/merchant'
const { t } = useI18n() //
const message = useMessage() //
//
const xGrid = ref<VxeGridInstance>() // Grid Ref
const { gridOptions, getList, deleteData, exportList } = useVxeGrid<MerchantApi.MerchantVO>({
const [registerTable, { reload, deleteData, exportList }] = useXTable({
allSchemas: allSchemas,
getListApi: MerchantApi.getMerchantPageApi,
deleteApi: MerchantApi.deleteMerchantApi,
@ -115,11 +113,6 @@ const handleCreate = () => {
setDialogTile('create')
}
//
const handleExport = async () => {
await exportList(xGrid, '商户列表.xls')
}
//
const handleUpdate = async (rowId: number) => {
setDialogTile('update')
@ -135,11 +128,6 @@ const handleDetail = async (rowId: number) => {
detailData.value = res
}
//
const handleDelete = async (rowId: number) => {
await deleteData(xGrid, rowId)
}
//
const submitForm = async () => {
const elForm = unref(formRef)?.getElFormRef()
@ -161,7 +149,7 @@ const submitForm = async () => {
} finally {
actionLoading.value = false
//
await getList(xGrid)
await reload()
}
}
})

View File

@ -1,7 +1,7 @@
<template>
<ContentWrap>
<!-- 列表 -->
<vxe-grid ref="xGrid" v-bind="gridOptions" class="xtable-scrollbar">
<XTable @register="registerTable">
<template #toolbar_buttons>
<!-- 操作新增 -->
<XButton
@ -17,7 +17,7 @@
preIcon="ep:download"
:title="t('action.export')"
v-hasPermi="['pay:order:export']"
@click="handleExport()"
@click="exportList('订单数据.xls')"
/>
</template>
<template #actionbtns_default="{ row }">
@ -29,7 +29,7 @@
@click="handleDetail(row.id)"
/>
</template>
</vxe-grid>
</XTable>
</ContentWrap>
<XModal v-model="dialogVisible" :title="dialogTitle">
<!-- 对话框(详情) -->
@ -44,15 +44,13 @@
<script setup lang="ts" name="Order">
import { ref } from 'vue'
import { useI18n } from '@/hooks/web/useI18n'
import { useVxeGrid } from '@/hooks/web/useVxeGrid'
import { VxeGridInstance } from 'vxe-table'
import { useXTable } from '@/hooks/web/useXTable'
import { allSchemas } from './order.data'
import * as OrderApi from '@/api/pay/order'
const { t } = useI18n() //
//
const xGrid = ref<VxeGridInstance>() // Grid Ref
const { gridOptions, exportList } = useVxeGrid<OrderApi.OrderVO>({
const [registerTable, { exportList }] = useXTable({
allSchemas: allSchemas,
getListApi: OrderApi.getOrderPageApi,
exportListApi: OrderApi.exportOrderApi
@ -74,10 +72,6 @@ const setDialogTile = (type: string) => {
const handleCreate = () => {
setDialogTile('create')
}
//
const handleExport = async () => {
await exportList(xGrid, '订单数据.xls')
}
//
const handleDetail = async (rowId: number) => {

View File

@ -1,7 +1,7 @@
<template>
<ContentWrap>
<!-- 列表 -->
<vxe-grid ref="xGrid" v-bind="gridOptions" class="xtable-scrollbar">
<XTable @register="registerTable">
<template #toolbar_buttons>
<!-- 操作导出 -->
<XButton
@ -9,7 +9,7 @@
preIcon="ep:download"
:title="t('action.export')"
v-hasPermi="['pay:refund:export']"
@click="handleExport()"
@click="exportList('退款订单.xls')"
/>
</template>
<template #actionbtns_default="{ row }">
@ -21,7 +21,7 @@
@click="handleDetail(row.id)"
/>
</template>
</vxe-grid>
</XTable>
</ContentWrap>
<XModal v-model="dialogVisible" :title="t('action.detail')">
@ -36,26 +36,19 @@
<script setup lang="ts" name="Refund">
import { ref } from 'vue'
import { useI18n } from '@/hooks/web/useI18n'
import { useVxeGrid } from '@/hooks/web/useVxeGrid'
import { VxeGridInstance } from 'vxe-table'
import { useXTable } from '@/hooks/web/useXTable'
import { allSchemas } from './refund.data'
import * as RefundApi from '@/api/pay/refund'
const { t } = useI18n() //
//
const xGrid = ref<VxeGridInstance>() // Grid Ref
const { gridOptions, exportList } = useVxeGrid<RefundApi.RefundVO>({
const [registerTable, { exportList }] = useXTable({
allSchemas: allSchemas,
getListApi: RefundApi.getRefundPageApi,
exportListApi: RefundApi.exportRefundApi
})
//
const handleExport = async () => {
await exportList(xGrid, '退款订单.xls')
}
// ========== CRUD ==========
const dialogVisible = ref(false) //
const detailData = ref() // Ref

View File

@ -1,7 +1,7 @@
<template>
<ContentWrap>
<!-- 列表 -->
<vxe-grid ref="xGrid" v-bind="gridOptions" show-overflow class="xtable-scrollbar">
<XTable ref="xGrid" @register="registerTable" show-overflow>
<template #toolbar_buttons>
<!-- 操作新增 -->
<XButton
@ -11,8 +11,8 @@
v-hasPermi="['system:dept:create']"
@click="handleCreate()"
/>
<XButton title="展开所有" @click="xGrid?.setAllTreeExpand(true)" />
<XButton title="关闭所有" @click="xGrid?.clearTreeExpand()" />
<XButton title="展开所有" @click="xGrid?.Ref.setAllTreeExpand(true)" />
<XButton title="关闭所有" @click="xGrid?.Ref.clearTreeExpand()" />
</template>
<template #leaderUserId_default="{ row }">
<span>{{ userNicknameFormat(row) }}</span>
@ -30,10 +30,10 @@
preIcon="ep:delete"
:title="t('action.del')"
v-hasPermi="['system:dept:delete']"
@click="handleDelete(row.id)"
@click="deleteData(row.id)"
/>
</template>
</vxe-grid>
</XTable>
</ContentWrap>
<!-- 添加或修改菜单对话框 -->
<XModal id="deptModel" v-model="dialogVisible" :title="dialogTitle">
@ -77,11 +77,10 @@
<script setup lang="ts" name="Dept">
import { nextTick, onMounted, ref, unref } from 'vue'
import { ElSelect, ElTreeSelect, ElOption } from 'element-plus'
import { VxeGridInstance } from 'vxe-table'
import { handleTree, defaultProps } from '@/utils/tree'
import { useI18n } from '@/hooks/web/useI18n'
import { useMessage } from '@/hooks/web/useMessage'
import { useVxeGrid } from '@/hooks/web/useVxeGrid'
import { useXTable } from '@/hooks/web/useXTable'
import { FormExpose } from '@/components/Form'
import { allSchemas, rules } from './dept.data'
import * as DeptApi from '@/api/system/dept'
@ -90,7 +89,7 @@ import { getListSimpleUsersApi, UserVO } from '@/api/system/user'
const { t } = useI18n() //
const message = useMessage() //
//
const xGrid = ref<VxeGridInstance>() // Grid Ref
const xGrid = ref<any>() // Grid Ref
const treeConfig = {
transform: true,
rowField: 'id',
@ -119,7 +118,7 @@ const getTree = async () => {
dept.children = handleTree(res)
deptOptions.value.push(dept)
}
const { gridOptions, getList, deleteData } = useVxeGrid<DeptApi.DeptVO>({
const [registerTable, { reload, deleteData }] = useXTable({
allSchemas: allSchemas,
treeConfig: treeConfig,
getListApi: DeptApi.getDeptPageApi,
@ -168,17 +167,12 @@ const submitForm = async () => {
dialogVisible.value = false
} finally {
actionLoading.value = false
await getList(xGrid)
await reload()
}
}
})
}
//
const handleDelete = async (rowId: number) => {
await deleteData(xGrid, rowId)
}
const userNicknameFormat = (row) => {
if (!row || !row.leaderUserId) {
return '未设置'

View File

@ -7,12 +7,7 @@
<span>字典分类</span>
</div>
</template>
<vxe-grid
ref="xTypeGrid"
v-bind="typeGridOptions"
@cell-click="cellClickEvent"
class="xtable-scrollbar"
>
<XTable @register="registerType" @cell-click="cellClickEvent">
<!-- 操作新增类型 -->
<template #toolbar_buttons>
<XButton
@ -36,10 +31,10 @@
preIcon="ep:delete"
:title="t('action.del')"
v-hasPermi="['system:dict:delete']"
@click="handleTypeDelete(row.id)"
@click="typeDeleteData(row.id)"
/>
</template>
</vxe-grid>
</XTable>
<!-- @星语分页和列表重叠在一起了 -->
</el-card>
<!-- ====== 字典数据 ====== -->
@ -55,7 +50,7 @@
</div>
<div v-if="tableTypeSelect">
<!-- 列表 -->
<vxe-grid ref="xDataGrid" v-bind="dataGridOptions" class="xtable-scrollbar">
<XTable @register="registerData">
<!-- 操作新增数据 -->
<template #toolbar_buttons>
<XButton
@ -79,10 +74,10 @@
v-hasPermi="['system:dict:delete']"
preIcon="ep:delete"
:title="t('action.del')"
@click="handleDataDelete(row.id)"
@click="dataDeleteData(row.id)"
/>
</template>
</vxe-grid>
</XTable>
</div>
</el-card>
<XModal id="dictModel" v-model="dialogVisible" :title="dialogTitle">
@ -130,8 +125,8 @@
import { ref, unref, reactive } from 'vue'
import { useI18n } from '@/hooks/web/useI18n'
import { useMessage } from '@/hooks/web/useMessage'
import { useVxeGrid } from '@/hooks/web/useVxeGrid'
import { VxeGridInstance, VxeTableEvents } from 'vxe-table'
import { useXTable } from '@/hooks/web/useXTable'
import { VxeTableEvents } from 'vxe-table'
import { FormExpose } from '@/components/Form'
import { ElInput, ElTag, ElCard } from 'element-plus'
import * as DictTypeSchemas from './dict.type'
@ -143,28 +138,18 @@ import { DictDataVO, DictTypeVO } from '@/api/system/dict/types'
const { t } = useI18n() //
const message = useMessage() //
const xTypeGrid = ref<VxeGridInstance>() // Grid Ref
const {
gridOptions: typeGridOptions,
getList: typeGetList,
deleteData: typeDeleteData
} = useVxeGrid<DictTypeVO>({
const [registerType, { reload: typeGetList, deleteData: typeDeleteData }] = useXTable({
allSchemas: DictTypeSchemas.allSchemas,
getListApi: DictTypeApi.getDictTypePageApi,
deleteApi: DictTypeApi.deleteDictTypeApi
})
const xDataGrid = ref<VxeGridInstance>() // Grid Ref
const queryParams = reactive({
dictType: null
})
const {
gridOptions: dataGridOptions,
getList: dataGetList,
deleteData: dataDeleteData
} = useVxeGrid<DictDataVO>({
const [registerData, { reload: dataGetList, deleteData: dataDeleteData }] = useXTable({
allSchemas: DictDataSchemas.allSchemas,
queryParams: queryParams,
params: queryParams,
getListApi: DictDataApi.getDictDataPageApi,
deleteApi: DictDataApi.deleteDictDataApi
})
@ -199,7 +184,7 @@ const tableTypeSelect = ref(false)
const cellClickEvent: VxeTableEvents.CellClick = async ({ row }) => {
tableTypeSelect.value = true
queryParams.dictType = row['type']
await dataGetList(xDataGrid)
await dataGetList()
parentType.value = row['type']
}
//
@ -217,15 +202,6 @@ const setDialogTile = (type: string) => {
dialogVisible.value = true
}
//
const handleTypeDelete = async (rowId: number) => {
await typeDeleteData(xTypeGrid, rowId)
}
const handleDataDelete = async (rowId: number) => {
await dataDeleteData(xDataGrid, rowId)
}
//
const submitTypeForm = async () => {
const elForm = unref(typeFormRef)?.getElFormRef()
@ -247,7 +223,7 @@ const submitTypeForm = async () => {
dialogVisible.value = false
} finally {
actionLoading.value = false
typeGetList(xTypeGrid)
typeGetList()
}
}
})
@ -272,7 +248,7 @@ const submitDataForm = async () => {
dialogVisible.value = false
} finally {
actionLoading.value = false
dataGetList(xDataGrid)
dataGetList()
}
}
})

View File

@ -1,7 +1,7 @@
<template>
<ContentWrap>
<!-- 列表 -->
<vxe-grid ref="xGrid" v-bind="gridOptions" class="xtable-scrollbar">
<XTable @register="registerTable">
<!-- 操作新增 -->
<template #toolbar_buttons>
<XButton
@ -32,10 +32,10 @@
preIcon="ep:delete"
:title="t('action.del')"
v-hasPermi="['system:error-code:delete']"
@click="handleDelete(row.id)"
@click="deleteData(row.id)"
/>
</template>
</vxe-grid>
</XTable>
</ContentWrap>
<!-- 弹窗 -->
<XModal id="errorCodeModel" v-model="dialogVisible" :title="dialogTitle">
@ -71,8 +71,7 @@
import { ref, unref } from 'vue'
import { useI18n } from '@/hooks/web/useI18n'
import { useMessage } from '@/hooks/web/useMessage'
import { useVxeGrid } from '@/hooks/web/useVxeGrid'
import { VxeGridInstance } from 'vxe-table'
import { useXTable } from '@/hooks/web/useXTable'
import { FormExpose } from '@/components/Form'
// import
import { rules, allSchemas } from './errorCode.data'
@ -81,8 +80,7 @@ import * as ErrorCodeApi from '@/api/system/errorCode'
const { t } = useI18n() //
const message = useMessage() //
//
const xGrid = ref<VxeGridInstance>() // grid Ref
const { gridOptions, getList, deleteData } = useVxeGrid<ErrorCodeApi.ErrorCodeVO>({
const [registerTable, { reload, deleteData }] = useXTable({
allSchemas: allSchemas,
getListApi: ErrorCodeApi.getErrorCodePageApi,
deleteApi: ErrorCodeApi.deleteErrorCodeApi
@ -123,11 +121,6 @@ const handleDetail = async (rowId: number) => {
detailData.value = res
}
//
const handleDelete = async (rowId: number) => {
await deleteData(xGrid, rowId)
}
// /
const submitForm = async () => {
const elForm = unref(formRef)?.getElFormRef()
@ -149,7 +142,7 @@ const submitForm = async () => {
} finally {
actionLoading.value = false
//
await getList(xGrid)
await reload()
}
}
})

View File

@ -1,21 +1,21 @@
<template>
<ContentWrap>
<!-- 列表 -->
<vxe-grid ref="xGrid" v-bind="gridOptions" class="xtable-scrollbar">
<XTable @register="registerTable">
<!-- 操作导出 -->
<template #toolbar_buttons>
<XButton
type="warning"
preIcon="ep:download"
:title="t('action.export')"
@click="handleExport()"
@click="exportList('登录列表.xls')"
/>
</template>
<template #actionbtns_default="{ row }">
<!-- 操作详情 -->
<XTextButton preIcon="ep:view" :title="t('action.detail')" @click="handleDetail(row)" />
</template>
</vxe-grid>
</XTable>
</ContentWrap>
<!-- 弹窗 -->
<XModal id="postModel" v-model="dialogVisible" :title="dialogTitle">
@ -31,16 +31,14 @@
// import
import { ref } from 'vue'
import { useI18n } from '@/hooks/web/useI18n'
import { useVxeGrid } from '@/hooks/web/useVxeGrid'
import { VxeGridInstance } from 'vxe-table'
import { useXTable } from '@/hooks/web/useXTable'
// import
import { allSchemas } from './loginLog.data'
import { getLoginLogPageApi, exportLoginLogApi, LoginLogVO } from '@/api/system/loginLog'
const { t } = useI18n() //
//
const xGrid = ref<VxeGridInstance>() // Grid Ref
const { gridOptions, exportList } = useVxeGrid<LoginLogVO>({
const [registerTable, { exportList }] = useXTable({
allSchemas: allSchemas,
getListApi: getLoginLogPageApi,
exportListApi: exportLoginLogApi
@ -56,9 +54,4 @@ const handleDetail = async (row: LoginLogVO) => {
detailData.value = row
dialogVisible.value = true
}
//
const handleExport = async () => {
await exportList(xGrid, '登录列表.xls')
}
</script>

View File

@ -1,7 +1,7 @@
<template>
<ContentWrap>
<!-- 列表 -->
<vxe-grid ref="xGrid" v-bind="gridOptions" show-overflow class="xtable-scrollbar">
<XTable ref="xGrid" @register="registerTable" show-overflow>
<template #toolbar_buttons>
<!-- 操作新增 -->
<XButton
@ -11,8 +11,8 @@
v-hasPermi="['system:menu:create']"
@click="handleCreate()"
/>
<XButton title="展开所有" @click="xGrid?.setAllTreeExpand(true)" />
<XButton title="关闭所有" @click="xGrid?.clearTreeExpand()" />
<XButton title="展开所有" @click="xGrid?.Ref.setAllTreeExpand(true)" />
<XButton title="关闭所有" @click="xGrid?.Ref.clearTreeExpand()" />
</template>
<template #name_default="{ row }">
<Icon :icon="row.icon" />
@ -31,10 +31,10 @@
preIcon="ep:delete"
:title="t('action.del')"
v-hasPermi="['system:menu:delete']"
@click="handleDelete(row.id)"
@click="deleteData(row.id)"
/>
</template>
</vxe-grid>
</XTable>
</ContentWrap>
<!-- 添加或修改菜单对话框 -->
<XModal id="menuModel" v-model="dialogVisible" :title="dialogTitle">
@ -194,28 +194,28 @@ import {
} from 'element-plus'
import { Tooltip } from '@/components/Tooltip'
import { IconSelect } from '@/components/Icon'
import { VxeGridInstance } from 'vxe-table'
// import
import { DICT_TYPE, getIntDictOptions } from '@/utils/dict'
import { SystemMenuTypeEnum, CommonStatusEnum } from '@/utils/constants'
import { handleTree, defaultProps } from '@/utils/tree'
import * as MenuApi from '@/api/system/menu'
import { allSchemas, rules } from './menu.data'
import { useVxeGrid } from '@/hooks/web/useVxeGrid'
import { useXTable } from '@/hooks/web/useXTable'
const { t } = useI18n() //
const message = useMessage() //
const { wsCache } = useCache()
const xGrid = ref<any>(null)
//
//
const xGrid = ref<VxeGridInstance>() // Grid Ref
const treeConfig = {
transform: true,
rowField: 'id',
parentField: 'parentId',
expandAll: false
}
const { gridOptions, getList, deleteData } = useVxeGrid<MenuApi.MenuVO>({
const [registerTable, { reload, deleteData }] = useXTable({
allSchemas: allSchemas,
treeConfig: treeConfig,
getListApi: MenuApi.getMenuListApi,
@ -326,7 +326,7 @@ const submitForm = async () => {
actionLoading.value = false
wsCache.delete(CACHE_KEY.ROLE_ROUTERS)
//
await getList(xGrid)
await reload()
}
}
@ -334,10 +334,4 @@ const submitForm = async () => {
const isExternal = (path: string) => {
return /^(https?:|mailto:|tel:)/.test(path)
}
// ========== ==========
//
const handleDelete = async (rowId: number) => {
await deleteData(xGrid, rowId)
}
</script>

View File

@ -1,7 +1,7 @@
<template>
<ContentWrap>
<!-- 列表 -->
<vxe-grid ref="xGrid" v-bind="gridOptions" class="xtable-scrollbar">
<XTable @register="registerTable">
<!-- 操作新增 -->
<template #toolbar_buttons>
<XButton
@ -32,10 +32,10 @@
preIcon="ep:delete"
:title="t('action.del')"
v-hasPermi="['system:notice:delete']"
@click="handleDelete(row.id)"
@click="deleteData(row.id)"
/>
</template>
</vxe-grid>
</XTable>
</ContentWrap>
<!-- 弹窗 -->
<XModal id="noticeModel" v-model="dialogVisible" :title="dialogTitle">
@ -75,8 +75,7 @@
import { ref, unref } from 'vue'
import { useI18n } from '@/hooks/web/useI18n'
import { useMessage } from '@/hooks/web/useMessage'
import { useVxeGrid } from '@/hooks/web/useVxeGrid'
import { VxeGridInstance } from 'vxe-table'
import { useXTable } from '@/hooks/web/useXTable'
import { FormExpose } from '@/components/Form'
// import
import * as NoticeApi from '@/api/system/notice'
@ -86,8 +85,7 @@ import { Editor } from '@/components/Editor'
const { t } = useI18n() //
const message = useMessage() //
//
const xGrid = ref<VxeGridInstance>() // Grid Ref
const { gridOptions, getList, deleteData } = useVxeGrid<NoticeApi.NoticeVO>({
const [registerTable, { reload, deleteData }] = useXTable({
allSchemas: allSchemas,
getListApi: NoticeApi.getNoticePageApi,
deleteApi: NoticeApi.deleteNoticeApi
@ -128,11 +126,6 @@ const handleDetail = async (rowId: number) => {
detailData.value = res
}
//
const handleDelete = async (rowId: number) => {
await deleteData(xGrid, rowId)
}
// /
const submitForm = async () => {
const elForm = unref(formRef)?.getElFormRef()
@ -153,7 +146,7 @@ const submitForm = async () => {
dialogVisible.value = false
} finally {
actionLoading.value = false
await getList(xGrid)
await reload()
}
}
})

View File

@ -1,7 +1,7 @@
<template>
<ContentWrap>
<!-- 列表 -->
<vxe-grid ref="xGrid" v-bind="gridOptions" class="xtable-scrollbar">
<XTable @register="registerTable">
<template #toolbar_buttons>
<!-- 操作新增 -->
<XButton
@ -48,10 +48,10 @@
preIcon="ep:delete"
:title="t('action.del')"
v-hasPermi="['system:oauth2-client:delete']"
@click="handleDelete(row.id)"
@click="deleteData(row.id)"
/>
</template>
</vxe-grid>
</XTable>
</ContentWrap>
<!-- 弹窗 -->
<XModal id="postModel" v-model="dialogVisible" :title="dialogTitle">
@ -135,8 +135,7 @@ import { ref, unref } from 'vue'
import { ElTag } from 'element-plus'
import { useI18n } from '@/hooks/web/useI18n'
import { useMessage } from '@/hooks/web/useMessage'
import { useVxeGrid } from '@/hooks/web/useVxeGrid'
import { VxeGridInstance } from 'vxe-table'
import { useXTable } from '@/hooks/web/useXTable'
import { FormExpose } from '@/components/Form'
// import
import * as ClientApi from '@/api/system/oauth2/client'
@ -146,8 +145,7 @@ const { t } = useI18n() // 国际化
const message = useMessage() //
//
const xGrid = ref<VxeGridInstance>() // Grid Ref
const { gridOptions, getList, deleteData } = useVxeGrid<ClientApi.OAuth2ClientVO>({
const [registerTable, { reload, deleteData }] = useXTable({
allSchemas: allSchemas,
getListApi: ClientApi.getOAuth2ClientPageApi,
deleteApi: ClientApi.deleteOAuth2ClientApi
@ -186,11 +184,6 @@ const handleDetail = async (rowId: number) => {
detailData.value = res
}
//
const handleDelete = async (rowId: number) => {
await deleteData(xGrid, rowId)
}
// /
const submitForm = async () => {
const elForm = unref(formRef)?.getElFormRef()
@ -212,7 +205,7 @@ const submitForm = async () => {
} finally {
actionLoading.value = false
//
await getList(xGrid)
await reload()
}
}
})

View File

@ -1,11 +1,11 @@
<template>
<ContentWrap>
<!-- 列表 -->
<vxe-grid ref="xGrid" v-bind="gridOptions" class="xtable-scrollbar">
<XTable @register="registerTable">
<template #actionbtns_default="{ row }">
<!-- 操作详情 -->
<XTextButton preIcon="ep:view" :title="t('action.detail')" @click="handleDetail(row)" />
<!-- 操作删除 -->
<!-- 操作登出 -->
<XTextButton
preIcon="ep:delete"
:title="t('action.logout')"
@ -13,7 +13,7 @@
@click="handleForceLogout(row.id)"
/>
</template>
</vxe-grid>
</XTable>
</ContentWrap>
<XModal v-model="dialogVisible" :title="dialogTitle">
<!-- 对话框(详情) -->
@ -28,8 +28,7 @@
import { ref } from 'vue'
import { useI18n } from '@/hooks/web/useI18n'
import { useMessage } from '@/hooks/web/useMessage'
import { useVxeGrid } from '@/hooks/web/useVxeGrid'
import { VxeGridInstance } from 'vxe-table'
import { useXTable } from '@/hooks/web/useXTable'
import { allSchemas } from './token.data'
import * as TokenApi from '@/api/system/oauth2/token'
@ -37,8 +36,7 @@ import * as TokenApi from '@/api/system/oauth2/token'
const { t } = useI18n() //
const message = useMessage() //
//
const xGrid = ref<VxeGridInstance>() // Grid Ref
const { gridOptions, getList } = useVxeGrid<TokenApi.OAuth2TokenVO>({
const [registerTable, { reload }] = useXTable({
allSchemas: allSchemas,
topActionSlots: false,
getListApi: TokenApi.getAccessTokenPageApi
@ -65,7 +63,7 @@ const handleForceLogout = (rowId: number) => {
})
.finally(async () => {
//
await getList(xGrid)
await reload()
})
}
</script>

View File

@ -1,7 +1,7 @@
<template>
<ContentWrap>
<!-- 列表 -->
<vxe-grid ref="xGrid" v-bind="gridOptions" class="xtable-scrollbar">
<XTable @register="registerTable">
<template #toolbar_buttons>
<!-- 操作新增 -->
<XButton
@ -9,7 +9,7 @@
preIcon="ep:download"
:title="t('action.export')"
v-hasPermi="['system:operate-log:export']"
@click="handleExport()"
@click="exportList('操作日志.xls')"
/>
</template>
<template #duration="{ row }">
@ -22,7 +22,7 @@
<!-- 操作详情 -->
<XTextButton preIcon="ep:view" :title="t('action.detail')" @click="handleDetail(row)" />
</template>
</vxe-grid>
</XTable>
</ContentWrap>
<!-- 弹窗 -->
<XModal id="postModel" v-model="dialogVisible" :title="t('action.detail')">
@ -45,16 +45,14 @@
// import
import { ref } from 'vue'
import { useI18n } from '@/hooks/web/useI18n'
import { useVxeGrid } from '@/hooks/web/useVxeGrid'
import { VxeGridInstance } from 'vxe-table'
import { useXTable } from '@/hooks/web/useXTable'
// import
import * as OperateLogApi from '@/api/system/operatelog'
import { allSchemas } from './operatelog.data'
const { t } = useI18n() //
//
const xGrid = ref<VxeGridInstance>() // Grid Ref
const { gridOptions, exportList } = useVxeGrid<OperateLogApi.OperateLogVO>({
const [registerTable, { exportList }] = useXTable({
allSchemas: allSchemas,
getListApi: OperateLogApi.getOperateLogPageApi,
exportListApi: OperateLogApi.exportOperateLogApi
@ -70,9 +68,4 @@ const handleDetail = (row: OperateLogApi.OperateLogVO) => {
detailData.value = row
dialogVisible.value = true
}
//
const handleExport = async () => {
await exportList(xGrid, '操作日志.xls')
}
</script>

View File

@ -1,7 +1,7 @@
<template>
<ContentWrap>
<!-- 列表 -->
<vxe-grid ref="xGrid" v-bind="gridOptions" class="xtable-scrollbar">
<XTable @register="registerTable">
<template #toolbar_buttons>
<!-- 操作新增 -->
<XButton
@ -17,7 +17,7 @@
preIcon="ep:download"
:title="t('action.export')"
v-hasPermi="['system:post:export']"
@click="handleExport()"
@click="exportList('岗位列表.xls')"
/>
</template>
<template #actionbtns_default="{ row }">
@ -40,10 +40,10 @@
preIcon="ep:delete"
:title="t('action.del')"
v-hasPermi="['system:post:delete']"
@click="handleDelete(row.id)"
@click="deleteData(row.id)"
/>
</template>
</vxe-grid>
</XTable>
</ContentWrap>
<!-- 弹窗 -->
<XModal id="postModel" :loading="modelLoading" v-model="modelVisible" :title="modelTitle">
@ -79,8 +79,7 @@
import { ref, unref } from 'vue'
import { useI18n } from '@/hooks/web/useI18n'
import { useMessage } from '@/hooks/web/useMessage'
import { useVxeGrid } from '@/hooks/web/useVxeGrid'
import { VxeGridInstance } from 'vxe-table'
import { useXTable } from '@/hooks/web/useXTable'
import { FormExpose } from '@/components/Form'
// import
import * as PostApi from '@/api/system/post'
@ -89,8 +88,7 @@ import { rules, allSchemas } from './post.data'
const { t } = useI18n() //
const message = useMessage() //
//
const xGrid = ref<VxeGridInstance>() // Grid Ref
const { gridOptions, getList, deleteData, exportList } = useVxeGrid<PostApi.PostVO>({
const [registerTable, { reload, deleteData, exportList }] = useXTable({
allSchemas: allSchemas,
getListApi: PostApi.getPostPageApi,
deleteApi: PostApi.deletePostApi,
@ -119,11 +117,6 @@ const handleCreate = () => {
modelLoading.value = false
}
//
const handleExport = async () => {
await exportList(xGrid, '岗位列表.xls')
}
//
const handleUpdate = async (rowId: number) => {
setDialogTile('update')
@ -141,11 +134,6 @@ const handleDetail = async (rowId: number) => {
modelLoading.value = false
}
//
const handleDelete = async (rowId: number) => {
await deleteData(xGrid, rowId)
}
// /
const submitForm = async () => {
const elForm = unref(formRef)?.getElFormRef()
@ -167,7 +155,7 @@ const submitForm = async () => {
} finally {
actionLoading.value = false
//
await getList(xGrid)
await reload()
}
}
})

View File

@ -1,7 +1,7 @@
<template>
<ContentWrap>
<!-- 列表 -->
<vxe-grid ref="xGrid" v-bind="gridOptions" class="xtable-scrollbar">
<XTable @register="registerTable">
<!-- 操作新增 -->
<template #toolbar_buttons>
<XButton
@ -46,10 +46,10 @@
preIcon="ep:delete"
:title="t('action.del')"
v-hasPermi="['system:role:delete']"
@click="handleDelete(row.id)"
@click="deleteData(row.id)"
/>
</template>
</vxe-grid>
</XTable>
</ContentWrap>
<XModal v-model="dialogVisible" :title="dialogTitle">
@ -159,11 +159,10 @@ import {
ElSwitch,
ElTag
} from 'element-plus'
import { VxeGridInstance } from 'vxe-table'
import { FormExpose } from '@/components/Form'
import { useI18n } from '@/hooks/web/useI18n'
import { useMessage } from '@/hooks/web/useMessage'
import { useVxeGrid } from '@/hooks/web/useVxeGrid'
import { useXTable } from '@/hooks/web/useXTable'
import { handleTree, defaultProps } from '@/utils/tree'
import { SystemDataScopeEnum } from '@/utils/constants'
import { DICT_TYPE, getIntDictOptions } from '@/utils/dict'
@ -176,8 +175,7 @@ import * as PermissionApi from '@/api/system/permission'
const { t } = useI18n() //
const message = useMessage() //
//
const xGrid = ref<VxeGridInstance>() // Grid Ref
const { gridOptions, getList, deleteData } = useVxeGrid<RoleApi.RoleVO>({
const [registerTable, { reload, deleteData }] = useXTable({
allSchemas: allSchemas,
getListApi: RoleApi.getRolePageApi,
deleteApi: RoleApi.deleteRoleApi
@ -219,11 +217,6 @@ const handleDetail = async (rowId: number) => {
detailData.value = res
}
//
const handleDelete = async (rowId: number) => {
await deleteData(xGrid, rowId)
}
//
const submitForm = async () => {
const elForm = unref(formRef)?.getElFormRef()
@ -245,7 +238,7 @@ const submitForm = async () => {
} finally {
actionLoading.value = false
//
await getList(xGrid)
await reload()
}
}
})

View File

@ -1,7 +1,7 @@
<template>
<ContentWrap>
<!-- 列表 -->
<vxe-grid ref="xGrid" v-bind="gridOptions" class="xtable-scrollbar">
<XTable @register="registerTable">
<template #toolbar_buttons>
<!-- 操作新增 -->
<XButton
@ -17,7 +17,7 @@
preIcon="ep:download"
:title="t('action.export')"
v-hasPermi="['system:sensitive-word:export']"
@click="handleExport()"
@click="exportList('敏感词数据.xls')"
/>
</template>
<template #tags_default="{ row }">
@ -50,10 +50,10 @@
preIcon="ep:delete"
:title="t('action.del')"
v-hasPermi="['system:sensitive-word:delete']"
@click="handleDelete(row.id)"
@click="deleteData(row.id)"
/>
</template>
</vxe-grid>
</XTable>
</ContentWrap>
<XModal v-model="dialogVisible" :title="dialogTitle">
@ -106,8 +106,7 @@
import { onMounted, ref, unref } from 'vue'
import { useI18n } from '@/hooks/web/useI18n'
import { useMessage } from '@/hooks/web/useMessage'
import { useVxeGrid } from '@/hooks/web/useVxeGrid'
import { VxeGridInstance } from 'vxe-table'
import { useXTable } from '@/hooks/web/useXTable'
import { FormExpose } from '@/components/Form'
import { ElTag, ElSelect, ElOption } from 'element-plus'
import * as SensitiveWordApi from '@/api/system/sensitiveWord'
@ -116,14 +115,12 @@ import { rules, allSchemas } from './sensitiveWord.data'
const { t } = useI18n() //
const message = useMessage() //
//
const xGrid = ref<VxeGridInstance>() // Grid Ref
const { gridOptions, getList, deleteData, exportList } =
useVxeGrid<SensitiveWordApi.SensitiveWordVO>({
allSchemas: allSchemas,
getListApi: SensitiveWordApi.getSensitiveWordPageApi,
deleteApi: SensitiveWordApi.deleteSensitiveWordApi,
exportListApi: SensitiveWordApi.exportSensitiveWordApi
})
const [registerTable, { reload, deleteData, exportList }] = useXTable({
allSchemas: allSchemas,
getListApi: SensitiveWordApi.getSensitiveWordPageApi,
deleteApi: SensitiveWordApi.deleteSensitiveWordApi,
exportListApi: SensitiveWordApi.exportSensitiveWordApi
})
const actionLoading = ref(false) //
const actionType = ref('') //
const dialogVisible = ref(false) //
@ -150,11 +147,6 @@ const handleCreate = () => {
setDialogTile('create')
}
//
const handleExport = async () => {
await exportList(xGrid, '敏感词数据.xls')
}
//
const handleUpdate = async (rowId: number) => {
setDialogTile('update')
@ -170,11 +162,6 @@ const handleDetail = async (rowId: number) => {
detailData.value = res
}
//
const handleDelete = async (rowId: number) => {
await deleteData(xGrid, rowId)
}
//
const submitForm = async () => {
const elForm = unref(formRef)?.getElFormRef()
@ -196,7 +183,7 @@ const submitForm = async () => {
} finally {
actionLoading.value = false
//
await getList(xGrid)
await reload()
}
}
})

View File

@ -1,7 +1,7 @@
<template>
<ContentWrap>
<!-- 列表 -->
<vxe-grid ref="xGrid" v-bind="gridOptions" class="xtable-scrollbar">
<XTable @register="registerTable">
<!-- 操作新增 -->
<template #toolbar_buttons>
<XButton
@ -32,10 +32,10 @@
preIcon="ep:delete"
:title="t('action.del')"
v-hasPermi="['system:sms-channel:delete']"
@click="handleDelete(row.id)"
@click="deleteData(row.id)"
/>
</template>
</vxe-grid>
</XTable>
</ContentWrap>
<XModal id="smsChannel" v-model="dialogVisible" :title="dialogTitle">
@ -72,8 +72,7 @@
import { ref, unref } from 'vue'
import { useI18n } from '@/hooks/web/useI18n'
import { useMessage } from '@/hooks/web/useMessage'
import { useVxeGrid } from '@/hooks/web/useVxeGrid'
import { VxeGridInstance } from 'vxe-table'
import { useXTable } from '@/hooks/web/useXTable'
import { FormExpose } from '@/components/Form'
// import
import * as SmsChannelApi from '@/api/system/sms/smsChannel'
@ -83,8 +82,7 @@ const { t } = useI18n() // 国际化
const message = useMessage() //
//
const xGrid = ref<VxeGridInstance>() // Grid Ref
const { gridOptions, getList, deleteData } = useVxeGrid<SmsChannelApi.SmsChannelVO>({
const [registerTable, { reload, deleteData }] = useXTable({
allSchemas: allSchemas,
getListApi: SmsChannelApi.getSmsChannelPageApi,
deleteApi: SmsChannelApi.deleteSmsChannelApi
@ -125,11 +123,6 @@ const handleDetail = async (rowId: number) => {
detailData.value = res
}
//
const handleDelete = async (rowId: number) => {
await deleteData(xGrid, rowId)
}
//
const submitForm = async () => {
const elForm = unref(formRef)?.getElFormRef()
@ -151,7 +144,7 @@ const submitForm = async () => {
} finally {
actionLoading.value = false
//
await getList(xGrid)
await reload()
}
}
})

View File

@ -1,20 +1,20 @@
<template>
<ContentWrap>
<!-- 列表 -->
<vxe-grid ref="xGrid" v-bind="gridOptions" class="xtable-scrollbar">
<XTable @register="registerTable">
<!-- 操作导出 -->
<template #toolbar_buttons>
<XButton
type="warning"
preIcon="ep:download"
:title="t('action.export')"
@click="handleExport()"
@click="exportList('短信日志.xls')"
/>
</template>
<template #actionbtns_default="{ row }">
<XTextButton preIcon="ep:view" :title="t('action.detail')" @click="handleDetail(row)" />
</template>
</vxe-grid>
</XTable>
</ContentWrap>
<XModal id="smsLog" v-model="dialogVisible" :title="dialogTitle">
@ -34,15 +34,13 @@
// import
import { ref } from 'vue'
import { useI18n } from '@/hooks/web/useI18n'
import { useVxeGrid } from '@/hooks/web/useVxeGrid'
import { VxeGridInstance } from 'vxe-table'
import { useXTable } from '@/hooks/web/useXTable'
import { allSchemas } from './sms.log.data'
import * as SmsLoglApi from '@/api/system/sms/smsLog'
const { t } = useI18n() //
//
const xGrid = ref<VxeGridInstance>() // Grid Ref
const { gridOptions, exportList } = useVxeGrid<SmsLoglApi.SmsLogVO>({
const [registerTable, { exportList }] = useXTable({
allSchemas: allSchemas,
getListApi: SmsLoglApi.getSmsLogPageApi,
exportListApi: SmsLoglApi.exportSmsLogApi
@ -59,9 +57,4 @@ const handleDetail = (row: SmsLoglApi.SmsLogVO) => {
detailData.value = row
dialogVisible.value = true
}
//
const handleExport = async () => {
await exportList(xGrid, '短信日志.xls')
}
</script>

View File

@ -1,7 +1,7 @@
<template>
<ContentWrap>
<!-- 列表 -->
<vxe-grid ref="xGrid" v-bind="gridOptions" class="xtable-scrollbar">
<XTable @register="registerTable">
<!-- 操作新增 -->
<template #toolbar_buttons>
<XButton
@ -38,10 +38,10 @@
preIcon="ep:delete"
:title="t('action.del')"
v-hasPermi="['system:sms-template:delete']"
@click="handleDelete(row.id)"
@click="deleteData(row.id)"
/>
</template>
</vxe-grid>
</XTable>
</ContentWrap>
<XModal id="smsTemplate" v-model="dialogVisible" :title="dialogTitle">
<!-- 对话框(添加 / 修改) -->
@ -113,8 +113,7 @@
import { ref, unref } from 'vue'
import { useI18n } from '@/hooks/web/useI18n'
import { useMessage } from '@/hooks/web/useMessage'
import { useVxeGrid } from '@/hooks/web/useVxeGrid'
import { VxeGridInstance } from 'vxe-table'
import { useXTable } from '@/hooks/web/useXTable'
import { FormExpose } from '@/components/Form'
import { ElForm, ElFormItem, ElInput } from 'element-plus'
// import
@ -125,8 +124,7 @@ const { t } = useI18n() // 国际化
const message = useMessage() //
//
const xGrid = ref<VxeGridInstance>() // Grid Ref
const { gridOptions, getList, deleteData } = useVxeGrid<SmsTemplateApi.SmsTemplateVO>({
const [registerTable, { reload, deleteData }] = useXTable({
allSchemas: allSchemas,
getListApi: SmsTemplateApi.getSmsTemplatePageApi,
deleteApi: SmsTemplateApi.deleteSmsTemplateApi
@ -168,11 +166,6 @@ const handleDetail = async (rowId: number) => {
detailData.value = res
}
//
const handleDelete = async (rowId: number) => {
await deleteData(xGrid, rowId)
}
//
const submitForm = async () => {
const elForm = unref(formRef)?.getElFormRef()
@ -194,7 +187,7 @@ const submitForm = async () => {
} finally {
actionLoading.value = false
//
await getList(xGrid)
await reload()
}
}
})

View File

@ -1,7 +1,7 @@
<template>
<ContentWrap>
<!-- 列表 -->
<vxe-grid ref="xGrid" v-bind="gridOptions" class="xtable-scrollbar">
<XTable @register="registerTable">
<template #toolbar_buttons>
<!-- 操作新增 -->
<XButton
@ -16,7 +16,7 @@
preIcon="ep:download"
:title="t('action.export')"
v-hasPermi="['system:tenant:export']"
@click="handleExport()"
@click="exportList('租户列表.xls')"
/>
</template>
<template #accountCount_default="{ row }">
@ -46,10 +46,10 @@
preIcon="ep:delete"
:title="t('action.del')"
v-hasPermi="['system:tenant:delete']"
@click="handleDelete(row.id)"
@click="deleteData(row.id)"
/>
</template>
</vxe-grid>
</XTable>
</ContentWrap>
<XModal v-model="dialogVisible" :title="dialogTitle">
<!-- 对话框(添加 / 修改) -->
@ -89,8 +89,7 @@
import { ref, unref } from 'vue'
import { useI18n } from '@/hooks/web/useI18n'
import { useMessage } from '@/hooks/web/useMessage'
import { useVxeGrid } from '@/hooks/web/useVxeGrid'
import { VxeGridInstance } from 'vxe-table'
import { useXTable } from '@/hooks/web/useXTable'
import { ElTag } from 'element-plus'
import { FormExpose } from '@/components/Form'
import * as TenantApi from '@/api/system/tenant'
@ -99,8 +98,7 @@ import { rules, allSchemas, tenantPackageOption } from './tenant.data'
const { t } = useI18n() //
const message = useMessage() //
//
const xGrid = ref<VxeGridInstance>() // Grid Ref
const { gridOptions, getList, deleteData, exportList } = useVxeGrid<TenantApi.TenantVO>({
const [registerTable, { reload, deleteData, exportList }] = useXTable({
allSchemas: allSchemas,
getListApi: TenantApi.getTenantPageApi,
deleteApi: TenantApi.deleteTenantApi,
@ -151,16 +149,6 @@ const handleDetail = async (rowId: number) => {
setDialogTile('detail')
}
//
const handleDelete = async (rowId: number) => {
await deleteData(xGrid, rowId)
}
//
const handleExport = async () => {
await exportList(xGrid, '租户列表.xls')
}
//
const submitForm = async () => {
const elForm = unref(formRef)?.getElFormRef()
@ -183,7 +171,7 @@ const submitForm = async () => {
} finally {
actionLoading.value = false
//
await getList(xGrid)
await reload()
}
}
})

View File

@ -1,7 +1,7 @@
<template>
<ContentWrap>
<!-- 列表 -->
<vxe-grid ref="xGrid" v-bind="gridOptions" class="xtable-scrollbar">
<XTable @register="registerTable">
<template #toolbar_buttons>
<XButton
type="primary"
@ -12,9 +12,9 @@
</template>
<template #actionbtns_default="{ row }">
<XTextButton preIcon="ep:edit" :title="t('action.edit')" @click="handleUpdate(row.id)" />
<XTextButton preIcon="ep:delete" :title="t('action.del')" @click="handleDelete(row.id)" />
<XTextButton preIcon="ep:delete" :title="t('action.del')" @click="deleteData(row.id)" />
</template>
</vxe-grid>
</XTable>
</ContentWrap>
<XModal v-model="dialogVisible" :title="dialogTitle">
<!-- 对话框(添加 / 修改) -->
@ -69,8 +69,7 @@ import { onMounted, ref, unref } from 'vue'
import { handleTree, defaultProps } from '@/utils/tree'
import { useI18n } from '@/hooks/web/useI18n'
import { useMessage } from '@/hooks/web/useMessage'
import { useVxeGrid } from '@/hooks/web/useVxeGrid'
import { VxeGridInstance } from 'vxe-table'
import { useXTable } from '@/hooks/web/useXTable'
import { FormExpose } from '@/components/Form'
import { ElCard, ElSwitch, ElTree } from 'element-plus'
// import
@ -86,7 +85,6 @@ const menuExpand = ref(false)
const menuNodeAll = ref(false)
const treeRef = ref<InstanceType<typeof ElTree>>()
const treeNodeAll = ref(false)
const xGrid = ref<VxeGridInstance>() // Grid Ref
const formRef = ref<FormExpose>() // Ref
const loading = ref(false) //
const actionType = ref('') //
@ -102,7 +100,7 @@ const getTree = async () => {
menuOptions.value = handleTree(res)
}
const { gridOptions, getList, deleteData } = useVxeGrid<TenantPackageApi.TenantPackageVO>({
const [registerTable, { reload, deleteData }] = useXTable({
allSchemas: allSchemas,
getListApi: TenantPackageApi.getTenantPackageTypePageApi,
deleteApi: TenantPackageApi.deleteTenantPackageTypeApi
@ -134,11 +132,6 @@ const handleUpdate = async (rowId: number) => {
unref(treeRef)?.setCheckedKeys(res.menuIds)
}
//
const handleDelete = async (rowId: number) => {
await deleteData(xGrid, rowId)
}
//
const submitForm = async () => {
const elForm = unref(formRef)?.getElFormRef()
@ -149,7 +142,10 @@ const submitForm = async () => {
//
try {
const data = unref(formRef)?.formModel as TenantPackageApi.TenantPackageVO
data.menuIds = treeRef.value!.getCheckedKeys(false) as number[]
data.menuIds = [
...(treeRef.value!.getCheckedKeys(false) as unknown as Array<number>),
...(treeRef.value!.getHalfCheckedKeys() as unknown as Array<number>)
]
if (actionType.value === 'create') {
await TenantPackageApi.createTenantPackageTypeApi(data)
message.success(t('common.createSuccess'))
@ -162,7 +158,7 @@ const submitForm = async () => {
} finally {
loading.value = false
//
await getList(xGrid)
await reload()
}
}
})

View File

@ -27,7 +27,7 @@
</div>
</template>
<!-- 列表 -->
<vxe-grid ref="xGrid" v-bind="gridOptions" class="xtable-scrollbar">
<XTable @register="registerTable">
<template #toolbar_buttons>
<!-- 操作新增 -->
<XButton
@ -112,14 +112,14 @@
preIcon="ep:delete"
:title="t('action.del')"
v-hasPermi="['system:user:delete']"
@click="handleDelete(row.id)"
@click="deleteData(row.id)"
/>
</el-dropdown-item>
</el-dropdown-menu>
</template>
</el-dropdown>
</template>
</vxe-grid>
</XTable>
</el-card>
</div>
<XModal v-model="dialogVisible" :title="dialogTitle">
@ -283,14 +283,13 @@ import {
UploadRawFile
} from 'element-plus'
import { useRouter } from 'vue-router'
import { VxeGridInstance } from 'vxe-table'
import { handleTree, defaultProps } from '@/utils/tree'
import download from '@/utils/download'
import { CommonStatusEnum } from '@/utils/constants'
import { getAccessToken, getTenantId } from '@/utils/auth'
import { useI18n } from '@/hooks/web/useI18n'
import { useMessage } from '@/hooks/web/useMessage'
import { useVxeGrid } from '@/hooks/web/useVxeGrid'
import { useXTable } from '@/hooks/web/useXTable'
import { FormExpose } from '@/components/Form'
import { rules, allSchemas } from './user.data'
import * as UserApi from '@/api/system/user'
@ -312,10 +311,9 @@ const queryParams = reactive({
// ========== ==========
const tableTitle = ref('用户列表')
//
const xGrid = ref<VxeGridInstance>() // Grid Ref
const { gridOptions, getList, deleteData, exportList } = useVxeGrid<UserApi.UserVO>({
const [registerTable, { reload, deleteData, exportList }] = useXTable({
allSchemas: allSchemas,
queryParams: queryParams,
params: queryParams,
getListApi: UserApi.getUserPageApi,
deleteApi: UserApi.deleteUserApi,
exportListApi: UserApi.exportUserApi
@ -334,7 +332,7 @@ const filterNode = (value: string, data: Tree) => {
}
const handleDeptNodeClick = async (row: { [key: string]: any }) => {
queryParams.deptId = row.id
await getList(xGrid)
await reload()
}
const { push } = useRouter()
const handleDeptEdit = () => {
@ -407,10 +405,7 @@ const handleDetail = async (rowId: number) => {
detailData.value = res
await setDialogTile('detail')
}
//
const handleDelete = async (rowId: number) => {
await deleteData(xGrid, rowId)
}
//
const submitForm = async () => {
loading.value = true
@ -428,7 +423,7 @@ const submitForm = async () => {
} finally {
// unref(formRef)?.setSchema(allSchemas.formSchema)
//
await getList(xGrid)
await reload()
loading.value = false
}
}
@ -443,7 +438,7 @@ const handleStatusChange = async (row: UserApi.UserVO) => {
await UserApi.updateUserStatusApi(row.id, row.status)
message.success(text + '成功')
//
await getList(xGrid)
await reload()
})
.catch(() => {
row.status =
@ -544,7 +539,7 @@ const handleFileSuccess = async (response: any): Promise<void> => {
text += '< ' + username + ': ' + data.failureUsernames[username] + ' >'
}
message.alert(text)
await getList(xGrid)
await reload()
}
//
const handleExceed = (): void => {

View File

@ -1,6 +1,6 @@
{
"name": "yudao-ui-admin",
"version": "1.6.5-snapshot",
"version": "1.6.6-snapshot",
"description": "芋道管理系统",
"author": "芋道",
"license": "MIT",

View File

@ -1,14 +1,16 @@
<template>
<div class="app-container">
<doc-alert title="上传下载" url="https://doc.iocoder.cn/file/" />
<doc-alert title="上传下载" url="https://doc.iocoder.cn/file/"/>
<!-- 搜索工作栏 -->
<el-form :model="queryParams" ref="queryForm" size="small" :inline="true" v-show="showSearch" label-width="68px">
<el-form-item label="文件路径" prop="path">
<el-input v-model="queryParams.path" placeholder="请输入文件路径" clearable @keyup.enter.native="handleQuery"/>
</el-form-item>
<el-form-item label="创建时间" prop="createTime">
<el-date-picker v-model="queryParams.createTime" style="width: 240px" value-format="yyyy-MM-dd HH:mm:ss" type="daterange"
range-separator="-" start-placeholder="开始日期" end-placeholder="结束日期" :default-time="['00:00:00', '23:59:59']" />
<el-date-picker v-model="queryParams.createTime" style="width: 240px" value-format="yyyy-MM-dd HH:mm:ss"
type="daterange"
range-separator="-" start-placeholder="开始日期" end-placeholder="结束日期"
:default-time="['00:00:00', '23:59:59']"/>
</el-form-item>
<el-form-item>
<el-button type="primary" icon="el-icon-search" @click="handleQuery">搜索</el-button>
@ -35,6 +37,9 @@
<template v-slot="scope">
<image-preview v-if="scope.row.type&&scope.row.type.indexOf('image/') === 0" :src="scope.row.url"
:width="'100px'"></image-preview>
<video v-else-if="scope.row.type&&scope.row.type.indexOf('video/') === 0" :width="'100px'">
<source :src="scope.row.url"/>
</video>
<i v-else>无法预览点击
<el-link type="primary" :underline="false" style="font-size:12px;vertical-align: baseline;" target="_blank"
:href="getFileUrl + scope.row.configId + '/get/' + scope.row.path">下载
@ -118,7 +123,7 @@ export default {
title: "", //
isUploading: false, //
url: process.env.VUE_APP_BASE_API + "/admin-api/infra/file/upload", //
headers: { Authorization: "Bearer " + getAccessToken() }, //
headers: {Authorization: "Bearer " + getAccessToken()}, //
data: {} //
},
};
@ -189,19 +194,20 @@ export default {
/** 删除按钮操作 */
handleDelete(row) {
const id = row.id;
this.$modal.confirm('是否确认删除文件编号为"' + id + '"的数据项?').then(function() {
this.$modal.confirm('是否确认删除文件编号为"' + id + '"的数据项?').then(function () {
return deleteFile(id);
}).then(() => {
this.getList();
this.$modal.msgSuccess("删除成功");
}).catch(() => {});
}).catch(() => {
});
},
//
sizeFormat(row, column) {
const unitArr = ["Bytes","KB","MB","GB","TB","PB","EB","ZB","YB"];
const unitArr = ["Bytes", "KB", "MB", "GB", "TB", "PB", "EB", "ZB", "YB"];
const srcSize = parseFloat(row.size);
const index = Math.floor(Math.log(srcSize) / Math.log(1024));
let size =srcSize/Math.pow(1024,index);
let size = srcSize / Math.pow(1024, index);
size = size.toFixed(2);//
return size + ' ' + unitArr[index];
},

View File

@ -136,8 +136,8 @@
<script>
import {getCache, getKeyDefineList, getKeyList, getKeyValue, deleteKey, deleteKeys} from "@/api/infra/redis";
import echarts from "echarts";
import * as echarts from 'echarts'
require('echarts/theme/macarons') // echarts theme
export default {
name: "Server",
data () {

View File

@ -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>

View File

@ -30,7 +30,7 @@ module.exports = vm => {
if (!isRefreshToken) {
isRefreshToken = true
// 1. 如果获取不到刷新令牌,则只能执行登出操作
if (!vm.$store.getters.refreshToken()) {
if (!vm.$store.getters.refreshToken) {
vm.$store.commit('CLEAR_LOGIN_INFO')
return Promise.reject(res)
}