同步前端代码并提交相关修复
This commit is contained in:
@@ -9,7 +9,9 @@ CONSOLE_LOG="$LOG_DIR/backend-console.log"
|
||||
PID_FILE="$LOG_DIR/backend-java.pid"
|
||||
TARGET_DIR="$ROOT_DIR/ruoyi-admin/target"
|
||||
JAR_NAME="ruoyi-admin.jar"
|
||||
SERVER_PORT=8080
|
||||
JAVA_HOME_1_8="/Library/Java/JavaVirtualMachines/jdk1.8.0_202.jdk/Contents/Home"
|
||||
JAVA_BIN="$JAVA_HOME_1_8/bin/java"
|
||||
SERVER_PORT=63310
|
||||
STOP_WAIT_SECONDS=30
|
||||
APP_MARKER="-Dloan.pricing.backend.root=$ROOT_DIR"
|
||||
JAVA_OPTS="$APP_MARKER -Duser.timezone=Asia/Shanghai -Xms512m -Xmx1024m -XX:MetaspaceSize=128m -XX:MaxMetaspaceSize=512m -XX:+HeapDumpOnOutOfMemoryError"
|
||||
@@ -42,6 +44,18 @@ ensure_command() {
|
||||
fi
|
||||
}
|
||||
|
||||
setup_java_env() {
|
||||
if [ ! -x "$JAVA_BIN" ]; then
|
||||
log_error "未找到 JDK 1.8: $JAVA_BIN"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
JAVA_HOME="$JAVA_HOME_1_8"
|
||||
export JAVA_HOME
|
||||
PATH="$JAVA_HOME/bin:$PATH"
|
||||
export PATH
|
||||
}
|
||||
|
||||
is_managed_backend_pid() {
|
||||
pid="$1"
|
||||
if [ -z "${pid:-}" ] || ! kill -0 "$pid" 2>/dev/null; then
|
||||
@@ -175,7 +189,7 @@ start_backend() {
|
||||
|
||||
(
|
||||
cd "$TARGET_DIR"
|
||||
nohup java $JAVA_OPTS -jar "$JAR_NAME" >> "$CONSOLE_LOG" 2>&1 &
|
||||
nohup "$JAVA_BIN" $JAVA_OPTS -jar "$JAR_NAME" >> "$CONSOLE_LOG" 2>&1 &
|
||||
echo $! > "$PID_FILE"
|
||||
)
|
||||
|
||||
@@ -232,8 +246,8 @@ restart_action() {
|
||||
}
|
||||
|
||||
main() {
|
||||
setup_java_env
|
||||
ensure_command mvn
|
||||
ensure_command java
|
||||
ensure_command lsof
|
||||
ensure_command ps
|
||||
ensure_command tail
|
||||
|
||||
17
doc/2026-04-15-AGENTS中文化实施记录.md
Normal file
17
doc/2026-04-15-AGENTS中文化实施记录.md
Normal file
@@ -0,0 +1,17 @@
|
||||
# AGENTS 中文化实施记录
|
||||
|
||||
## 修改内容
|
||||
- 将根目录 `AGENTS.md` 中英文版仓库协作指南完整转换为中文表述。
|
||||
- 保留原有章节结构与约束语义,仅调整为中文描述,未改变规则内容。
|
||||
|
||||
## 影响范围
|
||||
- 影响文件:`/Users/wkc/Desktop/loan-pricing/loan-pricing-jdk-1.8/AGENTS.md`
|
||||
- 本次实施记录:`/Users/wkc/Desktop/loan-pricing/loan-pricing-jdk-1.8/doc/2026-04-15-AGENTS中文化实施记录.md`
|
||||
|
||||
## 说明
|
||||
- 本次修改仅涉及文档文本,不涉及前端、后端逻辑或配置变更。
|
||||
- 未执行构建与测试,原因是本次仅为文档中文化调整。
|
||||
|
||||
## 保存路径确认
|
||||
- `AGENTS.md` 保存路径正确:`/Users/wkc/Desktop/loan-pricing/loan-pricing-jdk-1.8/AGENTS.md`
|
||||
- 实施记录保存路径正确:`/Users/wkc/Desktop/loan-pricing/loan-pricing-jdk-1.8/doc/2026-04-15-AGENTS中文化实施记录.md`
|
||||
19
doc/2026-04-15-后端启动脚本固定JDK18实施文档.md
Normal file
19
doc/2026-04-15-后端启动脚本固定JDK18实施文档.md
Normal file
@@ -0,0 +1,19 @@
|
||||
# 后端启动脚本固定 JDK 1.8 实施文档
|
||||
|
||||
## 修改内容
|
||||
- 在 `bin/restart_java_backend.sh` 中新增固定的 JDK 1.8 路径 `/Library/Java/JavaVirtualMachines/jdk1.8.0_202.jdk/Contents/Home`。
|
||||
- 脚本启动时统一设置 `JAVA_HOME` 和 `PATH`,确保脚本内执行的 `mvn` 与 `java -jar` 都使用 JDK 1.8。
|
||||
- 启动 Java 进程时改为显式调用固定路径下的 `bin/java`,避免落回系统默认的 JDK 21。
|
||||
|
||||
## 适配依据
|
||||
- 当前机器默认 `JAVA_HOME` 为 JDK 21,不满足本项目需要固定 JDK 1.8 的要求。
|
||||
- 当前机器已安装可用的 JDK 1.8:`/Library/Java/JavaVirtualMachines/jdk1.8.0_202.jdk/Contents/Home`。
|
||||
- 本次按“写死当前机器 JDK 1.8 路径”的方案实现,不引入额外的自动探测逻辑。
|
||||
|
||||
## 验证方式
|
||||
- 执行 `sh -n bin/restart_java_backend.sh` 校验脚本语法。
|
||||
- 执行 `/Library/Java/JavaVirtualMachines/jdk1.8.0_202.jdk/Contents/Home/bin/java -version` 校验固定路径下的 Java 为 1.8。
|
||||
|
||||
## 保存路径确认
|
||||
- 脚本路径:`/Users/wkc/Desktop/loan-pricing/loan-pricing-jdk-1.8/bin/restart_java_backend.sh`
|
||||
- 本次实施文档路径:`/Users/wkc/Desktop/loan-pricing/loan-pricing-jdk-1.8/doc/2026-04-15-后端启动脚本固定JDK18实施文档.md`
|
||||
25
doc/2026-04-15-后端端口改为63310实施记录.md
Normal file
25
doc/2026-04-15-后端端口改为63310实施记录.md
Normal file
@@ -0,0 +1,25 @@
|
||||
# 后端端口改为 `63310` 实施记录
|
||||
|
||||
## 修改时间
|
||||
|
||||
- 2026-04-15
|
||||
|
||||
## 修改内容
|
||||
|
||||
- 将重启脚本 [bin/restart_java_backend.sh](/Users/wkc/Desktop/loan-pricing/loan-pricing-jdk-1.8/bin/restart_java_backend.sh) 中的 `SERVER_PORT` 从 `8080` 调整为 `63310`
|
||||
- 核对后端环境配置,确认 [application-dev.yml](/Users/wkc/Desktop/loan-pricing/loan-pricing-jdk-1.8/ruoyi-admin/src/main/resources/application-dev.yml)、[application-pro.yml](/Users/wkc/Desktop/loan-pricing/loan-pricing-jdk-1.8/ruoyi-admin/src/main/resources/application-pro.yml)、[application-uat.yml](/Users/wkc/Desktop/loan-pricing/loan-pricing-jdk-1.8/ruoyi-admin/src/main/resources/application-uat.yml) 已经使用 `63310`
|
||||
|
||||
## 影响范围
|
||||
|
||||
- 后端重启脚本的端口监听检测
|
||||
- 后端脚本状态查询结果
|
||||
|
||||
## 验证结果
|
||||
|
||||
- 通过源码检索确认后端主环境配置和脚本统一指向 `63310`
|
||||
- 通过 `bash -n bin/restart_java_backend.sh` 确认脚本语法正常
|
||||
|
||||
## 说明
|
||||
|
||||
- 当前开发、生产、UAT 环境配置原本已经是 `63310`
|
||||
- 本次主要修正的是脚本里仍残留的 `8080`
|
||||
16
doc/2026-04-15-直接复制源分支前端代码实施记录.md
Normal file
16
doc/2026-04-15-直接复制源分支前端代码实施记录.md
Normal file
@@ -0,0 +1,16 @@
|
||||
# 2026-04-15 直接复制源分支前端代码实施记录
|
||||
|
||||
## 修改内容
|
||||
|
||||
- 按用户要求,直接将 `origin/892-without-redis` 的 `ruoyi-ui` 整体复制到当前分支。
|
||||
- 覆盖了源分支已有的前端页面、路由、API、布局、样式、构建配置与依赖锁文件。
|
||||
- 同步删除了当前分支中源分支不存在的前端文件,确保前端代码基线与源分支保持一致。
|
||||
|
||||
## 涉及范围
|
||||
|
||||
- `ruoyi-ui/`
|
||||
|
||||
## 验证结果
|
||||
|
||||
- 使用 `nvm` 切换 Node 版本后执行 `npm install`
|
||||
- 执行 `npm run build:prod`
|
||||
16
doc/2026-04-15-贷款定价密钥统一为密码传输配置后端实施记录.md
Normal file
16
doc/2026-04-15-贷款定价密钥统一为密码传输配置后端实施记录.md
Normal file
@@ -0,0 +1,16 @@
|
||||
# 2026-04-15 贷款定价密钥统一为密码传输配置后端实施记录
|
||||
|
||||
## 修改内容
|
||||
|
||||
- 将贷款定价敏感字段加解密服务统一为只读取 `security.password-transfer.key`。
|
||||
- 删除对 `loan-pricing.sensitive.key` 的依赖,避免出现双配置源。
|
||||
- 调整定向单元测试,校验未配置时抛出的错误信息改为 `security.password-transfer.key 未配置`。
|
||||
|
||||
## 涉及文件
|
||||
|
||||
- `ruoyi-loan-pricing/src/main/java/com/ruoyi/loanpricing/service/SensitiveFieldCryptoService.java`
|
||||
- `ruoyi-loan-pricing/src/test/java/com/ruoyi/loanpricing/service/SensitiveFieldCryptoServiceTest.java`
|
||||
|
||||
## 验证结果
|
||||
|
||||
- 执行 `mvn -pl ruoyi-admin -am -DskipTests package` 验证后端整体打包。
|
||||
14
doc/2026-04-15-贷款定价敏感字段密钥配置修复后端实施记录.md
Normal file
14
doc/2026-04-15-贷款定价敏感字段密钥配置修复后端实施记录.md
Normal file
@@ -0,0 +1,14 @@
|
||||
# 2026-04-15 贷款定价敏感字段密钥配置修复后端实施记录
|
||||
|
||||
## 修改内容
|
||||
|
||||
- 修复贷款定价首页列表请求返回 `loan-pricing.sensitive.key 未配置` 的问题。
|
||||
- 在 `application.yml` 中新增 `loan-pricing.sensitive.key`,并直接复用现有的 `security.password-transfer.key`,保证贷款定价敏感字段加解密与密码传输使用同一把密钥。
|
||||
|
||||
## 涉及文件
|
||||
|
||||
- `ruoyi-admin/src/main/resources/application.yml`
|
||||
|
||||
## 验证结果
|
||||
|
||||
- 执行 `mvn -pl ruoyi-admin -am -DskipTests package`,验证后端配置修改后可正常完成打包。
|
||||
14
doc/2026-04-15-首页改为利率测算列表前端实施记录.md
Normal file
14
doc/2026-04-15-首页改为利率测算列表前端实施记录.md
Normal file
@@ -0,0 +1,14 @@
|
||||
# 2026-04-15 首页改为利率测算列表前端实施记录
|
||||
|
||||
## 修改内容
|
||||
|
||||
- 将前端默认首页路由从 `@/views/index` 调整为 `@/views/loanPricing/workflow/index`,使登录后的首页直接进入利率测算列表。
|
||||
- 为利率测算详情页补充显式命名路由 `LoanPricingWorkflowDetail`,保证首页列表点击“查看”后可以正常进入详情页。
|
||||
|
||||
## 涉及文件
|
||||
|
||||
- `ruoyi-ui/src/router/index.js`
|
||||
|
||||
## 验证结果
|
||||
|
||||
- 使用 `nvm` 切换 Node 版本后执行 `npm run build:prod`,验证前端路由配置可以正常通过生产构建。
|
||||
18
doc/2026-04-15-首页面包屑与贷款定价密钥异常修复实施记录.md
Normal file
18
doc/2026-04-15-首页面包屑与贷款定价密钥异常修复实施记录.md
Normal file
@@ -0,0 +1,18 @@
|
||||
# 2026-04-15 首页面包屑与贷款定价密钥异常修复实施记录
|
||||
|
||||
## 修改内容
|
||||
|
||||
- 修复首页访问 `/index` 时面包屑重复注入首页节点,导致前端出现 `Duplicate keys detected: '/index'` 警告的问题。
|
||||
- 修复贷款定价敏感字段加解密服务只读取 `loan-pricing.sensitive.key`,未在专用配置缺失时回退到 `security.password-transfer.key`,导致首页列表接口返回 `loan-pricing.sensitive.key 未配置` 的问题。
|
||||
|
||||
## 涉及文件
|
||||
|
||||
- `ruoyi-ui/src/components/Breadcrumb/index.vue`
|
||||
- `ruoyi-loan-pricing/src/main/java/com/ruoyi/loanpricing/service/SensitiveFieldCryptoService.java`
|
||||
- `ruoyi-loan-pricing/src/test/java/com/ruoyi/loanpricing/service/SensitiveFieldCryptoServiceTest.java`
|
||||
- `ruoyi-ui/tests/home-breadcrumb-dedup.test.js`
|
||||
|
||||
## 验证结果
|
||||
|
||||
- 已执行 `node ruoyi-ui/tests/home-breadcrumb-dedup.test.js`,结果通过。
|
||||
- 已尝试执行 `mvn -pl ruoyi-loan-pricing -am -Dtest=SensitiveFieldCryptoServiceTest -Dsurefire.failIfNoSpecifiedTests=false test`,但当前 `ruoyi-loan-pricing` 模块存在与本次改动无关的编译问题,导致无法完成该条 Maven 定向验证。
|
||||
2
pom.xml
2
pom.xml
@@ -35,7 +35,7 @@
|
||||
<logback.version>1.2.13</logback.version>
|
||||
<spring-security.version>5.7.14</spring-security.version>
|
||||
<spring-framework.version>5.3.39</spring-framework.version>
|
||||
<mybatis-plus.version>3.5.7</mybatis-plus.version>
|
||||
<mybatis-plus.version>3.5.1</mybatis-plus.version>
|
||||
</properties>
|
||||
|
||||
<!-- 依赖声明 -->
|
||||
|
||||
@@ -1,3 +1,22 @@
|
||||
# 开发环境配置
|
||||
server:
|
||||
# 服务器的HTTP端口,默认为63310
|
||||
port: 63310
|
||||
servlet:
|
||||
# 应用的访问路径
|
||||
context-path: /
|
||||
tomcat:
|
||||
# tomcat的URI编码
|
||||
uri-encoding: UTF-8
|
||||
# 连接数满后的排队数,默认为100
|
||||
accept-count: 1000
|
||||
threads:
|
||||
# tomcat最大线程数,默认为200
|
||||
max: 800
|
||||
# Tomcat启动初始化的线程数,默认值10
|
||||
min-spare: 100
|
||||
|
||||
|
||||
# 数据源配置
|
||||
spring:
|
||||
datasource:
|
||||
@@ -59,3 +78,6 @@ spring:
|
||||
wall:
|
||||
config:
|
||||
multi-statement-allow: true
|
||||
|
||||
model:
|
||||
url: http://localhost:63310/rate/pricing/mock/invokeModel
|
||||
|
||||
@@ -81,6 +81,4 @@ spring:
|
||||
model:
|
||||
url: http://64.202.32.40:8083/api/service/interface/invokeService/syllcs
|
||||
|
||||
security:
|
||||
password-transfer:
|
||||
key: "1234567890abcdef"
|
||||
|
||||
|
||||
@@ -81,6 +81,4 @@ spring:
|
||||
model:
|
||||
url: http://localhost:63310/rate/pricing/mock/invokeModel
|
||||
|
||||
security:
|
||||
password-transfer:
|
||||
key: "1234567890abcdef"
|
||||
|
||||
|
||||
@@ -13,23 +13,6 @@ ruoyi:
|
||||
# 验证码类型 math 数字计算 char 字符验证
|
||||
captchaType: math
|
||||
|
||||
# 开发环境配置
|
||||
server:
|
||||
# 服务器的HTTP端口,默认为8080
|
||||
port: 8080
|
||||
servlet:
|
||||
# 应用的访问路径
|
||||
context-path: /
|
||||
tomcat:
|
||||
# tomcat的URI编码
|
||||
uri-encoding: UTF-8
|
||||
# 连接数满后的排队数,默认为100
|
||||
accept-count: 1000
|
||||
threads:
|
||||
# tomcat最大线程数,默认为200
|
||||
max: 800
|
||||
# Tomcat启动初始化的线程数,默认值10
|
||||
min-spare: 100
|
||||
|
||||
# 日志配置
|
||||
logging:
|
||||
@@ -75,8 +58,8 @@ token:
|
||||
# 令牌有效期(默认30分钟)
|
||||
expireTime: 30
|
||||
|
||||
# MyBatis配置
|
||||
mybatis:
|
||||
# MyBatis-Plus配置
|
||||
mybatis-plus:
|
||||
# 搜索指定包别名
|
||||
typeAliasesPackage: com.ruoyi.**.domain
|
||||
# 配置mapper的扫描,找到所有的mapper.xml映射文件
|
||||
@@ -104,10 +87,6 @@ referer:
|
||||
# 允许的域名列表
|
||||
allowed-domains: localhost,127.0.0.1,ruoyi.vip,www.ruoyi.vip
|
||||
|
||||
security:
|
||||
password-transfer:
|
||||
key: "1234567890abcdef"
|
||||
|
||||
# 防止XSS攻击
|
||||
xss:
|
||||
# 过滤开关
|
||||
@@ -116,3 +95,7 @@ xss:
|
||||
excludes: /system/notice
|
||||
# 匹配链接
|
||||
urlPatterns: /system/*,/monitor/*,/tool/*
|
||||
|
||||
security:
|
||||
password-transfer:
|
||||
key: "1234567890abcdef"
|
||||
@@ -41,6 +41,11 @@
|
||||
<artifactId>pagehelper-spring-boot-starter</artifactId>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>com.baomidou</groupId>
|
||||
<artifactId>mybatis-plus-boot-starter</artifactId>
|
||||
</dependency>
|
||||
|
||||
<!-- 自定义验证注解 -->
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
|
||||
@@ -41,9 +41,14 @@ public class DictUtils
|
||||
*/
|
||||
public static List<SysDictData> getDictCache(String key)
|
||||
{
|
||||
JSONArray arrayCache = SpringUtils.getBean(RedisCache.class).getCacheObject(getCacheKey(key));
|
||||
if (StringUtils.isNotNull(arrayCache))
|
||||
Object cacheObject = SpringUtils.getBean(RedisCache.class).getCacheObject(getCacheKey(key));
|
||||
if (cacheObject instanceof List)
|
||||
{
|
||||
return (List<SysDictData>) cacheObject;
|
||||
}
|
||||
if (cacheObject instanceof JSONArray)
|
||||
{
|
||||
JSONArray arrayCache = (JSONArray) cacheObject;
|
||||
return arrayCache.toList(SysDictData.class);
|
||||
}
|
||||
return null;
|
||||
|
||||
@@ -0,0 +1,46 @@
|
||||
package com.ruoyi.common.utils;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertNotNull;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import com.ruoyi.common.core.cache.InMemoryCacheStore;
|
||||
import com.ruoyi.common.core.domain.entity.SysDictData;
|
||||
import com.ruoyi.common.core.redis.RedisCache;
|
||||
import com.ruoyi.common.utils.spring.SpringUtils;
|
||||
import org.junit.jupiter.api.AfterEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.beans.factory.support.DefaultListableBeanFactory;
|
||||
import org.springframework.test.util.ReflectionTestUtils;
|
||||
|
||||
class DictUtilsTest
|
||||
{
|
||||
@AfterEach
|
||||
void tearDown()
|
||||
{
|
||||
ReflectionTestUtils.setField(SpringUtils.class, "beanFactory", null);
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldReturnDictListWhenCacheStoresArrayList()
|
||||
{
|
||||
DefaultListableBeanFactory beanFactory = new DefaultListableBeanFactory();
|
||||
beanFactory.registerSingleton("redisCache", new RedisCache(new InMemoryCacheStore()));
|
||||
ReflectionTestUtils.setField(SpringUtils.class, "beanFactory", beanFactory);
|
||||
|
||||
SysDictData dictData = new SysDictData();
|
||||
dictData.setDictType("sys_normal_disable");
|
||||
dictData.setDictLabel("正常");
|
||||
dictData.setDictValue("0");
|
||||
|
||||
DictUtils.setDictCache("sys_normal_disable", Collections.singletonList(dictData));
|
||||
|
||||
List<SysDictData> dictCache = DictUtils.getDictCache("sys_normal_disable");
|
||||
|
||||
assertNotNull(dictCache);
|
||||
assertEquals(1, dictCache.size());
|
||||
assertEquals("正常", dictCache.get(0).getDictLabel());
|
||||
assertEquals("0", dictCache.get(0).getDictValue());
|
||||
}
|
||||
}
|
||||
@@ -35,11 +35,6 @@
|
||||
<artifactId>druid-spring-boot-starter</artifactId>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>com.baomidou</groupId>
|
||||
<artifactId>mybatis-plus-boot-starter</artifactId>
|
||||
</dependency>
|
||||
|
||||
<!-- 验证码 -->
|
||||
<dependency>
|
||||
<groupId>pro.fessional</groupId>
|
||||
|
||||
@@ -1,9 +1,6 @@
|
||||
package com.ruoyi.framework.config;
|
||||
|
||||
import java.util.TimeZone;
|
||||
import com.baomidou.mybatisplus.annotation.DbType;
|
||||
import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor;
|
||||
import com.baomidou.mybatisplus.extension.plugins.inner.PaginationInnerInterceptor;
|
||||
import org.mybatis.spring.annotation.MapperScan;
|
||||
import org.springframework.boot.autoconfigure.jackson.Jackson2ObjectMapperBuilderCustomizer;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
@@ -30,12 +27,4 @@ public class ApplicationConfig
|
||||
{
|
||||
return jacksonObjectMapperBuilder -> jacksonObjectMapperBuilder.timeZone(TimeZone.getDefault());
|
||||
}
|
||||
|
||||
@Bean
|
||||
public MybatisPlusInterceptor mybatisPlusInterceptor()
|
||||
{
|
||||
MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
|
||||
interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL));
|
||||
return interceptor;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,140 +0,0 @@
|
||||
package com.ruoyi.framework.config;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import javax.sql.DataSource;
|
||||
import com.baomidou.mybatisplus.extension.spring.MybatisSqlSessionFactoryBean;
|
||||
import org.apache.ibatis.io.VFS;
|
||||
import org.apache.ibatis.plugin.Interceptor;
|
||||
import org.apache.ibatis.session.SqlSessionFactory;
|
||||
import org.mybatis.spring.boot.autoconfigure.SpringBootVFS;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.core.env.Environment;
|
||||
import org.springframework.core.io.DefaultResourceLoader;
|
||||
import org.springframework.core.io.Resource;
|
||||
import org.springframework.core.io.support.PathMatchingResourcePatternResolver;
|
||||
import org.springframework.core.io.support.ResourcePatternResolver;
|
||||
import org.springframework.core.type.classreading.CachingMetadataReaderFactory;
|
||||
import org.springframework.core.type.classreading.MetadataReader;
|
||||
import org.springframework.core.type.classreading.MetadataReaderFactory;
|
||||
import org.springframework.util.ClassUtils;
|
||||
import com.ruoyi.common.utils.StringUtils;
|
||||
|
||||
/**
|
||||
* Mybatis支持*匹配扫描包
|
||||
*
|
||||
* @author ruoyi
|
||||
*/
|
||||
@Configuration
|
||||
public class MyBatisConfig
|
||||
{
|
||||
@Autowired
|
||||
private Environment env;
|
||||
|
||||
@Autowired(required = false)
|
||||
private Interceptor[] interceptors;
|
||||
|
||||
static final String DEFAULT_RESOURCE_PATTERN = "**/*.class";
|
||||
|
||||
public static String setTypeAliasesPackage(String typeAliasesPackage)
|
||||
{
|
||||
ResourcePatternResolver resolver = (ResourcePatternResolver) new PathMatchingResourcePatternResolver();
|
||||
MetadataReaderFactory metadataReaderFactory = new CachingMetadataReaderFactory(resolver);
|
||||
List<String> allResult = new ArrayList<String>();
|
||||
try
|
||||
{
|
||||
for (String aliasesPackage : typeAliasesPackage.split(","))
|
||||
{
|
||||
List<String> result = new ArrayList<String>();
|
||||
aliasesPackage = ResourcePatternResolver.CLASSPATH_ALL_URL_PREFIX
|
||||
+ ClassUtils.convertClassNameToResourcePath(aliasesPackage.trim()) + "/" + DEFAULT_RESOURCE_PATTERN;
|
||||
Resource[] resources = resolver.getResources(aliasesPackage);
|
||||
if (resources != null && resources.length > 0)
|
||||
{
|
||||
MetadataReader metadataReader = null;
|
||||
for (Resource resource : resources)
|
||||
{
|
||||
if (resource.isReadable())
|
||||
{
|
||||
metadataReader = metadataReaderFactory.getMetadataReader(resource);
|
||||
try
|
||||
{
|
||||
result.add(Class.forName(metadataReader.getClassMetadata().getClassName()).getPackage().getName());
|
||||
}
|
||||
catch (ClassNotFoundException e)
|
||||
{
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if (result.size() > 0)
|
||||
{
|
||||
HashSet<String> hashResult = new HashSet<String>(result);
|
||||
allResult.addAll(hashResult);
|
||||
}
|
||||
}
|
||||
if (allResult.size() > 0)
|
||||
{
|
||||
typeAliasesPackage = String.join(",", (String[]) allResult.toArray(new String[0]));
|
||||
}
|
||||
else
|
||||
{
|
||||
throw new RuntimeException("mybatis typeAliasesPackage 路径扫描错误,参数typeAliasesPackage:" + typeAliasesPackage + "未找到任何包");
|
||||
}
|
||||
}
|
||||
catch (IOException e)
|
||||
{
|
||||
e.printStackTrace();
|
||||
}
|
||||
return typeAliasesPackage;
|
||||
}
|
||||
|
||||
public Resource[] resolveMapperLocations(String[] mapperLocations)
|
||||
{
|
||||
ResourcePatternResolver resourceResolver = new PathMatchingResourcePatternResolver();
|
||||
List<Resource> resources = new ArrayList<Resource>();
|
||||
if (mapperLocations != null)
|
||||
{
|
||||
for (String mapperLocation : mapperLocations)
|
||||
{
|
||||
try
|
||||
{
|
||||
Resource[] mappers = resourceResolver.getResources(mapperLocation);
|
||||
resources.addAll(Arrays.asList(mappers));
|
||||
}
|
||||
catch (IOException e)
|
||||
{
|
||||
// ignore
|
||||
}
|
||||
}
|
||||
}
|
||||
return resources.toArray(new Resource[resources.size()]);
|
||||
}
|
||||
|
||||
@Bean
|
||||
public SqlSessionFactory sqlSessionFactory(DataSource dataSource) throws Exception
|
||||
{
|
||||
String typeAliasesPackage = env.getProperty("mybatis.typeAliasesPackage");
|
||||
String mapperLocations = env.getProperty("mybatis.mapperLocations");
|
||||
String configLocation = env.getProperty("mybatis.configLocation");
|
||||
typeAliasesPackage = setTypeAliasesPackage(typeAliasesPackage);
|
||||
VFS.addImplClass(SpringBootVFS.class);
|
||||
|
||||
final MybatisSqlSessionFactoryBean sessionFactory = new MybatisSqlSessionFactoryBean();
|
||||
sessionFactory.setDataSource(dataSource);
|
||||
sessionFactory.setTypeAliasesPackage(typeAliasesPackage);
|
||||
sessionFactory.setMapperLocations(resolveMapperLocations(StringUtils.split(mapperLocations, ",")));
|
||||
sessionFactory.setConfigLocation(new DefaultResourceLoader().getResource(configLocation));
|
||||
if (interceptors != null)
|
||||
{
|
||||
sessionFactory.setPlugins(interceptors);
|
||||
}
|
||||
return sessionFactory.getObject();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
package com.ruoyi.framework.config;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.DbType;
|
||||
import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor;
|
||||
import com.baomidou.mybatisplus.extension.plugins.inner.PaginationInnerInterceptor;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
|
||||
@Configuration
|
||||
public class MybatisPlusConfig
|
||||
{
|
||||
@Bean
|
||||
public MybatisPlusInterceptor mybatisPlusInterceptor()
|
||||
{
|
||||
MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
|
||||
interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL));
|
||||
return interceptor;
|
||||
}
|
||||
}
|
||||
@@ -14,13 +14,13 @@ public class LoanPricingSensitiveDisplayService
|
||||
}
|
||||
if (custName.contains("公司") && custName.length() > 4)
|
||||
{
|
||||
return custName.substring(0, 2) + "*".repeat(custName.length() - 4) + custName.substring(custName.length() - 2);
|
||||
return custName.substring(0, 2) + repeatMask(custName.length() - 4) + custName.substring(custName.length() - 2);
|
||||
}
|
||||
if (custName.length() == 1)
|
||||
{
|
||||
return custName;
|
||||
}
|
||||
return custName.substring(0, 1) + "*".repeat(custName.length() - 1);
|
||||
return custName.substring(0, 1) + repeatMask(custName.length() - 1);
|
||||
}
|
||||
|
||||
public String maskIdNum(String idNum)
|
||||
@@ -31,16 +31,30 @@ public class LoanPricingSensitiveDisplayService
|
||||
}
|
||||
if (idNum.startsWith("91") && idNum.length() == 18)
|
||||
{
|
||||
return idNum.substring(0, 2) + "*".repeat(13) + idNum.substring(idNum.length() - 3);
|
||||
return idNum.substring(0, 2) + repeatMask(13) + idNum.substring(idNum.length() - 3);
|
||||
}
|
||||
if (idNum.matches("\\d{17}[\\dXx]"))
|
||||
{
|
||||
return idNum.substring(0, 4) + "*".repeat(8) + idNum.substring(idNum.length() - 4);
|
||||
return idNum.substring(0, 4) + repeatMask(8) + idNum.substring(idNum.length() - 4);
|
||||
}
|
||||
if (idNum.length() > 5)
|
||||
{
|
||||
return idNum.substring(0, 2) + "*".repeat(idNum.length() - 5) + idNum.substring(idNum.length() - 3);
|
||||
return idNum.substring(0, 2) + repeatMask(idNum.length() - 5) + idNum.substring(idNum.length() - 3);
|
||||
}
|
||||
return "*".repeat(idNum.length());
|
||||
return repeatMask(idNum.length());
|
||||
}
|
||||
|
||||
private String repeatMask(int count)
|
||||
{
|
||||
if (count <= 0)
|
||||
{
|
||||
return "";
|
||||
}
|
||||
StringBuilder builder = new StringBuilder(count);
|
||||
for (int i = 0; i < count; i++)
|
||||
{
|
||||
builder.append('*');
|
||||
}
|
||||
return builder.toString();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,7 +15,7 @@ public class SensitiveFieldCryptoService
|
||||
{
|
||||
private final String key;
|
||||
|
||||
public SensitiveFieldCryptoService(@Value("${loan-pricing.sensitive.key:}") String key)
|
||||
public SensitiveFieldCryptoService(@Value("${security.password-transfer.key:}") String key)
|
||||
{
|
||||
this.key = key;
|
||||
}
|
||||
@@ -62,7 +62,7 @@ public class SensitiveFieldCryptoService
|
||||
{
|
||||
if (!StringUtils.hasText(key))
|
||||
{
|
||||
throw new IllegalStateException("loan-pricing.sensitive.key 未配置");
|
||||
throw new IllegalStateException("security.password-transfer.key 未配置");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -27,6 +27,7 @@ class SensitiveFieldCryptoServiceTest
|
||||
{
|
||||
SensitiveFieldCryptoService service = new SensitiveFieldCryptoService("");
|
||||
|
||||
assertThrows(IllegalStateException.class, () -> service.encrypt("张三"));
|
||||
IllegalStateException exception = assertThrows(IllegalStateException.class, () -> service.encrypt("张三"));
|
||||
assertEquals("security.password-transfer.key 未配置", exception.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -84,7 +84,7 @@ class LoanPricingWorkflowServiceImplTest
|
||||
row.setCalculateRate("6.15");
|
||||
|
||||
Page<LoanPricingWorkflowListVO> pageResult = new Page<>(1, 10);
|
||||
pageResult.setRecords(java.util.List.of(row));
|
||||
pageResult.setRecords(Collections.singletonList(row));
|
||||
|
||||
when(loanPricingWorkflowMapper.selectWorkflowPageWithRates(any(), any())).thenReturn(pageResult);
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
# 页面标题
|
||||
VUE_APP_TITLE = 若依管理系统
|
||||
VUE_APP_TITLE = 上虞利率定价系统
|
||||
|
||||
# 开发环境配置
|
||||
ENV = 'development'
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
# 页面标题
|
||||
VUE_APP_TITLE = 若依管理系统
|
||||
VUE_APP_TITLE = 上虞利率定价系统
|
||||
|
||||
# 生产环境配置
|
||||
ENV = 'production'
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
# 页面标题
|
||||
VUE_APP_TITLE = 若依管理系统
|
||||
VUE_APP_TITLE = 上虞利率定价系统
|
||||
|
||||
BABEL_ENV = production
|
||||
|
||||
@@ -10,3 +10,4 @@ ENV = 'staging'
|
||||
|
||||
# 若依管理系统/测试环境
|
||||
VUE_APP_BASE_API = '/stage-api'
|
||||
VUE_APP_PASSWORD_TRANSFER_KEY = '1234567890abcdef'
|
||||
|
||||
BIN
ruoyi-ui/dist.zip
Normal file
BIN
ruoyi-ui/dist.zip
Normal file
Binary file not shown.
14195
ruoyi-ui/package-lock.json
generated
Normal file
14195
ruoyi-ui/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "ruoyi",
|
||||
"version": "3.9.2",
|
||||
"version": "3.9.1",
|
||||
"description": "若依管理系统",
|
||||
"author": "若依",
|
||||
"license": "MIT",
|
||||
@@ -8,7 +8,11 @@
|
||||
"dev": "vue-cli-service serve",
|
||||
"build:prod": "vue-cli-service build",
|
||||
"build:stage": "vue-cli-service build --mode staging",
|
||||
"preview": "node build/index.js --preview"
|
||||
"preview": "node build/index.js --preview",
|
||||
"test:password-transfer": "node tests/password-transfer-api.test.js",
|
||||
"test:retail-display-fields": "node tests/retail-display-fields.test.js",
|
||||
"test:personal-create-input-params": "node tests/personal-create-input-params.test.js",
|
||||
"test:id-number-validation-removal": "node tests/id-number-validation-removal.test.js"
|
||||
},
|
||||
"keywords": [
|
||||
"vue",
|
||||
@@ -25,15 +29,15 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@riophae/vue-treeselect": "0.4.0",
|
||||
"axios": "0.30.3",
|
||||
"axios": "0.28.1",
|
||||
"clipboard": "2.0.8",
|
||||
"core-js": "3.37.1",
|
||||
"crypto-js": "4.2.0",
|
||||
"echarts": "5.4.0",
|
||||
"element-ui": "2.15.14",
|
||||
"file-saver": "2.0.5",
|
||||
"fuse.js": "6.4.3",
|
||||
"highlight.js": "9.18.5",
|
||||
"crypto-js": "4.2.0",
|
||||
"js-beautify": "1.13.0",
|
||||
"js-cookie": "3.0.1",
|
||||
"jsencrypt": "3.0.0-rc.1",
|
||||
@@ -41,6 +45,7 @@
|
||||
"quill": "2.0.2",
|
||||
"screenfull": "5.0.2",
|
||||
"sortablejs": "1.10.2",
|
||||
"splitpanes": "2.4.1",
|
||||
"vue": "2.6.12",
|
||||
"vue-count-to": "1.0.13",
|
||||
"vue-cropper": "0.5.5",
|
||||
@@ -55,7 +60,6 @@
|
||||
"chalk": "4.1.0",
|
||||
"compression-webpack-plugin": "6.1.2",
|
||||
"connect": "3.6.6",
|
||||
"html-webpack-plugin": "3.2.0",
|
||||
"sass": "1.32.13",
|
||||
"sass-loader": "10.1.1",
|
||||
"script-ext-html-webpack-plugin": "2.1.5",
|
||||
|
||||
@@ -41,15 +41,6 @@ export function getInfo() {
|
||||
})
|
||||
}
|
||||
|
||||
// 解锁屏幕
|
||||
export function unlockScreen(password) {
|
||||
return request({
|
||||
url: '/unlockscreen',
|
||||
method: 'post',
|
||||
data: { password }
|
||||
})
|
||||
}
|
||||
|
||||
// 退出方法
|
||||
export function logout() {
|
||||
return request({
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import request from '@/utils/request'
|
||||
|
||||
// 查询缓存详细
|
||||
// 查询缓存概览
|
||||
export function getCache() {
|
||||
return request({
|
||||
url: '/monitor/cache',
|
||||
@@ -32,7 +32,7 @@ export function getCacheValue(cacheName, cacheKey) {
|
||||
})
|
||||
}
|
||||
|
||||
// 清理指定名称缓存
|
||||
// 清理指定名称下的缓存
|
||||
export function clearCacheName(cacheName) {
|
||||
return request({
|
||||
url: '/monitor/cache/clearCacheName/' + cacheName,
|
||||
|
||||
@@ -43,15 +43,6 @@ export function updateDept(data) {
|
||||
})
|
||||
}
|
||||
|
||||
// 保存部门排序
|
||||
export function updateDeptSort(data) {
|
||||
return request({
|
||||
url: '/system/dept/updateSort',
|
||||
method: 'put',
|
||||
data: data
|
||||
})
|
||||
}
|
||||
|
||||
// 删除部门
|
||||
export function delDept(deptId) {
|
||||
return request({
|
||||
|
||||
@@ -51,15 +51,6 @@ export function updateMenu(data) {
|
||||
})
|
||||
}
|
||||
|
||||
// 保存菜单排序
|
||||
export function updateMenuSort(data) {
|
||||
return request({
|
||||
url: '/system/menu/updateSort',
|
||||
method: 'put',
|
||||
data: data
|
||||
})
|
||||
}
|
||||
|
||||
// 删除菜单
|
||||
export function delMenu(menuId) {
|
||||
return request({
|
||||
|
||||
@@ -42,38 +42,3 @@ export function delNotice(noticeId) {
|
||||
method: 'delete'
|
||||
})
|
||||
}
|
||||
|
||||
// 首页顶部公告列表(带已读状态)
|
||||
export function listNoticeTop() {
|
||||
return request({
|
||||
url: '/system/notice/listTop',
|
||||
method: 'get'
|
||||
})
|
||||
}
|
||||
|
||||
// 标记公告已读
|
||||
export function markNoticeRead(noticeId) {
|
||||
return request({
|
||||
url: '/system/notice/markRead',
|
||||
method: 'post',
|
||||
params: { noticeId }
|
||||
})
|
||||
}
|
||||
|
||||
// 批量标记已读
|
||||
export function markNoticeReadAll(ids) {
|
||||
return request({
|
||||
url: '/system/notice/markReadAll',
|
||||
method: 'post',
|
||||
params: { ids }
|
||||
})
|
||||
}
|
||||
|
||||
// 查询公告已读用户列表
|
||||
export function listNoticeReadUsers(query) {
|
||||
return request({
|
||||
url: '/system/notice/readUsers/list',
|
||||
method: 'get',
|
||||
params: query
|
||||
})
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import request from '@/utils/request'
|
||||
import { parseStrEmpty } from "@/utils/ruoyi"
|
||||
import { parseStrEmpty } from "@/utils/ruoyi";
|
||||
import { encryptPasswordFields } from '@/utils/passwordTransfer'
|
||||
|
||||
// 查询用户列表
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1773923748724" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="5930" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200"><path d="M512 212l48.8 12c101.6 24.8 176 117.6 176 220.8v254.4l18.4 18.4 24.8 25.6h-536l24.8-25.6 18.4-18.4V444.8c0-103.2 73.6-196.8 176-220.8l48.8-12M512 64c-36.8 0-64 30.4-64 68v30.4C320.8 192 223.2 307.2 223.2 444.8v228.8L136 763.2v44.8h752v-44.8l-87.2-89.6V444.8c0-137.6-97.6-252.8-224.8-283.2v-28.8c0-32-17.6-60.8-48-67.2-5.6-1.6-11.2-1.6-16-1.6z m88 808H424c0 49.6 38.4 88 88 88s88-38.4 88-88z" p-id="5931"></path></svg>
|
||||
|
Before Width: | Height: | Size: 750 B |
@@ -81,10 +81,6 @@
|
||||
margin-top: 6vh !important;
|
||||
}
|
||||
|
||||
.el-dialog__body {
|
||||
padding: 8px 20px !important;
|
||||
}
|
||||
|
||||
.el-dialog__wrapper.scrollbar .el-dialog .el-dialog__body {
|
||||
overflow: auto;
|
||||
overflow-x: hidden;
|
||||
@@ -232,191 +228,6 @@
|
||||
color: #FFFFFF;
|
||||
}
|
||||
|
||||
/** 详细卡片样式 */
|
||||
.detail-drawer {
|
||||
.el-drawer__header {
|
||||
margin-bottom: 6px;
|
||||
padding: 8px 12px 6px;
|
||||
font-size: 15px;
|
||||
color: #303133;
|
||||
background: #f8f8f8;
|
||||
}
|
||||
.section-header {
|
||||
font-size: 15px;
|
||||
color: #6379bb;
|
||||
border-bottom: 1px solid #ddd;
|
||||
margin: 12px 0 16px 0;
|
||||
padding-bottom: 8px;
|
||||
}
|
||||
.drawer-content {
|
||||
padding: 0 20px 20px 20px;
|
||||
.info-item {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
padding: 8px 0;
|
||||
min-height: 40px;
|
||||
}
|
||||
.info-label {
|
||||
flex-shrink: 0;
|
||||
width: 200px;
|
||||
color: #606266;
|
||||
font-size: 13px;
|
||||
line-height: 1.6;
|
||||
padding-top: 4px;
|
||||
text-align: right;
|
||||
margin-right: 14px;
|
||||
}
|
||||
.info-value {
|
||||
flex: 1;
|
||||
color: #303133;
|
||||
font-size: 13px;
|
||||
font-weight: 500;
|
||||
line-height: 1.6;
|
||||
word-break: break-all;
|
||||
padding-top: 4px;
|
||||
min-height: 1.6em;
|
||||
&.plaintext {
|
||||
border-bottom: 1px dashed #dde1e6;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.detail-wrap { padding: 0 4px; }
|
||||
|
||||
.detail-card {
|
||||
border: 1px solid #ebeef5;
|
||||
border-radius: 6px;
|
||||
margin-bottom: 14px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.detail-card-title {
|
||||
background: #f7f9fb;
|
||||
padding: 8px 16px;
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
color: #333;
|
||||
border-bottom: 1px solid #ebeef5;
|
||||
}
|
||||
.detail-card-title i { margin-right: 5px; color: #409EFF; }
|
||||
|
||||
.detail-row { padding: 0 8px; }
|
||||
|
||||
.detail-item {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
padding: 10px 8px;
|
||||
font-size: 13px;
|
||||
border-bottom: 1px solid #f5f7fa;
|
||||
}
|
||||
.detail-item:last-child { border-bottom: none; }
|
||||
|
||||
.detail-label {
|
||||
flex-shrink: 0;
|
||||
width: 72px;
|
||||
color: #909399;
|
||||
margin-right: 12px;
|
||||
}
|
||||
.detail-value { color: #303133; flex: 1; word-break: break-all; }
|
||||
.detail-location { color: #999; font-size: 12px; }
|
||||
|
||||
.method-tag {
|
||||
display: inline-block;
|
||||
padding: 1px 7px;
|
||||
border-radius: 3px;
|
||||
font-size: 11px;
|
||||
font-weight: 700;
|
||||
margin-right: 6px;
|
||||
vertical-align: middle;
|
||||
}
|
||||
.mono { font-family: Consolas, 'SFMono-Regular', monospace; font-size: 12px; }
|
||||
.code-body { padding: 14px; }
|
||||
.code-wrap {
|
||||
background: #f7f9fb;
|
||||
border: 1px solid #e8ecf0;
|
||||
border-radius: 4px;
|
||||
overflow: hidden;
|
||||
max-height: 260px;
|
||||
position: relative;
|
||||
}
|
||||
.code-action {
|
||||
position: absolute;
|
||||
top: 8px;
|
||||
right: 8px;
|
||||
z-index: 10;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
.code-action .el-button {
|
||||
height: 24px;
|
||||
font-size: 12px;
|
||||
padding: 4px 8px;
|
||||
background: rgba(255, 255, 255, 0.9);
|
||||
border: 1px solid #dcdcdc;
|
||||
}
|
||||
.code-action .el-button:hover {
|
||||
background: #ffffff;
|
||||
border-color: #409EFF;
|
||||
}
|
||||
.code-pre {
|
||||
margin: 0;
|
||||
padding: 12px 14px;
|
||||
font-size: 12px;
|
||||
line-height: 1.6;
|
||||
font-family: Consolas, 'SFMono-Regular', monospace;
|
||||
color: #444;
|
||||
white-space: pre-wrap;
|
||||
word-break: break-all;
|
||||
overflow: auto;
|
||||
max-height: 240px;
|
||||
display: block;
|
||||
}
|
||||
|
||||
/* tree-sidebar content */
|
||||
.tree-sidebar-manage-wrap {
|
||||
display: flex;
|
||||
gap: 0;
|
||||
min-height: calc(100vh - 130px);
|
||||
padding: 0 !important;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.tree-sidebar-content {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
background: #fff;
|
||||
|
||||
.content-inner {
|
||||
padding: 12px 16px;
|
||||
height: 100%;
|
||||
overflow-y: auto;
|
||||
}
|
||||
}
|
||||
|
||||
/* error */
|
||||
.error-title { color: #c0392b !important; }
|
||||
.error-title i { color: #c0392b !important; }
|
||||
.error-body { padding: 12px 16px; }
|
||||
.error-msg {
|
||||
background: #fff8f8;
|
||||
border-left: 3px solid #e74c3c;
|
||||
border-radius: 3px;
|
||||
padding: 8px 12px;
|
||||
color: #c0392b;
|
||||
font-size: 12px;
|
||||
line-height: 1.7;
|
||||
word-break: break-all;
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
|
||||
/* http method */
|
||||
.method-GET { background: #e8f5e9; color: #27ae60; }
|
||||
.method-POST { background: #e3f2fd; color: #1565c0; }
|
||||
.method-PUT { background: #fff3e0; color: #e65100; }
|
||||
.method-DELETE { background: #ffebee; color: #c62828; }
|
||||
|
||||
/* text color */
|
||||
.text-navy {
|
||||
color: #1ab394;
|
||||
@@ -469,9 +280,6 @@
|
||||
}
|
||||
|
||||
/* 拖拽列样式 */
|
||||
.allowDrag { cursor: grab; }
|
||||
.allowDrag:active { cursor: grabbing; }
|
||||
|
||||
.sortable-ghost {
|
||||
opacity: .8;
|
||||
color: #fff !important;
|
||||
@@ -482,3 +290,8 @@
|
||||
position: relative;
|
||||
float: right;
|
||||
}
|
||||
|
||||
/* 分割面板样式 */
|
||||
.splitpanes.default-theme .splitpanes__pane {
|
||||
background-color: #fff!important;
|
||||
}
|
||||
|
||||
@@ -74,8 +74,6 @@
|
||||
overflow: hidden !important;
|
||||
text-overflow: ellipsis !important;
|
||||
white-space: nowrap !important;
|
||||
height: 44px !important;
|
||||
line-height: 44px !important;
|
||||
}
|
||||
|
||||
// menu hover
|
||||
@@ -107,96 +105,6 @@
|
||||
background-color: $base-sub-menu-hover !important;
|
||||
}
|
||||
}
|
||||
|
||||
// theme-dark 深色主题
|
||||
&.theme-dark {
|
||||
box-shadow: 2px 0 8px rgba(0, 0, 0, 0.4);
|
||||
border-right: none;
|
||||
|
||||
.el-menu-item.is-active {
|
||||
position: relative;
|
||||
|
||||
&::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
background-color: var(--current-color-dark-bg, rgba(64, 158, 255, 0.2));
|
||||
border-right: 3px solid var(--current-color, #409eff);
|
||||
pointer-events: none;
|
||||
z-index: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.el-submenu.is-active > .el-submenu__title {
|
||||
color: var(--current-color, #409eff) !important;
|
||||
}
|
||||
|
||||
.el-menu-item:not(.is-active),
|
||||
.submenu-title-noDropdown,
|
||||
.el-submenu__title {
|
||||
position: relative;
|
||||
|
||||
&::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
background-color: transparent;
|
||||
pointer-events: none;
|
||||
z-index: 1;
|
||||
transition: background-color 0.2s;
|
||||
}
|
||||
|
||||
&:hover::before {
|
||||
background-color: var(--current-color-dark-bg, rgba(64, 158, 255, 0.2));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// theme-light 浅色主题
|
||||
&.theme-light {
|
||||
border-right: 1px solid #e8eaf0;
|
||||
box-shadow: none;
|
||||
|
||||
.el-menu-item,
|
||||
.el-submenu__title {
|
||||
color: rgba(0, 0, 0, 0.65);
|
||||
}
|
||||
|
||||
.el-menu-item.is-active {
|
||||
color: var(--current-color, #409eff) !important;
|
||||
position: relative;
|
||||
|
||||
&::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
background-color: var(--current-color-light, #ecf5ff);
|
||||
border-right: 3px solid var(--current-color, #409eff);
|
||||
pointer-events: none;
|
||||
z-index: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.el-submenu.is-active > .el-submenu__title {
|
||||
color: var(--current-color, #409eff) !important;
|
||||
}
|
||||
|
||||
.el-menu-item:not(.is-active):hover,
|
||||
.submenu-title-noDropdown:hover,
|
||||
.el-submenu__title:hover {
|
||||
background-color: #f5f7fa !important;
|
||||
color: rgba(0, 0, 0, 0.85) !important;
|
||||
}
|
||||
|
||||
.nest-menu .el-submenu > .el-submenu__title,
|
||||
.el-submenu .el-menu-item {
|
||||
background-color: #fafafa !important;
|
||||
|
||||
&:hover {
|
||||
background-color: #f0f5ff !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.hideSidebar {
|
||||
|
||||
@@ -9,17 +9,32 @@ $yellow:#FEC171;
|
||||
$panGreen: #30B08F;
|
||||
|
||||
// 默认菜单主题风格
|
||||
$base-menu-color: #bfcbd9;
|
||||
$base-menu-color-active: #ffffff;
|
||||
$base-menu-background: #1a1f2e;
|
||||
$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,.70);
|
||||
$base-menu-light-background:#ffffff;
|
||||
$base-logo-light-title-color: #001529;
|
||||
|
||||
$base-sub-menu-background: #141824;
|
||||
$base-sub-menu-hover: rgba(255,255,255,.06);
|
||||
$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;
|
||||
*/
|
||||
|
||||
$base-sidebar-width: 200px;
|
||||
|
||||
|
||||
@@ -1,126 +0,0 @@
|
||||
<template>
|
||||
<el-dialog :title="title" :visible.sync="visible" :width="width" append-to-body @close="handleClose">
|
||||
<el-upload ref="uploadRef" :limit="1" accept=".xlsx, .xls" :headers="headers" :action="uploadUrl" :disabled="isUploading" :on-progress="handleProgress" :on-success="handleSuccess" :auto-upload="false" drag>
|
||||
<i class="el-icon-upload"></i>
|
||||
<div class="el-upload__text">将文件拖到此处,或<em>点击上传</em></div>
|
||||
<div class="el-upload__tip text-center" slot="tip">
|
||||
<div class="el-upload__tip" slot="tip">
|
||||
<el-checkbox v-model="updateSupport"> {{ updateSupportLabel }} </el-checkbox>
|
||||
</div>
|
||||
<span>仅允许导入xls、xlsx格式文件。</span>
|
||||
<el-link v-if="templateUrl" type="primary" :underline="false" style="font-size: 12px; vertical-align: baseline" @click="handleDownloadTemplate">下载模板</el-link>
|
||||
</div>
|
||||
</el-upload>
|
||||
<div slot="footer" class="dialog-footer">
|
||||
<el-button type="primary" @click="handleSubmit">确 定</el-button>
|
||||
<el-button @click="visible = false">取 消</el-button>
|
||||
</div>
|
||||
</el-dialog>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { getToken } from '@/utils/auth'
|
||||
|
||||
export default {
|
||||
props: {
|
||||
// 对话框标题
|
||||
title: {
|
||||
type: String,
|
||||
default: '数据导入'
|
||||
},
|
||||
// 对话框宽度
|
||||
width: {
|
||||
type: String,
|
||||
default: '400px'
|
||||
},
|
||||
// 上传接口地址(必传)
|
||||
action: {
|
||||
type: String,
|
||||
required: true
|
||||
},
|
||||
// 模板下载接口地址,不传则不显示下载模板链接
|
||||
templateAction: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
// 模板文件名
|
||||
templateFileName: {
|
||||
type: String,
|
||||
default: 'template'
|
||||
},
|
||||
// 覆盖更新勾选框的说明文字
|
||||
updateSupportLabel: {
|
||||
type: String,
|
||||
default: '是否更新已经存在的数据'
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
visible: false,
|
||||
isUploading: false,
|
||||
updateSupport: false,
|
||||
headers: { Authorization: 'Bearer ' + getToken() }
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
uploadUrl() {
|
||||
return process.env.VUE_APP_BASE_API + this.action + '?updateSupport=' + (this.updateSupport ? 1 : 0)
|
||||
},
|
||||
templateUrl() {
|
||||
return !!this.templateAction
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
// 打开对话框(供父组件通过 ref 调用)
|
||||
open() {
|
||||
this.updateSupport = false
|
||||
this.isUploading = false
|
||||
this.visible = true
|
||||
this.$nextTick(() => {
|
||||
if (this.$refs.uploadRef) {
|
||||
this.$refs.uploadRef.clearFiles()
|
||||
}
|
||||
})
|
||||
},
|
||||
// 关闭时清理
|
||||
handleClose() {
|
||||
this.isUploading = false
|
||||
if (this.$refs.uploadRef) {
|
||||
this.$refs.uploadRef.clearFiles()
|
||||
}
|
||||
},
|
||||
// 下载模板
|
||||
handleDownloadTemplate() {
|
||||
this.download(this.templateAction, {}, `${this.templateFileName}_${new Date().getTime()}.xlsx`)
|
||||
},
|
||||
// 上传进度
|
||||
handleProgress() {
|
||||
this.isUploading = true
|
||||
},
|
||||
// 上传成功
|
||||
handleSuccess(response) {
|
||||
this.visible = false
|
||||
this.isUploading = false
|
||||
if (this.$refs.uploadRef) {
|
||||
this.$refs.uploadRef.clearFiles()
|
||||
}
|
||||
this.$alert("<div style='overflow:auto;overflow-x:hidden;max-height:70vh;padding:10px 20px 0;'>" + response.msg + '</div>', '导入结果', { dangerouslyUseHTMLString: true })
|
||||
this.$emit('success')
|
||||
},
|
||||
// 提交上传
|
||||
handleSubmit() {
|
||||
const files = this.$refs.uploadRef.uploadFiles
|
||||
if (!files || files.length === 0) {
|
||||
this.$modal.msgError('请选择要上传的文件。')
|
||||
return
|
||||
}
|
||||
const name = files[0].name.toLowerCase()
|
||||
if (!name.endsWith('.xls') && !name.endsWith('.xlsx')) {
|
||||
this.$modal.msgError('请选择后缀为 "xls" 或 "xlsx" 的文件。')
|
||||
return
|
||||
}
|
||||
this.$refs.uploadRef.submit()
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@@ -5,7 +5,6 @@
|
||||
:visible.sync="show"
|
||||
width="600px"
|
||||
@close="close"
|
||||
@opened="onDialogOpened"
|
||||
:show-close="false"
|
||||
append-to-body
|
||||
>
|
||||
@@ -22,54 +21,24 @@
|
||||
@keydown.down.native="navigateResult('down')"
|
||||
>
|
||||
</el-input>
|
||||
|
||||
<div class="result-count" v-if="search && options.length > 0">
|
||||
找到 <strong>{{ options.length }}</strong> 个结果
|
||||
</div>
|
||||
|
||||
<el-scrollbar wrap-class="right-scrollbar-wrapper">
|
||||
<div class="result-wrap">
|
||||
<template v-if="options.length > 0">
|
||||
<div
|
||||
class="search-item"
|
||||
v-for="(item, index) in options"
|
||||
:key="item.path"
|
||||
:class="{ 'is-active': index === activeIndex }"
|
||||
:style="activeStyle(index)"
|
||||
@mouseenter="activeIndex = index"
|
||||
@mouseleave="activeIndex = -1"
|
||||
>
|
||||
<div class="search-item" v-for="(item, index) in options" :key="item.path" :style="activeStyle(index)" @mouseenter="activeIndex = index" @mouseleave="activeIndex = -1">
|
||||
<div class="left">
|
||||
<svg-icon class="menu-icon" :icon-class="item.icon" />
|
||||
</div>
|
||||
<div class="search-info" @click="change(item)">
|
||||
<div class="menu-title" v-html="highlightText(item.title.join(' / '))"></div>
|
||||
<div class="menu-path" v-html="highlightText(item.path)"></div>
|
||||
<div class="menu-title">
|
||||
{{ item.title.join(" / ") }}
|
||||
</div>
|
||||
<svg-icon icon-class="enter" v-show="index === activeIndex" />
|
||||
<div class="menu-path">
|
||||
{{ item.path }}
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<div class="empty-state" v-else-if="search && options.length === 0">
|
||||
<i class="el-icon-search empty-icon"></i>
|
||||
<p class="empty-text">未找到 "<strong>{{ search }}</strong>" 相关菜单</p>
|
||||
<p class="empty-tip">试试其他关键词或路径</p>
|
||||
</div>
|
||||
|
||||
<svg-icon icon-class="enter" v-show="index === activeIndex"/>
|
||||
</div>
|
||||
</div>
|
||||
</el-scrollbar>
|
||||
|
||||
<div class="search-footer">
|
||||
<span class="shortcut-item">
|
||||
<kbd>↑</kbd><kbd>↓</kbd> 切换
|
||||
</span>
|
||||
<span class="shortcut-item">
|
||||
<kbd>↵</kbd> 选择
|
||||
</span>
|
||||
<span class="shortcut-item">
|
||||
<kbd>Esc</kbd> 关闭
|
||||
</span>
|
||||
</div>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
@@ -114,37 +83,33 @@ export default {
|
||||
click() {
|
||||
this.show = !this.show
|
||||
if (this.show) {
|
||||
this.$refs.headerSearchSelect && this.$refs.headerSearchSelect.focus()
|
||||
this.options = this.searchPool
|
||||
}
|
||||
},
|
||||
onDialogOpened() {
|
||||
this.$nextTick(() => {
|
||||
this.$refs.headerSearchSelectRef && this.$refs.headerSearchSelectRef.focus()
|
||||
})
|
||||
},
|
||||
close() {
|
||||
this.$refs.headerSearchSelectRef && this.$refs.headerSearchSelectRef.blur()
|
||||
this.$refs.headerSearchSelect && this.$refs.headerSearchSelect.blur()
|
||||
this.search = ''
|
||||
this.options = this.searchPool
|
||||
this.options = []
|
||||
this.show = false
|
||||
this.activeIndex = -1
|
||||
},
|
||||
change(val) {
|
||||
const p = val.path
|
||||
const path = val.path
|
||||
const query = val.query
|
||||
if (isHttp(val.path)) {
|
||||
if(isHttp(val.path)) {
|
||||
// http(s):// 路径新窗口打开
|
||||
const pindex = p.indexOf('http')
|
||||
window.open(p.substr(pindex, p.length), '_blank')
|
||||
const pindex = path.indexOf("http")
|
||||
window.open(path.substr(pindex, path.length), "_blank")
|
||||
} else {
|
||||
if (query) {
|
||||
this.$router.push({ path: p, query: JSON.parse(query) })
|
||||
this.$router.push({ path: path, query: JSON.parse(query) })
|
||||
} else {
|
||||
this.$router.push(p)
|
||||
this.$router.push(path)
|
||||
}
|
||||
}
|
||||
this.search = ''
|
||||
this.options = this.searchPool
|
||||
this.options = []
|
||||
this.$nextTick(() => {
|
||||
this.show = false
|
||||
})
|
||||
@@ -152,7 +117,9 @@ export default {
|
||||
initFuse(list) {
|
||||
this.fuse = new Fuse(list, {
|
||||
shouldSort: true,
|
||||
threshold: 0.2,
|
||||
threshold: 0.4,
|
||||
location: 0,
|
||||
distance: 100,
|
||||
minMatchCharLength: 1,
|
||||
keys: [{
|
||||
name: 'title',
|
||||
@@ -163,25 +130,37 @@ export default {
|
||||
}]
|
||||
})
|
||||
},
|
||||
// Filter out the routes that can be displayed in the sidebar
|
||||
// And generate the internationalized title
|
||||
generateRoutes(routes, basePath = '/', prefixTitle = []) {
|
||||
let res = []
|
||||
|
||||
for (const router of routes) {
|
||||
// skip hidden router
|
||||
if (router.hidden) { continue }
|
||||
|
||||
const data = {
|
||||
path: !isHttp(router.path) ? path.resolve(basePath, router.path) : router.path,
|
||||
title: [...prefixTitle],
|
||||
icon: ''
|
||||
}
|
||||
|
||||
if (router.meta && router.meta.title) {
|
||||
data.title = [...data.title, router.meta.title]
|
||||
data.icon = router.meta.icon
|
||||
|
||||
if (router.redirect !== 'noRedirect') {
|
||||
// only push the routes with title
|
||||
// special case: need to exclude parent router without redirect
|
||||
res.push(data)
|
||||
}
|
||||
}
|
||||
|
||||
if (router.query) {
|
||||
data.query = router.query
|
||||
}
|
||||
|
||||
// recursive child routes
|
||||
if (router.children) {
|
||||
const tempRoutes = this.generateRoutes(router.children, data.path, data.title)
|
||||
if (tempRoutes.length >= 1) {
|
||||
@@ -194,18 +173,7 @@ export default {
|
||||
querySearch(query) {
|
||||
this.activeIndex = -1
|
||||
if (query !== '') {
|
||||
const q = query.toLowerCase()
|
||||
const pathMatches = this.searchPool.filter(item =>
|
||||
item.path.toLowerCase().includes(q)
|
||||
)
|
||||
const fuseMatches = this.fuse.search(query).map(item => item.item)
|
||||
const merged = [...pathMatches]
|
||||
fuseMatches.forEach(item => {
|
||||
if (!merged.find(m => m.path === item.path)) {
|
||||
merged.push(item)
|
||||
}
|
||||
})
|
||||
this.options = merged
|
||||
this.options = this.fuse.search(query).map((item) => item.item) ?? this.searchPool
|
||||
} else {
|
||||
this.options = this.searchPool
|
||||
}
|
||||
@@ -213,14 +181,14 @@ export default {
|
||||
activeStyle(index) {
|
||||
if (index !== this.activeIndex) return {}
|
||||
return {
|
||||
'background-color': this.theme,
|
||||
'color': '#fff'
|
||||
"background-color": this.theme,
|
||||
"color": "#fff"
|
||||
}
|
||||
},
|
||||
navigateResult(direction) {
|
||||
if (direction === 'up') {
|
||||
if (direction === "up") {
|
||||
this.activeIndex = this.activeIndex <= 0 ? this.options.length - 1 : this.activeIndex - 1
|
||||
} else if (direction === 'down') {
|
||||
} else if (direction === "down") {
|
||||
this.activeIndex = this.activeIndex >= this.options.length - 1 ? 0 : this.activeIndex + 1
|
||||
}
|
||||
},
|
||||
@@ -228,16 +196,6 @@ export default {
|
||||
if (this.options.length > 0 && this.activeIndex >= 0) {
|
||||
this.change(this.options[this.activeIndex])
|
||||
}
|
||||
},
|
||||
highlightText(text) {
|
||||
if (!text) return ''
|
||||
if (!this.search) return text
|
||||
const keyword = this.escapeRegExp(this.search)
|
||||
const reg = new RegExp(`(${keyword})`, 'gi')
|
||||
return text.replace(reg, '<span class="highlight">$1</span>')
|
||||
},
|
||||
escapeRegExp(str) {
|
||||
return str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -246,17 +204,7 @@ export default {
|
||||
<style lang='scss' scoped>
|
||||
::v-deep {
|
||||
.el-dialog__header {
|
||||
padding: 6px !important;
|
||||
}
|
||||
|
||||
.highlight {
|
||||
color: red;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.is-active .highlight {
|
||||
color: rgba(255, 255, 255, 0.9);
|
||||
font-weight: 600;
|
||||
padding: 0 !important;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -268,33 +216,19 @@ export default {
|
||||
}
|
||||
}
|
||||
|
||||
.result-count {
|
||||
padding: 6px 16px 0;
|
||||
font-size: 12px;
|
||||
color: #aaa;
|
||||
|
||||
strong {
|
||||
color: red;
|
||||
font-weight: 600;
|
||||
}
|
||||
}
|
||||
|
||||
.result-wrap {
|
||||
height: 280px;
|
||||
margin: 4px 0;
|
||||
margin: 6px 0;
|
||||
|
||||
.search-item {
|
||||
display: flex;
|
||||
height: 48px;
|
||||
align-items: center;
|
||||
padding-right: 10px;
|
||||
border-radius: 4px;
|
||||
transition: background 0.15s;
|
||||
|
||||
.left {
|
||||
width: 60px;
|
||||
text-align: center;
|
||||
flex-shrink: 0;
|
||||
|
||||
.menu-icon {
|
||||
width: 18px;
|
||||
@@ -310,16 +244,11 @@ export default {
|
||||
flex-direction: column;
|
||||
justify-content: flex-start;
|
||||
flex: 1;
|
||||
overflow: hidden;
|
||||
|
||||
.menu-title,
|
||||
.menu-path {
|
||||
height: 20px;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.menu-path {
|
||||
color: #ccc;
|
||||
font-size: 10px;
|
||||
@@ -330,68 +259,6 @@ export default {
|
||||
.search-item:hover {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.empty-state {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 100%;
|
||||
|
||||
.empty-icon {
|
||||
font-size: 42px;
|
||||
color: #e0e0e0;
|
||||
margin-bottom: 14px;
|
||||
}
|
||||
|
||||
.empty-text {
|
||||
font-size: 14px;
|
||||
color: #999;
|
||||
margin: 0 0 6px;
|
||||
|
||||
strong {
|
||||
color: #666;
|
||||
}
|
||||
}
|
||||
|
||||
.empty-tip {
|
||||
font-size: 12px;
|
||||
color: #bbb;
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.search-footer {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 28px;
|
||||
padding: 10px 20px;
|
||||
border-top: 1px solid #f0f0f0;
|
||||
color: #999;
|
||||
font-size: 12px;
|
||||
|
||||
.shortcut-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 5px;
|
||||
}
|
||||
|
||||
kbd {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
min-width: 20px;
|
||||
height: 20px;
|
||||
padding: 0 5px;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 4px;
|
||||
background: #f7f7f7;
|
||||
color: #555;
|
||||
font-size: 11px;
|
||||
font-family: inherit;
|
||||
line-height: 1;
|
||||
box-shadow: 0 1px 0 #ccc;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
|
||||
@@ -38,8 +38,6 @@
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import cache from '@/plugins/cache'
|
||||
|
||||
export default {
|
||||
name: "RightToolbar",
|
||||
data() {
|
||||
@@ -78,11 +76,6 @@ export default {
|
||||
type: Number,
|
||||
default: 10
|
||||
},
|
||||
/* 列显隐状态记忆的 localStorage key(传入则启用记忆,不传则不记忆) */
|
||||
storageKey: {
|
||||
type: String,
|
||||
default: ""
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
style() {
|
||||
@@ -110,23 +103,6 @@ export default {
|
||||
}
|
||||
},
|
||||
created() {
|
||||
// 如果传入了 storageKey,从 localStorage 恢复列显隐状态
|
||||
if (this.storageKey) {
|
||||
try {
|
||||
const saved = cache.local.getJSON(this.storageKey)
|
||||
if (saved && typeof saved === 'object') {
|
||||
if (Array.isArray(this.columns)) {
|
||||
this.columns.forEach((col, index) => {
|
||||
if (saved[index] !== undefined) col.visible = saved[index]
|
||||
})
|
||||
} else {
|
||||
Object.keys(this.columns).forEach(key => {
|
||||
if (saved[key] !== undefined) this.columns[key].visible = saved[key]
|
||||
})
|
||||
}
|
||||
}
|
||||
} catch (e) {}
|
||||
}
|
||||
if (this.showColumnsType == 'transfer') {
|
||||
// transfer穿梭显隐列初始默认隐藏列
|
||||
if (Array.isArray(this.columns)) {
|
||||
@@ -147,34 +123,7 @@ export default {
|
||||
methods: {
|
||||
// 搜索
|
||||
toggleSearch() {
|
||||
let el = this.$el
|
||||
let formEl = null
|
||||
while ((el = el.parentElement) && el !== document.body) {
|
||||
if ((formEl = el.querySelector('.el-form'))) break
|
||||
}
|
||||
if (!formEl) return this.$emit('update:showSearch', !this.showSearch)
|
||||
this._animateSearch(formEl, this.showSearch)
|
||||
},
|
||||
// 搜索栏动画
|
||||
_animateSearch(el, isHide) {
|
||||
const DURATION = 260
|
||||
const TRANSITION = 'max-height 0.25s ease, opacity 0.2s ease'
|
||||
const clear = () => Object.assign(el.style, { transition: '', maxHeight: '', opacity: '', overflow: '' })
|
||||
Object.assign(el.style, { overflow: 'hidden', transition: '' })
|
||||
if (isHide) {
|
||||
Object.assign(el.style, { maxHeight: el.scrollHeight + 'px', opacity: '1', transition: TRANSITION })
|
||||
requestAnimationFrame(() => Object.assign(el.style, { maxHeight: '0', opacity: '0' }))
|
||||
setTimeout(() => { this.$emit('update:showSearch', false); clear() }, DURATION)
|
||||
} else {
|
||||
this.$emit('update:showSearch', true)
|
||||
this.$nextTick(() => {
|
||||
Object.assign(el.style, { maxHeight: '0', opacity: '0' })
|
||||
requestAnimationFrame(() => requestAnimationFrame(() => {
|
||||
Object.assign(el.style, { transition: TRANSITION, maxHeight: el.scrollHeight + 'px', opacity: '1' })
|
||||
}))
|
||||
setTimeout(clear, DURATION)
|
||||
})
|
||||
}
|
||||
this.$emit("update:showSearch", !this.showSearch)
|
||||
},
|
||||
// 刷新
|
||||
refresh() {
|
||||
@@ -192,7 +141,6 @@ export default {
|
||||
this.columns[key].visible = !data.includes(index)
|
||||
})
|
||||
}
|
||||
this.saveStorage()
|
||||
},
|
||||
// 打开显隐列dialog
|
||||
showColumn() {
|
||||
@@ -205,7 +153,6 @@ export default {
|
||||
} else {
|
||||
this.columns[key].visible = event
|
||||
}
|
||||
this.saveStorage()
|
||||
},
|
||||
// 切换全选/反选
|
||||
toggleCheckAll() {
|
||||
@@ -215,20 +162,6 @@ export default {
|
||||
} else {
|
||||
Object.values(this.columns).forEach((col) => (col.visible = newValue))
|
||||
}
|
||||
this.saveStorage()
|
||||
},
|
||||
// 将当前列显隐状态持久化到 localStorage
|
||||
saveStorage() {
|
||||
if (!this.storageKey) return
|
||||
try {
|
||||
let state = {}
|
||||
if (Array.isArray(this.columns)) {
|
||||
this.columns.forEach((col, index) => { state[index] = col.visible })
|
||||
} else {
|
||||
Object.keys(this.columns).forEach(key => { state[key] = this.columns[key].visible })
|
||||
}
|
||||
cache.local.setJSON(this.storageKey, state)
|
||||
} catch (e) {}
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
@@ -1,709 +0,0 @@
|
||||
<template>
|
||||
<div class="tree-sidebar" :class="{ collapsed: collapsed, resizing: isResizing, 'no-initial-transition': isLoadingFromStorage}" :style="{ width: sidebarWidth + 'px' }">
|
||||
<!-- 右侧拖动条 -->
|
||||
<div v-if="!collapsed" class="resize-handle" @mousedown="startResize" @touchstart="startResize" :class="{ active: isResizing }" />
|
||||
<div class="tree-header">
|
||||
<span class="tree-title" v-show="!collapsed">
|
||||
<i :class="titleIconClass"></i> {{ title }}
|
||||
</span>
|
||||
<div class="tree-actions" v-show="!collapsed">
|
||||
<el-tooltip :content="isExpandedAll ? '收起全部' : '展开全部'" placement="right">
|
||||
<i class="tree-action-icon" :class="isExpandedAll ? 'el-icon-arrow-down' : 'el-icon-arrow-up'" @click="toggleExpandAll" />
|
||||
</el-tooltip>
|
||||
<el-tooltip content="刷新" placement="right">
|
||||
<i class="tree-action-icon el-icon-refresh" @click="handleRefresh" />
|
||||
</el-tooltip>
|
||||
<slot name="actions"></slot>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 侧边栏展开/收起按钮 -->
|
||||
<div class="collapse-button-container">
|
||||
<el-tooltip :content="collapsed ? '展开' : '收起'" placement="right">
|
||||
<i class="collapse-button" :class="collapsed ? 'el-icon-d-arrow-right' : 'el-icon-d-arrow-left'" @click="toggleCollapsed" />
|
||||
</el-tooltip>
|
||||
</div>
|
||||
|
||||
<div class="tree-search" v-show="!collapsed" v-if="showSearch">
|
||||
<el-input v-model="searchKeyword" :placeholder="searchPlaceholder" clearable size="small" prefix-icon="el-icon-search" @input="onSearch" />
|
||||
</div>
|
||||
|
||||
<div class="tree-wrap" v-show="!collapsed">
|
||||
<el-tree
|
||||
ref="treeRef"
|
||||
:data="treeData"
|
||||
:props="treeProps"
|
||||
:expand-on-click-node="expandOnClickNode"
|
||||
:filter-node-method="filterNodeMethod"
|
||||
:default-expand-all="defaultExpandAll"
|
||||
:default-expanded-keys="defaultExpandedKeys"
|
||||
:node-key="nodeKey"
|
||||
:check-strictly="checkStrictly"
|
||||
:show-checkbox="showCheckbox"
|
||||
@node-click="onNodeClick"
|
||||
@check="onCheck"
|
||||
@node-expand="onNodeExpand"
|
||||
@node-collapse="onNodeCollapse"
|
||||
>
|
||||
<span class="tree-node" slot-scope="{ node, data }">
|
||||
<slot name="node" :node="node" :data="data">
|
||||
<i :class="data.children && data.children.length ? 'el-icon-folder' : 'el-icon-document'" class="node-icon" />
|
||||
<span class="node-label" :title="node.label">{{ node.label }}</span>
|
||||
</slot>
|
||||
</span>
|
||||
</el-tree>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: "TreeSidebar",
|
||||
props: {
|
||||
// 树形数据
|
||||
treeData: {
|
||||
type: Array,
|
||||
default: () => []
|
||||
},
|
||||
// 标题
|
||||
title: {
|
||||
type: String,
|
||||
default: '树形结构'
|
||||
},
|
||||
// 标题图标类名
|
||||
titleIconClass: {
|
||||
type: String,
|
||||
default: 'el-icon-office-building'
|
||||
},
|
||||
// 是否显示搜索框
|
||||
showSearch: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
},
|
||||
// 搜索框占位符
|
||||
searchPlaceholder: {
|
||||
type: String,
|
||||
default: '请输入名称'
|
||||
},
|
||||
// 是否默认收起侧边栏
|
||||
defaultCollapsed: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
// 树配置项
|
||||
treeProps: {
|
||||
type: Object,
|
||||
default: () => ({
|
||||
children: "children",
|
||||
label: "label"
|
||||
})
|
||||
},
|
||||
// 节点唯一标识字段
|
||||
nodeKey: {
|
||||
type: String,
|
||||
default: 'id'
|
||||
},
|
||||
// 是否在点击节点时展开或收起
|
||||
expandOnClickNode: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
// 是否显示复选框
|
||||
showCheckbox: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
// 是否严格的遵循父子不互相关联
|
||||
checkStrictly: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
// 是否默认展开所有节点
|
||||
defaultExpandAll: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
// 默认展开的节点的key数组
|
||||
defaultExpandedKeys: {
|
||||
type: Array,
|
||||
default: () => []
|
||||
},
|
||||
// 默认宽度
|
||||
defaultWidth: {
|
||||
type: Number,
|
||||
default: 220
|
||||
},
|
||||
// 收起时的宽度
|
||||
collapsedWidth: {
|
||||
type: Number,
|
||||
default: 20
|
||||
},
|
||||
// 最小宽度
|
||||
minWidth: {
|
||||
type: Number,
|
||||
default: 180
|
||||
},
|
||||
// 最大宽度
|
||||
maxWidth: {
|
||||
type: Number,
|
||||
default: 400
|
||||
},
|
||||
// 本地存储的宽度key
|
||||
storageKey: {
|
||||
type: String,
|
||||
default: 'tree-sidebar-width'
|
||||
},
|
||||
// 是否启用本地存储宽度
|
||||
enableStorage: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
},
|
||||
// 自定义过滤方法
|
||||
filterMethod: {
|
||||
type: Function,
|
||||
default: null
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
searchKeyword: "",
|
||||
collapsed: this.defaultCollapsed,
|
||||
sidebarWidth: this.defaultCollapsed ? this.collapsedWidth : this.defaultWidth,
|
||||
isResizing: false,
|
||||
startX: 0,
|
||||
startWidth: 0,
|
||||
saveWidthTimer: null,
|
||||
rafId: null,
|
||||
isLoadingFromStorage: false,
|
||||
expandedAll: this.defaultExpandAll
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
// 计算当前是否全部展开
|
||||
isExpandedAll: {
|
||||
get() {
|
||||
return this.expandedAll;
|
||||
},
|
||||
set(val) {
|
||||
this.expandedAll = val;
|
||||
}
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
collapsed(newVal, oldVal) {
|
||||
if (newVal !== oldVal) {
|
||||
this.handleCollapseChange(newVal);
|
||||
this.$emit("collapsed-change", newVal);
|
||||
}
|
||||
},
|
||||
// 监听内部展开状态变化,触发实际树的展开/收起
|
||||
expandedAll(newVal) {
|
||||
this.$nextTick(() => {
|
||||
if (newVal) {
|
||||
this.expandAllNodes();
|
||||
} else {
|
||||
this.collapseAllNodes();
|
||||
}
|
||||
});
|
||||
this.$emit("expanded-all-change", newVal);
|
||||
},
|
||||
// 监听搜索关键词
|
||||
searchKeyword(val) {
|
||||
if (this.$refs.treeRef) {
|
||||
this.$refs.treeRef.filter(val);
|
||||
this.$emit("search", val);
|
||||
}
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
this.isLoadingFromStorage = true
|
||||
if (!this.collapsed && this.enableStorage) {
|
||||
const savedWidth = this.getSavedWidth();
|
||||
if (savedWidth !== null) {
|
||||
this.sidebarWidth = savedWidth;
|
||||
}
|
||||
}
|
||||
this.$nextTick(() => {
|
||||
this.isLoadingFromStorage = false
|
||||
})
|
||||
if (this.expandedAll) {
|
||||
this.$nextTick(() => {
|
||||
this.expandAllNodes();
|
||||
});
|
||||
}
|
||||
},
|
||||
beforeDestroy() {
|
||||
this.cleanup();
|
||||
},
|
||||
methods: {
|
||||
// 节点过滤方法
|
||||
filterNodeMethod(value, data) {
|
||||
if (this.filterMethod) {
|
||||
return this.filterMethod(value, data);
|
||||
}
|
||||
if (!value) return true;
|
||||
return data.label && data.label.indexOf(value) !== -1;
|
||||
},
|
||||
// 清理定时器和动画帧
|
||||
cleanup() {
|
||||
if (this.rafId) {
|
||||
cancelAnimationFrame(this.rafId);
|
||||
this.rafId = null;
|
||||
}
|
||||
if (this.saveWidthTimer) {
|
||||
clearTimeout(this.saveWidthTimer);
|
||||
this.saveWidthTimer = null;
|
||||
}
|
||||
},
|
||||
// 处理收起/展开状态变化
|
||||
handleCollapseChange(isCollapsed) {
|
||||
if (isCollapsed) {
|
||||
this.saveWidthToStorage();
|
||||
this.sidebarWidth = this.collapsedWidth;
|
||||
} else {
|
||||
const savedWidth = this.getSavedWidth();
|
||||
this.sidebarWidth = savedWidth !== null ? savedWidth : this.defaultWidth;
|
||||
}
|
||||
},
|
||||
// 获取保存的宽度
|
||||
getSavedWidth() {
|
||||
if (!this.enableStorage) {
|
||||
return null;
|
||||
}
|
||||
try {
|
||||
const savedWidth = localStorage.getItem(this.storageKey);
|
||||
if (savedWidth) {
|
||||
const width = parseInt(savedWidth, 10);
|
||||
if (!isNaN(width) && width >= this.minWidth && width <= this.maxWidth) {
|
||||
return width;
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn(`Failed to load sidebar width from storage with key ${this.storageKey}:`, error);
|
||||
}
|
||||
return null;
|
||||
},
|
||||
// 保存宽度到本地存储
|
||||
saveWidthToStorage() {
|
||||
if (this.collapsed || !this.enableStorage) return;
|
||||
try {
|
||||
localStorage.setItem(this.storageKey, this.sidebarWidth.toString());
|
||||
} catch (error) {
|
||||
console.warn(`Failed to save sidebar width to storage with key ${this.storageKey}:`, error);
|
||||
}
|
||||
},
|
||||
// 切换侧边栏收起/展开状态
|
||||
toggleCollapsed() {
|
||||
this.collapsed = !this.collapsed;
|
||||
},
|
||||
// 切换展开/折叠所有节点
|
||||
toggleExpandAll() {
|
||||
this.isExpandedAll = !this.isExpandedAll;
|
||||
},
|
||||
// 展开所有节点
|
||||
expandAllNodes() {
|
||||
if (!this.$refs.treeRef) return;
|
||||
const allNodes = this.getAllNodes(this.$refs.treeRef.root);
|
||||
allNodes.forEach(node => {
|
||||
if (node.expanded !== undefined && !node.expanded) {
|
||||
node.expanded = true;
|
||||
}
|
||||
});
|
||||
},
|
||||
// 获取所有节点
|
||||
getAllNodes(rootNode) {
|
||||
const nodes = [];
|
||||
const traverse = (node) => {
|
||||
if (!node) return;
|
||||
nodes.push(node);
|
||||
if (node.childNodes && node.childNodes.length) {
|
||||
node.childNodes.forEach(child => traverse(child));
|
||||
}
|
||||
};
|
||||
traverse(rootNode);
|
||||
return nodes;
|
||||
},
|
||||
// 收起所有节点
|
||||
collapseAllNodes() {
|
||||
if (!this.$refs.treeRef) return;
|
||||
const allNodes = this.getAllNodes(this.$refs.treeRef.root);
|
||||
allNodes.forEach(node => {
|
||||
if (node.expanded !== undefined && node.expanded) {
|
||||
node.expanded = false;
|
||||
}
|
||||
});
|
||||
},
|
||||
// 处理刷新操作
|
||||
handleRefresh() {
|
||||
this.$emit("refresh");
|
||||
},
|
||||
// 节点点击事件
|
||||
onNodeClick(data, node, e) {
|
||||
this.$emit("node-click", data, node, e);
|
||||
},
|
||||
// 复选框选中事件
|
||||
onCheck(data, checkedInfo) {
|
||||
this.$emit("check", data, checkedInfo);
|
||||
},
|
||||
// 节点展开事件
|
||||
onNodeExpand(data, node, e) {
|
||||
this.$emit("node-expand", data, node, e);
|
||||
},
|
||||
// 节点折叠事件
|
||||
onNodeCollapse(data, node, e) {
|
||||
this.$emit("node-collapse", data, node, e);
|
||||
},
|
||||
// 搜索处理
|
||||
onSearch() {
|
||||
// 搜索逻辑已在 watch 中处理
|
||||
},
|
||||
// 设置当前选中的节点
|
||||
setCurrentKey(key) {
|
||||
if (this.$refs.treeRef) {
|
||||
this.$refs.treeRef.setCurrentKey(key);
|
||||
}
|
||||
},
|
||||
// 获取当前选中的节点
|
||||
getCurrentNode() {
|
||||
if (this.$refs.treeRef) {
|
||||
return this.$refs.treeRef.getCurrentNode();
|
||||
}
|
||||
return null;
|
||||
},
|
||||
// 获取当前选中的节点的key
|
||||
getCurrentKey() {
|
||||
if (this.$refs.treeRef) {
|
||||
return this.$refs.treeRef.getCurrentKey();
|
||||
}
|
||||
return null;
|
||||
},
|
||||
// 设置选中的节点keys(复选框)
|
||||
setCheckedKeys(keys) {
|
||||
if (this.$refs.treeRef && this.showCheckbox) {
|
||||
this.$refs.treeRef.setCheckedKeys(keys);
|
||||
}
|
||||
},
|
||||
// 获取选中的节点keys(复选框)
|
||||
getCheckedKeys() {
|
||||
if (this.$refs.treeRef && this.showCheckbox) {
|
||||
return this.$refs.treeRef.getCheckedKeys();
|
||||
}
|
||||
return [];
|
||||
},
|
||||
// 获取选中的节点(复选框)
|
||||
getCheckedNodes() {
|
||||
if (this.$refs.treeRef && this.showCheckbox) {
|
||||
return this.$refs.treeRef.getCheckedNodes();
|
||||
}
|
||||
return [];
|
||||
},
|
||||
// 清空搜索
|
||||
clearSearch() {
|
||||
this.searchKeyword = "";
|
||||
if (this.$refs.treeRef) {
|
||||
this.$refs.treeRef.filter("");
|
||||
}
|
||||
},
|
||||
// 过滤树
|
||||
filter(value) {
|
||||
this.searchKeyword = value;
|
||||
},
|
||||
// 开始调整大小
|
||||
startResize(e) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
this.isResizing = true;
|
||||
this.startX = e.type === 'mousedown' ? e.clientX : e.touches[0].clientX;
|
||||
this.startWidth = this.sidebarWidth;
|
||||
|
||||
if (e.type === 'mousedown') {
|
||||
document.addEventListener('mousemove', this.handleResizeMove);
|
||||
document.addEventListener('mouseup', this.stopResize);
|
||||
} else {
|
||||
document.addEventListener('touchmove', this.handleResizeMove, { passive: false });
|
||||
document.addEventListener('touchend', this.stopResize);
|
||||
}
|
||||
this.disableUserSelect();
|
||||
},
|
||||
// 处理调整大小移动
|
||||
handleResizeMove(e) {
|
||||
if (!this.isResizing) return;
|
||||
if (this.rafId) {
|
||||
cancelAnimationFrame(this.rafId);
|
||||
}
|
||||
this.rafId = requestAnimationFrame(() => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
const clientX = e.type === 'mousemove' ? e.clientX : e.touches[0].clientX;
|
||||
const deltaX = clientX - this.startX;
|
||||
const newWidth = this.startWidth + deltaX;
|
||||
const clampedWidth = Math.max(this.minWidth, Math.min(this.maxWidth, newWidth));
|
||||
if (Math.abs(clampedWidth - this.sidebarWidth) >= 1) {
|
||||
this.sidebarWidth = clampedWidth;
|
||||
}
|
||||
});
|
||||
},
|
||||
// 停止调整大小
|
||||
stopResize() {
|
||||
if (!this.isResizing) return;
|
||||
this.isResizing = false;
|
||||
if (this.rafId) {
|
||||
cancelAnimationFrame(this.rafId);
|
||||
this.rafId = null;
|
||||
}
|
||||
this.startX = 0;
|
||||
this.startWidth = 0;
|
||||
document.removeEventListener('mousemove', this.handleResizeMove);
|
||||
document.removeEventListener('mouseup', this.stopResize);
|
||||
document.removeEventListener('touchmove', this.handleResizeMove);
|
||||
document.removeEventListener('touchend', this.stopResize);
|
||||
this.enableUserSelect();
|
||||
this.saveWidthToStorage();
|
||||
},
|
||||
// 禁用用户选择
|
||||
disableUserSelect() {
|
||||
document.body.style.userSelect = 'none';
|
||||
document.body.style.webkitUserSelect = 'none';
|
||||
document.body.style.mozUserSelect = 'none';
|
||||
document.body.style.msUserSelect = 'none';
|
||||
},
|
||||
// 启用用户选择
|
||||
enableUserSelect() {
|
||||
document.body.style.userSelect = '';
|
||||
document.body.style.webkitUserSelect = '';
|
||||
document.body.style.mozUserSelect = '';
|
||||
document.body.style.msUserSelect = '';
|
||||
},
|
||||
// 重置宽度到默认值
|
||||
resetWidth() {
|
||||
this.sidebarWidth = this.defaultWidth;
|
||||
this.saveWidthToStorage();
|
||||
},
|
||||
// 获取当前宽度
|
||||
getCurrentWidth() {
|
||||
return this.sidebarWidth;
|
||||
},
|
||||
// 设置宽度
|
||||
setWidth(width) {
|
||||
if (typeof width === 'number' && width >= this.minWidth && width <= this.maxWidth) {
|
||||
this.sidebarWidth = width;
|
||||
if (!this.collapsed) {
|
||||
this.saveWidthToStorage();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.tree-sidebar {
|
||||
flex-shrink: 0;
|
||||
width: 220px;
|
||||
background: #fff;
|
||||
border-right: 1px solid #e8eaed;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
transition: width 0.25s ease;
|
||||
|
||||
&.collapsed {
|
||||
width: 42px;
|
||||
}
|
||||
|
||||
&.resizing {
|
||||
transition: none;
|
||||
will-change: width;
|
||||
|
||||
* {
|
||||
pointer-events: none !important;
|
||||
}
|
||||
}
|
||||
|
||||
&.no-initial-transition {
|
||||
transition: none;
|
||||
}
|
||||
}
|
||||
|
||||
.resize-handle {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 0;
|
||||
width: 6px;
|
||||
height: 100%;
|
||||
cursor: col-resize;
|
||||
z-index: 20;
|
||||
background: transparent;
|
||||
transition: background 0.2s;
|
||||
|
||||
&:hover {
|
||||
background: rgba(64, 158, 255, 0.3);
|
||||
}
|
||||
|
||||
&.active {
|
||||
background: rgba(64, 158, 255, 0.5);
|
||||
}
|
||||
}
|
||||
|
||||
.collapse-button-container {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
right: 0;
|
||||
transform: translateY(-50%);
|
||||
z-index: 100;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 15px;
|
||||
height: 20px;
|
||||
background: #fff;
|
||||
border-radius: 0 4px 4px 0;
|
||||
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
|
||||
transition: all 0.2s ease;
|
||||
|
||||
.tree-sidebar.collapsed & {
|
||||
right: 0;
|
||||
background: #f7f8fa;
|
||||
border-radius: 0 4px 4px 0;
|
||||
}
|
||||
|
||||
.tree-sidebar.resizing & {
|
||||
pointer-events: none;
|
||||
}
|
||||
}
|
||||
|
||||
.collapse-button {
|
||||
font-size: 14px;
|
||||
color: #909399;
|
||||
cursor: pointer;
|
||||
padding: 4px;
|
||||
border-radius: 4px;
|
||||
transition: all 0.2s;
|
||||
|
||||
&:hover {
|
||||
color: #409eff;
|
||||
background: #ecf5ff;
|
||||
}
|
||||
}
|
||||
|
||||
.tree-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 0 10px;
|
||||
height: 40px;
|
||||
border-bottom: 1px solid #e8eaed;
|
||||
background: #f7f8fa;
|
||||
flex-shrink: 0;
|
||||
|
||||
.tree-title {
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
color: #303133;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 5px;
|
||||
|
||||
i {
|
||||
color: #409eff;
|
||||
font-size: 14px;
|
||||
}
|
||||
}
|
||||
|
||||
.tree-actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.tree-action-icon {
|
||||
font-size: 14px;
|
||||
color: #909399;
|
||||
cursor: pointer;
|
||||
padding: 4px;
|
||||
border-radius: 4px;
|
||||
transition: all 0.2s;
|
||||
|
||||
&:hover {
|
||||
color: #409eff;
|
||||
background: #ecf5ff;
|
||||
}
|
||||
}
|
||||
|
||||
.tree-search {
|
||||
padding: 10px 10px 4px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.tree-wrap {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
padding: 6px 6px 12px;
|
||||
|
||||
.tree-sidebar.resizing & {
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
&::-webkit-scrollbar {
|
||||
width: 4px;
|
||||
}
|
||||
|
||||
&::-webkit-scrollbar-thumb {
|
||||
background: #dcdfe6;
|
||||
border-radius: 4px;
|
||||
|
||||
&:hover {
|
||||
background: #c0c4cc;
|
||||
}
|
||||
}
|
||||
|
||||
::v-deep .el-tree-node__content {
|
||||
height: 32px;
|
||||
border-radius: 4px;
|
||||
margin-bottom: 1px;
|
||||
|
||||
&:hover {
|
||||
background: #f0f7ff;
|
||||
}
|
||||
}
|
||||
|
||||
::v-deep .el-tree-node.is-current > .el-tree-node__content {
|
||||
background: #e6f0fd;
|
||||
color: #409eff;
|
||||
font-weight: 600;
|
||||
|
||||
.node-icon {
|
||||
color: #409eff !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.tree-node {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 5px;
|
||||
font-size: 13px;
|
||||
overflow: hidden;
|
||||
|
||||
.node-icon {
|
||||
font-size: 14px;
|
||||
color: #f5a623;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.node-label {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
}
|
||||
|
||||
::v-deep .el-icon-document.node-icon {
|
||||
color: #909399 !important;
|
||||
}
|
||||
</style>
|
||||
@@ -51,14 +51,6 @@ export default {
|
||||
width: 100%;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
|
||||
&:fullscreen,
|
||||
&:-webkit-full-screen,
|
||||
&:-moz-full-screen,
|
||||
&:-ms-fullscreen {
|
||||
background: #fff;
|
||||
overflow-y: auto;
|
||||
}
|
||||
}
|
||||
|
||||
.fixed-header + .app-main {
|
||||
|
||||
@@ -1,362 +0,0 @@
|
||||
<template>
|
||||
<el-drawer title="公告详情" :visible.sync="visible" direction="rtl" size="50%" append-to-body :before-close="handleClose" custom-class="notice-detail-drawer">
|
||||
<div v-loading="loading" class="notice-detail-drawer__body">
|
||||
<div v-if="!detail" class="notice-empty">
|
||||
<i class="el-icon-document"></i>
|
||||
<span>暂无数据</span>
|
||||
</div>
|
||||
<div v-else class="notice-page">
|
||||
<div class="notice-type-wrap">
|
||||
<span v-if="detail.noticeType === '1'" class="notice-type-tag type-notify">
|
||||
<i class="el-icon-bell"></i> 通知
|
||||
</span>
|
||||
<span v-else-if="detail.noticeType === '2'" class="notice-type-tag type-announce">
|
||||
<i class="el-icon-message"></i> 公告
|
||||
</span>
|
||||
<span v-else class="notice-type-tag type-notify">
|
||||
<i class="el-icon-document"></i> 消息
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<h1 class="notice-title">{{ detail.noticeTitle }}</h1>
|
||||
|
||||
<div class="notice-meta">
|
||||
<span class="meta-item">
|
||||
<i class="el-icon-user"></i>
|
||||
<span>{{ detail.createBy || '—' }}</span>
|
||||
</span>
|
||||
<span class="meta-item">
|
||||
<i class="el-icon-time"></i>
|
||||
<span>{{ detail.createTime || '—' }}</span>
|
||||
</span>
|
||||
<span class="meta-item">
|
||||
<span :class="['status-dot', isStatusNormal ? 'status-ok' : 'status-off']"></span>
|
||||
<span>{{ isStatusNormal ? '正常' : '已关闭' }}</span>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div class="notice-divider">
|
||||
<span class="notice-divider-dot"></span>
|
||||
<span class="notice-divider-dot"></span>
|
||||
<span class="notice-divider-dot"></span>
|
||||
</div>
|
||||
|
||||
<div class="notice-body">
|
||||
<div v-if="hasContent" class="notice-content" v-html="detail.noticeContent" />
|
||||
<div v-else class="notice-empty notice-empty--inner">
|
||||
<i class="el-icon-document"></i> 暂无内容
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</el-drawer>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { getNotice } from '@/api/system/notice'
|
||||
|
||||
export default {
|
||||
name: 'NoticeDetailView',
|
||||
data() {
|
||||
return {
|
||||
visible: false,
|
||||
loading: false,
|
||||
detail: null
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
isStatusNormal() {
|
||||
const s = this.detail && this.detail.status
|
||||
return s === '0' || s === 0
|
||||
},
|
||||
hasContent() {
|
||||
const c = this.detail && this.detail.noticeContent
|
||||
return c != null && String(c).trim() !== ''
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
open(payload) {
|
||||
let id = null
|
||||
let preset = null
|
||||
if (payload != null && typeof payload === 'object') {
|
||||
id = payload.noticeId
|
||||
if (payload.noticeContent != null) {
|
||||
preset = payload
|
||||
}
|
||||
} else {
|
||||
id = payload
|
||||
}
|
||||
this.visible = true
|
||||
if (preset) {
|
||||
this.detail = preset
|
||||
return
|
||||
}
|
||||
if (id == null || id === '') {
|
||||
this.detail = null
|
||||
return
|
||||
}
|
||||
this.loading = true
|
||||
this.detail = null
|
||||
getNotice(id).then(res => {
|
||||
this.detail = res.data
|
||||
}).catch(() => {
|
||||
this.detail = null
|
||||
}).finally(() => {
|
||||
this.loading = false
|
||||
})
|
||||
},
|
||||
handleClose() {
|
||||
this.visible = false
|
||||
this.detail = null
|
||||
this.loading = false
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.notice-page {
|
||||
max-width: 760px;
|
||||
margin: 0 auto;
|
||||
padding: 8px 8px 20px;
|
||||
animation: notice-fade-up 0.28s ease both;
|
||||
}
|
||||
|
||||
@keyframes notice-fade-up {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(14px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
.notice-type-tag {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 5px;
|
||||
padding: 3px 12px;
|
||||
border-radius: 2px;
|
||||
font-size: 11px;
|
||||
font-weight: 700;
|
||||
letter-spacing: 1px;
|
||||
text-transform: uppercase;
|
||||
margin-bottom: 14px;
|
||||
}
|
||||
|
||||
.type-notify {
|
||||
background: #fff8e6;
|
||||
color: #b7791f;
|
||||
border-left: 3px solid #d97706;
|
||||
}
|
||||
|
||||
.type-announce {
|
||||
background: #e8f5e9;
|
||||
color: #276749;
|
||||
border-left: 3px solid #38a169;
|
||||
}
|
||||
|
||||
.notice-title {
|
||||
font-size: 22px;
|
||||
font-weight: 700;
|
||||
color: #1a202c;
|
||||
line-height: 1.45;
|
||||
margin: 0 0 16px;
|
||||
letter-spacing: -0.2px;
|
||||
}
|
||||
|
||||
.notice-meta {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex-wrap: wrap;
|
||||
gap: 16px;
|
||||
padding: 12px 0;
|
||||
border-top: 1px solid #e9ecef;
|
||||
border-bottom: 1px solid #e9ecef;
|
||||
margin-bottom: 28px;
|
||||
}
|
||||
|
||||
.meta-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 5px;
|
||||
font-size: 12px;
|
||||
color: #718096;
|
||||
}
|
||||
|
||||
.meta-item i {
|
||||
font-size: 12px;
|
||||
color: #a0aec0;
|
||||
}
|
||||
|
||||
.status-dot {
|
||||
display: inline-block;
|
||||
width: 7px;
|
||||
height: 7px;
|
||||
border-radius: 50%;
|
||||
margin-right: 4px;
|
||||
}
|
||||
|
||||
.status-ok {
|
||||
background: #38a169;
|
||||
}
|
||||
|
||||
.status-off {
|
||||
background: #e53e3e;
|
||||
}
|
||||
|
||||
.notice-divider {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
.notice-divider::before,
|
||||
.notice-divider::after {
|
||||
content: '';
|
||||
flex: 1;
|
||||
height: 1px;
|
||||
background: linear-gradient(to right, transparent, #dee2e6, transparent);
|
||||
}
|
||||
|
||||
.notice-divider-dot {
|
||||
width: 6px;
|
||||
height: 6px;
|
||||
border-radius: 50%;
|
||||
background: #cbd5e0;
|
||||
}
|
||||
|
||||
.notice-body {
|
||||
background: #fff;
|
||||
border-radius: 6px;
|
||||
padding: 28px 32px;
|
||||
box-shadow: 0 1px 4px rgba(0, 0, 0, 0.06), 0 0 0 1px rgba(0, 0, 0, 0.04);
|
||||
min-height: 120px;
|
||||
}
|
||||
|
||||
.notice-content {
|
||||
font-size: 14px;
|
||||
line-height: 1.85;
|
||||
color: #2d3748;
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
.notice-content ::v-deep p {
|
||||
margin: 0 0 1em;
|
||||
}
|
||||
|
||||
.notice-content ::v-deep h1,
|
||||
.notice-content ::v-deep h2,
|
||||
.notice-content ::v-deep h3 {
|
||||
font-weight: 700;
|
||||
color: #1a202c;
|
||||
margin: 1.4em 0 0.6em;
|
||||
}
|
||||
|
||||
.notice-content ::v-deep h1 {
|
||||
font-size: 18px;
|
||||
}
|
||||
|
||||
.notice-content ::v-deep h2 {
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.notice-content ::v-deep h3 {
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.notice-content ::v-deep a {
|
||||
color: #3182ce;
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.notice-content ::v-deep a:hover {
|
||||
color: #2b6cb0;
|
||||
}
|
||||
|
||||
.notice-content ::v-deep img {
|
||||
max-width: 100%;
|
||||
border-radius: 4px;
|
||||
margin: 8px 0;
|
||||
}
|
||||
|
||||
.notice-content ::v-deep ul,
|
||||
.notice-content ::v-deep ol {
|
||||
padding-left: 20px;
|
||||
margin: 0 0 1em;
|
||||
}
|
||||
|
||||
.notice-content ::v-deep li {
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.notice-content ::v-deep blockquote {
|
||||
border-left: 3px solid #cbd5e0;
|
||||
margin: 1em 0;
|
||||
padding: 6px 16px;
|
||||
color: #718096;
|
||||
background: #f7fafc;
|
||||
}
|
||||
|
||||
.notice-content ::v-deep table {
|
||||
border-collapse: collapse;
|
||||
width: 100%;
|
||||
margin: 1em 0;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.notice-content ::v-deep table th,
|
||||
.notice-content ::v-deep table td {
|
||||
border: 1px solid #e2e8f0;
|
||||
padding: 7px 12px;
|
||||
}
|
||||
|
||||
.notice-content ::v-deep table th {
|
||||
background: #f7fafc;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.notice-empty {
|
||||
text-align: center;
|
||||
padding: 40px 0;
|
||||
color: #a0aec0;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.notice-empty i {
|
||||
font-size: 28px;
|
||||
display: block;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.notice-empty--inner {
|
||||
padding: 32px 0;
|
||||
}
|
||||
|
||||
.notice-empty--inner i {
|
||||
font-size: 28px;
|
||||
}
|
||||
|
||||
::v-deep .notice-detail-drawer {
|
||||
.el-drawer__header {
|
||||
margin-bottom: 0;
|
||||
padding: 16px 20px;
|
||||
border-bottom: 1px solid #ebeef5;
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
color: #303133;
|
||||
}
|
||||
.el-drawer__body {
|
||||
background: #f5f6f8;
|
||||
}
|
||||
}
|
||||
|
||||
.notice-detail-drawer__body {
|
||||
height: 100%;
|
||||
overflow: auto;
|
||||
padding: 10px 16px 22px;
|
||||
}
|
||||
</style>
|
||||
@@ -1,181 +0,0 @@
|
||||
<template>
|
||||
<div>
|
||||
<el-popover ref="noticePopover" placement="bottom-end" width="320" trigger="manual" :value="noticeVisible" popper-class="notice-popover">
|
||||
<div class="notice-header">
|
||||
<span class="notice-title">通知公告</span>
|
||||
<span class="notice-mark-all" @click="markAllRead">全部已读</span>
|
||||
</div>
|
||||
<div v-if="noticeLoading" class="notice-loading"><i class="el-icon-loading"></i> 加载中...</div>
|
||||
<div v-else-if="noticeList.length === 0" class="notice-empty"><i class="el-icon-inbox"></i><br>暂无公告</div>
|
||||
<div v-else>
|
||||
<div v-for="item in noticeList" :key="item.noticeId" class="notice-item" :class="{ 'is-read': item.isRead }" @click="previewNotice(item)">
|
||||
<el-tag size="mini" :type="item.noticeType === '1' ? 'warning' : 'success'" class="notice-tag">
|
||||
{{ item.noticeType === '1' ? '通知' : '公告' }}
|
||||
</el-tag>
|
||||
<span class="notice-item-title">{{ item.noticeTitle }}</span>
|
||||
<span class="notice-item-date">{{ item.createTime }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</el-popover>
|
||||
|
||||
<div v-popover:noticePopover class="right-menu-item hover-effect notice-trigger" @mouseenter="onNoticeEnter" @mouseleave="onNoticeLeave">
|
||||
<svg-icon icon-class="bell" />
|
||||
<span v-if="unreadCount > 0" class="notice-badge">{{ unreadCount }}</span>
|
||||
</div>
|
||||
|
||||
<notice-detail-view ref="noticeViewRef" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import NoticeDetailView from './DetailView'
|
||||
import { listNoticeTop, markNoticeRead, markNoticeReadAll } from '@/api/system/notice'
|
||||
|
||||
export default {
|
||||
name: 'HeaderNotice',
|
||||
components: { NoticeDetailView },
|
||||
data() {
|
||||
return {
|
||||
noticeList: [], // 通知列表
|
||||
unreadCount: 0, // 未读数量
|
||||
noticeLoading: false, // 加载状态
|
||||
noticeVisible: false, // 弹出层显示状态
|
||||
noticeLeaveTimer: null // 鼠标离开计时器
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
this.loadNoticeTop()
|
||||
},
|
||||
methods: {
|
||||
// 鼠标移入铃铛区域
|
||||
onNoticeEnter() {
|
||||
clearTimeout(this.noticeLeaveTimer)
|
||||
this.noticeVisible = true
|
||||
this.$nextTick(() => {
|
||||
const popper = this.$refs.noticePopover.$refs.popper
|
||||
if (popper && !popper._noticeBound) {
|
||||
popper._noticeBound = true
|
||||
popper.addEventListener('mouseenter', () => clearTimeout(this.noticeLeaveTimer))
|
||||
popper.addEventListener('mouseleave', () => {
|
||||
this.noticeLeaveTimer = setTimeout(() => { this.noticeVisible = false }, 100)
|
||||
})
|
||||
}
|
||||
})
|
||||
},
|
||||
// 鼠标离开铃铛区域
|
||||
onNoticeLeave() {
|
||||
this.noticeLeaveTimer = setTimeout(() => { this.noticeVisible = false }, 150)
|
||||
},
|
||||
// 加载顶部公告列表
|
||||
loadNoticeTop() {
|
||||
this.noticeLoading = true
|
||||
listNoticeTop().then(res => {
|
||||
this.noticeList = res.data || []
|
||||
this.unreadCount = res.unreadCount !== undefined ? res.unreadCount : this.noticeList.filter(n => !n.isRead).length
|
||||
}).finally(() => {
|
||||
this.noticeLoading = false
|
||||
})
|
||||
},
|
||||
// 预览公告详情
|
||||
previewNotice(item) {
|
||||
if (!item.isRead) {
|
||||
markNoticeRead(item.noticeId).catch(() => {})
|
||||
item.isRead = true
|
||||
const idx = this.noticeList.indexOf(item)
|
||||
if (idx !== -1) this.$set(this.noticeList, idx, { ...item, isRead: true })
|
||||
this.unreadCount = Math.max(0, this.unreadCount - 1)
|
||||
}
|
||||
this.$refs.noticeViewRef.open(item.noticeId)
|
||||
},
|
||||
// 全部已读
|
||||
markAllRead() {
|
||||
const ids = this.noticeList.map(n => n.noticeId).join(',')
|
||||
if (!ids) return
|
||||
markNoticeReadAll(ids).catch(() => {})
|
||||
this.noticeList = this.noticeList.map(n => ({ ...n, isRead: true }))
|
||||
this.unreadCount = 0
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.notice-trigger {
|
||||
position: relative;
|
||||
transform: translateX(-6px);
|
||||
.svg-icon { width: 1.2em; height: 1.2em; vertical-align: -0.2em; }
|
||||
.notice-badge {
|
||||
position: absolute;
|
||||
top: 7px;
|
||||
right: -3px;
|
||||
background: #f56c6c;
|
||||
color: #fff;
|
||||
border-radius: 10px;
|
||||
font-size: 10px;
|
||||
height: 16px;
|
||||
line-height: 16px;
|
||||
padding: 0 4px;
|
||||
min-width: 16px;
|
||||
text-align: center;
|
||||
white-space: nowrap;
|
||||
pointer-events: none;
|
||||
}
|
||||
}
|
||||
.notice-popover {
|
||||
padding: 0 !important;
|
||||
}
|
||||
.notice-popover .notice-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 10px 14px;
|
||||
background: #f7f9fb;
|
||||
border-bottom: 1px solid #eee;
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
color: #333;
|
||||
}
|
||||
.notice-popover .notice-mark-all {
|
||||
font-size: 12px;
|
||||
color: #409EFF;
|
||||
font-weight: normal;
|
||||
cursor: pointer;
|
||||
}
|
||||
.notice-popover .notice-mark-all:hover { color: #2b7cc1; }
|
||||
.notice-popover .notice-loading,
|
||||
.notice-popover .notice-empty {
|
||||
padding: 24px;
|
||||
text-align: center;
|
||||
color: #bbb;
|
||||
font-size: 12px;
|
||||
line-height: 1.8;
|
||||
}
|
||||
.notice-popover .notice-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 10px 14px;
|
||||
border-bottom: 1px solid #f5f5f5;
|
||||
cursor: pointer;
|
||||
transition: background 0.15s;
|
||||
}
|
||||
.notice-popover .notice-item:last-child { border-bottom: none; }
|
||||
.notice-popover .notice-item:hover { background: #f7f9fb; }
|
||||
.notice-popover .notice-item.is-read .notice-tag,
|
||||
.notice-popover .notice-item.is-read .notice-item-title,
|
||||
.notice-popover .notice-item.is-read .notice-item-date { opacity: 0.45; filter: grayscale(1); color: #999; }
|
||||
.notice-popover .notice-tag { flex-shrink: 0; }
|
||||
.notice-popover .notice-item-title {
|
||||
flex: 1;
|
||||
font-size: 12px;
|
||||
color: #333;
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
.notice-popover .notice-item-date {
|
||||
flex-shrink: 0;
|
||||
font-size: 11px;
|
||||
color: #bbb;
|
||||
}
|
||||
</style>
|
||||
@@ -12,13 +12,15 @@
|
||||
<template v-if="device!=='mobile'">
|
||||
<search id="header-search" class="right-menu-item" />
|
||||
|
||||
<el-tooltip content="源码地址" effect="dark" placement="bottom">
|
||||
<!-- 源码地址按钮已隐藏 -->
|
||||
<!-- <el-tooltip content="源码地址" effect="dark" placement="bottom">
|
||||
<ruo-yi-git id="ruoyi-git" class="right-menu-item hover-effect" />
|
||||
</el-tooltip>
|
||||
</el-tooltip> -->
|
||||
|
||||
<el-tooltip content="文档地址" effect="dark" placement="bottom">
|
||||
<!-- 文档地址按钮已隐藏 -->
|
||||
<!-- <el-tooltip content="文档地址" effect="dark" placement="bottom">
|
||||
<ruo-yi-doc id="ruoyi-doc" class="right-menu-item hover-effect" />
|
||||
</el-tooltip>
|
||||
</el-tooltip> -->
|
||||
|
||||
<screenfull id="screenfull" class="right-menu-item hover-effect" />
|
||||
|
||||
@@ -26,10 +28,6 @@
|
||||
<size-select id="size-select" class="right-menu-item hover-effect" />
|
||||
</el-tooltip>
|
||||
|
||||
<el-tooltip content="消息通知" effect="dark" placement="bottom">
|
||||
<header-notice id="header-notice" class="right-menu-item hover-effect" />
|
||||
</el-tooltip>
|
||||
|
||||
</template>
|
||||
|
||||
<el-dropdown class="avatar-container right-menu-item hover-effect" trigger="hover">
|
||||
@@ -44,9 +42,6 @@
|
||||
<el-dropdown-item @click.native="setLayout" v-if="setting">
|
||||
<span>布局设置</span>
|
||||
</el-dropdown-item>
|
||||
<el-dropdown-item @click.native="lockScreen">
|
||||
<span>锁定屏幕</span>
|
||||
</el-dropdown-item>
|
||||
<el-dropdown-item divided @click.native="logout">
|
||||
<span>退出登录</span>
|
||||
</el-dropdown-item>
|
||||
@@ -59,7 +54,7 @@
|
||||
<script>
|
||||
import { mapGetters } from 'vuex'
|
||||
import Breadcrumb from '@/components/Breadcrumb'
|
||||
import TopNav from './TopNav'
|
||||
import TopNav from '@/components/TopNav'
|
||||
import TopBar from './TopBar'
|
||||
import Logo from './Sidebar/Logo'
|
||||
import Hamburger from '@/components/Hamburger'
|
||||
@@ -68,9 +63,9 @@ import SizeSelect from '@/components/SizeSelect'
|
||||
import Search from '@/components/HeaderSearch'
|
||||
import RuoYiGit from '@/components/RuoYi/Git'
|
||||
import RuoYiDoc from '@/components/RuoYi/Doc'
|
||||
import HeaderNotice from './HeaderNotice'
|
||||
|
||||
export default {
|
||||
emits: ['setLayout'],
|
||||
components: {
|
||||
Breadcrumb,
|
||||
Logo,
|
||||
@@ -81,8 +76,7 @@ export default {
|
||||
SizeSelect,
|
||||
Search,
|
||||
RuoYiGit,
|
||||
RuoYiDoc,
|
||||
HeaderNotice
|
||||
RuoYiDoc
|
||||
},
|
||||
computed: {
|
||||
...mapGetters([
|
||||
@@ -114,12 +108,6 @@ export default {
|
||||
setLayout(event) {
|
||||
this.$emit('setLayout')
|
||||
},
|
||||
lockScreen() {
|
||||
const currentPath = this.$route.fullPath
|
||||
this.$store.dispatch('lock/lockScreen', currentPath).then(() => {
|
||||
this.$router.push('/lock')
|
||||
})
|
||||
},
|
||||
logout() {
|
||||
this.$confirm('确定注销并退出系统吗?', '提示', {
|
||||
confirmButtonText: '确定',
|
||||
@@ -187,6 +175,11 @@ export default {
|
||||
margin-left: 8px;
|
||||
}
|
||||
|
||||
.errLog-container {
|
||||
display: inline-block;
|
||||
vertical-align: top;
|
||||
}
|
||||
|
||||
.right-menu {
|
||||
height: 100%;
|
||||
line-height: 50px;
|
||||
|
||||
@@ -61,28 +61,15 @@
|
||||
<h3 class="drawer-title">系统布局配置</h3>
|
||||
|
||||
<div class="drawer-item">
|
||||
<span>开启页签</span>
|
||||
<span>开启 Tags-Views</span>
|
||||
<el-switch v-model="tagsView" class="drawer-switch" />
|
||||
</div>
|
||||
|
||||
<div class="drawer-item">
|
||||
<span>持久化标签页</span>
|
||||
<el-switch v-model="tagsViewPersist" :disabled="!tagsView" class="drawer-switch" />
|
||||
</div>
|
||||
|
||||
<div class="drawer-item">
|
||||
<span>显示页签图标</span>
|
||||
<el-switch v-model="tagsIcon" :disabled="!tagsView" class="drawer-switch" />
|
||||
</div>
|
||||
|
||||
<div class="drawer-item">
|
||||
<span>标签页样式</span>
|
||||
<el-radio-group v-model="tagsViewStyle" :disabled="!tagsView" size="mini" class="drawer-switch">
|
||||
<el-radio-button label="card">卡片</el-radio-button>
|
||||
<el-radio-button label="chrome">谷歌</el-radio-button>
|
||||
</el-radio-group>
|
||||
</div>
|
||||
|
||||
<div class="drawer-item">
|
||||
<span>固定 Header</span>
|
||||
<el-switch v-model="fixedHeader" class="drawer-switch" />
|
||||
@@ -138,17 +125,6 @@ export default {
|
||||
})
|
||||
}
|
||||
},
|
||||
tagsViewPersist: {
|
||||
get() {
|
||||
return this.$store.state.settings.tagsViewPersist
|
||||
},
|
||||
set(val) {
|
||||
this.$store.dispatch('settings/changeSetting', {
|
||||
key: 'tagsViewPersist',
|
||||
value: val
|
||||
})
|
||||
}
|
||||
},
|
||||
tagsView: {
|
||||
get() {
|
||||
return this.$store.state.settings.tagsView
|
||||
@@ -171,17 +147,6 @@ export default {
|
||||
})
|
||||
}
|
||||
},
|
||||
tagsViewStyle: {
|
||||
get() {
|
||||
return this.$store.state.settings.tagsViewStyle
|
||||
},
|
||||
set(val) {
|
||||
this.$store.dispatch('settings/changeSetting', {
|
||||
key: 'tagsViewStyle',
|
||||
value: val
|
||||
})
|
||||
}
|
||||
},
|
||||
sidebarLogo: {
|
||||
get() {
|
||||
return this.$store.state.settings.sidebarLogo
|
||||
@@ -266,17 +231,12 @@ export default {
|
||||
},
|
||||
saveSetting() {
|
||||
this.$modal.loading("正在保存到本地,请稍候...")
|
||||
if (!this.tagsViewPersist) {
|
||||
this.$cache.local.remove('tags-view-visited')
|
||||
}
|
||||
this.$cache.local.set(
|
||||
"layout-setting",
|
||||
`{
|
||||
"navType":${this.navType},
|
||||
"tagsView":${this.tagsView},
|
||||
"tagsIcon":${this.tagsIcon},
|
||||
"tagsViewStyle":"${this.tagsViewStyle}",
|
||||
"tagsViewPersist":${this.tagsViewPersist},
|
||||
"fixedHeader":${this.fixedHeader},
|
||||
"sidebarLogo":${this.sidebarLogo},
|
||||
"dynamicTitle":${this.dynamicTitle},
|
||||
@@ -289,7 +249,6 @@ export default {
|
||||
},
|
||||
resetSetting() {
|
||||
this.$modal.loading("正在清除设置缓存并刷新,请稍候...")
|
||||
this.$cache.local.remove('tags-view-visited')
|
||||
this.$cache.local.remove("layout-setting")
|
||||
setTimeout("window.location.reload()", 1000)
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<div :class="['sidebar-theme-wrapper', {'has-logo':showLogo}, settings.sideTheme]" :style="{ backgroundColor: settings.sideTheme === 'theme-dark' ? variables.menuBackground : variables.menuLightBackground }">
|
||||
<div :class="{'has-logo':showLogo}" :style="{ backgroundColor: settings.sideTheme === 'theme-dark' ? variables.menuBackground : variables.menuLightBackground }">
|
||||
<logo v-if="showLogo" :collapse="isCollapse" />
|
||||
<el-scrollbar :class="settings.sideTheme" wrap-class="scrollbar-wrapper">
|
||||
<el-menu
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
</template>
|
||||
|
||||
<script>
|
||||
const tagAndTagSpacing = 4
|
||||
const tagAndTagSpacing = 4 // tagAndTagSpacing
|
||||
|
||||
export default {
|
||||
name: 'ScrollPane',
|
||||
@@ -26,44 +26,13 @@ export default {
|
||||
this.scrollWrapper.removeEventListener('scroll', this.emitScroll)
|
||||
},
|
||||
methods: {
|
||||
smoothScrollTo(target) {
|
||||
const $scrollWrapper = this.scrollWrapper
|
||||
const start = $scrollWrapper.scrollLeft
|
||||
const distance = target - start
|
||||
const duration = 300
|
||||
let startTime = null
|
||||
|
||||
function ease(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
|
||||
}
|
||||
|
||||
const emit = this.$emit.bind(this)
|
||||
function step(timestamp) {
|
||||
if (!startTime) startTime = timestamp
|
||||
const elapsed = timestamp - startTime
|
||||
$scrollWrapper.scrollLeft = ease(elapsed, start, distance, duration)
|
||||
if (elapsed < duration) {
|
||||
requestAnimationFrame(step)
|
||||
} else {
|
||||
$scrollWrapper.scrollLeft = target
|
||||
emit('updateArrows')
|
||||
}
|
||||
}
|
||||
|
||||
requestAnimationFrame(step)
|
||||
},
|
||||
handleScroll(e) {
|
||||
const eventDelta = e.wheelDelta || -e.deltaY * 40
|
||||
const $scrollWrapper = this.scrollWrapper
|
||||
$scrollWrapper.scrollLeft = $scrollWrapper.scrollLeft + eventDelta / 4
|
||||
this.$emit('updateArrows')
|
||||
},
|
||||
emitScroll() {
|
||||
this.$emit('scroll')
|
||||
this.$emit('updateArrows')
|
||||
},
|
||||
moveToTarget(currentTag) {
|
||||
const $container = this.$refs.scrollContainer.$el
|
||||
@@ -74,56 +43,34 @@ export default {
|
||||
let firstTag = null
|
||||
let lastTag = null
|
||||
|
||||
// find first tag and last tag
|
||||
if (tagList.length > 0) {
|
||||
firstTag = tagList[0]
|
||||
lastTag = tagList[tagList.length - 1]
|
||||
}
|
||||
|
||||
if (firstTag === currentTag) {
|
||||
this.smoothScrollTo(0)
|
||||
$scrollWrapper.scrollLeft = 0
|
||||
} else if (lastTag === currentTag) {
|
||||
this.smoothScrollTo($scrollWrapper.scrollWidth - $containerWidth)
|
||||
$scrollWrapper.scrollLeft = $scrollWrapper.scrollWidth - $containerWidth
|
||||
} else {
|
||||
// find preTag and nextTag
|
||||
const currentIndex = tagList.findIndex(item => item === currentTag)
|
||||
const prevTag = tagList[currentIndex - 1]
|
||||
const nextTag = tagList[currentIndex + 1]
|
||||
|
||||
// the tag's offsetLeft after of nextTag
|
||||
const afterNextTagOffsetLeft = nextTag.$el.offsetLeft + nextTag.$el.offsetWidth + tagAndTagSpacing
|
||||
|
||||
// the tag's offsetLeft before of prevTag
|
||||
const beforePrevTagOffsetLeft = prevTag.$el.offsetLeft - tagAndTagSpacing
|
||||
|
||||
if (afterNextTagOffsetLeft > $scrollWrapper.scrollLeft + $containerWidth) {
|
||||
this.smoothScrollTo(afterNextTagOffsetLeft - $containerWidth)
|
||||
$scrollWrapper.scrollLeft = afterNextTagOffsetLeft - $containerWidth
|
||||
} else if (beforePrevTagOffsetLeft < $scrollWrapper.scrollLeft) {
|
||||
this.smoothScrollTo(beforePrevTagOffsetLeft)
|
||||
$scrollWrapper.scrollLeft = beforePrevTagOffsetLeft
|
||||
}
|
||||
}
|
||||
},
|
||||
// 向左滚动固定距离
|
||||
scrollLeft() {
|
||||
const $scrollWrapper = this.scrollWrapper
|
||||
this.smoothScrollTo(Math.max(0, $scrollWrapper.scrollLeft - 200))
|
||||
},
|
||||
// 向右滚动固定距离
|
||||
scrollRight() {
|
||||
const $scrollWrapper = this.scrollWrapper
|
||||
const maxScroll = $scrollWrapper.scrollWidth - $scrollWrapper.clientWidth
|
||||
this.smoothScrollTo(Math.min(maxScroll, $scrollWrapper.scrollLeft + 200))
|
||||
},
|
||||
// 直接平滑滚动到最左端
|
||||
scrollToStart() {
|
||||
this.smoothScrollTo(0)
|
||||
},
|
||||
// 直接平滑滚动到最右端
|
||||
scrollToEnd() {
|
||||
const $scrollWrapper = this.scrollWrapper
|
||||
this.smoothScrollTo($scrollWrapper.scrollWidth - $scrollWrapper.clientWidth)
|
||||
},
|
||||
// 获取是否可以继续向左/右滚动
|
||||
getScrollState() {
|
||||
const $scrollWrapper = this.scrollWrapper
|
||||
return {
|
||||
canLeft: $scrollWrapper.scrollLeft > 0,
|
||||
canRight: $scrollWrapper.scrollLeft < $scrollWrapper.scrollWidth - $scrollWrapper.clientWidth - 1
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -140,9 +87,7 @@ export default {
|
||||
bottom: 0px;
|
||||
}
|
||||
.el-scrollbar__wrap {
|
||||
height: 34px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
height: 49px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,12 +1,6 @@
|
||||
<template>
|
||||
<div id="tags-view-container" class="tags-view-container" :class="{ 'tags-view-container--chrome': tagsViewStyle === 'chrome' }" :style="chromeVars">
|
||||
<!-- 左切换箭头 -->
|
||||
<span class="tags-nav-btn tags-nav-btn--left" :class="{ disabled: !canScrollLeft }" @click="scrollLeft">
|
||||
<i class="el-icon-arrow-left" />
|
||||
</span>
|
||||
|
||||
<!-- 标签滚动区 -->
|
||||
<scroll-pane ref="scrollPane" class="tags-view-wrapper" @scroll="handleScroll" @updateArrows="updateArrowState">
|
||||
<div id="tags-view-container" class="tags-view-container">
|
||||
<scroll-pane ref="scrollPane" class="tags-view-wrapper" @scroll="handleScroll">
|
||||
<router-link
|
||||
v-for="tag in visitedViews"
|
||||
ref="tag"
|
||||
@@ -15,46 +9,16 @@
|
||||
:to="{ path: tag.path, query: tag.query, fullPath: tag.fullPath }"
|
||||
tag="span"
|
||||
class="tags-view-item"
|
||||
:style="tagActiveStyle(tag)"
|
||||
@click.middle.native="!isAffix(tag) ? closeSelectedTag(tag) : ''"
|
||||
@contextmenu.prevent.native="openMenu(tag, $event)"
|
||||
:style="activeStyle(tag)"
|
||||
@click.middle.native="!isAffix(tag)?closeSelectedTag(tag):''"
|
||||
@contextmenu.prevent.native="openMenu(tag,$event)"
|
||||
>
|
||||
<svg-icon v-if="tagsIcon && tag.meta && tag.meta.icon && tag.meta.icon !== '#'" :icon-class="tag.meta.icon" style="margin-right: 3px;" />
|
||||
<svg-icon v-if="tagsIcon && tag.meta && tag.meta.icon && tag.meta.icon !== '#'" :icon-class="tag.meta.icon" />
|
||||
{{ tag.title }}
|
||||
<span v-if="!isAffix(tag)" class="el-icon-close" @click.prevent.stop="closeSelectedTag(tag)" />
|
||||
</router-link>
|
||||
</scroll-pane>
|
||||
|
||||
<!-- 右切换箭头 -->
|
||||
<span class="tags-nav-btn tags-nav-btn--right" :class="{ disabled: !canScrollRight }" @click="scrollRight">
|
||||
<i class="el-icon-arrow-right" />
|
||||
</span>
|
||||
|
||||
<!-- 下拉操作菜单 -->
|
||||
<el-dropdown class="tags-action-dropdown" trigger="click" placement="bottom-end" @command="handleDropdownCommand">
|
||||
<span class="tags-action-btn">
|
||||
<i class="el-icon-arrow-down" />
|
||||
</span>
|
||||
<el-dropdown-menu slot="dropdown" class="tags-dropdown-menu">
|
||||
<el-dropdown-item v-if="!isAffix(selectedDropdownTag)" command="close" icon="el-icon-close">关闭当前</el-dropdown-item>
|
||||
<el-dropdown-item command="closeOthers" icon="el-icon-circle-close">关闭其他</el-dropdown-item>
|
||||
<el-dropdown-item command="closeLeft" :disabled="isFirstView()" icon="el-icon-back">关闭左侧</el-dropdown-item>
|
||||
<el-dropdown-item command="closeRight" :disabled="isLastView()" icon="el-icon-right">关闭右侧</el-dropdown-item>
|
||||
<el-dropdown-item command="closeAll" icon="el-icon-circle-close">全部关闭</el-dropdown-item>
|
||||
<el-dropdown-item command="fullscreen" divided>
|
||||
<template v-if="!isFullscreen"><i class="el-icon-full-screen"></i>全屏显示</template>
|
||||
<template v-else><i class="el-icon-close"></i>退出全屏</template>
|
||||
</el-dropdown-item>
|
||||
</el-dropdown-menu>
|
||||
</el-dropdown>
|
||||
|
||||
<!-- 刷新按钮 -->
|
||||
<span class="tags-action-btn tags-refresh-btn" title="刷新页面" @click="refreshSelectedTag(selectedDropdownTag)">
|
||||
<i class="el-icon-refresh-right" /> 刷新
|
||||
</span>
|
||||
|
||||
<!-- 右键上下文菜单 -->
|
||||
<ul v-show="visible" :style="{ left: left + 'px', top: top + 'px' }" class="contextmenu">
|
||||
<ul v-show="visible" :style="{left:left+'px',top:top+'px'}" class="contextmenu">
|
||||
<li @click="refreshSelectedTag(selectedTag)"><i class="el-icon-refresh-right"></i> 刷新页面</li>
|
||||
<li v-if="!isAffix(selectedTag)" @click="closeSelectedTag(selectedTag)"><i class="el-icon-close"></i> 关闭当前</li>
|
||||
<li @click="closeOthersTags"><i class="el-icon-circle-close"></i> 关闭其他</li>
|
||||
@@ -77,11 +41,7 @@ export default {
|
||||
top: 0,
|
||||
left: 0,
|
||||
selectedTag: {},
|
||||
affixTags: [],
|
||||
canScrollLeft: false,
|
||||
canScrollRight: false,
|
||||
isFullscreen: false,
|
||||
hiddenElements: []
|
||||
affixTags: []
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
@@ -96,21 +56,6 @@ export default {
|
||||
},
|
||||
tagsIcon() {
|
||||
return this.$store.state.settings.tagsIcon
|
||||
},
|
||||
tagsViewStyle() {
|
||||
return this.$store.state.settings.tagsViewStyle
|
||||
},
|
||||
selectedDropdownTag() {
|
||||
return this.visitedViews.find(v => this.isActive(v)) || {}
|
||||
},
|
||||
chromeVars() {
|
||||
if (this.tagsViewStyle !== 'chrome') return {}
|
||||
const primary = this.theme || '#409EFF'
|
||||
return {
|
||||
'--chrome-tab-active-bg': this.mixHexWithWhite(primary, 0.15),
|
||||
'--chrome-tab-text-active': primary,
|
||||
'--chrome-wing-r': '14px'
|
||||
}
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
@@ -124,65 +69,36 @@ export default {
|
||||
} else {
|
||||
document.body.removeEventListener('click', this.closeMenu)
|
||||
}
|
||||
},
|
||||
visitedViews() {
|
||||
this.$nextTick(() => {
|
||||
this.updateArrowState()
|
||||
})
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
this.initTags()
|
||||
this.addTags()
|
||||
window.addEventListener('resize', this.updateArrowState)
|
||||
window.addEventListener('keydown', this.handleKeyDown)
|
||||
},
|
||||
beforeDestroy() {
|
||||
window.removeEventListener('resize', this.updateArrowState)
|
||||
window.removeEventListener('keydown', this.handleKeyDown)
|
||||
},
|
||||
methods: {
|
||||
handleKeyDown(event) {
|
||||
// 当按下Esc键且处于全屏状态时,退出全屏
|
||||
if (event.key === 'Escape' && this.isFullscreen) {
|
||||
this.toggleFullscreen()
|
||||
}
|
||||
},
|
||||
mixHexWithWhite(hex, ratio) {
|
||||
const clean = hex.replace('#', '')
|
||||
const r = parseInt(clean.substring(0, 2), 16)
|
||||
const g = parseInt(clean.substring(2, 4), 16)
|
||||
const b = parseInt(clean.substring(4, 6), 16)
|
||||
const mr = Math.round(r * ratio + 255 * (1 - ratio))
|
||||
const mg = Math.round(g * ratio + 255 * (1 - ratio))
|
||||
const mb = Math.round(b * ratio + 255 * (1 - ratio))
|
||||
return `rgb(${mr}, ${mg}, ${mb})`
|
||||
},
|
||||
isActive(route) {
|
||||
return route.path === this.$route.path
|
||||
},
|
||||
tagActiveStyle(tag) {
|
||||
if (!this.isActive(tag) || this.tagsViewStyle !== 'card') return {}
|
||||
activeStyle(tag) {
|
||||
if (!this.isActive(tag)) return {}
|
||||
return {
|
||||
"background-color": this.theme,
|
||||
"border-color": this.theme
|
||||
}
|
||||
},
|
||||
isAffix(tag) {
|
||||
return tag && tag.meta && tag.meta.affix
|
||||
return tag.meta && tag.meta.affix
|
||||
},
|
||||
isFirstView() {
|
||||
try {
|
||||
const tag = this.selectedTag && this.selectedTag.fullPath ? this.selectedTag : this.selectedDropdownTag
|
||||
return tag.fullPath === '/index' || tag.fullPath === this.visitedViews[1].fullPath
|
||||
return this.selectedTag.fullPath === '/index' || this.selectedTag.fullPath === this.visitedViews[1].fullPath
|
||||
} catch (err) {
|
||||
return false
|
||||
}
|
||||
},
|
||||
isLastView() {
|
||||
try {
|
||||
const tag = this.selectedTag && this.selectedTag.fullPath ? this.selectedTag : this.selectedDropdownTag
|
||||
return tag.fullPath === this.visitedViews[this.visitedViews.length - 1].fullPath
|
||||
return this.selectedTag.fullPath === this.visitedViews[this.visitedViews.length - 1].fullPath
|
||||
} catch (err) {
|
||||
return false
|
||||
}
|
||||
@@ -209,13 +125,11 @@ export default {
|
||||
return tags
|
||||
},
|
||||
initTags() {
|
||||
if (this.$store.state.settings.tagsViewPersist) {
|
||||
this.$store.dispatch('tagsView/loadPersistedViews')
|
||||
}
|
||||
const affixTags = this.affixTags = this.filterAffixTags(this.routes)
|
||||
for (const tag of affixTags) {
|
||||
// Must have tag name
|
||||
if (tag.name) {
|
||||
this.$store.dispatch('tagsView/addAffixView', tag)
|
||||
this.$store.dispatch('tagsView/addVisitedView', tag)
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -231,6 +145,7 @@ export default {
|
||||
for (const tag of tags) {
|
||||
if (tag.to.path === this.$route.path) {
|
||||
this.$refs.scrollPane.moveToTarget(tag)
|
||||
// when query is different then update
|
||||
if (tag.to.fullPath !== this.$route.fullPath) {
|
||||
this.$store.dispatch('tagsView/updateVisitedView', this.$route)
|
||||
}
|
||||
@@ -239,85 +154,6 @@ export default {
|
||||
}
|
||||
})
|
||||
},
|
||||
scrollLeft() {
|
||||
if (!this.canScrollLeft) return
|
||||
this.$refs.scrollPane.scrollToStart()
|
||||
},
|
||||
scrollRight() {
|
||||
if (!this.canScrollRight) return
|
||||
this.$refs.scrollPane.scrollToEnd()
|
||||
},
|
||||
updateArrowState() {
|
||||
this.$nextTick(() => {
|
||||
if (this.$refs.scrollPane) {
|
||||
const state = this.$refs.scrollPane.getScrollState()
|
||||
this.canScrollLeft = state.canLeft
|
||||
this.canScrollRight = state.canRight
|
||||
}
|
||||
})
|
||||
},
|
||||
toggleFullscreen() {
|
||||
const mainContainer = document.querySelector('.main-container')
|
||||
const navbar = document.querySelector('.navbar')
|
||||
const sidebar = document.querySelector('.sidebar-container')
|
||||
if (!mainContainer) return
|
||||
|
||||
if (!this.isFullscreen) {
|
||||
mainContainer.classList.add('fullscreen-mode')
|
||||
document.body.style.overflow = 'hidden'
|
||||
const elementsToHide = [
|
||||
{ el: navbar, originalDisplay: (navbar && navbar.style.display) || '' },
|
||||
{ el: sidebar, originalDisplay: (sidebar && sidebar.style.display) || '' }
|
||||
]
|
||||
elementsToHide.forEach(item => {
|
||||
if (item.el && item.el.style.display !== 'none') {
|
||||
item.originalDisplay = item.el.style.display
|
||||
item.el.style.display = 'none'
|
||||
this.hiddenElements.push(item)
|
||||
}
|
||||
})
|
||||
this.isFullscreen = true
|
||||
} else {
|
||||
mainContainer.classList.remove('fullscreen-mode')
|
||||
document.body.style.overflow = ''
|
||||
this.hiddenElements.forEach(item => {
|
||||
if (item.el) {
|
||||
item.el.style.display = item.originalDisplay
|
||||
}
|
||||
})
|
||||
this.hiddenElements = []
|
||||
const btn = document.querySelector('.tags-action-btn')
|
||||
if (btn) btn.blur()
|
||||
this.isFullscreen = false
|
||||
}
|
||||
},
|
||||
handleDropdownCommand(command) {
|
||||
const tag = this.selectedDropdownTag
|
||||
this.selectedTag = tag
|
||||
switch (command) {
|
||||
case 'refresh':
|
||||
this.refreshSelectedTag(tag)
|
||||
break
|
||||
case 'fullscreen':
|
||||
this.toggleFullscreen()
|
||||
break
|
||||
case 'close':
|
||||
this.closeSelectedTag(tag)
|
||||
break
|
||||
case 'closeOthers':
|
||||
this.closeOthersTags()
|
||||
break
|
||||
case 'closeLeft':
|
||||
this.closeLeftTags()
|
||||
break
|
||||
case 'closeRight':
|
||||
this.closeRightTags()
|
||||
break
|
||||
case 'closeAll':
|
||||
this.closeAllTags(tag)
|
||||
break
|
||||
}
|
||||
},
|
||||
refreshSelectedTag(view) {
|
||||
this.$tab.refreshPage(view)
|
||||
if (this.$route.meta.link) {
|
||||
@@ -346,7 +182,7 @@ export default {
|
||||
})
|
||||
},
|
||||
closeOthersTags() {
|
||||
this.$router.push(this.selectedTag.fullPath).catch(() => {})
|
||||
this.$router.push(this.selectedTag.fullPath).catch(()=>{})
|
||||
this.$tab.closeOtherPage(this.selectedTag).then(() => {
|
||||
this.moveToCurrentTag()
|
||||
})
|
||||
@@ -364,7 +200,10 @@ export default {
|
||||
if (latestView) {
|
||||
this.$router.push(latestView.fullPath)
|
||||
} else {
|
||||
if (view && view.name === 'Dashboard') {
|
||||
// now the default is to redirect to the home page if there is no tags-view,
|
||||
// you can adjust it according to your needs.
|
||||
if (view.name === 'Dashboard') {
|
||||
// to reload home page
|
||||
this.$router.replace({ path: '/redirect' + view.fullPath })
|
||||
} else {
|
||||
this.$router.push('/')
|
||||
@@ -372,7 +211,18 @@ export default {
|
||||
}
|
||||
},
|
||||
openMenu(tag, e) {
|
||||
this.left = e.clientX
|
||||
const menuMinWidth = 105
|
||||
const offsetLeft = this.$el.getBoundingClientRect().left // container margin left
|
||||
const offsetWidth = this.$el.offsetWidth // container width
|
||||
const maxLeft = offsetWidth - menuMinWidth // left boundary
|
||||
const left = e.clientX - offsetLeft + 15 // 15: margin right
|
||||
|
||||
if (left > maxLeft) {
|
||||
this.left = maxLeft
|
||||
} else {
|
||||
this.left = left
|
||||
}
|
||||
|
||||
this.top = e.clientY
|
||||
this.visible = true
|
||||
this.selectedTag = tag
|
||||
@@ -382,63 +232,19 @@ export default {
|
||||
},
|
||||
handleScroll() {
|
||||
this.closeMenu()
|
||||
this.updateArrowState()
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
$tags-bar-height: 34px;
|
||||
|
||||
.tags-view-container {
|
||||
height: $tags-bar-height;
|
||||
height: 34px;
|
||||
width: 100%;
|
||||
background: #fff;
|
||||
border-bottom: 1px solid #d8dce5;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
overflow: hidden;
|
||||
|
||||
$btn-width: 28px;
|
||||
$btn-color: #71717a;
|
||||
$btn-hover-bg: #f0f2f5;
|
||||
$btn-hover-color: #303133;
|
||||
$btn-disabled-color: #c0c4cc;
|
||||
$divider: 1px solid #d8dce5;
|
||||
|
||||
.tags-nav-btn {
|
||||
flex-shrink: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: $btn-width;
|
||||
height: $tags-bar-height;
|
||||
cursor: pointer;
|
||||
color: $btn-color;
|
||||
font-size: 13px;
|
||||
user-select: none;
|
||||
transition: background 0.15s, color 0.15s;
|
||||
|
||||
&:hover:not(.disabled) {
|
||||
background: $btn-hover-bg;
|
||||
color: $btn-hover-color;
|
||||
}
|
||||
|
||||
&.disabled {
|
||||
color: $btn-disabled-color;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
&--left { border-right: $divider; }
|
||||
&--right { border-left: $divider; }
|
||||
}
|
||||
|
||||
box-shadow: 0 1px 3px 0 rgba(0, 0, 0, .12), 0 0 3px 0 rgba(0, 0, 0, .04);
|
||||
.tags-view-wrapper {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
height: 100%;
|
||||
|
||||
.tags-view-item {
|
||||
display: inline-block;
|
||||
position: relative;
|
||||
@@ -451,13 +257,14 @@ $tags-bar-height: 34px;
|
||||
padding: 0 8px;
|
||||
font-size: 12px;
|
||||
margin-left: 5px;
|
||||
border-radius: 3px;
|
||||
|
||||
&:first-of-type { margin-left: 6px; }
|
||||
&:last-of-type { margin-right: 15px; }
|
||||
margin-top: 4px;
|
||||
&:first-of-type {
|
||||
margin-left: 15px;
|
||||
}
|
||||
&:last-of-type {
|
||||
margin-right: 15px;
|
||||
}
|
||||
&:not(.tags-view-container--chrome) .tags-view-wrapper .tags-view-item.active {
|
||||
&.active {
|
||||
background-color: #42b983;
|
||||
color: #fff;
|
||||
border-color: #42b983;
|
||||
@@ -472,45 +279,18 @@ $tags-bar-height: 34px;
|
||||
margin-right: 2px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&:not(.tags-view-container--chrome) .tags-view-wrapper .tags-view-item.active.has-icon::before {
|
||||
.tags-view-item.active.has-icon::before {
|
||||
content: none !important;
|
||||
}
|
||||
|
||||
.tags-action-dropdown {
|
||||
flex-shrink: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.tags-action-btn {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: $btn-width;
|
||||
height: $tags-bar-height;
|
||||
cursor: pointer;
|
||||
color: $btn-color;
|
||||
font-size: 13px;
|
||||
border-left: $divider;
|
||||
user-select: none;
|
||||
transition: background 0.15s, color 0.15s;
|
||||
|
||||
&:hover {
|
||||
background: $btn-hover-bg;
|
||||
color: $btn-hover-color;
|
||||
}
|
||||
}
|
||||
|
||||
.tags-refresh-btn {
|
||||
width: 60px;
|
||||
}
|
||||
|
||||
.contextmenu {
|
||||
margin: 0;
|
||||
background: #fff;
|
||||
z-index: 3000;
|
||||
position: fixed;
|
||||
position: absolute;
|
||||
list-style-type: none;
|
||||
padding: 5px 0;
|
||||
border-radius: 4px;
|
||||
@@ -518,7 +298,6 @@ $tags-bar-height: 34px;
|
||||
font-weight: 400;
|
||||
color: #333;
|
||||
box-shadow: 2px 2px 3px 0 rgba(0, 0, 0, .3);
|
||||
|
||||
li {
|
||||
margin: 0;
|
||||
padding: 7px 16px;
|
||||
@@ -528,174 +307,12 @@ $tags-bar-height: 34px;
|
||||
}
|
||||
}
|
||||
}
|
||||
&.tags-view-container--chrome {
|
||||
--chrome-strip-bg: #ffffff;
|
||||
--chrome-strip-border: #e4e7ed;
|
||||
--chrome-tab-text: #606266;
|
||||
|
||||
overflow: visible;
|
||||
background: var(--chrome-strip-bg);
|
||||
border-bottom: 1px solid var(--chrome-strip-border);
|
||||
align-items: flex-end;
|
||||
|
||||
.tags-nav-btn {
|
||||
align-self: stretch;
|
||||
height: auto;
|
||||
min-height: $tags-bar-height;
|
||||
border-color: var(--chrome-strip-border);
|
||||
}
|
||||
|
||||
.tags-action-btn {
|
||||
border-color: var(--chrome-strip-border);
|
||||
}
|
||||
|
||||
.tags-view-wrapper {
|
||||
.tags-view-item {
|
||||
display: inline-flex !important;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
height: 30px;
|
||||
min-height: 30px;
|
||||
margin: 0 0 -1px;
|
||||
padding: 0 12px;
|
||||
font-size: 13px;
|
||||
font-weight: 400;
|
||||
line-height: 1.2;
|
||||
border: none !important;
|
||||
border-radius: 0;
|
||||
background: transparent !important;
|
||||
color: var(--chrome-tab-text) !important;
|
||||
padding-top: 0 !important;
|
||||
box-shadow: none !important;
|
||||
transition: background 0.12s ease, color 0.12s ease, border-radius 0.12s ease;
|
||||
|
||||
&::before,
|
||||
&::after {
|
||||
content: '' !important;
|
||||
display: block !important;
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
width: var(--chrome-wing-r);
|
||||
height: var(--chrome-wing-r);
|
||||
margin: 0 !important;
|
||||
pointer-events: none;
|
||||
background: transparent !important;
|
||||
border-radius: 0 !important;
|
||||
transition: box-shadow 0.12s ease;
|
||||
}
|
||||
|
||||
&::before {
|
||||
left: calc(-1 * var(--chrome-wing-r));
|
||||
border-bottom-right-radius: var(--chrome-wing-r) !important;
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
&::after {
|
||||
right: calc(-1 * var(--chrome-wing-r));
|
||||
border-bottom-left-radius: var(--chrome-wing-r) !important;
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
&:first-of-type { margin-left: 6px; }
|
||||
&:last-of-type { margin-right: 10px; }
|
||||
|
||||
&:not(.active) + .tags-view-item:not(.active) {
|
||||
border-left: 1px solid #e4e7ed;
|
||||
padding-left: 11px;
|
||||
}
|
||||
|
||||
&:hover:not(.active) {
|
||||
background: #f5f7fa !important;
|
||||
border-radius: 6px 6px 0 0;
|
||||
color: #303133 !important;
|
||||
}
|
||||
|
||||
&.active {
|
||||
height: 31px;
|
||||
min-height: 31px;
|
||||
padding: 0 14px;
|
||||
color: var(--chrome-tab-text-active) !important;
|
||||
font-weight: 500;
|
||||
background: var(--chrome-tab-active-bg) !important;
|
||||
border: none !important;
|
||||
border-radius: var(--chrome-wing-r) var(--chrome-wing-r) 0 0;
|
||||
box-shadow: 0 1px 4px rgba(0, 0, 0, 0.06);
|
||||
|
||||
&::before {
|
||||
box-shadow: calc(var(--chrome-wing-r) * 0.5) calc(var(--chrome-wing-r) * 0.5) 0 calc(var(--chrome-wing-r) * 0.5) var(--chrome-tab-active-bg);
|
||||
}
|
||||
|
||||
&::after {
|
||||
box-shadow: calc(var(--chrome-wing-r) * -0.5) calc(var(--chrome-wing-r) * 0.5) 0 calc(var(--chrome-wing-r) * 0.5) var(--chrome-tab-active-bg);
|
||||
}
|
||||
}
|
||||
.el-icon-close {
|
||||
margin-left: 3px;
|
||||
&:before {
|
||||
vertical-align: -2px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
<style lang="scss">
|
||||
//reset element css of el-icon-close
|
||||
.tags-view-wrapper {
|
||||
.el-scrollbar {
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.el-scrollbar__wrap {
|
||||
height: 34px !important;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
overflow-x: auto;
|
||||
overflow-y: hidden;
|
||||
|
||||
&::-webkit-scrollbar {
|
||||
width: 0;
|
||||
height: 0;
|
||||
}
|
||||
|
||||
.tags-view-container:hover & {
|
||||
&::-webkit-scrollbar {
|
||||
width: 6px;
|
||||
height: 6px;
|
||||
}
|
||||
|
||||
&::-webkit-scrollbar-track {
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
&::-webkit-scrollbar-thumb {
|
||||
background-color: rgba(0, 0, 0, 0.2);
|
||||
border-radius: 3px;
|
||||
transition: background-color 0.2s;
|
||||
|
||||
&:hover {
|
||||
background-color: rgba(0, 0, 0, 0.4);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
scrollbar-width: none;
|
||||
-ms-overflow-style: none;
|
||||
}
|
||||
|
||||
.el-scrollbar__bar {
|
||||
opacity: 0;
|
||||
transition: opacity 0.3s;
|
||||
|
||||
.tags-view-container:hover & {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.tags-view-item {
|
||||
.el-icon-close {
|
||||
width: 16px;
|
||||
@@ -717,44 +334,4 @@ $tags-bar-height: 34px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* 页签全屏模式样式 */
|
||||
.main-container.fullscreen-mode {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
overflow: hidden;
|
||||
margin-left: 0 !important;
|
||||
transition: none !important;
|
||||
}
|
||||
|
||||
.main-container.fullscreen-mode .fixed-header {
|
||||
display: block !important;
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
width: 100% !important;
|
||||
z-index: 1000;
|
||||
transition: none !important;
|
||||
}
|
||||
|
||||
.main-container.fullscreen-mode .fixed-header .navbar {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
.main-container.fullscreen-mode .app-main {
|
||||
position: fixed;
|
||||
top: 34px;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
margin: 0 !important;
|
||||
padding: 0 !important;
|
||||
height: calc(100vh - 34px) !important;
|
||||
min-height: calc(100vh - 34px) !important;
|
||||
overflow: auto;
|
||||
}
|
||||
</style>
|
||||
@@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<div :class="classObj" class="app-wrapper" :style="{'--current-color': theme, '--current-color-light': theme + '1a', '--current-color-dark-bg': theme + '33'}">
|
||||
<div :class="classObj" class="app-wrapper" :style="{'--current-color': theme}">
|
||||
<div v-if="device==='mobile'&&sidebar.opened" class="drawer-bg" @click="handleClickOutside"/>
|
||||
<sidebar v-if="!sidebar.hide" class="sidebar-container"/>
|
||||
<div :class="{hasTagsView:needTagsView,sidebarHide:sidebar.hide}" class="main-container">
|
||||
|
||||
@@ -19,19 +19,12 @@ router.beforeEach((to, from, next) => {
|
||||
NProgress.start()
|
||||
if (getToken()) {
|
||||
to.meta.title && store.dispatch('settings/setTitle', to.meta.title)
|
||||
const isLock = store.getters.isLock
|
||||
/* has token*/
|
||||
if (to.path === '/login') {
|
||||
next({ path: '/' })
|
||||
NProgress.done()
|
||||
} else if (isWhiteList(to.path)) {
|
||||
next()
|
||||
} else if (isLock && to.path !== '/lock') {
|
||||
next({ path: '/lock' })
|
||||
NProgress.done()
|
||||
} else if (!isLock && to.path === '/lock') {
|
||||
next({ path: '/' })
|
||||
NProgress.done()
|
||||
} else {
|
||||
if (store.getters.roles.length === 0) {
|
||||
isRelogin.show = true
|
||||
|
||||
@@ -5,10 +5,6 @@ export default {
|
||||
// 刷新当前tab页签
|
||||
refreshPage(obj) {
|
||||
const { path, query, matched } = router.currentRoute
|
||||
// 防止在重定向过程中重复刷新
|
||||
if (path.startsWith('/redirect/')) {
|
||||
return Promise.resolve()
|
||||
}
|
||||
if (obj === undefined) {
|
||||
matched.forEach((m) => {
|
||||
if (m.components && m.components.default && m.components.default.name) {
|
||||
|
||||
@@ -68,18 +68,12 @@ export const constantRoutes = [
|
||||
children: [
|
||||
{
|
||||
path: 'index',
|
||||
component: () => import('@/views/index'),
|
||||
name: 'Index',
|
||||
meta: { title: '首页', icon: 'dashboard', affix: true }
|
||||
component: () => import('@/views/loanPricing/workflow/index'),
|
||||
name: 'LoanPricingWorkflow',
|
||||
meta: { title: '流程列表', icon: 'dashboard', affix: true }
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
path: '/lock',
|
||||
component: () => import('@/views/lock'),
|
||||
hidden: true,
|
||||
meta: { title: '锁定屏幕' }
|
||||
},
|
||||
{
|
||||
path: '/user',
|
||||
component: Layout,
|
||||
@@ -167,6 +161,20 @@ export const dynamicRoutes = [
|
||||
meta: { title: '修改生成配置', activeMenu: '/tool/gen' }
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
path: '/loanPricing/workflow-detail',
|
||||
component: Layout,
|
||||
hidden: true,
|
||||
permissions: ['loanPricing:workflow:query'],
|
||||
children: [
|
||||
{
|
||||
path: ':serialNum',
|
||||
component: () => import('@/views/loanPricing/workflow/detail'),
|
||||
name: 'LoanPricingWorkflowDetail',
|
||||
meta: { title: '流程详情', activeMenu: '/loanPricing/workflow' }
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
|
||||
|
||||
@@ -24,21 +24,11 @@ module.exports = {
|
||||
*/
|
||||
tagsView: true,
|
||||
|
||||
/**
|
||||
* 持久化标签页
|
||||
*/
|
||||
tagsViewPersist: false,
|
||||
|
||||
/**
|
||||
* 显示页签图标
|
||||
*/
|
||||
tagsIcon: false,
|
||||
|
||||
/**
|
||||
* 标签页样式:card 卡片(默认)、chrome 谷歌浏览器风格
|
||||
*/
|
||||
tagsViewStyle: 'card',
|
||||
|
||||
/**
|
||||
* 是否固定头部
|
||||
*/
|
||||
|
||||
@@ -3,8 +3,6 @@ const getters = {
|
||||
size: state => state.app.size,
|
||||
device: state => state.app.device,
|
||||
dict: state => state.dict.dict,
|
||||
isLock: state => state.lock.isLock,
|
||||
lockPath: state => state.lock.lockPath,
|
||||
visitedViews: state => state.tagsView.visitedViews,
|
||||
cachedViews: state => state.tagsView.cachedViews,
|
||||
token: state => state.user.token,
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import Vue from 'vue'
|
||||
import Vuex from 'vuex'
|
||||
import app from './modules/app'
|
||||
import lock from './modules/lock'
|
||||
import dict from './modules/dict'
|
||||
import user from './modules/user'
|
||||
import tagsView from './modules/tagsView'
|
||||
@@ -14,7 +13,6 @@ Vue.use(Vuex)
|
||||
const store = new Vuex.Store({
|
||||
modules: {
|
||||
app,
|
||||
lock,
|
||||
dict,
|
||||
user,
|
||||
tagsView,
|
||||
|
||||
@@ -1,34 +0,0 @@
|
||||
const LOCK_KEY = 'screen-lock'
|
||||
const LOCK_PATH_KEY = 'screen-lock-path'
|
||||
|
||||
const lock = {
|
||||
namespaced: true,
|
||||
state: {
|
||||
isLock: JSON.parse(localStorage.getItem(LOCK_KEY) || 'false'),
|
||||
lockPath: localStorage.getItem(LOCK_PATH_KEY) || '/index'
|
||||
},
|
||||
mutations: {
|
||||
SET_LOCK(state, status) {
|
||||
state.isLock = status
|
||||
localStorage.setItem(LOCK_KEY, JSON.stringify(status))
|
||||
},
|
||||
SET_LOCK_PATH(state, path) {
|
||||
state.lockPath = path
|
||||
localStorage.setItem(LOCK_PATH_KEY, path)
|
||||
}
|
||||
},
|
||||
actions: {
|
||||
// 锁定屏幕,同时记录当前路径
|
||||
lockScreen({ commit }, currentPath) {
|
||||
commit('SET_LOCK_PATH', currentPath || '/index')
|
||||
commit('SET_LOCK', true)
|
||||
},
|
||||
// 解锁屏幕,清除路径
|
||||
unlockScreen({ commit }) {
|
||||
commit('SET_LOCK', false)
|
||||
commit('SET_LOCK_PATH', '/index')
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default lock
|
||||
@@ -1,7 +1,7 @@
|
||||
import defaultSettings from '@/settings'
|
||||
import { useDynamicTitle } from '@/utils/dynamicTitle'
|
||||
|
||||
const { sideTheme, showSettings, navType, tagsView, tagsViewPersist, tagsIcon, tagsViewStyle, fixedHeader, sidebarLogo, dynamicTitle, footerVisible, footerContent } = defaultSettings
|
||||
const { sideTheme, showSettings, navType, tagsView, tagsIcon, fixedHeader, sidebarLogo, dynamicTitle, footerVisible, footerContent } = defaultSettings
|
||||
|
||||
const storageSetting = JSON.parse(localStorage.getItem('layout-setting')) || ''
|
||||
const state = {
|
||||
@@ -11,9 +11,7 @@ const state = {
|
||||
showSettings: showSettings,
|
||||
navType: storageSetting.navType === undefined ? navType : storageSetting.navType,
|
||||
tagsView: storageSetting.tagsView === undefined ? tagsView : storageSetting.tagsView,
|
||||
tagsViewPersist: storageSetting.tagsViewPersist === undefined ? tagsViewPersist : storageSetting.tagsViewPersist,
|
||||
tagsIcon: storageSetting.tagsIcon === undefined ? tagsIcon : storageSetting.tagsIcon,
|
||||
tagsViewStyle: storageSetting.tagsViewStyle === undefined ? tagsViewStyle : storageSetting.tagsViewStyle,
|
||||
fixedHeader: storageSetting.fixedHeader === undefined ? fixedHeader : storageSetting.fixedHeader,
|
||||
sidebarLogo: storageSetting.sidebarLogo === undefined ? sidebarLogo : storageSetting.sidebarLogo,
|
||||
dynamicTitle: storageSetting.dynamicTitle === undefined ? dynamicTitle : storageSetting.dynamicTitle,
|
||||
|
||||
@@ -1,26 +1,3 @@
|
||||
import store from '@/store'
|
||||
import cache from '@/plugins/cache'
|
||||
|
||||
const PERSIST_KEY = 'tags-view-visited'
|
||||
|
||||
function isPersistEnabled() {
|
||||
return store.state.settings.tagsViewPersist
|
||||
}
|
||||
|
||||
function saveVisitedViews(views) {
|
||||
if (!isPersistEnabled()) return
|
||||
const toSave = views.filter(v => !(v.meta && v.meta.affix)).map(v => ({ path: v.path, fullPath: v.fullPath, name: v.name, title: v.title, query: v.query, meta: v.meta }))
|
||||
cache.local.setJSON(PERSIST_KEY, toSave)
|
||||
}
|
||||
|
||||
function loadVisitedViews() {
|
||||
return cache.local.getJSON(PERSIST_KEY) || []
|
||||
}
|
||||
|
||||
function clearVisitedViews() {
|
||||
cache.local.remove(PERSIST_KEY)
|
||||
}
|
||||
|
||||
const state = {
|
||||
visitedViews: [],
|
||||
cachedViews: [],
|
||||
@@ -43,15 +20,6 @@ const mutations = {
|
||||
title: view.meta.title || 'no-name'
|
||||
})
|
||||
)
|
||||
saveVisitedViews(state.visitedViews)
|
||||
},
|
||||
ADD_VISITED_VIEW_FIRST: (state, view) => {
|
||||
if (state.visitedViews.some(v => v.path === view.path)) return
|
||||
state.visitedViews.unshift(
|
||||
Object.assign({}, view, {
|
||||
title: view.meta.title || 'no-name'
|
||||
})
|
||||
)
|
||||
},
|
||||
ADD_CACHED_VIEW: (state, view) => {
|
||||
if (state.cachedViews.includes(view.name)) return
|
||||
@@ -67,7 +35,6 @@ const mutations = {
|
||||
}
|
||||
}
|
||||
state.iframeViews = state.iframeViews.filter(item => item.path !== view.path)
|
||||
saveVisitedViews(state.visitedViews)
|
||||
},
|
||||
DEL_IFRAME_VIEW: (state, view) => {
|
||||
state.iframeViews = state.iframeViews.filter(item => item.path !== view.path)
|
||||
@@ -82,7 +49,6 @@ const mutations = {
|
||||
return v.meta.affix || v.path === view.path
|
||||
})
|
||||
state.iframeViews = state.iframeViews.filter(item => item.path === view.path)
|
||||
saveVisitedViews(state.visitedViews)
|
||||
},
|
||||
DEL_OTHERS_CACHED_VIEWS: (state, view) => {
|
||||
const index = state.cachedViews.indexOf(view.name)
|
||||
@@ -97,7 +63,6 @@ const mutations = {
|
||||
const affixTags = state.visitedViews.filter(tag => tag.meta.affix)
|
||||
state.visitedViews = affixTags
|
||||
state.iframeViews = []
|
||||
clearVisitedViews()
|
||||
},
|
||||
DEL_ALL_CACHED_VIEWS: state => {
|
||||
state.cachedViews = []
|
||||
@@ -129,7 +94,6 @@ const mutations = {
|
||||
}
|
||||
return false
|
||||
})
|
||||
saveVisitedViews(state.visitedViews)
|
||||
},
|
||||
DEL_LEFT_VIEWS: (state, view) => {
|
||||
const index = state.visitedViews.findIndex(v => v.path === view.path)
|
||||
@@ -150,7 +114,6 @@ const mutations = {
|
||||
}
|
||||
return false
|
||||
})
|
||||
saveVisitedViews(state.visitedViews)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -165,9 +128,6 @@ const actions = {
|
||||
addVisitedView({ commit }, view) {
|
||||
commit('ADD_VISITED_VIEW', view)
|
||||
},
|
||||
addAffixView({ commit }, view) {
|
||||
commit('ADD_VISITED_VIEW_FIRST', view)
|
||||
},
|
||||
addCachedView({ commit }, view) {
|
||||
commit('ADD_CACHED_VIEW', view)
|
||||
},
|
||||
@@ -258,13 +218,6 @@ const actions = {
|
||||
resolve([...state.visitedViews])
|
||||
})
|
||||
},
|
||||
// 恢复持久化的 tags
|
||||
loadPersistedViews({ commit }) {
|
||||
const views = loadVisitedViews()
|
||||
views.forEach(view => {
|
||||
commit('ADD_VISITED_VIEW', view)
|
||||
})
|
||||
},
|
||||
}
|
||||
|
||||
export default {
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import store from '@/store'
|
||||
import router from '@/router'
|
||||
import { MessageBox, } from 'element-ui'
|
||||
import { login, logout, getInfo } from '@/api/login'
|
||||
@@ -52,7 +51,6 @@ const user = {
|
||||
login(username, password, code, uuid).then(res => {
|
||||
setToken(res.token)
|
||||
commit('SET_TOKEN', res.token)
|
||||
store.dispatch('lock/unlockScreen')
|
||||
resolve()
|
||||
}).catch(error => {
|
||||
reject(error)
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,375 +0,0 @@
|
||||
<template>
|
||||
<div class="lock-container">
|
||||
<!-- 动态粒子背景 -->
|
||||
<canvas ref="particleCanvas" class="particle-bg"></canvas>
|
||||
|
||||
<!-- 时钟 -->
|
||||
<div class="lock-time">{{ currentTime }}</div>
|
||||
<div class="lock-date">{{ currentDate }}</div>
|
||||
|
||||
<!-- 锁屏卡片 -->
|
||||
<div class="lock-card">
|
||||
<div class="avatar-wrap">
|
||||
<img :src="avatar" class="lock-avatar" @error="onAvatarError" />
|
||||
<div class="lock-icon">🔒</div>
|
||||
</div>
|
||||
<div class="lock-username">{{ nickName }}</div>
|
||||
<div class="lock-hint">系统已锁定,请输入密码解锁</div>
|
||||
|
||||
<div class="input-wrap" :class="{ shake: isShaking }">
|
||||
<input ref="passwordInput" v-model="password" type="password" placeholder="请输入登录密码" class="lock-input" @keydown.enter="handleUnlock" autocomplete="off" />
|
||||
<button class="unlock-btn" @click="handleUnlock" :disabled="loading">
|
||||
<span v-if="!loading">→</span>
|
||||
<span v-else class="loading-dot">···</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div v-if="errorMsg" class="error-msg">{{ errorMsg }}</div>
|
||||
|
||||
<div class="lock-footer">
|
||||
<a href="/login" @click.prevent="goLogin">退出重新登录</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { mapGetters } from 'vuex'
|
||||
import { unlockScreen } from '@/api/login'
|
||||
import defAva from '@/assets/images/profile.jpg'
|
||||
|
||||
export default {
|
||||
name: 'LockScreen',
|
||||
data() {
|
||||
return {
|
||||
password: '',
|
||||
loading: false,
|
||||
errorMsg: '',
|
||||
isShaking: false,
|
||||
currentTime: '',
|
||||
currentDate: '',
|
||||
timer: null,
|
||||
animationId: null,
|
||||
particles: []
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
...mapGetters(['avatar', 'nickName'])
|
||||
},
|
||||
mounted() {
|
||||
this.startClock()
|
||||
this.initParticles()
|
||||
this.$nextTick(() => {
|
||||
this.$refs.passwordInput && this.$refs.passwordInput.focus()
|
||||
})
|
||||
},
|
||||
beforeDestroy() {
|
||||
clearInterval(this.timer)
|
||||
cancelAnimationFrame(this.animationId)
|
||||
},
|
||||
methods: {
|
||||
onAvatarError(e) {
|
||||
e.target.src = defAva
|
||||
},
|
||||
startClock() {
|
||||
const update = () => {
|
||||
const now = new Date()
|
||||
const h = String(now.getHours()).padStart(2, '0')
|
||||
const m = String(now.getMinutes()).padStart(2, '0')
|
||||
const s = String(now.getSeconds()).padStart(2, '0')
|
||||
this.currentTime = `${h}:${m}:${s}`
|
||||
const days = ['星期日','星期一','星期二','星期三','星期四','星期五','星期六']
|
||||
this.currentDate = `${now.getFullYear()}年${now.getMonth()+1}月${now.getDate()}日 ${days[now.getDay()]}`
|
||||
}
|
||||
update()
|
||||
this.timer = setInterval(update, 1000)
|
||||
},
|
||||
async handleUnlock() {
|
||||
if (!this.password) {
|
||||
this.showError('请输入密码')
|
||||
return
|
||||
}
|
||||
this.loading = true
|
||||
this.errorMsg = ''
|
||||
try {
|
||||
await unlockScreen(this.password)
|
||||
const lockPath = this.$store.getters.lockPath // 取锁屏前的路径
|
||||
await this.$store.dispatch('lock/unlockScreen')
|
||||
this.$router.replace(lockPath)
|
||||
} catch (err) {
|
||||
const msg = err.message || err.toString()
|
||||
this.showError(msg)
|
||||
this.password = ''
|
||||
this.$refs.passwordInput && this.$refs.passwordInput.focus()
|
||||
} finally {
|
||||
this.loading = false
|
||||
}
|
||||
},
|
||||
showError(msg) {
|
||||
this.errorMsg = msg
|
||||
this.isShaking = true
|
||||
setTimeout(() => { this.isShaking = false }, 600)
|
||||
},
|
||||
goLogin() {
|
||||
this.$store.dispatch('lock/unlockScreen')
|
||||
this.$store.dispatch('LogOut').then(() => {
|
||||
this.$router.push('/login')
|
||||
})
|
||||
},
|
||||
// 粒子背景
|
||||
initParticles() {
|
||||
const canvas = this.$refs.particleCanvas
|
||||
if (!canvas) return
|
||||
const ctx = canvas.getContext('2d')
|
||||
const resize = () => {
|
||||
canvas.width = window.innerWidth
|
||||
canvas.height = window.innerHeight
|
||||
}
|
||||
resize()
|
||||
window.addEventListener('resize', resize)
|
||||
const count = 80
|
||||
for (let i = 0; i < count; i++) {
|
||||
this.particles.push({
|
||||
x: Math.random() * canvas.width,
|
||||
y: Math.random() * canvas.height,
|
||||
r: Math.random() * 2 + 1,
|
||||
dx: (Math.random() - 0.5) * 0.6,
|
||||
dy: (Math.random() - 0.5) * 0.6,
|
||||
alpha: Math.random() * 0.5 + 0.2
|
||||
})
|
||||
}
|
||||
const draw = () => {
|
||||
ctx.clearRect(0, 0, canvas.width, canvas.height)
|
||||
this.particles.forEach(p => {
|
||||
ctx.beginPath()
|
||||
ctx.arc(p.x, p.y, p.r, 0, Math.PI * 2)
|
||||
ctx.fillStyle = `rgba(255,255,255,${p.alpha})`
|
||||
ctx.fill()
|
||||
p.x += p.dx
|
||||
p.y += p.dy
|
||||
if (p.x < 0 || p.x > canvas.width) p.dx *= -1
|
||||
if (p.y < 0 || p.y > canvas.height) p.dy *= -1
|
||||
})
|
||||
// 连线
|
||||
for (let i = 0; i < this.particles.length; i++) {
|
||||
for (let j = i + 1; j < this.particles.length; j++) {
|
||||
const a = this.particles[i], b = this.particles[j]
|
||||
const dist = Math.hypot(a.x - b.x, a.y - b.y)
|
||||
if (dist < 120) {
|
||||
ctx.beginPath()
|
||||
ctx.moveTo(a.x, a.y)
|
||||
ctx.lineTo(b.x, b.y)
|
||||
ctx.strokeStyle = `rgba(255,255,255,${0.15 * (1 - dist / 120)})`
|
||||
ctx.lineWidth = 0.5
|
||||
ctx.stroke()
|
||||
}
|
||||
}
|
||||
}
|
||||
this.animationId = requestAnimationFrame(draw)
|
||||
}
|
||||
draw()
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.lock-container {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
background: linear-gradient(135deg, #0f0c29, #302b63, #24243e);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
z-index: 9999;
|
||||
font-family: 'PingFang SC', 'Microsoft YaHei', sans-serif;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.particle-bg {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
z-index: 0;
|
||||
}
|
||||
|
||||
.lock-time {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
font-size: 72px;
|
||||
font-weight: 200;
|
||||
color: #fff;
|
||||
letter-spacing: 4px;
|
||||
text-shadow: 0 0 40px rgba(255,255,255,0.3);
|
||||
margin-bottom: 8px;
|
||||
font-variant-numeric: tabular-nums;
|
||||
}
|
||||
|
||||
.lock-date {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
font-size: 15px;
|
||||
color: rgba(255,255,255,0.6);
|
||||
margin-bottom: 48px;
|
||||
letter-spacing: 2px;
|
||||
}
|
||||
|
||||
.lock-card {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
background: rgba(255, 255, 255, 0.08);
|
||||
backdrop-filter: blur(20px);
|
||||
-webkit-backdrop-filter: blur(20px);
|
||||
border: 1px solid rgba(255, 255, 255, 0.15);
|
||||
border-radius: 24px;
|
||||
padding: 40px 48px;
|
||||
width: 360px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
box-shadow: 0 25px 60px rgba(0,0,0,0.4);
|
||||
}
|
||||
|
||||
.avatar-wrap {
|
||||
position: relative;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.lock-avatar {
|
||||
width: 80px;
|
||||
height: 80px;
|
||||
border-radius: 50%;
|
||||
border: 3px solid rgba(255,255,255,0.3);
|
||||
object-fit: cover;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.lock-icon {
|
||||
position: absolute;
|
||||
bottom: -4px;
|
||||
right: -4px;
|
||||
background: rgba(255,255,255,0.15);
|
||||
border-radius: 50%;
|
||||
width: 26px;
|
||||
height: 26px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 13px;
|
||||
backdrop-filter: blur(8px);
|
||||
}
|
||||
|
||||
.lock-username {
|
||||
color: #fff;
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
margin-bottom: 6px;
|
||||
letter-spacing: 1px;
|
||||
}
|
||||
|
||||
.lock-hint {
|
||||
color: rgba(255,255,255,0.5);
|
||||
font-size: 13px;
|
||||
margin-bottom: 28px;
|
||||
}
|
||||
|
||||
.input-wrap {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
background: rgba(255,255,255,0.1);
|
||||
border: 1px solid rgba(255,255,255,0.2);
|
||||
border-radius: 50px;
|
||||
padding: 4px 4px 4px 20px;
|
||||
transition: border-color 0.3s;
|
||||
}
|
||||
|
||||
.input-wrap:focus-within {
|
||||
border-color: rgba(255,255,255,0.6);
|
||||
background: rgba(255,255,255,0.13);
|
||||
}
|
||||
|
||||
.input-wrap.shake {
|
||||
animation: shake 0.5s ease;
|
||||
}
|
||||
|
||||
@keyframes shake {
|
||||
0%, 100% { transform: translateX(0); }
|
||||
20% { transform: translateX(-8px); }
|
||||
40% { transform: translateX(8px); }
|
||||
60% { transform: translateX(-6px); }
|
||||
80% { transform: translateX(6px); }
|
||||
}
|
||||
|
||||
.lock-input {
|
||||
flex: 1;
|
||||
background: transparent;
|
||||
border: none;
|
||||
outline: none;
|
||||
color: #fff;
|
||||
font-size: 15px;
|
||||
padding: 10px 0;
|
||||
}
|
||||
|
||||
.lock-input::placeholder {
|
||||
color: rgba(255,255,255,0.35);
|
||||
}
|
||||
|
||||
.unlock-btn {
|
||||
width: 42px;
|
||||
height: 42px;
|
||||
border-radius: 50%;
|
||||
background: linear-gradient(135deg, #667eea, #764ba2);
|
||||
border: none;
|
||||
color: #fff;
|
||||
font-size: 18px;
|
||||
cursor: pointer;
|
||||
transition: transform 0.2s, opacity 0.2s;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.unlock-btn:hover:not(:disabled) {
|
||||
transform: scale(1.08);
|
||||
}
|
||||
|
||||
.unlock-btn:disabled {
|
||||
opacity: 0.6;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.loading-dot {
|
||||
font-size: 13px;
|
||||
letter-spacing: 1px;
|
||||
}
|
||||
|
||||
.error-msg {
|
||||
margin-top: 14px;
|
||||
color: #ff7675;
|
||||
font-size: 13px;
|
||||
text-align: center;
|
||||
animation: fadeIn 0.3s ease;
|
||||
}
|
||||
|
||||
@keyframes fadeIn {
|
||||
from { opacity: 0; transform: translateY(-4px); }
|
||||
to { opacity: 1; transform: translateY(0); }
|
||||
}
|
||||
|
||||
.lock-footer {
|
||||
margin-top: 24px;
|
||||
}
|
||||
|
||||
.lock-footer a {
|
||||
color: rgba(255,255,255,0.4);
|
||||
font-size: 13px;
|
||||
text-decoration: none;
|
||||
transition: color 0.2s;
|
||||
}
|
||||
|
||||
.lock-footer a:hover {
|
||||
color: rgba(255,255,255,0.8);
|
||||
}
|
||||
</style>
|
||||
20
ruoyi-ui/src/views/monitor/cache/index.vue
vendored
20
ruoyi-ui/src/views/monitor/cache/index.vue
vendored
@@ -45,7 +45,7 @@
|
||||
|
||||
<el-col :span="12" class="card-box">
|
||||
<el-card>
|
||||
<div slot="header"><span><i class="el-icon-pie-chart"></i> 缓存统计</span></div>
|
||||
<div slot="header"><span><i class="el-icon-pie-chart"></i> 命令统计</span></div>
|
||||
<div class="el-table el-table--enable-row-hover el-table--medium">
|
||||
<div ref="commandstats" style="height: 420px" />
|
||||
</div>
|
||||
@@ -54,7 +54,7 @@
|
||||
|
||||
<el-col :span="12" class="card-box">
|
||||
<el-card>
|
||||
<div slot="header"><span><i class="el-icon-odometer"></i> 命中概览</span></div>
|
||||
<div slot="header"><span><i class="el-icon-odometer"></i> 缓存命中概览</span></div>
|
||||
<div class="el-table el-table--enable-row-hover el-table--medium">
|
||||
<div ref="usedmemory" style="height: 420px" />
|
||||
</div>
|
||||
@@ -72,8 +72,11 @@ export default {
|
||||
name: "Cache",
|
||||
data() {
|
||||
return {
|
||||
// 统计命令信息
|
||||
commandstats: null,
|
||||
// 缓存统计
|
||||
usedmemory: null,
|
||||
// cache信息
|
||||
cache: {
|
||||
info: {
|
||||
cache_type: "IN_MEMORY",
|
||||
@@ -130,6 +133,7 @@ export default {
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
/** 查缓存询信息 */
|
||||
getList() {
|
||||
getCache().then((response) => {
|
||||
this.cache = this.normalizeCacheData(response.data)
|
||||
@@ -144,6 +148,7 @@ export default {
|
||||
this.$modal.closeLoading()
|
||||
})
|
||||
},
|
||||
// 打开加载层
|
||||
openLoading() {
|
||||
this.$modal.loading("正在加载缓存监控数据,请稍候!")
|
||||
},
|
||||
@@ -208,7 +213,6 @@ export default {
|
||||
if (!this.usedmemory) {
|
||||
this.usedmemory = echarts.init(this.$refs.usedmemory, "macarons")
|
||||
}
|
||||
|
||||
this.commandstats.setOption({
|
||||
tooltip: {
|
||||
trigger: "item",
|
||||
@@ -227,7 +231,6 @@ export default {
|
||||
}
|
||||
]
|
||||
})
|
||||
|
||||
this.usedmemory.setOption({
|
||||
tooltip: {
|
||||
trigger: "axis",
|
||||
@@ -255,15 +258,10 @@ export default {
|
||||
}
|
||||
]
|
||||
})
|
||||
|
||||
if (!this.resizeHandler) {
|
||||
this.resizeHandler = () => {
|
||||
if (this.commandstats) {
|
||||
this.commandstats.resize()
|
||||
}
|
||||
if (this.usedmemory) {
|
||||
this.usedmemory.resize()
|
||||
}
|
||||
this.commandstats && this.commandstats.resize()
|
||||
this.usedmemory && this.usedmemory.resize()
|
||||
}
|
||||
window.addEventListener("resize", this.resizeHandler)
|
||||
}
|
||||
|
||||
20
ruoyi-ui/src/views/monitor/cache/list.vue
vendored
20
ruoyi-ui/src/views/monitor/cache/list.vue
vendored
@@ -175,7 +175,10 @@ export default {
|
||||
getCacheNames() {
|
||||
this.loading = true
|
||||
listCacheName().then(response => {
|
||||
this.cacheNames = response.data
|
||||
this.cacheNames = response.data || []
|
||||
this.loading = false
|
||||
}).catch(() => {
|
||||
this.cacheNames = []
|
||||
this.loading = false
|
||||
})
|
||||
},
|
||||
@@ -188,7 +191,7 @@ export default {
|
||||
handleClearCacheName(row) {
|
||||
clearCacheName(row.cacheName).then(response => {
|
||||
this.$modal.msgSuccess("清理缓存名称[" + row.cacheName + "]成功")
|
||||
this.getCacheKeys()
|
||||
this.getCacheKeys(row)
|
||||
})
|
||||
},
|
||||
/** 查询缓存键名列表 */
|
||||
@@ -199,7 +202,13 @@ export default {
|
||||
}
|
||||
this.subLoading = true
|
||||
listCacheKey(cacheName).then(response => {
|
||||
this.cacheKeys = response.data
|
||||
this.cacheKeys = response.data || []
|
||||
this.cacheForm = {}
|
||||
this.subLoading = false
|
||||
this.nowCacheName = cacheName
|
||||
}).catch(() => {
|
||||
this.cacheKeys = []
|
||||
this.cacheForm = {}
|
||||
this.subLoading = false
|
||||
this.nowCacheName = cacheName
|
||||
})
|
||||
@@ -227,17 +236,16 @@ export default {
|
||||
/** 查询缓存内容详细 */
|
||||
handleCacheValue(cacheKey) {
|
||||
getCacheValue(this.nowCacheName, cacheKey).then(response => {
|
||||
this.cacheForm = response.data
|
||||
this.cacheForm = response.data || {}
|
||||
})
|
||||
},
|
||||
/** 清理全部缓存 */
|
||||
handleClearCacheAll() {
|
||||
clearCacheAll().then(response => {
|
||||
this.$modal.msgSuccess("清理全部缓存成功")
|
||||
this.cacheKeys = []
|
||||
this.cacheForm = {}
|
||||
this.nowCacheName = ""
|
||||
this.getCacheNames()
|
||||
this.$modal.msgSuccess("清理全部缓存成功")
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,197 +0,0 @@
|
||||
<template>
|
||||
<el-dialog :title="type === 'log' ? '调度日志详细' : '任务详细'" :visible.sync="visible" width="780px" append-to-body @close="$emit('update:visible', false)">
|
||||
<div class="detail-wrap">
|
||||
|
||||
<template v-if="type === 'log'">
|
||||
<!-- 基本信息 -->
|
||||
<div class="detail-card">
|
||||
<div class="detail-card-title"><i class="el-icon-info"></i> 基本信息</div>
|
||||
<el-row class="detail-row">
|
||||
<el-col :span="12">
|
||||
<div class="detail-item"><span class="detail-label">日志编号</span><span class="detail-value">{{ form.jobLogId }}</span></div>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<div class="detail-item">
|
||||
<span class="detail-label">执行状态</span>
|
||||
<el-tag v-if="form.status == 0" type="success" size="small">正常</el-tag>
|
||||
<el-tag v-else type="danger" size="small">失败</el-tag>
|
||||
</div>
|
||||
</el-col>
|
||||
</el-row>
|
||||
<el-row class="detail-row">
|
||||
<el-col :span="12">
|
||||
<div class="detail-item"><span class="detail-label">开始时间</span><span class="detail-value">{{ form.startTime }}</span></div>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<div class="detail-item"><span class="detail-label">结束时间</span><span class="detail-value">{{ form.endTime }}</span></div>
|
||||
</el-col>
|
||||
</el-row>
|
||||
<el-row class="detail-row">
|
||||
<el-col :span="12">
|
||||
<div class="detail-item"><span class="detail-label">记录时间</span><span class="detail-value">{{ form.createTime }}</span></div>
|
||||
</el-col>
|
||||
<el-col :span="12" v-if="form.status == 0 && form.startTime && form.endTime">
|
||||
<div class="detail-item"><span class="detail-label">执行耗时</span><span class="detail-value">{{ costTime }} 毫秒</span></div>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</div>
|
||||
|
||||
<!-- 任务信息 -->
|
||||
<div class="detail-card">
|
||||
<div class="detail-card-title"><i class="el-icon-time"></i> 任务信息</div>
|
||||
<el-row class="detail-row">
|
||||
<el-col :span="12">
|
||||
<div class="detail-item"><span class="detail-label">任务名称</span><span class="detail-value">{{ form.jobName }}</span></div>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<div class="detail-item">
|
||||
<span class="detail-label">任务分组</span>
|
||||
<dict-tag :options="dict.type.sys_job_group" :value="form.jobGroup" />
|
||||
</div>
|
||||
</el-col>
|
||||
</el-row>
|
||||
<el-row class="detail-row">
|
||||
<el-col :span="24">
|
||||
<div class="detail-item"><span class="detail-label">日志信息</span><span class="detail-value">{{ form.jobMessage }}</span></div>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</div>
|
||||
|
||||
<!-- 调用目标 -->
|
||||
<div class="detail-card">
|
||||
<div class="detail-card-title"><i class="el-icon-s-operation"></i> 调用目标</div>
|
||||
<div class="code-body">
|
||||
<div class="code-wrap"><pre class="code-pre">{{ form.invokeTarget || '(无)' }}</pre></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 异常信息 -->
|
||||
<div class="detail-card" v-if="form.status == 1">
|
||||
<div class="detail-card-title error-title"><i class="el-icon-warning"></i> 异常信息</div>
|
||||
<div class="error-body"><div class="error-msg">{{ form.exceptionInfo }}</div></div>
|
||||
</div>
|
||||
|
||||
</template>
|
||||
|
||||
<template v-else>
|
||||
<!-- 任务配置 -->
|
||||
<div class="detail-card">
|
||||
<div class="detail-card-title"><i class="el-icon-setting"></i> 任务配置</div>
|
||||
<el-row class="detail-row">
|
||||
<el-col :span="12">
|
||||
<div class="detail-item"><span class="detail-label">任务编号</span><span class="detail-value">{{ form.jobId }}</span></div>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<div class="detail-item"><span class="detail-label">任务名称</span><span class="detail-value">{{ form.jobName }}</span></div>
|
||||
</el-col>
|
||||
</el-row>
|
||||
<el-row class="detail-row">
|
||||
<el-col :span="12">
|
||||
<div class="detail-item">
|
||||
<span class="detail-label">任务分组</span>
|
||||
<dict-tag :options="dict.type.sys_job_group" :value="form.jobGroup" />
|
||||
</div>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<div class="detail-item">
|
||||
<span class="detail-label">执行状态</span>
|
||||
<el-tag v-if="form.status == 0" type="success" size="small">正常</el-tag>
|
||||
<el-tag v-else type="info" size="small">暂停</el-tag>
|
||||
</div>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</div>
|
||||
|
||||
<!-- 调度信息 -->
|
||||
<div class="detail-card">
|
||||
<div class="detail-card-title"><i class="el-icon-date"></i> 调度信息</div>
|
||||
<el-row class="detail-row">
|
||||
<el-col :span="12">
|
||||
<div class="detail-item"><span class="detail-label">cron 表达式</span><span class="detail-value mono">{{ form.cronExpression }}</span></div>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<div class="detail-item"><span class="detail-label">下次执行时间</span><span class="detail-value">{{ parseTime(form.nextValidTime) }}</span></div>
|
||||
</el-col>
|
||||
</el-row>
|
||||
<el-row class="detail-row">
|
||||
<el-col :span="12">
|
||||
<div class="detail-item">
|
||||
<span class="detail-label">执行策略</span>
|
||||
<el-tag v-if="form.misfirePolicy == 0" type="info" size="small">默认策略</el-tag>
|
||||
<el-tag v-else-if="form.misfirePolicy == 1" type="warning" size="small">立即执行</el-tag>
|
||||
<el-tag v-else-if="form.misfirePolicy == 2" type="primary" size="small">执行一次</el-tag>
|
||||
<el-tag v-else-if="form.misfirePolicy == 3" type="danger" size="small">放弃执行</el-tag>
|
||||
</div>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<div class="detail-item">
|
||||
<span class="detail-label">并发执行</span>
|
||||
<el-tag v-if="form.concurrent == 0" type="success" size="small">允许</el-tag>
|
||||
<el-tag v-else type="danger" size="small">禁止</el-tag>
|
||||
</div>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</div>
|
||||
|
||||
<!-- 执行方法 -->
|
||||
<div class="detail-card">
|
||||
<div class="detail-card-title"><i class="el-icon-s-operation"></i> 执行方法</div>
|
||||
<div class="code-body">
|
||||
<div class="code-wrap"><pre class="code-pre">{{ form.invokeTarget || '(无)' }}</pre></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 元信息 -->
|
||||
<div class="detail-card">
|
||||
<div class="detail-card-title"><i class="el-icon-document"></i> 元信息</div>
|
||||
<el-row class="detail-row">
|
||||
<el-col :span="12">
|
||||
<div class="detail-item"><span class="detail-label">创建人</span><span class="detail-value">{{ form.createBy || '-' }}</span></div>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<div class="detail-item"><span class="detail-label">创建时间</span><span class="detail-value">{{ form.createTime }}</span></div>
|
||||
</el-col>
|
||||
</el-row>
|
||||
<el-row class="detail-row">
|
||||
<el-col :span="12">
|
||||
<div class="detail-item"><span class="detail-label">更新人</span><span class="detail-value">{{ form.updateBy || '-' }}</span></div>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<div class="detail-item"><span class="detail-label">更新时间</span><span class="detail-value">{{ form.updateTime || '-' }}</span></div>
|
||||
</el-col>
|
||||
</el-row>
|
||||
<el-row class="detail-row" v-if="form.remark">
|
||||
<el-col :span="24">
|
||||
<div class="detail-item"><span class="detail-label">备注</span><span class="detail-value">{{ form.remark }}</span></div>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</el-dialog>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
dicts: ['sys_job_group'],
|
||||
props: {
|
||||
visible: { type: Boolean, default: false },
|
||||
row: { type: Object, default: () => ({}) },
|
||||
// 'job' 任务详细 | 'log' 调度日志详细
|
||||
type: { type: String, default: 'job' }
|
||||
},
|
||||
computed: {
|
||||
form() { return this.row || {} },
|
||||
costTime() {
|
||||
if (!this.form.startTime || !this.form.endTime) return 0
|
||||
return new Date(this.form.endTime).getTime() - new Date(this.form.startTime).getTime()
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.detail-label {
|
||||
width: 80px;
|
||||
}
|
||||
</style>
|
||||
@@ -94,11 +94,7 @@
|
||||
<el-table v-loading="loading" :data="jobList" @selection-change="handleSelectionChange">
|
||||
<el-table-column type="selection" width="55" align="center" />
|
||||
<el-table-column label="任务编号" width="100" align="center" prop="jobId" />
|
||||
<el-table-column label="任务名称" align="center" :show-overflow-tooltip="true">
|
||||
<template slot-scope="scope">
|
||||
<a class="link-type" style="cursor:pointer" @click="handleView(scope.row)">{{ scope.row.jobName }}</a>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="任务名称" align="center" prop="jobName" :show-overflow-tooltip="true" />
|
||||
<el-table-column label="任务组名" align="center" prop="jobGroup">
|
||||
<template slot-scope="scope">
|
||||
<dict-tag :options="dict.type.sys_job_group" :value="scope.row.jobGroup"/>
|
||||
@@ -137,6 +133,8 @@
|
||||
<el-dropdown-menu slot="dropdown">
|
||||
<el-dropdown-item command="handleRun" icon="el-icon-caret-right"
|
||||
v-hasPermi="['monitor:job:changeStatus']">执行一次</el-dropdown-item>
|
||||
<el-dropdown-item command="handleView" icon="el-icon-view"
|
||||
v-hasPermi="['monitor:job:query']">任务详细</el-dropdown-item>
|
||||
<el-dropdown-item command="handleJobLog" icon="el-icon-s-operation"
|
||||
v-hasPermi="['monitor:job:query']">调度日志</el-dropdown-item>
|
||||
</el-dropdown-menu>
|
||||
@@ -243,17 +241,61 @@
|
||||
</el-dialog>
|
||||
|
||||
<!-- 任务日志详细 -->
|
||||
<job-detail :visible.sync="openView" :row="form" type="job" />
|
||||
<el-dialog title="任务详细" :visible.sync="openView" width="700px" append-to-body>
|
||||
<el-form ref="form" :model="form" label-width="120px" size="mini">
|
||||
<el-row>
|
||||
<el-col :span="12">
|
||||
<el-form-item label="任务编号:">{{ form.jobId }}</el-form-item>
|
||||
<el-form-item label="任务名称:">{{ form.jobName }}</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<el-form-item label="任务分组:">{{ jobGroupFormat(form) }}</el-form-item>
|
||||
<el-form-item label="创建时间:">{{ form.createTime }}</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<el-form-item label="cron表达式:">{{ form.cronExpression }}</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<el-form-item label="下次执行时间:">{{ parseTime(form.nextValidTime) }}</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="24">
|
||||
<el-form-item label="调用目标方法:">{{ form.invokeTarget }}</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<el-form-item label="任务状态:">
|
||||
<div v-if="form.status == 0">正常</div>
|
||||
<div v-else-if="form.status == 1">暂停</div>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<el-form-item label="是否并发:">
|
||||
<div v-if="form.concurrent == 0">允许</div>
|
||||
<div v-else-if="form.concurrent == 1">禁止</div>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<el-form-item label="执行策略:">
|
||||
<div v-if="form.misfirePolicy == 0">默认策略</div>
|
||||
<div v-else-if="form.misfirePolicy == 1">立即执行</div>
|
||||
<div v-else-if="form.misfirePolicy == 2">执行一次</div>
|
||||
<div v-else-if="form.misfirePolicy == 3">放弃执行</div>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</el-form>
|
||||
<div slot="footer" class="dialog-footer">
|
||||
<el-button @click="openView = false">关 闭</el-button>
|
||||
</div>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { listJob, getJob, delJob, addJob, updateJob, runJob, changeJobStatus } from "@/api/monitor/job"
|
||||
import JobDetail from './detail'
|
||||
import Crontab from '@/components/Crontab'
|
||||
|
||||
export default {
|
||||
components: { Crontab, JobDetail },
|
||||
components: { Crontab },
|
||||
name: "Job",
|
||||
dicts: ['sys_job_group', 'sys_job_status'],
|
||||
data() {
|
||||
@@ -431,17 +473,17 @@ export default {
|
||||
})
|
||||
},
|
||||
/** 提交按钮 */
|
||||
submitForm() {
|
||||
submitForm: function() {
|
||||
this.$refs["form"].validate(valid => {
|
||||
if (valid) {
|
||||
if (this.form.jobId != undefined) {
|
||||
updateJob(this.form).then(() => {
|
||||
updateJob(this.form).then(response => {
|
||||
this.$modal.msgSuccess("修改成功")
|
||||
this.open = false
|
||||
this.getList()
|
||||
})
|
||||
} else {
|
||||
addJob(this.form).then(() => {
|
||||
addJob(this.form).then(response => {
|
||||
this.$modal.msgSuccess("新增成功")
|
||||
this.open = false
|
||||
this.getList()
|
||||
|
||||
@@ -143,18 +143,48 @@
|
||||
@pagination="getList"
|
||||
/>
|
||||
|
||||
<job-log-detail :visible.sync="open" :row="form" type="log" />
|
||||
<!-- 调度日志详细 -->
|
||||
<el-dialog title="调度日志详细" :visible.sync="open" width="700px" append-to-body>
|
||||
<el-form ref="form" :model="form" label-width="100px" size="mini">
|
||||
<el-row>
|
||||
<el-col :span="12">
|
||||
<el-form-item label="日志序号:">{{ form.jobLogId }}</el-form-item>
|
||||
<el-form-item label="任务名称:">{{ form.jobName }}</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<el-form-item label="任务分组:">{{ form.jobGroup }}</el-form-item>
|
||||
<el-form-item label="执行时间:">{{ form.createTime }}</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="24">
|
||||
<el-form-item label="调用方法:">{{ form.invokeTarget }}</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="24">
|
||||
<el-form-item label="日志信息:">{{ form.jobMessage }}</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="24">
|
||||
<el-form-item label="执行状态:">
|
||||
<div v-if="form.status == 0">正常</div>
|
||||
<div v-else-if="form.status == 1">失败</div>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="24">
|
||||
<el-form-item label="异常信息:" v-if="form.status == 1">{{ form.exceptionInfo }}</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</el-form>
|
||||
<div slot="footer" class="dialog-footer">
|
||||
<el-button @click="open = false">关 闭</el-button>
|
||||
</div>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { getJob} from "@/api/monitor/job"
|
||||
import { listJobLog, delJobLog, cleanJobLog } from "@/api/monitor/jobLog"
|
||||
import JobLogDetail from './detail'
|
||||
|
||||
export default {
|
||||
name: "JobLog",
|
||||
components: { JobLogDetail },
|
||||
dicts: ['sys_common_status', 'sys_job_group'],
|
||||
data() {
|
||||
return {
|
||||
|
||||
@@ -1,147 +0,0 @@
|
||||
<template>
|
||||
<el-dialog title="操作日志详细" :visible.sync="visible" width="780px" append-to-body @close="$emit('update:visible', false)">
|
||||
<div class="detail-wrap">
|
||||
<!-- 基本信息 -->
|
||||
<div class="detail-card">
|
||||
<div class="detail-card-title"><i class="el-icon-info"></i> 基本信息</div>
|
||||
<el-row class="detail-row">
|
||||
<el-col :span="12">
|
||||
<div class="detail-item"><span class="detail-label">操作模块</span><span class="detail-value">{{ form.title }}</span></div>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<div class="detail-item"><span class="detail-label">业务类型</span><span class="detail-value">{{ typeLabel }}</span></div>
|
||||
</el-col>
|
||||
</el-row>
|
||||
<el-row class="detail-row">
|
||||
<el-col :span="12">
|
||||
<div class="detail-item"><span class="detail-label">操作时间</span><span class="detail-value">{{ form.operTime }}</span></div>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<div class="detail-item">
|
||||
<span class="detail-label">执行状态</span>
|
||||
<el-tag v-if="form.status === 0" type="success" size="small"><i class="el-icon-check"></i> 正常</el-tag>
|
||||
<el-tag v-else type="danger" size="small"><i class="el-icon-close"></i> 异常</el-tag>
|
||||
</div>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</div>
|
||||
|
||||
<!-- 操作人员 -->
|
||||
<div class="detail-card">
|
||||
<div class="detail-card-title"><i class="el-icon-user"></i> 操作人员</div>
|
||||
<el-row class="detail-row">
|
||||
<el-col :span="12">
|
||||
<div class="detail-item"><span class="detail-label">操作人员</span><span class="detail-value">{{ form.operName }}</span></div>
|
||||
</el-col>
|
||||
<el-col :span="12" v-if="form.deptName">
|
||||
<div class="detail-item"><span class="detail-label">所属部门</span><span class="detail-value">{{ form.deptName }}</span></div>
|
||||
</el-col>
|
||||
</el-row>
|
||||
<el-row class="detail-row">
|
||||
<el-col :span="24">
|
||||
<div class="detail-item">
|
||||
<span class="detail-label">操作地址</span>
|
||||
<span class="detail-value">{{ form.operIp }} <span class="detail-location">{{ form.operLocation }}</span></span>
|
||||
</div>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</div>
|
||||
|
||||
<!-- 请求信息 -->
|
||||
<div class="detail-card">
|
||||
<div class="detail-card-title"><i class="el-icon-sort"></i> 请求信息</div>
|
||||
<el-row class="detail-row">
|
||||
<el-col :span="24">
|
||||
<div class="detail-item">
|
||||
<span class="detail-label">请求地址</span>
|
||||
<span class="detail-value">
|
||||
<span :class="'method-tag method-' + form.requestMethod">{{ form.requestMethod }}</span>
|
||||
{{ form.operUrl }}
|
||||
</span>
|
||||
</div>
|
||||
</el-col>
|
||||
</el-row>
|
||||
<el-row class="detail-row">
|
||||
<el-col :span="24">
|
||||
<div class="detail-item"><span class="detail-label">操作方法</span><span class="detail-value mono">{{ form.method }}</span></div>
|
||||
</el-col>
|
||||
</el-row>
|
||||
<el-row class="detail-row">
|
||||
<el-col :span="12">
|
||||
<div class="detail-item"><span class="detail-label">消耗时间</span><span class="detail-value">{{ form.costTime }} 毫秒</span></div>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</div>
|
||||
|
||||
<!-- 请求参数 -->
|
||||
<div class="detail-card">
|
||||
<div class="detail-card-title"><i class="el-icon-upload2"></i> 请求参数</div>
|
||||
<div class="code-body">
|
||||
<div class="code-wrap">
|
||||
<div class="code-action">
|
||||
<el-button size="mini" icon="el-icon-copy-document" @click="copyText(form.operParam)">复制</el-button>
|
||||
</div>
|
||||
<pre class="code-pre">{{ formatJson(form.operParam) }}</pre>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 返回参数 -->
|
||||
<div class="detail-card">
|
||||
<div class="detail-card-title"><i class="el-icon-download"></i> 返回参数</div>
|
||||
<div class="code-body">
|
||||
<div class="code-wrap">
|
||||
<div class="code-action">
|
||||
<el-button size="mini" icon="el-icon-copy-document" @click="copyText(form.jsonResult)">复制</el-button>
|
||||
</div>
|
||||
<pre class="code-pre">{{ formatJson(form.jsonResult) }}</pre>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 异常信息 -->
|
||||
<div class="detail-card" v-if="form.status !== 0">
|
||||
<div class="detail-card-title error-title"><i class="el-icon-warning"></i> 异常信息</div>
|
||||
<div class="error-body">
|
||||
<div class="error-msg">{{ form.errorMsg }}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</el-dialog>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'OperlogDetail',
|
||||
dicts: ['sys_oper_type'],
|
||||
props: {
|
||||
visible: { type: Boolean, default: false },
|
||||
row: { type: Object, default: () => ({}) }
|
||||
},
|
||||
computed: {
|
||||
form() { return this.row || {} },
|
||||
typeLabel() { return this.selectDictLabel(this.dict.type.sys_oper_type, this.form.businessType) || '-' }
|
||||
},
|
||||
methods: {
|
||||
formatJson(str) {
|
||||
if (!str) return '(无数据)'
|
||||
try { return JSON.stringify(JSON.parse(str), null, 2) } catch { return str }
|
||||
},
|
||||
copyText(str) {
|
||||
const text = this.formatJson(str)
|
||||
if (navigator.clipboard) {
|
||||
navigator.clipboard.writeText(text).then(() => this.$message({ message: '已复制', type: 'success', duration: 1500 }))
|
||||
} else {
|
||||
const ta = document.createElement('textarea')
|
||||
ta.value = text
|
||||
document.body.appendChild(ta)
|
||||
ta.select()
|
||||
document.execCommand('copy')
|
||||
document.body.removeChild(ta)
|
||||
this.$message({ message: '已复制', type: 'success', duration: 1500 })
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@@ -144,7 +144,7 @@
|
||||
size="mini"
|
||||
type="text"
|
||||
icon="el-icon-view"
|
||||
@click="handleDetail(scope.row,scope.index)"
|
||||
@click="handleView(scope.row,scope.index)"
|
||||
v-hasPermi="['monitor:operlog:query']"
|
||||
>详细</el-button>
|
||||
</template>
|
||||
@@ -159,17 +159,58 @@
|
||||
@pagination="getList"
|
||||
/>
|
||||
|
||||
<operlog-detail :visible.sync="detailVisible" :row="detailRow" />
|
||||
<!-- 操作日志详细 -->
|
||||
<el-dialog title="操作日志详细" :visible.sync="open" width="800px" append-to-body>
|
||||
<el-form ref="form" :model="form" label-width="100px" size="mini">
|
||||
<el-row>
|
||||
<el-col :span="12">
|
||||
<el-form-item label="操作模块:">{{ form.title }} / {{ typeFormat(form) }}</el-form-item>
|
||||
<el-form-item
|
||||
label="登录信息:"
|
||||
>{{ form.operName }} / {{ form.operIp }} / {{ form.operLocation }}</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<el-form-item label="请求地址:">{{ form.operUrl }}</el-form-item>
|
||||
<el-form-item label="请求方式:">{{ form.requestMethod }}</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="24">
|
||||
<el-form-item label="操作方法:">{{ form.method }}</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="24">
|
||||
<el-form-item label="请求参数:">{{ form.operParam }}</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="24">
|
||||
<el-form-item label="返回参数:">{{ form.jsonResult }}</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="8">
|
||||
<el-form-item label="操作状态:">
|
||||
<div v-if="form.status === 0">正常</div>
|
||||
<div v-else-if="form.status === 1">失败</div>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="8">
|
||||
<el-form-item label="消耗时间:">{{ form.costTime }}毫秒</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="8">
|
||||
<el-form-item label="操作时间:">{{ parseTime(form.operTime) }}</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="24">
|
||||
<el-form-item label="异常信息:" v-if="form.status === 1">{{ form.errorMsg }}</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</el-form>
|
||||
<div slot="footer" class="dialog-footer">
|
||||
<el-button @click="open = false">关 闭</el-button>
|
||||
</div>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import OperlogDetail from './detail'
|
||||
import { list, delOperlog, cleanOperlog } from "@/api/monitor/operlog"
|
||||
|
||||
export default {
|
||||
name: "Operlog",
|
||||
components: { OperlogDetail },
|
||||
dicts: ['sys_oper_type', 'sys_common_status'],
|
||||
data() {
|
||||
return {
|
||||
@@ -186,8 +227,7 @@ export default {
|
||||
// 表格数据
|
||||
list: [],
|
||||
// 是否显示弹出层
|
||||
detailVisible: false,
|
||||
detailRow: {},
|
||||
open: false,
|
||||
// 日期范围
|
||||
dateRange: [],
|
||||
// 默认排序
|
||||
@@ -220,10 +260,9 @@ export default {
|
||||
}
|
||||
)
|
||||
},
|
||||
// 详细按钮操作
|
||||
handleDetail(row) {
|
||||
this.detailRow = row
|
||||
this.detailVisible = true
|
||||
// 操作日志类型字典翻译
|
||||
typeFormat(row, column) {
|
||||
return this.selectDictLabel(this.dict.type.sys_oper_type, row.businessType)
|
||||
},
|
||||
/** 搜索按钮操作 */
|
||||
handleQuery() {
|
||||
@@ -248,6 +287,11 @@ export default {
|
||||
this.queryParams.isAsc = column.order
|
||||
this.getList()
|
||||
},
|
||||
/** 详细按钮操作 */
|
||||
handleView(row) {
|
||||
this.open = true
|
||||
this.form = row
|
||||
},
|
||||
/** 删除按钮操作 */
|
||||
handleDelete(row) {
|
||||
const operIds = row.operId || this.ids
|
||||
|
||||
@@ -128,7 +128,7 @@ export default {
|
||||
this.$refs.registerForm.validate(valid => {
|
||||
if (valid) {
|
||||
this.loading = true
|
||||
register(this.registerForm).then(() => {
|
||||
register(this.registerForm).then(res => {
|
||||
const username = this.registerForm.username
|
||||
this.$alert("<font color='red'>恭喜你,您的账号 " + username + " 注册成功!</font>", '系统提示', {
|
||||
dangerouslyUseHTMLString: true,
|
||||
|
||||
@@ -283,7 +283,7 @@ export default {
|
||||
// 多选框选中数据
|
||||
handleSelectionChange(selection) {
|
||||
this.ids = selection.map(item => item.configId)
|
||||
this.single = selection.length != 1
|
||||
this.single = selection.length!=1
|
||||
this.multiple = !selection.length
|
||||
},
|
||||
/** 修改按钮操作 */
|
||||
@@ -297,17 +297,17 @@ export default {
|
||||
})
|
||||
},
|
||||
/** 提交按钮 */
|
||||
submitForm() {
|
||||
submitForm: function() {
|
||||
this.$refs["form"].validate(valid => {
|
||||
if (valid) {
|
||||
if (this.form.configId != undefined) {
|
||||
updateConfig(this.form).then(() => {
|
||||
updateConfig(this.form).then(response => {
|
||||
this.$modal.msgSuccess("修改成功")
|
||||
this.open = false
|
||||
this.getList()
|
||||
})
|
||||
} else {
|
||||
addConfig(this.form).then(() => {
|
||||
addConfig(this.form).then(response => {
|
||||
this.$modal.msgSuccess("新增成功")
|
||||
this.open = false
|
||||
this.getList()
|
||||
|
||||
@@ -36,16 +36,6 @@
|
||||
v-hasPermi="['system:dept:add']"
|
||||
>新增</el-button>
|
||||
</el-col>
|
||||
<el-col :span="1.5">
|
||||
<el-button
|
||||
type="warning"
|
||||
plain
|
||||
icon="el-icon-check"
|
||||
size="mini"
|
||||
@click="handleSaveSort"
|
||||
v-hasPermi="['system:dept:edit']"
|
||||
>保存排序</el-button>
|
||||
</el-col>
|
||||
<el-col :span="1.5">
|
||||
<el-button
|
||||
type="info"
|
||||
@@ -67,11 +57,7 @@
|
||||
:tree-props="{children: 'children', hasChildren: 'hasChildren'}"
|
||||
>
|
||||
<el-table-column prop="deptName" label="部门名称" width="260"></el-table-column>
|
||||
<el-table-column prop="orderNum" label="排序" width="200">
|
||||
<template slot-scope="scope">
|
||||
<el-input-number v-model="scope.row.orderNum" controls-position="right" :min="0" size="mini" style="width: 88px" />
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="orderNum" label="排序" width="200"></el-table-column>
|
||||
<el-table-column prop="status" label="状态" width="100">
|
||||
<template slot-scope="scope">
|
||||
<dict-tag :options="dict.type.sys_normal_disable" :value="scope.row.status"/>
|
||||
@@ -172,7 +158,7 @@
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { listDept, getDept, delDept, addDept, updateDept, updateDeptSort, listDeptExcludeChild } from "@/api/system/dept"
|
||||
import { listDept, getDept, delDept, addDept, updateDept, listDeptExcludeChild } from "@/api/system/dept"
|
||||
import Treeselect from "@riophae/vue-treeselect"
|
||||
import "@riophae/vue-treeselect/dist/vue-treeselect.css"
|
||||
|
||||
@@ -198,8 +184,6 @@ export default {
|
||||
isExpandAll: true,
|
||||
// 重新渲染表格状态
|
||||
refreshTable: true,
|
||||
// 记录原始排序,用于对比变更
|
||||
originalOrders: {},
|
||||
// 查询参数
|
||||
queryParams: {
|
||||
deptName: undefined,
|
||||
@@ -244,8 +228,6 @@ export default {
|
||||
this.loading = true
|
||||
listDept(this.queryParams).then(response => {
|
||||
this.deptList = this.handleTree(response.data, "deptId")
|
||||
// 记录原始排序值
|
||||
this.recordOriginalOrders(this.deptList)
|
||||
this.loading = false
|
||||
})
|
||||
},
|
||||
@@ -325,17 +307,17 @@ export default {
|
||||
})
|
||||
},
|
||||
/** 提交按钮 */
|
||||
submitForm() {
|
||||
submitForm: function() {
|
||||
this.$refs["form"].validate(valid => {
|
||||
if (valid) {
|
||||
if (this.form.deptId != undefined) {
|
||||
updateDept(this.form).then(() => {
|
||||
updateDept(this.form).then(response => {
|
||||
this.$modal.msgSuccess("修改成功")
|
||||
this.open = false
|
||||
this.getList()
|
||||
})
|
||||
} else {
|
||||
addDept(this.form).then(() => {
|
||||
addDept(this.form).then(response => {
|
||||
this.$modal.msgSuccess("新增成功")
|
||||
this.open = false
|
||||
this.getList()
|
||||
@@ -344,40 +326,6 @@ export default {
|
||||
}
|
||||
})
|
||||
},
|
||||
/** 递归记录原始排序 */
|
||||
recordOriginalOrders(list) {
|
||||
list.forEach(item => {
|
||||
this.originalOrders[item.deptId] = item.orderNum
|
||||
if (item.children && item.children.length) {
|
||||
this.recordOriginalOrders(item.children)
|
||||
}
|
||||
})
|
||||
},
|
||||
/** 保存排序 */
|
||||
handleSaveSort() {
|
||||
const changedDeptIds = []
|
||||
const changedOrderNums = []
|
||||
const collectChanged = (list) => {
|
||||
list.forEach(item => {
|
||||
if (String(this.originalOrders[item.deptId]) !== String(item.orderNum)) {
|
||||
changedDeptIds.push(item.deptId)
|
||||
changedOrderNums.push(item.orderNum)
|
||||
}
|
||||
if (item.children && item.children.length) {
|
||||
collectChanged(item.children)
|
||||
}
|
||||
})
|
||||
}
|
||||
collectChanged(this.deptList)
|
||||
if (changedDeptIds.length === 0) {
|
||||
this.$modal.msgWarning("未检测到排序修改")
|
||||
return
|
||||
}
|
||||
updateDeptSort({ deptIds: changedDeptIds.join(","), orderNums: changedOrderNums.join(",") }).then(() => {
|
||||
this.$modal.msgSuccess("排序保存成功")
|
||||
this.recordOriginalOrders(this.deptList)
|
||||
})
|
||||
},
|
||||
/** 删除按钮操作 */
|
||||
handleDelete(row) {
|
||||
this.$modal.confirm('是否确认删除名称为"' + row.deptName + '"的数据项?').then(function() {
|
||||
|
||||
@@ -345,7 +345,7 @@ export default {
|
||||
// 多选框选中数据
|
||||
handleSelectionChange(selection) {
|
||||
this.ids = selection.map(item => item.dictCode)
|
||||
this.single = selection.length != 1
|
||||
this.single = selection.length!=1
|
||||
this.multiple = !selection.length
|
||||
},
|
||||
/** 修改按钮操作 */
|
||||
@@ -359,18 +359,18 @@ export default {
|
||||
})
|
||||
},
|
||||
/** 提交按钮 */
|
||||
submitForm() {
|
||||
submitForm: function() {
|
||||
this.$refs["form"].validate(valid => {
|
||||
if (valid) {
|
||||
if (this.form.dictCode != undefined) {
|
||||
updateData(this.form).then(() => {
|
||||
updateData(this.form).then(response => {
|
||||
this.$store.dispatch('dict/removeDict', this.queryParams.dictType)
|
||||
this.$modal.msgSuccess("修改成功")
|
||||
this.open = false
|
||||
this.getList()
|
||||
})
|
||||
} else {
|
||||
addData(this.form).then(() => {
|
||||
addData(this.form).then(response => {
|
||||
this.$store.dispatch('dict/removeDict', this.queryParams.dictType)
|
||||
this.$modal.msgSuccess("新增成功")
|
||||
this.open = false
|
||||
|
||||
@@ -1,203 +0,0 @@
|
||||
<template>
|
||||
<el-drawer :title="drawerTitle" :visible.sync="localVisible" direction="rtl" size="700px" append-to-body>
|
||||
<div class="drawer-wrap">
|
||||
<div v-if="loading" class="drawer-loading">
|
||||
<i class="el-icon-loading"></i>
|
||||
<span>加载中...</span>
|
||||
</div>
|
||||
<div v-else-if="!dataList.length" class="drawer-empty">
|
||||
<i class="el-icon-document"></i>
|
||||
<div>暂无字典数据</div>
|
||||
</div>
|
||||
<template v-else>
|
||||
<el-row :gutter="12" class="stat-row">
|
||||
<el-col :span="disabledCount > 0 ? 8 : 12">
|
||||
<div class="stat-card">
|
||||
<div class="stat-num">{{ dataList.length }}</div>
|
||||
<div class="stat-label">共计条目</div>
|
||||
</div>
|
||||
</el-col>
|
||||
<el-col :span="disabledCount > 0 ? 8 : 12">
|
||||
<div class="stat-card">
|
||||
<div class="stat-num success">{{ normalCount }}</div>
|
||||
<div class="stat-label">正常</div>
|
||||
</div>
|
||||
</el-col>
|
||||
<el-col :span="8" v-if="disabledCount > 0">
|
||||
<div class="stat-card">
|
||||
<div class="stat-num danger">{{ disabledCount }}</div>
|
||||
<div class="stat-label">停用</div>
|
||||
</div>
|
||||
</el-col>
|
||||
</el-row>
|
||||
<div v-for="item in dataList" :key="item.dictCode" class="dict-item">
|
||||
<div class="dict-cell">
|
||||
<div class="dict-cell-key">标签</div>
|
||||
<div class="dict-cell-val">
|
||||
<el-tag v-if="item.listClass && item.listClass !== 'default'" :type="item.listClass === 'primary' ? '' : item.listClass" size="small">{{ item.dictLabel }}</el-tag>
|
||||
<span v-else>{{ item.dictLabel }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="dict-cell">
|
||||
<div class="dict-cell-key">键值</div>
|
||||
<div class="dict-cell-val">{{ item.dictValue }}</div>
|
||||
</div>
|
||||
<div class="dict-cell">
|
||||
<div class="dict-cell-key">状态</div>
|
||||
<div class="dict-cell-val">
|
||||
<el-tag :type="item.status === '0' ? 'success' : 'danger'" size="small">
|
||||
{{ item.status === '0' ? '正常' : '停用' }}
|
||||
</el-tag>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</el-drawer>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { listData } from '@/api/system/dict/data'
|
||||
|
||||
export default {
|
||||
props: {
|
||||
visible: { type: Boolean, default: false },
|
||||
row: { type: Object, default: () => ({}) }
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
localVisible: false,
|
||||
loading: false,
|
||||
dataList: []
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
drawerTitle() {
|
||||
return this.row.dictName + ' ' + (this.row.dictType || '')
|
||||
},
|
||||
normalCount() {
|
||||
return this.dataList.filter(r => r.status === '0').length
|
||||
},
|
||||
disabledCount() {
|
||||
return this.dataList.filter(r => r.status !== '0').length
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
visible(val) {
|
||||
this.localVisible = val
|
||||
},
|
||||
localVisible(val) {
|
||||
this.$emit('update:visible', val)
|
||||
if (val) {
|
||||
this.loadData()
|
||||
} else {
|
||||
this.dataList = []
|
||||
}
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
loadData() {
|
||||
if (!this.row || !this.row.dictType) return
|
||||
this.loading = true
|
||||
this.dataList = []
|
||||
listData({ dictType: this.row.dictType, pageSize: 100, pageNum: 1 }).then(response => {
|
||||
this.dataList = response.rows || []
|
||||
}).catch(() => {}).finally(() => {
|
||||
this.loading = false
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.drawer-wrap {
|
||||
padding: 0 20px 20px;
|
||||
}
|
||||
|
||||
.drawer-loading {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 120px;
|
||||
color: #aaa;
|
||||
font-size: 13px;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.drawer-empty {
|
||||
text-align: center;
|
||||
color: #bbb;
|
||||
padding: 60px 0;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.drawer-empty i {
|
||||
font-size: 36px;
|
||||
display: block;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.stat-row {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.stat-card {
|
||||
background: #f7f9fb;
|
||||
border: 1px solid #e8ecf0;
|
||||
border-radius: 6px;
|
||||
padding: 10px 14px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.stat-num {
|
||||
font-size: 22px;
|
||||
font-weight: 700;
|
||||
color: #2c3e50;
|
||||
}
|
||||
|
||||
.stat-num.success { color: #27ae60; }
|
||||
.stat-num.danger { color: #e74c3c; }
|
||||
|
||||
.stat-label {
|
||||
font-size: 11px;
|
||||
color: #95a5a6;
|
||||
margin-top: 4px;
|
||||
}
|
||||
|
||||
.dict-item {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr 1fr;
|
||||
border: 1px solid #e8ecf0;
|
||||
border-radius: 6px;
|
||||
overflow: hidden;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.dict-cell {
|
||||
display: grid;
|
||||
grid-template-columns: 70px 1fr;
|
||||
border-right: 1px solid #f0f4f8;
|
||||
}
|
||||
|
||||
.dict-cell:last-child {
|
||||
border-right: 0;
|
||||
}
|
||||
|
||||
.dict-cell-key {
|
||||
padding: 9px 14px;
|
||||
font-size: 12px;
|
||||
color: #888;
|
||||
background: #f7f9fb;
|
||||
border-right: 1px solid #f0f4f8;
|
||||
}
|
||||
|
||||
.dict-cell-val {
|
||||
padding: 9px 14px;
|
||||
font-size: 13px;
|
||||
color: #2c3e50;
|
||||
word-break: break-all;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
</style>
|
||||
@@ -113,7 +113,9 @@
|
||||
<el-table-column label="字典名称" align="center" prop="dictName" :show-overflow-tooltip="true" />
|
||||
<el-table-column label="字典类型" align="center" :show-overflow-tooltip="true">
|
||||
<template slot-scope="scope">
|
||||
<a class="link-type" style="cursor:pointer" @click="handleViewData(scope.row)">{{ scope.row.dictType }}</a>
|
||||
<router-link :to="'/system/dict-data/index/' + scope.row.dictId" class="link-type">
|
||||
<span>{{ scope.row.dictType }}</span>
|
||||
</router-link>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="状态" align="center" prop="status">
|
||||
@@ -136,13 +138,6 @@
|
||||
@click="handleUpdate(scope.row)"
|
||||
v-hasPermi="['system:dict:edit']"
|
||||
>修改</el-button>
|
||||
<el-button
|
||||
size="mini"
|
||||
type="text"
|
||||
icon="el-icon-s-operation"
|
||||
@click="handleDataList(scope.row)"
|
||||
v-hasPermi="['system:dict:edit']"
|
||||
>列表</el-button>
|
||||
<el-button
|
||||
size="mini"
|
||||
type="text"
|
||||
@@ -164,18 +159,12 @@
|
||||
|
||||
<!-- 添加或修改参数配置对话框 -->
|
||||
<el-dialog :title="title" :visible.sync="open" width="500px" append-to-body>
|
||||
<el-form ref="form" :model="form" :rules="rules" label-width="100px">
|
||||
<el-form ref="form" :model="form" :rules="rules" label-width="80px">
|
||||
<el-form-item label="字典名称" prop="dictName">
|
||||
<el-input v-model="form.dictName" placeholder="请输入字典名称" />
|
||||
</el-form-item>
|
||||
<el-form-item prop="dictType">
|
||||
<el-input v-model="form.dictType" placeholder="请输入字典类型" maxlength="100" />
|
||||
<span slot="label">
|
||||
<el-tooltip content="数据存储中的Key值,如:sys_user_sex" placement="top">
|
||||
<i class="el-icon-question"></i>
|
||||
</el-tooltip>
|
||||
字典类型
|
||||
</span>
|
||||
<el-form-item label="字典类型" prop="dictType">
|
||||
<el-input v-model="form.dictType" placeholder="请输入字典类型" />
|
||||
</el-form-item>
|
||||
<el-form-item label="状态" prop="status">
|
||||
<el-radio-group v-model="form.status">
|
||||
@@ -195,18 +184,14 @@
|
||||
<el-button @click="cancel">取 消</el-button>
|
||||
</div>
|
||||
</el-dialog>
|
||||
|
||||
<dict-data-drawer :visible.sync="drawerVisible" :row="drawerRow" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import DictDataDrawer from './detail'
|
||||
import { listType, getType, delType, addType, updateType, refreshCache } from "@/api/system/dict/type"
|
||||
|
||||
export default {
|
||||
name: "Dict",
|
||||
components: { DictDataDrawer },
|
||||
dicts: ['sys_normal_disable'],
|
||||
data() {
|
||||
return {
|
||||
@@ -228,10 +213,6 @@ export default {
|
||||
title: "",
|
||||
// 是否显示弹出层
|
||||
open: false,
|
||||
// 字典数据抽屉状态
|
||||
drawerVisible: false,
|
||||
// 字典数据信息
|
||||
drawerRow: {},
|
||||
// 日期范围
|
||||
dateRange: [],
|
||||
// 查询参数
|
||||
@@ -305,18 +286,9 @@ export default {
|
||||
// 多选框选中数据
|
||||
handleSelectionChange(selection) {
|
||||
this.ids = selection.map(item => item.dictId)
|
||||
this.single = selection.length != 1
|
||||
this.single = selection.length!=1
|
||||
this.multiple = !selection.length
|
||||
},
|
||||
/** 字典数据抽屉显示信息 */
|
||||
handleViewData(row) {
|
||||
this.drawerRow = row
|
||||
this.drawerVisible = true
|
||||
},
|
||||
/** 字典数据列表页面 */
|
||||
handleDataList(row) {
|
||||
this.$tab.openPage("字典数据", '/system/dict-data/index/' + row.dictId)
|
||||
},
|
||||
/** 修改按钮操作 */
|
||||
handleUpdate(row) {
|
||||
this.reset()
|
||||
@@ -328,17 +300,17 @@ export default {
|
||||
})
|
||||
},
|
||||
/** 提交按钮 */
|
||||
submitForm() {
|
||||
submitForm: function() {
|
||||
this.$refs["form"].validate(valid => {
|
||||
if (valid) {
|
||||
if (this.form.dictId != undefined) {
|
||||
updateType(this.form).then(() => {
|
||||
updateType(this.form).then(response => {
|
||||
this.$modal.msgSuccess("修改成功")
|
||||
this.open = false
|
||||
this.getList()
|
||||
})
|
||||
} else {
|
||||
addType(this.form).then(() => {
|
||||
addType(this.form).then(response => {
|
||||
this.$modal.msgSuccess("新增成功")
|
||||
this.open = false
|
||||
this.getList()
|
||||
|
||||
@@ -36,16 +36,6 @@
|
||||
v-hasPermi="['system:menu:add']"
|
||||
>新增</el-button>
|
||||
</el-col>
|
||||
<el-col :span="1.5">
|
||||
<el-button
|
||||
type="warning"
|
||||
plain
|
||||
icon="el-icon-check"
|
||||
size="mini"
|
||||
@click="handleSaveSort"
|
||||
v-hasPermi="['system:menu:edit']"
|
||||
>保存排序</el-button>
|
||||
</el-col>
|
||||
<el-col :span="1.5">
|
||||
<el-button
|
||||
type="info"
|
||||
@@ -66,31 +56,23 @@
|
||||
:default-expand-all="isExpandAll"
|
||||
:tree-props="{children: 'children', hasChildren: 'hasChildren'}"
|
||||
>
|
||||
<el-table-column prop="menuName" label="菜单名称" :show-overflow-tooltip="true" width="220">
|
||||
<el-table-column prop="menuName" label="菜单名称" :show-overflow-tooltip="true" width="160"></el-table-column>
|
||||
<el-table-column prop="icon" label="图标" align="center" width="100">
|
||||
<template slot-scope="scope">
|
||||
<svg-icon :icon-class="scope.row.icon" />
|
||||
<span class="ml5">{{ scope.row.menuName }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="menuName" label="类型" :show-overflow-tooltip="true" width="100">
|
||||
<template slot-scope="scope">
|
||||
<el-tag v-if="scope.row.menuType === 'M' && scope.row.isFrame === '0'" type="danger" size="small">外链</el-tag>
|
||||
<el-tag v-else-if="scope.row.menuType === 'M'" type="primary" size="small">目录</el-tag>
|
||||
<el-tag v-else-if="scope.row.menuType === 'C' && scope.row.isFrame === '0'" type="danger" size="small">外链</el-tag>
|
||||
<el-tag v-else-if="scope.row.menuType === 'C'" type="success" size="small">菜单</el-tag>
|
||||
<el-tag v-else-if="scope.row.menuType === 'F'" type="warning" size="small">按钮</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="orderNum" label="排序" width="200">
|
||||
<template slot-scope="scope">
|
||||
<el-input-number v-model="scope.row.orderNum" controls-position="right" :min="0" size="mini" style="width: 88px" />
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="perms" label="权限标识" :show-overflow-tooltip="true" />
|
||||
<el-table-column prop="component" label="组件路径" :show-overflow-tooltip="true" />
|
||||
<el-table-column prop="orderNum" label="排序" width="60"></el-table-column>
|
||||
<el-table-column prop="perms" label="权限标识" :show-overflow-tooltip="true"></el-table-column>
|
||||
<el-table-column prop="component" label="组件路径" :show-overflow-tooltip="true"></el-table-column>
|
||||
<el-table-column prop="status" label="状态" width="80">
|
||||
<template slot-scope="scope">
|
||||
<dict-tag :options="dict.type.sys_normal_disable" :value="scope.row.status" />
|
||||
<dict-tag :options="dict.type.sys_normal_disable" :value="scope.row.status"/>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="创建时间" align="center" prop="createTime">
|
||||
<template slot-scope="scope">
|
||||
<span>{{ parseTime(scope.row.createTime) }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" align="center" class-name="small-padding fixed-width">
|
||||
@@ -317,7 +299,7 @@
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { listMenu, getMenu, delMenu, addMenu, updateMenu, updateMenuSort } from "@/api/system/menu"
|
||||
import { listMenu, getMenu, delMenu, addMenu, updateMenu } from "@/api/system/menu"
|
||||
import Treeselect from "@riophae/vue-treeselect"
|
||||
import "@riophae/vue-treeselect/dist/vue-treeselect.css"
|
||||
import IconSelect from "@/components/IconSelect"
|
||||
@@ -344,8 +326,6 @@ export default {
|
||||
isExpandAll: false,
|
||||
// 重新渲染表格状态
|
||||
refreshTable: true,
|
||||
// 记录原始排序,用于对比变更
|
||||
originalOrders: {},
|
||||
// 查询参数
|
||||
queryParams: {
|
||||
menuName: undefined,
|
||||
@@ -380,8 +360,6 @@ export default {
|
||||
this.loading = true
|
||||
listMenu(this.queryParams).then(response => {
|
||||
this.menuList = this.handleTree(response.data, "menuId")
|
||||
// 记录原始排序值
|
||||
this.recordOriginalOrders(this.menuList)
|
||||
this.loading = false
|
||||
})
|
||||
},
|
||||
@@ -466,17 +444,17 @@ export default {
|
||||
})
|
||||
},
|
||||
/** 提交按钮 */
|
||||
submitForm() {
|
||||
submitForm: function() {
|
||||
this.$refs["form"].validate(valid => {
|
||||
if (valid) {
|
||||
if (this.form.menuId != undefined) {
|
||||
updateMenu(this.form).then(() => {
|
||||
updateMenu(this.form).then(response => {
|
||||
this.$modal.msgSuccess("修改成功")
|
||||
this.open = false
|
||||
this.getList()
|
||||
})
|
||||
} else {
|
||||
addMenu(this.form).then(() => {
|
||||
addMenu(this.form).then(response => {
|
||||
this.$modal.msgSuccess("新增成功")
|
||||
this.open = false
|
||||
this.getList()
|
||||
@@ -485,40 +463,6 @@ export default {
|
||||
}
|
||||
})
|
||||
},
|
||||
/** 递归记录原始排序 */
|
||||
recordOriginalOrders(list) {
|
||||
list.forEach(item => {
|
||||
this.originalOrders[item.menuId] = item.orderNum
|
||||
if (item.children && item.children.length) {
|
||||
this.recordOriginalOrders(item.children)
|
||||
}
|
||||
})
|
||||
},
|
||||
/** 保存排序 */
|
||||
handleSaveSort() {
|
||||
const changedMenuIds = []
|
||||
const changedOrderNums = []
|
||||
const collectChanged = (list) => {
|
||||
list.forEach(item => {
|
||||
if (String(this.originalOrders[item.menuId]) !== String(item.orderNum)) {
|
||||
changedMenuIds.push(item.menuId)
|
||||
changedOrderNums.push(item.orderNum)
|
||||
}
|
||||
if (item.children && item.children.length) {
|
||||
collectChanged(item.children)
|
||||
}
|
||||
})
|
||||
}
|
||||
collectChanged(this.menuList)
|
||||
if (changedMenuIds.length === 0) {
|
||||
this.$modal.msgWarning("未检测到排序修改")
|
||||
return
|
||||
}
|
||||
updateMenuSort({ menuIds: changedMenuIds.join(","), orderNums: changedOrderNums.join(",") }).then(() => {
|
||||
this.$modal.msgSuccess("排序保存成功")
|
||||
this.recordOriginalOrders(this.menuList)
|
||||
})
|
||||
},
|
||||
/** 删除按钮操作 */
|
||||
handleDelete(row) {
|
||||
this.$modal.confirm('是否确认删除名称为"' + row.menuName + '"的数据项?').then(function() {
|
||||
|
||||
@@ -1,109 +0,0 @@
|
||||
<template>
|
||||
<el-dialog :title="`「${noticeTitle}」已读用户`" :visible.sync="visible" width="760px" top="6vh" append-to-body @close="handleClose">
|
||||
<el-form :model="queryParams" ref="queryForm" size="small" :inline="true" style="margin-bottom: 4px;">
|
||||
<el-form-item prop="searchValue">
|
||||
<el-input
|
||||
v-model="queryParams.searchValue"
|
||||
placeholder="登录名称 / 用户名称"
|
||||
clearable
|
||||
prefix-icon="el-icon-search"
|
||||
style="width: 220px;"
|
||||
@keyup.enter.native="handleQuery"
|
||||
@clear="handleQuery"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-button type="primary" icon="el-icon-search" size="mini" @click="handleQuery">搜索</el-button>
|
||||
<el-button icon="el-icon-refresh" size="mini" @click="resetQuery">重置</el-button>
|
||||
</el-form-item>
|
||||
<el-form-item style="float: right; margin-right: 0;">
|
||||
<span class="read-stat">
|
||||
共 <strong>{{ total }}</strong> 人已读
|
||||
</span>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<el-table v-loading="loading" :data="userList" size="small" stripe height="340px">
|
||||
<el-table-column type="index" label="序号" width="55" align="center" />
|
||||
<el-table-column label="登录名称" prop="userName" align="center" :show-overflow-tooltip="true" />
|
||||
<el-table-column label="用户名称" prop="nickName" align="center" :show-overflow-tooltip="true" />
|
||||
<el-table-column label="所属部门" prop="deptName" align="center" :show-overflow-tooltip="true" />
|
||||
<el-table-column label="手机号码" prop="phonenumber" align="center" width="120" />
|
||||
<el-table-column label="阅读时间" prop="readTime" align="center" width="160">
|
||||
<template slot-scope="scope">
|
||||
<span>{{ parseTime(scope.row.readTime) }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
<pagination v-show="total > 0" :total="total" :page.sync="queryParams.pageNum" :limit.sync="queryParams.pageSize" @pagination="getList" style="padding: 6px 0px;"/>
|
||||
</el-dialog>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { listNoticeReadUsers } from "@/api/system/notice"
|
||||
|
||||
export default {
|
||||
name: "ReadUsers",
|
||||
data() {
|
||||
return {
|
||||
visible: false,
|
||||
loading: false,
|
||||
noticeId: undefined,
|
||||
noticeTitle: "",
|
||||
total: 0,
|
||||
userList: [],
|
||||
queryParams: {
|
||||
pageNum: 1,
|
||||
pageSize: 10,
|
||||
noticeId: undefined,
|
||||
searchValue: undefined
|
||||
}
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
open(row) {
|
||||
this.noticeId = row.noticeId
|
||||
this.noticeTitle = row.noticeTitle
|
||||
this.queryParams.noticeId = row.noticeId
|
||||
this.queryParams.searchValue = undefined
|
||||
this.queryParams.pageNum = 1
|
||||
this.visible = true
|
||||
this.getList()
|
||||
},
|
||||
getList() {
|
||||
this.loading = true
|
||||
listNoticeReadUsers(this.queryParams).then(res => {
|
||||
this.userList = res.rows
|
||||
this.total = res.total
|
||||
}).finally(() => {
|
||||
this.loading = false
|
||||
})
|
||||
},
|
||||
handleQuery() {
|
||||
this.queryParams.pageNum = 1
|
||||
this.getList()
|
||||
},
|
||||
resetQuery() {
|
||||
this.resetForm("queryForm")
|
||||
this.handleQuery()
|
||||
},
|
||||
handleClose() {
|
||||
this.userList = []
|
||||
this.total = 0
|
||||
this.queryParams.searchValue = undefined
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.read-stat {
|
||||
font-size: 13px;
|
||||
color: #606266;
|
||||
line-height: 28px;
|
||||
}
|
||||
.read-stat strong {
|
||||
color: #409eff;
|
||||
font-size: 15px;
|
||||
margin: 0 2px;
|
||||
}
|
||||
</style>
|
||||
@@ -72,11 +72,12 @@
|
||||
<el-table v-loading="loading" :data="noticeList" @selection-change="handleSelectionChange">
|
||||
<el-table-column type="selection" width="55" align="center" />
|
||||
<el-table-column label="序号" align="center" prop="noticeId" width="100" />
|
||||
<el-table-column label="公告标题" align="center" :show-overflow-tooltip="true">
|
||||
<template slot-scope="scope">
|
||||
<a class="link-type" style="cursor:pointer" @click="handleViewData(scope.row)">{{ scope.row.noticeTitle }}</a>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column
|
||||
label="公告标题"
|
||||
align="center"
|
||||
prop="noticeTitle"
|
||||
:show-overflow-tooltip="true"
|
||||
/>
|
||||
<el-table-column label="公告类型" align="center" prop="noticeType" width="100">
|
||||
<template slot-scope="scope">
|
||||
<dict-tag :options="dict.type.sys_notice_type" :value="scope.row.noticeType"/>
|
||||
@@ -95,13 +96,6 @@
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" align="center" class-name="small-padding fixed-width">
|
||||
<template slot-scope="scope">
|
||||
<el-button
|
||||
size="mini"
|
||||
type="text"
|
||||
icon="el-icon-user"
|
||||
@click="handleReadUsers(scope.row)"
|
||||
v-hasPermi="['system:notice:list']"
|
||||
>阅读用户</el-button>
|
||||
<el-button
|
||||
size="mini"
|
||||
type="text"
|
||||
@@ -172,20 +166,14 @@
|
||||
<el-button @click="cancel">取 消</el-button>
|
||||
</div>
|
||||
</el-dialog>
|
||||
|
||||
<notice-detail-view ref="noticeViewRef" />
|
||||
<read-users-dialog ref="readUsersRef" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import NoticeDetailView from "@/layout/components/HeaderNotice/DetailView"
|
||||
import ReadUsersDialog from "./ReadUsers"
|
||||
import { listNotice, getNotice, delNotice, addNotice, updateNotice } from "@/api/system/notice"
|
||||
|
||||
export default {
|
||||
name: "Notice",
|
||||
components: { NoticeDetailView, ReadUsersDialog },
|
||||
dicts: ['sys_notice_status', 'sys_notice_type'],
|
||||
data() {
|
||||
return {
|
||||
@@ -270,7 +258,7 @@ export default {
|
||||
// 多选框选中数据
|
||||
handleSelectionChange(selection) {
|
||||
this.ids = selection.map(item => item.noticeId)
|
||||
this.single = selection.length != 1
|
||||
this.single = selection.length!=1
|
||||
this.multiple = !selection.length
|
||||
},
|
||||
/** 新增按钮操作 */
|
||||
@@ -290,17 +278,17 @@ export default {
|
||||
})
|
||||
},
|
||||
/** 提交按钮 */
|
||||
submitForm() {
|
||||
submitForm: function() {
|
||||
this.$refs["form"].validate(valid => {
|
||||
if (valid) {
|
||||
if (this.form.noticeId != undefined) {
|
||||
updateNotice(this.form).then(() => {
|
||||
updateNotice(this.form).then(response => {
|
||||
this.$modal.msgSuccess("修改成功")
|
||||
this.open = false
|
||||
this.getList()
|
||||
})
|
||||
} else {
|
||||
addNotice(this.form).then(() => {
|
||||
addNotice(this.form).then(response => {
|
||||
this.$modal.msgSuccess("新增成功")
|
||||
this.open = false
|
||||
this.getList()
|
||||
@@ -309,14 +297,6 @@ export default {
|
||||
}
|
||||
})
|
||||
},
|
||||
/** 查看公告详情 */
|
||||
handleViewData(row) {
|
||||
this.$refs.noticeViewRef.open(row)
|
||||
},
|
||||
/** 查看已读用户 */
|
||||
handleReadUsers(row) {
|
||||
this.$refs.readUsersRef.open(row)
|
||||
},
|
||||
/** 删除按钮操作 */
|
||||
handleDelete(row) {
|
||||
const noticeIds = row.noticeId || this.ids
|
||||
|
||||
@@ -249,7 +249,7 @@ export default {
|
||||
// 多选框选中数据
|
||||
handleSelectionChange(selection) {
|
||||
this.ids = selection.map(item => item.postId)
|
||||
this.single = selection.length != 1
|
||||
this.single = selection.length!=1
|
||||
this.multiple = !selection.length
|
||||
},
|
||||
/** 新增按钮操作 */
|
||||
@@ -269,17 +269,17 @@ export default {
|
||||
})
|
||||
},
|
||||
/** 提交按钮 */
|
||||
submitForm() {
|
||||
submitForm: function() {
|
||||
this.$refs["form"].validate(valid => {
|
||||
if (valid) {
|
||||
if (this.form.postId != undefined) {
|
||||
updatePost(this.form).then(() => {
|
||||
updatePost(this.form).then(response => {
|
||||
this.$modal.msgSuccess("修改成功")
|
||||
this.open = false
|
||||
this.getList()
|
||||
})
|
||||
} else {
|
||||
addPost(this.form).then(() => {
|
||||
addPost(this.form).then(response => {
|
||||
this.$modal.msgSuccess("新增成功")
|
||||
this.open = false
|
||||
this.getList()
|
||||
|
||||
@@ -184,7 +184,7 @@ export default {
|
||||
}).catch(() => {})
|
||||
},
|
||||
/** 批量取消授权按钮操作 */
|
||||
cancelAuthUserAll() {
|
||||
cancelAuthUserAll(row) {
|
||||
const roleId = this.queryParams.roleId
|
||||
const userIds = this.userIds.join(",")
|
||||
this.$modal.confirm('是否取消选中用户授权数据项?').then(function() {
|
||||
|
||||
@@ -451,7 +451,7 @@ export default {
|
||||
// 多选框选中数据
|
||||
handleSelectionChange(selection) {
|
||||
this.ids = selection.map(item => item.roleId)
|
||||
this.single = selection.length != 1
|
||||
this.single = selection.length!=1
|
||||
this.multiple = !selection.length
|
||||
},
|
||||
// 更多操作触发
|
||||
@@ -547,24 +547,24 @@ export default {
|
||||
this.title = "分配数据权限"
|
||||
},
|
||||
/** 分配用户操作 */
|
||||
handleAuthUser(row) {
|
||||
handleAuthUser: function(row) {
|
||||
const roleId = row.roleId
|
||||
this.$router.push("/system/role-auth/user/" + roleId)
|
||||
},
|
||||
/** 提交按钮 */
|
||||
submitForm() {
|
||||
submitForm: function() {
|
||||
this.$refs["form"].validate(valid => {
|
||||
if (valid) {
|
||||
if (this.form.roleId != undefined) {
|
||||
this.form.menuIds = this.getMenuAllCheckedKeys()
|
||||
updateRole(this.form).then(() => {
|
||||
updateRole(this.form).then(response => {
|
||||
this.$modal.msgSuccess("修改成功")
|
||||
this.open = false
|
||||
this.getList()
|
||||
})
|
||||
} else {
|
||||
this.form.menuIds = this.getMenuAllCheckedKeys()
|
||||
addRole(this.form).then(() => {
|
||||
addRole(this.form).then(response => {
|
||||
this.$modal.msgSuccess("新增成功")
|
||||
this.open = false
|
||||
this.getList()
|
||||
@@ -574,10 +574,10 @@ export default {
|
||||
})
|
||||
},
|
||||
/** 提交按钮(数据权限) */
|
||||
submitDataScope() {
|
||||
submitDataScope: function() {
|
||||
if (this.form.roleId != undefined) {
|
||||
this.form.deptIds = this.getDeptAllCheckedKeys()
|
||||
dataScope(this.form).then(() => {
|
||||
dataScope(this.form).then(response => {
|
||||
this.$modal.msgSuccess("修改成功")
|
||||
this.openDataScope = false
|
||||
this.getList()
|
||||
|
||||
@@ -108,7 +108,7 @@ export default {
|
||||
submitForm() {
|
||||
const userId = this.form.userId
|
||||
const roleIds = this.roleIds.join(",")
|
||||
updateAuthRole({ userId: userId, roleIds: roleIds }).then(() => {
|
||||
updateAuthRole({ userId: userId, roleIds: roleIds }).then((response) => {
|
||||
this.$modal.msgSuccess("授权成功")
|
||||
this.close()
|
||||
})
|
||||
|
||||
@@ -1,11 +1,24 @@
|
||||
<template>
|
||||
<div class="app-container tree-sidebar-manage-wrap">
|
||||
<tree-panel title="组织机构" :tree-data="deptOptions" search-placeholder="请输入部门名称" storage-key="dept-sidebar-width" :defaultExpandAll="true" @node-click="handleNodeClick" @refresh="getDeptTree" ref="deptTreeRef" />
|
||||
<div class="tree-sidebar-content">
|
||||
<div class="content-inner">
|
||||
<div class="app-container">
|
||||
<el-row :gutter="20">
|
||||
<splitpanes :horizontal="this.$store.getters.device === 'mobile'" class="default-theme">
|
||||
<!--部门数据-->
|
||||
<pane size="16">
|
||||
<el-col>
|
||||
<div class="head-container">
|
||||
<el-input v-model="deptName" placeholder="请输入部门名称" clearable size="small" prefix-icon="el-icon-search" style="margin-bottom: 20px" />
|
||||
</div>
|
||||
<div class="head-container">
|
||||
<el-tree :data="deptOptions" :props="defaultProps" :expand-on-click-node="false" :filter-node-method="filterNode" ref="tree" node-key="id" default-expand-all highlight-current @node-click="handleNodeClick" />
|
||||
</div>
|
||||
</el-col>
|
||||
</pane>
|
||||
<!--用户数据-->
|
||||
<pane size="84">
|
||||
<el-col>
|
||||
<el-form :model="queryParams" ref="queryForm" size="small" :inline="true" v-show="showSearch" label-width="68px">
|
||||
<el-form-item label="用户名称" prop="userName">
|
||||
<el-input v-model="queryParams.userName" placeholder="请输入用户名称" clearable style="width: 240px" @keyup.enter.native="handleQuery" />
|
||||
<el-form-item label="柜员号" prop="userName">
|
||||
<el-input v-model="queryParams.userName" placeholder="请输入柜员号" clearable style="width: 240px" @keyup.enter.native="handleQuery" />
|
||||
</el-form-item>
|
||||
<el-form-item label="手机号码" prop="phonenumber">
|
||||
<el-input v-model="queryParams.phonenumber" placeholder="请输入手机号码" clearable style="width: 240px" @keyup.enter.native="handleQuery" />
|
||||
@@ -46,11 +59,7 @@
|
||||
<el-table v-loading="loading" :data="userList" @selection-change="handleSelectionChange">
|
||||
<el-table-column type="selection" width="50" align="center" />
|
||||
<el-table-column label="用户编号" align="center" key="userId" prop="userId" v-if="columns.userId.visible" />
|
||||
<el-table-column label="用户名称" align="center" key="userName" v-if="columns.userName.visible" :show-overflow-tooltip="true">
|
||||
<template slot-scope="scope">
|
||||
<a class="link-type" style="cursor:pointer" @click="handleViewData(scope.row)">{{ scope.row.userName }}</a>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="柜员号" align="center" key="userName" prop="userName" v-if="columns.userName.visible" :show-overflow-tooltip="true" />
|
||||
<el-table-column label="用户昵称" align="center" key="nickName" prop="nickName" v-if="columns.nickName.visible" :show-overflow-tooltip="true" />
|
||||
<el-table-column label="部门" align="center" key="deptName" prop="dept.deptName" v-if="columns.deptName.visible" :show-overflow-tooltip="true" />
|
||||
<el-table-column label="手机号码" align="center" key="phonenumber" prop="phonenumber" v-if="columns.phonenumber.visible" width="120" />
|
||||
@@ -78,9 +87,12 @@
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
|
||||
<pagination v-show="total > 0" :total="total" :page.sync="queryParams.pageNum" :limit.sync="queryParams.pageSize" @pagination="getList" />
|
||||
</div>
|
||||
</div>
|
||||
</el-col>
|
||||
</pane>
|
||||
</splitpanes>
|
||||
</el-row>
|
||||
|
||||
<!-- 添加或修改用户配置对话框 -->
|
||||
<el-dialog :title="title" :visible.sync="open" width="600px" append-to-body>
|
||||
@@ -93,7 +105,7 @@
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<el-form-item label="归属部门" prop="deptId">
|
||||
<treeselect v-model="form.deptId" :options="enabledDeptOptions" :show-count="true" placeholder="请选择归属部门" />
|
||||
<treeselect v-model="form.deptId" :options="enabledDeptOptions" :show-count="true" placeholder="请选择归属部门" @input="handleDeptChange" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
@@ -111,8 +123,8 @@
|
||||
</el-row>
|
||||
<el-row>
|
||||
<el-col :span="12">
|
||||
<el-form-item v-if="form.userId == undefined" label="用户名称" prop="userName">
|
||||
<el-input v-model="form.userName" placeholder="请输入用户名称" maxlength="30" />
|
||||
<el-form-item v-if="form.userId == undefined" label="柜员号" prop="userName">
|
||||
<el-input v-model="form.userName" placeholder="请输入柜员号" maxlength="30" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
@@ -167,25 +179,39 @@
|
||||
</div>
|
||||
</el-dialog>
|
||||
|
||||
<!-- 用户详情抽屉 -->
|
||||
<user-view-drawer ref="userViewRef" />
|
||||
<!-- 用户导入对话框 -->
|
||||
<excel-import-dialog ref="importUserRef" title="用户导入" action="/system/user/importData" template-action="/system/user/importTemplate" template-file-name="user_template" update-support-label="是否更新已经存在的用户数据" @success="getList" />
|
||||
<el-dialog :title="upload.title" :visible.sync="upload.open" width="400px" append-to-body>
|
||||
<el-upload ref="upload" :limit="1" accept=".xlsx, .xls" :headers="upload.headers" :action="upload.url + '?updateSupport=' + upload.updateSupport" :disabled="upload.isUploading" :on-progress="handleFileUploadProgress" :on-success="handleFileSuccess" :auto-upload="false" drag>
|
||||
<i class="el-icon-upload"></i>
|
||||
<div class="el-upload__text">将文件拖到此处,或<em>点击上传</em></div>
|
||||
<div class="el-upload__tip text-center" slot="tip">
|
||||
<div class="el-upload__tip" slot="tip">
|
||||
<el-checkbox v-model="upload.updateSupport" />是否更新已经存在的用户数据
|
||||
</div>
|
||||
<span>仅允许导入xls、xlsx格式文件。</span>
|
||||
<el-link type="primary" :underline="false" style="font-size: 12px; vertical-align: baseline" @click="importTemplate">下载模板</el-link>
|
||||
</div>
|
||||
</el-upload>
|
||||
<div slot="footer" class="dialog-footer">
|
||||
<el-button type="primary" @click="submitFileForm">确 定</el-button>
|
||||
<el-button @click="upload.open = false">取 消</el-button>
|
||||
</div>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { listUser, getUser, delUser, addUser, updateUser, resetUserPwd, changeUserStatus, deptTreeSelect } from "@/api/system/user"
|
||||
import { getToken } from "@/utils/auth"
|
||||
import Treeselect from "@riophae/vue-treeselect"
|
||||
import "@riophae/vue-treeselect/dist/vue-treeselect.css"
|
||||
import TreePanel from "@/components/TreePanel"
|
||||
import ExcelImportDialog from "@/components/ExcelImportDialog"
|
||||
import UserViewDrawer from "./view"
|
||||
import { Splitpanes, Pane } from "splitpanes"
|
||||
import "splitpanes/dist/splitpanes.css"
|
||||
|
||||
export default {
|
||||
name: "User",
|
||||
dicts: ['sys_normal_disable', 'sys_user_sex'],
|
||||
components: { Treeselect, TreePanel, ExcelImportDialog, UserViewDrawer },
|
||||
components: { Treeselect, Splitpanes, Pane },
|
||||
data() {
|
||||
return {
|
||||
// 遮罩层
|
||||
@@ -210,6 +236,8 @@ export default {
|
||||
enabledDeptOptions: undefined,
|
||||
// 是否显示弹出层
|
||||
open: false,
|
||||
// 部门名称
|
||||
deptName: undefined,
|
||||
// 默认密码
|
||||
initPassword: undefined,
|
||||
// 日期范围
|
||||
@@ -220,6 +248,25 @@ export default {
|
||||
roleOptions: [],
|
||||
// 表单参数
|
||||
form: {},
|
||||
defaultProps: {
|
||||
children: "children",
|
||||
label: "label"
|
||||
},
|
||||
// 用户导入参数
|
||||
upload: {
|
||||
// 是否显示弹出层(用户导入)
|
||||
open: false,
|
||||
// 弹出层标题(用户导入)
|
||||
title: "",
|
||||
// 是否禁用上传
|
||||
isUploading: false,
|
||||
// 是否更新已经存在的用户数据
|
||||
updateSupport: 0,
|
||||
// 设置上传的请求头部
|
||||
headers: { Authorization: "Bearer " + getToken() },
|
||||
// 上传的地址
|
||||
url: process.env.VUE_APP_BASE_API + "/system/user/importData"
|
||||
},
|
||||
// 查询参数
|
||||
queryParams: {
|
||||
pageNum: 1,
|
||||
@@ -232,7 +279,7 @@ export default {
|
||||
// 列信息
|
||||
columns: {
|
||||
userId: { label: '用户编号', visible: true },
|
||||
userName: { label: '用户名称', visible: true },
|
||||
userName: { label: '柜员号', visible: true },
|
||||
nickName: { label: '用户昵称', visible: true },
|
||||
deptName: { label: '部门', visible: true },
|
||||
phonenumber: { label: '手机号码', visible: true },
|
||||
@@ -242,12 +289,15 @@ export default {
|
||||
// 表单校验
|
||||
rules: {
|
||||
userName: [
|
||||
{ required: true, message: "用户名称不能为空", trigger: "blur" },
|
||||
{ min: 2, max: 20, message: '用户名称长度必须介于 2 和 20 之间', trigger: 'blur' }
|
||||
{ required: true, message: "柜员号不能为空", trigger: "blur" },
|
||||
{ min: 2, max: 20, message: '柜员号长度必须介于 2 和 20 之间', trigger: 'blur' }
|
||||
],
|
||||
nickName: [
|
||||
{ required: true, message: "用户昵称不能为空", trigger: "blur" }
|
||||
],
|
||||
deptId: [
|
||||
{ required: true, message: "归属部门不能为空", trigger: "change" }
|
||||
],
|
||||
password: [
|
||||
{ required: true, message: "用户密码不能为空", trigger: "blur" },
|
||||
{ min: 5, max: 20, message: '用户密码长度必须介于 5 和 20 之间', trigger: 'blur' },
|
||||
@@ -270,6 +320,12 @@ export default {
|
||||
}
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
// 根据名称筛选部门树
|
||||
deptName(val) {
|
||||
this.$refs.tree.filter(val)
|
||||
}
|
||||
},
|
||||
created() {
|
||||
this.getList()
|
||||
this.getDeptTree()
|
||||
@@ -285,7 +341,8 @@ export default {
|
||||
this.userList = response.rows
|
||||
this.total = response.total
|
||||
this.loading = false
|
||||
})
|
||||
}
|
||||
)
|
||||
},
|
||||
/** 查询部门下拉树结构 */
|
||||
getDeptTree() {
|
||||
@@ -306,11 +363,20 @@ export default {
|
||||
return true
|
||||
})
|
||||
},
|
||||
// 筛选节点
|
||||
filterNode(value, data) {
|
||||
if (!value) return true
|
||||
return data.label.indexOf(value) !== -1
|
||||
},
|
||||
// 节点单击事件
|
||||
handleNodeClick(data) {
|
||||
this.queryParams.deptId = data.id
|
||||
this.handleQuery()
|
||||
},
|
||||
// 归属部门选择变化事件
|
||||
handleDeptChange() {
|
||||
this.$refs.form.validateField('deptId')
|
||||
},
|
||||
// 用户状态修改
|
||||
handleStatusChange(row) {
|
||||
let text = row.status === "0" ? "启用" : "停用"
|
||||
@@ -355,7 +421,7 @@ export default {
|
||||
this.dateRange = []
|
||||
this.resetForm("queryForm")
|
||||
this.queryParams.deptId = undefined
|
||||
this.$refs.deptTreeRef.setCurrentKey(null)
|
||||
this.$refs.tree.setCurrentKey(null)
|
||||
this.handleQuery()
|
||||
},
|
||||
// 多选框选中数据
|
||||
@@ -417,28 +483,28 @@ export default {
|
||||
}
|
||||
},
|
||||
}).then(({ value }) => {
|
||||
resetUserPwd(row.userId, value).then(() => {
|
||||
resetUserPwd(row.userId, value).then(response => {
|
||||
this.$modal.msgSuccess("修改成功,新密码是:" + value)
|
||||
})
|
||||
}).catch(() => {})
|
||||
},
|
||||
/** 分配角色操作 */
|
||||
handleAuthRole(row) {
|
||||
handleAuthRole: function(row) {
|
||||
const userId = row.userId
|
||||
this.$router.push("/system/user-auth/role/" + userId)
|
||||
},
|
||||
/** 提交按钮 */
|
||||
submitForm() {
|
||||
submitForm: function() {
|
||||
this.$refs["form"].validate(valid => {
|
||||
if (valid) {
|
||||
if (this.form.userId != undefined) {
|
||||
updateUser(this.form).then(() => {
|
||||
updateUser(this.form).then(response => {
|
||||
this.$modal.msgSuccess("修改成功")
|
||||
this.open = false
|
||||
this.getList()
|
||||
})
|
||||
} else {
|
||||
addUser(this.form).then(() => {
|
||||
addUser(this.form).then(response => {
|
||||
this.$modal.msgSuccess("新增成功")
|
||||
this.open = false
|
||||
this.getList()
|
||||
@@ -463,13 +529,36 @@ export default {
|
||||
...this.queryParams
|
||||
}, `user_${new Date().getTime()}.xlsx`)
|
||||
},
|
||||
/** 详情按钮操作 */
|
||||
handleViewData(row) {
|
||||
this.$refs.userViewRef.open(row.userId)
|
||||
},
|
||||
/** 导入按钮操作 */
|
||||
handleImport() {
|
||||
this.$refs.importUserRef.open()
|
||||
this.upload.title = "用户导入"
|
||||
this.upload.open = true
|
||||
},
|
||||
/** 下载模板操作 */
|
||||
importTemplate() {
|
||||
this.download('system/user/importTemplate', {
|
||||
}, `user_template_${new Date().getTime()}.xlsx`)
|
||||
},
|
||||
// 文件上传中处理
|
||||
handleFileUploadProgress(event, file, fileList) {
|
||||
this.upload.isUploading = true
|
||||
},
|
||||
// 文件上传成功处理
|
||||
handleFileSuccess(response, file, fileList) {
|
||||
this.upload.open = false
|
||||
this.upload.isUploading = false
|
||||
this.$refs.upload.clearFiles()
|
||||
this.$alert("<div style='overflow: auto;overflow-x: hidden;max-height: 70vh;padding: 10px 20px 0;'>" + response.msg + "</div>", "导入结果", { dangerouslyUseHTMLString: true })
|
||||
this.getList()
|
||||
},
|
||||
// 提交上传文件
|
||||
submitFileForm() {
|
||||
const file = this.$refs.upload.uploadFiles
|
||||
if (!file || file.length === 0 || !file[0].name.toLowerCase().endsWith('.xls') && !file[0].name.toLowerCase().endsWith('.xlsx')) {
|
||||
this.$modal.msgError("请选择后缀为 “xls”或“xlsx”的文件。")
|
||||
return
|
||||
}
|
||||
this.$refs.upload.submit()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -55,7 +55,7 @@ export default {
|
||||
submit() {
|
||||
this.$refs["form"].validate(valid => {
|
||||
if (valid) {
|
||||
updateUserPwd(this.user.oldPassword, this.user.newPassword).then(() => {
|
||||
updateUserPwd(this.user.oldPassword, this.user.newPassword).then(response => {
|
||||
this.$modal.msgSuccess("修改成功")
|
||||
})
|
||||
}
|
||||
|
||||
@@ -72,7 +72,7 @@ export default {
|
||||
submit() {
|
||||
this.$refs["form"].validate(valid => {
|
||||
if (valid) {
|
||||
updateUserProfile(this.form).then(() => {
|
||||
updateUserProfile(this.form).then(response => {
|
||||
this.$modal.msgSuccess("修改成功")
|
||||
this.user.phonenumber = this.form.phonenumber
|
||||
this.user.email = this.form.email
|
||||
|
||||
@@ -1,177 +0,0 @@
|
||||
<template>
|
||||
<el-drawer title="用户信息详情" :visible.sync="visible" direction="rtl" size="68%" append-to-body :before-close="handleClose" custom-class="detail-drawer">
|
||||
<div v-loading="loading" class="drawer-content">
|
||||
<!-- 基本信息 -->
|
||||
<h4 class="section-header">基本信息</h4>
|
||||
<el-row :gutter="20" class="mb8">
|
||||
<el-col :span="12">
|
||||
<div class="info-item">
|
||||
<label class="info-label">用户名称:</label>
|
||||
<span class="info-value plaintext">{{ info.nickName }}</span>
|
||||
</div>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<div class="info-item">
|
||||
<label class="info-label">归属部门:</label>
|
||||
<span class="info-value plaintext">{{ (info.dept && info.dept.deptName) }}</span>
|
||||
</div>
|
||||
</el-col>
|
||||
</el-row>
|
||||
<el-row :gutter="20" class="mb8">
|
||||
<el-col :span="12">
|
||||
<div class="info-item">
|
||||
<label class="info-label">手机号码:</label>
|
||||
<span class="info-value plaintext">{{ info.phonenumber }}</span>
|
||||
</div>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<div class="info-item">
|
||||
<label class="info-label">邮箱:</label>
|
||||
<span class="info-value plaintext">{{ info.email }}</span>
|
||||
</div>
|
||||
</el-col>
|
||||
</el-row>
|
||||
<el-row :gutter="20" class="mb8">
|
||||
<el-col :span="12">
|
||||
<div class="info-item">
|
||||
<label class="info-label">登录账号:</label>
|
||||
<span class="info-value plaintext">{{ info.userName }}</span>
|
||||
</div>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<div class="info-item">
|
||||
<label class="info-label">用户状态:</label>
|
||||
<span class="info-value plaintext">
|
||||
<el-tag size="small" :type="info.status === '0' ? 'success' : 'danger'">{{ info.status === '0' ? '正常' : '停用' }}</el-tag>
|
||||
</span>
|
||||
</div>
|
||||
</el-col>
|
||||
</el-row>
|
||||
<el-row :gutter="20" class="mb8">
|
||||
<el-col :span="12">
|
||||
<div class="info-item">
|
||||
<label class="info-label">岗位:</label>
|
||||
<span class="info-value plaintext">{{ postNames || '无岗位' }}</span>
|
||||
</div>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<div class="info-item">
|
||||
<label class="info-label">用户性别:</label>
|
||||
<span class="info-value plaintext">{{ sexLabel }}</span>
|
||||
</div>
|
||||
</el-col>
|
||||
</el-row>
|
||||
<el-row :gutter="20" class="mb8">
|
||||
<el-col :span="24">
|
||||
<div class="info-item full-width">
|
||||
<label class="info-label">角色:</label>
|
||||
<span class="info-value plaintext">{{ roleNames || '无角色' }}</span>
|
||||
</div>
|
||||
</el-col>
|
||||
</el-row>
|
||||
<!-- 其他信息 -->
|
||||
<h4 class="section-header">其他信息</h4>
|
||||
<el-row :gutter="20" class="mb8">
|
||||
<el-col :span="12">
|
||||
<div class="info-item">
|
||||
<label class="info-label">创建者:</label>
|
||||
<span class="info-value plaintext">{{ info.createBy }}</span>
|
||||
</div>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<div class="info-item">
|
||||
<label class="info-label">创建时间:</label>
|
||||
<span class="info-value plaintext">{{ info.createTime }}</span>
|
||||
</div>
|
||||
</el-col>
|
||||
</el-row>
|
||||
<el-row :gutter="20" class="mb8">
|
||||
<el-col :span="12">
|
||||
<div class="info-item">
|
||||
<label class="info-label">更新者:</label>
|
||||
<span class="info-value plaintext">{{ info.updateBy }}</span>
|
||||
</div>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<div class="info-item">
|
||||
<label class="info-label">更新时间:</label>
|
||||
<span class="info-value plaintext">{{ info.updateTime }}</span>
|
||||
</div>
|
||||
</el-col>
|
||||
</el-row>
|
||||
<el-row :gutter="20" class="mb8">
|
||||
<el-col :span="12">
|
||||
<div class="info-item">
|
||||
<label class="info-label">最后登录IP:</label>
|
||||
<span class="info-value plaintext">{{ info.loginIp }}</span>
|
||||
</div>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<div class="info-item">
|
||||
<label class="info-label">最后登录时间:</label>
|
||||
<span class="info-value plaintext">{{ info.loginDate }}</span>
|
||||
</div>
|
||||
</el-col>
|
||||
</el-row>
|
||||
<el-row :gutter="20" class="mb8">
|
||||
<el-col :span="24">
|
||||
<div class="info-item full-width">
|
||||
<label class="info-label">备注:</label>
|
||||
<span class="info-value plaintext">{{ info.remark }}</span>
|
||||
</div>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</div>
|
||||
</el-drawer>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { getUser } from '@/api/system/user'
|
||||
|
||||
export default {
|
||||
name: 'UserViewDrawer',
|
||||
dicts: ['sys_user_sex'],
|
||||
data() {
|
||||
return {
|
||||
visible: false,
|
||||
loading: false,
|
||||
info: {},
|
||||
postOptions: [],
|
||||
roleOptions: []
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
sexLabel() {
|
||||
return this.selectDictLabel(this.dict.type.sys_user_sex, this.info.sex) || '-'
|
||||
},
|
||||
postNames() {
|
||||
if (!this.postOptions.length) return ''
|
||||
const ids = this.info.postIds || []
|
||||
return this.postOptions.filter(p => ids.includes(p.postId)).map(p => p.postName).join('、') || ''
|
||||
},
|
||||
roleNames() {
|
||||
if (!this.roleOptions.length) return ''
|
||||
const ids = this.info.roleIds || []
|
||||
return this.roleOptions.filter(r => ids.includes(r.roleId)).map(r => r.roleName).join('、') || ''
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
open(userId) {
|
||||
this.visible = true
|
||||
this.loading = true
|
||||
getUser(userId).then(res => {
|
||||
this.info = res.data || {}
|
||||
this.postOptions = res.posts || []
|
||||
this.roleOptions = res.roles || []
|
||||
this.info.postIds = res.postIds || []
|
||||
this.info.roleIds = res.roleIds || []
|
||||
}).finally(() => {
|
||||
this.loading = false
|
||||
})
|
||||
},
|
||||
handleClose() {
|
||||
this.visible = false
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@@ -32,7 +32,7 @@ export default {
|
||||
this.$modal.msgError("请输入建表语句")
|
||||
return
|
||||
}
|
||||
createTable({ sql: this.content, tplWebType: 'element-ui' }).then(res => {
|
||||
createTable({ sql: this.content }).then(res => {
|
||||
this.$modal.msgSuccess(res.msg)
|
||||
if (res.code === 200) {
|
||||
this.visible = false
|
||||
|
||||
@@ -183,7 +183,6 @@ export default {
|
||||
const genTable = Object.assign({}, basicForm.model, genForm.model)
|
||||
genTable.columns = this.columns
|
||||
genTable.params = {
|
||||
genView: genTable.view ? '1' : '0',
|
||||
treeCode: genTable.treeCode,
|
||||
treeName: genTable.treeName,
|
||||
treeParentCode: genTable.treeParentCode,
|
||||
|
||||
@@ -17,7 +17,6 @@
|
||||
<el-select v-model="info.tplWebType">
|
||||
<el-option label="Vue2 Element UI 模版" value="element-ui" />
|
||||
<el-option label="Vue3 Element Plus 模版" value="element-plus" />
|
||||
<el-option label="Vue3 Element Plus TypeScript 模版" value="element-plus-typescript" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
@@ -69,29 +68,6 @@
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
|
||||
<el-col :span="12">
|
||||
<el-form-item prop="formColNum">
|
||||
<span slot="label">
|
||||
表单布局
|
||||
<el-tooltip content="选择表单的栅格布局方式" placement="top">
|
||||
<i class="el-icon-question"></i>
|
||||
</el-tooltip>
|
||||
</span>
|
||||
<el-select v-model="info.formColNum">
|
||||
<el-option label="单列" :value="1" />
|
||||
<el-option label="双列" :value="2" />
|
||||
<el-option label="三列" :value="3" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
|
||||
<el-col :span="12">
|
||||
<el-form-item prop="genView">
|
||||
<span slot="label">扩展功能</span>
|
||||
<el-checkbox v-model="info.view">生成详情页</el-checkbox>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
|
||||
<el-col :span="12">
|
||||
<el-form-item prop="genType">
|
||||
<span slot="label">
|
||||
|
||||
@@ -107,7 +107,7 @@ export default {
|
||||
this.$modal.msgError("请选择要导入的表")
|
||||
return
|
||||
}
|
||||
importTable({ tables: tableNames, tplWebType: 'element-ui' }).then(res => {
|
||||
importTable({ tables: tableNames }).then(res => {
|
||||
this.$modal.msgSuccess(res.msg)
|
||||
if (res.code === 200) {
|
||||
this.visible = false
|
||||
|
||||
@@ -98,9 +98,9 @@
|
||||
<span>{{(queryParams.pageNum - 1) * queryParams.pageSize + scope.$index + 1}}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="表名称" align="center" prop="tableName" :show-overflow-tooltip="true" width="140" />
|
||||
<el-table-column label="表描述" align="center" prop="tableComment" :show-overflow-tooltip="true" width="140" />
|
||||
<el-table-column label="实体" align="center" prop="className" :show-overflow-tooltip="true" width="140" />
|
||||
<el-table-column label="表名称" align="center" prop="tableName" :show-overflow-tooltip="true" width="120" />
|
||||
<el-table-column label="表描述" align="center" prop="tableComment" :show-overflow-tooltip="true" width="120" />
|
||||
<el-table-column label="实体" align="center" prop="className" :show-overflow-tooltip="true" width="120" />
|
||||
<el-table-column label="创建时间" align="center" prop="createTime" sortable="custom" :sort-orders="['descending', 'ascending']" width="160" />
|
||||
<el-table-column label="更新时间" align="center" prop="updateTime" sortable="custom" :sort-orders="['descending', 'ascending']" width="160" />
|
||||
<el-table-column label="操作" align="center" class-name="small-padding fixed-width">
|
||||
@@ -180,7 +180,6 @@ hljs.registerLanguage("xml", require("highlight.js/lib/languages/xml"))
|
||||
hljs.registerLanguage("html", require("highlight.js/lib/languages/xml"))
|
||||
hljs.registerLanguage("vue", require("highlight.js/lib/languages/xml"))
|
||||
hljs.registerLanguage("javascript", require("highlight.js/lib/languages/javascript"))
|
||||
hljs.registerLanguage("typescript", require("highlight.js/lib/languages/typescript"))
|
||||
hljs.registerLanguage("sql", require("highlight.js/lib/languages/sql"))
|
||||
|
||||
export default {
|
||||
@@ -263,7 +262,7 @@ export default {
|
||||
return
|
||||
}
|
||||
if(row.genType === "1") {
|
||||
genCode(row.tableName).then(() => {
|
||||
genCode(row.tableName).then(response => {
|
||||
this.$modal.msgSuccess("成功生成到自定义路径:" + row.genPath)
|
||||
})
|
||||
} else {
|
||||
|
||||
@@ -9,7 +9,7 @@ const CompressionPlugin = require('compression-webpack-plugin')
|
||||
|
||||
const name = process.env.VUE_APP_TITLE || '若依管理系统' // 网页标题
|
||||
|
||||
const baseUrl = 'http://localhost:8080' // 后端接口
|
||||
const baseUrl = 'http://localhost:63310' // 后端接口
|
||||
|
||||
const port = process.env.port || process.env.npm_config_port || 80 // 端口
|
||||
|
||||
|
||||
Reference in New Issue
Block a user