同步前端代码并提交相关修复

This commit is contained in:
wkc
2026-04-15 15:28:50 +08:00
parent 79c5317414
commit 71c5744b3d
198 changed files with 28811 additions and 19433 deletions

View File

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

View 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`

View 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`

View 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`

View 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`

View 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` 验证后端整体打包。

View 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`,验证后端配置修改后可正常完成打包。

View 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`,验证前端路由配置可以正常通过生产构建。

View 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 定向验证。

View File

@@ -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>
<!-- 依赖声明 -->

View File

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

View File

@@ -81,6 +81,4 @@ spring:
model:
url: http://64.202.32.40:8083/api/service/interface/invokeService/syllcs
security:
password-transfer:
key: "1234567890abcdef"

View File

@@ -81,6 +81,4 @@ spring:
model:
url: http://localhost:63310/rate/pricing/mock/invokeModel
security:
password-transfer:
key: "1234567890abcdef"

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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 未配置");
}
}
}

View File

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

View File

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

View File

@@ -1,5 +1,5 @@
# 页面标题
VUE_APP_TITLE = 若依管理系统
VUE_APP_TITLE = 上虞利率定价系统
# 开发环境配置
ENV = 'development'

View File

@@ -1,5 +1,5 @@
# 页面标题
VUE_APP_TITLE = 若依管理系统
VUE_APP_TITLE = 上虞利率定价系统
# 生产环境配置
ENV = 'production'

View File

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

Binary file not shown.

14195
ruoyi-ui/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

View File

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

View File

@@ -41,15 +41,6 @@ export function getInfo() {
})
}
// 解锁屏幕
export function unlockScreen(password) {
return request({
url: '/unlockscreen',
method: 'post',
data: { password }
})
}
// 退出方法
export function logout() {
return request({

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,5 +1,5 @@
import request from '@/utils/request'
import { parseStrEmpty } from "@/utils/ruoyi"
import { parseStrEmpty } from "@/utils/ruoyi";
import { encryptPasswordFields } from '@/utils/passwordTransfer'
// 查询用户列表

View File

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

View File

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

View File

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

View File

@@ -10,16 +10,31 @@ $panGreen: #30B08F;
// 默认菜单主题风格
$base-menu-color:#bfcbd9;
$base-menu-color-active: #ffffff;
$base-menu-background: #1a1f2e;
$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;

View File

@@ -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>仅允许导入xlsxlsx格式文件</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>

View File

@@ -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>
<div class="menu-path">
{{ item.path }}
</div>
</div>
<svg-icon icon-class="enter" v-show="index === activeIndex"/>
</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>
</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)) {
// 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>

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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,45 +9,15 @@
:to="{ path: tag.path, query: tag.query, fullPath: tag.fullPath }"
tag="span"
class="tags-view-item"
:style="tagActiveStyle(tag)"
: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">
<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>
@@ -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) {
@@ -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>

View File

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

View File

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

View File

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

View File

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

View File

@@ -24,21 +24,11 @@ module.exports = {
*/
tagsView: true,
/**
* 持久化标签页
*/
tagsViewPersist: false,
/**
* 显示页签图标
*/
tagsIcon: false,
/**
* 标签页样式card 卡片默认、chrome 谷歌浏览器风格
*/
tagsViewStyle: 'card',
/**
* 是否固定头部
*/

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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("清理全部缓存成功")
})
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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: [],
// 查询参数
@@ -308,15 +289,6 @@ export default {
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()

View File

@@ -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,33 +56,25 @@
: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"/>
</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">
<template slot-scope="scope">
<el-button
@@ -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() {

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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>仅允许导入xlsxlsx格式文件</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()
}
}
}

View File

@@ -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("修改成功")
})
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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 // 端口