diff --git a/.run/ruoyi-monitor-admin.run.xml b/.run/ruoyi-monitor-admin.run.xml
index 57a0e1709..b2bf329bb 100644
--- a/.run/ruoyi-monitor-admin.run.xml
+++ b/.run/ruoyi-monitor-admin.run.xml
@@ -2,7 +2,7 @@
-
+
diff --git a/.run/ruoyi-server.run.xml b/.run/ruoyi-server.run.xml
index c51b9d5fd..7dc652824 100644
--- a/.run/ruoyi-server.run.xml
+++ b/.run/ruoyi-server.run.xml
@@ -2,7 +2,7 @@
-
+
diff --git a/.run/ruoyi-xxl-job-admin.run.xml b/.run/ruoyi-xxl-job-admin.run.xml
index a9657b03d..47b662a58 100644
--- a/.run/ruoyi-xxl-job-admin.run.xml
+++ b/.run/ruoyi-xxl-job-admin.run.xml
@@ -2,7 +2,7 @@
-
+
diff --git a/README.md b/README.md
index a87d4037d..7f3782076 100644
--- a/README.md
+++ b/README.md
@@ -10,7 +10,7 @@
[](https://gitee.com/dromara/RuoYi-Vue-Plus/blob/master/LICENSE)
[](https://www.jetbrains.com/?from=RuoYi-Vue-Plus)
-[](https://gitee.com/dromara/RuoYi-Vue-Plus)
+[](https://gitee.com/dromara/RuoYi-Vue-Plus)
[]()
[]()
[]()
@@ -53,7 +53,7 @@
| 分布式任务调度 | 采用 Xxl-Job 天生支持分布式 统一的管理中心 | 采用 Quartz 基于数据库锁性能差 集群需要做很多配置与改造 |
| 文件存储 | 采用 Minio 分布式文件存储 天生支持多机、多硬盘、多分片、多副本存储 支持权限管理 安全可靠 文件可加密存储 | 采用 本机文件存储 文件裸漏 易丢失泄漏 不支持集群有单点效应 |
| 云存储 | 采用 AWS S3 协议客户端 支持 七牛、阿里、腾讯 等一切支持S3协议的厂家 | 不支持 |
-| 短信 | 支持 阿里、腾讯 只需在yml配置好厂家密钥即可使用 接口化支持扩展其他厂家 | 不支持 |
+| 短信 | 采用 sms4j 短信融合包 支持数十种短信厂家 只需在yml配置好厂家密钥即可使用 可多厂家共用 | 不支持 |
| 邮件 | 采用 mail-api 通用协议支持大部分邮件厂商 | 不支持 |
| 接口文档 | 采用 SpringDoc、javadoc 无注解零入侵基于java注释 只需把注释写好 无需再写一大堆的文档注解了 | 采用 Springfox 已停止维护 需要编写大量的注解来支持文档生成 |
| 校验框架 | 采用 Validation 支持注解与工具类校验 注解支持国际化 | 仅支持注解 且注解不支持国际化 |
@@ -125,7 +125,6 @@
* GitHub 地址 [RuoYi-Vue-Plus-github](https://github.com/dromara/RuoYi-Vue-Plus)
* 单模块 分支 [RuoYi-Vue-Plus-fast](https://gitee.com/dromara/RuoYi-Vue-Plus/tree/fast/)
* 微服务 分支 [RuoYi-Cloud-Plus](https://gitee.com/JavaLionLi/RuoYi-Cloud-Plus)
-* Vue3 分支 [RuoYi-Vue-Plus-UI](https://gitee.com/JavaLionLi/RuoYi-Vue-Plus-UI)
* 用户扩展项目 [扩展项目列表](https://gitee.com/dromara/RuoYi-Vue-Plus/wikis/pages?sort_id=4478302&doc_id=1469725)
## 加群与捐献
diff --git a/pom.xml b/pom.xml
index 16f49a77f..9184209d5 100644
--- a/pom.xml
+++ b/pom.xml
@@ -6,15 +6,15 @@
com.ruoyi
ruoyi-vue-plus
- 4.7.0
+ 4.8.0
RuoYi-Vue-Plus
https://gitee.com/dromara/RuoYi-Vue-Plus
RuoYi-Vue-Plus后台管理系统
- 4.7.0
- 2.7.11
+ 4.8.0
+ 2.7.13
UTF-8
UTF-8
1.8
@@ -22,9 +22,9 @@
2.2.2
1.6.15
5.2.3
- 3.2.1
+ 3.3.1
2.3
- 1.34.0
+ 1.35.0.RC
3.5.3.1
3.9.1
5.8.18
@@ -46,8 +46,7 @@
1.12.400
- 2.0.23
- 3.1.687
+ 2.2.0
@@ -200,16 +199,11 @@
${aws-java-sdk-s3.version}
+
- com.aliyun
- dysmsapi20170525
- ${aliyun.sms.version}
-
-
-
- com.tencentcloudapi
- tencentcloud-sdk-java-sms
- ${tencent.sms.version}
+ org.dromara.sms4j
+ sms4j-spring-boot-starter
+ ${sms4j.version}
@@ -420,8 +414,8 @@
public
- aliyun nexus
- https://maven.aliyun.com/repository/public/
+ huawei nexus
+ https://mirrors.huaweicloud.com/repository/maven/
true
@@ -431,8 +425,8 @@
public
- aliyun nexus
- https://maven.aliyun.com/repository/public/
+ huawei nexus
+ https://mirrors.huaweicloud.com/repository/maven/
true
diff --git a/ruoyi-admin/pom.xml b/ruoyi-admin/pom.xml
index da22b1419..2d70db620 100644
--- a/ruoyi-admin/pom.xml
+++ b/ruoyi-admin/pom.xml
@@ -5,7 +5,7 @@
ruoyi-vue-plus
com.ruoyi
- 4.7.0
+ 4.8.0
4.0.0
jar
diff --git a/ruoyi-admin/src/main/java/com/ruoyi/web/controller/common/CaptchaController.java b/ruoyi-admin/src/main/java/com/ruoyi/web/controller/common/CaptchaController.java
index f5a603a43..604b5dfa1 100644
--- a/ruoyi-admin/src/main/java/com/ruoyi/web/controller/common/CaptchaController.java
+++ b/ruoyi-admin/src/main/java/com/ruoyi/web/controller/common/CaptchaController.java
@@ -16,12 +16,13 @@ import com.ruoyi.common.utils.reflect.ReflectUtils;
import com.ruoyi.common.utils.spring.SpringUtils;
import com.ruoyi.framework.config.properties.CaptchaProperties;
import com.ruoyi.framework.config.properties.MailProperties;
-import com.ruoyi.sms.config.properties.SmsProperties;
-import com.ruoyi.sms.core.SmsTemplate;
-import com.ruoyi.sms.entity.SmsResult;
import com.ruoyi.system.service.ISysConfigService;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
+import org.dromara.sms4j.api.SmsBlend;
+import org.dromara.sms4j.api.entity.SmsResponse;
+import org.dromara.sms4j.core.factory.SmsFactory;
+import org.dromara.sms4j.provider.enumerate.SupplierType;
import org.springframework.expression.Expression;
import org.springframework.expression.ExpressionParser;
import org.springframework.expression.spel.standard.SpelExpressionParser;
@@ -32,6 +33,7 @@ import org.springframework.web.bind.annotation.RestController;
import javax.validation.constraints.NotBlank;
import java.time.Duration;
import java.util.HashMap;
+import java.util.LinkedHashMap;
import java.util.Map;
/**
@@ -47,7 +49,6 @@ import java.util.Map;
public class CaptchaController {
private final CaptchaProperties captchaProperties;
- private final SmsProperties smsProperties;
private final ISysConfigService configService;
private final MailProperties mailProperties;
@@ -58,21 +59,18 @@ public class CaptchaController {
*/
@GetMapping("/captchaSms")
public R smsCaptcha(@NotBlank(message = "{user.phonenumber.not.blank}") String phonenumber) {
- if (!smsProperties.getEnabled()) {
- return R.fail("当前系统没有开启短信功能!");
- }
String key = CacheConstants.CAPTCHA_CODE_KEY + phonenumber;
String code = RandomUtil.randomNumbers(4);
RedisUtils.setCacheObject(key, code, Duration.ofMinutes(Constants.CAPTCHA_EXPIRATION));
// 验证码模板id 自行处理 (查数据库或写死均可)
String templateId = "";
- Map map = new HashMap<>(1);
+ LinkedHashMap map = new LinkedHashMap<>(1);
map.put("code", code);
- SmsTemplate smsTemplate = SpringUtils.getBean(SmsTemplate.class);
- SmsResult result = smsTemplate.send(phonenumber, templateId, map);
- if (!result.isSuccess()) {
- log.error("验证码短信发送异常 => {}", result);
- return R.fail(result.getMessage());
+ SmsBlend smsBlend = SmsFactory.createSmsBlend(SupplierType.ALIBABA);
+ SmsResponse smsResponse = smsBlend.sendMessage(phonenumber, templateId, map);
+ if (!"OK".equals(smsResponse.getCode())) {
+ log.error("验证码短信发送异常 => {}", smsResponse);
+ return R.fail(smsResponse.getMessage());
}
return R.ok();
}
diff --git a/ruoyi-admin/src/main/java/com/ruoyi/web/controller/monitor/SysUserOnlineController.java b/ruoyi-admin/src/main/java/com/ruoyi/web/controller/monitor/SysUserOnlineController.java
index 33d425a6a..17e6d5af5 100644
--- a/ruoyi-admin/src/main/java/com/ruoyi/web/controller/monitor/SysUserOnlineController.java
+++ b/ruoyi-admin/src/main/java/com/ruoyi/web/controller/monitor/SysUserOnlineController.java
@@ -47,7 +47,7 @@ public class SysUserOnlineController extends BaseController {
for (String key : keys) {
String token = StringUtils.substringAfterLast(key, ":");
// 如果已经过期则跳过
- if (StpUtil.stpLogic.getTokenActivityTimeoutByToken(token) < -1) {
+ if (StpUtil.stpLogic.getTokenActiveTimeoutByToken(token) < -1) {
continue;
}
userOnlineDTOList.add(RedisUtils.getCacheObject(CacheConstants.ONLINE_TOKEN_KEY + token));
diff --git a/ruoyi-admin/src/main/java/com/ruoyi/web/controller/system/SysRoleController.java b/ruoyi-admin/src/main/java/com/ruoyi/web/controller/system/SysRoleController.java
index f8edb5d81..97d7b3c12 100644
--- a/ruoyi-admin/src/main/java/com/ruoyi/web/controller/system/SysRoleController.java
+++ b/ruoyi-admin/src/main/java/com/ruoyi/web/controller/system/SysRoleController.java
@@ -80,6 +80,7 @@ public class SysRoleController extends BaseController {
@Log(title = "角色管理", businessType = BusinessType.INSERT)
@PostMapping
public R add(@Validated @RequestBody SysRole role) {
+ roleService.checkRoleAllowed(role);
if (!roleService.checkRoleNameUnique(role)) {
return R.fail("新增角色'" + role.getRoleName() + "'失败,角色名称已存在");
} else if (!roleService.checkRoleKeyUnique(role)) {
diff --git a/ruoyi-admin/src/main/resources/application-dev.yml b/ruoyi-admin/src/main/resources/application-dev.yml
index 32123a764..ffbc1c81a 100644
--- a/ruoyi-admin/src/main/resources/application-dev.yml
+++ b/ruoyi-admin/src/main/resources/application-dev.yml
@@ -158,14 +158,29 @@ mail:
# Socket连接超时值,单位毫秒,缺省值不超时
connectionTimeout: 0
---- # sms 短信
+--- # sms 短信 支持 阿里云 腾讯云 云片 等等各式各样的短信服务商
+# https://wind.kim/doc/start 文档地址 各个厂商可同时使用
sms:
- enabled: false
# 阿里云 dysmsapi.aliyuncs.com
- # 腾讯云 sms.tencentcloudapi.com
- endpoint: "dysmsapi.aliyuncs.com"
- accessKeyId: xxxxxxx
- accessKeySecret: xxxxxx
- signName: 测试
- # 腾讯专用
- sdkAppId:
+ alibaba:
+ #请求地址 默认为 dysmsapi.aliyuncs.com 如无特殊改变可以不用设置
+ requestUrl: dysmsapi.aliyuncs.com
+ #阿里云的accessKey
+ accessKeyId: xxxxxxx
+ #阿里云的accessKeySecret
+ accessKeySecret: xxxxxxx
+ #短信签名
+ signature: 测试
+ tencent:
+ #请求地址默认为 sms.tencentcloudapi.com 如无特殊改变可不用设置
+ requestUrl: sms.tencentcloudapi.com
+ #腾讯云的accessKey
+ accessKeyId: xxxxxxx
+ #腾讯云的accessKeySecret
+ accessKeySecret: xxxxxxx
+ #短信签名
+ signature: 测试
+ #短信sdkAppId
+ sdkAppId: appid
+ #地域信息默认为 ap-guangzhou 如无特殊改变可不用设置
+ territory: ap-guangzhou
diff --git a/ruoyi-admin/src/main/resources/application-prod.yml b/ruoyi-admin/src/main/resources/application-prod.yml
index 85f6e935d..6292ceab6 100644
--- a/ruoyi-admin/src/main/resources/application-prod.yml
+++ b/ruoyi-admin/src/main/resources/application-prod.yml
@@ -161,14 +161,29 @@ mail:
# Socket连接超时值,单位毫秒,缺省值不超时
connectionTimeout: 0
---- # sms 短信
+--- # sms 短信 支持 阿里云 腾讯云 云片 等等各式各样的短信服务商
+# https://wind.kim/doc/start 文档地址 各个厂商可同时使用
sms:
- enabled: false
# 阿里云 dysmsapi.aliyuncs.com
- # 腾讯云 sms.tencentcloudapi.com
- endpoint: "dysmsapi.aliyuncs.com"
- accessKeyId: xxxxxxx
- accessKeySecret: xxxxxx
- signName: 测试
- # 腾讯专用
- sdkAppId:
+ alibaba:
+ #请求地址 默认为 dysmsapi.aliyuncs.com 如无特殊改变可以不用设置
+ requestUrl: dysmsapi.aliyuncs.com
+ #阿里云的accessKey
+ accessKeyId: xxxxxxx
+ #阿里云的accessKeySecret
+ accessKeySecret: xxxxxxx
+ #短信签名
+ signature: 测试
+ tencent:
+ #请求地址默认为 sms.tencentcloudapi.com 如无特殊改变可不用设置
+ requestUrl: sms.tencentcloudapi.com
+ #腾讯云的accessKey
+ accessKeyId: xxxxxxx
+ #腾讯云的accessKeySecret
+ accessKeySecret: xxxxxxx
+ #短信签名
+ signature: 测试
+ #短信sdkAppId
+ sdkAppId: appid
+ #地域信息默认为 ap-guangzhou 如无特殊改变可不用设置
+ territory: ap-guangzhou
diff --git a/ruoyi-admin/src/main/resources/application.yml b/ruoyi-admin/src/main/resources/application.yml
index a6617bc6f..da4154526 100644
--- a/ruoyi-admin/src/main/resources/application.yml
+++ b/ruoyi-admin/src/main/resources/application.yml
@@ -104,8 +104,9 @@ sa-token:
token-name: Authorization
# token有效期 设为一天 (必定过期) 单位: 秒
timeout: 86400
- # token临时有效期 (指定时间无操作就过期) 单位: 秒
- activity-timeout: 1800
+ # 多端不同 token 有效期 可查看 LoginHelper.loginByDevice 方法自定义
+ # token最低活跃时间 (指定时间无操作就过期) 单位: 秒
+ active-timeout: 1800
# 是否允许同一账号并发登录 (为true时允许一起登录, 为false时新登录挤掉旧登录)
is-concurrent: true
# 在多人登录同一账号时,是否共用一个token (为true时所有登录共用一个token, 为false时每次登录新建一个token)
diff --git a/ruoyi-common/pom.xml b/ruoyi-common/pom.xml
index 3ac834bf2..b1ae8c99c 100644
--- a/ruoyi-common/pom.xml
+++ b/ruoyi-common/pom.xml
@@ -5,7 +5,7 @@
ruoyi-vue-plus
com.ruoyi
- 4.7.0
+ 4.8.0
4.0.0
diff --git a/ruoyi-common/src/main/java/com/ruoyi/common/annotation/EncryptField.java b/ruoyi-common/src/main/java/com/ruoyi/common/annotation/EncryptField.java
index 2d6a321f1..2242351a9 100644
--- a/ruoyi-common/src/main/java/com/ruoyi/common/annotation/EncryptField.java
+++ b/ruoyi-common/src/main/java/com/ruoyi/common/annotation/EncryptField.java
@@ -32,7 +32,7 @@ public @interface EncryptField {
String publicKey() default "";
/**
- * 公钥。RSA、SM2需要
+ * 私钥。RSA、SM2需要
*/
String privateKey() default "";
diff --git a/ruoyi-common/src/main/java/com/ruoyi/common/constant/UserConstants.java b/ruoyi-common/src/main/java/com/ruoyi/common/constant/UserConstants.java
index 4a095faad..569c5dafa 100644
--- a/ruoyi-common/src/main/java/com/ruoyi/common/constant/UserConstants.java
+++ b/ruoyi-common/src/main/java/com/ruoyi/common/constant/UserConstants.java
@@ -129,4 +129,9 @@ public interface UserConstants {
*/
Long ADMIN_ID = 1L;
+ /**
+ * 管理员角色key
+ */
+ String ADMIN_ROLE_KEY = "admin";
+
}
diff --git a/ruoyi-common/src/main/java/com/ruoyi/common/convert/ExcelEnumConvert.java b/ruoyi-common/src/main/java/com/ruoyi/common/convert/ExcelEnumConvert.java
index c69d90cfb..9a3eb31db 100644
--- a/ruoyi-common/src/main/java/com/ruoyi/common/convert/ExcelEnumConvert.java
+++ b/ruoyi-common/src/main/java/com/ruoyi/common/convert/ExcelEnumConvert.java
@@ -37,14 +37,36 @@ public class ExcelEnumConvert implements Converter {
@Override
public Object convertToJavaData(ReadCellData> cellData, ExcelContentProperty contentProperty, GlobalConfiguration globalConfiguration) {
- Object codeValue = cellData.getData();
+ cellData.checkEmpty();
+ // Excel中填入的是枚举中指定的描述
+ Object textValue = null;
+ switch (cellData.getType()) {
+ case STRING:
+ case DIRECT_STRING:
+ case RICH_TEXT_STRING:
+ textValue = cellData.getStringValue();
+ break;
+ case NUMBER:
+ textValue = cellData.getNumberValue();
+ break;
+ case BOOLEAN:
+ textValue = cellData.getBooleanValue();
+ break;
+ default:
+ throw new IllegalArgumentException("单元格类型异常!");
+ }
// 如果是空值
- if (ObjectUtil.isNull(codeValue)) {
+ if (ObjectUtil.isNull(textValue)) {
return null;
}
- Map enumValueMap = beforeConvert(contentProperty);
- String textValue = enumValueMap.get(codeValue);
- return Convert.convert(contentProperty.getField().getType(), textValue);
+ Map enumCodeToTextMap = beforeConvert(contentProperty);
+ // 从Java输出至Excel是code转text
+ // 因此从Excel转Java应该将text与code对调
+ Map enumTextToCodeMap = new HashMap<>();
+ enumCodeToTextMap.forEach((key, value) -> enumTextToCodeMap.put(value, key));
+ // 应该从text -> code中查找
+ Object codeValue = enumTextToCodeMap.get(textValue);
+ return Convert.convert(contentProperty.getField().getType(), codeValue);
}
@Override
diff --git a/ruoyi-common/src/main/java/com/ruoyi/common/core/domain/entity/SysUser.java b/ruoyi-common/src/main/java/com/ruoyi/common/core/domain/entity/SysUser.java
index d9139c511..3196e9ecf 100644
--- a/ruoyi-common/src/main/java/com/ruoyi/common/core/domain/entity/SysUser.java
+++ b/ruoyi-common/src/main/java/com/ruoyi/common/core/domain/entity/SysUser.java
@@ -53,6 +53,7 @@ public class SysUser extends BaseEntity {
* 用户昵称
*/
@Xss(message = "用户昵称不能包含脚本字符")
+ @NotBlank(message = "用户昵称不能为空")
@Size(min = 0, max = 30, message = "用户昵称长度不能超过{max}个字符")
private String nickName;
diff --git a/ruoyi-common/src/main/java/com/ruoyi/common/core/domain/model/EmailLoginBody.java b/ruoyi-common/src/main/java/com/ruoyi/common/core/domain/model/EmailLoginBody.java
index b7bf81bdb..692a210de 100644
--- a/ruoyi-common/src/main/java/com/ruoyi/common/core/domain/model/EmailLoginBody.java
+++ b/ruoyi-common/src/main/java/com/ruoyi/common/core/domain/model/EmailLoginBody.java
@@ -6,7 +6,7 @@ import javax.validation.constraints.Email;
import javax.validation.constraints.NotBlank;
/**
- * 短信登录对象
+ * 邮箱登录对象
*
* @author Lion Li
*/
diff --git a/ruoyi-common/src/main/java/com/ruoyi/common/core/service/DictService.java b/ruoyi-common/src/main/java/com/ruoyi/common/core/service/DictService.java
index b334c82af..a4dbfd5b7 100644
--- a/ruoyi-common/src/main/java/com/ruoyi/common/core/service/DictService.java
+++ b/ruoyi-common/src/main/java/com/ruoyi/common/core/service/DictService.java
@@ -1,5 +1,7 @@
package com.ruoyi.common.core.service;
+import java.util.Map;
+
/**
* 通用 字典服务
*
@@ -54,4 +56,11 @@ public interface DictService {
*/
String getDictValue(String dictType, String dictLabel, String separator);
+ /**
+ * 获取字典下所有的字典值与标签
+ *
+ * @param dictType 字典类型
+ * @return dictValue为key,dictLabel为值组成的Map
+ */
+ Map getAllDictByDictType(String dictType);
}
diff --git a/ruoyi-common/src/main/java/com/ruoyi/common/excel/CellMergeStrategy.java b/ruoyi-common/src/main/java/com/ruoyi/common/excel/CellMergeStrategy.java
index 6102eec50..2e8b6ae7b 100644
--- a/ruoyi-common/src/main/java/com/ruoyi/common/excel/CellMergeStrategy.java
+++ b/ruoyi-common/src/main/java/com/ruoyi/common/excel/CellMergeStrategy.java
@@ -1,19 +1,20 @@
package com.ruoyi.common.excel;
+import cn.hutool.core.collection.CollUtil;
+import com.alibaba.excel.annotation.ExcelProperty;
import com.alibaba.excel.metadata.Head;
import com.alibaba.excel.write.merge.AbstractMergeStrategy;
import com.ruoyi.common.annotation.CellMerge;
+import com.ruoyi.common.utils.reflect.ReflectUtils;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j;
-import org.apache.commons.collections4.CollectionUtils;
import org.apache.poi.ss.usermodel.Cell;
import org.apache.poi.ss.usermodel.Sheet;
import org.apache.poi.ss.util.CellRangeAddress;
import java.lang.reflect.Field;
-import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
@@ -24,91 +25,97 @@ import java.util.Map;
*
* @author Lion Li
*/
-@AllArgsConstructor
@Slf4j
public class CellMergeStrategy extends AbstractMergeStrategy {
- private List> list;
- private boolean hasTitle;
+ private final List cellList;
+ private final boolean hasTitle;
+ private int rowIndex;
- @Override
- protected void merge(Sheet sheet, Cell cell, Head head, Integer relativeRowIndex) {
- List cellList = handle(list, hasTitle);
- // judge the list is not null
- if (CollectionUtils.isNotEmpty(cellList)) {
- // the judge is necessary
- if (cell.getRowIndex() == 1 && cell.getColumnIndex() == 0) {
- for (CellRangeAddress item : cellList) {
- sheet.addMergedRegion(item);
- }
- }
- }
- }
+ public CellMergeStrategy(List> list, boolean hasTitle) {
+ this.hasTitle = hasTitle;
+ // 行合并开始下标
+ this.rowIndex = hasTitle ? 1 : 0;
+ this.cellList = handle(list, hasTitle);
+ }
- @SneakyThrows
- private static List handle(List> list, boolean hasTitle) {
- List cellList = new ArrayList<>();
- if (CollectionUtils.isEmpty(list)) {
- return cellList;
- }
- Class> clazz = list.get(0).getClass();
- Field[] fields = clazz.getDeclaredFields();
- // 有注解的字段
- List mergeFields = new ArrayList<>();
- List mergeFieldsIndex = new ArrayList<>();
- for (int i = 0; i < fields.length; i++) {
- Field field = fields[i];
- if (field.isAnnotationPresent(CellMerge.class)) {
- CellMerge cm = field.getAnnotation(CellMerge.class);
- mergeFields.add(field);
- mergeFieldsIndex.add(cm.index() == -1 ? i : cm.index());
- }
- }
- // 行合并开始下标
- int rowIndex = hasTitle ? 1 : 0;
- Map map = new HashMap<>();
- // 生成两两合并单元格
- for (int i = 0; i < list.size(); i++) {
- for (int j = 0; j < mergeFields.size(); j++) {
- Field field = mergeFields.get(j);
- String name = field.getName();
- String methodName = "get" + name.substring(0, 1).toUpperCase() + name.substring(1);
- Method readMethod = clazz.getMethod(methodName);
- Object val = readMethod.invoke(list.get(i));
+ @Override
+ protected void merge(Sheet sheet, Cell cell, Head head, Integer relativeRowIndex) {
+ // judge the list is not null
+ if (CollUtil.isNotEmpty(cellList)) {
+ // the judge is necessary
+ if (cell.getRowIndex() == rowIndex && cell.getColumnIndex() == 0) {
+ for (CellRangeAddress item : cellList) {
+ sheet.addMergedRegion(item);
+ }
+ }
+ }
+ }
- int colNum = mergeFieldsIndex.get(j);
- if (!map.containsKey(field)) {
- map.put(field, new RepeatCell(val, i));
- } else {
- RepeatCell repeatCell = map.get(field);
- Object cellValue = repeatCell.getValue();
- if (cellValue == null || "".equals(cellValue)) {
- // 空值跳过不合并
- continue;
- }
- if (!cellValue.equals(val)) {
- if (i - repeatCell.getCurrent() > 1) {
- cellList.add(new CellRangeAddress(repeatCell.getCurrent() + rowIndex, i + rowIndex - 1, colNum, colNum));
- }
- map.put(field, new RepeatCell(val, i));
- } else if (i == list.size() - 1) {
- if (i > repeatCell.getCurrent()) {
- cellList.add(new CellRangeAddress(repeatCell.getCurrent() + rowIndex, i + rowIndex, colNum, colNum));
- }
- }
- }
- }
- }
- return cellList;
- }
+ @SneakyThrows
+ private List handle(List> list, boolean hasTitle) {
+ List cellList = new ArrayList<>();
+ if (CollUtil.isEmpty(list)) {
+ return cellList;
+ }
+ Field[] fields = ReflectUtils.getFields(list.get(0).getClass(), field -> !"serialVersionUID".equals(field.getName()));
- @Data
- @AllArgsConstructor
- static class RepeatCell {
+ // 有注解的字段
+ List mergeFields = new ArrayList<>();
+ List mergeFieldsIndex = new ArrayList<>();
+ for (int i = 0; i < fields.length; i++) {
+ Field field = fields[i];
+ if (field.isAnnotationPresent(CellMerge.class)) {
+ CellMerge cm = field.getAnnotation(CellMerge.class);
+ mergeFields.add(field);
+ mergeFieldsIndex.add(cm.index() == -1 ? i : cm.index());
+ if (hasTitle) {
+ ExcelProperty property = field.getAnnotation(ExcelProperty.class);
+ rowIndex = Math.max(rowIndex, property.value().length);
+ }
+ }
+ }
- private Object value;
+ Map map = new HashMap<>();
+ // 生成两两合并单元格
+ for (int i = 0; i < list.size(); i++) {
+ for (int j = 0; j < mergeFields.size(); j++) {
+ Field field = mergeFields.get(j);
+ Object val = ReflectUtils.invokeGetter(list.get(i), field.getName());
- private int current;
+ int colNum = mergeFieldsIndex.get(j);
+ if (!map.containsKey(field)) {
+ map.put(field, new RepeatCell(val, i));
+ } else {
+ RepeatCell repeatCell = map.get(field);
+ Object cellValue = repeatCell.getValue();
+ if (cellValue == null || "".equals(cellValue)) {
+ // 空值跳过不合并
+ continue;
+ }
+ if (!cellValue.equals(val)) {
+ if (i - repeatCell.getCurrent() > 1) {
+ cellList.add(new CellRangeAddress(repeatCell.getCurrent() + rowIndex, i + rowIndex - 1, colNum, colNum));
+ }
+ map.put(field, new RepeatCell(val, i));
+ } else if (i == list.size() - 1) {
+ if (i > repeatCell.getCurrent()) {
+ cellList.add(new CellRangeAddress(repeatCell.getCurrent() + rowIndex, i + rowIndex, colNum, colNum));
+ }
+ }
+ }
+ }
+ }
+ return cellList;
+ }
- }
+ @Data
+ @AllArgsConstructor
+ static class RepeatCell {
+
+ private Object value;
+
+ private int current;
+
+ }
}
diff --git a/ruoyi-common/src/main/java/com/ruoyi/common/excel/DropDownOptions.java b/ruoyi-common/src/main/java/com/ruoyi/common/excel/DropDownOptions.java
new file mode 100644
index 000000000..b649ebc69
--- /dev/null
+++ b/ruoyi-common/src/main/java/com/ruoyi/common/excel/DropDownOptions.java
@@ -0,0 +1,149 @@
+package com.ruoyi.common.excel;
+
+import cn.hutool.core.util.StrUtil;
+import com.ruoyi.common.exception.ServiceException;
+import lombok.AllArgsConstructor;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.function.Function;
+import java.util.stream.Collectors;
+
+/**
+ * Excel下拉可选项
+ * 注意:为确保下拉框解析正确,传值务必使用createOptionValue()做为值的拼接
+ *
+ * @author Emil.Zhang
+ */
+@Data
+@AllArgsConstructor
+@NoArgsConstructor
+@SuppressWarnings("unused")
+public class DropDownOptions {
+ /**
+ * 一级下拉所在列index,从0开始算
+ */
+ private int index = 0;
+ /**
+ * 二级下拉所在的index,从0开始算,不能与一级相同
+ */
+ private int nextIndex = 0;
+ /**
+ * 一级下拉所包含的数据
+ */
+ private List options = new ArrayList<>();
+ /**
+ * 二级下拉所包含的数据Map
+ * 以每一个一级选项值为Key,每个一级选项对应的二级数据为Value
+ */
+ private Map> nextOptions = new HashMap<>();
+ /**
+ * 分隔符
+ */
+ private static final String DELIMITER = "_";
+
+ /**
+ * 创建只有一级的下拉选
+ */
+ public DropDownOptions(int index, List options) {
+ this.index = index;
+ this.options = options;
+ }
+
+ /**
+ * 创建每个选项可选值
+ * 注意:不能以数字,特殊符号开头,选项中不可以包含任何运算符号
+ *
+ * @param vars 可选值内包含的参数
+ * @return 合规的可选值
+ */
+ public static String createOptionValue(Object... vars) {
+ StringBuilder stringBuffer = new StringBuilder();
+ String regex = "^[\\S\\d\\u4e00-\\u9fa5]+$";
+ for (int i = 0; i < vars.length; i++) {
+ String var = StrUtil.trimToEmpty(String.valueOf(vars[i]));
+ if (!var.matches(regex)) {
+ throw new ServiceException("选项数据不符合规则,仅允许使用中英文字符以及数字");
+ }
+ stringBuffer.append(var);
+ if (i < vars.length - 1) {
+ // 直至最后一个前,都以_作为切割线
+ stringBuffer.append(DELIMITER);
+ }
+ }
+ if (stringBuffer.toString().matches("^\\d_*$")) {
+ throw new ServiceException("禁止以数字开头");
+ }
+ return stringBuffer.toString();
+ }
+
+ /**
+ * 将处理后合理的可选值解析为原始的参数
+ *
+ * @param option 经过处理后的合理的可选项
+ * @return 原始的参数
+ */
+ public static List analyzeOptionValue(String option) {
+ return StrUtil.split(option, DELIMITER, true, true);
+ }
+
+ /**
+ * 创建级联下拉选项
+ *
+ * @param parentList 父实体可选项原始数据
+ * @param parentIndex 父下拉选位置
+ * @param sonList 子实体可选项原始数据
+ * @param sonIndex 子下拉选位置
+ * @param parentHowToGetIdFunction 父类如何获取唯一标识
+ * @param sonHowToGetParentIdFunction 子类如何获取父类的唯一标识
+ * @param howToBuildEveryOption 如何生成下拉选内容
+ * @return 级联下拉选项
+ */
+ public static DropDownOptions buildLinkedOptions(List parentList,
+ int parentIndex,
+ List sonList,
+ int sonIndex,
+ Function parentHowToGetIdFunction,
+ Function sonHowToGetParentIdFunction,
+ Function howToBuildEveryOption) {
+ DropDownOptions parentLinkSonOptions = new DropDownOptions();
+ // 先创建父类的下拉
+ parentLinkSonOptions.setIndex(parentIndex);
+ parentLinkSonOptions.setOptions(
+ parentList.stream()
+ .map(howToBuildEveryOption)
+ .collect(Collectors.toList())
+ );
+ // 提取父-子级联下拉
+ Map> sonOptions = new HashMap<>();
+ // 父级依据自己的ID分组
+ Map> parentGroupByIdMap =
+ parentList.stream().collect(Collectors.groupingBy(parentHowToGetIdFunction));
+ // 遍历每个子集,提取到Map中
+ sonList.forEach(everySon -> {
+ if (parentGroupByIdMap.containsKey(sonHowToGetParentIdFunction.apply(everySon))) {
+ // 找到对应的上级
+ T parentObj = parentGroupByIdMap.get(sonHowToGetParentIdFunction.apply(everySon)).get(0);
+ // 提取名称和ID作为Key
+ String key = howToBuildEveryOption.apply(parentObj);
+ // Key对应的Value
+ List thisParentSonOptionList;
+ if (sonOptions.containsKey(key)) {
+ thisParentSonOptionList = sonOptions.get(key);
+ } else {
+ thisParentSonOptionList = new ArrayList<>();
+ sonOptions.put(key, thisParentSonOptionList);
+ }
+ // 往Value中添加当前子集选项
+ thisParentSonOptionList.add(howToBuildEveryOption.apply(everySon));
+ }
+ });
+ parentLinkSonOptions.setNextIndex(sonIndex);
+ parentLinkSonOptions.setNextOptions(sonOptions);
+ return parentLinkSonOptions;
+ }
+}
diff --git a/ruoyi-common/src/main/java/com/ruoyi/common/excel/ExcelDownHandler.java b/ruoyi-common/src/main/java/com/ruoyi/common/excel/ExcelDownHandler.java
new file mode 100644
index 000000000..442e353dd
--- /dev/null
+++ b/ruoyi-common/src/main/java/com/ruoyi/common/excel/ExcelDownHandler.java
@@ -0,0 +1,370 @@
+package com.ruoyi.common.excel;
+
+import cn.hutool.core.collection.CollUtil;
+import cn.hutool.core.util.ArrayUtil;
+import cn.hutool.core.util.EnumUtil;
+import cn.hutool.core.util.ObjectUtil;
+import cn.hutool.core.util.StrUtil;
+import com.alibaba.excel.annotation.ExcelProperty;
+import com.alibaba.excel.write.handler.SheetWriteHandler;
+import com.alibaba.excel.write.metadata.holder.WriteSheetHolder;
+import com.alibaba.excel.write.metadata.holder.WriteWorkbookHolder;
+import com.ruoyi.common.annotation.ExcelDictFormat;
+import com.ruoyi.common.annotation.ExcelEnumFormat;
+import com.ruoyi.common.core.service.DictService;
+import com.ruoyi.common.exception.ServiceException;
+import com.ruoyi.common.utils.StreamUtils;
+import com.ruoyi.common.utils.spring.SpringUtils;
+import lombok.extern.slf4j.Slf4j;
+import org.apache.poi.ss.usermodel.*;
+import org.apache.poi.ss.util.CellRangeAddressList;
+import org.apache.poi.ss.util.WorkbookUtil;
+import org.apache.poi.xssf.usermodel.XSSFDataValidation;
+
+import java.lang.reflect.Field;
+import java.util.*;
+
+/**
+ * Excel表格下拉选操作
+ * 考虑到下拉选过多可能导致Excel打开缓慢的问题,只校验前1000行
+ *
+ * 即只有前1000行的数据可以用下拉框,超出的自行通过限制数据量的形式,第二次输出
+ *
+ * @author Emil.Zhang
+ */
+@Slf4j
+public class ExcelDownHandler implements SheetWriteHandler {
+
+ /**
+ * Excel表格中的列名英文
+ * 仅为了解析列英文,禁止修改
+ */
+ private static final String EXCEL_COLUMN_NAME = "ABCDEFGHIJKLMNOPQRSTUVWXYZ";
+ /**
+ * 单选数据Sheet名
+ */
+ private static final String OPTIONS_SHEET_NAME = "options";
+ /**
+ * 联动选择数据Sheet名的头
+ */
+ private static final String LINKED_OPTIONS_SHEET_NAME = "linkedOptions";
+ /**
+ * 下拉可选项
+ */
+ private final List dropDownOptions;
+ /**
+ * 当前单选进度
+ */
+ private int currentOptionsColumnIndex;
+ /**
+ * 当前联动选择进度
+ */
+ private int currentLinkedOptionsSheetIndex;
+ private final DictService dictService;
+
+ public ExcelDownHandler(List options) {
+ this.dropDownOptions = options;
+ this.currentOptionsColumnIndex = 0;
+ this.currentLinkedOptionsSheetIndex = 0;
+ this.dictService = SpringUtils.getBean(DictService.class);
+ }
+
+ /**
+ * 开始创建下拉数据
+ * 1.通过解析传入的@ExcelProperty同级是否标注有@DropDown选项
+ * 如果有且设置了value值,则将其直接置为下拉可选项
+ *
+ * 2.或者在调用ExcelUtil时指定了可选项,将依据传入的可选项做下拉
+ *
+ * 3.二者并存,注意调用方式
+ */
+ @Override
+ public void afterSheetCreate(WriteWorkbookHolder writeWorkbookHolder, WriteSheetHolder writeSheetHolder) {
+ Sheet sheet = writeSheetHolder.getSheet();
+ // 开始设置下拉框 HSSFWorkbook
+ DataValidationHelper helper = sheet.getDataValidationHelper();
+ Field[] fields = writeWorkbookHolder.getClazz().getDeclaredFields();
+ Workbook workbook = writeWorkbookHolder.getWorkbook();
+ int length = fields.length;
+ for (int i = 0; i < length; i++) {
+ // 循环实体中的每个属性
+ // 可选的下拉值
+ List options = new ArrayList<>();
+ if (fields[i].isAnnotationPresent(ExcelDictFormat.class)) {
+ // 如果指定了@ExcelDictFormat,则使用字典的逻辑
+ ExcelDictFormat format = fields[i].getDeclaredAnnotation(ExcelDictFormat.class);
+ String dictType = format.dictType();
+ String converterExp = format.readConverterExp();
+ if (StrUtil.isNotBlank(dictType)) {
+ // 如果传递了字典名,则依据字典建立下拉
+ Collection values = Optional.ofNullable(dictService.getAllDictByDictType(dictType))
+ .orElseThrow(() -> new ServiceException(String.format("字典 %s 不存在", dictType)))
+ .values();
+ options = new ArrayList<>(values);
+ } else if (StrUtil.isNotBlank(converterExp)) {
+ // 如果指定了确切的值,则直接解析确切的值
+ options = StrUtil.split(converterExp, format.separator(), true, true);
+ }
+ } else if (fields[i].isAnnotationPresent(ExcelEnumFormat.class)) {
+ // 否则如果指定了@ExcelEnumFormat,则使用枚举的逻辑
+ ExcelEnumFormat format = fields[i].getDeclaredAnnotation(ExcelEnumFormat.class);
+ List values = EnumUtil.getFieldValues(format.enumClass(), format.textField());
+ options = StreamUtils.toList(values, String::valueOf);
+ }
+ if (ObjectUtil.isNotEmpty(options)) {
+ // 仅当下拉可选项不为空时执行
+ // 获取列下标,默认为当前循环次数
+ int index = i;
+ if (fields[i].isAnnotationPresent(ExcelProperty.class)) {
+ // 如果指定了列下标,以指定的为主
+ index = fields[i].getDeclaredAnnotation(ExcelProperty.class).index();
+ }
+ if (options.size() > 20) {
+ // 这里限制如果可选项大于20,则使用额外表形式
+ dropDownWithSheet(helper, workbook, sheet, index, options);
+ } else {
+ // 否则使用固定值形式
+ dropDownWithSimple(helper, sheet, index, options);
+ }
+ }
+ }
+ dropDownOptions.forEach(everyOptions -> {
+ // 如果传递了下拉框选择器参数
+ if (!everyOptions.getNextOptions().isEmpty()) {
+ // 当二级选项不为空时,使用额外关联表的形式
+ dropDownLinkedOptions(helper, workbook, sheet, everyOptions);
+ } else if (everyOptions.getOptions().size() > 10) {
+ // 当一级选项参数个数大于10,使用额外表的形式
+ dropDownWithSheet(helper, workbook, sheet, everyOptions.getIndex(), everyOptions.getOptions());
+ } else if (everyOptions.getOptions().size() != 0) {
+ // 当一级选项个数不为空,使用默认形式
+ dropDownWithSimple(helper, sheet, everyOptions.getIndex(), everyOptions.getOptions());
+ }
+ });
+ }
+
+ /**
+ * 简单下拉框
+ * 直接将可选项拼接为指定列的数据校验值
+ *
+ * @param celIndex 列index
+ * @param value 下拉选可选值
+ */
+ private void dropDownWithSimple(DataValidationHelper helper, Sheet sheet, Integer celIndex, List value) {
+ if (ObjectUtil.isEmpty(value)) {
+ return;
+ }
+ this.markOptionsToSheet(helper, sheet, celIndex, helper.createExplicitListConstraint(ArrayUtil.toArray(value, String.class)));
+ }
+
+ /**
+ * 额外表格形式的级联下拉框
+ *
+ * @param options 额外表格形式存储的下拉可选项
+ */
+ private void dropDownLinkedOptions(DataValidationHelper helper, Workbook workbook, Sheet sheet, DropDownOptions options) {
+ String linkedOptionsSheetName = String.format("%s_%d", LINKED_OPTIONS_SHEET_NAME, currentLinkedOptionsSheetIndex);
+ // 创建联动下拉数据表
+ Sheet linkedOptionsDataSheet = workbook.createSheet(WorkbookUtil.createSafeSheetName(linkedOptionsSheetName));
+ // 将下拉表隐藏
+ workbook.setSheetHidden(workbook.getSheetIndex(linkedOptionsDataSheet), true);
+ // 完善横向的一级选项数据表
+ List firstOptions = options.getOptions();
+ Map> secoundOptionsMap = options.getNextOptions();
+
+ // 创建名称管理器
+ Name name = workbook.createName();
+ // 设置名称管理器的别名
+ name.setNameName(linkedOptionsSheetName);
+ // 以横向第一行创建一级下拉拼接引用位置
+ String firstOptionsFunction = String.format("%s!$%s$1:$%s$1",
+ linkedOptionsSheetName,
+ getExcelColumnName(0),
+ getExcelColumnName(firstOptions.size())
+ );
+ // 设置名称管理器的引用位置
+ name.setRefersToFormula(firstOptionsFunction);
+ // 设置数据校验为序列模式,引用的是名称管理器中的别名
+ this.markOptionsToSheet(helper, sheet, options.getIndex(), helper.createFormulaListConstraint(linkedOptionsSheetName));
+
+ for (int columIndex = 0; columIndex < firstOptions.size(); columIndex++) {
+ // 先提取主表中一级下拉的列名
+ String firstOptionsColumnName = getExcelColumnName(columIndex);
+ // 一次循环是每一个一级选项
+ int finalI = columIndex;
+ // 本次循环的一级选项值
+ String thisFirstOptionsValue = firstOptions.get(columIndex);
+ // 创建第一行的数据
+ Optional.ofNullable(linkedOptionsDataSheet.getRow(0))
+ // 如果不存在则创建第一行
+ .orElseGet(() -> linkedOptionsDataSheet.createRow(finalI))
+ // 第一行当前列
+ .createCell(columIndex)
+ // 设置值为当前一级选项值
+ .setCellValue(thisFirstOptionsValue);
+
+ // 第二行开始,设置第二级别选项参数
+ List secondOptions = secoundOptionsMap.get(thisFirstOptionsValue);
+ if (CollUtil.isEmpty(secondOptions)) {
+ // 必须保证至少有一个关联选项,否则将导致Excel解析错误
+ secondOptions = Collections.singletonList("暂无_0");
+ }
+
+ // 以该一级选项值创建子名称管理器
+ Name sonName = workbook.createName();
+ // 设置名称管理器的别名
+ sonName.setNameName(thisFirstOptionsValue);
+ // 以第二行该列数据拼接引用位置
+ String sonFunction = String.format("%s!$%s$2:$%s$%d",
+ linkedOptionsSheetName,
+ firstOptionsColumnName,
+ firstOptionsColumnName,
+ secondOptions.size() + 1
+ );
+ // 设置名称管理器的引用位置
+ sonName.setRefersToFormula(sonFunction);
+ // 数据验证为序列模式,引用到每一个主表中的二级选项位置
+ // 创建子项的名称管理器,只是为了使得Excel可以识别到数据
+ String mainSheetFirstOptionsColumnName = getExcelColumnName(options.getIndex());
+ for (int i = 0; i < 100; i++) {
+ // 以一级选项对应的主体所在位置创建二级下拉
+ String secondOptionsFunction = String.format("=INDIRECT(%s%d)", mainSheetFirstOptionsColumnName, i + 1);
+ // 二级只能主表每一行的每一列添加二级校验
+ markLinkedOptionsToSheet(helper, sheet, i, options.getNextIndex(), helper.createFormulaListConstraint(secondOptionsFunction));
+ }
+
+ for (int rowIndex = 0; rowIndex < secondOptions.size(); rowIndex++) {
+ // 从第二行开始填充二级选项
+ int finalRowIndex = rowIndex + 1;
+ int finalColumIndex = columIndex;
+
+ Row row = Optional.ofNullable(linkedOptionsDataSheet.getRow(finalRowIndex))
+ // 没有则创建
+ .orElseGet(() -> linkedOptionsDataSheet.createRow(finalRowIndex));
+ Optional
+ // 在本级一级选项所在的列
+ .ofNullable(row.getCell(finalColumIndex))
+ // 不存在则创建
+ .orElseGet(() -> row.createCell(finalColumIndex))
+ // 设置二级选项值
+ .setCellValue(secondOptions.get(rowIndex));
+ }
+ }
+
+ currentLinkedOptionsSheetIndex++;
+ }
+
+ /**
+ * 额外表格形式的普通下拉框
+ * 由于下拉框可选值数量过多,为提升Excel打开效率,使用额外表格形式做下拉
+ *
+ * @param celIndex 下拉选
+ * @param value 下拉选可选值
+ */
+ private void dropDownWithSheet(DataValidationHelper helper, Workbook workbook, Sheet sheet, Integer celIndex, List value) {
+ // 创建下拉数据表
+ Sheet simpleDataSheet = Optional.ofNullable(workbook.getSheet(WorkbookUtil.createSafeSheetName(OPTIONS_SHEET_NAME)))
+ .orElseGet(() -> workbook.createSheet(WorkbookUtil.createSafeSheetName(OPTIONS_SHEET_NAME)));
+ // 将下拉表隐藏
+ workbook.setSheetHidden(workbook.getSheetIndex(simpleDataSheet), true);
+ // 完善纵向的一级选项数据表
+ for (int i = 0; i < value.size(); i++) {
+ int finalI = i;
+ // 获取每一选项行,如果没有则创建
+ Row row = Optional.ofNullable(simpleDataSheet.getRow(i))
+ .orElseGet(() -> simpleDataSheet.createRow(finalI));
+ // 获取本级选项对应的选项列,如果没有则创建
+ Cell cell = Optional.ofNullable(row.getCell(currentOptionsColumnIndex))
+ .orElseGet(() -> row.createCell(currentOptionsColumnIndex));
+ // 设置值
+ cell.setCellValue(value.get(i));
+ }
+
+ // 创建名称管理器
+ Name name = workbook.createName();
+ // 设置名称管理器的别名
+ String nameName = String.format("%s_%d", OPTIONS_SHEET_NAME, celIndex);
+ name.setNameName(nameName);
+ // 以纵向第一列创建一级下拉拼接引用位置
+ String function = String.format("%s!$%s$1:$%s$%d",
+ OPTIONS_SHEET_NAME,
+ getExcelColumnName(currentOptionsColumnIndex),
+ getExcelColumnName(currentOptionsColumnIndex),
+ value.size());
+ // 设置名称管理器的引用位置
+ name.setRefersToFormula(function);
+ // 设置数据校验为序列模式,引用的是名称管理器中的别名
+ this.markOptionsToSheet(helper, sheet, celIndex, helper.createFormulaListConstraint(nameName));
+ currentOptionsColumnIndex++;
+ }
+
+ /**
+ * 挂载下拉的列,仅限一级选项
+ */
+ private void markOptionsToSheet(DataValidationHelper helper, Sheet sheet, Integer celIndex,
+ DataValidationConstraint constraint) {
+ // 设置数据有效性加载在哪个单元格上,四个参数分别是:起始行、终止行、起始列、终止列
+ CellRangeAddressList addressList = new CellRangeAddressList(1, 1000, celIndex, celIndex);
+ markDataValidationToSheet(helper, sheet, constraint, addressList);
+ }
+
+ /**
+ * 挂载下拉的列,仅限二级选项
+ */
+ private void markLinkedOptionsToSheet(DataValidationHelper helper, Sheet sheet, Integer rowIndex,
+ Integer celIndex, DataValidationConstraint constraint) {
+ // 设置数据有效性加载在哪个单元格上,四个参数分别是:起始行、终止行、起始列、终止列
+ CellRangeAddressList addressList = new CellRangeAddressList(rowIndex, rowIndex, celIndex, celIndex);
+ markDataValidationToSheet(helper, sheet, constraint, addressList);
+ }
+
+ /**
+ * 应用数据校验
+ */
+ private void markDataValidationToSheet(DataValidationHelper helper, Sheet sheet,
+ DataValidationConstraint constraint, CellRangeAddressList addressList) {
+ // 数据有效性对象
+ DataValidation dataValidation = helper.createValidation(constraint, addressList);
+ // 处理Excel兼容性问题
+ if (dataValidation instanceof XSSFDataValidation) {
+ //数据校验
+ dataValidation.setSuppressDropDownArrow(true);
+ //错误提示
+ dataValidation.setErrorStyle(DataValidation.ErrorStyle.STOP);
+ dataValidation.createErrorBox("提示", "此值与单元格定义数据不一致");
+ dataValidation.setShowErrorBox(true);
+ //选定提示
+ dataValidation.createPromptBox("填写说明:", "填写内容只能为下拉中数据,其他数据将导致导入失败");
+ dataValidation.setShowPromptBox(true);
+ sheet.addValidationData(dataValidation);
+ } else {
+ dataValidation.setSuppressDropDownArrow(false);
+ }
+ sheet.addValidationData(dataValidation);
+ }
+
+ /**
+ * 依据列index获取列名英文
+ * 依据列index转换为Excel中的列名英文
+ * 例如第1列,index为0,解析出来为A列
+ * 第27列,index为26,解析为AA列
+ * 第28列,index为27,解析为AB列
+ *
+ * @param columnIndex 列index
+ * @return 列index所在得英文名
+ */
+ private String getExcelColumnName(int columnIndex) {
+ // 26一循环的次数
+ int columnCircleCount = columnIndex / 26;
+ // 26一循环内的位置
+ int thisCircleColumnIndex = columnIndex % 26;
+ // 26一循环的次数大于0,则视为栏名至少两位
+ String columnPrefix = columnCircleCount == 0
+ ? StrUtil.EMPTY
+ : StrUtil.subWithLength(EXCEL_COLUMN_NAME, columnCircleCount - 1, 1);
+ // 从26一循环内取对应的栏位名
+ String columnNext = StrUtil.subWithLength(EXCEL_COLUMN_NAME, thisCircleColumnIndex, 1);
+ // 将二者拼接即为最终的栏位名
+ return columnPrefix + columnNext;
+ }
+}
diff --git a/ruoyi-common/src/main/java/com/ruoyi/common/helper/LoginHelper.java b/ruoyi-common/src/main/java/com/ruoyi/common/helper/LoginHelper.java
index 3cbddb137..b2a863eb9 100644
--- a/ruoyi-common/src/main/java/com/ruoyi/common/helper/LoginHelper.java
+++ b/ruoyi-common/src/main/java/com/ruoyi/common/helper/LoginHelper.java
@@ -2,6 +2,7 @@ package com.ruoyi.common.helper;
import cn.dev33.satoken.context.SaHolder;
import cn.dev33.satoken.context.model.SaStorage;
+import cn.dev33.satoken.session.SaSession;
import cn.dev33.satoken.stp.SaLoginModel;
import cn.dev33.satoken.stp.StpUtil;
import cn.hutool.core.convert.Convert;
@@ -54,6 +55,14 @@ public class LoginHelper {
if (ObjectUtil.isNotNull(deviceType)) {
model.setDevice(deviceType.getDevice());
}
+ // 自定义分配 不同用户体系 不同 token 授权时间 不设置默认走全局 yml 配置
+ // 例如: 后台用户30分钟过期 app用户1天过期
+// UserType userType = UserType.getUserType(loginUser.getUserType());
+// if (userType == UserType.SYS_USER) {
+// model.setTimeout(86400).setActiveTimeout(1800);
+// } else if (userType == UserType.APP_USER) {
+// model.setTimeout(86400).setActiveTimeout(1800);
+// }
StpUtil.login(loginUser.getLoginId(), model.setExtra(USER_KEY, loginUser.getUserId()));
StpUtil.getTokenSession().set(LOGIN_USER_KEY, loginUser);
}
@@ -66,7 +75,11 @@ public class LoginHelper {
if (loginUser != null) {
return loginUser;
}
- loginUser = (LoginUser) StpUtil.getTokenSession().get(LOGIN_USER_KEY);
+ SaSession session = StpUtil.getTokenSession();
+ if (ObjectUtil.isNull(session)) {
+ return null;
+ }
+ loginUser = (LoginUser) session.get(LOGIN_USER_KEY);
SaHolder.getStorage().set(LOGIN_USER_KEY, loginUser);
return loginUser;
}
@@ -75,7 +88,11 @@ public class LoginHelper {
* 获取用户基于token
*/
public static LoginUser getLoginUser(String token) {
- return (LoginUser) StpUtil.getTokenSessionByToken(token).get(LOGIN_USER_KEY);
+ SaSession session = StpUtil.getTokenSessionByToken(token);
+ if (ObjectUtil.isNull(session)) {
+ return null;
+ }
+ return (LoginUser) session.get(LOGIN_USER_KEY);
}
/**
@@ -113,8 +130,8 @@ public class LoginHelper {
* 获取用户类型
*/
public static UserType getUserType() {
- String loginId = StpUtil.getLoginIdAsString();
- return UserType.getUserType(loginId);
+ String loginType = StpUtil.getLoginIdAsString();
+ return UserType.getUserType(loginType);
}
/**
diff --git a/ruoyi-common/src/main/java/com/ruoyi/common/utils/BeanCopyUtils.java b/ruoyi-common/src/main/java/com/ruoyi/common/utils/BeanCopyUtils.java
index e1bafee22..d5b341419 100644
--- a/ruoyi-common/src/main/java/com/ruoyi/common/utils/BeanCopyUtils.java
+++ b/ruoyi-common/src/main/java/com/ruoyi/common/utils/BeanCopyUtils.java
@@ -17,11 +17,10 @@ import java.util.List;
import java.util.Map;
/**
- * bean深拷贝工具(基于 cglib 性能优异)
+ * bean拷贝工具(基于 cglib 性能优异)
*
* 重点 cglib 不支持 拷贝到链式对象
* 例如: 源对象 拷贝到 目标(链式对象)
- * 请区分好`浅拷贝`和`深拷贝`再做使用
*
* @author Lion Li
*/
diff --git a/ruoyi-common/src/main/java/com/ruoyi/common/utils/StreamUtils.java b/ruoyi-common/src/main/java/com/ruoyi/common/utils/StreamUtils.java
index a03e5bee4..0f57122eb 100644
--- a/ruoyi-common/src/main/java/com/ruoyi/common/utils/StreamUtils.java
+++ b/ruoyi-common/src/main/java/com/ruoyi/common/utils/StreamUtils.java
@@ -30,6 +30,7 @@ public class StreamUtils {
if (CollUtil.isEmpty(collection)) {
return CollUtil.newArrayList();
}
+ // 注意此处不要使用 .toList() 新语法 因为返回的是不可变List 会导致序列化问题
return collection.stream().filter(function).collect(Collectors.toList());
}
@@ -70,7 +71,8 @@ public class StreamUtils {
if (CollUtil.isEmpty(collection)) {
return CollUtil.newArrayList();
}
- return collection.stream().sorted(comparing).collect(Collectors.toList());
+ // 注意此处不要使用 .toList() 新语法 因为返回的是不可变List 会导致序列化问题
+ return collection.stream().filter(Objects::nonNull).sorted(comparing).collect(Collectors.toList());
}
/**
@@ -87,7 +89,7 @@ public class StreamUtils {
if (CollUtil.isEmpty(collection)) {
return MapUtil.newHashMap();
}
- return collection.stream().collect(Collectors.toMap(key, Function.identity(), (l, r) -> l));
+ return collection.stream().filter(Objects::nonNull).collect(Collectors.toMap(key, Function.identity(), (l, r) -> l));
}
/**
@@ -106,7 +108,7 @@ public class StreamUtils {
if (CollUtil.isEmpty(collection)) {
return MapUtil.newHashMap();
}
- return collection.stream().collect(Collectors.toMap(key, value, (l, r) -> l));
+ return collection.stream().filter(Objects::nonNull).collect(Collectors.toMap(key, value, (l, r) -> l));
}
/**
@@ -124,7 +126,7 @@ public class StreamUtils {
return MapUtil.newHashMap();
}
return collection
- .stream()
+ .stream().filter(Objects::nonNull)
.collect(Collectors.groupingBy(key, LinkedHashMap::new, Collectors.toList()));
}
@@ -145,7 +147,7 @@ public class StreamUtils {
return MapUtil.newHashMap();
}
return collection
- .stream()
+ .stream().filter(Objects::nonNull)
.collect(Collectors.groupingBy(key1, LinkedHashMap::new, Collectors.groupingBy(key2, LinkedHashMap::new, Collectors.toList())));
}
@@ -166,7 +168,7 @@ public class StreamUtils {
return MapUtil.newHashMap();
}
return collection
- .stream()
+ .stream().filter(Objects::nonNull)
.collect(Collectors.groupingBy(key1, LinkedHashMap::new, Collectors.toMap(key2, Function.identity(), (l, r) -> l)));
}
@@ -188,6 +190,7 @@ public class StreamUtils {
.stream()
.map(function)
.filter(Objects::nonNull)
+ // 注意此处不要使用 .toList() 新语法 因为返回的是不可变List 会导致序列化问题
.collect(Collectors.toList());
}
diff --git a/ruoyi-common/src/main/java/com/ruoyi/common/utils/poi/ExcelUtil.java b/ruoyi-common/src/main/java/com/ruoyi/common/utils/poi/ExcelUtil.java
index cf2ba885c..8459e1478 100644
--- a/ruoyi-common/src/main/java/com/ruoyi/common/utils/poi/ExcelUtil.java
+++ b/ruoyi-common/src/main/java/com/ruoyi/common/utils/poi/ExcelUtil.java
@@ -11,10 +11,7 @@ import com.alibaba.excel.write.metadata.fill.FillConfig;
import com.alibaba.excel.write.metadata.fill.FillWrapper;
import com.alibaba.excel.write.style.column.LongestMatchColumnWidthStyleStrategy;
import com.ruoyi.common.convert.ExcelBigNumberConvert;
-import com.ruoyi.common.excel.CellMergeStrategy;
-import com.ruoyi.common.excel.DefaultExcelListener;
-import com.ruoyi.common.excel.ExcelListener;
-import com.ruoyi.common.excel.ExcelResult;
+import com.ruoyi.common.excel.*;
import com.ruoyi.common.utils.StringUtils;
import com.ruoyi.common.utils.file.FileUtils;
import lombok.AccessLevel;
@@ -88,7 +85,26 @@ public class ExcelUtil {
try {
resetResponse(sheetName, response);
ServletOutputStream os = response.getOutputStream();
- exportExcel(list, sheetName, clazz, false, os);
+ exportExcel(list, sheetName, clazz, false, os, null);
+ } catch (IOException e) {
+ throw new RuntimeException("导出Excel异常");
+ }
+ }
+
+ /**
+ * 导出excel
+ *
+ * @param list 导出数据集合
+ * @param sheetName 工作表的名称
+ * @param clazz 实体类
+ * @param response 响应体
+ * @param options 级联下拉选
+ */
+ public static void exportExcel(List list, String sheetName, Class clazz, HttpServletResponse response, List options) {
+ try {
+ resetResponse(sheetName, response);
+ ServletOutputStream os = response.getOutputStream();
+ exportExcel(list, sheetName, clazz, false, os, options);
} catch (IOException e) {
throw new RuntimeException("导出Excel异常");
}
@@ -107,7 +123,27 @@ public class ExcelUtil {
try {
resetResponse(sheetName, response);
ServletOutputStream os = response.getOutputStream();
- exportExcel(list, sheetName, clazz, merge, os);
+ exportExcel(list, sheetName, clazz, merge, os, null);
+ } catch (IOException e) {
+ throw new RuntimeException("导出Excel异常");
+ }
+ }
+
+ /**
+ * 导出excel
+ *
+ * @param list 导出数据集合
+ * @param sheetName 工作表的名称
+ * @param clazz 实体类
+ * @param merge 是否合并单元格
+ * @param response 响应体
+ * @param options 级联下拉选
+ */
+ public static void exportExcel(List list, String sheetName, Class clazz, boolean merge, HttpServletResponse response, List options) {
+ try {
+ resetResponse(sheetName, response);
+ ServletOutputStream os = response.getOutputStream();
+ exportExcel(list, sheetName, clazz, merge, os, options);
} catch (IOException e) {
throw new RuntimeException("导出Excel异常");
}
@@ -122,7 +158,20 @@ public class ExcelUtil {
* @param os 输出流
*/
public static void exportExcel(List list, String sheetName, Class clazz, OutputStream os) {
- exportExcel(list, sheetName, clazz, false, os);
+ exportExcel(list, sheetName, clazz, false, os, null);
+ }
+
+ /**
+ * 导出excel
+ *
+ * @param list 导出数据集合
+ * @param sheetName 工作表的名称
+ * @param clazz 实体类
+ * @param os 输出流
+ * @param options 级联下拉选内容
+ */
+ public static void exportExcel(List list, String sheetName, Class clazz, OutputStream os, List options) {
+ exportExcel(list, sheetName, clazz, false, os, options);
}
/**
@@ -134,7 +183,8 @@ public class ExcelUtil {
* @param merge 是否合并单元格
* @param os 输出流
*/
- public static void exportExcel(List list, String sheetName, Class clazz, boolean merge, OutputStream os) {
+ public static void exportExcel(List list, String sheetName, Class clazz, boolean merge,
+ OutputStream os, List options) {
ExcelWriterSheetBuilder builder = EasyExcel.write(os, clazz)
.autoCloseStream(false)
// 自动适配
@@ -146,6 +196,10 @@ public class ExcelUtil {
// 合并处理器
builder.registerWriteHandler(new CellMergeStrategy(list, true));
}
+ if (CollUtil.isNotEmpty(options)) {
+ // 添加下拉框操作
+ builder.registerWriteHandler(new ExcelDownHandler(options));
+ }
builder.doWrite(list);
}
diff --git a/ruoyi-common/src/main/java/com/ruoyi/common/utils/redis/RedisUtils.java b/ruoyi-common/src/main/java/com/ruoyi/common/utils/redis/RedisUtils.java
index 162380890..7a852bf90 100644
--- a/ruoyi-common/src/main/java/com/ruoyi/common/utils/redis/RedisUtils.java
+++ b/ruoyi-common/src/main/java/com/ruoyi/common/utils/redis/RedisUtils.java
@@ -129,6 +129,18 @@ public class RedisUtils {
batch.execute();
}
+ /**
+ * 如果不存在则设置 并返回 true 如果存在则返回 false
+ *
+ * @param key 缓存的键值
+ * @param value 缓存的值
+ * @return set成功或失败
+ */
+ public static boolean setObjectIfAbsent(final String key, final T value, final Duration duration) {
+ RBucket bucket = CLIENT.getBucket(key);
+ return bucket.setIfAbsent(value, duration);
+ }
+
/**
* 注册对象监听器
*
@@ -374,6 +386,21 @@ public class RedisUtils {
return rMap.remove(hKey);
}
+ /**
+ * 删除Hash中的数据
+ *
+ * @param key Redis键
+ * @param hKeys Hash键
+ */
+ public static void delMultiCacheMapValue(final String key, final Set hKeys) {
+ RBatch batch = CLIENT.createBatch();
+ RMapAsync rMap = batch.getMap(key);
+ for (String hKey : hKeys) {
+ rMap.removeAsync(hKey);
+ }
+ batch.execute();
+ }
+
/**
* 获取多个Hash中的数据
*
diff --git a/ruoyi-demo/pom.xml b/ruoyi-demo/pom.xml
index 61425b67b..a1d235eaa 100644
--- a/ruoyi-demo/pom.xml
+++ b/ruoyi-demo/pom.xml
@@ -5,7 +5,7 @@
ruoyi-vue-plus
com.ruoyi
- 4.7.0
+ 4.8.0
4.0.0
@@ -28,17 +28,6 @@
ruoyi-sms
-
-
-
-
-
-
-
-
-
-
-
diff --git a/ruoyi-demo/src/main/java/com/ruoyi/demo/controller/SmsController.java b/ruoyi-demo/src/main/java/com/ruoyi/demo/controller/SmsController.java
index cc6012f44..8965a2d88 100644
--- a/ruoyi-demo/src/main/java/com/ruoyi/demo/controller/SmsController.java
+++ b/ruoyi-demo/src/main/java/com/ruoyi/demo/controller/SmsController.java
@@ -1,17 +1,17 @@
package com.ruoyi.demo.controller;
import com.ruoyi.common.core.domain.R;
-import com.ruoyi.common.utils.spring.SpringUtils;
-import com.ruoyi.sms.config.properties.SmsProperties;
-import com.ruoyi.sms.core.SmsTemplate;
import lombok.RequiredArgsConstructor;
+import org.dromara.sms4j.api.SmsBlend;
+import org.dromara.sms4j.api.entity.SmsResponse;
+import org.dromara.sms4j.core.factory.SmsFactory;
+import org.dromara.sms4j.provider.enumerate.SupplierType;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
-import java.util.HashMap;
-import java.util.Map;
+import java.util.LinkedHashMap;
/**
* 短信演示案例
@@ -26,10 +26,6 @@ import java.util.Map;
@RequestMapping("/demo/sms")
public class SmsController {
- private final SmsProperties smsProperties;
-// private final SmsTemplate smsTemplate; // 可以使用spring注入
-// private final AliyunSmsTemplate smsTemplate; // 也可以注入某个厂家的模板工具
-
/**
* 发送短信Aliyun
*
@@ -38,17 +34,11 @@ public class SmsController {
*/
@GetMapping("/sendAliyun")
public R sendAliyun(String phones, String templateId) {
- if (!smsProperties.getEnabled()) {
- return R.fail("当前系统没有开启短信功能!");
- }
- if (!SpringUtils.containsBean("aliyunSmsTemplate")) {
- return R.fail("阿里云依赖未引入!");
- }
- SmsTemplate smsTemplate = SpringUtils.getBean(SmsTemplate.class);
- Map map = new HashMap<>(1);
+ LinkedHashMap map = new LinkedHashMap<>(1);
map.put("code", "1234");
- Object send = smsTemplate.send(phones, templateId, map);
- return R.ok(send);
+ SmsBlend smsBlend = SmsFactory.createSmsBlend(SupplierType.ALIBABA);
+ SmsResponse smsResponse = smsBlend.sendMessage(phones, templateId, map);
+ return R.ok(smsResponse);
}
/**
@@ -59,18 +49,12 @@ public class SmsController {
*/
@GetMapping("/sendTencent")
public R sendTencent(String phones, String templateId) {
- if (!smsProperties.getEnabled()) {
- return R.fail("当前系统没有开启短信功能!");
- }
- if (!SpringUtils.containsBean("tencentSmsTemplate")) {
- return R.fail("腾讯云依赖未引入!");
- }
- SmsTemplate smsTemplate = SpringUtils.getBean(SmsTemplate.class);
- Map map = new HashMap<>(1);
+ LinkedHashMap map = new LinkedHashMap<>(1);
// map.put("2", "测试测试");
map.put("1", "1234");
- Object send = smsTemplate.send(phones, templateId, map);
- return R.ok(send);
+ SmsBlend smsBlend = SmsFactory.createSmsBlend(SupplierType.TENCENT);
+ SmsResponse smsResponse = smsBlend.sendMessage(phones, templateId, map);
+ return R.ok(smsResponse);
}
}
diff --git a/ruoyi-demo/src/main/java/com/ruoyi/demo/controller/TestExcelController.java b/ruoyi-demo/src/main/java/com/ruoyi/demo/controller/TestExcelController.java
index 51b81c582..e1ecb2074 100644
--- a/ruoyi-demo/src/main/java/com/ruoyi/demo/controller/TestExcelController.java
+++ b/ruoyi-demo/src/main/java/com/ruoyi/demo/controller/TestExcelController.java
@@ -1,12 +1,17 @@
package com.ruoyi.demo.controller;
import cn.hutool.core.collection.CollUtil;
+import com.ruoyi.common.excel.ExcelResult;
import com.ruoyi.common.utils.poi.ExcelUtil;
+import com.ruoyi.demo.domain.vo.ExportDemoVo;
+import com.ruoyi.demo.listener.ExportDemoListener;
+import com.ruoyi.demo.service.IExportExcelService;
import lombok.AllArgsConstructor;
import lombok.Data;
-import org.springframework.web.bind.annotation.GetMapping;
-import org.springframework.web.bind.annotation.RequestMapping;
-import org.springframework.web.bind.annotation.RestController;
+import lombok.RequiredArgsConstructor;
+import org.springframework.http.MediaType;
+import org.springframework.web.bind.annotation.*;
+import org.springframework.web.multipart.MultipartFile;
import javax.servlet.http.HttpServletResponse;
import java.util.ArrayList;
@@ -20,9 +25,12 @@ import java.util.Map;
* @author Lion Li
*/
@RestController
+@RequiredArgsConstructor
@RequestMapping("/demo/excel")
public class TestExcelController {
+ private final IExportExcelService exportExcelService;
+
/**
* 单列表多数据
*/
@@ -76,6 +84,26 @@ public class TestExcelController {
ExcelUtil.exportTemplateMultiList(multiListMap, "多列表.xlsx", "excel/多列表.xlsx", response);
}
+ /**
+ * 导出下拉框
+ *
+ * @param response /
+ */
+ @GetMapping("/exportWithOptions")
+ public void exportWithOptions(HttpServletResponse response) {
+ exportExcelService.exportWithOptions(response);
+ }
+
+ /**
+ * 导入表格
+ */
+ @PostMapping(value = "/importWithOptions", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
+ public List importWithOptions(@RequestPart("file") MultipartFile file) throws Exception {
+ // 处理解析结果
+ ExcelResult excelResult = ExcelUtil.importExcel(file.getInputStream(), ExportDemoVo.class, new ExportDemoListener());
+ return excelResult.getList();
+ }
+
@Data
@AllArgsConstructor
static class TestObj1 {
diff --git a/ruoyi-demo/src/main/java/com/ruoyi/demo/domain/vo/ExportDemoVo.java b/ruoyi-demo/src/main/java/com/ruoyi/demo/domain/vo/ExportDemoVo.java
new file mode 100644
index 000000000..e417dbc4c
--- /dev/null
+++ b/ruoyi-demo/src/main/java/com/ruoyi/demo/domain/vo/ExportDemoVo.java
@@ -0,0 +1,119 @@
+package com.ruoyi.demo.domain.vo;
+
+import com.alibaba.excel.annotation.ExcelIgnoreUnannotated;
+import com.alibaba.excel.annotation.ExcelProperty;
+import com.ruoyi.common.annotation.ExcelDictFormat;
+import com.ruoyi.common.annotation.ExcelEnumFormat;
+import com.ruoyi.common.convert.ExcelDictConvert;
+import com.ruoyi.common.convert.ExcelEnumConvert;
+import com.ruoyi.common.core.validate.AddGroup;
+import com.ruoyi.common.core.validate.EditGroup;
+import com.ruoyi.common.enums.UserStatus;
+import lombok.AllArgsConstructor;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+import javax.validation.constraints.NotEmpty;
+import javax.validation.constraints.NotNull;
+
+/**
+ * 带有下拉选的Excel导出
+ *
+ * @author Emil.Zhang
+ */
+@Data
+@ExcelIgnoreUnannotated
+@AllArgsConstructor
+@NoArgsConstructor
+public class ExportDemoVo {
+
+ private static final long serialVersionUID = 1L;
+
+ /**
+ * 用户昵称
+ */
+ @ExcelProperty(value = "用户名", index = 0)
+ @NotEmpty(message = "用户名不能为空", groups = AddGroup.class)
+ private String nickName;
+
+ /**
+ * 用户类型
+ *
+ * 使用ExcelEnumFormat注解需要进行下拉选的部分
+ */
+ @ExcelProperty(value = "用户类型", index = 1, converter = ExcelEnumConvert.class)
+ @ExcelEnumFormat(enumClass = UserStatus.class, textField = "info")
+ @NotEmpty(message = "用户类型不能为空", groups = AddGroup.class)
+ private String userStatus;
+
+ /**
+ * 性别
+ *
+ * 使用ExcelDictFormat注解需要进行下拉选的部分
+ */
+ @ExcelProperty(value = "性别", index = 2, converter = ExcelDictConvert.class)
+ @ExcelDictFormat(dictType = "sys_user_sex")
+ @NotEmpty(message = "性别不能为空", groups = AddGroup.class)
+ private String gender;
+
+ /**
+ * 手机号
+ */
+ @ExcelProperty(value = "手机号", index = 3)
+ @NotEmpty(message = "手机号不能为空", groups = AddGroup.class)
+ private String phoneNumber;
+
+ /**
+ * Email
+ */
+ @ExcelProperty(value = "Email", index = 4)
+ @NotEmpty(message = "Email不能为空", groups = AddGroup.class)
+ private String email;
+
+ /**
+ * 省
+ *
+ * 级联下拉,仅判断是否选了
+ */
+ @ExcelProperty(value = "省", index = 5)
+ @NotNull(message = "省不能为空", groups = AddGroup.class)
+ private String province;
+
+ /**
+ * 数据库中的省ID
+ *
+ * 处理完毕后再判断是否市正确的值
+ */
+ @NotNull(message = "请勿手动输入", groups = EditGroup.class)
+ private Integer provinceId;
+
+ /**
+ * 市
+ *
+ * 级联下拉
+ */
+ @ExcelProperty(value = "市", index = 6)
+ @NotNull(message = "市不能为空", groups = AddGroup.class)
+ private String city;
+
+ /**
+ * 数据库中的市ID
+ */
+ @NotNull(message = "请勿手动输入", groups = EditGroup.class)
+ private Integer cityId;
+
+ /**
+ * 县
+ *
+ * 级联下拉
+ */
+ @ExcelProperty(value = "县", index = 7)
+ @NotNull(message = "县不能为空", groups = AddGroup.class)
+ private String area;
+
+ /**
+ * 数据库中的县ID
+ */
+ @NotNull(message = "请勿手动输入", groups = EditGroup.class)
+ private Integer areaId;
+}
diff --git a/ruoyi-demo/src/main/java/com/ruoyi/demo/listener/ExportDemoListener.java b/ruoyi-demo/src/main/java/com/ruoyi/demo/listener/ExportDemoListener.java
new file mode 100644
index 000000000..95d7093f2
--- /dev/null
+++ b/ruoyi-demo/src/main/java/com/ruoyi/demo/listener/ExportDemoListener.java
@@ -0,0 +1,68 @@
+package com.ruoyi.demo.listener;
+
+import cn.hutool.core.util.NumberUtil;
+import com.alibaba.excel.context.AnalysisContext;
+import com.ruoyi.common.core.validate.AddGroup;
+import com.ruoyi.common.core.validate.EditGroup;
+import com.ruoyi.common.excel.DefaultExcelListener;
+import com.ruoyi.common.excel.DropDownOptions;
+import com.ruoyi.common.utils.ValidatorUtils;
+import com.ruoyi.demo.domain.vo.ExportDemoVo;
+
+import java.util.List;
+
+/**
+ * Excel带下拉框的解析处理器
+ *
+ * @author Emil.Zhang
+ */
+public class ExportDemoListener extends DefaultExcelListener {
+
+ public ExportDemoListener() {
+ // 显示使用构造函数,否则将导致空指针
+ super(true);
+ }
+
+ @Override
+ public void invoke(ExportDemoVo data, AnalysisContext context) {
+ // 先校验必填
+ ValidatorUtils.validate(data, AddGroup.class);
+
+ // 处理级联下拉的部分
+ String province = data.getProvince();
+ String city = data.getCity();
+ String area = data.getArea();
+ // 本行用户选择的省
+ List thisRowSelectedProvinceOption = DropDownOptions.analyzeOptionValue(province);
+ if (thisRowSelectedProvinceOption.size() == 2) {
+ String provinceIdStr = thisRowSelectedProvinceOption.get(1);
+ if (NumberUtil.isNumber(provinceIdStr)) {
+ // 严格要求数据的话可以在这里做与数据库相关的判断
+ // 例如判断省信息是否在数据库中存在等,建议结合RedisCache做缓存10s,减少数据库调用
+ data.setProvinceId(Integer.parseInt(provinceIdStr));
+ }
+ }
+ // 本行用户选择的市
+ List thisRowSelectedCityOption = DropDownOptions.analyzeOptionValue(city);
+ if (thisRowSelectedCityOption.size() == 2) {
+ String cityIdStr = thisRowSelectedCityOption.get(1);
+ if (NumberUtil.isNumber(cityIdStr)) {
+ data.setCityId(Integer.parseInt(cityIdStr));
+ }
+ }
+ // 本行用户选择的县
+ List thisRowSelectedAreaOption = DropDownOptions.analyzeOptionValue(area);
+ if (thisRowSelectedAreaOption.size() == 2) {
+ String areaIdStr = thisRowSelectedAreaOption.get(1);
+ if (NumberUtil.isNumber(areaIdStr)) {
+ data.setAreaId(Integer.parseInt(areaIdStr));
+ }
+ }
+
+ // 处理完毕以后判断是否符合规则
+ ValidatorUtils.validate(data, EditGroup.class);
+
+ // 添加到处理结果中
+ getExcelResult().getList().add(data);
+ }
+}
diff --git a/ruoyi-demo/src/main/java/com/ruoyi/demo/service/IExportExcelService.java b/ruoyi-demo/src/main/java/com/ruoyi/demo/service/IExportExcelService.java
new file mode 100644
index 000000000..d8f024784
--- /dev/null
+++ b/ruoyi-demo/src/main/java/com/ruoyi/demo/service/IExportExcelService.java
@@ -0,0 +1,18 @@
+package com.ruoyi.demo.service;
+
+import javax.servlet.http.HttpServletResponse;
+
+/**
+ * 导出下拉框Excel示例
+ *
+ * @author Emil.Zhang
+ */
+public interface IExportExcelService {
+
+ /**
+ * 导出下拉框
+ *
+ * @param response /
+ */
+ void exportWithOptions(HttpServletResponse response);
+}
diff --git a/ruoyi-demo/src/main/java/com/ruoyi/demo/service/impl/ExportExcelServiceImpl.java b/ruoyi-demo/src/main/java/com/ruoyi/demo/service/impl/ExportExcelServiceImpl.java
new file mode 100644
index 000000000..37221c18d
--- /dev/null
+++ b/ruoyi-demo/src/main/java/com/ruoyi/demo/service/impl/ExportExcelServiceImpl.java
@@ -0,0 +1,223 @@
+package com.ruoyi.demo.service.impl;
+
+import cn.hutool.core.util.StrUtil;
+import com.ruoyi.common.enums.UserStatus;
+import com.ruoyi.common.excel.DropDownOptions;
+import com.ruoyi.common.utils.StreamUtils;
+import com.ruoyi.common.utils.poi.ExcelUtil;
+import com.ruoyi.demo.domain.vo.ExportDemoVo;
+import com.ruoyi.demo.service.IExportExcelService;
+import lombok.Data;
+import lombok.RequiredArgsConstructor;
+import org.springframework.stereotype.Service;
+
+import javax.servlet.http.HttpServletResponse;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.stream.Collectors;
+
+/**
+ * 导出下拉框Excel示例
+ *
+ * @author Emil.Zhang
+ */
+@Service
+@RequiredArgsConstructor
+public class ExportExcelServiceImpl implements IExportExcelService {
+
+ @Override
+ public void exportWithOptions(HttpServletResponse response) {
+ // 创建表格数据,业务中一般通过数据库查询
+ List excelDataList = new ArrayList<>();
+ for (int i = 0; i < 3; i++) {
+ // 模拟数据库中的一条数据
+ ExportDemoVo everyRowData = new ExportDemoVo();
+ everyRowData.setNickName("用户-" + i);
+ everyRowData.setUserStatus(UserStatus.OK.getCode());
+ everyRowData.setGender("1");
+ everyRowData.setPhoneNumber(String.format("175%08d", i));
+ everyRowData.setEmail(String.format("175%08d", i) + "@163.com");
+ everyRowData.setProvinceId(i);
+ everyRowData.setCityId(i);
+ everyRowData.setAreaId(i);
+ excelDataList.add(everyRowData);
+ }
+
+ // 通过@ExcelIgnoreUnannotated配合@ExcelProperty合理显示需要的列
+ // 并通过@DropDown注解指定下拉值,或者通过创建ExcelOptions来指定下拉框
+ // 使用ExcelOptions时建议指定列index,防止出现下拉列解析不对齐
+
+ // 首先从数据库中查询下拉框内的可选项
+ // 这里模拟查询结果
+ List provinceList = getProvinceList(),
+ cityList = getCityList(provinceList),
+ areaList = getAreaList(cityList);
+ int provinceIndex = 5, cityIndex = 6, areaIndex = 7;
+
+ DropDownOptions provinceToCity = DropDownOptions.buildLinkedOptions(
+ provinceList,
+ provinceIndex,
+ cityList,
+ cityIndex,
+ DemoCityData::getId,
+ DemoCityData::getPid,
+ everyOptions -> DropDownOptions.createOptionValue(
+ everyOptions.getName(),
+ everyOptions.getId()
+ )
+ );
+
+ DropDownOptions cityToArea = DropDownOptions.buildLinkedOptions(
+ cityList,
+ cityIndex,
+ areaList,
+ areaIndex,
+ DemoCityData::getId,
+ DemoCityData::getPid,
+ everyOptions -> DropDownOptions.createOptionValue(
+ everyOptions.getName(),
+ everyOptions.getId()
+ )
+ );
+
+ // 把所有的下拉框存储
+ List options = new ArrayList<>();
+ options.add(provinceToCity);
+ options.add(cityToArea);
+
+ // 到此为止所有的下拉框可选项已全部配置完毕
+
+ // 接下来需要将Excel中的展示数据转换为对应的下拉选
+ List outList = StreamUtils.toList(excelDataList, everyRowData -> {
+ // 只需要处理没有使用@ExcelDictFormat注解的下拉框
+ // 一般来说,可以直接在数据库查询即查询出省市县信息,这里通过模拟操作赋值
+ everyRowData.setProvince(buildOptions(provinceList, everyRowData.getProvinceId()));
+ everyRowData.setCity(buildOptions(cityList, everyRowData.getCityId()));
+ everyRowData.setArea(buildOptions(areaList, everyRowData.getAreaId()));
+ return everyRowData;
+ });
+
+ ExcelUtil.exportExcel(outList, "下拉框示例", ExportDemoVo.class, response, options);
+ }
+
+ private String buildOptions(List cityDataList, Integer id) {
+ Map> groupByIdMap =
+ cityDataList.stream().collect(Collectors.groupingBy(DemoCityData::getId));
+ if (groupByIdMap.containsKey(id)) {
+ DemoCityData demoCityData = groupByIdMap.get(id).get(0);
+ return DropDownOptions.createOptionValue(demoCityData.getName(), demoCityData.getId());
+ } else {
+ return StrUtil.EMPTY;
+ }
+ }
+
+ /**
+ * 模拟查询数据库操作
+ *
+ * @return /
+ */
+ private List getProvinceList() {
+ List provinceList = new ArrayList<>();
+
+ // 实际业务中一般采用数据库读取的形式,这里直接拼接创建
+ provinceList.add(new DemoCityData(0, null, "安徽省"));
+ provinceList.add(new DemoCityData(1, null, "江苏省"));
+
+ return provinceList;
+ }
+
+ /**
+ * 模拟查找数据库操作,需要连带查询出省的数据
+ *
+ * @param provinceList 模拟的父省数据
+ * @return /
+ */
+ private List getCityList(List provinceList) {
+ List cityList = new ArrayList<>();
+
+ // 实际业务中一般采用数据库读取的形式,这里直接拼接创建
+ cityList.add(new DemoCityData(0, 0, "合肥市"));
+ cityList.add(new DemoCityData(1, 0, "芜湖市"));
+ cityList.add(new DemoCityData(2, 1, "南京市"));
+ cityList.add(new DemoCityData(3, 1, "无锡市"));
+ cityList.add(new DemoCityData(4, 1, "徐州市"));
+
+ selectParentData(provinceList, cityList);
+
+ return cityList;
+ }
+
+ /**
+ * 模拟查找数据库操作,需要连带查询出市的数据
+ *
+ * @param cityList 模拟的父市数据
+ * @return /
+ */
+ private List getAreaList(List cityList) {
+ List areaList = new ArrayList<>();
+
+ // 实际业务中一般采用数据库读取的形式,这里直接拼接创建
+ areaList.add(new DemoCityData(0, 0, "瑶海区"));
+ areaList.add(new DemoCityData(1, 0, "庐江区"));
+ areaList.add(new DemoCityData(2, 1, "南宁县"));
+ areaList.add(new DemoCityData(3, 1, "镜湖区"));
+ areaList.add(new DemoCityData(4, 2, "玄武区"));
+ areaList.add(new DemoCityData(5, 2, "秦淮区"));
+ areaList.add(new DemoCityData(6, 3, "宜兴市"));
+ areaList.add(new DemoCityData(7, 3, "新吴区"));
+ areaList.add(new DemoCityData(8, 4, "鼓楼区"));
+ areaList.add(new DemoCityData(9, 4, "丰县"));
+
+ selectParentData(cityList, areaList);
+
+ return areaList;
+ }
+
+ /**
+ * 模拟数据库的查询父数据操作
+ *
+ * @param parentList /
+ * @param sonList /
+ */
+ private void selectParentData(List parentList, List sonList) {
+ Map> parentGroupByIdMap =
+ parentList.stream().collect(Collectors.groupingBy(DemoCityData::getId));
+
+ sonList.forEach(everySon -> {
+ if (parentGroupByIdMap.containsKey(everySon.getPid())) {
+ everySon.setPData(parentGroupByIdMap.get(everySon.getPid()).get(0));
+ }
+ });
+ }
+
+ /**
+ * 模拟的数据库省市县
+ */
+ @Data
+ private static class DemoCityData {
+ /**
+ * 数据库id字段
+ */
+ private Integer id;
+ /**
+ * 数据库pid字段
+ */
+ private Integer pid;
+ /**
+ * 数据库name字段
+ */
+ private String name;
+ /**
+ * MyBatisPlus连带查询父数据
+ */
+ private DemoCityData pData;
+
+ public DemoCityData(Integer id, Integer pid, String name) {
+ this.id = id;
+ this.pid = pid;
+ this.name = name;
+ }
+ }
+}
diff --git a/ruoyi-extend/pom.xml b/ruoyi-extend/pom.xml
index 212836acd..3fc1c3722 100644
--- a/ruoyi-extend/pom.xml
+++ b/ruoyi-extend/pom.xml
@@ -5,7 +5,7 @@
ruoyi-vue-plus
com.ruoyi
- 4.7.0
+ 4.8.0
4.0.0
ruoyi-extend
diff --git a/ruoyi-extend/ruoyi-monitor-admin/pom.xml b/ruoyi-extend/ruoyi-monitor-admin/pom.xml
index 89cf455f2..7f614985b 100644
--- a/ruoyi-extend/ruoyi-monitor-admin/pom.xml
+++ b/ruoyi-extend/ruoyi-monitor-admin/pom.xml
@@ -5,7 +5,7 @@
ruoyi-extend
com.ruoyi
- 4.7.0
+ 4.8.0
4.0.0
jar
diff --git a/ruoyi-extend/ruoyi-xxl-job-admin/pom.xml b/ruoyi-extend/ruoyi-xxl-job-admin/pom.xml
index 56650a2fc..558c3b067 100644
--- a/ruoyi-extend/ruoyi-xxl-job-admin/pom.xml
+++ b/ruoyi-extend/ruoyi-xxl-job-admin/pom.xml
@@ -4,7 +4,7 @@
ruoyi-extend
com.ruoyi
- 4.7.0
+ 4.8.0
ruoyi-xxl-job-admin
jar
diff --git a/ruoyi-framework/pom.xml b/ruoyi-framework/pom.xml
index 43b8056fd..4d821f328 100644
--- a/ruoyi-framework/pom.xml
+++ b/ruoyi-framework/pom.xml
@@ -5,7 +5,7 @@
ruoyi-vue-plus
com.ruoyi
- 4.7.0
+ 4.8.0
4.0.0
diff --git a/ruoyi-framework/src/main/java/com/ruoyi/framework/aspectj/LogAspect.java b/ruoyi-framework/src/main/java/com/ruoyi/framework/aspectj/LogAspect.java
index 00918327b..1e0c05285 100644
--- a/ruoyi-framework/src/main/java/com/ruoyi/framework/aspectj/LogAspect.java
+++ b/ruoyi-framework/src/main/java/com/ruoyi/framework/aspectj/LogAspect.java
@@ -2,6 +2,7 @@ package com.ruoyi.framework.aspectj;
import cn.hutool.core.lang.Dict;
import cn.hutool.core.map.MapUtil;
+import cn.hutool.core.util.ArrayUtil;
import cn.hutool.core.util.ObjectUtil;
import com.ruoyi.common.annotation.Log;
import com.ruoyi.common.core.domain.event.OperLogEvent;
@@ -25,6 +26,7 @@ import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.util.Collection;
import java.util.Map;
+import java.util.StringJoiner;
/**
* 操作日志记录处理
@@ -144,26 +146,23 @@ public class LogAspect {
* 参数拼装
*/
private String argsArrayToString(Object[] paramsArray, String[] excludeParamNames) {
- StringBuilder params = new StringBuilder();
- if (paramsArray != null && paramsArray.length > 0) {
- for (Object o : paramsArray) {
- if (ObjectUtil.isNotNull(o) && !isFilterObject(o)) {
- try {
- String str = JsonUtils.toJsonString(o);
- Dict dict = JsonUtils.parseMap(str);
- if (MapUtil.isNotEmpty(dict)) {
- MapUtil.removeAny(dict, EXCLUDE_PROPERTIES);
- MapUtil.removeAny(dict, excludeParamNames);
- str = JsonUtils.toJsonString(dict);
- }
- params.append(str).append(" ");
- } catch (Exception e) {
- e.printStackTrace();
- }
+ StringJoiner params = new StringJoiner(" ");
+ if (ArrayUtil.isEmpty(paramsArray)) {
+ return params.toString();
+ }
+ for (Object o : paramsArray) {
+ if (ObjectUtil.isNotNull(o) && !isFilterObject(o)) {
+ String str = JsonUtils.toJsonString(o);
+ Dict dict = JsonUtils.parseMap(str);
+ if (MapUtil.isNotEmpty(dict)) {
+ MapUtil.removeAny(dict, EXCLUDE_PROPERTIES);
+ MapUtil.removeAny(dict, excludeParamNames);
+ str = JsonUtils.toJsonString(dict);
}
+ params.add(str);
}
}
- return params.toString().trim();
+ return params.toString();
}
/**
@@ -184,9 +183,8 @@ public class LogAspect {
}
} else if (Map.class.isAssignableFrom(clazz)) {
Map map = (Map) o;
- for (Object value : map.entrySet()) {
- Map.Entry entry = (Map.Entry) value;
- return entry.getValue() instanceof MultipartFile;
+ for (Object value : map.values()) {
+ return value instanceof MultipartFile;
}
}
return o instanceof MultipartFile || o instanceof HttpServletRequest || o instanceof HttpServletResponse
diff --git a/ruoyi-framework/src/main/java/com/ruoyi/framework/aspectj/RepeatSubmitAspect.java b/ruoyi-framework/src/main/java/com/ruoyi/framework/aspectj/RepeatSubmitAspect.java
index 7c2fa2a41..4c4249c30 100644
--- a/ruoyi-framework/src/main/java/com/ruoyi/framework/aspectj/RepeatSubmitAspect.java
+++ b/ruoyi-framework/src/main/java/com/ruoyi/framework/aspectj/RepeatSubmitAspect.java
@@ -1,6 +1,7 @@
package com.ruoyi.framework.aspectj;
import cn.dev33.satoken.SaManager;
+import cn.hutool.core.util.ArrayUtil;
import cn.hutool.core.util.ObjectUtil;
import cn.hutool.crypto.SecureUtil;
import com.ruoyi.common.annotation.RepeatSubmit;
@@ -12,8 +13,6 @@ import com.ruoyi.common.utils.MessageUtils;
import com.ruoyi.common.utils.ServletUtils;
import com.ruoyi.common.utils.StringUtils;
import com.ruoyi.common.utils.redis.RedisUtils;
-import lombok.RequiredArgsConstructor;
-import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.AfterReturning;
import org.aspectj.lang.annotation.AfterThrowing;
@@ -28,14 +27,13 @@ import javax.servlet.http.HttpServletResponse;
import java.time.Duration;
import java.util.Collection;
import java.util.Map;
+import java.util.StringJoiner;
/**
* 防止重复提交(参考美团GTIS防重系统)
*
* @author Lion Li
*/
-@Slf4j
-@RequiredArgsConstructor
@Aspect
@Component
public class RepeatSubmitAspect {
@@ -45,10 +43,8 @@ public class RepeatSubmitAspect {
@Before("@annotation(repeatSubmit)")
public void doBefore(JoinPoint point, RepeatSubmit repeatSubmit) throws Throwable {
// 如果注解不为0 则使用注解数值
- long interval = 0;
- if (repeatSubmit.interval() > 0) {
- interval = repeatSubmit.timeUnit().toMillis(repeatSubmit.interval());
- }
+ long interval = repeatSubmit.timeUnit().toMillis(repeatSubmit.interval());
+
if (interval < 1000) {
throw new ServiceException("重复提交间隔时间不能小于'1'秒");
}
@@ -64,9 +60,7 @@ public class RepeatSubmitAspect {
submitKey = SecureUtil.md5(submitKey + ":" + nowParams);
// 唯一标识(指定key + url + 消息头)
String cacheRepeatKey = CacheConstants.REPEAT_SUBMIT_KEY + url + submitKey;
- String key = RedisUtils.getCacheObject(cacheRepeatKey);
- if (key == null) {
- RedisUtils.setCacheObject(cacheRepeatKey, "", Duration.ofMillis(interval));
+ if (RedisUtils.setObjectIfAbsent(cacheRepeatKey, "", Duration.ofMillis(interval))) {
KEY_CACHE.set(cacheRepeatKey);
} else {
String message = repeatSubmit.message();
@@ -114,19 +108,16 @@ public class RepeatSubmitAspect {
* 参数拼装
*/
private String argsArrayToString(Object[] paramsArray) {
- StringBuilder params = new StringBuilder();
- if (paramsArray != null && paramsArray.length > 0) {
- for (Object o : paramsArray) {
- if (ObjectUtil.isNotNull(o) && !isFilterObject(o)) {
- try {
- params.append(JsonUtils.toJsonString(o)).append(" ");
- } catch (Exception e) {
- e.printStackTrace();
- }
- }
+ StringJoiner params = new StringJoiner(" ");
+ if (ArrayUtil.isEmpty(paramsArray)) {
+ return params.toString();
+ }
+ for (Object o : paramsArray) {
+ if (ObjectUtil.isNotNull(o) && !isFilterObject(o)) {
+ params.add(JsonUtils.toJsonString(o));
}
}
- return params.toString().trim();
+ return params.toString();
}
/**
@@ -147,9 +138,8 @@ public class RepeatSubmitAspect {
}
} else if (Map.class.isAssignableFrom(clazz)) {
Map map = (Map) o;
- for (Object value : map.entrySet()) {
- Map.Entry entry = (Map.Entry) value;
- return entry.getValue() instanceof MultipartFile;
+ for (Object value : map.values()) {
+ return value instanceof MultipartFile;
}
}
return o instanceof MultipartFile || o instanceof HttpServletRequest || o instanceof HttpServletResponse
diff --git a/ruoyi-framework/src/main/java/com/ruoyi/framework/manager/PlusSpringCacheManager.java b/ruoyi-framework/src/main/java/com/ruoyi/framework/manager/PlusSpringCacheManager.java
index d8bfce7c5..ddd0f33bb 100644
--- a/ruoyi-framework/src/main/java/com/ruoyi/framework/manager/PlusSpringCacheManager.java
+++ b/ruoyi-framework/src/main/java/com/ruoyi/framework/manager/PlusSpringCacheManager.java
@@ -118,6 +118,10 @@ public class PlusSpringCacheManager implements CacheManager {
@Override
public Cache getCache(String name) {
+ // 重写 cacheName 支持多参数
+ String[] array = StringUtils.delimitedListToStringArray(name, "#");
+ name = array[0];
+
Cache cache = instanceMap.get(name);
if (cache != null) {
return cache;
@@ -132,9 +136,6 @@ public class PlusSpringCacheManager implements CacheManager {
configMap.put(name, config);
}
- // 重写 cacheName 支持多参数
- String[] array = StringUtils.delimitedListToStringArray(name, "#");
- name = array[0];
if (array.length > 1) {
config.setTTL(DurationStyle.detectAndParse(array[1]).toMillis());
}
diff --git a/ruoyi-framework/src/main/java/com/ruoyi/framework/web/exception/GlobalExceptionHandler.java b/ruoyi-framework/src/main/java/com/ruoyi/framework/web/exception/GlobalExceptionHandler.java
index 40f2d27c3..f1cbff22a 100644
--- a/ruoyi-framework/src/main/java/com/ruoyi/framework/web/exception/GlobalExceptionHandler.java
+++ b/ruoyi-framework/src/main/java/com/ruoyi/framework/web/exception/GlobalExceptionHandler.java
@@ -16,8 +16,10 @@ import org.springframework.dao.DuplicateKeyException;
import org.springframework.validation.BindException;
import org.springframework.web.HttpRequestMethodNotSupportedException;
import org.springframework.web.bind.MethodArgumentNotValidException;
+import org.springframework.web.bind.MissingPathVariableException;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;
+import org.springframework.web.method.annotation.MethodArgumentTypeMismatchException;
import javax.servlet.http.HttpServletRequest;
import javax.validation.ConstraintViolation;
@@ -108,6 +110,26 @@ public class GlobalExceptionHandler {
return ObjectUtil.isNotNull(code) ? R.fail(code, e.getMessage()) : R.fail(e.getMessage());
}
+ /**
+ * 请求路径中缺少必需的路径变量
+ */
+ @ExceptionHandler(MissingPathVariableException.class)
+ public R handleMissingPathVariableException(MissingPathVariableException e, HttpServletRequest request) {
+ String requestURI = request.getRequestURI();
+ log.error("请求路径中缺少必需的路径变量'{}',发生系统异常.", requestURI, e);
+ return R.fail(String.format("请求路径中缺少必需的路径变量[%s]", e.getVariableName()));
+ }
+
+ /**
+ * 请求参数类型不匹配
+ */
+ @ExceptionHandler(MethodArgumentTypeMismatchException.class)
+ public R handleMethodArgumentTypeMismatchException(MethodArgumentTypeMismatchException e, HttpServletRequest request) {
+ String requestURI = request.getRequestURI();
+ log.error("请求参数类型不匹配'{}',发生系统异常.", requestURI, e);
+ return R.fail(String.format("请求参数类型不匹配,参数[%s]要求类型为:'%s',但输入值为:'%s'", e.getName(), e.getRequiredType().getName(), e.getValue()));
+ }
+
/**
* 拦截未知的运行时异常
*/
diff --git a/ruoyi-generator/pom.xml b/ruoyi-generator/pom.xml
index 8524a2352..4b89616b4 100644
--- a/ruoyi-generator/pom.xml
+++ b/ruoyi-generator/pom.xml
@@ -5,7 +5,7 @@
ruoyi-vue-plus
com.ruoyi
- 4.7.0
+ 4.8.0
4.0.0
diff --git a/ruoyi-job/pom.xml b/ruoyi-job/pom.xml
index 558451cf7..b97339484 100644
--- a/ruoyi-job/pom.xml
+++ b/ruoyi-job/pom.xml
@@ -5,7 +5,7 @@
ruoyi-vue-plus
com.ruoyi
- 4.7.0
+ 4.8.0
4.0.0
jar
diff --git a/ruoyi-oss/pom.xml b/ruoyi-oss/pom.xml
index 8da43cbb6..715eef5c5 100644
--- a/ruoyi-oss/pom.xml
+++ b/ruoyi-oss/pom.xml
@@ -5,7 +5,7 @@
ruoyi-vue-plus
com.ruoyi
- 4.7.0
+ 4.8.0
4.0.0
diff --git a/ruoyi-oss/src/main/java/com/ruoyi/oss/core/OssClient.java b/ruoyi-oss/src/main/java/com/ruoyi/oss/core/OssClient.java
index a82853e9c..bf2bb6c71 100644
--- a/ruoyi-oss/src/main/java/com/ruoyi/oss/core/OssClient.java
+++ b/ruoyi-oss/src/main/java/com/ruoyi/oss/core/OssClient.java
@@ -24,6 +24,7 @@ import com.ruoyi.oss.exception.OssException;
import com.ruoyi.oss.properties.OssProperties;
import java.io.ByteArrayInputStream;
+import java.io.File;
import java.io.InputStream;
import java.net.URL;
import java.util.Date;
@@ -115,6 +116,18 @@ public class OssClient {
return UploadResult.builder().url(getUrl() + "/" + path).filename(path).build();
}
+ public UploadResult upload(File file, String path) {
+ try {
+ PutObjectRequest putObjectRequest = new PutObjectRequest(properties.getBucketName(), path, file);
+ // 设置上传对象的 Acl 为公共读
+ putObjectRequest.setCannedAcl(getAccessPolicy().getAcl());
+ client.putObject(putObjectRequest);
+ } catch (Exception e) {
+ throw new OssException("上传文件失败,请检查配置信息:[" + e.getMessage() + "]");
+ }
+ return UploadResult.builder().url(getUrl() + "/" + path).filename(path).build();
+ }
+
public void delete(String path) {
path = path.replace(getUrl() + "/", "");
try {
@@ -132,6 +145,10 @@ public class OssClient {
return upload(inputStream, getPath(properties.getPrefix(), suffix), contentType);
}
+ public UploadResult uploadSuffix(File file, String suffix) {
+ return upload(file, getPath(properties.getPrefix(), suffix));
+ }
+
/**
* 获取文件元数据
*
diff --git a/ruoyi-sms/pom.xml b/ruoyi-sms/pom.xml
index efd0bbdfe..33dc69250 100644
--- a/ruoyi-sms/pom.xml
+++ b/ruoyi-sms/pom.xml
@@ -5,7 +5,7 @@
ruoyi-vue-plus
com.ruoyi
- 4.7.0
+ 4.8.0
4.0.0
@@ -24,15 +24,15 @@
- com.aliyun
- dysmsapi20170525
- true
-
-
-
- com.tencentcloudapi
- tencentcloud-sdk-java-sms
- true
+ org.dromara.sms4j
+ sms4j-spring-boot-starter
+
+
+
+ com.alibaba
+ fastjson
+
+
diff --git a/ruoyi-sms/src/main/java/com/ruoyi/sms/config/SmsConfig.java b/ruoyi-sms/src/main/java/com/ruoyi/sms/config/SmsConfig.java
index 753773e87..cf718e637 100644
--- a/ruoyi-sms/src/main/java/com/ruoyi/sms/config/SmsConfig.java
+++ b/ruoyi-sms/src/main/java/com/ruoyi/sms/config/SmsConfig.java
@@ -1,45 +1,12 @@
package com.ruoyi.sms.config;
-import com.ruoyi.sms.config.properties.SmsProperties;
-import com.ruoyi.sms.core.AliyunSmsTemplate;
-import com.ruoyi.sms.core.SmsTemplate;
-import com.ruoyi.sms.core.TencentSmsTemplate;
-import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
-import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
-import org.springframework.context.annotation.Bean;
-import org.springframework.context.annotation.Configuration;
-
/**
* 短信配置类
*
* @author Lion Li
* @version 4.2.0
*/
-@Configuration
+//@Configuration // 暂时用不上 留着后续扩展使用
public class SmsConfig {
- @Configuration
- @ConditionalOnProperty(value = "sms.enabled", havingValue = "true")
- @ConditionalOnClass(com.aliyun.dysmsapi20170525.Client.class)
- static class AliyunSmsConfig {
-
- @Bean
- public SmsTemplate aliyunSmsTemplate(SmsProperties smsProperties) {
- return new AliyunSmsTemplate(smsProperties);
- }
-
- }
-
- @Configuration
- @ConditionalOnProperty(value = "sms.enabled", havingValue = "true")
- @ConditionalOnClass(com.tencentcloudapi.sms.v20190711.SmsClient.class)
- static class TencentSmsConfig {
-
- @Bean
- public SmsTemplate tencentSmsTemplate(SmsProperties smsProperties) {
- return new TencentSmsTemplate(smsProperties);
- }
-
- }
-
}
diff --git a/ruoyi-sms/src/main/java/com/ruoyi/sms/config/properties/SmsProperties.java b/ruoyi-sms/src/main/java/com/ruoyi/sms/config/properties/SmsProperties.java
index 39359cdfd..3f5e03621 100644
--- a/ruoyi-sms/src/main/java/com/ruoyi/sms/config/properties/SmsProperties.java
+++ b/ruoyi-sms/src/main/java/com/ruoyi/sms/config/properties/SmsProperties.java
@@ -1,47 +1,20 @@
-package com.ruoyi.sms.config.properties;
-
-import lombok.Data;
-import org.springframework.boot.context.properties.ConfigurationProperties;
-import org.springframework.stereotype.Component;
-
-/**
- * SMS短信 配置属性
- *
- * @author Lion Li
- * @version 4.2.0
- */
-@Data
-@Component
-@ConfigurationProperties(prefix = "sms")
-public class SmsProperties {
-
- private Boolean enabled;
-
- /**
- * 配置节点
- * 阿里云 dysmsapi.aliyuncs.com
- * 腾讯云 sms.tencentcloudapi.com
- */
- private String endpoint;
-
- /**
- * key
- */
- private String accessKeyId;
-
- /**
- * 密匙
- */
- private String accessKeySecret;
-
- /*
- * 短信签名
- */
- private String signName;
-
- /**
- * 短信应用ID (腾讯专属)
- */
- private String sdkAppId;
-
-}
+//package com.ruoyi.sms.config.properties;
+//
+//import lombok.Data;
+//import org.springframework.boot.context.properties.ConfigurationProperties;
+//import org.springframework.stereotype.Component;
+//
+///**
+// * SMS短信 配置属性
+// *
+// * @author Lion Li
+// * @version 4.2.0
+// */
+//@Data
+//@Component
+//@ConfigurationProperties(prefix = "sms")
+//public class SmsProperties {
+//
+// private Boolean enabled;
+//
+//}
diff --git a/ruoyi-sms/src/main/java/com/ruoyi/sms/core/AliyunSmsTemplate.java b/ruoyi-sms/src/main/java/com/ruoyi/sms/core/AliyunSmsTemplate.java
deleted file mode 100644
index 3c16a5b41..000000000
--- a/ruoyi-sms/src/main/java/com/ruoyi/sms/core/AliyunSmsTemplate.java
+++ /dev/null
@@ -1,66 +0,0 @@
-package com.ruoyi.sms.core;
-
-import com.aliyun.dysmsapi20170525.Client;
-import com.aliyun.dysmsapi20170525.models.SendSmsRequest;
-import com.aliyun.dysmsapi20170525.models.SendSmsResponse;
-import com.aliyun.teaopenapi.models.Config;
-import com.ruoyi.common.utils.JsonUtils;
-import com.ruoyi.common.utils.StringUtils;
-import com.ruoyi.sms.config.properties.SmsProperties;
-import com.ruoyi.sms.entity.SmsResult;
-import com.ruoyi.sms.exception.SmsException;
-import lombok.SneakyThrows;
-
-import java.util.Map;
-
-/**
- * Aliyun 短信模板
- *
- * @author Lion Li
- * @version 4.2.0
- */
-public class AliyunSmsTemplate implements SmsTemplate {
-
- private SmsProperties properties;
-
- private Client client;
-
- @SneakyThrows(Exception.class)
- public AliyunSmsTemplate(SmsProperties smsProperties) {
- this.properties = smsProperties;
- Config config = new Config()
- // 您的AccessKey ID
- .setAccessKeyId(smsProperties.getAccessKeyId())
- // 您的AccessKey Secret
- .setAccessKeySecret(smsProperties.getAccessKeySecret())
- // 访问的域名
- .setEndpoint(smsProperties.getEndpoint());
- this.client = new Client(config);
- }
-
- @Override
- public SmsResult send(String phones, String templateId, Map param) {
- if (StringUtils.isBlank(phones)) {
- throw new SmsException("手机号不能为空");
- }
- if (StringUtils.isBlank(templateId)) {
- throw new SmsException("模板ID不能为空");
- }
- SendSmsRequest req = new SendSmsRequest()
- .setPhoneNumbers(phones)
- .setSignName(properties.getSignName())
- .setTemplateCode(templateId)
- .setTemplateParam(JsonUtils.toJsonString(param));
- try {
- SendSmsResponse resp = client.sendSms(req);
- return SmsResult.builder()
- .isSuccess("OK".equals(resp.getBody().getCode()))
- .message(resp.getBody().getMessage())
- .response(JsonUtils.toJsonString(resp))
- .build();
- } catch (Exception e) {
- throw new SmsException(e.getMessage());
- }
- }
-
-}
diff --git a/ruoyi-sms/src/main/java/com/ruoyi/sms/core/SmsTemplate.java b/ruoyi-sms/src/main/java/com/ruoyi/sms/core/SmsTemplate.java
deleted file mode 100644
index 0aec3ddbe..000000000
--- a/ruoyi-sms/src/main/java/com/ruoyi/sms/core/SmsTemplate.java
+++ /dev/null
@@ -1,26 +0,0 @@
-package com.ruoyi.sms.core;
-
-import com.ruoyi.sms.entity.SmsResult;
-
-import java.util.Map;
-
-/**
- * 短信模板
- *
- * @author Lion Li
- * @version 4.2.0
- */
-public interface SmsTemplate {
-
- /**
- * 发送短信
- *
- * @param phones 电话号(多个逗号分割)
- * @param templateId 模板id
- * @param param 模板对应参数
- * 阿里 需使用 模板变量名称对应内容 例如: code=1234
- * 腾讯 需使用 模板变量顺序对应内容 例如: 1=1234, 1为模板内第一个参数
- */
- SmsResult send(String phones, String templateId, Map param);
-
-}
diff --git a/ruoyi-sms/src/main/java/com/ruoyi/sms/core/TencentSmsTemplate.java b/ruoyi-sms/src/main/java/com/ruoyi/sms/core/TencentSmsTemplate.java
deleted file mode 100644
index 69ec7f1ec..000000000
--- a/ruoyi-sms/src/main/java/com/ruoyi/sms/core/TencentSmsTemplate.java
+++ /dev/null
@@ -1,82 +0,0 @@
-package com.ruoyi.sms.core;
-
-import cn.hutool.core.collection.CollUtil;
-import cn.hutool.core.util.ArrayUtil;
-import com.ruoyi.common.utils.JsonUtils;
-import com.ruoyi.common.utils.StringUtils;
-import com.ruoyi.sms.config.properties.SmsProperties;
-import com.ruoyi.sms.entity.SmsResult;
-import com.ruoyi.sms.exception.SmsException;
-import com.tencentcloudapi.common.Credential;
-import com.tencentcloudapi.common.profile.ClientProfile;
-import com.tencentcloudapi.common.profile.HttpProfile;
-import com.tencentcloudapi.sms.v20190711.SmsClient;
-import com.tencentcloudapi.sms.v20190711.models.SendSmsRequest;
-import com.tencentcloudapi.sms.v20190711.models.SendSmsResponse;
-import com.tencentcloudapi.sms.v20190711.models.SendStatus;
-import lombok.SneakyThrows;
-
-import java.util.Arrays;
-import java.util.Map;
-import java.util.Set;
-import java.util.stream.Collectors;
-
-/**
- * Tencent 短信模板
- *
- * @author Lion Li
- * @version 4.2.0
- */
-public class TencentSmsTemplate implements SmsTemplate {
-
- private SmsProperties properties;
-
- private SmsClient client;
-
- @SneakyThrows(Exception.class)
- public TencentSmsTemplate(SmsProperties smsProperties) {
- this.properties = smsProperties;
- Credential credential = new Credential(smsProperties.getAccessKeyId(), smsProperties.getAccessKeySecret());
- HttpProfile httpProfile = new HttpProfile();
- httpProfile.setEndpoint(smsProperties.getEndpoint());
- ClientProfile clientProfile = new ClientProfile();
- clientProfile.setHttpProfile(httpProfile);
- this.client = new SmsClient(credential, "", clientProfile);
- }
-
- @Override
- public SmsResult send(String phones, String templateId, Map param) {
- if (StringUtils.isBlank(phones)) {
- throw new SmsException("手机号不能为空");
- }
- if (StringUtils.isBlank(templateId)) {
- throw new SmsException("模板ID不能为空");
- }
- SendSmsRequest req = new SendSmsRequest();
- Set set = Arrays.stream(phones.split(StringUtils.SEPARATOR)).map(p -> "+86" + p).collect(Collectors.toSet());
- req.setPhoneNumberSet(ArrayUtil.toArray(set, String.class));
- if (CollUtil.isNotEmpty(param)) {
- req.setTemplateParamSet(ArrayUtil.toArray(param.values(), String.class));
- }
- req.setTemplateID(templateId);
- req.setSign(properties.getSignName());
- req.setSmsSdkAppid(properties.getSdkAppId());
- try {
- SendSmsResponse resp = client.SendSms(req);
- SmsResult.SmsResultBuilder builder = SmsResult.builder()
- .isSuccess(true)
- .message("send success")
- .response(JsonUtils.toJsonString(resp));
- for (SendStatus sendStatus : resp.getSendStatusSet()) {
- if (!"Ok".equals(sendStatus.getCode())) {
- builder.isSuccess(false).message(sendStatus.getMessage());
- break;
- }
- }
- return builder.build();
- } catch (Exception e) {
- throw new SmsException(e.getMessage());
- }
- }
-
-}
diff --git a/ruoyi-sms/src/main/java/com/ruoyi/sms/entity/SmsResult.java b/ruoyi-sms/src/main/java/com/ruoyi/sms/entity/SmsResult.java
deleted file mode 100644
index 89c39b403..000000000
--- a/ruoyi-sms/src/main/java/com/ruoyi/sms/entity/SmsResult.java
+++ /dev/null
@@ -1,31 +0,0 @@
-package com.ruoyi.sms.entity;
-
-import lombok.Builder;
-import lombok.Data;
-
-/**
- * 上传返回体
- *
- * @author Lion Li
- */
-@Data
-@Builder
-public class SmsResult {
-
- /**
- * 是否成功
- */
- private boolean isSuccess;
-
- /**
- * 响应消息
- */
- private String message;
-
- /**
- * 实际响应体
- *
- * 可自行转换为 SDK 对应的 SendSmsResponse
- */
- private String response;
-}
diff --git a/ruoyi-sms/src/main/java/com/ruoyi/sms/exception/SmsException.java b/ruoyi-sms/src/main/java/com/ruoyi/sms/exception/SmsException.java
deleted file mode 100644
index 28632a375..000000000
--- a/ruoyi-sms/src/main/java/com/ruoyi/sms/exception/SmsException.java
+++ /dev/null
@@ -1,16 +0,0 @@
-package com.ruoyi.sms.exception;
-
-/**
- * Sms异常类
- *
- * @author Lion Li
- */
-public class SmsException extends RuntimeException {
-
- private static final long serialVersionUID = 1L;
-
- public SmsException(String msg) {
- super(msg);
- }
-
-}
diff --git a/ruoyi-system/pom.xml b/ruoyi-system/pom.xml
index 57c1017f9..7db3a02a0 100644
--- a/ruoyi-system/pom.xml
+++ b/ruoyi-system/pom.xml
@@ -5,7 +5,7 @@
ruoyi-vue-plus
com.ruoyi
- 4.7.0
+ 4.8.0
4.0.0
diff --git a/ruoyi-system/src/main/java/com/ruoyi/system/service/ISysOssService.java b/ruoyi-system/src/main/java/com/ruoyi/system/service/ISysOssService.java
index 6472cebcb..f2221b107 100644
--- a/ruoyi-system/src/main/java/com/ruoyi/system/service/ISysOssService.java
+++ b/ruoyi-system/src/main/java/com/ruoyi/system/service/ISysOssService.java
@@ -2,12 +2,12 @@ package com.ruoyi.system.service;
import com.ruoyi.common.core.domain.PageQuery;
import com.ruoyi.common.core.page.TableDataInfo;
-import com.ruoyi.system.domain.SysOss;
import com.ruoyi.system.domain.bo.SysOssBo;
import com.ruoyi.system.domain.vo.SysOssVo;
import org.springframework.web.multipart.MultipartFile;
import javax.servlet.http.HttpServletResponse;
+import java.io.File;
import java.io.IOException;
import java.util.Collection;
import java.util.List;
@@ -27,6 +27,8 @@ public interface ISysOssService {
SysOssVo upload(MultipartFile file);
+ SysOssVo upload(File file);
+
void download(Long ossId, HttpServletResponse response) throws IOException;
Boolean deleteWithValidByIds(Collection ids, Boolean isValid);
diff --git a/ruoyi-system/src/main/java/com/ruoyi/system/service/SysLoginService.java b/ruoyi-system/src/main/java/com/ruoyi/system/service/SysLoginService.java
index 8b0616471..1fa41b805 100644
--- a/ruoyi-system/src/main/java/com/ruoyi/system/service/SysLoginService.java
+++ b/ruoyi-system/src/main/java/com/ruoyi/system/service/SysLoginService.java
@@ -8,9 +8,9 @@ import cn.hutool.core.util.ObjectUtil;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.ruoyi.common.constant.CacheConstants;
import com.ruoyi.common.constant.Constants;
-import com.ruoyi.common.core.domain.event.LogininforEvent;
import com.ruoyi.common.core.domain.dto.RoleDTO;
import com.ruoyi.common.core.domain.entity.SysUser;
+import com.ruoyi.common.core.domain.event.LogininforEvent;
import com.ruoyi.common.core.domain.model.LoginUser;
import com.ruoyi.common.core.domain.model.XcxLoginUser;
import com.ruoyi.common.enums.DeviceType;
@@ -71,9 +71,10 @@ public class SysLoginService {
if (captchaEnabled) {
validateCaptcha(username, code, uuid);
}
+ // 框架登录不限制从什么表查询 只要最终构建出 LoginUser 即可
SysUser user = loadUserByUsername(username);
checkLogin(LoginType.PASSWORD, username, () -> !BCrypt.checkpw(password, user.getPassword()));
- // 此处可根据登录用户的数据不同 自行创建 loginUser
+ // 此处可根据登录用户的数据不同 自行创建 loginUser 属性不够用继承扩展就行了
LoginUser loginUser = buildLoginUser(user);
// 生成token
LoginHelper.loginByDevice(loginUser, DeviceType.PC);
@@ -88,7 +89,7 @@ public class SysLoginService {
SysUser user = loadUserByPhonenumber(phonenumber);
checkLogin(LoginType.SMS, user.getUserName(), () -> !validateSmsCode(phonenumber, smsCode));
- // 此处可根据登录用户的数据不同 自行创建 loginUser
+ // 此处可根据登录用户的数据不同 自行创建 loginUser 属性不够用继承扩展就行了
LoginUser loginUser = buildLoginUser(user);
// 生成token
LoginHelper.loginByDevice(loginUser, DeviceType.APP);
@@ -99,11 +100,11 @@ public class SysLoginService {
}
public String emailLogin(String email, String emailCode) {
- // 通过手机号查找用户
+ // 通过手邮箱查找用户
SysUser user = loadUserByEmail(email);
checkLogin(LoginType.EMAIL, user.getUserName(), () -> !validateEmailCode(email, emailCode));
- // 此处可根据登录用户的数据不同 自行创建 loginUser
+ // 此处可根据登录用户的数据不同 自行创建 loginUser 属性不够用继承扩展就行了
LoginUser loginUser = buildLoginUser(user);
// 生成token
LoginHelper.loginByDevice(loginUser, DeviceType.APP);
@@ -118,9 +119,11 @@ public class SysLoginService {
// todo 以下自行实现
// 校验 appid + appsrcret + xcxCode 调用登录凭证校验接口 获取 session_key 与 openid
String openid = "";
+
+ // 框架登录不限制从什么表查询 只要最终构建出 LoginUser 即可
SysUser user = loadUserByOpenid(openid);
- // 此处可根据登录用户的数据不同 自行创建 loginUser
+ // 此处可根据登录用户的数据不同 自行创建 loginUser 属性不够用继承扩展就行了
XcxLoginUser loginUser = new XcxLoginUser();
loginUser.setUserId(user.getUserId());
loginUser.setUsername(user.getUserName());
@@ -301,25 +304,24 @@ public class SysLoginService {
String errorKey = CacheConstants.PWD_ERR_CNT_KEY + username;
String loginFail = Constants.LOGIN_FAIL;
- // 获取用户登录错误次数(可自定义限制策略 例如: key + username + ip)
- Integer errorNumber = RedisUtils.getCacheObject(errorKey);
+ // 获取用户登录错误次数,默认为0 (可自定义限制策略 例如: key + username + ip)
+ int errorNumber = ObjectUtil.defaultIfNull(RedisUtils.getCacheObject(errorKey), 0);
// 锁定时间内登录 则踢出
- if (ObjectUtil.isNotNull(errorNumber) && errorNumber.equals(maxRetryCount)) {
+ if (errorNumber >= maxRetryCount) {
recordLogininfor(username, loginFail, MessageUtils.message(loginType.getRetryLimitExceed(), maxRetryCount, lockTime));
throw new UserException(loginType.getRetryLimitExceed(), maxRetryCount, lockTime);
}
if (supplier.get()) {
- // 是否第一次
- errorNumber = ObjectUtil.isNull(errorNumber) ? 1 : errorNumber + 1;
+ // 错误次数递增
+ errorNumber++;
+ RedisUtils.setCacheObject(errorKey, errorNumber, Duration.ofMinutes(lockTime));
// 达到规定错误次数 则锁定登录
- if (errorNumber.equals(maxRetryCount)) {
- RedisUtils.setCacheObject(errorKey, errorNumber, Duration.ofMinutes(lockTime));
+ if (errorNumber >= maxRetryCount) {
recordLogininfor(username, loginFail, MessageUtils.message(loginType.getRetryLimitExceed(), maxRetryCount, lockTime));
throw new UserException(loginType.getRetryLimitExceed(), maxRetryCount, lockTime);
} else {
- // 未达到规定错误次数 则递增
- RedisUtils.setCacheObject(errorKey, errorNumber);
+ // 未达到规定错误次数
recordLogininfor(username, loginFail, MessageUtils.message(loginType.getRetryLimitCount(), errorNumber));
throw new UserException(loginType.getRetryLimitCount(), errorNumber);
}
diff --git a/ruoyi-system/src/main/java/com/ruoyi/system/service/impl/SysDictTypeServiceImpl.java b/ruoyi-system/src/main/java/com/ruoyi/system/service/impl/SysDictTypeServiceImpl.java
index 0c897df61..85d892411 100644
--- a/ruoyi-system/src/main/java/com/ruoyi/system/service/impl/SysDictTypeServiceImpl.java
+++ b/ruoyi-system/src/main/java/com/ruoyi/system/service/impl/SysDictTypeServiceImpl.java
@@ -116,7 +116,6 @@ public class SysDictTypeServiceImpl implements ISysDictTypeService, DictService
* @param dictType 字典类型
* @return 字典类型
*/
- @Cacheable(cacheNames = CacheNames.SYS_DICT, key = "#dictType")
@Override
public SysDictType selectDictTypeByType(String dictType) {
return baseMapper.selectById(new LambdaQueryWrapper().eq(SysDictType::getDictType, dictType));
@@ -148,7 +147,7 @@ public class SysDictTypeServiceImpl implements ISysDictTypeService, DictService
List dictDataList = dictDataMapper.selectList(
new LambdaQueryWrapper().eq(SysDictData::getStatus, UserConstants.DICT_NORMAL));
Map> dictDataMap = StreamUtils.groupByKey(dictDataList, SysDictData::getDictType);
- dictDataMap.forEach((k,v) -> {
+ dictDataMap.forEach((k, v) -> {
List dictList = StreamUtils.sorted(v, Comparator.comparing(SysDictData::getDictSort));
CacheUtils.put(CacheNames.SYS_DICT, k, dictList);
});
@@ -182,6 +181,7 @@ public class SysDictTypeServiceImpl implements ISysDictTypeService, DictService
public List insertDictType(SysDictType dict) {
int row = baseMapper.insert(dict);
if (row > 0) {
+ // 新增 type 下无 data 数据 返回空防止缓存穿透
return new ArrayList<>();
}
throw new ServiceException("操作失败");
@@ -279,4 +279,9 @@ public class SysDictTypeServiceImpl implements ISysDictTypeService, DictService
}
}
+ @Override
+ public Map getAllDictByDictType(String dictType) {
+ List list = selectDictDataByType(dictType);
+ return StreamUtils.toMap(list, SysDictData::getDictValue, SysDictData::getDictLabel);
+ }
}
diff --git a/ruoyi-system/src/main/java/com/ruoyi/system/service/impl/SysOssServiceImpl.java b/ruoyi-system/src/main/java/com/ruoyi/system/service/impl/SysOssServiceImpl.java
index c36bfa274..ce60d31a8 100644
--- a/ruoyi-system/src/main/java/com/ruoyi/system/service/impl/SysOssServiceImpl.java
+++ b/ruoyi-system/src/main/java/com/ruoyi/system/service/impl/SysOssServiceImpl.java
@@ -1,5 +1,6 @@
package com.ruoyi.system.service.impl;
+import cn.hutool.core.bean.BeanUtil;
import cn.hutool.core.convert.Convert;
import cn.hutool.core.io.IoUtil;
import cn.hutool.core.util.ObjectUtil;
@@ -11,7 +12,6 @@ import com.ruoyi.common.core.domain.PageQuery;
import com.ruoyi.common.core.page.TableDataInfo;
import com.ruoyi.common.core.service.OssService;
import com.ruoyi.common.exception.ServiceException;
-import com.ruoyi.common.utils.BeanCopyUtils;
import com.ruoyi.common.utils.StringUtils;
import com.ruoyi.common.utils.file.FileUtils;
import com.ruoyi.common.utils.spring.SpringUtils;
@@ -31,9 +31,13 @@ import org.springframework.stereotype.Service;
import org.springframework.web.multipart.MultipartFile;
import javax.servlet.http.HttpServletResponse;
+import java.io.File;
import java.io.IOException;
import java.io.InputStream;
-import java.util.*;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.List;
+import java.util.Map;
import java.util.stream.Collectors;
/**
@@ -108,7 +112,7 @@ public class SysOssServiceImpl implements ISysOssService, OssService {
}
FileUtils.setAttachmentResponseHeader(response, sysOss.getOriginalName());
response.setContentType(MediaType.APPLICATION_OCTET_STREAM_VALUE + "; charset=UTF-8");
- OssClient storage = OssFactory.instance();
+ OssClient storage = OssFactory.instance(sysOss.getService());
try(InputStream inputStream = storage.getObjectContent(sysOss.getUrl())) {
int available = inputStream.available();
IoUtil.copy(inputStream, response.getOutputStream(), available);
@@ -130,15 +134,28 @@ public class SysOssServiceImpl implements ISysOssService, OssService {
throw new ServiceException(e.getMessage());
}
// 保存文件信息
+ return buildResultEntity(originalfileName, suffix, storage.getConfigKey(), uploadResult);
+ }
+
+ @Override
+ public SysOssVo upload(File file) {
+ String originalfileName = file.getName();
+ String suffix = StringUtils.substring(originalfileName, originalfileName.lastIndexOf("."), originalfileName.length());
+ OssClient storage = OssFactory.instance();
+ UploadResult uploadResult = storage.uploadSuffix(file, suffix);
+ // 保存文件信息
+ return buildResultEntity(originalfileName, suffix, storage.getConfigKey(), uploadResult);
+ }
+
+ private SysOssVo buildResultEntity(String originalfileName, String suffix, String configKey, UploadResult uploadResult) {
SysOss oss = new SysOss();
oss.setUrl(uploadResult.getUrl());
oss.setFileSuffix(suffix);
oss.setFileName(uploadResult.getFilename());
oss.setOriginalName(originalfileName);
- oss.setService(storage.getConfigKey());
+ oss.setService(configKey);
baseMapper.insert(oss);
- SysOssVo sysOssVo = new SysOssVo();
- BeanCopyUtils.copy(oss, sysOssVo);
+ SysOssVo sysOssVo = BeanUtil.toBean(oss, SysOssVo.class);
return this.matchingUrl(sysOssVo);
}
diff --git a/ruoyi-system/src/main/java/com/ruoyi/system/service/impl/SysRoleServiceImpl.java b/ruoyi-system/src/main/java/com/ruoyi/system/service/impl/SysRoleServiceImpl.java
index b28df9e98..84e762175 100644
--- a/ruoyi-system/src/main/java/com/ruoyi/system/service/impl/SysRoleServiceImpl.java
+++ b/ruoyi-system/src/main/java/com/ruoyi/system/service/impl/SysRoleServiceImpl.java
@@ -186,6 +186,20 @@ public class SysRoleServiceImpl implements ISysRoleService {
if (ObjectUtil.isNotNull(role.getRoleId()) && role.isAdmin()) {
throw new ServiceException("不允许操作超级管理员角色");
}
+ // 新增不允许使用 管理员标识符
+ if (ObjectUtil.isNull(role.getRoleId())
+ && StringUtils.equals(role.getRoleKey(), UserConstants.ADMIN_ROLE_KEY)) {
+ throw new ServiceException("不允许使用系统内置管理员角色标识符!");
+ }
+ // 修改不允许修改 管理员标识符
+ if (ObjectUtil.isNotNull(role.getRoleId())) {
+ SysRole sysRole = baseMapper.selectById(role.getRoleId());
+ // 如果标识符不相等 判断为修改了管理员标识符
+ if (!StringUtils.equals(sysRole.getRoleKey(), role.getRoleKey())
+ && StringUtils.equals(sysRole.getRoleKey(), UserConstants.ADMIN_ROLE_KEY)) {
+ throw new ServiceException("不允许修改系统内置管理员角色标识符!");
+ }
+ }
}
/**
@@ -342,9 +356,9 @@ public class SysRoleServiceImpl implements ISysRoleService {
@Transactional(rollbackFor = Exception.class)
public int deleteRoleByIds(Long[] roleIds) {
for (Long roleId : roleIds) {
- checkRoleAllowed(new SysRole(roleId));
- checkRoleDataScope(roleId);
SysRole role = selectRoleById(roleId);
+ checkRoleAllowed(role);
+ checkRoleDataScope(roleId);
if (countUserRoleByRoleId(roleId) > 0) {
throw new ServiceException(String.format("%1$s已分配,不能删除", role.getRoleName()));
}
@@ -420,6 +434,11 @@ public class SysRoleServiceImpl implements ISysRoleService {
@Override
public void cleanOnlineUserByRole(Long roleId) {
+ // 如果角色未绑定用户 直接返回
+ Long num = userRoleMapper.selectCount(new LambdaQueryWrapper().eq(SysUserRole::getRoleId, roleId));
+ if (num == 0) {
+ return;
+ }
List keys = StpUtil.searchTokenValue("", 0, -1, false);
if (CollUtil.isEmpty(keys)) {
return;
@@ -428,11 +447,11 @@ public class SysRoleServiceImpl implements ISysRoleService {
keys.parallelStream().forEach(key -> {
String token = StringUtils.substringAfterLast(key, ":");
// 如果已经过期则跳过
- if (StpUtil.stpLogic.getTokenActivityTimeoutByToken(token) < -1) {
+ if (StpUtil.stpLogic.getTokenActiveTimeoutByToken(token) < -1) {
return;
}
LoginUser loginUser = LoginHelper.getLoginUser(token);
- if (loginUser.getRoles().stream().anyMatch(r -> r.getRoleId().equals(roleId))) {
+ if (ObjectUtil.isNotNull(loginUser) && loginUser.getRoles().stream().anyMatch(r -> r.getRoleId().equals(roleId))) {
try {
StpUtil.logoutByTokenValue(token);
} catch (NotLoginException ignored) {
diff --git a/ruoyi-ui-vue3/.editorconfig b/ruoyi-ui-vue3/.editorconfig
new file mode 100644
index 000000000..5b1a80448
--- /dev/null
+++ b/ruoyi-ui-vue3/.editorconfig
@@ -0,0 +1,21 @@
+# 告诉EditorConfig插件,这是根文件,不用继续往上查找
+root = true
+
+# 匹配全部文件
+[*]
+# 缩进风格,可选space、tab
+indent_style = space
+# 缩进的空格数
+indent_size = 2
+# 设置字符集
+charset = utf-8
+# 结尾换行符,可选lf、cr、crlf
+end_of_line = lf
+# 在文件结尾插入新行
+trim_trailing_whitespace = true
+# 删除一行中的前后空格
+insert_final_newline = true
+
+[*.md]
+insert_final_newline = false
+trim_trailing_whitespace = false
diff --git a/ruoyi-ui-vue3/.env.development b/ruoyi-ui-vue3/.env.development
new file mode 100644
index 000000000..ac4d2c13f
--- /dev/null
+++ b/ruoyi-ui-vue3/.env.development
@@ -0,0 +1,17 @@
+# 页面标题
+VITE_APP_TITLE = RuoYi-Vue-Plus后台管理系统
+
+# 开发环境配置
+VITE_APP_ENV = 'development'
+
+# 若依管理系统/开发环境
+VITE_APP_BASE_API = '/dev-api'
+
+# 应用访问路径 例如使用前缀 /admin/
+VITE_APP_CONTEXT_PATH = '/'
+
+# 监控地址
+VITE_APP_MONITRO_ADMIN = 'http://localhost:9090/admin/applications'
+
+# xxl-job 控制台地址
+VITE_APP_XXL_JOB_ADMIN = 'http://localhost:9100/xxl-job-admin'
diff --git a/ruoyi-ui-vue3/.env.production b/ruoyi-ui-vue3/.env.production
new file mode 100644
index 000000000..18bb273cb
--- /dev/null
+++ b/ruoyi-ui-vue3/.env.production
@@ -0,0 +1,20 @@
+# 页面标题
+VITE_APP_TITLE = RuoYi-Vue-Plus后台管理系统
+
+# 生产环境配置
+VITE_APP_ENV = 'production'
+
+# 应用访问路径 例如使用前缀 /admin/
+VITE_APP_CONTEXT_PATH = '/'
+
+# 监控地址
+VITE_APP_MONITRO_ADMIN = '/admin/applications'
+
+# 监控地址
+VITE_APP_XXL_JOB_ADMIN = '/xxl-job-admin'
+
+# 若依管理系统/生产环境
+VITE_APP_BASE_API = '/prod-api'
+
+# 是否在打包时开启压缩,支持 gzip 和 brotli
+VITE_BUILD_COMPRESS = gzip
diff --git a/ruoyi-ui-vue3/.gitignore b/ruoyi-ui-vue3/.gitignore
new file mode 100644
index 000000000..78a752d87
--- /dev/null
+++ b/ruoyi-ui-vue3/.gitignore
@@ -0,0 +1,23 @@
+.DS_Store
+node_modules/
+dist/
+npm-debug.log*
+yarn-debug.log*
+yarn-error.log*
+**/*.log
+
+tests/**/coverage/
+tests/e2e/reports
+selenium-debug.log
+
+# Editor directories and files
+.idea
+.vscode
+*.suo
+*.ntvs*
+*.njsproj
+*.sln
+*.local
+
+package-lock.json
+yarn.lock
diff --git a/ruoyi-ui-vue3/README.md b/ruoyi-ui-vue3/README.md
new file mode 100644
index 000000000..199f19652
--- /dev/null
+++ b/ruoyi-ui-vue3/README.md
@@ -0,0 +1,83 @@
+## 平台简介
+
+* 本仓库为前端技术栈 [Vue3](https://v3.cn.vuejs.org) + [Element Plus](https://element-plus.org/zh-CN) + [Vite](https://cn.vitejs.dev) 版本。
+* 配套后端代码仓库地址[RuoYi-Vue-Plus 4.X(注意版本号)](https://gitee.com/JavaLionLi/RuoYi-Vue-Plus)
+* 5.X后端需要使用此项目 [plus-ui](https://gitee.com/JavaLionLi/plus-ui)
+
+## 前端运行
+
+```bash
+# 进入项目目录
+cd ruoyi-ui-vue3
+
+# 安装依赖
+npm install --registry=https://registry.npmmirror.com
+
+# 启动服务
+npm run dev
+
+# 构建测试环境 yarn build:stage
+# 构建生产环境 yarn build:prod
+# 前端访问地址 http://localhost:80
+```
+
+## 后端改造
+参考后端代码内 `ruoyi-generator/resources/vm/vue/v3/readme.txt` 说明
+
+## 内置功能
+
+1. 用户管理:用户是系统操作者,该功能主要完成系统用户配置。
+2. 部门管理:配置系统组织机构(公司、部门、小组),树结构展现支持数据权限。
+3. 岗位管理:配置系统用户所属担任职务。
+4. 菜单管理:配置系统菜单,操作权限,按钮权限标识等。
+5. 角色管理:角色菜单权限分配、设置角色按机构进行数据范围权限划分。
+6. 字典管理:对系统中经常使用的一些较为固定的数据进行维护。
+7. 参数管理:对系统动态配置常用参数。
+8. 通知公告:系统通知公告信息发布维护。
+9. 操作日志:系统正常操作日志记录和查询;系统异常信息日志记录和查询。
+10. 登录日志:系统登录日志记录查询包含登录异常。
+11. 在线用户:当前系统中活跃用户状态监控。
+12. 定时任务:在线(添加、修改、删除)任务调度包含执行结果日志。
+13. 代码生成:前后端代码的生成(java、html、xml、sql)支持CRUD下载 。
+14. 系统接口:根据业务代码自动生成相关的api接口文档。
+15. 服务监控:监视当前系统CPU、内存、磁盘、堆栈等相关信息。
+16. 缓存监控:对系统的缓存信息查询,命令统计等。
+17. 在线构建器:拖动表单元素生成相应的HTML代码。
+18. 连接池监视:监视当前系统数据库连接池状态,可进行分析SQL找出系统性能瓶颈。
+
+## 演示图
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/ruoyi-ui-vue3/bin/build.bat b/ruoyi-ui-vue3/bin/build.bat
new file mode 100644
index 000000000..ecbb4544b
--- /dev/null
+++ b/ruoyi-ui-vue3/bin/build.bat
@@ -0,0 +1,12 @@
+@echo off
+echo.
+echo [Ϣ] Weḅdistļ
+echo.
+
+%~d0
+cd %~dp0
+
+cd ..
+yarn build:prod
+
+pause
\ No newline at end of file
diff --git a/ruoyi-ui-vue3/bin/package.bat b/ruoyi-ui-vue3/bin/package.bat
new file mode 100644
index 000000000..f5b24e0ba
--- /dev/null
+++ b/ruoyi-ui-vue3/bin/package.bat
@@ -0,0 +1,12 @@
+@echo off
+echo.
+echo [Ϣ] װWeḅnode_modulesļ
+echo.
+
+%~d0
+cd %~dp0
+
+cd ..
+yarn --registry=https://registry.npmmirror.com
+
+pause
\ No newline at end of file
diff --git a/ruoyi-ui-vue3/bin/run-web.bat b/ruoyi-ui-vue3/bin/run-web.bat
new file mode 100644
index 000000000..d2fe3970f
--- /dev/null
+++ b/ruoyi-ui-vue3/bin/run-web.bat
@@ -0,0 +1,12 @@
+@echo off
+echo.
+echo [Ϣ] ʹ Vite Web ̡
+echo.
+
+%~d0
+cd %~dp0
+
+cd ..
+yarn dev
+
+pause
\ No newline at end of file
diff --git a/ruoyi-ui-vue3/html/ie.html b/ruoyi-ui-vue3/html/ie.html
new file mode 100644
index 000000000..052ffcd64
--- /dev/null
+++ b/ruoyi-ui-vue3/html/ie.html
@@ -0,0 +1,46 @@
+
+
+
+
+
+ 请升级您的浏览器
+
+
+
+
+
+
+请升级您的浏览器,以便我们更好的为您提供服务!
+您正在使用 Internet Explorer 的早期版本(IE11以下版本或使用该内核的浏览器)。这意味着在升级浏览器前,您将无法访问此网站。
+
+请注意:微软公司对Windows XP 及 Internet Explorer 早期版本的支持已经结束
+自 2016 年 1 月 12 日起,Microsoft 不再为 IE 11 以下版本提供相应支持和更新。没有关键的浏览器安全更新,您的电脑可能易受有害病毒、间谍软件和其他恶意软件的攻击,它们可以窃取或损害您的业务数据和信息。请参阅 微软对 Internet Explorer 早期版本的支持将于 2016 年 1 月 12 日结束的说明 。
+
+您可以选择更先进的浏览器
+推荐使用以下浏览器的最新版本。如果您的电脑已有以下浏览器的最新版本则直接使用该浏览器访问即可。
+
+
+
+
\ No newline at end of file
diff --git a/ruoyi-ui-vue3/index.html b/ruoyi-ui-vue3/index.html
new file mode 100644
index 000000000..3f0dcb5a7
--- /dev/null
+++ b/ruoyi-ui-vue3/index.html
@@ -0,0 +1,215 @@
+
+
+
+
+
+
+
+
+
+ RuoYi-Vue-Plus管理系统
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/ruoyi-ui-vue3/package.json b/ruoyi-ui-vue3/package.json
new file mode 100644
index 000000000..b570f1c64
--- /dev/null
+++ b/ruoyi-ui-vue3/package.json
@@ -0,0 +1,43 @@
+{
+ "name": "ruoyi-vue-plus",
+ "version": "4.8.0",
+ "description": "RuoYi-Vue-Plus后台管理系统",
+ "author": "LionLi",
+ "license": "MIT",
+ "scripts": {
+ "dev": "vite",
+ "build:prod": "vite build",
+ "preview": "vite preview"
+ },
+ "repository": {
+ "type": "git",
+ "url": "https://gitee.com/JavaLionLi/RuoYi-Vue-Plus-UI.git"
+ },
+ "dependencies": {
+ "@element-plus/icons-vue": "2.0.10",
+ "@vueup/vue-quill": "1.1.0",
+ "@vueuse/core": "9.5.0",
+ "axios": "0.27.2",
+ "echarts": "5.4.0",
+ "element-plus": "2.2.27",
+ "file-saver": "2.0.5",
+ "fuse.js": "6.6.2",
+ "js-cookie": "3.0.1",
+ "jsencrypt": "3.3.1",
+ "nprogress": "0.2.0",
+ "pinia": "2.0.22",
+ "vue": "3.2.45",
+ "vue-cropper": "1.0.3",
+ "vue-router": "4.1.4"
+ },
+ "devDependencies": {
+ "@vitejs/plugin-vue": "3.1.0",
+ "@vue/compiler-sfc": "3.2.45",
+ "sass": "1.56.1",
+ "unplugin-auto-import": "0.11.4",
+ "vite": "3.2.3",
+ "vite-plugin-compression": "0.5.1",
+ "vite-plugin-svg-icons": "2.0.1",
+ "vite-plugin-vue-setup-extend": "0.4.0"
+ }
+}
diff --git a/ruoyi-ui-vue3/public/favicon.ico b/ruoyi-ui-vue3/public/favicon.ico
new file mode 100644
index 000000000..3f919d85a
Binary files /dev/null and b/ruoyi-ui-vue3/public/favicon.ico differ
diff --git a/ruoyi-ui-vue3/src/App.vue b/ruoyi-ui-vue3/src/App.vue
new file mode 100644
index 000000000..31839f20f
--- /dev/null
+++ b/ruoyi-ui-vue3/src/App.vue
@@ -0,0 +1,15 @@
+
+
+
+
+
diff --git a/ruoyi-ui-vue3/src/api/demo/demo.js b/ruoyi-ui-vue3/src/api/demo/demo.js
new file mode 100644
index 000000000..04d40256f
--- /dev/null
+++ b/ruoyi-ui-vue3/src/api/demo/demo.js
@@ -0,0 +1,54 @@
+import request from '@/utils/request'
+
+// 查询测试单表列表
+export function listDemo(query) {
+ return request({
+ url: '/demo/demo/list',
+ method: 'get',
+ params: query
+ })
+}
+
+// 自定义分页接口
+export function pageDemo(query) {
+ return request({
+ url: '/demo/demo/page',
+ method: 'get',
+ params: query
+ })
+}
+
+// 查询测试单表详细
+export function getDemo(id) {
+ return request({
+ url: '/demo/demo/' + id,
+ method: 'get'
+ })
+}
+
+// 新增测试单表
+export function addDemo(data) {
+ return request({
+ url: '/demo/demo',
+ method: 'post',
+ data: data
+ })
+}
+
+// 修改测试单表
+export function updateDemo(data) {
+ return request({
+ url: '/demo/demo',
+ method: 'put',
+ data: data
+ })
+}
+
+// 删除测试单表
+export function delDemo(id) {
+ return request({
+ url: '/demo/demo/' + id,
+ method: 'delete'
+ })
+}
+
diff --git a/ruoyi-ui-vue3/src/api/demo/tree.js b/ruoyi-ui-vue3/src/api/demo/tree.js
new file mode 100644
index 000000000..4c7ebc070
--- /dev/null
+++ b/ruoyi-ui-vue3/src/api/demo/tree.js
@@ -0,0 +1,44 @@
+import request from '@/utils/request'
+
+// 查询测试树表列表
+export function listTree(query) {
+ return request({
+ url: '/demo/tree/list',
+ method: 'get',
+ params: query
+ })
+}
+
+// 查询测试树表详细
+export function getTree(id) {
+ return request({
+ url: '/demo/tree/' + id,
+ method: 'get'
+ })
+}
+
+// 新增测试树表
+export function addTree(data) {
+ return request({
+ url: '/demo/tree',
+ method: 'post',
+ data: data
+ })
+}
+
+// 修改测试树表
+export function updateTree(data) {
+ return request({
+ url: '/demo/tree',
+ method: 'put',
+ data: data
+ })
+}
+
+// 删除测试树表
+export function delTree(id) {
+ return request({
+ url: '/demo/tree/' + id,
+ method: 'delete'
+ })
+}
diff --git a/ruoyi-ui-vue3/src/api/login.js b/ruoyi-ui-vue3/src/api/login.js
new file mode 100644
index 000000000..649f59c81
--- /dev/null
+++ b/ruoyi-ui-vue3/src/api/login.js
@@ -0,0 +1,59 @@
+import request from '@/utils/request'
+
+// 登录方法
+export function login(username, password, code, uuid) {
+ const data = {
+ username,
+ password,
+ code,
+ uuid
+ }
+ return request({
+ url: '/login',
+ headers: {
+ isToken: false
+ },
+ method: 'post',
+ data: data
+ })
+}
+
+// 注册方法
+export function register(data) {
+ return request({
+ url: '/register',
+ headers: {
+ isToken: false
+ },
+ method: 'post',
+ data: data
+ })
+}
+
+// 获取用户详细信息
+export function getInfo() {
+ return request({
+ url: '/getInfo',
+ method: 'get'
+ })
+}
+
+// 退出方法
+export function logout() {
+ return request({
+ url: '/logout',
+ method: 'post'
+ })
+}
+
+// 获取验证码
+export function getCodeImg() {
+ return request({
+ url: '/captchaImage',
+ headers: {
+ isToken: false
+ },
+ method: 'get',
+ timeout: 20000
+ })
+}
\ No newline at end of file
diff --git a/ruoyi-ui-vue3/src/api/menu.js b/ruoyi-ui-vue3/src/api/menu.js
new file mode 100644
index 000000000..faef101c4
--- /dev/null
+++ b/ruoyi-ui-vue3/src/api/menu.js
@@ -0,0 +1,9 @@
+import request from '@/utils/request'
+
+// 获取路由
+export const getRouters = () => {
+ return request({
+ url: '/getRouters',
+ method: 'get'
+ })
+}
\ No newline at end of file
diff --git a/ruoyi-ui-vue3/src/api/monitor/cache.js b/ruoyi-ui-vue3/src/api/monitor/cache.js
new file mode 100644
index 000000000..45a9003df
--- /dev/null
+++ b/ruoyi-ui-vue3/src/api/monitor/cache.js
@@ -0,0 +1,57 @@
+import request from '@/utils/request'
+
+// 查询缓存详细
+export function getCache() {
+ return request({
+ url: '/monitor/cache',
+ method: 'get'
+ })
+}
+
+// 查询缓存名称列表
+export function listCacheName() {
+ return request({
+ url: '/monitor/cache/getNames',
+ method: 'get'
+ })
+}
+
+// 查询缓存键名列表
+export function listCacheKey(cacheName) {
+ return request({
+ url: '/monitor/cache/getKeys/' + cacheName,
+ method: 'get'
+ })
+}
+
+// 查询缓存内容
+export function getCacheValue(cacheName, cacheKey) {
+ return request({
+ url: '/monitor/cache/getValue/' + cacheName + '/' + cacheKey,
+ method: 'get'
+ })
+}
+
+// 清理指定名称缓存
+export function clearCacheName(cacheName) {
+ return request({
+ url: '/monitor/cache/clearCacheName/' + cacheName,
+ method: 'delete'
+ })
+}
+
+// 清理指定键名缓存
+export function clearCacheKey(cacheName, cacheKey) {
+ return request({
+ url: '/monitor/cache/clearCacheKey/' + cacheName + '/' + cacheKey,
+ method: 'delete'
+ })
+}
+
+// 清理全部缓存
+export function clearCacheAll() {
+ return request({
+ url: '/monitor/cache/clearCacheAll',
+ method: 'delete'
+ })
+}
diff --git a/ruoyi-ui-vue3/src/api/monitor/logininfor.js b/ruoyi-ui-vue3/src/api/monitor/logininfor.js
new file mode 100644
index 000000000..4d112b78a
--- /dev/null
+++ b/ruoyi-ui-vue3/src/api/monitor/logininfor.js
@@ -0,0 +1,34 @@
+import request from '@/utils/request'
+
+// 查询登录日志列表
+export function list(query) {
+ return request({
+ url: '/monitor/logininfor/list',
+ method: 'get',
+ params: query
+ })
+}
+
+// 删除登录日志
+export function delLogininfor(infoId) {
+ return request({
+ url: '/monitor/logininfor/' + infoId,
+ method: 'delete'
+ })
+}
+
+// 解锁用户登录状态
+export function unlockLogininfor(userName) {
+ return request({
+ url: '/monitor/logininfor/unlock/' + userName,
+ method: 'get'
+ })
+}
+
+// 清空登录日志
+export function cleanLogininfor() {
+ return request({
+ url: '/monitor/logininfor/clean',
+ method: 'delete'
+ })
+}
diff --git a/ruoyi-ui-vue3/src/api/monitor/online.js b/ruoyi-ui-vue3/src/api/monitor/online.js
new file mode 100644
index 000000000..bd2213780
--- /dev/null
+++ b/ruoyi-ui-vue3/src/api/monitor/online.js
@@ -0,0 +1,18 @@
+import request from '@/utils/request'
+
+// 查询在线用户列表
+export function list(query) {
+ return request({
+ url: '/monitor/online/list',
+ method: 'get',
+ params: query
+ })
+}
+
+// 强退用户
+export function forceLogout(tokenId) {
+ return request({
+ url: '/monitor/online/' + tokenId,
+ method: 'delete'
+ })
+}
diff --git a/ruoyi-ui-vue3/src/api/monitor/operlog.js b/ruoyi-ui-vue3/src/api/monitor/operlog.js
new file mode 100644
index 000000000..a04bca840
--- /dev/null
+++ b/ruoyi-ui-vue3/src/api/monitor/operlog.js
@@ -0,0 +1,26 @@
+import request from '@/utils/request'
+
+// 查询操作日志列表
+export function list(query) {
+ return request({
+ url: '/monitor/operlog/list',
+ method: 'get',
+ params: query
+ })
+}
+
+// 删除操作日志
+export function delOperlog(operId) {
+ return request({
+ url: '/monitor/operlog/' + operId,
+ method: 'delete'
+ })
+}
+
+// 清空操作日志
+export function cleanOperlog() {
+ return request({
+ url: '/monitor/operlog/clean',
+ method: 'delete'
+ })
+}
diff --git a/ruoyi-ui-vue3/src/api/system/config.js b/ruoyi-ui-vue3/src/api/system/config.js
new file mode 100644
index 000000000..9b93886a3
--- /dev/null
+++ b/ruoyi-ui-vue3/src/api/system/config.js
@@ -0,0 +1,72 @@
+import request from '@/utils/request'
+
+// 查询参数列表
+export function listConfig(query) {
+ return request({
+ url: '/system/config/list',
+ method: 'get',
+ params: query
+ })
+}
+
+// 查询参数详细
+export function getConfig(configId) {
+ return request({
+ url: '/system/config/' + configId,
+ method: 'get'
+ })
+}
+
+// 根据参数键名查询参数值
+export function getConfigKey(configKey) {
+ return request({
+ url: '/system/config/configKey/' + configKey,
+ method: 'get'
+ })
+}
+
+// 新增参数配置
+export function addConfig(data) {
+ return request({
+ url: '/system/config',
+ method: 'post',
+ data: data
+ })
+}
+
+// 修改参数配置
+export function updateConfig(data) {
+ return request({
+ url: '/system/config',
+ method: 'put',
+ data: data
+ })
+}
+
+// 修改参数配置
+export function updateConfigByKey(key, value) {
+ return request({
+ url: '/system/config/updateByKey',
+ method: 'put',
+ data: {
+ configKey: key,
+ configValue: value
+ }
+ })
+}
+
+// 删除参数配置
+export function delConfig(configId) {
+ return request({
+ url: '/system/config/' + configId,
+ method: 'delete'
+ })
+}
+
+// 刷新参数缓存
+export function refreshCache() {
+ return request({
+ url: '/system/config/refreshCache',
+ method: 'delete'
+ })
+}
diff --git a/ruoyi-ui-vue3/src/api/system/dept.js b/ruoyi-ui-vue3/src/api/system/dept.js
new file mode 100644
index 000000000..fc943cd41
--- /dev/null
+++ b/ruoyi-ui-vue3/src/api/system/dept.js
@@ -0,0 +1,52 @@
+import request from '@/utils/request'
+
+// 查询部门列表
+export function listDept(query) {
+ return request({
+ url: '/system/dept/list',
+ method: 'get',
+ params: query
+ })
+}
+
+// 查询部门列表(排除节点)
+export function listDeptExcludeChild(deptId) {
+ return request({
+ url: '/system/dept/list/exclude/' + deptId,
+ method: 'get'
+ })
+}
+
+// 查询部门详细
+export function getDept(deptId) {
+ return request({
+ url: '/system/dept/' + deptId,
+ method: 'get'
+ })
+}
+
+// 新增部门
+export function addDept(data) {
+ return request({
+ url: '/system/dept',
+ method: 'post',
+ data: data
+ })
+}
+
+// 修改部门
+export function updateDept(data) {
+ return request({
+ url: '/system/dept',
+ method: 'put',
+ data: data
+ })
+}
+
+// 删除部门
+export function delDept(deptId) {
+ return request({
+ url: '/system/dept/' + deptId,
+ method: 'delete'
+ })
+}
\ No newline at end of file
diff --git a/ruoyi-ui-vue3/src/api/system/dict/data.js b/ruoyi-ui-vue3/src/api/system/dict/data.js
new file mode 100644
index 000000000..6c9eb79b4
--- /dev/null
+++ b/ruoyi-ui-vue3/src/api/system/dict/data.js
@@ -0,0 +1,52 @@
+import request from '@/utils/request'
+
+// 查询字典数据列表
+export function listData(query) {
+ return request({
+ url: '/system/dict/data/list',
+ method: 'get',
+ params: query
+ })
+}
+
+// 查询字典数据详细
+export function getData(dictCode) {
+ return request({
+ url: '/system/dict/data/' + dictCode,
+ method: 'get'
+ })
+}
+
+// 根据字典类型查询字典数据信息
+export function getDicts(dictType) {
+ return request({
+ url: '/system/dict/data/type/' + dictType,
+ method: 'get'
+ })
+}
+
+// 新增字典数据
+export function addData(data) {
+ return request({
+ url: '/system/dict/data',
+ method: 'post',
+ data: data
+ })
+}
+
+// 修改字典数据
+export function updateData(data) {
+ return request({
+ url: '/system/dict/data',
+ method: 'put',
+ data: data
+ })
+}
+
+// 删除字典数据
+export function delData(dictCode) {
+ return request({
+ url: '/system/dict/data/' + dictCode,
+ method: 'delete'
+ })
+}
diff --git a/ruoyi-ui-vue3/src/api/system/dict/type.js b/ruoyi-ui-vue3/src/api/system/dict/type.js
new file mode 100644
index 000000000..a0254baa7
--- /dev/null
+++ b/ruoyi-ui-vue3/src/api/system/dict/type.js
@@ -0,0 +1,60 @@
+import request from '@/utils/request'
+
+// 查询字典类型列表
+export function listType(query) {
+ return request({
+ url: '/system/dict/type/list',
+ method: 'get',
+ params: query
+ })
+}
+
+// 查询字典类型详细
+export function getType(dictId) {
+ return request({
+ url: '/system/dict/type/' + dictId,
+ method: 'get'
+ })
+}
+
+// 新增字典类型
+export function addType(data) {
+ return request({
+ url: '/system/dict/type',
+ method: 'post',
+ data: data
+ })
+}
+
+// 修改字典类型
+export function updateType(data) {
+ return request({
+ url: '/system/dict/type',
+ method: 'put',
+ data: data
+ })
+}
+
+// 删除字典类型
+export function delType(dictId) {
+ return request({
+ url: '/system/dict/type/' + dictId,
+ method: 'delete'
+ })
+}
+
+// 刷新字典缓存
+export function refreshCache() {
+ return request({
+ url: '/system/dict/type/refreshCache',
+ method: 'delete'
+ })
+}
+
+// 获取字典选择框列表
+export function optionselect() {
+ return request({
+ url: '/system/dict/type/optionselect',
+ method: 'get'
+ })
+}
diff --git a/ruoyi-ui-vue3/src/api/system/menu.js b/ruoyi-ui-vue3/src/api/system/menu.js
new file mode 100644
index 000000000..f6415c656
--- /dev/null
+++ b/ruoyi-ui-vue3/src/api/system/menu.js
@@ -0,0 +1,60 @@
+import request from '@/utils/request'
+
+// 查询菜单列表
+export function listMenu(query) {
+ return request({
+ url: '/system/menu/list',
+ method: 'get',
+ params: query
+ })
+}
+
+// 查询菜单详细
+export function getMenu(menuId) {
+ return request({
+ url: '/system/menu/' + menuId,
+ method: 'get'
+ })
+}
+
+// 查询菜单下拉树结构
+export function treeselect() {
+ return request({
+ url: '/system/menu/treeselect',
+ method: 'get'
+ })
+}
+
+// 根据角色ID查询菜单下拉树结构
+export function roleMenuTreeselect(roleId) {
+ return request({
+ url: '/system/menu/roleMenuTreeselect/' + roleId,
+ method: 'get'
+ })
+}
+
+// 新增菜单
+export function addMenu(data) {
+ return request({
+ url: '/system/menu',
+ method: 'post',
+ data: data
+ })
+}
+
+// 修改菜单
+export function updateMenu(data) {
+ return request({
+ url: '/system/menu',
+ method: 'put',
+ data: data
+ })
+}
+
+// 删除菜单
+export function delMenu(menuId) {
+ return request({
+ url: '/system/menu/' + menuId,
+ method: 'delete'
+ })
+}
\ No newline at end of file
diff --git a/ruoyi-ui-vue3/src/api/system/notice.js b/ruoyi-ui-vue3/src/api/system/notice.js
new file mode 100644
index 000000000..c274ea5ba
--- /dev/null
+++ b/ruoyi-ui-vue3/src/api/system/notice.js
@@ -0,0 +1,44 @@
+import request from '@/utils/request'
+
+// 查询公告列表
+export function listNotice(query) {
+ return request({
+ url: '/system/notice/list',
+ method: 'get',
+ params: query
+ })
+}
+
+// 查询公告详细
+export function getNotice(noticeId) {
+ return request({
+ url: '/system/notice/' + noticeId,
+ method: 'get'
+ })
+}
+
+// 新增公告
+export function addNotice(data) {
+ return request({
+ url: '/system/notice',
+ method: 'post',
+ data: data
+ })
+}
+
+// 修改公告
+export function updateNotice(data) {
+ return request({
+ url: '/system/notice',
+ method: 'put',
+ data: data
+ })
+}
+
+// 删除公告
+export function delNotice(noticeId) {
+ return request({
+ url: '/system/notice/' + noticeId,
+ method: 'delete'
+ })
+}
\ No newline at end of file
diff --git a/ruoyi-ui-vue3/src/api/system/oss.js b/ruoyi-ui-vue3/src/api/system/oss.js
new file mode 100644
index 000000000..7d8002600
--- /dev/null
+++ b/ruoyi-ui-vue3/src/api/system/oss.js
@@ -0,0 +1,27 @@
+import request from '@/utils/request'
+
+// 查询OSS对象存储列表
+export function listOss(query) {
+ return request({
+ url: '/system/oss/list',
+ method: 'get',
+ params: query
+ })
+}
+
+// 查询OSS对象基于id串
+export function listByIds(ossId) {
+ return request({
+ url: '/system/oss/listByIds/' + ossId,
+ method: 'get'
+ })
+}
+
+// 删除OSS对象存储
+export function delOss(ossId) {
+ return request({
+ url: '/system/oss/' + ossId,
+ method: 'delete'
+ })
+}
+
diff --git a/ruoyi-ui-vue3/src/api/system/ossConfig.js b/ruoyi-ui-vue3/src/api/system/ossConfig.js
new file mode 100644
index 000000000..f29076283
--- /dev/null
+++ b/ruoyi-ui-vue3/src/api/system/ossConfig.js
@@ -0,0 +1,58 @@
+import request from '@/utils/request'
+
+// 查询对象存储配置列表
+export function listOssConfig(query) {
+ return request({
+ url: '/system/oss/config/list',
+ method: 'get',
+ params: query
+ })
+}
+
+// 查询对象存储配置详细
+export function getOssConfig(ossConfigId) {
+ return request({
+ url: '/system/oss/config/' + ossConfigId,
+ method: 'get'
+ })
+}
+
+// 新增对象存储配置
+export function addOssConfig(data) {
+ return request({
+ url: '/system/oss/config',
+ method: 'post',
+ data: data
+ })
+}
+
+// 修改对象存储配置
+export function updateOssConfig(data) {
+ return request({
+ url: '/system/oss/config',
+ method: 'put',
+ data: data
+ })
+}
+
+// 删除对象存储配置
+export function delOssConfig(ossConfigId) {
+ return request({
+ url: '/system/oss/config/' + ossConfigId,
+ method: 'delete'
+ })
+}
+
+// 对象存储状态修改
+export function changeOssConfigStatus(ossConfigId, status, configKey) {
+ const data = {
+ ossConfigId,
+ status,
+ configKey
+ }
+ return request({
+ url: '/system/oss/config/changeStatus',
+ method: 'put',
+ data: data
+ })
+}
diff --git a/ruoyi-ui-vue3/src/api/system/post.js b/ruoyi-ui-vue3/src/api/system/post.js
new file mode 100644
index 000000000..1a8e9ca04
--- /dev/null
+++ b/ruoyi-ui-vue3/src/api/system/post.js
@@ -0,0 +1,44 @@
+import request from '@/utils/request'
+
+// 查询岗位列表
+export function listPost(query) {
+ return request({
+ url: '/system/post/list',
+ method: 'get',
+ params: query
+ })
+}
+
+// 查询岗位详细
+export function getPost(postId) {
+ return request({
+ url: '/system/post/' + postId,
+ method: 'get'
+ })
+}
+
+// 新增岗位
+export function addPost(data) {
+ return request({
+ url: '/system/post',
+ method: 'post',
+ data: data
+ })
+}
+
+// 修改岗位
+export function updatePost(data) {
+ return request({
+ url: '/system/post',
+ method: 'put',
+ data: data
+ })
+}
+
+// 删除岗位
+export function delPost(postId) {
+ return request({
+ url: '/system/post/' + postId,
+ method: 'delete'
+ })
+}
diff --git a/ruoyi-ui-vue3/src/api/system/role.js b/ruoyi-ui-vue3/src/api/system/role.js
new file mode 100644
index 000000000..f13e6f404
--- /dev/null
+++ b/ruoyi-ui-vue3/src/api/system/role.js
@@ -0,0 +1,119 @@
+import request from '@/utils/request'
+
+// 查询角色列表
+export function listRole(query) {
+ return request({
+ url: '/system/role/list',
+ method: 'get',
+ params: query
+ })
+}
+
+// 查询角色详细
+export function getRole(roleId) {
+ return request({
+ url: '/system/role/' + roleId,
+ method: 'get'
+ })
+}
+
+// 新增角色
+export function addRole(data) {
+ return request({
+ url: '/system/role',
+ method: 'post',
+ data: data
+ })
+}
+
+// 修改角色
+export function updateRole(data) {
+ return request({
+ url: '/system/role',
+ method: 'put',
+ data: data
+ })
+}
+
+// 角色数据权限
+export function dataScope(data) {
+ return request({
+ url: '/system/role/dataScope',
+ method: 'put',
+ data: data
+ })
+}
+
+// 角色状态修改
+export function changeRoleStatus(roleId, status) {
+ const data = {
+ roleId,
+ status
+ }
+ return request({
+ url: '/system/role/changeStatus',
+ method: 'put',
+ data: data
+ })
+}
+
+// 删除角色
+export function delRole(roleId) {
+ return request({
+ url: '/system/role/' + roleId,
+ method: 'delete'
+ })
+}
+
+// 查询角色已授权用户列表
+export function allocatedUserList(query) {
+ return request({
+ url: '/system/role/authUser/allocatedList',
+ method: 'get',
+ params: query
+ })
+}
+
+// 查询角色未授权用户列表
+export function unallocatedUserList(query) {
+ return request({
+ url: '/system/role/authUser/unallocatedList',
+ method: 'get',
+ params: query
+ })
+}
+
+// 取消用户授权角色
+export function authUserCancel(data) {
+ return request({
+ url: '/system/role/authUser/cancel',
+ method: 'put',
+ data: data
+ })
+}
+
+// 批量取消用户授权角色
+export function authUserCancelAll(data) {
+ return request({
+ url: '/system/role/authUser/cancelAll',
+ method: 'put',
+ params: data
+ })
+}
+
+// 授权用户选择
+export function authUserSelectAll(data) {
+ return request({
+ url: '/system/role/authUser/selectAll',
+ method: 'put',
+ params: data
+ })
+}
+
+// 根据角色ID查询部门树结构
+export function deptTreeSelect(roleId) {
+ return request({
+ url: '/system/role/deptTree/' + roleId,
+ method: 'get'
+ })
+}
diff --git a/ruoyi-ui-vue3/src/api/system/user.js b/ruoyi-ui-vue3/src/api/system/user.js
new file mode 100644
index 000000000..f2f76ef9f
--- /dev/null
+++ b/ruoyi-ui-vue3/src/api/system/user.js
@@ -0,0 +1,135 @@
+import request from '@/utils/request'
+import { parseStrEmpty } from "@/utils/ruoyi";
+
+// 查询用户列表
+export function listUser(query) {
+ return request({
+ url: '/system/user/list',
+ method: 'get',
+ params: query
+ })
+}
+
+// 查询用户详细
+export function getUser(userId) {
+ return request({
+ url: '/system/user/' + parseStrEmpty(userId),
+ method: 'get'
+ })
+}
+
+// 新增用户
+export function addUser(data) {
+ return request({
+ url: '/system/user',
+ method: 'post',
+ data: data
+ })
+}
+
+// 修改用户
+export function updateUser(data) {
+ return request({
+ url: '/system/user',
+ method: 'put',
+ data: data
+ })
+}
+
+// 删除用户
+export function delUser(userId) {
+ return request({
+ url: '/system/user/' + userId,
+ method: 'delete'
+ })
+}
+
+// 用户密码重置
+export function resetUserPwd(userId, password) {
+ const data = {
+ userId,
+ password
+ }
+ return request({
+ url: '/system/user/resetPwd',
+ method: 'put',
+ data: data
+ })
+}
+
+// 用户状态修改
+export function changeUserStatus(userId, status) {
+ const data = {
+ userId,
+ status
+ }
+ return request({
+ url: '/system/user/changeStatus',
+ method: 'put',
+ data: data
+ })
+}
+
+// 查询用户个人信息
+export function getUserProfile() {
+ return request({
+ url: '/system/user/profile',
+ method: 'get'
+ })
+}
+
+// 修改用户个人信息
+export function updateUserProfile(data) {
+ return request({
+ url: '/system/user/profile',
+ method: 'put',
+ data: data
+ })
+}
+
+// 用户密码重置
+export function updateUserPwd(oldPassword, newPassword) {
+ const data = {
+ oldPassword,
+ newPassword
+ }
+ return request({
+ url: '/system/user/profile/updatePwd',
+ method: 'put',
+ params: data
+ })
+}
+
+// 用户头像上传
+export function uploadAvatar(data) {
+ return request({
+ url: '/system/user/profile/avatar',
+ method: 'post',
+ data: data
+ })
+}
+
+// 查询授权角色
+export function getAuthRole(userId) {
+ return request({
+ url: '/system/user/authRole/' + userId,
+ method: 'get'
+ })
+}
+
+// 保存授权角色
+export function updateAuthRole(data) {
+ return request({
+ url: '/system/user/authRole',
+ method: 'put',
+ params: data
+ })
+}
+
+// 查询部门下拉树结构
+export function deptTreeSelect() {
+ return request({
+ url: '/system/user/deptTree',
+ method: 'get'
+ })
+}
diff --git a/ruoyi-ui-vue3/src/api/tool/gen.js b/ruoyi-ui-vue3/src/api/tool/gen.js
new file mode 100644
index 000000000..441791c2c
--- /dev/null
+++ b/ruoyi-ui-vue3/src/api/tool/gen.js
@@ -0,0 +1,85 @@
+import request from '@/utils/request'
+
+// 查询生成表数据
+export function listTable(query) {
+ return request({
+ headers: { 'datasource': localStorage.getItem("dataName") },
+ url: '/tool/gen/list',
+ method: 'get',
+ params: query
+ })
+}
+// 查询db数据库列表
+export function listDbTable(query) {
+ return request({
+ headers: { 'datasource': localStorage.getItem("dataName") },
+ url: '/tool/gen/db/list',
+ method: 'get',
+ params: query
+ })
+}
+
+// 查询表详细信息
+export function getGenTable(tableId) {
+ return request({
+ headers: { 'datasource': localStorage.getItem("dataName") },
+ url: '/tool/gen/' + tableId,
+ method: 'get'
+ })
+}
+
+// 修改代码生成信息
+export function updateGenTable(data) {
+ return request({
+ headers: { 'datasource': localStorage.getItem("dataName") },
+ url: '/tool/gen',
+ method: 'put',
+ data: data
+ })
+}
+
+// 导入表
+export function importTable(data) {
+ return request({
+ headers: { 'datasource': localStorage.getItem("dataName") },
+ url: '/tool/gen/importTable',
+ method: 'post',
+ params: data
+ })
+}
+
+// 预览生成代码
+export function previewTable(tableId) {
+ return request({
+ headers: { 'datasource': localStorage.getItem("dataName") },
+ url: '/tool/gen/preview/' + tableId,
+ method: 'get'
+ })
+}
+
+// 删除表数据
+export function delTable(tableId) {
+ return request({
+ headers: { 'datasource': localStorage.getItem("dataName") },
+ url: '/tool/gen/' + tableId,
+ method: 'delete'
+ })
+}
+
+// 生成代码(自定义路径)
+export function genCode(tableName) {
+ return request({
+ headers: { 'datasource': localStorage.getItem("dataName") },
+ url: '/tool/gen/genCode/' + tableName,
+ method: 'get'
+ })
+}
+
+// 同步数据库
+export function synchDb(tableName) {
+ return request({
+ headers: { 'datasource': localStorage.getItem("dataName") },
+ url: '/tool/gen/synchDb/' + tableName,
+ method: 'get'
+ })
+}
diff --git a/ruoyi-ui-vue3/src/assets/401_images/401.gif b/ruoyi-ui-vue3/src/assets/401_images/401.gif
new file mode 100644
index 000000000..cd6e0d943
Binary files /dev/null and b/ruoyi-ui-vue3/src/assets/401_images/401.gif differ
diff --git a/ruoyi-ui-vue3/src/assets/404_images/404.png b/ruoyi-ui-vue3/src/assets/404_images/404.png
new file mode 100644
index 000000000..3d8e2305c
Binary files /dev/null and b/ruoyi-ui-vue3/src/assets/404_images/404.png differ
diff --git a/ruoyi-ui-vue3/src/assets/404_images/404_cloud.png b/ruoyi-ui-vue3/src/assets/404_images/404_cloud.png
new file mode 100644
index 000000000..c6281d090
Binary files /dev/null and b/ruoyi-ui-vue3/src/assets/404_images/404_cloud.png differ
diff --git a/ruoyi-ui-vue3/src/assets/icons/svg/404.svg b/ruoyi-ui-vue3/src/assets/icons/svg/404.svg
new file mode 100644
index 000000000..6df50190a
--- /dev/null
+++ b/ruoyi-ui-vue3/src/assets/icons/svg/404.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ruoyi-ui-vue3/src/assets/icons/svg/bug.svg b/ruoyi-ui-vue3/src/assets/icons/svg/bug.svg
new file mode 100644
index 000000000..05a150dc3
--- /dev/null
+++ b/ruoyi-ui-vue3/src/assets/icons/svg/bug.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ruoyi-ui-vue3/src/assets/icons/svg/build.svg b/ruoyi-ui-vue3/src/assets/icons/svg/build.svg
new file mode 100644
index 000000000..97c468863
--- /dev/null
+++ b/ruoyi-ui-vue3/src/assets/icons/svg/build.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ruoyi-ui-vue3/src/assets/icons/svg/button.svg b/ruoyi-ui-vue3/src/assets/icons/svg/button.svg
new file mode 100644
index 000000000..904fddc85
--- /dev/null
+++ b/ruoyi-ui-vue3/src/assets/icons/svg/button.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ruoyi-ui-vue3/src/assets/icons/svg/cascader.svg b/ruoyi-ui-vue3/src/assets/icons/svg/cascader.svg
new file mode 100644
index 000000000..e256024f9
--- /dev/null
+++ b/ruoyi-ui-vue3/src/assets/icons/svg/cascader.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ruoyi-ui-vue3/src/assets/icons/svg/chart.svg b/ruoyi-ui-vue3/src/assets/icons/svg/chart.svg
new file mode 100644
index 000000000..27728fb0b
--- /dev/null
+++ b/ruoyi-ui-vue3/src/assets/icons/svg/chart.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ruoyi-ui-vue3/src/assets/icons/svg/checkbox.svg b/ruoyi-ui-vue3/src/assets/icons/svg/checkbox.svg
new file mode 100644
index 000000000..013fd3a27
--- /dev/null
+++ b/ruoyi-ui-vue3/src/assets/icons/svg/checkbox.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ruoyi-ui-vue3/src/assets/icons/svg/clipboard.svg b/ruoyi-ui-vue3/src/assets/icons/svg/clipboard.svg
new file mode 100644
index 000000000..90923ff62
--- /dev/null
+++ b/ruoyi-ui-vue3/src/assets/icons/svg/clipboard.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ruoyi-ui-vue3/src/assets/icons/svg/code.svg b/ruoyi-ui-vue3/src/assets/icons/svg/code.svg
new file mode 100644
index 000000000..5f9c5abd5
--- /dev/null
+++ b/ruoyi-ui-vue3/src/assets/icons/svg/code.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ruoyi-ui-vue3/src/assets/icons/svg/color.svg b/ruoyi-ui-vue3/src/assets/icons/svg/color.svg
new file mode 100644
index 000000000..44a81aab1
--- /dev/null
+++ b/ruoyi-ui-vue3/src/assets/icons/svg/color.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ruoyi-ui-vue3/src/assets/icons/svg/component.svg b/ruoyi-ui-vue3/src/assets/icons/svg/component.svg
new file mode 100644
index 000000000..29c345809
--- /dev/null
+++ b/ruoyi-ui-vue3/src/assets/icons/svg/component.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ruoyi-ui-vue3/src/assets/icons/svg/dashboard.svg b/ruoyi-ui-vue3/src/assets/icons/svg/dashboard.svg
new file mode 100644
index 000000000..5317d3702
--- /dev/null
+++ b/ruoyi-ui-vue3/src/assets/icons/svg/dashboard.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ruoyi-ui-vue3/src/assets/icons/svg/date-range.svg b/ruoyi-ui-vue3/src/assets/icons/svg/date-range.svg
new file mode 100644
index 000000000..fda571e70
--- /dev/null
+++ b/ruoyi-ui-vue3/src/assets/icons/svg/date-range.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ruoyi-ui-vue3/src/assets/icons/svg/date.svg b/ruoyi-ui-vue3/src/assets/icons/svg/date.svg
new file mode 100644
index 000000000..52dc73eec
--- /dev/null
+++ b/ruoyi-ui-vue3/src/assets/icons/svg/date.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ruoyi-ui-vue3/src/assets/icons/svg/dict.svg b/ruoyi-ui-vue3/src/assets/icons/svg/dict.svg
new file mode 100644
index 000000000..484937730
--- /dev/null
+++ b/ruoyi-ui-vue3/src/assets/icons/svg/dict.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ruoyi-ui-vue3/src/assets/icons/svg/documentation.svg b/ruoyi-ui-vue3/src/assets/icons/svg/documentation.svg
new file mode 100644
index 000000000..704312289
--- /dev/null
+++ b/ruoyi-ui-vue3/src/assets/icons/svg/documentation.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ruoyi-ui-vue3/src/assets/icons/svg/download.svg b/ruoyi-ui-vue3/src/assets/icons/svg/download.svg
new file mode 100644
index 000000000..c89695134
--- /dev/null
+++ b/ruoyi-ui-vue3/src/assets/icons/svg/download.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ruoyi-ui-vue3/src/assets/icons/svg/drag.svg b/ruoyi-ui-vue3/src/assets/icons/svg/drag.svg
new file mode 100644
index 000000000..4185d3cee
--- /dev/null
+++ b/ruoyi-ui-vue3/src/assets/icons/svg/drag.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ruoyi-ui-vue3/src/assets/icons/svg/druid.svg b/ruoyi-ui-vue3/src/assets/icons/svg/druid.svg
new file mode 100644
index 000000000..a2b4b4ed2
--- /dev/null
+++ b/ruoyi-ui-vue3/src/assets/icons/svg/druid.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ruoyi-ui-vue3/src/assets/icons/svg/edit.svg b/ruoyi-ui-vue3/src/assets/icons/svg/edit.svg
new file mode 100644
index 000000000..d26101f29
--- /dev/null
+++ b/ruoyi-ui-vue3/src/assets/icons/svg/edit.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ruoyi-ui-vue3/src/assets/icons/svg/education.svg b/ruoyi-ui-vue3/src/assets/icons/svg/education.svg
new file mode 100644
index 000000000..7bfb01d18
--- /dev/null
+++ b/ruoyi-ui-vue3/src/assets/icons/svg/education.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ruoyi-ui-vue3/src/assets/icons/svg/email.svg b/ruoyi-ui-vue3/src/assets/icons/svg/email.svg
new file mode 100644
index 000000000..74d25e21a
--- /dev/null
+++ b/ruoyi-ui-vue3/src/assets/icons/svg/email.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ruoyi-ui-vue3/src/assets/icons/svg/example.svg b/ruoyi-ui-vue3/src/assets/icons/svg/example.svg
new file mode 100644
index 000000000..46f42b532
--- /dev/null
+++ b/ruoyi-ui-vue3/src/assets/icons/svg/example.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ruoyi-ui-vue3/src/assets/icons/svg/excel.svg b/ruoyi-ui-vue3/src/assets/icons/svg/excel.svg
new file mode 100644
index 000000000..74d97b802
--- /dev/null
+++ b/ruoyi-ui-vue3/src/assets/icons/svg/excel.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ruoyi-ui-vue3/src/assets/icons/svg/exit-fullscreen.svg b/ruoyi-ui-vue3/src/assets/icons/svg/exit-fullscreen.svg
new file mode 100644
index 000000000..485c128b6
--- /dev/null
+++ b/ruoyi-ui-vue3/src/assets/icons/svg/exit-fullscreen.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ruoyi-ui-vue3/src/assets/icons/svg/eye-open.svg b/ruoyi-ui-vue3/src/assets/icons/svg/eye-open.svg
new file mode 100644
index 000000000..88dcc98e6
--- /dev/null
+++ b/ruoyi-ui-vue3/src/assets/icons/svg/eye-open.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ruoyi-ui-vue3/src/assets/icons/svg/eye.svg b/ruoyi-ui-vue3/src/assets/icons/svg/eye.svg
new file mode 100644
index 000000000..16ed2d872
--- /dev/null
+++ b/ruoyi-ui-vue3/src/assets/icons/svg/eye.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ruoyi-ui-vue3/src/assets/icons/svg/form.svg b/ruoyi-ui-vue3/src/assets/icons/svg/form.svg
new file mode 100644
index 000000000..dcbaa185a
--- /dev/null
+++ b/ruoyi-ui-vue3/src/assets/icons/svg/form.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ruoyi-ui-vue3/src/assets/icons/svg/fullscreen.svg b/ruoyi-ui-vue3/src/assets/icons/svg/fullscreen.svg
new file mode 100644
index 000000000..0e86b6fa8
--- /dev/null
+++ b/ruoyi-ui-vue3/src/assets/icons/svg/fullscreen.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ruoyi-ui-vue3/src/assets/icons/svg/github.svg b/ruoyi-ui-vue3/src/assets/icons/svg/github.svg
new file mode 100644
index 000000000..db0a0d430
--- /dev/null
+++ b/ruoyi-ui-vue3/src/assets/icons/svg/github.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ruoyi-ui-vue3/src/assets/icons/svg/guide.svg b/ruoyi-ui-vue3/src/assets/icons/svg/guide.svg
new file mode 100644
index 000000000..b27100179
--- /dev/null
+++ b/ruoyi-ui-vue3/src/assets/icons/svg/guide.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ruoyi-ui-vue3/src/assets/icons/svg/icon.svg b/ruoyi-ui-vue3/src/assets/icons/svg/icon.svg
new file mode 100644
index 000000000..82be8eeed
--- /dev/null
+++ b/ruoyi-ui-vue3/src/assets/icons/svg/icon.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ruoyi-ui-vue3/src/assets/icons/svg/input.svg b/ruoyi-ui-vue3/src/assets/icons/svg/input.svg
new file mode 100644
index 000000000..ab91381e6
--- /dev/null
+++ b/ruoyi-ui-vue3/src/assets/icons/svg/input.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ruoyi-ui-vue3/src/assets/icons/svg/international.svg b/ruoyi-ui-vue3/src/assets/icons/svg/international.svg
new file mode 100644
index 000000000..e9b56eee2
--- /dev/null
+++ b/ruoyi-ui-vue3/src/assets/icons/svg/international.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ruoyi-ui-vue3/src/assets/icons/svg/job.svg b/ruoyi-ui-vue3/src/assets/icons/svg/job.svg
new file mode 100644
index 000000000..2a93a2519
--- /dev/null
+++ b/ruoyi-ui-vue3/src/assets/icons/svg/job.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ruoyi-ui-vue3/src/assets/icons/svg/language.svg b/ruoyi-ui-vue3/src/assets/icons/svg/language.svg
new file mode 100644
index 000000000..0082b577a
--- /dev/null
+++ b/ruoyi-ui-vue3/src/assets/icons/svg/language.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ruoyi-ui-vue3/src/assets/icons/svg/link.svg b/ruoyi-ui-vue3/src/assets/icons/svg/link.svg
new file mode 100644
index 000000000..48197ba4d
--- /dev/null
+++ b/ruoyi-ui-vue3/src/assets/icons/svg/link.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ruoyi-ui-vue3/src/assets/icons/svg/list.svg b/ruoyi-ui-vue3/src/assets/icons/svg/list.svg
new file mode 100644
index 000000000..20259eddb
--- /dev/null
+++ b/ruoyi-ui-vue3/src/assets/icons/svg/list.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ruoyi-ui-vue3/src/assets/icons/svg/lock.svg b/ruoyi-ui-vue3/src/assets/icons/svg/lock.svg
new file mode 100644
index 000000000..74fee543d
--- /dev/null
+++ b/ruoyi-ui-vue3/src/assets/icons/svg/lock.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ruoyi-ui-vue3/src/assets/icons/svg/log.svg b/ruoyi-ui-vue3/src/assets/icons/svg/log.svg
new file mode 100644
index 000000000..d879d33b6
--- /dev/null
+++ b/ruoyi-ui-vue3/src/assets/icons/svg/log.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ruoyi-ui-vue3/src/assets/icons/svg/logininfor.svg b/ruoyi-ui-vue3/src/assets/icons/svg/logininfor.svg
new file mode 100644
index 000000000..267f84474
--- /dev/null
+++ b/ruoyi-ui-vue3/src/assets/icons/svg/logininfor.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ruoyi-ui-vue3/src/assets/icons/svg/message.svg b/ruoyi-ui-vue3/src/assets/icons/svg/message.svg
new file mode 100644
index 000000000..14ca81728
--- /dev/null
+++ b/ruoyi-ui-vue3/src/assets/icons/svg/message.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ruoyi-ui-vue3/src/assets/icons/svg/money.svg b/ruoyi-ui-vue3/src/assets/icons/svg/money.svg
new file mode 100644
index 000000000..c1580de10
--- /dev/null
+++ b/ruoyi-ui-vue3/src/assets/icons/svg/money.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ruoyi-ui-vue3/src/assets/icons/svg/monitor.svg b/ruoyi-ui-vue3/src/assets/icons/svg/monitor.svg
new file mode 100644
index 000000000..bc308cb0f
--- /dev/null
+++ b/ruoyi-ui-vue3/src/assets/icons/svg/monitor.svg
@@ -0,0 +1,2 @@
+
\ No newline at end of file
diff --git a/ruoyi-ui-vue3/src/assets/icons/svg/nested.svg b/ruoyi-ui-vue3/src/assets/icons/svg/nested.svg
new file mode 100644
index 000000000..06713a86c
--- /dev/null
+++ b/ruoyi-ui-vue3/src/assets/icons/svg/nested.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ruoyi-ui-vue3/src/assets/icons/svg/number.svg b/ruoyi-ui-vue3/src/assets/icons/svg/number.svg
new file mode 100644
index 000000000..ad5ce9af2
--- /dev/null
+++ b/ruoyi-ui-vue3/src/assets/icons/svg/number.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ruoyi-ui-vue3/src/assets/icons/svg/online.svg b/ruoyi-ui-vue3/src/assets/icons/svg/online.svg
new file mode 100644
index 000000000..330a20293
--- /dev/null
+++ b/ruoyi-ui-vue3/src/assets/icons/svg/online.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ruoyi-ui-vue3/src/assets/icons/svg/password.svg b/ruoyi-ui-vue3/src/assets/icons/svg/password.svg
new file mode 100644
index 000000000..6c64defe3
--- /dev/null
+++ b/ruoyi-ui-vue3/src/assets/icons/svg/password.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ruoyi-ui-vue3/src/assets/icons/svg/pdf.svg b/ruoyi-ui-vue3/src/assets/icons/svg/pdf.svg
new file mode 100644
index 000000000..957aa0cc3
--- /dev/null
+++ b/ruoyi-ui-vue3/src/assets/icons/svg/pdf.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ruoyi-ui-vue3/src/assets/icons/svg/people.svg b/ruoyi-ui-vue3/src/assets/icons/svg/people.svg
new file mode 100644
index 000000000..2bd54aeb7
--- /dev/null
+++ b/ruoyi-ui-vue3/src/assets/icons/svg/people.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ruoyi-ui-vue3/src/assets/icons/svg/peoples.svg b/ruoyi-ui-vue3/src/assets/icons/svg/peoples.svg
new file mode 100644
index 000000000..aab852e52
--- /dev/null
+++ b/ruoyi-ui-vue3/src/assets/icons/svg/peoples.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ruoyi-ui-vue3/src/assets/icons/svg/phone.svg b/ruoyi-ui-vue3/src/assets/icons/svg/phone.svg
new file mode 100644
index 000000000..ab8e8c4e5
--- /dev/null
+++ b/ruoyi-ui-vue3/src/assets/icons/svg/phone.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ruoyi-ui-vue3/src/assets/icons/svg/post.svg b/ruoyi-ui-vue3/src/assets/icons/svg/post.svg
new file mode 100644
index 000000000..2922c613b
--- /dev/null
+++ b/ruoyi-ui-vue3/src/assets/icons/svg/post.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ruoyi-ui-vue3/src/assets/icons/svg/qq.svg b/ruoyi-ui-vue3/src/assets/icons/svg/qq.svg
new file mode 100644
index 000000000..ee13d4ec2
--- /dev/null
+++ b/ruoyi-ui-vue3/src/assets/icons/svg/qq.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ruoyi-ui-vue3/src/assets/icons/svg/question.svg b/ruoyi-ui-vue3/src/assets/icons/svg/question.svg
new file mode 100644
index 000000000..cf75bd4be
--- /dev/null
+++ b/ruoyi-ui-vue3/src/assets/icons/svg/question.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ruoyi-ui-vue3/src/assets/icons/svg/radio.svg b/ruoyi-ui-vue3/src/assets/icons/svg/radio.svg
new file mode 100644
index 000000000..0cde34521
--- /dev/null
+++ b/ruoyi-ui-vue3/src/assets/icons/svg/radio.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ruoyi-ui-vue3/src/assets/icons/svg/rate.svg b/ruoyi-ui-vue3/src/assets/icons/svg/rate.svg
new file mode 100644
index 000000000..aa3b14d7d
--- /dev/null
+++ b/ruoyi-ui-vue3/src/assets/icons/svg/rate.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ruoyi-ui-vue3/src/assets/icons/svg/redis-list.svg b/ruoyi-ui-vue3/src/assets/icons/svg/redis-list.svg
new file mode 100644
index 000000000..98a15b2a6
--- /dev/null
+++ b/ruoyi-ui-vue3/src/assets/icons/svg/redis-list.svg
@@ -0,0 +1,2 @@
+
\ No newline at end of file
diff --git a/ruoyi-ui-vue3/src/assets/icons/svg/redis.svg b/ruoyi-ui-vue3/src/assets/icons/svg/redis.svg
new file mode 100644
index 000000000..2f1d62dfc
--- /dev/null
+++ b/ruoyi-ui-vue3/src/assets/icons/svg/redis.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ruoyi-ui-vue3/src/assets/icons/svg/row.svg b/ruoyi-ui-vue3/src/assets/icons/svg/row.svg
new file mode 100644
index 000000000..078099222
--- /dev/null
+++ b/ruoyi-ui-vue3/src/assets/icons/svg/row.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ruoyi-ui-vue3/src/assets/icons/svg/search.svg b/ruoyi-ui-vue3/src/assets/icons/svg/search.svg
new file mode 100644
index 000000000..84233ddaa
--- /dev/null
+++ b/ruoyi-ui-vue3/src/assets/icons/svg/search.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ruoyi-ui-vue3/src/assets/icons/svg/select.svg b/ruoyi-ui-vue3/src/assets/icons/svg/select.svg
new file mode 100644
index 000000000..d6283828b
--- /dev/null
+++ b/ruoyi-ui-vue3/src/assets/icons/svg/select.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ruoyi-ui-vue3/src/assets/icons/svg/server.svg b/ruoyi-ui-vue3/src/assets/icons/svg/server.svg
new file mode 100644
index 000000000..eb287e36c
--- /dev/null
+++ b/ruoyi-ui-vue3/src/assets/icons/svg/server.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ruoyi-ui-vue3/src/assets/icons/svg/shopping.svg b/ruoyi-ui-vue3/src/assets/icons/svg/shopping.svg
new file mode 100644
index 000000000..87513e7c5
--- /dev/null
+++ b/ruoyi-ui-vue3/src/assets/icons/svg/shopping.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ruoyi-ui-vue3/src/assets/icons/svg/size.svg b/ruoyi-ui-vue3/src/assets/icons/svg/size.svg
new file mode 100644
index 000000000..ddb25b8d5
--- /dev/null
+++ b/ruoyi-ui-vue3/src/assets/icons/svg/size.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ruoyi-ui-vue3/src/assets/icons/svg/skill.svg b/ruoyi-ui-vue3/src/assets/icons/svg/skill.svg
new file mode 100644
index 000000000..a3b731218
--- /dev/null
+++ b/ruoyi-ui-vue3/src/assets/icons/svg/skill.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ruoyi-ui-vue3/src/assets/icons/svg/slider.svg b/ruoyi-ui-vue3/src/assets/icons/svg/slider.svg
new file mode 100644
index 000000000..fbe4f39f0
--- /dev/null
+++ b/ruoyi-ui-vue3/src/assets/icons/svg/slider.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ruoyi-ui-vue3/src/assets/icons/svg/star.svg b/ruoyi-ui-vue3/src/assets/icons/svg/star.svg
new file mode 100644
index 000000000..6cf86e66a
--- /dev/null
+++ b/ruoyi-ui-vue3/src/assets/icons/svg/star.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ruoyi-ui-vue3/src/assets/icons/svg/swagger.svg b/ruoyi-ui-vue3/src/assets/icons/svg/swagger.svg
new file mode 100644
index 000000000..05d4e7bce
--- /dev/null
+++ b/ruoyi-ui-vue3/src/assets/icons/svg/swagger.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ruoyi-ui-vue3/src/assets/icons/svg/switch.svg b/ruoyi-ui-vue3/src/assets/icons/svg/switch.svg
new file mode 100644
index 000000000..0ba61e38d
--- /dev/null
+++ b/ruoyi-ui-vue3/src/assets/icons/svg/switch.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ruoyi-ui-vue3/src/assets/icons/svg/system.svg b/ruoyi-ui-vue3/src/assets/icons/svg/system.svg
new file mode 100644
index 000000000..5992593e0
--- /dev/null
+++ b/ruoyi-ui-vue3/src/assets/icons/svg/system.svg
@@ -0,0 +1,2 @@
+
\ No newline at end of file
diff --git a/ruoyi-ui-vue3/src/assets/icons/svg/tab.svg b/ruoyi-ui-vue3/src/assets/icons/svg/tab.svg
new file mode 100644
index 000000000..b4b48e480
--- /dev/null
+++ b/ruoyi-ui-vue3/src/assets/icons/svg/tab.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ruoyi-ui-vue3/src/assets/icons/svg/table.svg b/ruoyi-ui-vue3/src/assets/icons/svg/table.svg
new file mode 100644
index 000000000..0e3dc9dea
--- /dev/null
+++ b/ruoyi-ui-vue3/src/assets/icons/svg/table.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ruoyi-ui-vue3/src/assets/icons/svg/textarea.svg b/ruoyi-ui-vue3/src/assets/icons/svg/textarea.svg
new file mode 100644
index 000000000..2709f292e
--- /dev/null
+++ b/ruoyi-ui-vue3/src/assets/icons/svg/textarea.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ruoyi-ui-vue3/src/assets/icons/svg/theme.svg b/ruoyi-ui-vue3/src/assets/icons/svg/theme.svg
new file mode 100644
index 000000000..5982a2f78
--- /dev/null
+++ b/ruoyi-ui-vue3/src/assets/icons/svg/theme.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ruoyi-ui-vue3/src/assets/icons/svg/time-range.svg b/ruoyi-ui-vue3/src/assets/icons/svg/time-range.svg
new file mode 100644
index 000000000..13c1202bd
--- /dev/null
+++ b/ruoyi-ui-vue3/src/assets/icons/svg/time-range.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ruoyi-ui-vue3/src/assets/icons/svg/time.svg b/ruoyi-ui-vue3/src/assets/icons/svg/time.svg
new file mode 100644
index 000000000..b376e32a6
--- /dev/null
+++ b/ruoyi-ui-vue3/src/assets/icons/svg/time.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ruoyi-ui-vue3/src/assets/icons/svg/tool.svg b/ruoyi-ui-vue3/src/assets/icons/svg/tool.svg
new file mode 100644
index 000000000..48e0e3573
--- /dev/null
+++ b/ruoyi-ui-vue3/src/assets/icons/svg/tool.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ruoyi-ui-vue3/src/assets/icons/svg/tree-table.svg b/ruoyi-ui-vue3/src/assets/icons/svg/tree-table.svg
new file mode 100644
index 000000000..8aafdb829
--- /dev/null
+++ b/ruoyi-ui-vue3/src/assets/icons/svg/tree-table.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ruoyi-ui-vue3/src/assets/icons/svg/tree.svg b/ruoyi-ui-vue3/src/assets/icons/svg/tree.svg
new file mode 100644
index 000000000..dd4b7dd22
--- /dev/null
+++ b/ruoyi-ui-vue3/src/assets/icons/svg/tree.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ruoyi-ui-vue3/src/assets/icons/svg/upload.svg b/ruoyi-ui-vue3/src/assets/icons/svg/upload.svg
new file mode 100644
index 000000000..bae49c0a5
--- /dev/null
+++ b/ruoyi-ui-vue3/src/assets/icons/svg/upload.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ruoyi-ui-vue3/src/assets/icons/svg/user.svg b/ruoyi-ui-vue3/src/assets/icons/svg/user.svg
new file mode 100644
index 000000000..0ba0716a6
--- /dev/null
+++ b/ruoyi-ui-vue3/src/assets/icons/svg/user.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ruoyi-ui-vue3/src/assets/icons/svg/validCode.svg b/ruoyi-ui-vue3/src/assets/icons/svg/validCode.svg
new file mode 100644
index 000000000..cfb10214c
--- /dev/null
+++ b/ruoyi-ui-vue3/src/assets/icons/svg/validCode.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ruoyi-ui-vue3/src/assets/icons/svg/wechat.svg b/ruoyi-ui-vue3/src/assets/icons/svg/wechat.svg
new file mode 100644
index 000000000..c586e5511
--- /dev/null
+++ b/ruoyi-ui-vue3/src/assets/icons/svg/wechat.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ruoyi-ui-vue3/src/assets/icons/svg/zip.svg b/ruoyi-ui-vue3/src/assets/icons/svg/zip.svg
new file mode 100644
index 000000000..f806fc482
--- /dev/null
+++ b/ruoyi-ui-vue3/src/assets/icons/svg/zip.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ruoyi-ui-vue3/src/assets/images/dark.svg b/ruoyi-ui-vue3/src/assets/images/dark.svg
new file mode 100644
index 000000000..f646bd7ea
--- /dev/null
+++ b/ruoyi-ui-vue3/src/assets/images/dark.svg
@@ -0,0 +1,39 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/ruoyi-ui-vue3/src/assets/images/light.svg b/ruoyi-ui-vue3/src/assets/images/light.svg
new file mode 100644
index 000000000..ab7cc088f
--- /dev/null
+++ b/ruoyi-ui-vue3/src/assets/images/light.svg
@@ -0,0 +1,39 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/ruoyi-ui-vue3/src/assets/images/login-background.jpg b/ruoyi-ui-vue3/src/assets/images/login-background.jpg
new file mode 100644
index 000000000..fa6408b95
Binary files /dev/null and b/ruoyi-ui-vue3/src/assets/images/login-background.jpg differ
diff --git a/ruoyi-ui-vue3/src/assets/images/profile.jpg b/ruoyi-ui-vue3/src/assets/images/profile.jpg
new file mode 100644
index 000000000..f4fde57de
Binary files /dev/null and b/ruoyi-ui-vue3/src/assets/images/profile.jpg differ
diff --git a/ruoyi-ui-vue3/src/assets/logo/logo.png b/ruoyi-ui-vue3/src/assets/logo/logo.png
new file mode 100644
index 000000000..3f919d85a
Binary files /dev/null and b/ruoyi-ui-vue3/src/assets/logo/logo.png differ
diff --git a/ruoyi-ui-vue3/src/assets/styles/btn.scss b/ruoyi-ui-vue3/src/assets/styles/btn.scss
new file mode 100644
index 000000000..3590d8d2d
--- /dev/null
+++ b/ruoyi-ui-vue3/src/assets/styles/btn.scss
@@ -0,0 +1,99 @@
+@import './variables.module.scss';
+
+@mixin colorBtn($color) {
+ background: $color;
+
+ &:hover {
+ color: $color;
+
+ &:before,
+ &:after {
+ background: $color;
+ }
+ }
+}
+
+.blue-btn {
+ @include colorBtn($blue)
+}
+
+.light-blue-btn {
+ @include colorBtn($light-blue)
+}
+
+.red-btn {
+ @include colorBtn($red)
+}
+
+.pink-btn {
+ @include colorBtn($pink)
+}
+
+.green-btn {
+ @include colorBtn($green)
+}
+
+.tiffany-btn {
+ @include colorBtn($tiffany)
+}
+
+.yellow-btn {
+ @include colorBtn($yellow)
+}
+
+.pan-btn {
+ font-size: 14px;
+ color: #fff;
+ padding: 14px 36px;
+ border-radius: 8px;
+ border: none;
+ outline: none;
+ transition: 600ms ease all;
+ position: relative;
+ display: inline-block;
+
+ &:hover {
+ background: #fff;
+
+ &:before,
+ &:after {
+ width: 100%;
+ transition: 600ms ease all;
+ }
+ }
+
+ &:before,
+ &:after {
+ content: '';
+ position: absolute;
+ top: 0;
+ right: 0;
+ height: 2px;
+ width: 0;
+ transition: 400ms ease all;
+ }
+
+ &::after {
+ right: inherit;
+ top: inherit;
+ left: 0;
+ bottom: 0;
+ }
+}
+
+.custom-button {
+ display: inline-block;
+ line-height: 1;
+ white-space: nowrap;
+ cursor: pointer;
+ background: #fff;
+ color: #fff;
+ -webkit-appearance: none;
+ text-align: center;
+ box-sizing: border-box;
+ outline: 0;
+ margin: 0;
+ padding: 10px 15px;
+ font-size: 14px;
+ border-radius: 4px;
+}
diff --git a/ruoyi-ui-vue3/src/assets/styles/element-ui.scss b/ruoyi-ui-vue3/src/assets/styles/element-ui.scss
new file mode 100644
index 000000000..0f175f25e
--- /dev/null
+++ b/ruoyi-ui-vue3/src/assets/styles/element-ui.scss
@@ -0,0 +1,96 @@
+// cover some element-ui styles
+
+.el-breadcrumb__inner,
+.el-breadcrumb__inner a {
+ font-weight: 400 !important;
+}
+
+.el-upload {
+ input[type="file"] {
+ display: none !important;
+ }
+}
+
+.el-upload__input {
+ display: none;
+}
+
+.cell {
+ .el-tag {
+ margin-right: 0px;
+ }
+}
+
+.small-padding {
+ .cell {
+ padding-left: 5px;
+ padding-right: 5px;
+ }
+}
+
+.fixed-width {
+ .el-button--mini {
+ padding: 7px 10px;
+ width: 60px;
+ }
+}
+
+.status-col {
+ .cell {
+ padding: 0 10px;
+ text-align: center;
+
+ .el-tag {
+ margin-right: 0px;
+ }
+ }
+}
+
+// to fixed https://github.com/ElemeFE/element/issues/2461
+.el-dialog {
+ transform: none;
+ left: 0;
+ position: relative;
+ margin: 0 auto;
+}
+
+// refine element ui upload
+.upload-container {
+ .el-upload {
+ width: 100%;
+
+ .el-upload-dragger {
+ width: 100%;
+ height: 200px;
+ }
+ }
+}
+
+// dropdown
+.el-dropdown-menu {
+ a {
+ display: block
+ }
+}
+
+// fix date-picker ui bug in filter-item
+.el-range-editor.el-input__inner {
+ display: inline-flex !important;
+}
+
+// to fix el-date-picker css style
+.el-range-separator {
+ box-sizing: content-box;
+}
+
+.el-menu--collapse
+ > div
+ > .el-submenu
+ > .el-submenu__title
+ .el-submenu__icon-arrow {
+ display: none;
+}
+
+.el-dropdown .el-dropdown-link{
+ color: var(--el-color-primary) !important;
+}
\ No newline at end of file
diff --git a/ruoyi-ui-vue3/src/assets/styles/index.scss b/ruoyi-ui-vue3/src/assets/styles/index.scss
new file mode 100644
index 000000000..2b8dca521
--- /dev/null
+++ b/ruoyi-ui-vue3/src/assets/styles/index.scss
@@ -0,0 +1,184 @@
+@import './variables.module.scss';
+@import './mixin.scss';
+@import './transition.scss';
+@import './element-ui.scss';
+@import './sidebar.scss';
+@import './btn.scss';
+@import './ruoyi.scss';
+
+body {
+ height: 100%;
+ margin: 0;
+ -moz-osx-font-smoothing: grayscale;
+ -webkit-font-smoothing: antialiased;
+ text-rendering: optimizeLegibility;
+ font-family: Helvetica Neue, Helvetica, PingFang SC, Hiragino Sans GB, Microsoft YaHei, Arial, sans-serif;
+}
+
+label {
+ font-weight: 700;
+}
+
+html {
+ height: 100%;
+ box-sizing: border-box;
+}
+
+#app {
+ height: 100%;
+}
+
+*,
+*:before,
+*:after {
+ box-sizing: inherit;
+}
+
+.no-padding {
+ padding: 0px !important;
+}
+
+.padding-content {
+ padding: 4px 0;
+}
+
+a:focus,
+a:active {
+ outline: none;
+}
+
+a,
+a:focus,
+a:hover {
+ cursor: pointer;
+ color: inherit;
+ text-decoration: none;
+}
+
+div:focus {
+ outline: none;
+}
+
+.fr {
+ float: right;
+}
+
+.fl {
+ float: left;
+}
+
+.pr-5 {
+ padding-right: 5px;
+}
+
+.pl-5 {
+ padding-left: 5px;
+}
+
+.block {
+ display: block;
+}
+
+.pointer {
+ cursor: pointer;
+}
+
+.inlineBlock {
+ display: block;
+}
+
+.clearfix {
+ &:after {
+ visibility: hidden;
+ display: block;
+ font-size: 0;
+ content: " ";
+ clear: both;
+ height: 0;
+ }
+}
+
+aside {
+ background: #eef1f6;
+ padding: 8px 24px;
+ margin-bottom: 20px;
+ border-radius: 2px;
+ display: block;
+ line-height: 32px;
+ font-size: 16px;
+ font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen, Ubuntu, Cantarell, "Fira Sans", "Droid Sans", "Helvetica Neue", sans-serif;
+ color: #2c3e50;
+ -webkit-font-smoothing: antialiased;
+ -moz-osx-font-smoothing: grayscale;
+
+ a {
+ color: #337ab7;
+ cursor: pointer;
+
+ &:hover {
+ color: rgb(32, 160, 255);
+ }
+ }
+}
+
+//main-container全局样式
+.app-container {
+ padding: 20px;
+}
+
+.components-container {
+ margin: 30px 50px;
+ position: relative;
+}
+
+.pagination-container {
+ margin-top: 30px;
+}
+
+.text-center {
+ text-align: center
+}
+
+.sub-navbar {
+ height: 50px;
+ line-height: 50px;
+ position: relative;
+ width: 100%;
+ text-align: right;
+ padding-right: 20px;
+ transition: 600ms ease position;
+ background: linear-gradient(90deg, rgba(32, 182, 249, 1) 0%, rgba(32, 182, 249, 1) 0%, rgba(33, 120, 241, 1) 100%, rgba(33, 120, 241, 1) 100%);
+
+ .subtitle {
+ font-size: 20px;
+ color: #fff;
+ }
+
+ &.draft {
+ background: #d0d0d0;
+ }
+
+ &.deleted {
+ background: #d0d0d0;
+ }
+}
+
+.link-type,
+.link-type:focus {
+ color: #337ab7;
+ cursor: pointer;
+
+ &:hover {
+ color: rgb(32, 160, 255);
+ }
+}
+
+.filter-container {
+ padding-bottom: 10px;
+
+ .filter-item {
+ display: inline-block;
+ vertical-align: middle;
+ margin-bottom: 10px;
+ }
+}
diff --git a/ruoyi-ui-vue3/src/assets/styles/mixin.scss b/ruoyi-ui-vue3/src/assets/styles/mixin.scss
new file mode 100644
index 000000000..06fa06125
--- /dev/null
+++ b/ruoyi-ui-vue3/src/assets/styles/mixin.scss
@@ -0,0 +1,66 @@
+@mixin clearfix {
+ &:after {
+ content: "";
+ display: table;
+ clear: both;
+ }
+}
+
+@mixin scrollBar {
+ &::-webkit-scrollbar-track-piece {
+ background: #d3dce6;
+ }
+
+ &::-webkit-scrollbar {
+ width: 6px;
+ }
+
+ &::-webkit-scrollbar-thumb {
+ background: #99a9bf;
+ border-radius: 20px;
+ }
+}
+
+@mixin relative {
+ position: relative;
+ width: 100%;
+ height: 100%;
+}
+
+@mixin pct($pct) {
+ width: #{$pct};
+ position: relative;
+ margin: 0 auto;
+}
+
+@mixin triangle($width, $height, $color, $direction) {
+ $width: $width/2;
+ $color-border-style: $height solid $color;
+ $transparent-border-style: $width solid transparent;
+ height: 0;
+ width: 0;
+
+ @if $direction==up {
+ border-bottom: $color-border-style;
+ border-left: $transparent-border-style;
+ border-right: $transparent-border-style;
+ }
+
+ @else if $direction==right {
+ border-left: $color-border-style;
+ border-top: $transparent-border-style;
+ border-bottom: $transparent-border-style;
+ }
+
+ @else if $direction==down {
+ border-top: $color-border-style;
+ border-left: $transparent-border-style;
+ border-right: $transparent-border-style;
+ }
+
+ @else if $direction==left {
+ border-right: $color-border-style;
+ border-top: $transparent-border-style;
+ border-bottom: $transparent-border-style;
+ }
+}
diff --git a/ruoyi-ui-vue3/src/assets/styles/ruoyi.scss b/ruoyi-ui-vue3/src/assets/styles/ruoyi.scss
new file mode 100644
index 000000000..88f37027f
--- /dev/null
+++ b/ruoyi-ui-vue3/src/assets/styles/ruoyi.scss
@@ -0,0 +1,277 @@
+ /**
+ * 通用css样式布局处理
+ * Copyright (c) 2019 ruoyi
+ */
+
+ /** 基础通用 **/
+.pt5 {
+ padding-top: 5px;
+}
+.pr5 {
+ padding-right: 5px;
+}
+.pb5 {
+ padding-bottom: 5px;
+}
+.mt5 {
+ margin-top: 5px;
+}
+.mr5 {
+ margin-right: 5px;
+}
+.mb5 {
+ margin-bottom: 5px;
+}
+.mb8 {
+ margin-bottom: 8px;
+}
+.ml5 {
+ margin-left: 5px;
+}
+.mt10 {
+ margin-top: 10px;
+}
+.mr10 {
+ margin-right: 10px;
+}
+.mb10 {
+ margin-bottom: 10px;
+}
+.ml10 {
+ margin-left: 10px;
+}
+.mt20 {
+ margin-top: 20px;
+}
+.mr20 {
+ margin-right: 20px;
+}
+.mb20 {
+ margin-bottom: 20px;
+}
+.ml20 {
+ margin-left: 20px;
+}
+
+.h1, .h2, .h3, .h4, .h5, .h6, h1, h2, h3, h4, h5, h6 {
+ font-family: inherit;
+ font-weight: 500;
+ line-height: 1.1;
+ color: inherit;
+}
+
+.el-form .el-form-item__label {
+ font-weight: 700;
+}
+.el-dialog:not(.is-fullscreen) {
+ margin-top: 6vh !important;
+}
+
+.el-dialog.scrollbar .el-dialog__body {
+ overflow: auto;
+ overflow-x: hidden;
+ max-height: 70vh;
+ padding: 10px 20px 0;
+}
+
+.el-table {
+ .el-table__header-wrapper, .el-table__fixed-header-wrapper {
+ th {
+ word-break: break-word;
+ background-color: #f8f8f9 !important;
+ color: #515a6e;
+ height: 40px !important;
+ font-size: 13px;
+ }
+ }
+ .el-table__body-wrapper {
+ .el-button [class*="el-icon-"] + span {
+ margin-left: 1px;
+ }
+ }
+}
+
+/** 表单布局 **/
+.form-header {
+ font-size:15px;
+ color:#6379bb;
+ border-bottom:1px solid #ddd;
+ margin:8px 10px 25px 10px;
+ padding-bottom:5px
+}
+
+/** 表格布局 **/
+.pagination-container {
+ // position: relative;
+ height: 25px;
+ margin-bottom: 10px;
+ margin-top: 15px;
+ padding: 10px 20px !important;
+}
+
+/* tree border */
+.tree-border {
+ margin-top: 5px;
+ border: 1px solid #e5e6e7;
+ background: #FFFFFF none;
+ border-radius:4px;
+ width: 100%;
+}
+
+.pagination-container .el-pagination {
+ right: 0;
+ position: absolute;
+}
+
+@media ( max-width : 768px) {
+ .pagination-container .el-pagination > .el-pagination__jump {
+ display: none !important;
+ }
+ .pagination-container .el-pagination > .el-pagination__sizes {
+ display: none !important;
+ }
+}
+
+.el-table .fixed-width .el-button--small {
+ padding-left: 0;
+ padding-right: 0;
+ width: inherit;
+}
+
+/** 表格更多操作下拉样式 */
+.el-table .el-dropdown-link {
+ cursor: pointer;
+ color: #409EFF;
+ margin-left: 10px;
+}
+
+.el-table .el-dropdown, .el-icon-arrow-down {
+ font-size: 12px;
+}
+
+.el-tree-node__content > .el-checkbox {
+ margin-right: 8px;
+}
+
+.list-group-striped > .list-group-item {
+ border-left: 0;
+ border-right: 0;
+ border-radius: 0;
+ padding-left: 0;
+ padding-right: 0;
+}
+
+.list-group {
+ padding-left: 0px;
+ list-style: none;
+}
+
+.list-group-item {
+ border-bottom: 1px solid #e7eaec;
+ border-top: 1px solid #e7eaec;
+ margin-bottom: -1px;
+ padding: 11px 0px;
+ font-size: 13px;
+}
+
+.pull-right {
+ float: right !important;
+}
+
+.el-card__header {
+ padding: 14px 15px 7px !important;
+ min-height: 40px;
+}
+
+.el-card__body {
+ padding: 15px 20px 20px 20px !important;
+}
+
+.card-box {
+ padding-right: 15px;
+ padding-left: 15px;
+ margin-bottom: 10px;
+}
+
+/* button color */
+.el-button--cyan.is-active,
+.el-button--cyan:active {
+ background: #20B2AA;
+ border-color: #20B2AA;
+ color: #FFFFFF;
+}
+
+.el-button--cyan:focus,
+.el-button--cyan:hover {
+ background: #48D1CC;
+ border-color: #48D1CC;
+ color: #FFFFFF;
+}
+
+.el-button--cyan {
+ background-color: #20B2AA;
+ border-color: #20B2AA;
+ color: #FFFFFF;
+}
+
+/* text color */
+.text-navy {
+ color: #1ab394;
+}
+
+.text-primary {
+ color: inherit;
+}
+
+.text-success {
+ color: #1c84c6;
+}
+
+.text-info {
+ color: #23c6c8;
+}
+
+.text-warning {
+ color: #f8ac59;
+}
+
+.text-danger {
+ color: #ed5565;
+}
+
+.text-muted {
+ color: #888888;
+}
+
+/* image */
+.img-circle {
+ border-radius: 50%;
+}
+
+.img-lg {
+ width: 120px;
+ height: 120px;
+}
+
+.avatar-upload-preview {
+ position: absolute;
+ top: 50%;
+ transform: translate(50%, -50%);
+ width: 200px;
+ height: 200px;
+ border-radius: 50%;
+ box-shadow: 0 0 4px #ccc;
+ overflow: hidden;
+}
+
+/* 拖拽列样式 */
+.sortable-ghost{
+ opacity: .8;
+ color: #fff!important;
+ background: #42b983!important;
+}
+
+/* 表格右侧工具栏样式 */
+.top-right-btn {
+ margin-left: auto;
+}
diff --git a/ruoyi-ui-vue3/src/assets/styles/sidebar.scss b/ruoyi-ui-vue3/src/assets/styles/sidebar.scss
new file mode 100644
index 000000000..8b3c472db
--- /dev/null
+++ b/ruoyi-ui-vue3/src/assets/styles/sidebar.scss
@@ -0,0 +1,238 @@
+#app {
+
+ .main-container {
+ height: 100%;
+ transition: margin-left .28s;
+ margin-left: $base-sidebar-width;
+ position: relative;
+ }
+
+ .sidebarHide {
+ margin-left: 0!important;
+ }
+
+ .sidebar-container {
+ -webkit-transition: width .28s;
+ transition: width 0.28s;
+ width: $base-sidebar-width !important;
+ background-color: $base-menu-background;
+ height: 100%;
+ position: fixed;
+ font-size: 0px;
+ top: 0;
+ bottom: 0;
+ left: 0;
+ z-index: 1001;
+ overflow: hidden;
+ -webkit-box-shadow: 2px 0 6px rgba(0,21,41,.35);
+ box-shadow: 2px 0 6px rgba(0,21,41,.35);
+
+ // reset element-ui css
+ .horizontal-collapse-transition {
+ transition: 0s width ease-in-out, 0s padding-left ease-in-out, 0s padding-right ease-in-out;
+ }
+
+ .scrollbar-wrapper {
+ overflow-x: hidden !important;
+ }
+
+ .el-scrollbar__bar.is-vertical {
+ right: 0px;
+ }
+
+ .el-scrollbar {
+ height: 100%;
+ }
+
+ &.has-logo {
+ .el-scrollbar {
+ height: calc(100% - 50px);
+ }
+ }
+
+ .is-horizontal {
+ display: none;
+ }
+
+ a {
+ display: inline-block;
+ width: 100%;
+ overflow: hidden;
+ }
+
+ .svg-icon {
+ margin-right: 16px;
+ }
+
+ .el-menu {
+ border: none;
+ height: 100%;
+ width: 100% !important;
+ }
+
+ .el-menu-item, .menu-title {
+ overflow: hidden !important;
+ text-overflow: ellipsis !important;
+ white-space: nowrap !important;
+ }
+
+ .el-menu-item .el-menu-tooltip__trigger {
+ display: inline-block !important;
+ }
+
+ // menu hover
+ .sub-menu-title-noDropdown,
+ .el-sub-menu__title {
+ &:hover {
+ background-color: rgba(0, 0, 0, 0.06) !important;
+ }
+ }
+
+ & .theme-dark .is-active > .el-sub-menu__title {
+ color: $base-menu-color-active !important;
+ }
+
+ & .nest-menu .el-sub-menu>.el-sub-menu__title,
+ & .el-sub-menu .el-menu-item {
+ min-width: $base-sidebar-width !important;
+
+ &:hover {
+ background-color: rgba(0, 0, 0, 0.06) !important;
+ }
+ }
+
+ & .theme-dark .nest-menu .el-sub-menu>.el-sub-menu__title,
+ & .theme-dark .el-sub-menu .el-menu-item {
+ background-color: $base-sub-menu-background !important;
+
+ &:hover {
+ background-color: $base-sub-menu-hover !important;
+ }
+ }
+ }
+
+ .hideSidebar {
+ .sidebar-container {
+ width: 54px !important;
+ }
+
+ .main-container {
+ margin-left: 54px;
+ }
+
+ .sub-menu-title-noDropdown {
+ padding: 0 !important;
+ position: relative;
+
+ .el-tooltip {
+ padding: 0 !important;
+
+ .svg-icon {
+ margin-left: 20px;
+ }
+ }
+ }
+
+ .el-sub-menu {
+ overflow: hidden;
+
+ &>.el-sub-menu__title {
+ padding: 0 !important;
+
+ .svg-icon {
+ margin-left: 20px;
+ }
+
+ }
+ }
+
+ .el-menu--collapse {
+ .el-sub-menu {
+ &>.el-sub-menu__title {
+ &>span {
+ height: 0;
+ width: 0;
+ overflow: hidden;
+ visibility: hidden;
+ display: inline-block;
+ }
+ &>i {
+ height: 0;
+ width: 0;
+ overflow: hidden;
+ visibility: hidden;
+ display: inline-block;
+ }
+ }
+ }
+ }
+ }
+
+ .el-menu--collapse .el-menu .el-sub-menu {
+ min-width: $base-sidebar-width !important;
+ }
+
+ // mobile responsive
+ .mobile {
+ .main-container {
+ margin-left: 0px;
+ }
+
+ .sidebar-container {
+ transition: transform .28s;
+ width: $base-sidebar-width !important;
+ }
+
+ &.hideSidebar {
+ .sidebar-container {
+ pointer-events: none;
+ transition-duration: 0.3s;
+ transform: translate3d(-$base-sidebar-width, 0, 0);
+ }
+ }
+ }
+
+ .withoutAnimation {
+
+ .main-container,
+ .sidebar-container {
+ transition: none;
+ }
+ }
+}
+
+// when menu collapsed
+.el-menu--vertical {
+ &>.el-menu {
+ .svg-icon {
+ margin-right: 16px;
+ }
+ }
+
+ .nest-menu .el-sub-menu>.el-sub-menu__title,
+ .el-menu-item {
+ &:hover {
+ // you can use $sub-menuHover
+ background-color: rgba(0, 0, 0, 0.06) !important;
+ }
+ }
+
+ // the scroll bar appears when the sub-menu is too long
+ >.el-menu--popup {
+ max-height: 100vh;
+ overflow-y: auto;
+
+ &::-webkit-scrollbar-track-piece {
+ background: #d3dce6;
+ }
+
+ &::-webkit-scrollbar {
+ width: 6px;
+ }
+
+ &::-webkit-scrollbar-thumb {
+ background: #99a9bf;
+ border-radius: 20px;
+ }
+ }
+}
diff --git a/ruoyi-ui-vue3/src/assets/styles/transition.scss b/ruoyi-ui-vue3/src/assets/styles/transition.scss
new file mode 100644
index 000000000..073f8c6ce
--- /dev/null
+++ b/ruoyi-ui-vue3/src/assets/styles/transition.scss
@@ -0,0 +1,49 @@
+// global transition css
+
+/* fade */
+.fade-enter-active,
+.fade-leave-active {
+ transition: opacity 0.28s;
+}
+
+.fade-enter,
+.fade-leave-active {
+ opacity: 0;
+}
+
+/* fade-transform */
+.fade-transform--move,
+.fade-transform-leave-active,
+.fade-transform-enter-active {
+ transition: all .5s;
+}
+
+.fade-transform-enter {
+ opacity: 0;
+ transform: translateX(-30px);
+}
+
+.fade-transform-leave-to {
+ opacity: 0;
+ transform: translateX(30px);
+}
+
+/* breadcrumb transition */
+.breadcrumb-enter-active,
+.breadcrumb-leave-active {
+ transition: all .5s;
+}
+
+.breadcrumb-enter,
+.breadcrumb-leave-active {
+ opacity: 0;
+ transform: translateX(20px);
+}
+
+.breadcrumb-move {
+ transition: all .5s;
+}
+
+.breadcrumb-leave-active {
+ position: absolute;
+}
diff --git a/ruoyi-ui-vue3/src/assets/styles/variables.module.scss b/ruoyi-ui-vue3/src/assets/styles/variables.module.scss
new file mode 100644
index 000000000..3dbfaa7c0
--- /dev/null
+++ b/ruoyi-ui-vue3/src/assets/styles/variables.module.scss
@@ -0,0 +1,65 @@
+// base color
+$blue: #324157;
+$light-blue: #3A71A8;
+$red: #C03639;
+$pink: #E65D6E;
+$green: #30B08F;
+$tiffany: #4AB7BD;
+$yellow: #FEC171;
+$panGreen: #30B08F;
+
+// 默认菜单主题风格
+$base-menu-color: #bfcbd9;
+$base-menu-color-active: #f4f4f5;
+$base-menu-background: #304156;
+$base-logo-title-color: #ffffff;
+
+$base-menu-light-color: rgba(0, 0, 0, 0.7);
+$base-menu-light-background: #ffffff;
+$base-logo-light-title-color: #001529;
+
+$base-sub-menu-background: #1f2d3d;
+$base-sub-menu-hover: #001528;
+
+// 自定义暗色菜单风格
+/**
+$base-menu-color:hsla(0,0%,100%,.65);
+$base-menu-color-active:#fff;
+$base-menu-background:#001529;
+$base-logo-title-color: #ffffff;
+
+$base-menu-light-color:rgba(0,0,0,.70);
+$base-menu-light-background:#ffffff;
+$base-logo-light-title-color: #001529;
+
+$base-sub-menu-background:#000c17;
+$base-sub-menu-hover:#001528;
+*/
+
+$--color-primary: #409EFF;
+$--color-success: #67C23A;
+$--color-warning: #E6A23C;
+$--color-danger: #F56C6C;
+$--color-info: #909399;
+
+$base-sidebar-width: 200px;
+
+// the :export directive is the magic sauce for webpack
+// https://www.bluematador.com/blog/how-to-share-variables-between-js-and-sass
+:export {
+ menuColor: $base-menu-color;
+ menuLightColor: $base-menu-light-color;
+ menuColorActive: $base-menu-color-active;
+ menuBackground: $base-menu-background;
+ menuLightBackground: $base-menu-light-background;
+ subMenuBackground: $base-sub-menu-background;
+ subMenuHover: $base-sub-menu-hover;
+ sideBarWidth: $base-sidebar-width;
+ logoTitleColor: $base-logo-title-color;
+ logoLightTitleColor: $base-logo-light-title-color;
+ primaryColor: $--color-primary;
+ successColor: $--color-success;
+ dangerColor: $--color-danger;
+ infoColor: $--color-info;
+ warningColor: $--color-warning;
+}
diff --git a/ruoyi-ui-vue3/src/components/Breadcrumb/index.vue b/ruoyi-ui-vue3/src/components/Breadcrumb/index.vue
new file mode 100644
index 000000000..489cba150
--- /dev/null
+++ b/ruoyi-ui-vue3/src/components/Breadcrumb/index.vue
@@ -0,0 +1,66 @@
+
+
+
+
+ {{ item.meta.title }}
+ {{ item.meta.title }}
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/ruoyi-ui-vue3/src/components/DictTag/index.vue b/ruoyi-ui-vue3/src/components/DictTag/index.vue
new file mode 100644
index 000000000..3b9511796
--- /dev/null
+++ b/ruoyi-ui-vue3/src/components/DictTag/index.vue
@@ -0,0 +1,86 @@
+
+
+
+
+ {{ item.label + " " }}
+ {{ item.label + " " }}
+
+
+
+ {{ unmatchArray | handleArray }}
+
+
+
+
+
+
+
diff --git a/ruoyi-ui-vue3/src/components/Editor/index.vue b/ruoyi-ui-vue3/src/components/Editor/index.vue
new file mode 100644
index 000000000..c27635c3e
--- /dev/null
+++ b/ruoyi-ui-vue3/src/components/Editor/index.vue
@@ -0,0 +1,245 @@
+
+
+
+
+
+ $emit('update:modelValue', content)"
+ :options="options"
+ :style="styles"
+ />
+
+
+
+
+
+
+
diff --git a/ruoyi-ui-vue3/src/components/FileUpload/index.vue b/ruoyi-ui-vue3/src/components/FileUpload/index.vue
new file mode 100644
index 000000000..a5c273415
--- /dev/null
+++ b/ruoyi-ui-vue3/src/components/FileUpload/index.vue
@@ -0,0 +1,218 @@
+
+
+
+
+ 选取文件
+
+
+
+ 请上传
+ 大小不超过 {{ fileSize }}MB
+ 格式为 {{ fileType.join("/") }}
+ 的文件
+
+
+
+
+
+ {{ getFileName(file.name) }}
+
+
+ 删除
+
+
+
+
+
+
+
+
+
diff --git a/ruoyi-ui-vue3/src/components/Hamburger/index.vue b/ruoyi-ui-vue3/src/components/Hamburger/index.vue
new file mode 100644
index 000000000..18c201e69
--- /dev/null
+++ b/ruoyi-ui-vue3/src/components/Hamburger/index.vue
@@ -0,0 +1,41 @@
+
+
+
+
+
+
+
diff --git a/ruoyi-ui-vue3/src/components/HeaderSearch/index.vue b/ruoyi-ui-vue3/src/components/HeaderSearch/index.vue
new file mode 100644
index 000000000..543559be8
--- /dev/null
+++ b/ruoyi-ui-vue3/src/components/HeaderSearch/index.vue
@@ -0,0 +1,179 @@
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/ruoyi-ui-vue3/src/components/IconSelect/index.vue b/ruoyi-ui-vue3/src/components/IconSelect/index.vue
new file mode 100644
index 000000000..059e88992
--- /dev/null
+++ b/ruoyi-ui-vue3/src/components/IconSelect/index.vue
@@ -0,0 +1,111 @@
+
+
+
+
+
+
+
diff --git a/ruoyi-ui-vue3/src/components/IconSelect/requireIcons.js b/ruoyi-ui-vue3/src/components/IconSelect/requireIcons.js
new file mode 100644
index 000000000..ac22fd78e
--- /dev/null
+++ b/ruoyi-ui-vue3/src/components/IconSelect/requireIcons.js
@@ -0,0 +1,8 @@
+let icons = []
+const modules = import.meta.glob('./../../assets/icons/svg/*.svg');
+for (const path in modules) {
+ const p = path.split('assets/icons/svg/')[1].split('.svg')[0];
+ icons.push(p);
+}
+
+export default icons
\ No newline at end of file
diff --git a/ruoyi-ui-vue3/src/components/ImagePreview/index.vue b/ruoyi-ui-vue3/src/components/ImagePreview/index.vue
new file mode 100644
index 000000000..1dfe12372
--- /dev/null
+++ b/ruoyi-ui-vue3/src/components/ImagePreview/index.vue
@@ -0,0 +1,85 @@
+
+
+
+
+
+
+
+
+
+
+
diff --git a/ruoyi-ui-vue3/src/components/ImageUpload/index.vue b/ruoyi-ui-vue3/src/components/ImageUpload/index.vue
new file mode 100644
index 000000000..8cad89c06
--- /dev/null
+++ b/ruoyi-ui-vue3/src/components/ImageUpload/index.vue
@@ -0,0 +1,223 @@
+
+
+
+
+
+
+
+ 请上传
+
+ 大小不超过 {{ fileSize }}MB
+
+
+ 格式为 {{ fileType.join("/") }}
+
+ 的文件
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/ruoyi-ui-vue3/src/components/Pagination/index.vue b/ruoyi-ui-vue3/src/components/Pagination/index.vue
new file mode 100644
index 000000000..38de953f0
--- /dev/null
+++ b/ruoyi-ui-vue3/src/components/Pagination/index.vue
@@ -0,0 +1,105 @@
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/ruoyi-ui-vue3/src/components/ParentView/index.vue b/ruoyi-ui-vue3/src/components/ParentView/index.vue
new file mode 100644
index 000000000..7bf614897
--- /dev/null
+++ b/ruoyi-ui-vue3/src/components/ParentView/index.vue
@@ -0,0 +1,3 @@
+
+
+
diff --git a/ruoyi-ui-vue3/src/components/RightToolbar/index.vue b/ruoyi-ui-vue3/src/components/RightToolbar/index.vue
new file mode 100644
index 000000000..becb12c18
--- /dev/null
+++ b/ruoyi-ui-vue3/src/components/RightToolbar/index.vue
@@ -0,0 +1,105 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/ruoyi-ui-vue3/src/components/RuoYi/Doc/index.vue b/ruoyi-ui-vue3/src/components/RuoYi/Doc/index.vue
new file mode 100644
index 000000000..3e69482e1
--- /dev/null
+++ b/ruoyi-ui-vue3/src/components/RuoYi/Doc/index.vue
@@ -0,0 +1,13 @@
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/ruoyi-ui-vue3/src/components/RuoYi/Git/index.vue b/ruoyi-ui-vue3/src/components/RuoYi/Git/index.vue
new file mode 100644
index 000000000..c8d6fd5dd
--- /dev/null
+++ b/ruoyi-ui-vue3/src/components/RuoYi/Git/index.vue
@@ -0,0 +1,13 @@
+
+
+
+
+
+
+
diff --git a/ruoyi-ui-vue3/src/components/Screenfull/index.vue b/ruoyi-ui-vue3/src/components/Screenfull/index.vue
new file mode 100644
index 000000000..7ad28eadc
--- /dev/null
+++ b/ruoyi-ui-vue3/src/components/Screenfull/index.vue
@@ -0,0 +1,22 @@
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/ruoyi-ui-vue3/src/components/SizeSelect/index.vue b/ruoyi-ui-vue3/src/components/SizeSelect/index.vue
new file mode 100644
index 000000000..4c2e7e952
--- /dev/null
+++ b/ruoyi-ui-vue3/src/components/SizeSelect/index.vue
@@ -0,0 +1,45 @@
+
+
+
+
+
+
+
+
+
+ {{ item.label }}
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/ruoyi-ui-vue3/src/components/SvgIcon/index.vue b/ruoyi-ui-vue3/src/components/SvgIcon/index.vue
new file mode 100644
index 000000000..8c101f649
--- /dev/null
+++ b/ruoyi-ui-vue3/src/components/SvgIcon/index.vue
@@ -0,0 +1,53 @@
+
+
+
+
+
+
+
+
+
diff --git a/ruoyi-ui-vue3/src/components/SvgIcon/svgicon.js b/ruoyi-ui-vue3/src/components/SvgIcon/svgicon.js
new file mode 100644
index 000000000..4431719a5
--- /dev/null
+++ b/ruoyi-ui-vue3/src/components/SvgIcon/svgicon.js
@@ -0,0 +1,10 @@
+import * as components from '@element-plus/icons-vue'
+
+export default {
+ install: (app) => {
+ for (const key in components) {
+ const componentConfig = components[key];
+ app.component(componentConfig.name, componentConfig);
+ }
+ },
+};
diff --git a/ruoyi-ui-vue3/src/components/TopNav/index.vue b/ruoyi-ui-vue3/src/components/TopNav/index.vue
new file mode 100644
index 000000000..811cd9f1e
--- /dev/null
+++ b/ruoyi-ui-vue3/src/components/TopNav/index.vue
@@ -0,0 +1,192 @@
+
+
+
+
+ {{ item.meta.title }}
+
+
+
+
+ 更多菜单
+
+
+ {{ item.meta.title }}
+
+
+
+
+
+
+
+
diff --git a/ruoyi-ui-vue3/src/components/TreeSelect/index.vue b/ruoyi-ui-vue3/src/components/TreeSelect/index.vue
new file mode 100644
index 000000000..4ff0e76d9
--- /dev/null
+++ b/ruoyi-ui-vue3/src/components/TreeSelect/index.vue
@@ -0,0 +1,156 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/ruoyi-ui-vue3/src/components/iFrame/index.vue b/ruoyi-ui-vue3/src/components/iFrame/index.vue
new file mode 100644
index 000000000..091b1a240
--- /dev/null
+++ b/ruoyi-ui-vue3/src/components/iFrame/index.vue
@@ -0,0 +1,31 @@
+
+
+
+
+
+
+
diff --git a/ruoyi-ui-vue3/src/directive/common/copyText.js b/ruoyi-ui-vue3/src/directive/common/copyText.js
new file mode 100644
index 000000000..7063df8ed
--- /dev/null
+++ b/ruoyi-ui-vue3/src/directive/common/copyText.js
@@ -0,0 +1,66 @@
+/**
+* v-copyText 复制文本内容
+* Copyright (c) 2022 ruoyi
+*/
+
+export default {
+ beforeMount(el, { value, arg }) {
+ if (arg === "callback") {
+ el.$copyCallback = value;
+ } else {
+ el.$copyValue = value;
+ const handler = () => {
+ copyTextToClipboard(el.$copyValue);
+ if (el.$copyCallback) {
+ el.$copyCallback(el.$copyValue);
+ }
+ };
+ el.addEventListener("click", handler);
+ el.$destroyCopy = () => el.removeEventListener("click", handler);
+ }
+ }
+}
+
+function copyTextToClipboard(input, { target = document.body } = {}) {
+ const element = document.createElement('textarea');
+ const previouslyFocusedElement = document.activeElement;
+
+ element.value = input;
+
+ // Prevent keyboard from showing on mobile
+ element.setAttribute('readonly', '');
+
+ element.style.contain = 'strict';
+ element.style.position = 'absolute';
+ element.style.left = '-9999px';
+ element.style.fontSize = '12pt'; // Prevent zooming on iOS
+
+ const selection = document.getSelection();
+ const originalRange = selection.rangeCount > 0 && selection.getRangeAt(0);
+
+ target.append(element);
+ element.select();
+
+ // Explicit selection workaround for iOS
+ element.selectionStart = 0;
+ element.selectionEnd = input.length;
+
+ let isSuccess = false;
+ try {
+ isSuccess = document.execCommand('copy');
+ } catch { }
+
+ element.remove();
+
+ if (originalRange) {
+ selection.removeAllRanges();
+ selection.addRange(originalRange);
+ }
+
+ // Get the focus back on the previously focused element, if any
+ if (previouslyFocusedElement) {
+ previouslyFocusedElement.focus();
+ }
+
+ return isSuccess;
+}
diff --git a/ruoyi-ui-vue3/src/directive/index.js b/ruoyi-ui-vue3/src/directive/index.js
new file mode 100644
index 000000000..86b8f88a8
--- /dev/null
+++ b/ruoyi-ui-vue3/src/directive/index.js
@@ -0,0 +1,9 @@
+import hasRole from './permission/hasRole'
+import hasPermi from './permission/hasPermi'
+import copyText from './common/copyText'
+
+export default function directive(app){
+ app.directive('hasRole', hasRole)
+ app.directive('hasPermi', hasPermi)
+ app.directive('copyText', copyText)
+}
\ No newline at end of file
diff --git a/ruoyi-ui-vue3/src/directive/permission/hasPermi.js b/ruoyi-ui-vue3/src/directive/permission/hasPermi.js
new file mode 100644
index 000000000..44ef3f821
--- /dev/null
+++ b/ruoyi-ui-vue3/src/directive/permission/hasPermi.js
@@ -0,0 +1,28 @@
+ /**
+ * v-hasPermi 操作权限处理
+ * Copyright (c) 2019 ruoyi
+ */
+
+import useUserStore from '@/store/modules/user'
+
+export default {
+ mounted(el, binding, vnode) {
+ const { value } = binding
+ const all_permission = "*:*:*";
+ const permissions = useUserStore().permissions
+
+ if (value && value instanceof Array && value.length > 0) {
+ const permissionFlag = value
+
+ const hasPermissions = permissions.some(permission => {
+ return all_permission === permission || permissionFlag.includes(permission)
+ })
+
+ if (!hasPermissions) {
+ el.parentNode && el.parentNode.removeChild(el)
+ }
+ } else {
+ throw new Error(`请设置操作权限标签值`)
+ }
+ }
+}
diff --git a/ruoyi-ui-vue3/src/directive/permission/hasRole.js b/ruoyi-ui-vue3/src/directive/permission/hasRole.js
new file mode 100644
index 000000000..8774bddd6
--- /dev/null
+++ b/ruoyi-ui-vue3/src/directive/permission/hasRole.js
@@ -0,0 +1,28 @@
+ /**
+ * v-hasRole 角色权限处理
+ * Copyright (c) 2019 ruoyi
+ */
+
+import useUserStore from '@/store/modules/user'
+
+export default {
+ mounted(el, binding, vnode) {
+ const { value } = binding
+ const super_admin = "admin";
+ const roles = useUserStore().roles
+
+ if (value && value instanceof Array && value.length > 0) {
+ const roleFlag = value
+
+ const hasRole = roles.some(role => {
+ return super_admin === role || roleFlag.includes(role)
+ })
+
+ if (!hasRole) {
+ el.parentNode && el.parentNode.removeChild(el)
+ }
+ } else {
+ throw new Error(`请设置角色权限标签值`)
+ }
+ }
+}
diff --git a/ruoyi-ui-vue3/src/layout/components/AppMain.vue b/ruoyi-ui-vue3/src/layout/components/AppMain.vue
new file mode 100644
index 000000000..016612c78
--- /dev/null
+++ b/ruoyi-ui-vue3/src/layout/components/AppMain.vue
@@ -0,0 +1,67 @@
+
+
+
+
+
+
+
+
+
diff --git a/ruoyi-ui-vue3/src/layout/components/IframeToggle/index.vue b/ruoyi-ui-vue3/src/layout/components/IframeToggle/index.vue
new file mode 100644
index 000000000..2e07fe883
--- /dev/null
+++ b/ruoyi-ui-vue3/src/layout/components/IframeToggle/index.vue
@@ -0,0 +1,19 @@
+
+
+
+
+
+
+
diff --git a/ruoyi-ui-vue3/src/layout/components/InnerLink/index.vue b/ruoyi-ui-vue3/src/layout/components/InnerLink/index.vue
new file mode 100644
index 000000000..53a903c21
--- /dev/null
+++ b/ruoyi-ui-vue3/src/layout/components/InnerLink/index.vue
@@ -0,0 +1,24 @@
+
+
+
+
+
+
+
diff --git a/ruoyi-ui-vue3/src/layout/components/Navbar.vue b/ruoyi-ui-vue3/src/layout/components/Navbar.vue
new file mode 100644
index 000000000..c46ebe24c
--- /dev/null
+++ b/ruoyi-ui-vue3/src/layout/components/Navbar.vue
@@ -0,0 +1,191 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/ruoyi-ui-vue3/src/layout/components/Settings/index.vue b/ruoyi-ui-vue3/src/layout/components/Settings/index.vue
new file mode 100644
index 000000000..0d551ba9d
--- /dev/null
+++ b/ruoyi-ui-vue3/src/layout/components/Settings/index.vue
@@ -0,0 +1,241 @@
+
+
+
+
主题风格设置
+
+
+
+
+
+
+
+
+
+
+
+
+ 主题颜色
+
+
+
+
+
+
+ 系统布局配置
+
+
+ 开启 TopNav
+
+
+
+
+
+
+ 开启 Tags-Views
+
+
+
+
+
+
+ 固定 Header
+
+
+
+
+
+
+ 显示 Logo
+
+
+
+
+
+
+ 动态标题
+
+
+
+
+
+
+
+ 保存配置
+ 重置配置
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/ruoyi-ui-vue3/src/layout/components/Sidebar/Link.vue b/ruoyi-ui-vue3/src/layout/components/Sidebar/Link.vue
new file mode 100644
index 000000000..80114316e
--- /dev/null
+++ b/ruoyi-ui-vue3/src/layout/components/Sidebar/Link.vue
@@ -0,0 +1,40 @@
+
+
+
+
+
+
+
diff --git a/ruoyi-ui-vue3/src/layout/components/Sidebar/Logo.vue b/ruoyi-ui-vue3/src/layout/components/Sidebar/Logo.vue
new file mode 100644
index 000000000..2cd9e0b77
--- /dev/null
+++ b/ruoyi-ui-vue3/src/layout/components/Sidebar/Logo.vue
@@ -0,0 +1,81 @@
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/ruoyi-ui-vue3/src/layout/components/Sidebar/SidebarItem.vue b/ruoyi-ui-vue3/src/layout/components/Sidebar/SidebarItem.vue
new file mode 100644
index 000000000..c423fb18e
--- /dev/null
+++ b/ruoyi-ui-vue3/src/layout/components/Sidebar/SidebarItem.vue
@@ -0,0 +1,102 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/ruoyi-ui-vue3/src/layout/components/Sidebar/index.vue b/ruoyi-ui-vue3/src/layout/components/Sidebar/index.vue
new file mode 100644
index 000000000..9b14dfc0e
--- /dev/null
+++ b/ruoyi-ui-vue3/src/layout/components/Sidebar/index.vue
@@ -0,0 +1,54 @@
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/ruoyi-ui-vue3/src/layout/components/TagsView/ScrollPane.vue b/ruoyi-ui-vue3/src/layout/components/TagsView/ScrollPane.vue
new file mode 100644
index 000000000..f6fbfbde8
--- /dev/null
+++ b/ruoyi-ui-vue3/src/layout/components/TagsView/ScrollPane.vue
@@ -0,0 +1,105 @@
+
+
+
+
+
+
+
+
+
diff --git a/ruoyi-ui-vue3/src/layout/components/TagsView/index.vue b/ruoyi-ui-vue3/src/layout/components/TagsView/index.vue
new file mode 100644
index 000000000..1f996234f
--- /dev/null
+++ b/ruoyi-ui-vue3/src/layout/components/TagsView/index.vue
@@ -0,0 +1,338 @@
+
+
+
+
+ {{ tag.title }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/ruoyi-ui-vue3/src/layout/components/index.js b/ruoyi-ui-vue3/src/layout/components/index.js
new file mode 100644
index 000000000..fd5773137
--- /dev/null
+++ b/ruoyi-ui-vue3/src/layout/components/index.js
@@ -0,0 +1,4 @@
+export { default as AppMain } from './AppMain'
+export { default as Navbar } from './Navbar'
+export { default as Settings } from './Settings'
+export { default as TagsView } from './TagsView/index.vue'
diff --git a/ruoyi-ui-vue3/src/layout/index.vue b/ruoyi-ui-vue3/src/layout/index.vue
new file mode 100644
index 000000000..c4d654e26
--- /dev/null
+++ b/ruoyi-ui-vue3/src/layout/index.vue
@@ -0,0 +1,111 @@
+
+
+
+
+
+
+
diff --git a/ruoyi-ui-vue3/src/main.js b/ruoyi-ui-vue3/src/main.js
new file mode 100644
index 000000000..fcdb585b9
--- /dev/null
+++ b/ruoyi-ui-vue3/src/main.js
@@ -0,0 +1,88 @@
+import { createApp } from 'vue'
+
+import Cookies from 'js-cookie'
+
+import ElementPlus from 'element-plus'
+import locale from 'element-plus/lib/locale/lang/zh-cn' // 中文语言
+
+import '@/assets/styles/index.scss' // global css
+import App from './App'
+import store from './store'
+import router from './router'
+import directive from './directive' // directive
+
+// 注册指令
+import plugins from './plugins' // plugins
+import { download } from '@/utils/request'
+
+// svg图标
+import 'virtual:svg-icons-register'
+import SvgIcon from '@/components/SvgIcon'
+import elementIcons from '@/components/SvgIcon/svgicon'
+
+import './permission' // permission control
+
+import { useDict } from '@/utils/dict'
+import { getConfigKey, updateConfigByKey } from "@/api/system/config";
+import { parseTime, resetForm, addDateRange, handleTree, selectDictLabel, selectDictLabels } from '@/utils/ruoyi'
+
+// 分页组件
+import Pagination from '@/components/Pagination'
+// 自定义表格工具组件
+import RightToolbar from '@/components/RightToolbar'
+// 富文本组件
+import Editor from "@/components/Editor"
+// 文件上传组件
+import FileUpload from "@/components/FileUpload"
+// 图片上传组件
+import ImageUpload from "@/components/ImageUpload"
+// 图片预览组件
+import ImagePreview from "@/components/ImagePreview"
+// 自定义树选择组件
+import TreeSelect from '@/components/TreeSelect'
+// 字典标签组件
+import DictTag from '@/components/DictTag'
+
+const app = createApp(App)
+
+// 全局方法挂载
+app.config.globalProperties.useDict = useDict
+app.config.globalProperties.getConfigKey = getConfigKey
+app.config.globalProperties.updateConfigByKey = updateConfigByKey
+app.config.globalProperties.download = download
+app.config.globalProperties.parseTime = parseTime
+app.config.globalProperties.resetForm = resetForm
+app.config.globalProperties.handleTree = handleTree
+app.config.globalProperties.addDateRange = addDateRange
+app.config.globalProperties.selectDictLabel = selectDictLabel
+app.config.globalProperties.selectDictLabels = selectDictLabels
+
+// 全局组件挂载
+app.component('DictTag', DictTag)
+app.component('Pagination', Pagination)
+app.component('TreeSelect', TreeSelect)
+app.component('FileUpload', FileUpload)
+app.component('ImageUpload', ImageUpload)
+app.component('ImagePreview', ImagePreview)
+app.component('RightToolbar', RightToolbar)
+app.component('Editor', Editor)
+
+app.use(router)
+app.use(store)
+app.use(plugins)
+app.use(elementIcons)
+app.component('svg-icon', SvgIcon)
+
+directive(app)
+
+// 使用element-plus 并且设置全局的大小
+app.use(ElementPlus, {
+ locale: locale,
+ // 支持 large、default、small
+ size: Cookies.get('size') || 'default'
+})
+
+// 修改 el-dialog 默认点击遮照为不关闭
+app._context.components.ElDialog.props.closeOnClickModal.default = false
+
+app.mount('#app')
diff --git a/ruoyi-ui-vue3/src/permission.js b/ruoyi-ui-vue3/src/permission.js
new file mode 100644
index 000000000..a474e0e68
--- /dev/null
+++ b/ruoyi-ui-vue3/src/permission.js
@@ -0,0 +1,63 @@
+import router from './router'
+import { ElMessage } from 'element-plus'
+import NProgress from 'nprogress'
+import 'nprogress/nprogress.css'
+import { getToken } from '@/utils/auth'
+import { isHttp } from '@/utils/validate'
+import { isRelogin } from '@/utils/request'
+import useUserStore from '@/store/modules/user'
+import useSettingsStore from '@/store/modules/settings'
+import usePermissionStore from '@/store/modules/permission'
+
+NProgress.configure({ showSpinner: false });
+
+const whiteList = ['/login', '/register'];
+
+router.beforeEach((to, from, next) => {
+ NProgress.start()
+ if (getToken()) {
+ to.meta.title && useSettingsStore().setTitle(to.meta.title)
+ /* has token*/
+ if (to.path === '/login') {
+ next({ path: '/' })
+ NProgress.done()
+ } else {
+ if (useUserStore().roles.length === 0) {
+ isRelogin.show = true
+ // 判断当前用户是否已拉取完user_info信息
+ useUserStore().getInfo().then(() => {
+ isRelogin.show = false
+ usePermissionStore().generateRoutes().then(accessRoutes => {
+ // 根据roles权限生成可访问的路由表
+ accessRoutes.forEach(route => {
+ if (!isHttp(route.path)) {
+ router.addRoute(route) // 动态添加可访问路由表
+ }
+ })
+ next({ ...to, replace: true }) // hack方法 确保addRoutes已完成
+ })
+ }).catch(err => {
+ useUserStore().logOut().then(() => {
+ ElMessage.error(err)
+ next({ path: '/' })
+ })
+ })
+ } else {
+ next()
+ }
+ }
+ } else {
+ // 没有token
+ if (whiteList.indexOf(to.path) !== -1) {
+ // 在免登录白名单,直接进入
+ next()
+ } else {
+ next(`/login?redirect=${to.fullPath}`) // 否则全部重定向到登录页
+ NProgress.done()
+ }
+ }
+})
+
+router.afterEach(() => {
+ NProgress.done()
+})
diff --git a/ruoyi-ui-vue3/src/plugins/auth.js b/ruoyi-ui-vue3/src/plugins/auth.js
new file mode 100644
index 000000000..5e8c28deb
--- /dev/null
+++ b/ruoyi-ui-vue3/src/plugins/auth.js
@@ -0,0 +1,60 @@
+import useUserStore from '@/store/modules/user'
+
+function authPermission(permission) {
+ const all_permission = "*:*:*";
+ const permissions = useUserStore().permissions
+ if (permission && permission.length > 0) {
+ return permissions.some(v => {
+ return all_permission === v || v === permission
+ })
+ } else {
+ return false
+ }
+}
+
+function authRole(role) {
+ const super_admin = "admin";
+ const roles = useUserStore().roles
+ if (role && role.length > 0) {
+ return roles.some(v => {
+ return super_admin === v || v === role
+ })
+ } else {
+ return false
+ }
+}
+
+export default {
+ // 验证用户是否具备某权限
+ hasPermi(permission) {
+ return authPermission(permission);
+ },
+ // 验证用户是否含有指定权限,只需包含其中一个
+ hasPermiOr(permissions) {
+ return permissions.some(item => {
+ return authPermission(item)
+ })
+ },
+ // 验证用户是否含有指定权限,必须全部拥有
+ hasPermiAnd(permissions) {
+ return permissions.every(item => {
+ return authPermission(item)
+ })
+ },
+ // 验证用户是否具备某角色
+ hasRole(role) {
+ return authRole(role);
+ },
+ // 验证用户是否含有指定角色,只需包含其中一个
+ hasRoleOr(roles) {
+ return roles.some(item => {
+ return authRole(item)
+ })
+ },
+ // 验证用户是否含有指定角色,必须全部拥有
+ hasRoleAnd(roles) {
+ return roles.every(item => {
+ return authRole(item)
+ })
+ }
+}
diff --git a/ruoyi-ui-vue3/src/plugins/cache.js b/ruoyi-ui-vue3/src/plugins/cache.js
new file mode 100644
index 000000000..6b5c00b9e
--- /dev/null
+++ b/ruoyi-ui-vue3/src/plugins/cache.js
@@ -0,0 +1,77 @@
+const sessionCache = {
+ set (key, value) {
+ if (!sessionStorage) {
+ return
+ }
+ if (key != null && value != null) {
+ sessionStorage.setItem(key, value)
+ }
+ },
+ get (key) {
+ if (!sessionStorage) {
+ return null
+ }
+ if (key == null) {
+ return null
+ }
+ return sessionStorage.getItem(key)
+ },
+ setJSON (key, jsonValue) {
+ if (jsonValue != null) {
+ this.set(key, JSON.stringify(jsonValue))
+ }
+ },
+ getJSON (key) {
+ const value = this.get(key)
+ if (value != null) {
+ return JSON.parse(value)
+ }
+ },
+ remove (key) {
+ sessionStorage.removeItem(key);
+ }
+}
+const localCache = {
+ set (key, value) {
+ if (!localStorage) {
+ return
+ }
+ if (key != null && value != null) {
+ localStorage.setItem(key, value)
+ }
+ },
+ get (key) {
+ if (!localStorage) {
+ return null
+ }
+ if (key == null) {
+ return null
+ }
+ return localStorage.getItem(key)
+ },
+ setJSON (key, jsonValue) {
+ if (jsonValue != null) {
+ this.set(key, JSON.stringify(jsonValue))
+ }
+ },
+ getJSON (key) {
+ const value = this.get(key)
+ if (value != null) {
+ return JSON.parse(value)
+ }
+ },
+ remove (key) {
+ localStorage.removeItem(key);
+ }
+}
+
+export default {
+ /**
+ * 会话级缓存
+ */
+ session: sessionCache,
+ /**
+ * 本地缓存
+ */
+ local: localCache
+}
diff --git a/ruoyi-ui-vue3/src/plugins/download.js b/ruoyi-ui-vue3/src/plugins/download.js
new file mode 100644
index 000000000..29dd469c3
--- /dev/null
+++ b/ruoyi-ui-vue3/src/plugins/download.js
@@ -0,0 +1,71 @@
+import axios from 'axios'
+import { ElLoading, ElMessage } from 'element-plus'
+import { saveAs } from 'file-saver'
+import { getToken } from '@/utils/auth'
+import errorCode from '@/utils/errorCode'
+import { blobValidate } from '@/utils/ruoyi'
+
+const baseURL = import.meta.env.VITE_APP_BASE_API
+let downloadLoadingInstance;
+
+export default {
+ oss(ossId) {
+ var url = baseURL + '/system/oss/download/' + ossId
+ downloadLoadingInstance = ElLoading.service({ text: "正在下载数据,请稍候", background: "rgba(0, 0, 0, 0.7)", })
+ axios({
+ method: 'get',
+ url: url,
+ responseType: 'blob',
+ headers: { 'Authorization': 'Bearer ' + getToken() }
+ }).then((res) => {
+ const isBlob = blobValidate(res.data);
+ if (isBlob) {
+ const blob = new Blob([res.data], { type: 'application/octet-stream' })
+ this.saveAs(blob, decodeURIComponent(res.headers['download-filename']))
+ } else {
+ this.printErrMsg(res.data);
+ }
+ downloadLoadingInstance.close();
+ }).catch((r) => {
+ console.error(r)
+ Message.error('下载文件出现错误,请联系管理员!')
+ downloadLoadingInstance.close();
+ })
+ },
+ zip(url, name) {
+ var url = baseURL + url
+ downloadLoadingInstance = ElLoading.service({ text: "正在下载数据,请稍候", spinner: "el-icon-loading", background: "rgba(0, 0, 0, 0.7)", })
+ axios({
+ method: 'get',
+ url: url,
+ responseType: 'blob',
+ headers: {
+ 'Authorization': 'Bearer ' + getToken(),
+ 'datasource': localStorage.getItem("dataName")
+ }
+ }).then((res) => {
+ const isBlob = blobValidate(res.data);
+ if (isBlob) {
+ const blob = new Blob([res.data], { type: 'application/zip' })
+ this.saveAs(blob, name)
+ } else {
+ this.printErrMsg(res.data);
+ }
+ downloadLoadingInstance.close();
+ }).catch((r) => {
+ console.error(r)
+ ElMessage.error('下载文件出现错误,请联系管理员!')
+ downloadLoadingInstance.close();
+ })
+ },
+ saveAs(text, name, opts) {
+ saveAs(text, name, opts);
+ },
+ async printErrMsg(data) {
+ const resText = await data.text();
+ const rspObj = JSON.parse(resText);
+ const errMsg = errorCode[rspObj.code] || rspObj.msg || errorCode['default']
+ ElMessage.error(errMsg);
+ }
+}
+
diff --git a/ruoyi-ui-vue3/src/plugins/index.js b/ruoyi-ui-vue3/src/plugins/index.js
new file mode 100644
index 000000000..47d1b41f0
--- /dev/null
+++ b/ruoyi-ui-vue3/src/plugins/index.js
@@ -0,0 +1,18 @@
+import tab from './tab'
+import auth from './auth'
+import cache from './cache'
+import modal from './modal'
+import download from './download'
+
+export default function installPlugins(app){
+ // 页签操作
+ app.config.globalProperties.$tab = tab
+ // 认证对象
+ app.config.globalProperties.$auth = auth
+ // 缓存对象
+ app.config.globalProperties.$cache = cache
+ // 模态框对象
+ app.config.globalProperties.$modal = modal
+ // 下载文件
+ app.config.globalProperties.$download = download
+}
diff --git a/ruoyi-ui-vue3/src/plugins/modal.js b/ruoyi-ui-vue3/src/plugins/modal.js
new file mode 100644
index 000000000..b59e14d69
--- /dev/null
+++ b/ruoyi-ui-vue3/src/plugins/modal.js
@@ -0,0 +1,82 @@
+import { ElMessage, ElMessageBox, ElNotification, ElLoading } from 'element-plus'
+
+let loadingInstance;
+
+export default {
+ // 消息提示
+ msg(content) {
+ ElMessage.info(content)
+ },
+ // 错误消息
+ msgError(content) {
+ ElMessage.error(content)
+ },
+ // 成功消息
+ msgSuccess(content) {
+ ElMessage.success(content)
+ },
+ // 警告消息
+ msgWarning(content) {
+ ElMessage.warning(content)
+ },
+ // 弹出提示
+ alert(content) {
+ ElMessageBox.alert(content, "系统提示")
+ },
+ // 错误提示
+ alertError(content) {
+ ElMessageBox.alert(content, "系统提示", { type: 'error' })
+ },
+ // 成功提示
+ alertSuccess(content) {
+ ElMessageBox.alert(content, "系统提示", { type: 'success' })
+ },
+ // 警告提示
+ alertWarning(content) {
+ ElMessageBox.alert(content, "系统提示", { type: 'warning' })
+ },
+ // 通知提示
+ notify(content) {
+ ElNotification.info(content)
+ },
+ // 错误通知
+ notifyError(content) {
+ ElNotification.error(content);
+ },
+ // 成功通知
+ notifySuccess(content) {
+ ElNotification.success(content)
+ },
+ // 警告通知
+ notifyWarning(content) {
+ ElNotification.warning(content)
+ },
+ // 确认窗体
+ confirm(content) {
+ return ElMessageBox.confirm(content, "系统提示", {
+ confirmButtonText: '确定',
+ cancelButtonText: '取消',
+ type: "warning",
+ })
+ },
+ // 提交内容
+ prompt(content) {
+ return ElMessageBox.prompt(content, "系统提示", {
+ confirmButtonText: '确定',
+ cancelButtonText: '取消',
+ type: "warning",
+ })
+ },
+ // 打开遮罩层
+ loading(content) {
+ loadingInstance = ElLoading.service({
+ lock: true,
+ text: content,
+ background: "rgba(0, 0, 0, 0.7)",
+ })
+ },
+ // 关闭遮罩层
+ closeLoading() {
+ loadingInstance.close();
+ }
+}
diff --git a/ruoyi-ui-vue3/src/plugins/tab.js b/ruoyi-ui-vue3/src/plugins/tab.js
new file mode 100644
index 000000000..7b51cf59f
--- /dev/null
+++ b/ruoyi-ui-vue3/src/plugins/tab.js
@@ -0,0 +1,69 @@
+import useTagsViewStore from '@/store/modules/tagsView'
+import router from '@/router'
+
+export default {
+ // 刷新当前tab页签
+ refreshPage(obj) {
+ const { path, query, matched } = router.currentRoute.value;
+ if (obj === undefined) {
+ matched.forEach((m) => {
+ if (m.components && m.components.default && m.components.default.name) {
+ if (!['Layout', 'ParentView'].includes(m.components.default.name)) {
+ obj = { name: m.components.default.name, path: path, query: query };
+ }
+ }
+ });
+ }
+ return useTagsViewStore().delCachedView(obj).then(() => {
+ const { path, query } = obj
+ router.replace({
+ path: '/redirect' + path,
+ query: query
+ })
+ })
+ },
+ // 关闭当前tab页签,打开新页签
+ closeOpenPage(obj) {
+ useTagsViewStore().delView(router.currentRoute.value);
+ if (obj !== undefined) {
+ return router.push(obj);
+ }
+ },
+ // 关闭指定tab页签
+ closePage(obj) {
+ if (obj === undefined) {
+ return useTagsViewStore().delView(router.currentRoute.value).then(({ visitedViews }) => {
+ const latestView = visitedViews.slice(-1)[0]
+ if (latestView) {
+ return router.push(latestView.fullPath)
+ }
+ return router.push('/');
+ });
+ }
+ return useTagsViewStore().delView(obj);
+ },
+ // 关闭所有tab页签
+ closeAllPage() {
+ return useTagsViewStore().delAllViews();
+ },
+ // 关闭左侧tab页签
+ closeLeftPage(obj) {
+ return useTagsViewStore().delLeftTags(obj || router.currentRoute.value);
+ },
+ // 关闭右侧tab页签
+ closeRightPage(obj) {
+ return useTagsViewStore().delRightTags(obj || router.currentRoute.value);
+ },
+ // 关闭其他tab页签
+ closeOtherPage(obj) {
+ return useTagsViewStore().delOthersViews(obj || router.currentRoute.value);
+ },
+ // 打开tab页签
+ openPage(url) {
+ return router.push(url);
+ },
+ // 修改tab页签
+ updatePage(obj) {
+ return useTagsViewStore().updateVisitedView(obj);
+ }
+}
diff --git a/ruoyi-ui-vue3/src/router/index.js b/ruoyi-ui-vue3/src/router/index.js
new file mode 100644
index 000000000..2559686ab
--- /dev/null
+++ b/ruoyi-ui-vue3/src/router/index.js
@@ -0,0 +1,175 @@
+import { createWebHistory, createRouter } from 'vue-router'
+/* Layout */
+import Layout from '@/layout'
+
+/**
+ * Note: 路由配置项
+ *
+ * hidden: true // 当设置 true 的时候该路由不会再侧边栏出现 如401,login等页面,或者如一些编辑页面/edit/1
+ * alwaysShow: true // 当你一个路由下面的 children 声明的路由大于1个时,自动会变成嵌套的模式--如组件页面
+ * // 只有一个时,会将那个子路由当做根路由显示在侧边栏--如引导页面
+ * // 若你想不管路由下面的 children 声明的个数都显示你的根路由
+ * // 你可以设置 alwaysShow: true,这样它就会忽略之前定义的规则,一直显示根路由
+ * redirect: noRedirect // 当设置 noRedirect 的时候该路由在面包屑导航中不可被点击
+ * name:'router-name' // 设定路由的名字,一定要填写不然使用时会出现各种问题
+ * query: '{"id": 1, "name": "ry"}' // 访问路由的默认传递参数
+ * roles: ['admin', 'common'] // 访问路由的角色权限
+ * permissions: ['a:a:a', 'b:b:b'] // 访问路由的菜单权限
+ * meta : {
+ noCache: true // 如果设置为true,则不会被 缓存(默认 false)
+ title: 'title' // 设置该路由在侧边栏和面包屑中展示的名字
+ icon: 'svg-name' // 设置该路由的图标,对应路径src/assets/icons/svg
+ breadcrumb: false // 如果设置为false,则不会在breadcrumb面包屑中显示
+ activeMenu: '/system/user' // 当路由设置了该属性,则会高亮相对应的侧边栏。
+ }
+ */
+
+// 公共路由
+export const constantRoutes = [
+ {
+ path: '/redirect',
+ component: Layout,
+ hidden: true,
+ children: [
+ {
+ path: '/redirect/:path(.*)',
+ component: () => import('@/views/redirect/index.vue')
+ }
+ ]
+ },
+ {
+ path: '/login',
+ component: () => import('@/views/login'),
+ hidden: true
+ },
+ {
+ path: '/register',
+ component: () => import('@/views/register'),
+ hidden: true
+ },
+ {
+ path: "/:pathMatch(.*)*",
+ component: () => import('@/views/error/404'),
+ hidden: true
+ },
+ {
+ path: '/401',
+ component: () => import('@/views/error/401'),
+ hidden: true
+ },
+ {
+ path: '',
+ component: Layout,
+ redirect: '/index',
+ children: [
+ {
+ path: '/index',
+ component: () => import('@/views/index'),
+ name: 'Index',
+ meta: { title: '首页', icon: 'dashboard', affix: true }
+ }
+ ]
+ },
+ {
+ path: '/user',
+ component: Layout,
+ hidden: true,
+ redirect: 'noredirect',
+ children: [
+ {
+ path: 'profile',
+ component: () => import('@/views/system/user/profile/index'),
+ name: 'Profile',
+ meta: { title: '个人中心', icon: 'user' }
+ }
+ ]
+ }
+]
+
+// 动态路由,基于用户权限动态去加载
+export const dynamicRoutes = [
+ {
+ path: '/system/user-auth',
+ component: Layout,
+ hidden: true,
+ permissions: ['system:user:edit'],
+ children: [
+ {
+ path: 'role/:userId(\\d+)',
+ component: () => import('@/views/system/user/authRole'),
+ name: 'AuthRole',
+ meta: { title: '分配角色', activeMenu: '/system/user' }
+ }
+ ]
+ },
+ {
+ path: '/system/role-auth',
+ component: Layout,
+ hidden: true,
+ permissions: ['system:role:edit'],
+ children: [
+ {
+ path: 'user/:roleId(\\d+)',
+ component: () => import('@/views/system/role/authUser'),
+ name: 'AuthUser',
+ meta: { title: '分配用户', activeMenu: '/system/role' }
+ }
+ ]
+ },
+ {
+ path: '/system/dict-data',
+ component: Layout,
+ hidden: true,
+ permissions: ['system:dict:list'],
+ children: [
+ {
+ path: 'index/:dictId(\\d+)',
+ component: () => import('@/views/system/dict/data'),
+ name: 'Data',
+ meta: { title: '字典数据', activeMenu: '/system/dict' }
+ }
+ ]
+ },
+ {
+ path: '/system/oss-config',
+ component: Layout,
+ hidden: true,
+ permissions: ['system:oss:list'],
+ children: [
+ {
+ path: 'index',
+ component: () => import('@/views/system/oss/config'),
+ name: 'OssConfig',
+ meta: { title: '配置管理', activeMenu: '/system/oss'}
+ }
+ ]
+ },
+ {
+ path: '/tool/gen-edit',
+ component: Layout,
+ hidden: true,
+ permissions: ['tool:gen:edit'],
+ children: [
+ {
+ path: 'index/:tableId(\\d+)',
+ component: () => import('@/views/tool/gen/editTable'),
+ name: 'GenEdit',
+ meta: { title: '修改生成配置', activeMenu: '/tool/gen' }
+ }
+ ]
+ }
+]
+
+const router = createRouter({
+ history: createWebHistory(import.meta.env.VITE_APP_CONTEXT_PATH),
+ routes: constantRoutes,
+ scrollBehavior(to, from, savedPosition) {
+ if (savedPosition) {
+ return savedPosition
+ } else {
+ return { top: 0 }
+ }
+ },
+});
+
+export default router;
diff --git a/ruoyi-ui-vue3/src/settings.js b/ruoyi-ui-vue3/src/settings.js
new file mode 100644
index 000000000..10e1db414
--- /dev/null
+++ b/ruoyi-ui-vue3/src/settings.js
@@ -0,0 +1,47 @@
+export default {
+ /**
+ * 网页标题
+ */
+ title: import.meta.env.VITE_APP_TITLE,
+ /**
+ * 侧边栏主题 深色主题theme-dark,浅色主题theme-light
+ */
+ sideTheme: 'theme-dark',
+ /**
+ * 是否系统布局配置
+ */
+ showSettings: false,
+
+ /**
+ * 是否显示顶部导航
+ */
+ topNav: false,
+
+ /**
+ * 是否显示 tagsView
+ */
+ tagsView: true,
+
+ /**
+ * 是否固定头部
+ */
+ fixedHeader: false,
+
+ /**
+ * 是否显示logo
+ */
+ sidebarLogo: true,
+
+ /**
+ * 是否显示动态标题
+ */
+ dynamicTitle: false,
+
+ /**
+ * @type {string | array} 'production' | ['production', 'development']
+ * @description Need show err logs component.
+ * The default is only used in the production env
+ * If you want to also use it in dev, you can pass ['production', 'development']
+ */
+ errorLog: 'production'
+}
diff --git a/ruoyi-ui-vue3/src/store/index.js b/ruoyi-ui-vue3/src/store/index.js
new file mode 100644
index 000000000..f10f38950
--- /dev/null
+++ b/ruoyi-ui-vue3/src/store/index.js
@@ -0,0 +1,3 @@
+const store = createPinia()
+
+export default store
\ No newline at end of file
diff --git a/ruoyi-ui-vue3/src/store/modules/app.js b/ruoyi-ui-vue3/src/store/modules/app.js
new file mode 100644
index 000000000..0b5715964
--- /dev/null
+++ b/ruoyi-ui-vue3/src/store/modules/app.js
@@ -0,0 +1,46 @@
+import Cookies from 'js-cookie'
+
+const useAppStore = defineStore(
+ 'app',
+ {
+ state: () => ({
+ sidebar: {
+ opened: Cookies.get('sidebarStatus') ? !!+Cookies.get('sidebarStatus') : true,
+ withoutAnimation: false,
+ hide: false
+ },
+ device: 'desktop',
+ size: Cookies.get('size') || 'default'
+ }),
+ actions: {
+ toggleSideBar(withoutAnimation) {
+ if (this.sidebar.hide) {
+ return false;
+ }
+ this.sidebar.opened = !this.sidebar.opened
+ this.sidebar.withoutAnimation = withoutAnimation
+ if (this.sidebar.opened) {
+ Cookies.set('sidebarStatus', 1)
+ } else {
+ Cookies.set('sidebarStatus', 0)
+ }
+ },
+ closeSideBar({ withoutAnimation }) {
+ Cookies.set('sidebarStatus', 0)
+ this.sidebar.opened = false
+ this.sidebar.withoutAnimation = withoutAnimation
+ },
+ toggleDevice(device) {
+ this.device = device
+ },
+ setSize(size) {
+ this.size = size;
+ Cookies.set('size', size)
+ },
+ toggleSideBarHide(status) {
+ this.sidebar.hide = status
+ }
+ }
+ })
+
+export default useAppStore
diff --git a/ruoyi-ui-vue3/src/store/modules/dict.js b/ruoyi-ui-vue3/src/store/modules/dict.js
new file mode 100644
index 000000000..27fc30869
--- /dev/null
+++ b/ruoyi-ui-vue3/src/store/modules/dict.js
@@ -0,0 +1,57 @@
+const useDictStore = defineStore(
+ 'dict',
+ {
+ state: () => ({
+ dict: new Array()
+ }),
+ actions: {
+ // 获取字典
+ getDict(_key) {
+ if (_key == null && _key == "") {
+ return null;
+ }
+ try {
+ for (let i = 0; i < this.dict.length; i++) {
+ if (this.dict[i].key == _key) {
+ return this.dict[i].value;
+ }
+ }
+ } catch (e) {
+ return null;
+ }
+ },
+ // 设置字典
+ setDict(_key, value) {
+ if (_key !== null && _key !== "") {
+ this.dict.push({
+ key: _key,
+ value: value
+ });
+ }
+ },
+ // 删除字典
+ removeDict(_key) {
+ var bln = false;
+ try {
+ for (let i = 0; i < this.dict.length; i++) {
+ if (this.dict[i].key == _key) {
+ this.dict.splice(i, 1);
+ return true;
+ }
+ }
+ } catch (e) {
+ bln = false;
+ }
+ return bln;
+ },
+ // 清空字典
+ cleanDict() {
+ this.dict = new Array();
+ },
+ // 初始字典
+ initDict() {
+ }
+ }
+ })
+
+export default useDictStore
diff --git a/ruoyi-ui-vue3/src/store/modules/permission.js b/ruoyi-ui-vue3/src/store/modules/permission.js
new file mode 100644
index 000000000..ef506e9b4
--- /dev/null
+++ b/ruoyi-ui-vue3/src/store/modules/permission.js
@@ -0,0 +1,138 @@
+import auth from '@/plugins/auth'
+import router, { constantRoutes, dynamicRoutes } from '@/router'
+import { getRouters } from '@/api/menu'
+import Layout from '@/layout/index'
+import ParentView from '@/components/ParentView'
+import InnerLink from '@/layout/components/InnerLink'
+
+// 匹配views里面所有的.vue文件
+const modules = import.meta.glob('./../../views/**/*.vue')
+
+const usePermissionStore = defineStore(
+ 'permission',
+ {
+ state: () => ({
+ routes: [],
+ addRoutes: [],
+ defaultRoutes: [],
+ topbarRouters: [],
+ sidebarRouters: []
+ }),
+ actions: {
+ setRoutes(routes) {
+ this.addRoutes = routes
+ this.routes = constantRoutes.concat(routes)
+ },
+ setDefaultRoutes(routes) {
+ this.defaultRoutes = constantRoutes.concat(routes)
+ },
+ setTopbarRoutes(routes) {
+ this.topbarRouters = routes
+ },
+ setSidebarRouters(routes) {
+ this.sidebarRouters = routes
+ },
+ generateRoutes(roles) {
+ return new Promise(resolve => {
+ // 向后端请求路由数据
+ getRouters().then(res => {
+ const sdata = JSON.parse(JSON.stringify(res.data))
+ const rdata = JSON.parse(JSON.stringify(res.data))
+ const defaultData = JSON.parse(JSON.stringify(res.data))
+ const sidebarRoutes = filterAsyncRouter(sdata)
+ const rewriteRoutes = filterAsyncRouter(rdata, false, true)
+ const defaultRoutes = filterAsyncRouter(defaultData)
+ const asyncRoutes = filterDynamicRoutes(dynamicRoutes)
+ asyncRoutes.forEach(route => { router.addRoute(route) })
+ this.setRoutes(rewriteRoutes)
+ this.setSidebarRouters(constantRoutes.concat(sidebarRoutes))
+ this.setDefaultRoutes(sidebarRoutes)
+ this.setTopbarRoutes(defaultRoutes)
+ resolve(rewriteRoutes)
+ })
+ })
+ }
+ }
+ })
+
+// 遍历后台传来的路由字符串,转换为组件对象
+function filterAsyncRouter(asyncRouterMap, lastRouter = false, type = false) {
+ return asyncRouterMap.filter(route => {
+ if (type && route.children) {
+ route.children = filterChildren(route.children)
+ }
+ if (route.component) {
+ // Layout ParentView 组件特殊处理
+ if (route.component === 'Layout') {
+ route.component = Layout
+ } else if (route.component === 'ParentView') {
+ route.component = ParentView
+ } else if (route.component === 'InnerLink') {
+ route.component = InnerLink
+ } else {
+ route.component = loadView(route.component)
+ }
+ }
+ if (route.children != null && route.children && route.children.length) {
+ route.children = filterAsyncRouter(route.children, route, type)
+ } else {
+ delete route['children']
+ delete route['redirect']
+ }
+ return true
+ })
+}
+
+function filterChildren(childrenMap, lastRouter = false) {
+ var children = []
+ childrenMap.forEach((el, index) => {
+ if (el.children && el.children.length) {
+ if (el.component === 'ParentView' && !lastRouter) {
+ el.children.forEach(c => {
+ c.path = el.path + '/' + c.path
+ if (c.children && c.children.length) {
+ children = children.concat(filterChildren(c.children, c))
+ return
+ }
+ children.push(c)
+ })
+ return
+ }
+ }
+ if (lastRouter) {
+ el.path = lastRouter.path + '/' + el.path
+ }
+ children = children.concat(el)
+ })
+ return children
+}
+
+// 动态路由遍历,验证是否具备权限
+export function filterDynamicRoutes(routes) {
+ const res = []
+ routes.forEach(route => {
+ if (route.permissions) {
+ if (auth.hasPermiOr(route.permissions)) {
+ res.push(route)
+ }
+ } else if (route.roles) {
+ if (auth.hasRoleOr(route.roles)) {
+ res.push(route)
+ }
+ }
+ })
+ return res
+}
+
+export const loadView = (view) => {
+ let res;
+ for (const path in modules) {
+ const dir = path.split('views/')[1].split('.vue')[0];
+ if (dir === view) {
+ res = () => modules[path]();
+ }
+ }
+ return res;
+}
+
+export default usePermissionStore
diff --git a/ruoyi-ui-vue3/src/store/modules/settings.js b/ruoyi-ui-vue3/src/store/modules/settings.js
new file mode 100644
index 000000000..22b733666
--- /dev/null
+++ b/ruoyi-ui-vue3/src/store/modules/settings.js
@@ -0,0 +1,38 @@
+import defaultSettings from '@/settings'
+import { useDynamicTitle } from '@/utils/dynamicTitle'
+
+const { sideTheme, showSettings, topNav, tagsView, fixedHeader, sidebarLogo, dynamicTitle } = defaultSettings
+
+const storageSetting = JSON.parse(localStorage.getItem('layout-setting')) || ''
+
+const useSettingsStore = defineStore(
+ 'settings',
+ {
+ state: () => ({
+ title: '',
+ theme: storageSetting.theme || '#409EFF',
+ sideTheme: storageSetting.sideTheme || sideTheme,
+ showSettings: showSettings,
+ topNav: storageSetting.topNav === undefined ? topNav : storageSetting.topNav,
+ tagsView: storageSetting.tagsView === undefined ? tagsView : storageSetting.tagsView,
+ fixedHeader: storageSetting.fixedHeader === undefined ? fixedHeader : storageSetting.fixedHeader,
+ sidebarLogo: storageSetting.sidebarLogo === undefined ? sidebarLogo : storageSetting.sidebarLogo,
+ dynamicTitle: storageSetting.dynamicTitle === undefined ? dynamicTitle : storageSetting.dynamicTitle
+ }),
+ actions: {
+ // 修改布局设置
+ changeSetting(data) {
+ const { key, value } = data
+ if (this.hasOwnProperty(key)) {
+ this[key] = value
+ }
+ },
+ // 设置网页标题
+ setTitle(title) {
+ this.title = title
+ useDynamicTitle();
+ }
+ }
+ })
+
+export default useSettingsStore
diff --git a/ruoyi-ui-vue3/src/store/modules/tagsView.js b/ruoyi-ui-vue3/src/store/modules/tagsView.js
new file mode 100644
index 000000000..9d07f33d1
--- /dev/null
+++ b/ruoyi-ui-vue3/src/store/modules/tagsView.js
@@ -0,0 +1,182 @@
+const useTagsViewStore = defineStore(
+ 'tags-view',
+ {
+ state: () => ({
+ visitedViews: [],
+ cachedViews: [],
+ iframeViews: []
+ }),
+ actions: {
+ addView(view) {
+ this.addVisitedView(view)
+ this.addCachedView(view)
+ },
+ addIframeView(view) {
+ if (this.iframeViews.some(v => v.path === view.path)) return
+ this.iframeViews.push(
+ Object.assign({}, view, {
+ title: view.meta.title || 'no-name'
+ })
+ )
+ },
+ addVisitedView(view) {
+ if (this.visitedViews.some(v => v.path === view.path)) return
+ this.visitedViews.push(
+ Object.assign({}, view, {
+ title: view.meta.title || 'no-name'
+ })
+ )
+ },
+ addCachedView(view) {
+ if (this.cachedViews.includes(view.name)) return
+ if (!view.meta.noCache) {
+ this.cachedViews.push(view.name)
+ }
+ },
+ delView(view) {
+ return new Promise(resolve => {
+ this.delVisitedView(view)
+ this.delCachedView(view)
+ resolve({
+ visitedViews: [...this.visitedViews],
+ cachedViews: [...this.cachedViews]
+ })
+ })
+ },
+ delVisitedView(view) {
+ return new Promise(resolve => {
+ for (const [i, v] of this.visitedViews.entries()) {
+ if (v.path === view.path) {
+ this.visitedViews.splice(i, 1)
+ break
+ }
+ }
+ this.iframeViews = this.iframeViews.filter(item => item.path !== view.path)
+ resolve([...this.visitedViews])
+ })
+ },
+ delIframeView(view) {
+ return new Promise(resolve => {
+ this.iframeViews = this.iframeViews.filter(item => item.path !== view.path)
+ resolve([...this.iframeViews])
+ })
+ },
+ delCachedView(view) {
+ return new Promise(resolve => {
+ const index = this.cachedViews.indexOf(view.name)
+ index > -1 && this.cachedViews.splice(index, 1)
+ resolve([...this.cachedViews])
+ })
+ },
+ delOthersViews(view) {
+ return new Promise(resolve => {
+ this.delOthersVisitedViews(view)
+ this.delOthersCachedViews(view)
+ resolve({
+ visitedViews: [...this.visitedViews],
+ cachedViews: [...this.cachedViews]
+ })
+ })
+ },
+ delOthersVisitedViews(view) {
+ return new Promise(resolve => {
+ this.visitedViews = this.visitedViews.filter(v => {
+ return v.meta.affix || v.path === view.path
+ })
+ this.iframeViews = this.iframeViews.filter(item => item.path === view.path)
+ resolve([...this.visitedViews])
+ })
+ },
+ delOthersCachedViews(view) {
+ return new Promise(resolve => {
+ const index = this.cachedViews.indexOf(view.name)
+ if (index > -1) {
+ this.cachedViews = this.cachedViews.slice(index, index + 1)
+ } else {
+ this.cachedViews = []
+ }
+ resolve([...this.cachedViews])
+ })
+ },
+ delAllViews(view) {
+ return new Promise(resolve => {
+ this.delAllVisitedViews(view)
+ this.delAllCachedViews(view)
+ resolve({
+ visitedViews: [...this.visitedViews],
+ cachedViews: [...this.cachedViews]
+ })
+ })
+ },
+ delAllVisitedViews(view) {
+ return new Promise(resolve => {
+ const affixTags = this.visitedViews.filter(tag => tag.meta.affix)
+ this.visitedViews = affixTags
+ this.iframeViews = []
+ resolve([...this.visitedViews])
+ })
+ },
+ delAllCachedViews(view) {
+ return new Promise(resolve => {
+ this.cachedViews = []
+ resolve([...this.cachedViews])
+ })
+ },
+ updateVisitedView(view) {
+ for (let v of this.visitedViews) {
+ if (v.path === view.path) {
+ v = Object.assign(v, view)
+ break
+ }
+ }
+ },
+ delRightTags(view) {
+ return new Promise(resolve => {
+ const index = this.visitedViews.findIndex(v => v.path === view.path)
+ if (index === -1) {
+ return
+ }
+ this.visitedViews = this.visitedViews.filter((item, idx) => {
+ if (idx <= index || (item.meta && item.meta.affix)) {
+ return true
+ }
+ const i = this.cachedViews.indexOf(item.name)
+ if (i > -1) {
+ this.cachedViews.splice(i, 1)
+ }
+ if(item.meta.link) {
+ const fi = this.iframeViews.findIndex(v => v.path === item.path)
+ this.iframeViews.splice(fi, 1)
+ }
+ return false
+ })
+ resolve([...this.visitedViews])
+ })
+ },
+ delLeftTags(view) {
+ return new Promise(resolve => {
+ const index = this.visitedViews.findIndex(v => v.path === view.path)
+ if (index === -1) {
+ return
+ }
+ this.visitedViews = this.visitedViews.filter((item, idx) => {
+ if (idx >= index || (item.meta && item.meta.affix)) {
+ return true
+ }
+ const i = this.cachedViews.indexOf(item.name)
+ if (i > -1) {
+ this.cachedViews.splice(i, 1)
+ }
+ if(item.meta.link) {
+ const fi = this.iframeViews.findIndex(v => v.path === item.path)
+ this.iframeViews.splice(fi, 1)
+ }
+ return false
+ })
+ resolve([...this.visitedViews])
+ })
+ }
+ }
+ })
+
+export default useTagsViewStore
diff --git a/ruoyi-ui-vue3/src/store/modules/user.js b/ruoyi-ui-vue3/src/store/modules/user.js
new file mode 100644
index 000000000..90fe853c1
--- /dev/null
+++ b/ruoyi-ui-vue3/src/store/modules/user.js
@@ -0,0 +1,70 @@
+import { login, logout, getInfo } from '@/api/login'
+import { getToken, setToken, removeToken } from '@/utils/auth'
+import defAva from '@/assets/images/profile.jpg'
+
+const useUserStore = defineStore(
+ 'user',
+ {
+ state: () => ({
+ token: getToken(),
+ name: '',
+ avatar: '',
+ roles: [],
+ permissions: []
+ }),
+ actions: {
+ // 登录
+ login(userInfo) {
+ const username = userInfo.username.trim()
+ const password = userInfo.password
+ const code = userInfo.code
+ const uuid = userInfo.uuid
+ return new Promise((resolve, reject) => {
+ login(username, password, code, uuid).then(res => {
+ setToken(res.data.token)
+ this.token = res.data.token
+ resolve()
+ }).catch(error => {
+ reject(error)
+ })
+ })
+ },
+ // 获取用户信息
+ getInfo() {
+ return new Promise((resolve, reject) => {
+ getInfo().then(res => {
+ const user = res.data.user
+ const avatar = (user.avatar == "" || user.avatar == null) ? defAva : user.avatar;
+
+ if (res.data.roles && res.data.roles.length > 0) { // 验证返回的roles是否是一个非空数组
+ this.roles = res.data.roles
+ this.permissions = res.data.permissions
+ } else {
+ this.roles = ['ROLE_DEFAULT']
+ }
+ this.name = user.userName
+ this.avatar = avatar;
+ resolve(res)
+ }).catch(error => {
+ reject(error)
+ })
+ })
+ },
+ // 退出系统
+ logOut() {
+ return new Promise((resolve, reject) => {
+ logout(this.token).then(() => {
+ this.token = ''
+ this.roles = []
+ this.permissions = []
+ removeToken()
+ resolve()
+ }).catch(error => {
+ reject(error)
+ })
+ })
+ }
+ }
+ })
+
+export default useUserStore
diff --git a/ruoyi-ui-vue3/src/utils/auth.js b/ruoyi-ui-vue3/src/utils/auth.js
new file mode 100644
index 000000000..08a43d6e2
--- /dev/null
+++ b/ruoyi-ui-vue3/src/utils/auth.js
@@ -0,0 +1,15 @@
+import Cookies from 'js-cookie'
+
+const TokenKey = 'Admin-Token'
+
+export function getToken() {
+ return Cookies.get(TokenKey)
+}
+
+export function setToken(token) {
+ return Cookies.set(TokenKey, token)
+}
+
+export function removeToken() {
+ return Cookies.remove(TokenKey)
+}
diff --git a/ruoyi-ui-vue3/src/utils/dict.js b/ruoyi-ui-vue3/src/utils/dict.js
new file mode 100644
index 000000000..9648f149e
--- /dev/null
+++ b/ruoyi-ui-vue3/src/utils/dict.js
@@ -0,0 +1,24 @@
+import useDictStore from '@/store/modules/dict'
+import { getDicts } from '@/api/system/dict/data'
+
+/**
+ * 获取字典数据
+ */
+export function useDict(...args) {
+ const res = ref({});
+ return (() => {
+ args.forEach((dictType, index) => {
+ res.value[dictType] = [];
+ const dicts = useDictStore().getDict(dictType);
+ if (dicts) {
+ res.value[dictType] = dicts;
+ } else {
+ getDicts(dictType).then(resp => {
+ res.value[dictType] = resp.data.map(p => ({ label: p.dictLabel, value: p.dictValue, elTagType: p.listClass, elTagClass: p.cssClass }))
+ useDictStore().setDict(dictType, res.value[dictType]);
+ })
+ }
+ })
+ return toRefs(res.value);
+ })()
+}
\ No newline at end of file
diff --git a/ruoyi-ui-vue3/src/utils/dynamicTitle.js b/ruoyi-ui-vue3/src/utils/dynamicTitle.js
new file mode 100644
index 000000000..64404b207
--- /dev/null
+++ b/ruoyi-ui-vue3/src/utils/dynamicTitle.js
@@ -0,0 +1,15 @@
+import store from '@/store'
+import defaultSettings from '@/settings'
+import useSettingsStore from '@/store/modules/settings'
+
+/**
+ * 动态修改标题
+ */
+export function useDynamicTitle() {
+ const settingsStore = useSettingsStore();
+ if (settingsStore.dynamicTitle) {
+ document.title = settingsStore.title + ' - ' + defaultSettings.title;
+ } else {
+ document.title = defaultSettings.title;
+ }
+}
\ No newline at end of file
diff --git a/ruoyi-ui-vue3/src/utils/errorCode.js b/ruoyi-ui-vue3/src/utils/errorCode.js
new file mode 100644
index 000000000..d2111ee10
--- /dev/null
+++ b/ruoyi-ui-vue3/src/utils/errorCode.js
@@ -0,0 +1,6 @@
+export default {
+ '401': '认证失败,无法访问系统资源',
+ '403': '当前操作没有权限',
+ '404': '访问资源不存在',
+ 'default': '系统未知错误,请反馈给管理员'
+}
diff --git a/ruoyi-ui-vue3/src/utils/index.js b/ruoyi-ui-vue3/src/utils/index.js
new file mode 100644
index 000000000..4e65504cd
--- /dev/null
+++ b/ruoyi-ui-vue3/src/utils/index.js
@@ -0,0 +1,390 @@
+import { parseTime } from './ruoyi'
+
+/**
+ * 表格时间格式化
+ */
+export function formatDate(cellValue) {
+ if (cellValue == null || cellValue == "") return "";
+ var date = new Date(cellValue)
+ var year = date.getFullYear()
+ var month = date.getMonth() + 1 < 10 ? '0' + (date.getMonth() + 1) : date.getMonth() + 1
+ var day = date.getDate() < 10 ? '0' + date.getDate() : date.getDate()
+ var hours = date.getHours() < 10 ? '0' + date.getHours() : date.getHours()
+ var minutes = date.getMinutes() < 10 ? '0' + date.getMinutes() : date.getMinutes()
+ var seconds = date.getSeconds() < 10 ? '0' + date.getSeconds() : date.getSeconds()
+ return year + '-' + month + '-' + day + ' ' + hours + ':' + minutes + ':' + seconds
+}
+
+/**
+ * @param {number} time
+ * @param {string} option
+ * @returns {string}
+ */
+export function formatTime(time, option) {
+ if (('' + time).length === 10) {
+ time = parseInt(time) * 1000
+ } else {
+ time = +time
+ }
+ const d = new Date(time)
+ const now = Date.now()
+
+ const diff = (now - d) / 1000
+
+ if (diff < 30) {
+ return '刚刚'
+ } else if (diff < 3600) {
+ // less 1 hour
+ return Math.ceil(diff / 60) + '分钟前'
+ } else if (diff < 3600 * 24) {
+ return Math.ceil(diff / 3600) + '小时前'
+ } else if (diff < 3600 * 24 * 2) {
+ return '1天前'
+ }
+ if (option) {
+ return parseTime(time, option)
+ } else {
+ return (
+ d.getMonth() +
+ 1 +
+ '月' +
+ d.getDate() +
+ '日' +
+ d.getHours() +
+ '时' +
+ d.getMinutes() +
+ '分'
+ )
+ }
+}
+
+/**
+ * @param {string} url
+ * @returns {Object}
+ */
+export function getQueryObject(url) {
+ url = url == null ? window.location.href : url
+ const search = url.substring(url.lastIndexOf('?') + 1)
+ const obj = {}
+ const reg = /([^?&=]+)=([^?&=]*)/g
+ search.replace(reg, (rs, $1, $2) => {
+ const name = decodeURIComponent($1)
+ let val = decodeURIComponent($2)
+ val = String(val)
+ obj[name] = val
+ return rs
+ })
+ return obj
+}
+
+/**
+ * @param {string} input value
+ * @returns {number} output value
+ */
+export function byteLength(str) {
+ // returns the byte length of an utf8 string
+ let s = str.length
+ for (var i = str.length - 1; i >= 0; i--) {
+ const code = str.charCodeAt(i)
+ if (code > 0x7f && code <= 0x7ff) s++
+ else if (code > 0x7ff && code <= 0xffff) s += 2
+ if (code >= 0xDC00 && code <= 0xDFFF) i--
+ }
+ return s
+}
+
+/**
+ * @param {Array} actual
+ * @returns {Array}
+ */
+export function cleanArray(actual) {
+ const newArray = []
+ for (let i = 0; i < actual.length; i++) {
+ if (actual[i]) {
+ newArray.push(actual[i])
+ }
+ }
+ return newArray
+}
+
+/**
+ * @param {Object} json
+ * @returns {Array}
+ */
+export function param(json) {
+ if (!json) return ''
+ return cleanArray(
+ Object.keys(json).map(key => {
+ if (json[key] === undefined) return ''
+ return encodeURIComponent(key) + '=' + encodeURIComponent(json[key])
+ })
+ ).join('&')
+}
+
+/**
+ * @param {string} url
+ * @returns {Object}
+ */
+export function param2Obj(url) {
+ const search = decodeURIComponent(url.split('?')[1]).replace(/\+/g, ' ')
+ if (!search) {
+ return {}
+ }
+ const obj = {}
+ const searchArr = search.split('&')
+ searchArr.forEach(v => {
+ const index = v.indexOf('=')
+ if (index !== -1) {
+ const name = v.substring(0, index)
+ const val = v.substring(index + 1, v.length)
+ obj[name] = val
+ }
+ })
+ return obj
+}
+
+/**
+ * @param {string} val
+ * @returns {string}
+ */
+export function html2Text(val) {
+ const div = document.createElement('div')
+ div.innerHTML = val
+ return div.textContent || div.innerText
+}
+
+/**
+ * Merges two objects, giving the last one precedence
+ * @param {Object} target
+ * @param {(Object|Array)} source
+ * @returns {Object}
+ */
+export function objectMerge(target, source) {
+ if (typeof target !== 'object') {
+ target = {}
+ }
+ if (Array.isArray(source)) {
+ return source.slice()
+ }
+ Object.keys(source).forEach(property => {
+ const sourceProperty = source[property]
+ if (typeof sourceProperty === 'object') {
+ target[property] = objectMerge(target[property], sourceProperty)
+ } else {
+ target[property] = sourceProperty
+ }
+ })
+ return target
+}
+
+/**
+ * @param {HTMLElement} element
+ * @param {string} className
+ */
+export function toggleClass(element, className) {
+ if (!element || !className) {
+ return
+ }
+ let classString = element.className
+ const nameIndex = classString.indexOf(className)
+ if (nameIndex === -1) {
+ classString += '' + className
+ } else {
+ classString =
+ classString.substr(0, nameIndex) +
+ classString.substr(nameIndex + className.length)
+ }
+ element.className = classString
+}
+
+/**
+ * @param {string} type
+ * @returns {Date}
+ */
+export function getTime(type) {
+ if (type === 'start') {
+ return new Date().getTime() - 3600 * 1000 * 24 * 90
+ } else {
+ return new Date(new Date().toDateString())
+ }
+}
+
+/**
+ * @param {Function} func
+ * @param {number} wait
+ * @param {boolean} immediate
+ * @return {*}
+ */
+export function debounce(func, wait, immediate) {
+ let timeout, args, context, timestamp, result
+
+ const later = function() {
+ // 据上一次触发时间间隔
+ const last = +new Date() - timestamp
+
+ // 上次被包装函数被调用时间间隔 last 小于设定时间间隔 wait
+ if (last < wait && last > 0) {
+ timeout = setTimeout(later, wait - last)
+ } else {
+ timeout = null
+ // 如果设定为immediate===true,因为开始边界已经调用过了此处无需调用
+ if (!immediate) {
+ result = func.apply(context, args)
+ if (!timeout) context = args = null
+ }
+ }
+ }
+
+ return function(...args) {
+ context = this
+ timestamp = +new Date()
+ const callNow = immediate && !timeout
+ // 如果延时不存在,重新设定延时
+ if (!timeout) timeout = setTimeout(later, wait)
+ if (callNow) {
+ result = func.apply(context, args)
+ context = args = null
+ }
+
+ return result
+ }
+}
+
+/**
+ * This is just a simple version of deep copy
+ * Has a lot of edge cases bug
+ * If you want to use a perfect deep copy, use lodash's _.cloneDeep
+ * @param {Object} source
+ * @returns {Object}
+ */
+export function deepClone(source) {
+ if (!source && typeof source !== 'object') {
+ throw new Error('error arguments', 'deepClone')
+ }
+ const targetObj = source.constructor === Array ? [] : {}
+ Object.keys(source).forEach(keys => {
+ if (source[keys] && typeof source[keys] === 'object') {
+ targetObj[keys] = deepClone(source[keys])
+ } else {
+ targetObj[keys] = source[keys]
+ }
+ })
+ return targetObj
+}
+
+/**
+ * @param {Array} arr
+ * @returns {Array}
+ */
+export function uniqueArr(arr) {
+ return Array.from(new Set(arr))
+}
+
+/**
+ * @returns {string}
+ */
+export function createUniqueString() {
+ const timestamp = +new Date() + ''
+ const randomNum = parseInt((1 + Math.random()) * 65536) + ''
+ return (+(randomNum + timestamp)).toString(32)
+}
+
+/**
+ * Check if an element has a class
+ * @param {HTMLElement} elm
+ * @param {string} cls
+ * @returns {boolean}
+ */
+export function hasClass(ele, cls) {
+ return !!ele.className.match(new RegExp('(\\s|^)' + cls + '(\\s|$)'))
+}
+
+/**
+ * Add class to element
+ * @param {HTMLElement} elm
+ * @param {string} cls
+ */
+export function addClass(ele, cls) {
+ if (!hasClass(ele, cls)) ele.className += ' ' + cls
+}
+
+/**
+ * Remove class from element
+ * @param {HTMLElement} elm
+ * @param {string} cls
+ */
+export function removeClass(ele, cls) {
+ if (hasClass(ele, cls)) {
+ const reg = new RegExp('(\\s|^)' + cls + '(\\s|$)')
+ ele.className = ele.className.replace(reg, ' ')
+ }
+}
+
+export function makeMap(str, expectsLowerCase) {
+ const map = Object.create(null)
+ const list = str.split(',')
+ for (let i = 0; i < list.length; i++) {
+ map[list[i]] = true
+ }
+ return expectsLowerCase
+ ? val => map[val.toLowerCase()]
+ : val => map[val]
+}
+
+export const exportDefault = 'export default '
+
+export const beautifierConf = {
+ html: {
+ indent_size: '2',
+ indent_char: ' ',
+ max_preserve_newlines: '-1',
+ preserve_newlines: false,
+ keep_array_indentation: false,
+ break_chained_methods: false,
+ indent_scripts: 'separate',
+ brace_style: 'end-expand',
+ space_before_conditional: true,
+ unescape_strings: false,
+ jslint_happy: false,
+ end_with_newline: true,
+ wrap_line_length: '110',
+ indent_inner_html: true,
+ comma_first: false,
+ e4x: true,
+ indent_empty_lines: true
+ },
+ js: {
+ indent_size: '2',
+ indent_char: ' ',
+ max_preserve_newlines: '-1',
+ preserve_newlines: false,
+ keep_array_indentation: false,
+ break_chained_methods: false,
+ indent_scripts: 'normal',
+ brace_style: 'end-expand',
+ space_before_conditional: true,
+ unescape_strings: false,
+ jslint_happy: true,
+ end_with_newline: true,
+ wrap_line_length: '110',
+ indent_inner_html: true,
+ comma_first: false,
+ e4x: true,
+ indent_empty_lines: true
+ }
+}
+
+// 首字母大小
+export function titleCase(str) {
+ return str.replace(/( |^)[a-z]/g, L => L.toUpperCase())
+}
+
+// 下划转驼峰
+export function camelCase(str) {
+ return str.replace(/_[a-z]/g, str1 => str1.substr(-1).toUpperCase())
+}
+
+export function isNumberStr(str) {
+ return /^[+-]?(0|([1-9]\d*))(\.\d+)?$/g.test(str)
+}
+
diff --git a/ruoyi-ui-vue3/src/utils/jsencrypt.js b/ruoyi-ui-vue3/src/utils/jsencrypt.js
new file mode 100644
index 000000000..78d95234a
--- /dev/null
+++ b/ruoyi-ui-vue3/src/utils/jsencrypt.js
@@ -0,0 +1,30 @@
+import JSEncrypt from 'jsencrypt/bin/jsencrypt.min'
+
+// 密钥对生成 http://web.chacuo.net/netrsakeypair
+
+const publicKey = 'MFwwDQYJKoZIhvcNAQEBBQADSwAwSAJBAKoR8mX0rGKLqzcWmOzbfj64K8ZIgOdH\n' +
+ 'nzkXSOVOZbFu/TJhZ7rFAN+eaGkl3C4buccQd/EjEsj9ir7ijT7h96MCAwEAAQ=='
+
+const privateKey = 'MIIBVAIBADANBgkqhkiG9w0BAQEFAASCAT4wggE6AgEAAkEAqhHyZfSsYourNxaY\n' +
+ '7Nt+PrgrxkiA50efORdI5U5lsW79MmFnusUA355oaSXcLhu5xxB38SMSyP2KvuKN\n' +
+ 'PuH3owIDAQABAkAfoiLyL+Z4lf4Myxk6xUDgLaWGximj20CUf+5BKKnlrK+Ed8gA\n' +
+ 'kM0HqoTt2UZwA5E2MzS4EI2gjfQhz5X28uqxAiEA3wNFxfrCZlSZHb0gn2zDpWow\n' +
+ 'cSxQAgiCstxGUoOqlW8CIQDDOerGKH5OmCJ4Z21v+F25WaHYPxCFMvwxpcw99Ecv\n' +
+ 'DQIgIdhDTIqD2jfYjPTY8Jj3EDGPbH2HHuffvflECt3Ek60CIQCFRlCkHpi7hthh\n' +
+ 'YhovyloRYsM+IS9h/0BzlEAuO0ktMQIgSPT3aFAgJYwKpqRYKlLDVcflZFCKY7u3\n' +
+ 'UP8iWi1Qw0Y='
+
+// 加密
+export function encrypt(txt) {
+ const encryptor = new JSEncrypt()
+ encryptor.setPublicKey(publicKey) // 设置公钥
+ return encryptor.encrypt(txt) // 对数据进行加密
+}
+
+// 解密
+export function decrypt(txt) {
+ const encryptor = new JSEncrypt()
+ encryptor.setPrivateKey(privateKey) // 设置私钥
+ return encryptor.decrypt(txt) // 对数据进行解密
+}
+
diff --git a/ruoyi-ui-vue3/src/utils/permission.js b/ruoyi-ui-vue3/src/utils/permission.js
new file mode 100644
index 000000000..93fee87f6
--- /dev/null
+++ b/ruoyi-ui-vue3/src/utils/permission.js
@@ -0,0 +1,51 @@
+import useUserStore from '@/store/modules/user'
+
+/**
+ * 字符权限校验
+ * @param {Array} value 校验值
+ * @returns {Boolean}
+ */
+export function checkPermi(value) {
+ if (value && value instanceof Array && value.length > 0) {
+ const permissions = useUserStore().permissions
+ const permissionDatas = value
+ const all_permission = "*:*:*";
+
+ const hasPermission = permissions.some(permission => {
+ return all_permission === permission || permissionDatas.includes(permission)
+ })
+
+ if (!hasPermission) {
+ return false
+ }
+ return true
+ } else {
+ console.error(`need roles! Like checkPermi="['system:user:add','system:user:edit']"`)
+ return false
+ }
+}
+
+/**
+ * 角色权限校验
+ * @param {Array} value 校验值
+ * @returns {Boolean}
+ */
+export function checkRole(value) {
+ if (value && value instanceof Array && value.length > 0) {
+ const roles = useUserStore().roles
+ const permissionRoles = value
+ const super_admin = "admin";
+
+ const hasRole = roles.some(role => {
+ return super_admin === role || permissionRoles.includes(role)
+ })
+
+ if (!hasRole) {
+ return false
+ }
+ return true
+ } else {
+ console.error(`need roles! Like checkRole="['admin','editor']"`)
+ return false
+ }
+}
\ No newline at end of file
diff --git a/ruoyi-ui-vue3/src/utils/request.js b/ruoyi-ui-vue3/src/utils/request.js
new file mode 100644
index 000000000..1da35094e
--- /dev/null
+++ b/ruoyi-ui-vue3/src/utils/request.js
@@ -0,0 +1,148 @@
+import axios from 'axios'
+import { ElNotification , ElMessageBox, ElMessage, ElLoading } from 'element-plus'
+import { getToken } from '@/utils/auth'
+import errorCode from '@/utils/errorCode'
+import { tansParams, blobValidate } from '@/utils/ruoyi'
+import cache from '@/plugins/cache'
+import { saveAs } from 'file-saver'
+import useUserStore from '@/store/modules/user'
+
+let downloadLoadingInstance;
+// 是否显示重新登录
+export let isRelogin = { show: false };
+
+axios.defaults.headers['Content-Type'] = 'application/json;charset=utf-8'
+// 对应国际化资源文件后缀
+axios.defaults.headers['Content-Language'] = 'zh_CN'
+// 创建axios实例
+const service = axios.create({
+ // axios中请求配置有baseURL选项,表示请求URL公共部分
+ baseURL: import.meta.env.VITE_APP_BASE_API,
+ // 超时
+ timeout: 10000
+})
+
+// request拦截器
+service.interceptors.request.use(config => {
+ // 是否需要设置 token
+ const isToken = (config.headers || {}).isToken === false
+ // 是否需要防止数据重复提交
+ const isRepeatSubmit = (config.headers || {}).repeatSubmit === false
+ if (getToken() && !isToken) {
+ config.headers['Authorization'] = 'Bearer ' + getToken() // 让每个请求携带自定义token 请根据实际情况自行修改
+ }
+ // get请求映射params参数
+ if (config.method === 'get' && config.params) {
+ let url = config.url + '?' + tansParams(config.params);
+ url = url.slice(0, -1);
+ config.params = {};
+ config.url = url;
+ }
+ if (!isRepeatSubmit && (config.method === 'post' || config.method === 'put')) {
+ const requestObj = {
+ url: config.url,
+ data: typeof config.data === 'object' ? JSON.stringify(config.data) : config.data,
+ time: new Date().getTime()
+ }
+ const sessionObj = cache.session.getJSON('sessionObj')
+ if (sessionObj === undefined || sessionObj === null || sessionObj === '') {
+ cache.session.setJSON('sessionObj', requestObj)
+ } else {
+ const s_url = sessionObj.url; // 请求地址
+ const s_data = sessionObj.data; // 请求数据
+ const s_time = sessionObj.time; // 请求时间
+ const interval = 1000; // 间隔时间(ms),小于此时间视为重复提交
+ if (s_data === requestObj.data && requestObj.time - s_time < interval && s_url === requestObj.url) {
+ const message = '数据正在处理,请勿重复提交';
+ console.warn(`[${s_url}]: ` + message)
+ return Promise.reject(new Error(message))
+ } else {
+ cache.session.setJSON('sessionObj', requestObj)
+ }
+ }
+ }
+ return config
+}, error => {
+ console.log(error)
+ Promise.reject(error)
+})
+
+// 响应拦截器
+service.interceptors.response.use(res => {
+ // 未设置状态码则默认成功状态
+ const code = res.data.code || 200;
+ // 获取错误信息
+ const msg = errorCode[code] || res.data.msg || errorCode['default']
+ // 二进制数据则直接返回
+ if (res.request.responseType === 'blob' || res.request.responseType === 'arraybuffer') {
+ return res.data
+ }
+ if (code === 401) {
+ if (!isRelogin.show) {
+ isRelogin.show = true;
+ ElMessageBox.confirm('登录状态已过期,您可以继续留在该页面,或者重新登录', '系统提示', { confirmButtonText: '重新登录', cancelButtonText: '取消', type: 'warning' }).then(() => {
+ isRelogin.show = false;
+ useUserStore().logOut().then(() => {
+ location.href = import.meta.env.VITE_APP_CONTEXT_PATH + 'index';
+ })
+ }).catch(() => {
+ isRelogin.show = false;
+ });
+ }
+ return Promise.reject('无效的会话,或者会话已过期,请重新登录。')
+ } else if (code === 500) {
+ ElMessage({ message: msg, type: 'error' })
+ return Promise.reject(new Error(msg))
+ } else if (code === 601) {
+ ElMessage({ message: msg, type: 'warning' })
+ return Promise.reject(new Error(msg))
+ } else if (code !== 200) {
+ ElNotification.error({ title: msg })
+ return Promise.reject('error')
+ } else {
+ return Promise.resolve(res.data)
+ }
+ },
+ error => {
+ console.log('err' + error)
+ let { message } = error;
+ if (message == "Network Error") {
+ message = "后端接口连接异常";
+ } else if (message.includes("timeout")) {
+ message = "系统接口请求超时";
+ } else if (message.includes("Request failed with status code")) {
+ message = "系统接口" + message.substr(message.length - 3) + "异常";
+ }
+ ElMessage({ message: message, type: 'error', duration: 5 * 1000 })
+ return Promise.reject(error)
+ }
+)
+
+// 通用下载方法
+export function download(url, params, filename, config) {
+ downloadLoadingInstance = ElLoading.service({ text: "正在下载数据,请稍候", background: "rgba(0, 0, 0, 0.7)", })
+ return service.post(url, params, {
+ transformRequest: [(params) => { return tansParams(params) }],
+ headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
+ responseType: 'blob',
+ ...config
+ }).then(async (data) => {
+ const isBlob = blobValidate(data);
+ if (isBlob) {
+ const blob = new Blob([data])
+ saveAs(blob, filename)
+ } else {
+ const resText = await data.text();
+ const rspObj = JSON.parse(resText);
+ const errMsg = errorCode[rspObj.code] || rspObj.msg || errorCode['default']
+ ElMessage.error(errMsg);
+ }
+ downloadLoadingInstance.close();
+ }).catch((r) => {
+ console.error(r)
+ ElMessage.error('下载文件出现错误,请联系管理员!')
+ downloadLoadingInstance.close();
+ })
+}
+
+export default service
diff --git a/ruoyi-ui-vue3/src/utils/ruoyi.js b/ruoyi-ui-vue3/src/utils/ruoyi.js
new file mode 100644
index 000000000..4efca08f6
--- /dev/null
+++ b/ruoyi-ui-vue3/src/utils/ruoyi.js
@@ -0,0 +1,246 @@
+
+
+/**
+ * 通用js方法封装处理
+ * Copyright (c) 2019 ruoyi
+ */
+
+// 日期格式化
+export function parseTime(time, pattern) {
+ if (arguments.length === 0 || !time) {
+ return null
+ }
+ const format = pattern || '{y}-{m}-{d} {h}:{i}:{s}'
+ let date
+ if (typeof time === 'object') {
+ date = time
+ } else {
+ if ((typeof time === 'string') && (/^[0-9]+$/.test(time))) {
+ time = parseInt(time)
+ } else if (typeof time === 'string') {
+ time = time.replace(new RegExp(/-/gm), '/').replace('T', ' ').replace(new RegExp(/\.[\d]{3}/gm), '');
+ }
+ if ((typeof time === 'number') && (time.toString().length === 10)) {
+ time = time * 1000
+ }
+ date = new Date(time)
+ }
+ const formatObj = {
+ y: date.getFullYear(),
+ m: date.getMonth() + 1,
+ d: date.getDate(),
+ h: date.getHours(),
+ i: date.getMinutes(),
+ s: date.getSeconds(),
+ a: date.getDay()
+ }
+ const time_str = format.replace(/{(y|m|d|h|i|s|a)+}/g, (result, key) => {
+ let value = formatObj[key]
+ // Note: getDay() returns 0 on Sunday
+ if (key === 'a') { return ['日', '一', '二', '三', '四', '五', '六'][value] }
+ if (result.length > 0 && value < 10) {
+ value = '0' + value
+ }
+ return value || 0
+ })
+ return time_str
+}
+
+// 表单重置
+export function resetForm(refName) {
+ if (this.$refs[refName]) {
+ this.$refs[refName].resetFields();
+ }
+}
+
+// 添加日期范围
+export function addDateRange(params, dateRange, propName) {
+ let search = params;
+ search.params = typeof (search.params) === 'object' && search.params !== null && !Array.isArray(search.params) ? search.params : {};
+ dateRange = Array.isArray(dateRange) ? dateRange : [];
+ if (typeof (propName) === 'undefined') {
+ search.params['beginTime'] = dateRange[0];
+ search.params['endTime'] = dateRange[1];
+ } else {
+ search.params['begin' + propName] = dateRange[0];
+ search.params['end' + propName] = dateRange[1];
+ }
+ return search;
+}
+
+// 回显数据字典
+export function selectDictLabel(datas, value) {
+ if (value === undefined) {
+ return "";
+ }
+ var actions = [];
+ Object.keys(datas).some((key) => {
+ if (datas[key].value == ('' + value)) {
+ actions.push(datas[key].label);
+ return true;
+ }
+ })
+ if (actions.length === 0) {
+ actions.push(value);
+ }
+ return actions.join('');
+}
+
+// 回显数据字典(字符串数组)
+export function selectDictLabels(datas, value, separator) {
+ if (value === undefined || value.length ===0) {
+ return "";
+ }
+ if (Array.isArray(value)) {
+ value = value.join(",");
+ }
+ var actions = [];
+ var currentSeparator = undefined === separator ? "," : separator;
+ var temp = value.split(currentSeparator);
+ Object.keys(value.split(currentSeparator)).some((val) => {
+ var match = false;
+ Object.keys(datas).some((key) => {
+ if (datas[key].value == ('' + temp[val])) {
+ actions.push(datas[key].label + currentSeparator);
+ match = true;
+ }
+ })
+ if (!match) {
+ actions.push(temp[val] + currentSeparator);
+ }
+ })
+ return actions.join('').substring(0, actions.join('').length - 1);
+}
+
+// 字符串格式化(%s )
+export function sprintf(str) {
+ var args = arguments, flag = true, i = 1;
+ str = str.replace(/%s/g, function () {
+ var arg = args[i++];
+ if (typeof arg === 'undefined') {
+ flag = false;
+ return '';
+ }
+ return arg;
+ });
+ return flag ? str : '';
+}
+
+// 转换字符串,undefined,null等转化为""
+export function parseStrEmpty(str) {
+ if (!str || str == "undefined" || str == "null") {
+ return "";
+ }
+ return str;
+}
+
+// 数据合并
+export function mergeRecursive(source, target) {
+ for (var p in target) {
+ try {
+ if (target[p].constructor == Object) {
+ source[p] = mergeRecursive(source[p], target[p]);
+ } else {
+ source[p] = target[p];
+ }
+ } catch (e) {
+ source[p] = target[p];
+ }
+ }
+ return source;
+};
+
+/**
+ * 构造树型结构数据
+ * @param {*} data 数据源
+ * @param {*} id id字段 默认 'id'
+ * @param {*} parentId 父节点字段 默认 'parentId'
+ * @param {*} children 孩子节点字段 默认 'children'
+ */
+export function handleTree(data, id, parentId, children) {
+ let config = {
+ id: id || 'id',
+ parentId: parentId || 'parentId',
+ childrenList: children || 'children'
+ };
+
+ var childrenListMap = {};
+ var nodeIds = {};
+ var tree = [];
+
+ for (let d of data) {
+ let parentId = d[config.parentId];
+ if (childrenListMap[parentId] == null) {
+ childrenListMap[parentId] = [];
+ }
+ nodeIds[d[config.id]] = d;
+ childrenListMap[parentId].push(d);
+ }
+
+ for (let d of data) {
+ let parentId = d[config.parentId];
+ if (nodeIds[parentId] == null) {
+ tree.push(d);
+ }
+ }
+
+ for (let t of tree) {
+ adaptToChildrenList(t);
+ }
+
+ function adaptToChildrenList(o) {
+ if (childrenListMap[o[config.id]] !== null) {
+ o[config.childrenList] = childrenListMap[o[config.id]];
+ }
+ if (o[config.childrenList]) {
+ for (let c of o[config.childrenList]) {
+ adaptToChildrenList(c);
+ }
+ }
+ }
+ return tree;
+}
+
+/**
+* 参数处理
+* @param {*} params 参数
+*/
+export function tansParams(params) {
+ let result = ''
+ for (const propName of Object.keys(params)) {
+ const value = params[propName];
+ var part = encodeURIComponent(propName) + "=";
+ if (value !== null && value !== "" && typeof (value) !== "undefined") {
+ if (typeof value === 'object') {
+ for (const key of Object.keys(value)) {
+ if (value[key] !== null && value[key] !== "" && typeof (value[key]) !== 'undefined') {
+ let params = propName + '[' + key + ']';
+ var subPart = encodeURIComponent(params) + "=";
+ result += subPart + encodeURIComponent(value[key]) + "&";
+ }
+ }
+ } else {
+ result += part + encodeURIComponent(value) + "&";
+ }
+ }
+ }
+ return result
+}
+
+
+// 返回项目路径
+export function getNormalPath(p) {
+ if (p.length === 0 || !p || p == 'undefined') {
+ return p
+ };
+ let res = p.replace('//', '/')
+ if (res[res.length - 1] === '/') {
+ return res.slice(0, res.length - 1)
+ }
+ return res;
+}
+
+// 验证是否为blob格式
+export function blobValidate(data) {
+ return data.type !== 'application/json'
+}
diff --git a/ruoyi-ui-vue3/src/utils/scroll-to.js b/ruoyi-ui-vue3/src/utils/scroll-to.js
new file mode 100644
index 000000000..c5d8e04e0
--- /dev/null
+++ b/ruoyi-ui-vue3/src/utils/scroll-to.js
@@ -0,0 +1,58 @@
+Math.easeInOutQuad = function(t, b, c, d) {
+ t /= d / 2
+ if (t < 1) {
+ return c / 2 * t * t + b
+ }
+ t--
+ return -c / 2 * (t * (t - 2) - 1) + b
+}
+
+// requestAnimationFrame for Smart Animating http://goo.gl/sx5sts
+var requestAnimFrame = (function() {
+ return window.requestAnimationFrame || window.webkitRequestAnimationFrame || window.mozRequestAnimationFrame || function(callback) { window.setTimeout(callback, 1000 / 60) }
+})()
+
+/**
+ * Because it's so fucking difficult to detect the scrolling element, just move them all
+ * @param {number} amount
+ */
+function move(amount) {
+ document.documentElement.scrollTop = amount
+ document.body.parentNode.scrollTop = amount
+ document.body.scrollTop = amount
+}
+
+function position() {
+ return document.documentElement.scrollTop || document.body.parentNode.scrollTop || document.body.scrollTop
+}
+
+/**
+ * @param {number} to
+ * @param {number} duration
+ * @param {Function} callback
+ */
+export function scrollTo(to, duration, callback) {
+ const start = position()
+ const change = to - start
+ const increment = 20
+ let currentTime = 0
+ duration = (typeof (duration) === 'undefined') ? 500 : duration
+ var animateScroll = function() {
+ // increment the time
+ currentTime += increment
+ // find the value with the quadratic in-out easing function
+ var val = Math.easeInOutQuad(currentTime, start, change, duration)
+ // move the document.body
+ move(val)
+ // do the animation unless its over
+ if (currentTime < duration) {
+ requestAnimFrame(animateScroll)
+ } else {
+ if (callback && typeof (callback) === 'function') {
+ // the animation is done so lets callback
+ callback()
+ }
+ }
+ }
+ animateScroll()
+}
diff --git a/ruoyi-ui-vue3/src/utils/theme.js b/ruoyi-ui-vue3/src/utils/theme.js
new file mode 100644
index 000000000..f4badc67e
--- /dev/null
+++ b/ruoyi-ui-vue3/src/utils/theme.js
@@ -0,0 +1,49 @@
+// 处理主题样式
+export function handleThemeStyle(theme) {
+ document.documentElement.style.setProperty('--el-color-primary', theme)
+ for (let i = 1; i <= 9; i++) {
+ document.documentElement.style.setProperty(`--el-color-primary-light-${i}`, `${getLightColor(theme, i / 10)}`)
+ }
+ for (let i = 1; i <= 9; i++) {
+ document.documentElement.style.setProperty(`--el-color-primary-dark-${i}`, `${getDarkColor(theme, i / 10)}`)
+ }
+}
+
+// hex颜色转rgb颜色
+export function hexToRgb(str) {
+ str = str.replace('#', '')
+ let hexs = str.match(/../g)
+ for (let i = 0; i < 3; i++) {
+ hexs[i] = parseInt(hexs[i], 16)
+ }
+ return hexs
+}
+
+// rgb颜色转Hex颜色
+export function rgbToHex(r, g, b) {
+ let hexs = [r.toString(16), g.toString(16), b.toString(16)]
+ for (let i = 0; i < 3; i++) {
+ if (hexs[i].length == 1) {
+ hexs[i] = `0${hexs[i]}`
+ }
+ }
+ return `#${hexs.join('')}`
+}
+
+// 变浅颜色值
+export function getLightColor(color, level) {
+ let rgb = hexToRgb(color)
+ for (let i = 0; i < 3; i++) {
+ rgb[i] = Math.floor((255 - rgb[i]) * level + rgb[i])
+ }
+ return rgbToHex(rgb[0], rgb[1], rgb[2])
+}
+
+// 变深颜色值
+export function getDarkColor(color, level) {
+ let rgb = hexToRgb(color)
+ for (let i = 0; i < 3; i++) {
+ rgb[i] = Math.floor(rgb[i] * (1 - level))
+ }
+ return rgbToHex(rgb[0], rgb[1], rgb[2])
+}
diff --git a/ruoyi-ui-vue3/src/utils/validate.js b/ruoyi-ui-vue3/src/utils/validate.js
new file mode 100644
index 000000000..702add4b5
--- /dev/null
+++ b/ruoyi-ui-vue3/src/utils/validate.js
@@ -0,0 +1,93 @@
+/**
+ * 判断url是否是http或https
+ * @param {string} path
+ * @returns {Boolean}
+ */
+ export function isHttp(url) {
+ return url.indexOf('http://') !== -1 || url.indexOf('https://') !== -1
+}
+
+/**
+ * 判断path是否为外链
+ * @param {string} path
+ * @returns {Boolean}
+ */
+ export function isExternal(path) {
+ return /^(https?:|mailto:|tel:)/.test(path)
+}
+
+/**
+ * @param {string} str
+ * @returns {Boolean}
+ */
+export function validUsername(str) {
+ const valid_map = ['admin', 'editor']
+ return valid_map.indexOf(str.trim()) >= 0
+}
+
+/**
+ * @param {string} url
+ * @returns {Boolean}
+ */
+export function validURL(url) {
+ const reg = /^(https?|ftp):\/\/([a-zA-Z0-9.-]+(:[a-zA-Z0-9.&%$-]+)*@)*((25[0-5]|2[0-4][0-9]|1[0-9]{2}|[1-9][0-9]?)(\.(25[0-5]|2[0-4][0-9]|1[0-9]{2}|[1-9]?[0-9])){3}|([a-zA-Z0-9-]+\.)*[a-zA-Z0-9-]+\.(com|edu|gov|int|mil|net|org|biz|arpa|info|name|pro|aero|coop|museum|[a-zA-Z]{2}))(:[0-9]+)*(\/($|[a-zA-Z0-9.,?'\\+&%$#=~_-]+))*$/
+ return reg.test(url)
+}
+
+/**
+ * @param {string} str
+ * @returns {Boolean}
+ */
+export function validLowerCase(str) {
+ const reg = /^[a-z]+$/
+ return reg.test(str)
+}
+
+/**
+ * @param {string} str
+ * @returns {Boolean}
+ */
+export function validUpperCase(str) {
+ const reg = /^[A-Z]+$/
+ return reg.test(str)
+}
+
+/**
+ * @param {string} str
+ * @returns {Boolean}
+ */
+export function validAlphabets(str) {
+ const reg = /^[A-Za-z]+$/
+ return reg.test(str)
+}
+
+/**
+ * @param {string} email
+ * @returns {Boolean}
+ */
+export function validEmail(email) {
+ const reg = /^(([^<>()\[\]\\.,;:\s@"]+(\.[^<>()\[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/
+ return reg.test(email)
+}
+
+/**
+ * @param {string} str
+ * @returns {Boolean}
+ */
+export function isString(str) {
+ if (typeof str === 'string' || str instanceof String) {
+ return true
+ }
+ return false
+}
+
+/**
+ * @param {Array} arg
+ * @returns {Boolean}
+ */
+export function isArray(arg) {
+ if (typeof Array.isArray === 'undefined') {
+ return Object.prototype.toString.call(arg) === '[object Array]'
+ }
+ return Array.isArray(arg)
+}
diff --git a/ruoyi-ui-vue3/src/views/demo/demo/index.vue b/ruoyi-ui-vue3/src/views/demo/demo/index.vue
new file mode 100644
index 000000000..fa81f82f1
--- /dev/null
+++ b/ruoyi-ui-vue3/src/views/demo/demo/index.vue
@@ -0,0 +1,403 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 搜索
+ 搜索(自定义分页接口)
+ 重置
+
+
+
+
+
+ 新增
+
+
+ 修改
+
+
+ 删除
+
+
+ 导入(校验)
+
+
+ 导出
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ parseTime(scope.row.createTime, '{y}-{m}-{d}') }}
+
+
+
+
+
+ {{ parseTime(scope.row.updateTime, '{y}-{m}-{d}') }}
+
+
+
+
+
+ 修改
+ 删除
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 将文件拖到此处,或点击上传
+
+
+
+
+
+
+
diff --git a/ruoyi-ui-vue3/src/views/demo/tree/index.vue b/ruoyi-ui-vue3/src/views/demo/tree/index.vue
new file mode 100644
index 000000000..4fa8ffd75
--- /dev/null
+++ b/ruoyi-ui-vue3/src/views/demo/tree/index.vue
@@ -0,0 +1,281 @@
+
+
+
+
+
+
+
+
+
+
+ 搜索
+ 重置
+
+
+
+
+
+ 新增
+
+
+ 展开/折叠
+
+
+
+
+
+
+
+
+
+
+
+ {{ parseTime(scope.row.createTime, '{y}-{m}-{d}') }}
+
+
+
+
+ 修改
+ 新增
+ 删除
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/ruoyi-ui-vue3/src/views/error/401.vue b/ruoyi-ui-vue3/src/views/error/401.vue
new file mode 100644
index 000000000..1ba379221
--- /dev/null
+++ b/ruoyi-ui-vue3/src/views/error/401.vue
@@ -0,0 +1,82 @@
+
+
+
+ 返回
+
+
+
+
+ 401错误!
+
+ 您没有访问权限!
+ 对不起,您没有访问权限,请不要进行非法操作!您可以返回主页面
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/ruoyi-ui-vue3/src/views/error/404.vue b/ruoyi-ui-vue3/src/views/error/404.vue
new file mode 100644
index 000000000..f20530322
--- /dev/null
+++ b/ruoyi-ui-vue3/src/views/error/404.vue
@@ -0,0 +1,227 @@
+
+
+
+
+
+
+ 404错误!
+
+
+ {{ message }}
+
+
+ 对不起,您正在寻找的页面不存在。尝试检查URL的错误,然后按浏览器上的刷新按钮或尝试在我们的应用程序中找到其他内容。
+
+
+ 返回首页
+
+
+
+
+
+
+
+
+
diff --git a/ruoyi-ui-vue3/src/views/index.vue b/ruoyi-ui-vue3/src/views/index.vue
new file mode 100644
index 000000000..148d17a47
--- /dev/null
+++ b/ruoyi-ui-vue3/src/views/index.vue
@@ -0,0 +1,176 @@
+
+
+
+
+ RuoYi-Vue-Plus后台管理系统
+
+ RuoYi-Vue-Plus 是基于 RuoYi-Vue 针对 分布式集群 场景升级(不兼容原框架)
+
+ * 前端开发框架 Vue、Element UI
+ * 后端开发框架 Spring Boot
+ * 容器框架 Undertow 基于 Netty 的高性能容器
+ * 权限认证框架 Sa-Token 支持多终端认证系统
+ * 关系数据库 MySQL 适配 8.X 最低 5.7
+ * 缓存数据库 Redis 适配 6.X 最低 4.X
+ * 数据库框架 Mybatis-Plus 快速 CRUD 增加开发效率
+ * 数据库框架 p6spy 更强劲的 SQL 分析
+ * 多数据源框架 dynamic-datasource 支持主从与多种类数据库异构
+ * 序列化框架 Jackson 统一使用 jackson 高效可靠
+ * Redis客户端 Redisson 性能强劲、API丰富
+ * 分布式限流 Redisson 全局、请求IP、集群ID 多种限流
+ * 分布式锁 Lock4j 注解锁、工具锁 多种多样
+ * 分布式幂等 Lock4j 基于分布式锁实现
+ * 分布式链路追踪 SkyWalking 支持链路追踪、网格分析、度量聚合、可视化
+ * 分布式任务调度 Xxl-Job 高性能 高可靠 易扩展
+ * 文件存储 Minio 本地存储
+ * 文件存储 七牛、阿里、腾讯 云存储
+ * 监控框架 SpringBoot-Admin 全方位服务监控
+ * 校验框架 Validation 增强接口安全性 严谨性
+ * Excel框架 Alibaba EasyExcel 性能优异 扩展性强
+ * 文档框架 SpringDoc、javadoc 无注解零入侵基于java注释
+ * 工具类框架 Hutool、Lombok 减少代码冗余 增加安全性
+ * 代码生成器 适配MP、SpringDoc规范化代码 一键生成前后端代码
+ * 部署方式 Docker 容器编排 一键部署业务集群
+ * 国际化 SpringMessage Spring标准国际化方案
+
+
+ 当前版本: v{{ version }}
+
+
+ ¥免费开源
+
+
+ 访问码云
+ 访问GitHub
+ 更新日志
+
+
+
+
+
+
+ 技术选型
+
+
+
+
+ 后端技术
+
+ SpringBoot
+ Sa-Token
+ JWT
+ MyBatis
+ Druid
+ Jackson
+ ...
+
+
+
+ 前端技术
+
+ Vue
+ Vuex
+ Element-ui
+ Axios
+ Sass
+ Quill
+ ...
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/ruoyi-ui-vue3/src/views/login.vue b/ruoyi-ui-vue3/src/views/login.vue
new file mode 100644
index 000000000..4dd95d3cb
--- /dev/null
+++ b/ruoyi-ui-vue3/src/views/login.vue
@@ -0,0 +1,215 @@
+
+
+
+ RuoYi-Vue-Plus后台管理系统
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 记住密码
+
+
+ 登 录
+ 登 录 中...
+
+
+ 立即注册
+
+
+
+
+
+
+
+
+
+
+
diff --git a/ruoyi-ui-vue3/src/views/monitor/admin/index.vue b/ruoyi-ui-vue3/src/views/monitor/admin/index.vue
new file mode 100644
index 000000000..1b13d64d7
--- /dev/null
+++ b/ruoyi-ui-vue3/src/views/monitor/admin/index.vue
@@ -0,0 +1,13 @@
+
+
+
+
+
+
+
diff --git a/ruoyi-ui-vue3/src/views/monitor/cache/index.vue b/ruoyi-ui-vue3/src/views/monitor/cache/index.vue
new file mode 100644
index 000000000..ccca20d6d
--- /dev/null
+++ b/ruoyi-ui-vue3/src/views/monitor/cache/index.vue
@@ -0,0 +1,132 @@
+
+
+
+
+
+ 基本信息
+
+
+
+
+ Redis版本
+ {{ cache.info.redis_version }}
+ 运行模式
+ {{ cache.info.redis_mode == "standalone" ? "单机" : "集群" }}
+ 端口
+ {{ cache.info.tcp_port }}
+ 客户端数
+ {{ cache.info.connected_clients }}
+
+
+ 运行时间(天)
+ {{ cache.info.uptime_in_days }}
+ 使用内存
+ {{ cache.info.used_memory_human }}
+ 使用CPU
+ {{ parseFloat(cache.info.used_cpu_user_children).toFixed(2) }}
+ 内存配置
+ {{ cache.info.maxmemory_human }}
+
+
+ AOF是否开启
+ {{ cache.info.aof_enabled == "0" ? "否" : "是" }}
+ RDB是否成功
+ {{ cache.info.rdb_last_bgsave_status }}
+ Key数量
+ {{ cache.dbSize }}
+ 网络入口/出口
+ {{ cache.info.instantaneous_input_kbps }}kps/{{cache.info.instantaneous_output_kbps}}kps
+
+
+
+
+
+
+
+
+
+ 命令统计
+
+
+
+
+
+
+ 内存信息
+
+
+
+
+
+
+
+
diff --git a/ruoyi-ui-vue3/src/views/monitor/cache/list.vue b/ruoyi-ui-vue3/src/views/monitor/cache/list.vue
new file mode 100644
index 000000000..072879944
--- /dev/null
+++ b/ruoyi-ui-vue3/src/views/monitor/cache/list.vue
@@ -0,0 +1,246 @@
+
+
+
+
+
+
+ 缓存列表
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 键名列表
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 缓存内容
+ 清理全部
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/ruoyi-ui-vue3/src/views/monitor/logininfor/index.vue b/ruoyi-ui-vue3/src/views/monitor/logininfor/index.vue
new file mode 100644
index 000000000..bd58a5db6
--- /dev/null
+++ b/ruoyi-ui-vue3/src/views/monitor/logininfor/index.vue
@@ -0,0 +1,225 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 搜索
+ 重置
+
+
+
+
+
+ 删除
+
+
+ 清空
+
+
+ 解锁
+
+
+ 导出
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ parseTime(scope.row.loginTime) }}
+
+
+
+
+
+
+
+
+
diff --git a/ruoyi-ui-vue3/src/views/monitor/online/index.vue b/ruoyi-ui-vue3/src/views/monitor/online/index.vue
new file mode 100644
index 000000000..eb17ebc3d
--- /dev/null
+++ b/ruoyi-ui-vue3/src/views/monitor/online/index.vue
@@ -0,0 +1,106 @@
+
+
+
+
+
+
+
+
+
+
+ 搜索
+ 重置
+
+
+
+
+
+ {{ (pageNum - 1) * pageSize + scope.$index + 1 }}
+
+
+
+
+
+
+
+
+
+
+
+ {{ parseTime(scope.row.loginTime) }}
+
+
+
+
+ 强退
+
+
+
+
+
+
+
+
+
diff --git a/ruoyi-ui-vue3/src/views/monitor/operlog/index.vue b/ruoyi-ui-vue3/src/views/monitor/operlog/index.vue
new file mode 100644
index 000000000..d1760f8dc
--- /dev/null
+++ b/ruoyi-ui-vue3/src/views/monitor/operlog/index.vue
@@ -0,0 +1,291 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 搜索
+ 重置
+
+
+
+
+
+ 删除
+
+
+ 清空
+
+
+ 导出
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ parseTime(scope.row.operTime) }}
+
+
+
+
+ {{ scope.row.costTime }}毫秒
+
+
+
+
+ 详细
+
+
+
+
+
+
+
+
+
+
+
+ {{ form.title }} / {{ typeFormat(form) }}
+ {{ form.operName }} / {{ form.operIp }} / {{ form.operLocation }}
+
+
+ {{ form.operUrl }}
+ {{ form.requestMethod }}
+
+
+ {{ form.method }}
+
+
+ {{ form.operParam }}
+
+
+ {{ form.jsonResult }}
+
+
+
+ 正常
+ 失败
+
+
+
+ {{ form.costTime }}毫秒
+
+
+ {{ parseTime(form.operTime) }}
+
+
+ {{ form.errorMsg }}
+
+
+
+
+
+
+
+
+
+
+
diff --git a/ruoyi-ui-vue3/src/views/monitor/xxljob/index.vue b/ruoyi-ui-vue3/src/views/monitor/xxljob/index.vue
new file mode 100644
index 000000000..9a73f52a2
--- /dev/null
+++ b/ruoyi-ui-vue3/src/views/monitor/xxljob/index.vue
@@ -0,0 +1,13 @@
+
+
+
+
+
+
+
diff --git a/ruoyi-ui-vue3/src/views/redirect/index.vue b/ruoyi-ui-vue3/src/views/redirect/index.vue
new file mode 100644
index 000000000..a469960dd
--- /dev/null
+++ b/ruoyi-ui-vue3/src/views/redirect/index.vue
@@ -0,0 +1,14 @@
+
+
+
+
+
\ No newline at end of file
diff --git a/ruoyi-ui-vue3/src/views/register.vue b/ruoyi-ui-vue3/src/views/register.vue
new file mode 100644
index 000000000..89bb530ec
--- /dev/null
+++ b/ruoyi-ui-vue3/src/views/register.vue
@@ -0,0 +1,219 @@
+
+
+
+ RuoYi-Vue-Plus后台管理系统
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 注 册
+ 注 册 中...
+
+
+ 使用已有账户登录
+
+
+
+
+
+
+
+
+
+
+
diff --git a/ruoyi-ui-vue3/src/views/system/config/index.vue b/ruoyi-ui-vue3/src/views/system/config/index.vue
new file mode 100644
index 000000000..5452f1d9d
--- /dev/null
+++ b/ruoyi-ui-vue3/src/views/system/config/index.vue
@@ -0,0 +1,306 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 搜索
+ 重置
+
+
+
+
+
+ 新增
+
+
+ 修改
+
+
+ 删除
+
+
+ 导出
+
+
+ 刷新缓存
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ parseTime(scope.row.createTime) }}
+
+
+
+
+ 修改
+ 删除
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ dict.label }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/ruoyi-ui-vue3/src/views/system/dept/index.vue b/ruoyi-ui-vue3/src/views/system/dept/index.vue
new file mode 100644
index 000000000..8f1c7bebd
--- /dev/null
+++ b/ruoyi-ui-vue3/src/views/system/dept/index.vue
@@ -0,0 +1,278 @@
+
+
+
+
+
+
+
+
+
+
+
+
+ 搜索
+ 重置
+
+
+
+
+
+ 新增
+
+
+ 展开/折叠
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ parseTime(scope.row.createTime) }}
+
+
+
+
+ 修改
+ 新增
+ 删除
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ dict.label }}
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/ruoyi-ui-vue3/src/views/system/dict/data.vue b/ruoyi-ui-vue3/src/views/system/dict/data.vue
new file mode 100644
index 000000000..b900666d1
--- /dev/null
+++ b/ruoyi-ui-vue3/src/views/system/dict/data.vue
@@ -0,0 +1,350 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 搜索
+ 重置
+
+
+
+
+
+ 新增
+
+
+ 修改
+
+
+ 删除
+
+
+ 导出
+
+
+ 关闭
+
+
+
+
+
+
+
+
+
+ {{ scope.row.dictLabel }}
+ {{ scope.row.dictLabel }}
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ parseTime(scope.row.createTime) }}
+
+
+
+
+ 修改
+ 删除
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ dict.label }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/ruoyi-ui-vue3/src/views/system/dict/index.vue b/ruoyi-ui-vue3/src/views/system/dict/index.vue
new file mode 100644
index 000000000..23d2a10bd
--- /dev/null
+++ b/ruoyi-ui-vue3/src/views/system/dict/index.vue
@@ -0,0 +1,313 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 搜索
+ 重置
+
+
+
+
+
+ 新增
+
+
+ 修改
+
+
+ 删除
+
+
+ 导出
+
+
+ 刷新缓存
+
+
+
+
+
+
+
+
+
+
+
+ {{ scope.row.dictType }}
+
+
+
+
+
+
+
+
+
+
+
+ {{ parseTime(scope.row.createTime) }}
+
+
+
+
+ 修改
+ 删除
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ dict.label }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/ruoyi-ui-vue3/src/views/system/menu/index.vue b/ruoyi-ui-vue3/src/views/system/menu/index.vue
new file mode 100644
index 000000000..77ce65b9e
--- /dev/null
+++ b/ruoyi-ui-vue3/src/views/system/menu/index.vue
@@ -0,0 +1,441 @@
+
+
+
+
+
+
+
+
+
+
+
+
+ 搜索
+ 重置
+
+
+
+
+
+ 新增
+
+
+ 展开/折叠
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ parseTime(scope.row.createTime) }}
+
+
+
+
+ 修改
+ 新增
+ 删除
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 目录
+ 菜单
+ 按钮
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 是否外链
+
+
+
+ 是
+ 否
+
+
+
+
+
+
+
+
+
+
+ 路由地址
+
+
+
+
+
+
+
+
+
+
+
+
+ 组件路径
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 权限字符
+
+
+
+
+
+
+
+
+
+
+
+
+ 路由参数
+
+
+
+
+
+
+
+
+
+
+
+ 是否缓存
+
+
+
+ 缓存
+ 不缓存
+
+
+
+
+
+
+
+
+
+
+ 显示状态
+
+
+
+ {{ dict.label }}
+
+
+
+
+
+
+
+
+
+
+ 菜单状态
+
+
+
+ {{ dict.label }}
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/ruoyi-ui-vue3/src/views/system/notice/index.vue b/ruoyi-ui-vue3/src/views/system/notice/index.vue
new file mode 100644
index 000000000..c26b8c406
--- /dev/null
+++ b/ruoyi-ui-vue3/src/views/system/notice/index.vue
@@ -0,0 +1,283 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 搜索
+ 重置
+
+
+
+
+
+ 新增
+
+
+ 修改
+
+
+ 删除
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ parseTime(scope.row.createTime, '{y}-{m}-{d}') }}
+
+
+
+
+ 修改
+ 删除
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ dict.label }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/ruoyi-ui-vue3/src/views/system/oss/config.vue b/ruoyi-ui-vue3/src/views/system/oss/config.vue
new file mode 100644
index 000000000..facea7ae7
--- /dev/null
+++ b/ruoyi-ui-vue3/src/views/system/oss/config.vue
@@ -0,0 +1,382 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 搜索
+ 重置
+
+
+
+
+
+ 新增
+
+
+ 修改
+
+
+ 删除
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ private
+ public
+ custom
+
+
+
+
+
+
+
+
+
+ 修改
+ 删除
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{dict.label}}
+
+
+
+
+ private
+ public
+ custom
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/ruoyi-ui-vue3/src/views/system/oss/index.vue b/ruoyi-ui-vue3/src/views/system/oss/index.vue
new file mode 100644
index 000000000..ecc045cc4
--- /dev/null
+++ b/ruoyi-ui-vue3/src/views/system/oss/index.vue
@@ -0,0 +1,372 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 搜索
+ 重置
+
+
+
+
+
+ 上传文件
+
+
+ 上传图片
+
+
+ 删除
+
+
+ 预览开关 : {{previewListResource ? "禁用" : "启用"}}
+
+
+ 配置管理
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ parseTime(scope.row.createTime, '{y}-{m}-{d}') }}
+
+
+
+
+
+
+ 下载
+ 删除
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/ruoyi-ui-vue3/src/views/system/post/index.vue b/ruoyi-ui-vue3/src/views/system/post/index.vue
new file mode 100644
index 000000000..6fd1b641a
--- /dev/null
+++ b/ruoyi-ui-vue3/src/views/system/post/index.vue
@@ -0,0 +1,277 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 搜索
+ 重置
+
+
+
+
+
+ 新增
+
+
+ 修改
+
+
+ 删除
+
+
+ 导出
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ parseTime(scope.row.createTime) }}
+
+
+
+
+ 修改
+ 删除
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ dict.label }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/ruoyi-ui-vue3/src/views/system/role/authUser.vue b/ruoyi-ui-vue3/src/views/system/role/authUser.vue
new file mode 100644
index 000000000..66b5f5e6c
--- /dev/null
+++ b/ruoyi-ui-vue3/src/views/system/role/authUser.vue
@@ -0,0 +1,172 @@
+
+
+
+
+
+
+
+
+
+
+
+ 搜索
+ 重置
+
+
+
+
+
+ 添加用户
+
+
+ 批量取消授权
+
+
+ 关闭
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ parseTime(scope.row.createTime) }}
+
+
+
+
+ 取消授权
+
+
+
+
+
+
+
+
+
+
diff --git a/ruoyi-ui-vue3/src/views/system/role/index.vue b/ruoyi-ui-vue3/src/views/system/role/index.vue
new file mode 100644
index 000000000..f84d99652
--- /dev/null
+++ b/ruoyi-ui-vue3/src/views/system/role/index.vue
@@ -0,0 +1,560 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 搜索
+ 重置
+
+
+
+
+ 新增
+
+
+ 修改
+
+
+ 删除
+
+
+ 导出
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ parseTime(scope.row.createTime) }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 权限字符
+
+
+
+
+
+
+
+
+
+ {{ dict.label }}
+
+
+
+ 展开/折叠
+ 全选/全不选
+ 父子联动
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 展开/折叠
+ 全选/全不选
+ 父子联动
+
+
+
+
+
+
+
+
+
+
+
diff --git a/ruoyi-ui-vue3/src/views/system/role/selectUser.vue b/ruoyi-ui-vue3/src/views/system/role/selectUser.vue
new file mode 100644
index 000000000..9be1ec96a
--- /dev/null
+++ b/ruoyi-ui-vue3/src/views/system/role/selectUser.vue
@@ -0,0 +1,140 @@
+
+
+
+
+
+
+
+
+
+
+
+ 搜索
+ 重置
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ parseTime(scope.row.createTime) }}
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/ruoyi-ui-vue3/src/views/system/user/authRole.vue b/ruoyi-ui-vue3/src/views/system/user/authRole.vue
new file mode 100644
index 000000000..5f1f2fe7b
--- /dev/null
+++ b/ruoyi-ui-vue3/src/views/system/user/authRole.vue
@@ -0,0 +1,112 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ (pageNum - 1) * pageSize + scope.$index + 1 }}
+
+
+
+
+
+
+
+
+ {{ parseTime(scope.row.createTime) }}
+
+
+
+
+
+
+
+
+ 提交
+ 返回
+
+
+
+
+
+
diff --git a/ruoyi-ui-vue3/src/views/system/user/index.vue b/ruoyi-ui-vue3/src/views/system/user/index.vue
new file mode 100644
index 000000000..883394075
--- /dev/null
+++ b/ruoyi-ui-vue3/src/views/system/user/index.vue
@@ -0,0 +1,608 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 搜索
+ 重置
+
+
+
+
+
+ 新增
+
+
+ 修改
+
+
+ 删除
+
+
+ 导入
+
+
+ 导出
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ parseTime(scope.row.createTime) }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ dict.label }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 将文件拖到此处,或点击上传
+
+
+
+ 是否更新已经存在的用户数据
+
+
仅允许导入xls、xlsx格式文件。
+
下载模板
+
+
+
+
+
+
+
+
+
+
+
diff --git a/ruoyi-ui-vue3/src/views/system/user/profile/index.vue b/ruoyi-ui-vue3/src/views/system/user/profile/index.vue
new file mode 100644
index 000000000..09e4c326a
--- /dev/null
+++ b/ruoyi-ui-vue3/src/views/system/user/profile/index.vue
@@ -0,0 +1,87 @@
+
+
+
+
+
+
+
+ 个人信息
+
+
+
+
+
+
+
+
+ 用户名称
+ {{ state.user.userName }}
+
+
+ 手机号码
+ {{ state.user.phonenumber }}
+
+
+ 用户邮箱
+ {{ state.user.email }}
+
+
+ 所属部门
+ {{ state.user.dept.deptName }} / {{ state.postGroup }}
+
+
+ 所属角色
+ {{ state.roleGroup }}
+
+
+ 创建日期
+ {{ state.user.createTime }}
+
+
+
+
+
+
+
+
+
+ 基本资料
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/ruoyi-ui-vue3/src/views/system/user/profile/resetPwd.vue b/ruoyi-ui-vue3/src/views/system/user/profile/resetPwd.vue
new file mode 100644
index 000000000..dec2d79c9
--- /dev/null
+++ b/ruoyi-ui-vue3/src/views/system/user/profile/resetPwd.vue
@@ -0,0 +1,57 @@
+
+
+
+
+
+
+
+
+
+
+
+
+ 保存
+ 关闭
+
+
+
+
+
diff --git a/ruoyi-ui-vue3/src/views/system/user/profile/userAvatar.vue b/ruoyi-ui-vue3/src/views/system/user/profile/userAvatar.vue
new file mode 100644
index 000000000..f576c9530
--- /dev/null
+++ b/ruoyi-ui-vue3/src/views/system/user/profile/userAvatar.vue
@@ -0,0 +1,171 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 选择
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 提 交
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/ruoyi-ui-vue3/src/views/system/user/profile/userInfo.vue b/ruoyi-ui-vue3/src/views/system/user/profile/userInfo.vue
new file mode 100644
index 000000000..2d62c842b
--- /dev/null
+++ b/ruoyi-ui-vue3/src/views/system/user/profile/userInfo.vue
@@ -0,0 +1,56 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 男
+ 女
+
+
+
+ 保存
+ 关闭
+
+
+
+
+
diff --git a/ruoyi-ui-vue3/src/views/tool/build/index.vue b/ruoyi-ui-vue3/src/views/tool/build/index.vue
new file mode 100644
index 000000000..c3543a92d
--- /dev/null
+++ b/ruoyi-ui-vue3/src/views/tool/build/index.vue
@@ -0,0 +1,3 @@
+
+ 表单构建
+
\ No newline at end of file
diff --git a/ruoyi-ui-vue3/src/views/tool/gen/basicInfoForm.vue b/ruoyi-ui-vue3/src/views/tool/gen/basicInfoForm.vue
new file mode 100644
index 000000000..39c851507
--- /dev/null
+++ b/ruoyi-ui-vue3/src/views/tool/gen/basicInfoForm.vue
@@ -0,0 +1,48 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/ruoyi-ui-vue3/src/views/tool/gen/editTable.vue b/ruoyi-ui-vue3/src/views/tool/gen/editTable.vue
new file mode 100644
index 000000000..a61556ce1
--- /dev/null
+++ b/ruoyi-ui-vue3/src/views/tool/gen/editTable.vue
@@ -0,0 +1,198 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ dict.dictName }}
+ {{ dict.dictType }}
+
+
+
+
+
+
+
+
+
+
+
+
+ 提交
+ 返回
+
+
+
+
+
+
diff --git a/ruoyi-ui-vue3/src/views/tool/gen/genInfoForm.vue b/ruoyi-ui-vue3/src/views/tool/gen/genInfoForm.vue
new file mode 100644
index 000000000..1cd95f9d7
--- /dev/null
+++ b/ruoyi-ui-vue3/src/views/tool/gen/genInfoForm.vue
@@ -0,0 +1,281 @@
+
+
+
+
+
+ 生成模板
+
+
+
+
+
+
+
+
+
+
+
+ 生成包路径
+
+
+
+
+
+
+
+
+
+
+
+ 生成模块名
+
+
+
+
+
+
+
+
+
+
+
+ 生成业务名
+
+
+
+
+
+
+
+
+
+
+
+ 生成功能名
+
+
+
+
+
+
+
+
+
+
+
+ 上级菜单
+
+
+
+
+
+
+
+
+
+
+
+ 生成代码方式
+
+
+
+
+ zip压缩包
+ 自定义路径
+
+
+
+
+
+
+ 自定义路径
+
+
+
+
+
+
+
+
+ 最近路径快速选择
+
+
+
+
+ 恢复默认的生成基础路径
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 树编码字段
+
+
+
+
+
+
+
+
+
+
+
+
+ 树父编码字段
+
+
+
+
+
+
+
+
+
+
+
+
+ 树名称字段
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 关联子表的表名
+
+
+
+
+
+
+
+
+
+
+
+
+ 子表关联的外键名
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/ruoyi-ui-vue3/src/views/tool/gen/importTable.vue b/ruoyi-ui-vue3/src/views/tool/gen/importTable.vue
new file mode 100644
index 000000000..33b563373
--- /dev/null
+++ b/ruoyi-ui-vue3/src/views/tool/gen/importTable.vue
@@ -0,0 +1,118 @@
+
+
+
+
+
+
+
+
+
+
+
+ 搜索
+ 重置
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/ruoyi-ui-vue3/src/views/tool/gen/index.vue b/ruoyi-ui-vue3/src/views/tool/gen/index.vue
new file mode 100644
index 000000000..fb2fc9edf
--- /dev/null
+++ b/ruoyi-ui-vue3/src/views/tool/gen/index.vue
@@ -0,0 +1,296 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 搜索
+ 重置
+
+
+
+
+
+ 生成
+
+
+ 导入
+
+
+ 修改
+
+
+ 删除
+
+
+
+
+
+
+
+
+ {{(queryParams.pageNum - 1) * queryParams.pageSize + scope.$index + 1}}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 复制
+ {{ value }}
+
+
+
+
+
+
+
+
diff --git a/ruoyi-ui-vue3/vite.config.js b/ruoyi-ui-vue3/vite.config.js
new file mode 100644
index 000000000..d260bbc33
--- /dev/null
+++ b/ruoyi-ui-vue3/vite.config.js
@@ -0,0 +1,57 @@
+import { defineConfig, loadEnv } from 'vite'
+import path from 'path'
+import createVitePlugins from './vite/plugins'
+
+// https://vitejs.dev/config/
+export default defineConfig(({ mode, command }) => {
+ const env = loadEnv(mode, process.cwd())
+ return {
+ // 部署生产环境和开发环境下的URL。
+ // 默认情况下,vite 会假设你的应用是被部署在一个域名的根路径上
+ // 例如 https://www.ruoyi.vip/。如果应用被部署在一个子路径上,你就需要用这个选项指定这个子路径。例如,如果你的应用被部署在 https://www.ruoyi.vip/admin/,则设置 baseUrl 为 /admin/。
+ base: env.VITE_APP_CONTEXT_PATH,
+ plugins: createVitePlugins(env, command === 'build'),
+ resolve: {
+ // https://cn.vitejs.dev/config/#resolve-alias
+ alias: {
+ // 设置路径
+ '~': path.resolve(__dirname, './'),
+ // 设置别名
+ '@': path.resolve(__dirname, './src')
+ },
+ // https://cn.vitejs.dev/config/#resolve-extensions
+ extensions: ['.mjs', '.js', '.ts', '.jsx', '.tsx', '.json', '.vue']
+ },
+ // vite 相关配置
+ server: {
+ port: 80,
+ host: true,
+ open: true,
+ proxy: {
+ // https://cn.vitejs.dev/config/#server-proxy
+ '/dev-api': {
+ target: 'http://localhost:8080',
+ changeOrigin: true,
+ rewrite: (p) => p.replace(/^\/dev-api/, '')
+ }
+ }
+ },
+ //fix:error:stdin>:7356:1: warning: "@charset" must be the first rule in the file
+ css: {
+ postcss: {
+ plugins: [
+ {
+ postcssPlugin: 'internal:charset-removal',
+ AtRule: {
+ charset: (atRule) => {
+ if (atRule.name === 'charset') {
+ atRule.remove();
+ }
+ }
+ }
+ }
+ ]
+ }
+ }
+ }
+})
diff --git a/ruoyi-ui-vue3/vite/plugins/auto-import.js b/ruoyi-ui-vue3/vite/plugins/auto-import.js
new file mode 100644
index 000000000..a5d357638
--- /dev/null
+++ b/ruoyi-ui-vue3/vite/plugins/auto-import.js
@@ -0,0 +1,12 @@
+import autoImport from 'unplugin-auto-import/vite'
+
+export default function createAutoImport() {
+ return autoImport({
+ imports: [
+ 'vue',
+ 'vue-router',
+ 'pinia'
+ ],
+ dts: false
+ })
+}
diff --git a/ruoyi-ui-vue3/vite/plugins/compression.js b/ruoyi-ui-vue3/vite/plugins/compression.js
new file mode 100644
index 000000000..e90aaecce
--- /dev/null
+++ b/ruoyi-ui-vue3/vite/plugins/compression.js
@@ -0,0 +1,28 @@
+import compression from 'vite-plugin-compression'
+
+export default function createCompression(env) {
+ const { VITE_BUILD_COMPRESS } = env
+ const plugin = []
+ if (VITE_BUILD_COMPRESS) {
+ const compressList = VITE_BUILD_COMPRESS.split(',')
+ if (compressList.includes('gzip')) {
+ // http://doc.ruoyi.vip/ruoyi-vue/other/faq.html#使用gzip解压缩静态文件
+ plugin.push(
+ compression({
+ ext: '.gz',
+ deleteOriginFile: false
+ })
+ )
+ }
+ if (compressList.includes('brotli')) {
+ plugin.push(
+ compression({
+ ext: '.br',
+ algorithm: 'brotliCompress',
+ deleteOriginFile: false
+ })
+ )
+ }
+ }
+ return plugin
+}
diff --git a/ruoyi-ui-vue3/vite/plugins/index.js b/ruoyi-ui-vue3/vite/plugins/index.js
new file mode 100644
index 000000000..10e17c3df
--- /dev/null
+++ b/ruoyi-ui-vue3/vite/plugins/index.js
@@ -0,0 +1,15 @@
+import vue from '@vitejs/plugin-vue'
+
+import createAutoImport from './auto-import'
+import createSvgIcon from './svg-icon'
+import createCompression from './compression'
+import createSetupExtend from './setup-extend'
+
+export default function createVitePlugins(viteEnv, isBuild = false) {
+ const vitePlugins = [vue()]
+ vitePlugins.push(createAutoImport())
+ vitePlugins.push(createSetupExtend())
+ vitePlugins.push(createSvgIcon(isBuild))
+ isBuild && vitePlugins.push(...createCompression(viteEnv))
+ return vitePlugins
+}
diff --git a/ruoyi-ui-vue3/vite/plugins/setup-extend.js b/ruoyi-ui-vue3/vite/plugins/setup-extend.js
new file mode 100644
index 000000000..a4980f300
--- /dev/null
+++ b/ruoyi-ui-vue3/vite/plugins/setup-extend.js
@@ -0,0 +1,5 @@
+import setupExtend from 'vite-plugin-vue-setup-extend'
+
+export default function createSetupExtend() {
+ return setupExtend()
+}
diff --git a/ruoyi-ui-vue3/vite/plugins/svg-icon.js b/ruoyi-ui-vue3/vite/plugins/svg-icon.js
new file mode 100644
index 000000000..30a4140f4
--- /dev/null
+++ b/ruoyi-ui-vue3/vite/plugins/svg-icon.js
@@ -0,0 +1,10 @@
+import { createSvgIconsPlugin } from 'vite-plugin-svg-icons'
+import path from 'path'
+
+export default function createSvgIcon(isBuild) {
+ return createSvgIconsPlugin({
+ iconDirs: [path.resolve(process.cwd(), 'src/assets/icons/svg')],
+ symbolId: 'icon-[dir]-[name]',
+ svgoOptions: isBuild
+ })
+}
diff --git a/ruoyi-ui/.env.development b/ruoyi-ui/.env.development
index 232d9077b..c5457c9a6 100644
--- a/ruoyi-ui/.env.development
+++ b/ruoyi-ui/.env.development
@@ -11,7 +11,7 @@ VUE_APP_BASE_API = '/dev-api'
VUE_APP_CONTEXT_PATH = '/'
# 监控地址
-VUE_APP_MONITRO_ADMIN = 'http://localhost:9090/admin/login'
+VUE_APP_MONITRO_ADMIN = 'http://localhost:9090/admin/applications'
# xxl-job 控制台地址
VUE_APP_XXL_JOB_ADMIN = 'http://localhost:9100/xxl-job-admin'
diff --git a/ruoyi-ui/.env.production b/ruoyi-ui/.env.production
index f92c1af3d..1d2cbae6a 100644
--- a/ruoyi-ui/.env.production
+++ b/ruoyi-ui/.env.production
@@ -11,7 +11,7 @@ VUE_APP_BASE_API = '/prod-api'
VUE_APP_CONTEXT_PATH = '/'
# 监控地址
-VUE_APP_MONITRO_ADMIN = '/admin/login'
+VUE_APP_MONITRO_ADMIN = '/admin/applications'
# xxl-job 控制台地址
VUE_APP_XXL_JOB_ADMIN = '/xxl-job-admin'
diff --git a/ruoyi-ui/README.md b/ruoyi-ui/README.md
index 00c0ab84f..9394319ec 100644
--- a/ruoyi-ui/README.md
+++ b/ruoyi-ui/README.md
@@ -1,9 +1,6 @@
## 开发
```bash
-# 克隆项目
-git clone https://gitee.com/y_project/RuoYi-Vue
-
# 进入项目目录
cd ruoyi-ui
diff --git a/ruoyi-ui/package.json b/ruoyi-ui/package.json
index 651eab334..56eebc75b 100644
--- a/ruoyi-ui/package.json
+++ b/ruoyi-ui/package.json
@@ -1,6 +1,6 @@
{
"name": "ruoyi-vue-plus",
- "version": "4.7.0",
+ "version": "4.8.0",
"description": "RuoYi-Vue-Plus后台管理系统",
"author": "LionLi",
"license": "MIT",
@@ -40,7 +40,7 @@
"clipboard": "2.0.8",
"core-js": "3.25.3",
"echarts": "5.4.0",
- "element-ui": "2.15.12",
+ "element-ui": "2.15.13",
"file-saver": "2.0.5",
"fuse.js": "6.4.3",
"highlight.js": "9.18.5",
diff --git a/ruoyi-ui/src/layout/components/TagsView/ScrollPane.vue b/ruoyi-ui/src/layout/components/TagsView/ScrollPane.vue
index f92d99b7a..bb753a124 100644
--- a/ruoyi-ui/src/layout/components/TagsView/ScrollPane.vue
+++ b/ruoyi-ui/src/layout/components/TagsView/ScrollPane.vue
@@ -87,7 +87,7 @@ export default {
bottom: 0px;
}
.el-scrollbar__wrap {
- height: 39px;
+ height: 49px;
}
}
}
diff --git a/ruoyi-ui/src/plugins/download.js b/ruoyi-ui/src/plugins/download.js
index b27702f18..dae2ba9cf 100644
--- a/ruoyi-ui/src/plugins/download.js
+++ b/ruoyi-ui/src/plugins/download.js
@@ -34,6 +34,7 @@ export default {
},
zip(url, name) {
var url = baseURL + url
+ downloadLoadingInstance = Loading.service({ text: "正在下载数据,请稍候", spinner: "el-icon-loading", background: "rgba(0, 0, 0, 0.7)", })
axios({
method: 'get',
url: url,
@@ -50,6 +51,11 @@ export default {
} else {
this.printErrMsg(res.data);
}
+ downloadLoadingInstance.close();
+ }).catch((r) => {
+ console.error(r)
+ Message.error('下载文件出现错误,请联系管理员!')
+ downloadLoadingInstance.close();
})
},
saveAs(text, name, opts) {
diff --git a/ruoyi-ui/src/views/index.vue b/ruoyi-ui/src/views/index.vue
index b62ca5866..9a7d747ab 100644
--- a/ruoyi-ui/src/views/index.vue
+++ b/ruoyi-ui/src/views/index.vue
@@ -114,7 +114,7 @@ export default {
data() {
return {
// 版本号
- version: "4.7.0",
+ version: "4.8.0",
};
},
methods: {
diff --git a/ruoyi-ui/src/views/monitor/cache/index.vue b/ruoyi-ui/src/views/monitor/cache/index.vue
index e81da2e8f..75832ab99 100644
--- a/ruoyi-ui/src/views/monitor/cache/index.vue
+++ b/ruoyi-ui/src/views/monitor/cache/index.vue
@@ -133,6 +133,10 @@ export default {
}
]
});
+ window.addEventListener("resize",()=>{
+ this.commandstats.resize()
+ this.usedmemory.resize()
+ });
});
},
// 打开加载层
diff --git a/script/docker/docker-compose.yml b/script/docker/docker-compose.yml
index 6e16c1310..4359e7469 100644
--- a/script/docker/docker-compose.yml
+++ b/script/docker/docker-compose.yml
@@ -100,7 +100,7 @@ services:
network_mode: "host"
ruoyi-server1:
- image: ruoyi/ruoyi-server:4.7.0
+ image: ruoyi/ruoyi-server:4.8.0
container_name: ruoyi-server1
environment:
# 时区上海
@@ -115,7 +115,7 @@ services:
network_mode: "host"
ruoyi-server2:
- image: "ruoyi/ruoyi-server:4.7.0"
+ image: "ruoyi/ruoyi-server:4.8.0"
container_name: ruoyi-server2
environment:
# 时区上海
@@ -130,7 +130,7 @@ services:
network_mode: "host"
ruoyi-monitor-admin:
- image: ruoyi/ruoyi-monitor-admin:4.7.0
+ image: ruoyi/ruoyi-monitor-admin:4.8.0
container_name: ruoyi-monitor-admin
environment:
# 时区上海
@@ -142,7 +142,7 @@ services:
network_mode: "host"
ruoyi-xxl-job-admin:
- image: ruoyi/ruoyi-xxl-job-admin:4.7.0
+ image: ruoyi/ruoyi-xxl-job-admin:4.8.0
container_name: ruoyi-xxl-job-admin
environment:
# 时区上海